From 11408a652e52e70d6caa01b88e565663b81afeb7 Mon Sep 17 00:00:00 2001 From: echo094 <20028238+echo094@users.noreply.github.com> Date: Tue, 3 Sep 2024 00:00:01 +0800 Subject: [PATCH 01/21] AntiTooling --- src/main.js | 3 ++ src/plugin/jsconfuser.js | 78 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+) create mode 100644 src/plugin/jsconfuser.js diff --git a/src/main.js b/src/main.js index 0c85154..b7f0d1c 100644 --- a/src/main.js +++ b/src/main.js @@ -1,6 +1,7 @@ const fs = require('fs') const PluginCommon = require('./plugin/common.js') const PluginJjencode = require('./plugin/jjencode.js') +const PluginJsconfuser = require('./plugin/jsconfuser.js') const PluginSojson = require('./plugin/sojson.js') const PluginSojsonV7 = require('./plugin/sojsonv7.js') const PluginObfuscator = require('./plugin/obfuscator.js') @@ -40,6 +41,8 @@ if (type === 'sojson') { code = PluginAwsc(sourceCode) } else if (type === 'jjencode') { code = PluginJjencode(sourceCode) +} else if (type === 'jsconfuser') { + code = PluginJsconfuser(sourceCode) } else { code = PluginCommon(sourceCode) } diff --git a/src/plugin/jsconfuser.js b/src/plugin/jsconfuser.js new file mode 100644 index 0000000..2fcdb3c --- /dev/null +++ b/src/plugin/jsconfuser.js @@ -0,0 +1,78 @@ +const { parse } = require('@babel/parser') +const generator = require('@babel/generator').default +const traverse = require('@babel/traverse').default +const t = require('@babel/types') +const ivm = require('isolated-vm') + +const isolate = new ivm.Isolate() +const globalContext = isolate.createContextSync() +function virtualGlobalEval(jsStr) { + return globalContext.evalSync(String(jsStr)) +} + +function deAntiToolingCheckFunc(path) { + if (path.node.params.length) { + return false + } + const body = path.node.body + if (!t.isBlockStatement(body)) { + return false + } + if (body.body.length) { + return false + } + return true +} + +function deAntiToolingExtract(path, func_name) { + let binding = path.scope.getBinding(func_name) + for (let ref of binding.referencePaths) { + if (!ref.parentPath.isCallExpression() || !ref.key === 'callee') { + continue + } + const call = ref.parentPath + if (!call.listKey === 'body') { + continue + } + for (let node of call.node.arguments) { + call.insertBefore(node) + } + call.remove() + } + binding.scope.crawl() + binding = path.scope.getBinding(func_name) + if (binding.references === 0) { + path.remove() + } +} + +const deAntiTooling = { + FunctionDeclaration(path) { + const func_name = path.node.id?.name + if (!func_name) { + return + } + if (!deAntiToolingCheckFunc(path)) { + return + } + console.log(`AntiTooling Func Name: ${func_name}`) + deAntiToolingExtract(path, func_name) + }, +} + +module.exports = function (code) { + let ast + try { + ast = parse(code, { errorRecovery: true }) + } catch (e) { + console.error(`Cannot parse code: ${e.reasonCode}`) + return null + } + // AntiTooling + traverse(ast, deAntiTooling) + code = generator(ast, { + comments: false, + jsescOption: { minimal: true }, + }).code + return code +} From 61f70019685ae81305234654564f76a67a662f5a Mon Sep 17 00:00:00 2001 From: echo094 <20028238+echo094@users.noreply.github.com> Date: Sat, 7 Sep 2024 13:43:19 +0800 Subject: [PATCH 02/21] Minify arrowFunctionName --- src/plugin/jsconfuser.js | 71 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/src/plugin/jsconfuser.js b/src/plugin/jsconfuser.js index 2fcdb3c..9a0bab9 100644 --- a/src/plugin/jsconfuser.js +++ b/src/plugin/jsconfuser.js @@ -60,6 +60,75 @@ const deAntiTooling = { }, } +function checkArrowWrap(path) { + if (path.node?.name !== 'arguments') { + return null + } + if (!path.parentPath.isSpreadElement()) { + return null + } + const call = path.parentPath.parentPath + if (path.parentPath.listKey !== 'arguments' || !call.isCallExpression()) { + return null + } + if (call.key !== 'argument' || !call.parentPath.isReturnStatement()) { + return null + } + const func_name = call.node.callee?.name + if (!func_name) { + return null + } + let wrap = call.getFunctionParent() + if (wrap.key !== 'init') { + return null + } + wrap = wrap.parentPath + const wrap_name = wrap.node.id?.name + wrap = wrap.parentPath + if ( + wrap.listKey !== 'body' || + wrap.key !== 0 || + wrap.container.length !== 2 + ) { + return null + } + const str = generator(wrap.container[1]).code + if (str.indexOf(wrap_name) === -1) { + return null + } + wrap = wrap.getFunctionParent() + const arrow_name = wrap.node?.id?.name + if (!arrow_name || wrap.node.params?.[0]?.name !== func_name) { + return null + } + return { + name: arrow_name, + path: wrap, + } +} + +const deMinifyArrow = { + Identifier(path) { + let obj = checkArrowWrap(path) + if (!obj) { + return + } + console.log(`Find arrowFunctionName: ${obj.name}`) + let binding = obj.path.parentPath.scope.bindings[obj.name] + for (const ref of binding.referencePaths) { + if (ref.key !== 'callee') { + console.warn(`Unexpected ref of arrowFunctionName: ${obj.name}`) + continue + } + const repl_path = ref.parentPath + repl_path.replaceWith(repl_path.node.arguments[0]) + } + binding.scope.crawl() + binding = obj.path.parentPath.scope.bindings[obj.name] + if (!binding.references) [obj.path.remove()] + }, +} + module.exports = function (code) { let ast try { @@ -70,6 +139,8 @@ module.exports = function (code) { } // AntiTooling traverse(ast, deAntiTooling) + // Minify + traverse(ast, deMinifyArrow) code = generator(ast, { comments: false, jsescOption: { minimal: true }, From b1b2f5076bb2a4a0ff4288241631a701a1b30ff5 Mon Sep 17 00:00:00 2001 From: echo094 <20028238+echo094@users.noreply.github.com> Date: Sat, 7 Sep 2024 15:18:25 +0800 Subject: [PATCH 03/21] Stack functionLengthName --- src/plugin/jsconfuser.js | 85 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 84 insertions(+), 1 deletion(-) diff --git a/src/plugin/jsconfuser.js b/src/plugin/jsconfuser.js index 9a0bab9..31d13af 100644 --- a/src/plugin/jsconfuser.js +++ b/src/plugin/jsconfuser.js @@ -10,6 +10,10 @@ function virtualGlobalEval(jsStr) { return globalContext.evalSync(String(jsStr)) } +const collect_id = { + arrowFunc: null, +} + function deAntiToolingCheckFunc(path) { if (path.node.params.length) { return false @@ -107,12 +111,25 @@ function checkArrowWrap(path) { } } +/** + * Template: + * ```javascript + * function arrowFunctionName (arrowFn, functionLength = 0){ + * var functionObject = function(){ return arrowFn(...arguments) }; + * return Object.defineProperty(functionObject, "length", { + * "value": functionLength, + * "configurable": true + * }); + * } + * ``` + */ const deMinifyArrow = { Identifier(path) { let obj = checkArrowWrap(path) if (!obj) { return } + collect_id.arrowFunc = obj.name console.log(`Find arrowFunctionName: ${obj.name}`) let binding = obj.path.parentPath.scope.bindings[obj.name] for (const ref of binding.referencePaths) { @@ -125,7 +142,71 @@ const deMinifyArrow = { } binding.scope.crawl() binding = obj.path.parentPath.scope.bindings[obj.name] - if (!binding.references) [obj.path.remove()] + if (!binding.references) { + obj.path.remove() + } + }, +} + +function checkFuncLen(path) { + if (path.node?.name !== 'configurable' || path.key !== 'key') { + return null + } + const prop = path.parentPath + if (!prop.isObjectProperty() || prop.key !== 1) { + return null + } + const obj = prop.parentPath + if (obj.node.properties.length !== 2) { + return null + } + if (obj.node.properties[0]?.key?.name !== 'value') { + return null + } + if (obj.listKey !== 'arguments' || obj.key !== 2) { + return null + } + const func_name = obj.container[0]?.name + const warp = obj.getFunctionParent() + if (warp.node.params?.[0]?.name !== func_name) { + return null + } + const func_len_name = warp.node?.id?.name + if (!func_len_name || func_len_name === collect_id.arrowFunc) { + return null + } + return { + name: func_len_name, + path: warp, + } +} + +const deStackFuncLen = { + Identifier(path) { + let obj = checkFuncLen(path) + if (!obj) { + return + } + console.log(`Find functionLengthName: ${obj.name}`) + let binding = obj.path.parentPath.scope.bindings[obj.name] + for (const ref of binding.referencePaths) { + if (ref.key !== 'callee') { + console.warn(`Unexpected ref of functionLengthName: ${obj.name}`) + continue + } + const repl_path = ref.parentPath + const arg = repl_path.node.arguments[0] + if (t.isIdentifier(arg)) { + repl_path.remove() + } else { + repl_path.replaceWith(arg) + } + } + binding.scope.crawl() + binding = obj.path.parentPath.scope.bindings[obj.name] + if (!binding.references) { + obj.path.remove() + } }, } @@ -141,6 +222,8 @@ module.exports = function (code) { traverse(ast, deAntiTooling) // Minify traverse(ast, deMinifyArrow) + // Stack + traverse(ast, deStackFuncLen) code = generator(ast, { comments: false, jsescOption: { minimal: true }, From 8f5c6fa25ba2fa6d28918fb41589a21c0df37e61 Mon Sep 17 00:00:00 2001 From: echo094 <20028238+echo094@users.noreply.github.com> Date: Sat, 7 Sep 2024 17:05:06 +0800 Subject: [PATCH 04/21] DuplicateLiteralsRemoval --- src/plugin/jsconfuser.js | 120 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 116 insertions(+), 4 deletions(-) diff --git a/src/plugin/jsconfuser.js b/src/plugin/jsconfuser.js index 31d13af..8fa1dc6 100644 --- a/src/plugin/jsconfuser.js +++ b/src/plugin/jsconfuser.js @@ -5,10 +5,6 @@ const t = require('@babel/types') const ivm = require('isolated-vm') const isolate = new ivm.Isolate() -const globalContext = isolate.createContextSync() -function virtualGlobalEval(jsStr) { - return globalContext.evalSync(String(jsStr)) -} const collect_id = { arrowFunc: null, @@ -148,6 +144,120 @@ const deMinifyArrow = { }, } +function checkArrayName(path) { + if (path.key !== 'argument') { + return null + } + const ret_path = path.parentPath + if (!ret_path.isReturnStatement() || ret_path.key !== 0) { + return null + } + const array_fn_path = ret_path.getFunctionParent() + const array_fn_name = array_fn_path.node.id?.name + if (!array_fn_name) { + return null + } + const binding = array_fn_path.parentPath.scope.bindings[array_fn_name] + if (binding.references !== 1) { + return null + } + let ref = binding.referencePaths[0] + while (ref && !ref.isAssignmentExpression()) { + ref = ref.parentPath + } + if (!ref) { + return null + } + const array_name = ref.node.left?.name + if (!array_name) { + return null + } + return { + func_name: array_fn_name, + func_path: array_fn_path, + array_name: array_name, + array_path: ref, + } +} + +function parseArrayWarp(vm, path) { + let func = path.getFunctionParent(path) + let name = null + let binding = null + if (func.isArrowFunctionExpression()) { + func = func.parentPath + name = func.node.id.name + binding = func.scope.getBinding(name) + } else { + name = func.node.id.name + binding = func.parentPath.scope.getBinding(name) + } + console.log(`Process array warp function: ${name}`) + vm.evalSync(generator(func.node).code) + for (const ref of binding.referencePaths) { + const call = ref.parentPath + if (ref.key !== 'callee') { + console.warn(`Unexpected ref of array warp function: ${call}`) + continue + } + const ret = vm.evalSync(generator(call.node).code) + if (typeof ret === 'string') { + call.replaceWith(t.stringLiteral(ret)) + } else { + call.replaceWithSourceString(ret) + } + } + binding.scope.crawl() + binding = binding.scope.getBinding(name) + if (!binding.references) { + func.remove() + } +} + +/** + * Template: + * ```javascript + * var arrayName = getArrayFn() + * function getArrayFn (){ + * return [...arrayExpression] + * } + * ``` + */ +const deDuplicateLiteral = { + ArrayExpression(path) { + let obj = checkArrayName(path) + if (!obj) { + return + } + console.log(`Find arrayName: ${obj.array_name}`) + let decl_node = t.variableDeclarator( + obj.array_path.node.left, + obj.array_path.node.right + ) + decl_node = t.variableDeclaration('var', [decl_node]) + const code = [generator(obj.func_path.node).code, generator(decl_node).code] + let binding = obj.array_path.scope.getBinding(obj.array_name) + for (const ref of binding.referencePaths) { + const vm = isolate.createContextSync() + vm.evalSync(code[0]) + vm.evalSync(code[1]) + parseArrayWarp(vm, ref) + } + binding.scope.crawl() + binding = binding.scope.bindings[obj.array_name] + if (!binding.references) { + obj.array_path.remove() + binding.path.remove() + } + binding = obj.func_path.parentPath.scope.getBinding(obj.func_name) + binding.scope.crawl() + binding = binding.scope.getBinding(obj.func_name) + if (!binding.references) { + obj.func_path.remove() + } + }, +} + function checkFuncLen(path) { if (path.node?.name !== 'configurable' || path.key !== 'key') { return null @@ -222,6 +332,8 @@ module.exports = function (code) { traverse(ast, deAntiTooling) // Minify traverse(ast, deMinifyArrow) + // DuplicateLiteralsRemoval + traverse(ast, deDuplicateLiteral) // Stack traverse(ast, deStackFuncLen) code = generator(ast, { From 6ae5e14696f74754dc1340c61e8c7310bbfa36aa Mon Sep 17 00:00:00 2001 From: echo094 <20028238+echo094@users.noreply.github.com> Date: Sat, 7 Sep 2024 21:59:07 +0800 Subject: [PATCH 05/21] StringCompression --- src/plugin/jsconfuser.js | 205 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 199 insertions(+), 6 deletions(-) diff --git a/src/plugin/jsconfuser.js b/src/plugin/jsconfuser.js index 8fa1dc6..e311262 100644 --- a/src/plugin/jsconfuser.js +++ b/src/plugin/jsconfuser.js @@ -10,6 +10,18 @@ const collect_id = { arrowFunc: null, } +function safeReplace(path, value) { + if (typeof value === 'string') { + path.replaceWith(t.stringLiteral(value)) + return + } + if (typeof value === 'number') { + path.replaceWith(t.numericLiteral(value)) + return + } + path.replaceWithSourceString(value) +} + function deAntiToolingCheckFunc(path) { if (path.node.params.length) { return false @@ -200,12 +212,8 @@ function parseArrayWarp(vm, path) { console.warn(`Unexpected ref of array warp function: ${call}`) continue } - const ret = vm.evalSync(generator(call.node).code) - if (typeof ret === 'string') { - call.replaceWith(t.stringLiteral(ret)) - } else { - call.replaceWithSourceString(ret) - } + const value = vm.evalSync(generator(call.node).code) + safeReplace(call, value) } binding.scope.crawl() binding = binding.scope.getBinding(name) @@ -320,6 +328,189 @@ const deStackFuncLen = { }, } +function checkPattern(code, pattern) { + let i = 0 + let j = 0 + while (i < code.length && j < pattern.length) { + if (code[i] == pattern[j]) { + ++j + } + ++i + } + return j == pattern.length +} + +function findStringDecoder(path) { + if (path.node?.name !== 'charCodeAt' || path.key !== 'property') { + return null + } + let loop = path + while (loop && !loop.isForStatement()) { + loop = loop.parentPath + } + const i = loop?.node?.update?.argument?.name + if (!i) { + return null + } + const func = loop.getFunctionParent() + const param = func.node.params?.[0]?.name + if (!param) { + return null + } + const code = generator(func.node).code + const template = + `function(${param}){var=${param}.split()for(${i}=1;${i}<.length;${i}++)` + + `[${i}].charCodeAt(0)[${i}].push().charAt(0)return.join().split()}` + if (!checkPattern(code, template)) { + return null + } + return { + name: func.node.id.name, + path: func, + } +} + +function findStringGet(path) { + const decoder_name = path.node.id.name + let binding = path.parentPath.scope.getBinding(decoder_name) + if (!binding || binding.references !== 1) { + return null + } + const ref = binding.referencePaths[0] + if (ref.key !== 1 || ref.listKey !== 'arguments') { + return null + } + const get_ref_path = ref.parentPath.get('arguments.0') + const get_name = get_ref_path.node?.name + if (!get_name) { + return null + } + binding = get_ref_path.scope.getBinding(get_name) + return { + name: get_name, + path: binding.path, + ref: get_ref_path, + } +} + +function findStringSplit(path) { + while (path && !path.isAssignmentExpression()) { + path = path.parentPath + } + const split_name = path?.node?.left?.name + if (!split_name) { + return null + } + const binding = path.scope.getBinding(split_name) + return { + name: split_name, + path: path, + def: binding.path, + } +} + +function findStringFn(path, name) { + const binding = path.scope.getBinding(name) + const ref = binding.referencePaths?.[0] + if (!ref) { + return null + } + const fn_path = ref.getFunctionParent(name) + const fn_name = fn_path.node.id.name + return { + name: fn_name, + path: fn_path, + } +} + +/** + * Template: + * ```javascript + * var split = (function (getStringParamName, decoderParamName) { + * return decoderParamName(getStringParamName()) + * })(getStringName, decoder) + * function getStringName () { + * var str = splits[0] + * var objectToTest = {} + * if ('testingFor' in objectToTest) { + * str += splits[1] + * } + * return str + * } + * function decoder (b) { + * // DecodeTemplate + * } + * function fnName (index) { + * return split[index] + * } + * ``` + */ +const deStringCompression = { + Identifier(path) { + const decoder_obj = findStringDecoder(path) + if (!decoder_obj) { + return + } + const get_obj = findStringGet(decoder_obj.path) + if (!get_obj) { + return + } + const split_obj = findStringSplit(get_obj.ref) + if (!get_obj) { + return + } + const fn_obj = findStringFn(split_obj.path, split_obj.name) + if (!get_obj) { + return + } + console.log(`Find stringCompression Fn: ${fn_obj.name}`) + const vm = isolate.createContextSync() + vm.evalSync(generator(decoder_obj.path.node).code) + vm.evalSync(generator(get_obj.path.node).code) + vm.evalSync('var ' + generator(split_obj.path.node).code) + vm.evalSync(generator(fn_obj.path.node).code) + let binding = fn_obj.path.parentPath.scope.getBinding(fn_obj.name) + for (const ref of binding.referencePaths) { + if (ref.key !== 'callee') { + console.warn( + `Unexpected ref of stringCompression Fn: ${ref.parentPath}` + ) + continue + } + const repl_path = ref.parentPath + try { + const value = vm.evalSync(generator(repl_path.node).code) + safeReplace(repl_path, value) + } catch (e) { + console.warn( + `Unexpected ref of stringCompression Fn: ${ref.parentPath}` + ) + } + } + binding.scope.crawl() + binding = binding.scope.bindings[fn_obj.name] + if (!binding.references) { + fn_obj.path.remove() + } + binding.scope.crawl() + binding = split_obj.path.scope.getBinding(split_obj.name) + if (!binding.references) { + split_obj.path.remove() + split_obj.def.remove() + } + binding.scope.crawl() + binding = get_obj.path.scope.getBinding(get_obj.name) + if (!binding.references) { + get_obj.path.remove() + } + binding.scope.crawl() + binding = decoder_obj.path.scope.getBinding(decoder_obj.name) + if (!binding.references) { + decoder_obj.path.remove() + } + }, +} + module.exports = function (code) { let ast try { @@ -336,6 +527,8 @@ module.exports = function (code) { traverse(ast, deDuplicateLiteral) // Stack traverse(ast, deStackFuncLen) + // StringCompression + traverse(ast, deStringCompression) code = generator(ast, { comments: false, jsescOption: { minimal: true }, From 8cb190ab53b5f95b8c55a6eef7b9a0282f8c5b98 Mon Sep 17 00:00:00 2001 From: echo094 <20028238+echo094@users.noreply.github.com> Date: Sun, 8 Sep 2024 14:51:15 +0800 Subject: [PATCH 06/21] Stack update --- src/plugin/jsconfuser.js | 195 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 195 insertions(+) diff --git a/src/plugin/jsconfuser.js b/src/plugin/jsconfuser.js index e311262..c24dc57 100644 --- a/src/plugin/jsconfuser.js +++ b/src/plugin/jsconfuser.js @@ -3,6 +3,7 @@ const generator = require('@babel/generator').default const traverse = require('@babel/traverse').default const t = require('@babel/types') const ivm = require('isolated-vm') +const calculateConstantExp = require('../visitor/calculate-constant-exp') const isolate = new ivm.Isolate() @@ -22,6 +23,16 @@ function safeReplace(path, value) { path.replaceWithSourceString(value) } +function safeGetName(path) { + if (path.isIdentifier()) { + return path.node.name + } + if (path.isLiteral()) { + return path.node.value + } + return null +} + function deAntiToolingCheckFunc(path) { if (path.node.params.length) { return false @@ -299,6 +310,185 @@ function checkFuncLen(path) { } } +/** + * type: param, value, ref, invalid + */ +function initStackCache(len) { + const cache = {} + for (let i = 0; i < len; ++i) { + cache[i] = { + type: 'param', + } + } + return cache +} + +function processAssignLeft(vm, cache, path, prop_name, stk_name) { + const father = path.parentPath + const right = father.get('right') + if (right.isBinaryExpression()) { + return + } + if (right.isLiteral()) { + vm.evalSync(generator(father.node).code) + cache[prop_name] = { + type: 'value', + value: right.node.value, + } + return + } + if (right.isUnaryExpression() && right.node.operator === '-') { + const value = vm.evalSync(generator(right.node).code) + vm.evalSync(generator(father.node).code) + cache[prop_name] = { + type: 'value', + value: value, + } + return + } + if (right.isMemberExpression() && right.node.object?.name === stk_name) { + const right_prop = right.get('property') + if (right_prop.isBinaryExpression()) { + return + } + let ref = safeGetName(right_prop) + if (!Object.prototype.hasOwnProperty.call(cache, ref)) { + vm.evalSync(generator(father.node).code) + cache[prop_name] = { + type: 'value', + value: undefined, + } + return + } + while (cache[ref].type === 'ref') { + ref = cache[ref].value + } + if (cache[ref].type === 'value') { + safeReplace(right, cache[ref].value) + vm.evalSync(generator(father.node).code) + cache[prop_name] = { + type: 'value', + value: cache[ref].value, + } + } else { + cache[prop_name] = { + type: 'ref', + value: ref, + } + } + return + } + cache[prop_name] = { + type: 'invalid', + } +} + +function processAssignInvalid(cache, path, prop_name) { + cache[prop_name] = { + type: 'invalid', + } +} + +function processReplace(cache, path, prop_name) { + const value = cache[prop_name].value + const type = cache[prop_name].type + if (type === 'ref') { + path.node.computed = true + safeReplace(path.get('property'), value) + return true + } + if (type === 'value') { + safeReplace(path, value) + return true + } + return false +} + +function checkStackInvalid(path) { + const stk_name = path.node.params[0].argument.name + const body_path = path.get('body') + const obj = {} + body_path.traverse({ + MemberExpression: { + exit(path) { + if (path.node.object.name !== stk_name) { + return + } + const father = path.parentPath + if (body_path.scope == father.scope) { + return + } + if (!father.isAssignmentExpression() || path.key !== 'left') { + return + } + const prop = path.get('property') + const prop_name = safeGetName(prop) + obj[prop_name] = 1 + }, + }, + }) + return obj +} + +function tryStackReplace(path, len, invalid) { + const stk_name = path.node.params[0].argument.name + const body_path = path.get('body') + const cache = initStackCache(len) + const vm = isolate.createContextSync() + vm.evalSync(`var ${stk_name} = []`) + let changed = false + body_path.traverse({ + MemberExpression: { + exit(path) { + if (path.node.object.name !== stk_name) { + return + } + const prop = path.get('property') + if (prop.isBinaryExpression()) { + return + } + const prop_name = safeGetName(prop) + if (!prop_name) { + return + } + if (Object.prototype.hasOwnProperty.call(invalid, prop_name)) { + processAssignInvalid(cache, path, prop_name) + return + } + const exist = Object.prototype.hasOwnProperty.call(cache, prop_name) + if (exist && cache[prop_name].type === 'param') { + return + } + const father = path.parentPath + if (father.isAssignmentExpression() && path.key === 'left') { + processAssignLeft(vm, cache, path, prop_name, stk_name) + } else if (exist) { + changed |= processReplace(cache, path, prop_name) + } + }, + }, + }) + const binding = body_path.scope.getBinding(stk_name) + binding.scope.crawl() + return changed +} + +function processStackParam(path, len) { + if (path.isArrowFunctionExpression()) { + console.log(`Process arrowFunctionExpression, len: ${len}`) + } else if (path.isFunctionExpression()) { + console.log(`Process functionExpression, len: ${len}`) + } else { + console.log(`Process Function ${path.node.id.name}, len: ${len}`) + } + let changed = true + const invalid = checkStackInvalid(path) + while (changed) { + changed = tryStackReplace(path, len, invalid) + path.traverse(calculateConstantExp) + } +} + const deStackFuncLen = { Identifier(path) { let obj = checkFuncLen(path) @@ -314,10 +504,15 @@ const deStackFuncLen = { } const repl_path = ref.parentPath const arg = repl_path.node.arguments[0] + const len = repl_path.node.arguments[1].value if (t.isIdentifier(arg)) { + const func_name = arg.name + const func_decl = repl_path.scope.getBinding(func_name).path + processStackParam(func_decl, len) repl_path.remove() } else { repl_path.replaceWith(arg) + processStackParam(repl_path, len) } } binding.scope.crawl() From 465eed67a80b0e632b59029b99e7fcd43641a81e Mon Sep 17 00:00:00 2001 From: echo094 <20028238+echo094@users.noreply.github.com> Date: Sun, 8 Sep 2024 15:25:47 +0800 Subject: [PATCH 07/21] StringConcealing prepare --- src/plugin/jsconfuser.js | 89 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/src/plugin/jsconfuser.js b/src/plugin/jsconfuser.js index c24dc57..4f2d15d 100644 --- a/src/plugin/jsconfuser.js +++ b/src/plugin/jsconfuser.js @@ -706,6 +706,93 @@ const deStringCompression = { }, } +/** + * Template: + * ```javascript + * // GetGlobalTemplate Begin + * function {getGlobalFnName}(){ + * var localVar = false; + * eval(${transform.jsConfuserVar("localVar")} + " = true") + * if (!localVar) { + * {countermeasures} + * } + * const root = eval("this"); + * return root; + * } + * // GetGlobalTemplate End + * // BufferToStringTemplate Begin + * var __globalObject = {getGlobalFnName}() || {}; + * var __TextDecoder = __globalObject["TextDecoder"]; + * var __Uint8Array = __globalObject["Uint8Array"]; + * var __Buffer = __globalObject["Buffer"]; + * var __String = __globalObject["String"] || String; + * var __Array = __globalObject["Array"] || Array; + * var utf8ArrayToStr = (function () { + * // ... + * })(); + * function bufferToStringName () { + * if(typeof __TextDecoder !== "undefined" && __TextDecoder) { + * return new __TextDecoder()["decode"](new __Uint8Array(buffer)); + * } else if(typeof __Buffer !== "undefined" && __Buffer) { + * return __Buffer["from"](buffer)["toString"]("utf-8"); + * } else { + * return utf8ArrayToStr(buffer); + * } + * } + * // BufferToStringTemplate End + * + * var cacheName = [], arrayName = [] + * + * // Below will appear multiple times + * var getterFnName = (x, y, z, a, b)=>{ + * if ( x !== y ) { + * return b[x] || (b[x] = a(arrayName[x])) + * } + * // Add fake ifStatements + * if(typeof a === "undefined") { + * a = decodeFn + * } + * if(typeof b === "undefined") { + * b = cacheName + * } + * } + * // Base91 Algo + * function decodeFn (str){ + * var table = {__strTable__}; + * var raw = "" + (str || ""); + * var len = raw.length; + * var ret = []; + * var b = 0; + * var n = 0; + * var v = -1; + * for (var i = 0; i < len; i++) { + * var p = table.indexOf(raw[i]); + * if (p === -1) continue; + * if (v < 0) { + * v = p; + * } else { + * v += p * 91; + * b |= v << n; + * n += (v & 8191) > 88 ? 13 : 14; + * do { + * ret.push(b & 0xff); + * b >>= 8; + * n -= 8; + * } while (n > 7); + * v = -1; + * } + * } + * if (v > -1) { + * ret.push((b | (v << n)) & 0xff); + * } + * return bufferToStringName(ret); + * } + * ``` + */ +const deStringConcealing = { + ArrayExpression(path) {}, +} + module.exports = function (code) { let ast try { @@ -724,6 +811,8 @@ module.exports = function (code) { traverse(ast, deStackFuncLen) // StringCompression traverse(ast, deStringCompression) + // StringConcealing + traverse(ast, deStringConcealing) code = generator(ast, { comments: false, jsescOption: { minimal: true }, From 862f40c0efb56e547742fea926e7caa0f9c47cee Mon Sep 17 00:00:00 2001 From: echo094 <20028238+echo094@users.noreply.github.com> Date: Sun, 8 Sep 2024 23:47:47 +0800 Subject: [PATCH 08/21] Stack update --- src/plugin/jsconfuser.js | 59 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 56 insertions(+), 3 deletions(-) diff --git a/src/plugin/jsconfuser.js b/src/plugin/jsconfuser.js index 4f2d15d..338b7f4 100644 --- a/src/plugin/jsconfuser.js +++ b/src/plugin/jsconfuser.js @@ -327,6 +327,9 @@ function processAssignLeft(vm, cache, path, prop_name, stk_name) { const father = path.parentPath const right = father.get('right') if (right.isBinaryExpression()) { + cache[prop_name] = { + type: 'invalid', + } return } if (right.isLiteral()) { @@ -353,10 +356,8 @@ function processAssignLeft(vm, cache, path, prop_name, stk_name) { } let ref = safeGetName(right_prop) if (!Object.prototype.hasOwnProperty.call(cache, ref)) { - vm.evalSync(generator(father.node).code) cache[prop_name] = { - type: 'value', - value: undefined, + type: 'invalid', } return } @@ -473,6 +474,43 @@ function tryStackReplace(path, len, invalid) { return changed } +function getStackParamLen(path) { + const stk_name = path.node.params[0].argument.name + const body_path = path.get('body') + let len = 'unknown' + body_path.traverse({ + MemberExpression: { + exit(path) { + if (path.node.object.name !== stk_name) { + return + } + const prop = path.get('property') + if (prop.isBinaryExpression()) { + return + } + const prop_name = safeGetName(prop) + if (!prop_name || prop_name !== 'length') { + return + } + const father = path.parentPath + if (!father.isAssignmentExpression() || path.key !== 'left') { + return + } + const right = father.get('right') + if (right.isBinaryExpression()) { + return + } + if (!right.isLiteral()) { + return + } + len = right.node.value + path.stop() + }, + }, + }) + return len +} + function processStackParam(path, len) { if (path.isArrowFunctionExpression()) { console.log(`Process arrowFunctionExpression, len: ${len}`) @@ -523,6 +561,20 @@ const deStackFuncLen = { }, } +const deStackFuncOther = { + RestElement(path) { + if (path.listKey !== 'params') { + return + } + const func = path.getFunctionParent() + const len = getStackParamLen(func) + if (len === 'unknown') { + return + } + processStackParam(func, len) + }, +} + function checkPattern(code, pattern) { let i = 0 let j = 0 @@ -809,6 +861,7 @@ module.exports = function (code) { traverse(ast, deDuplicateLiteral) // Stack traverse(ast, deStackFuncLen) + traverse(ast, deStackFuncOther) // StringCompression traverse(ast, deStringCompression) // StringConcealing From 40f5fb11d599f285964e572a4238de2c64d0afae Mon Sep 17 00:00:00 2001 From: echo094 <20028238+echo094@users.noreply.github.com> Date: Mon, 9 Sep 2024 23:27:33 +0800 Subject: [PATCH 09/21] StringConcealing main --- src/plugin/jsconfuser.js | 373 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 364 insertions(+), 9 deletions(-) diff --git a/src/plugin/jsconfuser.js b/src/plugin/jsconfuser.js index 338b7f4..2edd78f 100644 --- a/src/plugin/jsconfuser.js +++ b/src/plugin/jsconfuser.js @@ -33,6 +33,34 @@ function safeGetName(path) { return null } +function safeDeleteNode(name, path) { + let binding + if (path.isFunctionDeclaration()) { + binding = path.parentPath.scope.getBinding(name) + } else { + binding = path.scope.getBinding(name) + } + if (!binding) { + return + } + binding.scope.crawl() + binding = binding.scope.getBinding(name) + if (binding.references) { + return + } + for (const item of binding.constantViolations) { + item.remove() + } + const decl = binding.path + if (decl.removed) { + return + } + if (!decl.isVariableDeclarator() && !decl.isFunctionDeclaration()) { + return + } + binding.path.remove() +} + function deAntiToolingCheckFunc(path) { if (path.node.params.length) { return false @@ -758,10 +786,33 @@ const deStringCompression = { }, } +function insertDepItemVar(deps, name, path) { + const binding = path.scope.getBinding(name) + if (binding.path === path) { + deps.push({ + name: name, + path: binding.path, + node: t.variableDeclaration('var', [binding.path.node]), + pos: binding.path.node.start, + }) + return + } + deps.push({ + name: name, + path: path, + pos: path.node.start, + }) + deps.push({ + name: name, + path: binding.path, + node: t.variableDeclaration('var', [binding.path.node]), + pos: binding.path.node.start, + }) +} + /** - * Template: + * GlobalTemplate 1 (currently not support): * ```javascript - * // GetGlobalTemplate Begin * function {getGlobalFnName}(){ * var localVar = false; * eval(${transform.jsConfuserVar("localVar")} + " = true") @@ -771,14 +822,112 @@ const deStringCompression = { * const root = eval("this"); * return root; * } - * // GetGlobalTemplate End - * // BufferToStringTemplate Begin + * ``` + * GlobalTemplate 2: + * ```javascript + * function {getGlobalFnName}(array = [a, b, c, d]){ + * var bestMatch + * var itemsToSearch = [] + * try { + * bestMatch = Object + * itemsToSearch["push"](("")["__proto__"]["constructor"]["name"]) + * } catch(e) { + * } + * // ... + * return bestMatch || this; + * } + * ``` + */ +function findGlobalFn(path) { + const glo_fn_name = path.node.id?.name + if (!glo_fn_name) { + return null + } + let node = path.node.params?.[0] + if ( + !node || + !t.isAssignmentPattern(node) || + !t.isArrayExpression(node.right) || + node.right.elements.length !== 4 + ) { + return null + } + const array_name = node.left.name + const code = generator(path.node.body).code + const template = + 'try{=Objectpush(__proto__constructorname)}catch{}' + + `:for(;<${array_name}length;)try{=${array_name}[]()` + + 'for()if(typeof)continue}catch{}return||this' + if (!checkPattern(code, template)) { + return + } + const deps = [] + const array = path.get('params.0.right') + for (let i = 0; i < 4; ++i) { + const ele_name = safeGetName(array.get(`elements.${i}`)) + const binding = path.scope.getBinding(ele_name) + deps.push({ + name: ele_name, + path: binding.path, + pos: binding.path.node.start, + }) + } + deps.push({ + name: glo_fn_name, + path: path, + pos: path.node.start, + }) + return { + glo_fn_name: glo_fn_name, + glo_fn_path: path, + deps: deps, + } +} + +/** + * Template: + * ```javascript * var __globalObject = {getGlobalFnName}() || {}; * var __TextDecoder = __globalObject["TextDecoder"]; * var __Uint8Array = __globalObject["Uint8Array"]; * var __Buffer = __globalObject["Buffer"]; * var __String = __globalObject["String"] || String; * var __Array = __globalObject["Array"] || Array; + * ``` + */ +function findGlobalFnRef(obj) { + const path = obj.glo_fn_path + const glo_fn_name = obj.glo_fn_name + let binding = path.parentPath.scope.getBinding(glo_fn_name) + let glo_fn_ref = binding.referencePaths[0] + while (!glo_fn_ref.isAssignmentExpression()) { + glo_fn_ref = glo_fn_ref.parentPath + } + const glo_obj_name = glo_fn_ref.node.left.name + obj.glo_obj_name = glo_obj_name + obj.glo_obj_path = glo_fn_ref + obj.glo_obj_ref = {} + insertDepItemVar(obj.deps, glo_obj_name, glo_fn_ref) + binding = glo_fn_ref.scope.getBinding(glo_obj_name) + for (const ref of binding.referencePaths) { + const prop = safeGetName(ref.parentPath.get('property')) + if (!prop) { + continue + } + let root = ref + while (!root.isAssignmentExpression()) { + root = root.parentPath + } + const ref_name = safeGetName(root.get('left')) + obj.glo_obj_ref[prop] = ref_name + insertDepItemVar(obj.deps, ref_name, root) + } + return +} + +/** + * Template: + * ```javascript * var utf8ArrayToStr = (function () { * // ... * })(); @@ -787,14 +936,109 @@ const deStringCompression = { * return new __TextDecoder()["decode"](new __Uint8Array(buffer)); * } else if(typeof __Buffer !== "undefined" && __Buffer) { * return __Buffer["from"](buffer)["toString"]("utf-8"); - * } else { + * } else { * return utf8ArrayToStr(buffer); * } * } - * // BufferToStringTemplate End - * + * ``` + */ +function findBufferToString(obj) { + const path = obj.glo_obj_path + const ref_array = obj.glo_obj_ref['Array'] + let binding = path.scope.getBinding(ref_array) + for (const ref of binding.referencePaths) { + if (ref.key !== 'callee') { + continue + } + let a2s_path = ref.getFunctionParent() + while (!a2s_path.isAssignmentExpression()) { + a2s_path = a2s_path.parentPath + } + obj.a2s_name = safeGetName(a2s_path.get('left')) + obj.a2s_path = a2s_path + insertDepItemVar(obj.deps, obj.a2s_name, obj.a2s_path) + break + } + binding = obj.a2s_path.scope.getBinding(obj.a2s_name) + const b2s_path = binding.referencePaths[0].getFunctionParent() + obj.b2s_name = safeGetName(b2s_path.get('id')) + obj.b2s_path = b2s_path + obj.deps.push({ + name: obj.b2s_name, + path: b2s_path, + pos: b2s_path.node.start, + }) + binding = b2s_path.parentPath.scope.getBinding(obj.b2s_name) + const child = [] + for (const ref of binding.referencePaths) { + const decode_fn = ref.getFunctionParent() + let valid = false + decode_fn.traverse({ + StringLiteral(path) { + if (path.node.value.length === 91) { + valid = true + path.stop() + } + }, + }) + if (!valid) { + return + } + child.push({ + name: decode_fn.node.id.name, + decoder: decode_fn, + }) + } + obj.child = child +} + +function generatorStringConcealingDepCode(obj) { + obj.deps.sort((a, b) => { + return a.pos - b.pos + }) + const dep_node = t.program([]) + for (const item of obj.deps) { + if (item.node) { + dep_node.body.push(item.node) + } else { + dep_node.body.push(item.path.node) + } + } + obj.dep_code = generator(dep_node).code +} + +function renameProperty(member) { + const obj_name = safeGetName(member.get('object')) + const prop_name = safeGetName(member.get('property')) + const new_name = member.scope.generateUidIdentifier(`_tmp_local_`) + const binding = member.scope.getBinding(obj_name) + let first = true + for (const ref of binding.referencePaths) { + const item = ref.parentPath + const prop = safeGetName(item.get('property')) + if (prop !== prop_name) { + continue + } + if (first) { + let body = item + while (body.listKey !== 'body') { + body = body.parentPath + } + body.container.unshift( + t.variableDeclaration('var', [t.variableDeclarator(new_name)]) + ) + body.scope.crawl() + first = false + } + item.replaceWith(new_name) + } + member.scope.crawl() +} + +/** + * Template: + * ```javascript * var cacheName = [], arrayName = [] - * * // Below will appear multiple times * var getterFnName = (x, y, z, a, b)=>{ * if ( x !== y ) { @@ -841,8 +1085,119 @@ const deStringCompression = { * } * ``` */ +function processSingleGetter(obj, decoder_name, decoder_path) { + const decoder_code = generator(decoder_path.node).code + let binding = decoder_path.parentPath.scope.getBinding(decoder_name) + let getter_path = binding.referencePaths[0].getFunctionParent() + while ( + !getter_path.isAssignmentExpression() && + !getter_path.isVariableDeclarator() + ) { + getter_path = getter_path.parentPath + } + let getter_name + if (getter_path.isAssignmentExpression()) { + if (getter_path.get('left').isMemberExpression()) { + renameProperty(getter_path.get('left')) + } + getter_name = safeGetName(getter_path.get('left')) + } else { + getter_name = safeGetName(getter_path.get('id')) + } + console.log( + `[StringConcealing] getter: ${getter_name} decoder: ${decoder_name}` + ) + const getter_code = 'var ' + generator(getter_path.node).code + binding = getter_path.scope.getBinding(getter_name) + if (getter_path.isAssignmentExpression()) { + getter_path.get('right').replaceWith(t.numericLiteral(0)) + } else { + getter_path.get('init').replaceWith(t.numericLiteral(0)) + } + binding.scope.crawl() + binding = getter_path.scope.getBinding(getter_name) + let complete = false + while (!complete) { + complete = true + const vm = isolate.createContextSync() + vm.evalSync(obj.dep_code) + try { + for (const ref of binding.referencePaths) { + if (ref.findParent((path) => path.removed)) { + continue + } + let repl_path = ref.parentPath + if (repl_path.isCallExpression()) { + const args = repl_path.node.arguments + if (args.length !== 1 || !t.isLiteral(args[0])) { + console.warn(`[StringConcealing] Unexpected call: ${repl_path}`) + continue + } + } else if (repl_path.isMemberExpression()) { + repl_path = repl_path.parentPath + } else { + console.warn(`[StringConcealing] Unexpected ref: ${repl_path}`) + continue + } + const eval_code = generator(repl_path.node).code + // The name of getter can be the same as other dep functions + const value = vm.evalSync( + `(function (){${decoder_code}\n${getter_code}\nreturn ${eval_code}})()` + ) + safeReplace(repl_path, value) + } + } catch (e) { + if (e.name !== 'ReferenceError') { + console.warn(`[StringConcealing] Unexpected exception: ${e.message}`) + return + } + complete = false + const lost = e.message.split(' ')[0] + const binding = getter_path.scope.getBinding(lost) + if (!binding) { + console.warn(`[StringConcealing] Missing cache or array: ${lost}`) + return + } + let count = binding.constantViolations.length + if (count) { + console.warn(`[StringConcealing] Invalid violations ${lost} : ${count}`) + return + } + count = binding.path.node.init.elements.length + if (count) { + console.log(`[StringConcealing] Add array : ${lost}`) + obj.array_name = lost + obj.array_path = binding.path + } else { + console.log(`[StringConcealing] Add cache : ${lost}`) + obj.cache_name = lost + obj.cache_path = binding.path + } + insertDepItemVar(obj.deps, lost, binding.path) + generatorStringConcealingDepCode(obj) + } + } + safeDeleteNode(getter_name, getter_path) + safeDeleteNode(decoder_name, decoder_path) +} + const deStringConcealing = { - ArrayExpression(path) {}, + FunctionDeclaration(path) { + const obj = findGlobalFn(path) + if (!obj) { + return + } + findGlobalFnRef(obj) + findBufferToString(obj) + generatorStringConcealingDepCode(obj) + for (const item of obj.child) { + processSingleGetter(obj, item.name, item.decoder) + } + safeDeleteNode(obj.array_name, obj.array_path) + safeDeleteNode(obj.cache_name, obj.cache_path) + safeDeleteNode(obj.b2s_name, obj.b2s_path) + safeDeleteNode(obj.a2s_name, obj.a2s_path) + }, } module.exports = function (code) { From 37170a3fba5f3c5647a0458227c702524b3c8668 Mon Sep 17 00:00:00 2001 From: echo094 <20028238+echo094@users.noreply.github.com> Date: Tue, 10 Sep 2024 00:02:21 +0800 Subject: [PATCH 10/21] Stack update --- src/plugin/jsconfuser.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/plugin/jsconfuser.js b/src/plugin/jsconfuser.js index 2edd78f..f88d868 100644 --- a/src/plugin/jsconfuser.js +++ b/src/plugin/jsconfuser.js @@ -444,14 +444,18 @@ function checkStackInvalid(path) { return } const father = path.parentPath + const prop = path.get('property') + const prop_name = safeGetName(prop) + if (father.isUpdateExpression()) { + obj[prop_name] = 1 + return + } if (body_path.scope == father.scope) { return } if (!father.isAssignmentExpression() || path.key !== 'left') { return } - const prop = path.get('property') - const prop_name = safeGetName(prop) obj[prop_name] = 1 }, }, From 7a606fe30735656253dd505b68c15e3155b14dcb Mon Sep 17 00:00:00 2001 From: echo094 <20028238+echo094@users.noreply.github.com> Date: Tue, 10 Sep 2024 23:44:27 +0800 Subject: [PATCH 11/21] StringSplitting --- src/plugin/jsconfuser.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/plugin/jsconfuser.js b/src/plugin/jsconfuser.js index f88d868..ccb4243 100644 --- a/src/plugin/jsconfuser.js +++ b/src/plugin/jsconfuser.js @@ -1225,6 +1225,8 @@ module.exports = function (code) { traverse(ast, deStringCompression) // StringConcealing traverse(ast, deStringConcealing) + // StringSplitting + traverse(ast, calculateConstantExp) code = generator(ast, { comments: false, jsescOption: { minimal: true }, From d5b5d5a588729c5492a1defff33649642a8a0d46 Mon Sep 17 00:00:00 2001 From: echo094 <20028238+echo094@users.noreply.github.com> Date: Tue, 10 Sep 2024 23:44:50 +0800 Subject: [PATCH 12/21] OpaquePredicates prepare --- src/plugin/jsconfuser.js | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/plugin/jsconfuser.js b/src/plugin/jsconfuser.js index ccb4243..ffbaa73 100644 --- a/src/plugin/jsconfuser.js +++ b/src/plugin/jsconfuser.js @@ -1204,6 +1204,35 @@ const deStringConcealing = { }, } +/** + * Template: + * ```javascript + * // This is defined in the glocal space + * var predicateName = (function () { + * var tempName = { + * prop_array_1: [], + * prop_array: function (paramName = 'length') { + * if (!predicateName[prop_array_1][0]) { + * predicateName[prop_array_1][0].push(rand1) + * } + * return predicateName[prop_array_1][paramName] + * }, + * prop_number: rand2, + * prop_string: rand_str, + * } + * return tempName + * })() + * // Below will appear multiple times + * predicateName[prop_array]() ? test : fake + * predicateName[prop_number] > rand3 ? test : fake + * predicateName[prop_string].charAt(index) == real_char ? test : fake + * predicateName[prop_string].charCodeAt(index) == real_char ? test : fake + * ``` + */ +const deOpaquePredicates = { + MemberExpression(path) {}, +} + module.exports = function (code) { let ast try { @@ -1227,6 +1256,8 @@ module.exports = function (code) { traverse(ast, deStringConcealing) // StringSplitting traverse(ast, calculateConstantExp) + // OpaquePredicates + traverse(ast, deOpaquePredicates) code = generator(ast, { comments: false, jsescOption: { minimal: true }, From 4fb343d3f01163378ae5ddedfcb415c2e45bdecd Mon Sep 17 00:00:00 2001 From: echo094 <20028238+echo094@users.noreply.github.com> Date: Thu, 12 Sep 2024 01:02:06 +0800 Subject: [PATCH 13/21] StringConcealing and OpaquePredicates --- src/plugin/jsconfuser.js | 192 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 188 insertions(+), 4 deletions(-) diff --git a/src/plugin/jsconfuser.js b/src/plugin/jsconfuser.js index ffbaa73..c8c586a 100644 --- a/src/plugin/jsconfuser.js +++ b/src/plugin/jsconfuser.js @@ -4,6 +4,7 @@ const traverse = require('@babel/traverse').default const t = require('@babel/types') const ivm = require('isolated-vm') const calculateConstantExp = require('../visitor/calculate-constant-exp') +const pruneIfBranch = require('../visitor/prune-if-branch') const isolate = new ivm.Isolate() @@ -844,6 +845,9 @@ function insertDepItemVar(deps, name, path) { */ function findGlobalFn(path) { const glo_fn_name = path.node.id?.name + if (path.parentPath.getFunctionParent()) { + return null + } if (!glo_fn_name) { return null } @@ -963,6 +967,9 @@ function findBufferToString(obj) { insertDepItemVar(obj.deps, obj.a2s_name, obj.a2s_path) break } + if (!obj.a2s_name) { + return false + } binding = obj.a2s_path.scope.getBinding(obj.a2s_name) const b2s_path = binding.referencePaths[0].getFunctionParent() obj.b2s_name = safeGetName(b2s_path.get('id')) @@ -986,7 +993,7 @@ function findBufferToString(obj) { }, }) if (!valid) { - return + return false } child.push({ name: decode_fn.node.id.name, @@ -994,6 +1001,7 @@ function findBufferToString(obj) { }) } obj.child = child + return true } function generatorStringConcealingDepCode(obj) { @@ -1192,7 +1200,9 @@ const deStringConcealing = { return } findGlobalFnRef(obj) - findBufferToString(obj) + if (!findBufferToString(obj)) { + return + } generatorStringConcealingDepCode(obj) for (const item of obj.child) { processSingleGetter(obj, item.name, item.decoder) @@ -1204,10 +1214,147 @@ const deStringConcealing = { }, } +function tryStringConcealingPlace(path) { + const parent = path.parentPath + if (!parent.isAssignmentExpression()) { + return + } + const name = safeGetName(parent.get('left')) + let binding = parent.scope.getBinding(name) + if (binding?.constantViolations?.length !== 1) { + return + } + const code = generator(parent.node).code + const vm = isolate.createContextSync() + vm.evalSync('var ' + code) + for (const ref of binding.referencePaths) { + if (ref.key !== 'object') { + continue + } + const test = generator(ref.parent).code + const res = vm.evalSync(test) + safeReplace(ref.parentPath, res) + } + safeDeleteNode(name, parent) +} + +const deStringConcealingPlace = { + ArrayExpression(path) { + let valid = true + if (path.node.elements.length === 0) { + return + } + for (const ele of path.node.elements) { + if (!t.isStringLiteral(ele)) { + valid = false + break + } + } + if (!valid) { + return + } + tryStringConcealingPlace(path) + }, + ObjectExpression(path) { + let valid = true + if (path.node.properties.length === 0) { + return + } + for (const ele of path.node.properties) { + if (!t.isStringLiteral(ele.value)) { + valid = false + break + } + } + if (!valid) { + return + } + tryStringConcealingPlace(path) + }, +} + +function checkOpaqueObject(path) { + const parent = path.parentPath + if (!parent.isAssignmentExpression()) { + return null + } + const tmp_name = safeGetName(parent.get('left')) + const func_path = parent.getFunctionParent() + if ( + !func_path || + func_path.key !== 'callee' || + !func_path.parentPath.isCallExpression() + ) { + return null + } + const func_body = func_path.node.body?.body + if (!func_body || func_body.length < 2) { + return null + } + const last_node = func_body[func_body.length - 1] + if ( + !t.isReturnStatement(last_node) || + last_node.argument?.name !== tmp_name + ) { + return null + } + const root_path = func_path.parentPath.parentPath + if (!root_path.isAssignmentExpression()) { + return null + } + const pred_name = safeGetName(root_path.get('left')) + const obj = { + pred_name: pred_name, + pred_path: root_path, + props: {}, + } + for (const prop of path.node.properties) { + const key = prop.key.name + const value = prop.value + if (t.isNumericLiteral(value)) { + obj.props[key] = { + type: 'number', + } + continue + } + if (t.isStringLiteral(value)) { + obj.props[key] = { + type: 'string', + } + continue + } + if (t.isArrayExpression(value)) { + if (value.elements.length === 0) { + obj.props[key] = { + type: 'array_dep', + } + } + continue + } + if (t.isArrowFunctionExpression(value) || t.isFunctionExpression(value)) { + const param = value.params?.[0]?.left?.name + if (!param) { + continue + } + const code = generator(value).code + const template = + `(${param}=){if(${pred_name}[0])${pred_name}push()` + + `return${pred_name}${param}}` + if (checkPattern(code, template)) { + obj.props[key] = { + type: 'array', + } + } + continue + } + } + return obj +} + /** * Template: * ```javascript - * // This is defined in the glocal space + * // This is defined in the global space * var predicateName = (function () { * var tempName = { * prop_array_1: [], @@ -1230,7 +1377,41 @@ const deStringConcealing = { * ``` */ const deOpaquePredicates = { - MemberExpression(path) {}, + ObjectExpression(path) { + const obj = checkOpaqueObject(path) + if (!obj) { + return + } + console.log(`[OpaquePredicates] predicateName : ${obj.pred_name}`) + const vm = isolate.createContextSync() + const code = generator(obj.pred_path.node).code + vm.evalSync('var ' + code) + obj.pred_path.get('right').replaceWith(t.numericLiteral(0)) + let binding = obj.pred_path.scope.getBinding(obj.pred_name) + binding.scope.crawl() + binding = binding.scope.getBinding(obj.pred_name) + for (const ref of binding.referencePaths) { + if (ref.key !== 'object') { + continue + } + const member = ref.parentPath + const prop = member.get('property') + if (!prop || !Object.prototype.hasOwnProperty.call(obj.props, prop)) { + continue + } + let expr = member + while ( + expr.parentPath.isCallExpression() || + expr.parentPath.isMemberExpression() + ) { + expr = expr.parentPath + } + const test = generator(expr.node).code + const res = vm.evalSync(test) + safeReplace(expr, res) + } + safeDeleteNode(obj.pred_name, obj.pred_path) + }, } module.exports = function (code) { @@ -1254,10 +1435,13 @@ module.exports = function (code) { traverse(ast, deStringCompression) // StringConcealing traverse(ast, deStringConcealing) + traverse(ast, deStringConcealingPlace) // StringSplitting traverse(ast, calculateConstantExp) // OpaquePredicates traverse(ast, deOpaquePredicates) + traverse(ast, calculateConstantExp) + traverse(ast, pruneIfBranch) code = generator(ast, { comments: false, jsescOption: { minimal: true }, From cfd3688d7ee8c9ffc8706363ef972895684de0b0 Mon Sep 17 00:00:00 2001 From: echo094 <20028238+echo094@users.noreply.github.com> Date: Mon, 16 Sep 2024 16:44:03 +0800 Subject: [PATCH 14/21] StringConcealing and GlobalConcealing --- src/plugin/jsconfuser.js | 158 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 150 insertions(+), 8 deletions(-) diff --git a/src/plugin/jsconfuser.js b/src/plugin/jsconfuser.js index c8c586a..7bc90a2 100644 --- a/src/plugin/jsconfuser.js +++ b/src/plugin/jsconfuser.js @@ -42,24 +42,25 @@ function safeDeleteNode(name, path) { binding = path.scope.getBinding(name) } if (!binding) { - return + return false } binding.scope.crawl() binding = binding.scope.getBinding(name) if (binding.references) { - return + return false } for (const item of binding.constantViolations) { item.remove() } const decl = binding.path if (decl.removed) { - return + return true } if (!decl.isVariableDeclarator() && !decl.isFunctionDeclaration()) { - return + return true } binding.path.remove() + return true } function deAntiToolingCheckFunc(path) { @@ -845,9 +846,6 @@ function insertDepItemVar(deps, name, path) { */ function findGlobalFn(path) { const glo_fn_name = path.node.id?.name - if (path.parentPath.getFunctionParent()) { - return null - } if (!glo_fn_name) { return null } @@ -1197,7 +1195,10 @@ const deStringConcealing = { FunctionDeclaration(path) { const obj = findGlobalFn(path) if (!obj) { - return + return null + } + if (obj.glo_fn_path.parentPath.getFunctionParent()) { + return null } findGlobalFnRef(obj) if (!findBufferToString(obj)) { @@ -1239,6 +1240,26 @@ function tryStringConcealingPlace(path) { } const deStringConcealingPlace = { + StringLiteral(path) { + if (path.key !== 'right' || !path.parentPath.isAssignmentExpression()) { + return + } + const name = safeGetName(path.parentPath.get('left')) + if (!name) { + return + } + const binding = path.scope.getBinding(name) + if (binding.constantViolations.length !== 1) { + return + } + for (const ref of binding.referencePaths) { + if (ref.node.start < path.node.start) { + continue + } + ref.replaceWith(path.node) + } + safeDeleteNode(name, path.parentPath) + }, ArrayExpression(path) { let valid = true if (path.node.elements.length === 0) { @@ -1414,6 +1435,125 @@ const deOpaquePredicates = { }, } +function findGlobalVar(glo_name, glo_path) { + let tmp_path = glo_path.parentPath.getFunctionParent() + if ( + !tmp_path || + !tmp_path.parentPath.isMemberExpression() || + !tmp_path.parentPath.parentPath.isCallExpression() + ) { + return null + } + const tmp_body = tmp_path.node.body.body + tmp_path = tmp_path.parentPath.parentPath + const ret_node = tmp_body[tmp_body.length - 1] + if ( + !t.isReturnStatement(ret_node) || + !t.isAssignmentExpression(ret_node.argument) + ) { + return null + } + const code = generator(ret_node.argument.right).code + const template = `${glo_name}call(this)` + if (!checkPattern(code, template)) { + return null + } + const glo_var = ret_node.argument.left.name + const binding = glo_path.scope.getBinding(glo_var) + for (const ref of binding.referencePaths) { + if ( + !ref.parentPath.isMemberExpression() || + !ref.parentPath.parentPath.isReturnStatement() + ) { + continue + } + const func_path = ref.getFunctionParent() + const func_name = func_path.node.id.name + return { + glo_var: glo_var, + tmp_path: tmp_path, + glo_fn_name: func_name, + glo_fn_path: func_path, + } + } + return null +} + +function getGlobalConcealingNames(glo_fn_path) { + const obj = {} + glo_fn_path.traverse({ + SwitchCase(path) { + const key = parseInt(generator(path.node.test).code) + let consequent = path.node.consequent[0] + if (t.isReturnStatement(consequent)) { + obj[key] = consequent.argument.property.value + } else { + if (t.isExpressionStatement(consequent)) { + consequent = consequent.expression + } + obj[key] = consequent.right.left.value + } + }, + }) + return obj +} + +/** + * Hide the global vars found by module GlobalAnalysis + * + * Template: + * ```javascript + * // Add to head: + * var globalVar, tempVar = function () { + * getGlobalVariableFnName = createGetGlobalTemplate() + * return globalVar = getGlobalVariableFnName.call(this) + * }["call"]() + * // Add to foot: + * function globalFn (indexParamName) { + * var returnName + * switch (indexParamName) { + * case state_x: { + * return globalVar[name] + * } + * case state_y: { + * returnName = name || globalVar[name] + * break + * } + * } + * return globalVar[returnName] + * } + * // References: + * // name -> globalFn(state) + * ``` + */ +const deGlobalConcealing = { + FunctionDeclaration(path) { + const glo_obj = findGlobalFn(path) + if (!glo_obj) { + return null + } + const obj = findGlobalVar(glo_obj.glo_fn_name, glo_obj.glo_fn_path) + if (!obj) { + return null + } + console.log(`[GlobalConcealing] globalVar: ${obj.glo_var}`) + const glo_vars = getGlobalConcealingNames(obj.glo_fn_path) + console.log(`[GlobalConcealing] globalFn: ${obj.glo_fn_name}`) + let binding = obj.glo_fn_path.parentPath.scope.getBinding(obj.glo_fn_name) + for (const ref of binding.referencePaths) { + const repl_path = ref.parentPath + if (ref.key !== 'callee' || !repl_path.isCallExpression()) { + continue + } + const key = parseInt(generator(repl_path.node.arguments[0]).code) + repl_path.replaceWith(t.identifier(glo_vars[key])) + } + if (safeDeleteNode(obj.glo_fn_name, obj.glo_fn_path)) { + obj.tmp_path.remove() + } + }, +} + module.exports = function (code) { let ast try { @@ -1442,6 +1582,8 @@ module.exports = function (code) { traverse(ast, deOpaquePredicates) traverse(ast, calculateConstantExp) traverse(ast, pruneIfBranch) + // GlobalConcealing + traverse(ast, deGlobalConcealing) code = generator(ast, { comments: false, jsescOption: { minimal: true }, From fefcebc9cc89cdcd640d748d80ea1f83fd06a42d Mon Sep 17 00:00:00 2001 From: echo094 <20028238+echo094@users.noreply.github.com> Date: Sun, 22 Sep 2024 17:22:12 +0800 Subject: [PATCH 15/21] StringConcealing --- src/plugin/jsconfuser.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/plugin/jsconfuser.js b/src/plugin/jsconfuser.js index 7bc90a2..0874934 100644 --- a/src/plugin/jsconfuser.js +++ b/src/plugin/jsconfuser.js @@ -1210,8 +1210,11 @@ const deStringConcealing = { } safeDeleteNode(obj.array_name, obj.array_path) safeDeleteNode(obj.cache_name, obj.cache_path) - safeDeleteNode(obj.b2s_name, obj.b2s_path) - safeDeleteNode(obj.a2s_name, obj.a2s_path) + // a2s and b2s are pairs + if (safeDeleteNode(obj.b2s_name, obj.b2s_path)) { + obj.a2s_path.remove() + obj.a2s_path.scope.crawl() + } }, } From 799dd708b90edfacc4cbf865390549d4b5c08f7b Mon Sep 17 00:00:00 2001 From: echo094 <20028238+echo094@users.noreply.github.com> Date: Sun, 22 Sep 2024 17:54:32 +0800 Subject: [PATCH 16/21] ControlFlowFlattening part1 --- src/plugin/jsconfuser.js | 134 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) diff --git a/src/plugin/jsconfuser.js b/src/plugin/jsconfuser.js index 0874934..4a87880 100644 --- a/src/plugin/jsconfuser.js +++ b/src/plugin/jsconfuser.js @@ -34,6 +34,19 @@ function safeGetName(path) { return null } +function safeGetLiteral(path) { + if (path.isUnaryExpression()) { + if (path.node.operator === '-' && path.get('argument').isNumericLiteral()) { + return -1 * path.get('argument').node.value + } + return null + } + if (path.isLiteral()) { + return path.node.value + } + return null +} + function safeDeleteNode(name, path) { let binding if (path.isFunctionDeclaration()) { @@ -1557,6 +1570,124 @@ const deGlobalConcealing = { }, } +function checkControlVar(path) { + const parent = path.parentPath + if (path.key !== 'right' || !parent.isAssignmentExpression()) { + return false + } + const var_path = parent.get('left') + const var_name = var_path.node?.name + if (!var_name) { + return false + } + let root_path = parent.parentPath + if (root_path.isExpressionStatement) { + root_path = root_path.parentPath + } + const binding = parent.scope.getBinding(var_name) + for (const ref of binding.referencePaths) { + if (ref === var_path) { + continue + } + let cur = ref + let valid = false + while (cur && cur !== root_path) { + if (cur.isSwitchCase() || cur === path) { + valid = true + break + } + cur = cur.parentPath + } + if (!valid) { + return false + } + if (ref.key === 'object') { + const prop = ref.parentPath.get('property') + if (!prop.isLiteral() && !prop.isIdentifier()) { + return false + } + continue + } + if (ref.key === 'right') { + const left = ref.parentPath.get('left') + if (!left.isMemberExpression()) { + return false + } + const obj = safeGetName(left.get('object')) + if (obj !== var_name) { + return false + } + continue + } + } + return true +} + +/** + * Process the constant properties in the controlVar + * + * Template: + * ```javascript + * controlVar = { + * // strings + * key_string: 'StringLiteral', + * // numbers + * key_number: 'NumericLiteral', + * } + * ``` + * + * Some kinds of deadCode may in inserted to the fake chunks: + * + * ```javascript + * controlVar = false + * controlVar = undefined + * controlVar[randomControlKey] = undefined + * delete controlVar[randomControlKey] + * ``` + */ +const deControlFlowFlatteningStateless = { + ObjectExpression(path) { + if (!checkControlVar(path)) { + return + } + const parent = path.parentPath + const var_name = parent.get('left').node?.name + console.log(`[ControlFlowFlattening] parse stateless in obj: ${var_name}`) + const props = {} + const prop_num = path.node.properties.length + for (let i = 0; i < prop_num; ++i) { + const prop = path.get(`properties.${i}`) + const key = safeGetName(prop.get('key')) + const value = safeGetLiteral(prop.get('value')) + if (!key || !value) { + continue + } + props[key] = value + } + const binding = parent.scope.getBinding(var_name) + for (const ref of binding.referencePaths) { + if (ref.key !== 'object') { + continue + } + const prop = safeGetName(ref.parentPath.get('property')) + if (!prop) { + continue + } + if (!Object.prototype.hasOwnProperty.call(props, prop)) { + continue + } + const upper = ref.parentPath + if (upper.key === 'left' && upper.parentPath.isAssignmentExpression()) { + // this is in the fake chunk + ref.parentPath.parentPath.remove() + continue + } + safeReplace(ref.parentPath, props[prop]) + } + binding.scope.crawl() + }, +} + module.exports = function (code) { let ast try { @@ -1587,6 +1718,9 @@ module.exports = function (code) { traverse(ast, pruneIfBranch) // GlobalConcealing traverse(ast, deGlobalConcealing) + // ControlFlowFlattening + traverse(ast, deControlFlowFlatteningStateless) + traverse(ast, calculateConstantExp) code = generator(ast, { comments: false, jsescOption: { minimal: true }, From cc97a6403e62c6f1c8da9e80e722ac5224717868 Mon Sep 17 00:00:00 2001 From: echo094 <20028238+echo094@users.noreply.github.com> Date: Tue, 24 Sep 2024 23:03:53 +0800 Subject: [PATCH 17/21] Stack update --- src/plugin/jsconfuser.js | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/src/plugin/jsconfuser.js b/src/plugin/jsconfuser.js index 4a87880..f0c5fe4 100644 --- a/src/plugin/jsconfuser.js +++ b/src/plugin/jsconfuser.js @@ -379,16 +379,25 @@ function processAssignLeft(vm, cache, path, prop_name, stk_name) { vm.evalSync(generator(father.node).code) cache[prop_name] = { type: 'value', - value: right.node.value, + value: right.node, } return } + if (right.isArrayExpression()) { + const elements = right.node.elements + if (elements.length === 1 && elements[0]?.value === 'charCodeAt') { + cache[prop_name] = { + type: 'value', + value: right.node, + } + return + } + } if (right.isUnaryExpression() && right.node.operator === '-') { - const value = vm.evalSync(generator(right.node).code) vm.evalSync(generator(father.node).code) cache[prop_name] = { type: 'value', - value: value, + value: right.node, } return } @@ -442,7 +451,7 @@ function processReplace(cache, path, prop_name) { return true } if (type === 'value') { - safeReplace(path, value) + path.replaceWith(value) return true } return false @@ -522,7 +531,10 @@ function tryStackReplace(path, len, invalid) { } function getStackParamLen(path) { - const stk_name = path.node.params[0].argument.name + const stk_name = path.node.params?.[0]?.argument?.name + if (!stk_name) { + return 'unknown' + } const body_path = path.get('body') let len = 'unknown' body_path.traverse({ @@ -1712,6 +1724,8 @@ module.exports = function (code) { traverse(ast, deStringConcealingPlace) // StringSplitting traverse(ast, calculateConstantExp) + // Stack (run again) + traverse(ast, deStackFuncOther) // OpaquePredicates traverse(ast, deOpaquePredicates) traverse(ast, calculateConstantExp) From 48663b345b4dea1c395341baa0284344ce1cb04a Mon Sep 17 00:00:00 2001 From: echo094 <20028238+echo094@users.noreply.github.com> Date: Sun, 1 Dec 2024 10:40:41 +0800 Subject: [PATCH 18/21] split code --- src/plugin/jsconfuser.js | 1733 +----------------- src/utility/check-func.js | 15 + src/utility/safe-func.js | 72 + src/visitor/jsconfuser/anti-tooling.js | 53 + src/visitor/jsconfuser/control-flow.js | 182 ++ src/visitor/jsconfuser/duplicate-literal.js | 120 ++ src/visitor/jsconfuser/global-concealing.js | 129 ++ src/visitor/jsconfuser/global.js | 83 + src/visitor/jsconfuser/minify.js | 92 + src/visitor/jsconfuser/opaque-predicates.js | 155 ++ src/visitor/jsconfuser/stack.js | 335 ++++ src/visitor/jsconfuser/string-compression.js | 182 ++ src/visitor/jsconfuser/string-concealing.js | 447 +++++ 13 files changed, 1890 insertions(+), 1708 deletions(-) create mode 100644 src/utility/check-func.js create mode 100644 src/utility/safe-func.js create mode 100644 src/visitor/jsconfuser/anti-tooling.js create mode 100644 src/visitor/jsconfuser/control-flow.js create mode 100644 src/visitor/jsconfuser/duplicate-literal.js create mode 100644 src/visitor/jsconfuser/global-concealing.js create mode 100644 src/visitor/jsconfuser/global.js create mode 100644 src/visitor/jsconfuser/minify.js create mode 100644 src/visitor/jsconfuser/opaque-predicates.js create mode 100644 src/visitor/jsconfuser/stack.js create mode 100644 src/visitor/jsconfuser/string-compression.js create mode 100644 src/visitor/jsconfuser/string-concealing.js diff --git a/src/plugin/jsconfuser.js b/src/plugin/jsconfuser.js index f0c5fe4..6f87403 100644 --- a/src/plugin/jsconfuser.js +++ b/src/plugin/jsconfuser.js @@ -1,1704 +1,18 @@ const { parse } = require('@babel/parser') const generator = require('@babel/generator').default const traverse = require('@babel/traverse').default -const t = require('@babel/types') -const ivm = require('isolated-vm') + const calculateConstantExp = require('../visitor/calculate-constant-exp') const pruneIfBranch = require('../visitor/prune-if-branch') - -const isolate = new ivm.Isolate() - -const collect_id = { - arrowFunc: null, -} - -function safeReplace(path, value) { - if (typeof value === 'string') { - path.replaceWith(t.stringLiteral(value)) - return - } - if (typeof value === 'number') { - path.replaceWith(t.numericLiteral(value)) - return - } - path.replaceWithSourceString(value) -} - -function safeGetName(path) { - if (path.isIdentifier()) { - return path.node.name - } - if (path.isLiteral()) { - return path.node.value - } - return null -} - -function safeGetLiteral(path) { - if (path.isUnaryExpression()) { - if (path.node.operator === '-' && path.get('argument').isNumericLiteral()) { - return -1 * path.get('argument').node.value - } - return null - } - if (path.isLiteral()) { - return path.node.value - } - return null -} - -function safeDeleteNode(name, path) { - let binding - if (path.isFunctionDeclaration()) { - binding = path.parentPath.scope.getBinding(name) - } else { - binding = path.scope.getBinding(name) - } - if (!binding) { - return false - } - binding.scope.crawl() - binding = binding.scope.getBinding(name) - if (binding.references) { - return false - } - for (const item of binding.constantViolations) { - item.remove() - } - const decl = binding.path - if (decl.removed) { - return true - } - if (!decl.isVariableDeclarator() && !decl.isFunctionDeclaration()) { - return true - } - binding.path.remove() - return true -} - -function deAntiToolingCheckFunc(path) { - if (path.node.params.length) { - return false - } - const body = path.node.body - if (!t.isBlockStatement(body)) { - return false - } - if (body.body.length) { - return false - } - return true -} - -function deAntiToolingExtract(path, func_name) { - let binding = path.scope.getBinding(func_name) - for (let ref of binding.referencePaths) { - if (!ref.parentPath.isCallExpression() || !ref.key === 'callee') { - continue - } - const call = ref.parentPath - if (!call.listKey === 'body') { - continue - } - for (let node of call.node.arguments) { - call.insertBefore(node) - } - call.remove() - } - binding.scope.crawl() - binding = path.scope.getBinding(func_name) - if (binding.references === 0) { - path.remove() - } -} - -const deAntiTooling = { - FunctionDeclaration(path) { - const func_name = path.node.id?.name - if (!func_name) { - return - } - if (!deAntiToolingCheckFunc(path)) { - return - } - console.log(`AntiTooling Func Name: ${func_name}`) - deAntiToolingExtract(path, func_name) - }, -} - -function checkArrowWrap(path) { - if (path.node?.name !== 'arguments') { - return null - } - if (!path.parentPath.isSpreadElement()) { - return null - } - const call = path.parentPath.parentPath - if (path.parentPath.listKey !== 'arguments' || !call.isCallExpression()) { - return null - } - if (call.key !== 'argument' || !call.parentPath.isReturnStatement()) { - return null - } - const func_name = call.node.callee?.name - if (!func_name) { - return null - } - let wrap = call.getFunctionParent() - if (wrap.key !== 'init') { - return null - } - wrap = wrap.parentPath - const wrap_name = wrap.node.id?.name - wrap = wrap.parentPath - if ( - wrap.listKey !== 'body' || - wrap.key !== 0 || - wrap.container.length !== 2 - ) { - return null - } - const str = generator(wrap.container[1]).code - if (str.indexOf(wrap_name) === -1) { - return null - } - wrap = wrap.getFunctionParent() - const arrow_name = wrap.node?.id?.name - if (!arrow_name || wrap.node.params?.[0]?.name !== func_name) { - return null - } - return { - name: arrow_name, - path: wrap, - } -} - -/** - * Template: - * ```javascript - * function arrowFunctionName (arrowFn, functionLength = 0){ - * var functionObject = function(){ return arrowFn(...arguments) }; - * return Object.defineProperty(functionObject, "length", { - * "value": functionLength, - * "configurable": true - * }); - * } - * ``` - */ -const deMinifyArrow = { - Identifier(path) { - let obj = checkArrowWrap(path) - if (!obj) { - return - } - collect_id.arrowFunc = obj.name - console.log(`Find arrowFunctionName: ${obj.name}`) - let binding = obj.path.parentPath.scope.bindings[obj.name] - for (const ref of binding.referencePaths) { - if (ref.key !== 'callee') { - console.warn(`Unexpected ref of arrowFunctionName: ${obj.name}`) - continue - } - const repl_path = ref.parentPath - repl_path.replaceWith(repl_path.node.arguments[0]) - } - binding.scope.crawl() - binding = obj.path.parentPath.scope.bindings[obj.name] - if (!binding.references) { - obj.path.remove() - } - }, -} - -function checkArrayName(path) { - if (path.key !== 'argument') { - return null - } - const ret_path = path.parentPath - if (!ret_path.isReturnStatement() || ret_path.key !== 0) { - return null - } - const array_fn_path = ret_path.getFunctionParent() - const array_fn_name = array_fn_path.node.id?.name - if (!array_fn_name) { - return null - } - const binding = array_fn_path.parentPath.scope.bindings[array_fn_name] - if (binding.references !== 1) { - return null - } - let ref = binding.referencePaths[0] - while (ref && !ref.isAssignmentExpression()) { - ref = ref.parentPath - } - if (!ref) { - return null - } - const array_name = ref.node.left?.name - if (!array_name) { - return null - } - return { - func_name: array_fn_name, - func_path: array_fn_path, - array_name: array_name, - array_path: ref, - } -} - -function parseArrayWarp(vm, path) { - let func = path.getFunctionParent(path) - let name = null - let binding = null - if (func.isArrowFunctionExpression()) { - func = func.parentPath - name = func.node.id.name - binding = func.scope.getBinding(name) - } else { - name = func.node.id.name - binding = func.parentPath.scope.getBinding(name) - } - console.log(`Process array warp function: ${name}`) - vm.evalSync(generator(func.node).code) - for (const ref of binding.referencePaths) { - const call = ref.parentPath - if (ref.key !== 'callee') { - console.warn(`Unexpected ref of array warp function: ${call}`) - continue - } - const value = vm.evalSync(generator(call.node).code) - safeReplace(call, value) - } - binding.scope.crawl() - binding = binding.scope.getBinding(name) - if (!binding.references) { - func.remove() - } -} - -/** - * Template: - * ```javascript - * var arrayName = getArrayFn() - * function getArrayFn (){ - * return [...arrayExpression] - * } - * ``` - */ -const deDuplicateLiteral = { - ArrayExpression(path) { - let obj = checkArrayName(path) - if (!obj) { - return - } - console.log(`Find arrayName: ${obj.array_name}`) - let decl_node = t.variableDeclarator( - obj.array_path.node.left, - obj.array_path.node.right - ) - decl_node = t.variableDeclaration('var', [decl_node]) - const code = [generator(obj.func_path.node).code, generator(decl_node).code] - let binding = obj.array_path.scope.getBinding(obj.array_name) - for (const ref of binding.referencePaths) { - const vm = isolate.createContextSync() - vm.evalSync(code[0]) - vm.evalSync(code[1]) - parseArrayWarp(vm, ref) - } - binding.scope.crawl() - binding = binding.scope.bindings[obj.array_name] - if (!binding.references) { - obj.array_path.remove() - binding.path.remove() - } - binding = obj.func_path.parentPath.scope.getBinding(obj.func_name) - binding.scope.crawl() - binding = binding.scope.getBinding(obj.func_name) - if (!binding.references) { - obj.func_path.remove() - } - }, -} - -function checkFuncLen(path) { - if (path.node?.name !== 'configurable' || path.key !== 'key') { - return null - } - const prop = path.parentPath - if (!prop.isObjectProperty() || prop.key !== 1) { - return null - } - const obj = prop.parentPath - if (obj.node.properties.length !== 2) { - return null - } - if (obj.node.properties[0]?.key?.name !== 'value') { - return null - } - if (obj.listKey !== 'arguments' || obj.key !== 2) { - return null - } - const func_name = obj.container[0]?.name - const warp = obj.getFunctionParent() - if (warp.node.params?.[0]?.name !== func_name) { - return null - } - const func_len_name = warp.node?.id?.name - if (!func_len_name || func_len_name === collect_id.arrowFunc) { - return null - } - return { - name: func_len_name, - path: warp, - } -} - -/** - * type: param, value, ref, invalid - */ -function initStackCache(len) { - const cache = {} - for (let i = 0; i < len; ++i) { - cache[i] = { - type: 'param', - } - } - return cache -} - -function processAssignLeft(vm, cache, path, prop_name, stk_name) { - const father = path.parentPath - const right = father.get('right') - if (right.isBinaryExpression()) { - cache[prop_name] = { - type: 'invalid', - } - return - } - if (right.isLiteral()) { - vm.evalSync(generator(father.node).code) - cache[prop_name] = { - type: 'value', - value: right.node, - } - return - } - if (right.isArrayExpression()) { - const elements = right.node.elements - if (elements.length === 1 && elements[0]?.value === 'charCodeAt') { - cache[prop_name] = { - type: 'value', - value: right.node, - } - return - } - } - if (right.isUnaryExpression() && right.node.operator === '-') { - vm.evalSync(generator(father.node).code) - cache[prop_name] = { - type: 'value', - value: right.node, - } - return - } - if (right.isMemberExpression() && right.node.object?.name === stk_name) { - const right_prop = right.get('property') - if (right_prop.isBinaryExpression()) { - return - } - let ref = safeGetName(right_prop) - if (!Object.prototype.hasOwnProperty.call(cache, ref)) { - cache[prop_name] = { - type: 'invalid', - } - return - } - while (cache[ref].type === 'ref') { - ref = cache[ref].value - } - if (cache[ref].type === 'value') { - safeReplace(right, cache[ref].value) - vm.evalSync(generator(father.node).code) - cache[prop_name] = { - type: 'value', - value: cache[ref].value, - } - } else { - cache[prop_name] = { - type: 'ref', - value: ref, - } - } - return - } - cache[prop_name] = { - type: 'invalid', - } -} - -function processAssignInvalid(cache, path, prop_name) { - cache[prop_name] = { - type: 'invalid', - } -} - -function processReplace(cache, path, prop_name) { - const value = cache[prop_name].value - const type = cache[prop_name].type - if (type === 'ref') { - path.node.computed = true - safeReplace(path.get('property'), value) - return true - } - if (type === 'value') { - path.replaceWith(value) - return true - } - return false -} - -function checkStackInvalid(path) { - const stk_name = path.node.params[0].argument.name - const body_path = path.get('body') - const obj = {} - body_path.traverse({ - MemberExpression: { - exit(path) { - if (path.node.object.name !== stk_name) { - return - } - const father = path.parentPath - const prop = path.get('property') - const prop_name = safeGetName(prop) - if (father.isUpdateExpression()) { - obj[prop_name] = 1 - return - } - if (body_path.scope == father.scope) { - return - } - if (!father.isAssignmentExpression() || path.key !== 'left') { - return - } - obj[prop_name] = 1 - }, - }, - }) - return obj -} - -function tryStackReplace(path, len, invalid) { - const stk_name = path.node.params[0].argument.name - const body_path = path.get('body') - const cache = initStackCache(len) - const vm = isolate.createContextSync() - vm.evalSync(`var ${stk_name} = []`) - let changed = false - body_path.traverse({ - MemberExpression: { - exit(path) { - if (path.node.object.name !== stk_name) { - return - } - const prop = path.get('property') - if (prop.isBinaryExpression()) { - return - } - const prop_name = safeGetName(prop) - if (!prop_name) { - return - } - if (Object.prototype.hasOwnProperty.call(invalid, prop_name)) { - processAssignInvalid(cache, path, prop_name) - return - } - const exist = Object.prototype.hasOwnProperty.call(cache, prop_name) - if (exist && cache[prop_name].type === 'param') { - return - } - const father = path.parentPath - if (father.isAssignmentExpression() && path.key === 'left') { - processAssignLeft(vm, cache, path, prop_name, stk_name) - } else if (exist) { - changed |= processReplace(cache, path, prop_name) - } - }, - }, - }) - const binding = body_path.scope.getBinding(stk_name) - binding.scope.crawl() - return changed -} - -function getStackParamLen(path) { - const stk_name = path.node.params?.[0]?.argument?.name - if (!stk_name) { - return 'unknown' - } - const body_path = path.get('body') - let len = 'unknown' - body_path.traverse({ - MemberExpression: { - exit(path) { - if (path.node.object.name !== stk_name) { - return - } - const prop = path.get('property') - if (prop.isBinaryExpression()) { - return - } - const prop_name = safeGetName(prop) - if (!prop_name || prop_name !== 'length') { - return - } - const father = path.parentPath - if (!father.isAssignmentExpression() || path.key !== 'left') { - return - } - const right = father.get('right') - if (right.isBinaryExpression()) { - return - } - if (!right.isLiteral()) { - return - } - len = right.node.value - path.stop() - }, - }, - }) - return len -} - -function processStackParam(path, len) { - if (path.isArrowFunctionExpression()) { - console.log(`Process arrowFunctionExpression, len: ${len}`) - } else if (path.isFunctionExpression()) { - console.log(`Process functionExpression, len: ${len}`) - } else { - console.log(`Process Function ${path.node.id.name}, len: ${len}`) - } - let changed = true - const invalid = checkStackInvalid(path) - while (changed) { - changed = tryStackReplace(path, len, invalid) - path.traverse(calculateConstantExp) - } -} - -const deStackFuncLen = { - Identifier(path) { - let obj = checkFuncLen(path) - if (!obj) { - return - } - console.log(`Find functionLengthName: ${obj.name}`) - let binding = obj.path.parentPath.scope.bindings[obj.name] - for (const ref of binding.referencePaths) { - if (ref.key !== 'callee') { - console.warn(`Unexpected ref of functionLengthName: ${obj.name}`) - continue - } - const repl_path = ref.parentPath - const arg = repl_path.node.arguments[0] - const len = repl_path.node.arguments[1].value - if (t.isIdentifier(arg)) { - const func_name = arg.name - const func_decl = repl_path.scope.getBinding(func_name).path - processStackParam(func_decl, len) - repl_path.remove() - } else { - repl_path.replaceWith(arg) - processStackParam(repl_path, len) - } - } - binding.scope.crawl() - binding = obj.path.parentPath.scope.bindings[obj.name] - if (!binding.references) { - obj.path.remove() - } - }, -} - -const deStackFuncOther = { - RestElement(path) { - if (path.listKey !== 'params') { - return - } - const func = path.getFunctionParent() - const len = getStackParamLen(func) - if (len === 'unknown') { - return - } - processStackParam(func, len) - }, -} - -function checkPattern(code, pattern) { - let i = 0 - let j = 0 - while (i < code.length && j < pattern.length) { - if (code[i] == pattern[j]) { - ++j - } - ++i - } - return j == pattern.length -} - -function findStringDecoder(path) { - if (path.node?.name !== 'charCodeAt' || path.key !== 'property') { - return null - } - let loop = path - while (loop && !loop.isForStatement()) { - loop = loop.parentPath - } - const i = loop?.node?.update?.argument?.name - if (!i) { - return null - } - const func = loop.getFunctionParent() - const param = func.node.params?.[0]?.name - if (!param) { - return null - } - const code = generator(func.node).code - const template = - `function(${param}){var=${param}.split()for(${i}=1;${i}<.length;${i}++)` + - `[${i}].charCodeAt(0)[${i}].push().charAt(0)return.join().split()}` - if (!checkPattern(code, template)) { - return null - } - return { - name: func.node.id.name, - path: func, - } -} - -function findStringGet(path) { - const decoder_name = path.node.id.name - let binding = path.parentPath.scope.getBinding(decoder_name) - if (!binding || binding.references !== 1) { - return null - } - const ref = binding.referencePaths[0] - if (ref.key !== 1 || ref.listKey !== 'arguments') { - return null - } - const get_ref_path = ref.parentPath.get('arguments.0') - const get_name = get_ref_path.node?.name - if (!get_name) { - return null - } - binding = get_ref_path.scope.getBinding(get_name) - return { - name: get_name, - path: binding.path, - ref: get_ref_path, - } -} - -function findStringSplit(path) { - while (path && !path.isAssignmentExpression()) { - path = path.parentPath - } - const split_name = path?.node?.left?.name - if (!split_name) { - return null - } - const binding = path.scope.getBinding(split_name) - return { - name: split_name, - path: path, - def: binding.path, - } -} - -function findStringFn(path, name) { - const binding = path.scope.getBinding(name) - const ref = binding.referencePaths?.[0] - if (!ref) { - return null - } - const fn_path = ref.getFunctionParent(name) - const fn_name = fn_path.node.id.name - return { - name: fn_name, - path: fn_path, - } -} - -/** - * Template: - * ```javascript - * var split = (function (getStringParamName, decoderParamName) { - * return decoderParamName(getStringParamName()) - * })(getStringName, decoder) - * function getStringName () { - * var str = splits[0] - * var objectToTest = {} - * if ('testingFor' in objectToTest) { - * str += splits[1] - * } - * return str - * } - * function decoder (b) { - * // DecodeTemplate - * } - * function fnName (index) { - * return split[index] - * } - * ``` - */ -const deStringCompression = { - Identifier(path) { - const decoder_obj = findStringDecoder(path) - if (!decoder_obj) { - return - } - const get_obj = findStringGet(decoder_obj.path) - if (!get_obj) { - return - } - const split_obj = findStringSplit(get_obj.ref) - if (!get_obj) { - return - } - const fn_obj = findStringFn(split_obj.path, split_obj.name) - if (!get_obj) { - return - } - console.log(`Find stringCompression Fn: ${fn_obj.name}`) - const vm = isolate.createContextSync() - vm.evalSync(generator(decoder_obj.path.node).code) - vm.evalSync(generator(get_obj.path.node).code) - vm.evalSync('var ' + generator(split_obj.path.node).code) - vm.evalSync(generator(fn_obj.path.node).code) - let binding = fn_obj.path.parentPath.scope.getBinding(fn_obj.name) - for (const ref of binding.referencePaths) { - if (ref.key !== 'callee') { - console.warn( - `Unexpected ref of stringCompression Fn: ${ref.parentPath}` - ) - continue - } - const repl_path = ref.parentPath - try { - const value = vm.evalSync(generator(repl_path.node).code) - safeReplace(repl_path, value) - } catch (e) { - console.warn( - `Unexpected ref of stringCompression Fn: ${ref.parentPath}` - ) - } - } - binding.scope.crawl() - binding = binding.scope.bindings[fn_obj.name] - if (!binding.references) { - fn_obj.path.remove() - } - binding.scope.crawl() - binding = split_obj.path.scope.getBinding(split_obj.name) - if (!binding.references) { - split_obj.path.remove() - split_obj.def.remove() - } - binding.scope.crawl() - binding = get_obj.path.scope.getBinding(get_obj.name) - if (!binding.references) { - get_obj.path.remove() - } - binding.scope.crawl() - binding = decoder_obj.path.scope.getBinding(decoder_obj.name) - if (!binding.references) { - decoder_obj.path.remove() - } - }, -} - -function insertDepItemVar(deps, name, path) { - const binding = path.scope.getBinding(name) - if (binding.path === path) { - deps.push({ - name: name, - path: binding.path, - node: t.variableDeclaration('var', [binding.path.node]), - pos: binding.path.node.start, - }) - return - } - deps.push({ - name: name, - path: path, - pos: path.node.start, - }) - deps.push({ - name: name, - path: binding.path, - node: t.variableDeclaration('var', [binding.path.node]), - pos: binding.path.node.start, - }) -} - -/** - * GlobalTemplate 1 (currently not support): - * ```javascript - * function {getGlobalFnName}(){ - * var localVar = false; - * eval(${transform.jsConfuserVar("localVar")} + " = true") - * if (!localVar) { - * {countermeasures} - * } - * const root = eval("this"); - * return root; - * } - * ``` - * GlobalTemplate 2: - * ```javascript - * function {getGlobalFnName}(array = [a, b, c, d]){ - * var bestMatch - * var itemsToSearch = [] - * try { - * bestMatch = Object - * itemsToSearch["push"](("")["__proto__"]["constructor"]["name"]) - * } catch(e) { - * } - * // ... - * return bestMatch || this; - * } - * ``` - */ -function findGlobalFn(path) { - const glo_fn_name = path.node.id?.name - if (!glo_fn_name) { - return null - } - let node = path.node.params?.[0] - if ( - !node || - !t.isAssignmentPattern(node) || - !t.isArrayExpression(node.right) || - node.right.elements.length !== 4 - ) { - return null - } - const array_name = node.left.name - const code = generator(path.node.body).code - const template = - 'try{=Objectpush(__proto__constructorname)}catch{}' + - `:for(;<${array_name}length;)try{=${array_name}[]()` + - 'for()if(typeof)continue}catch{}return||this' - if (!checkPattern(code, template)) { - return - } - const deps = [] - const array = path.get('params.0.right') - for (let i = 0; i < 4; ++i) { - const ele_name = safeGetName(array.get(`elements.${i}`)) - const binding = path.scope.getBinding(ele_name) - deps.push({ - name: ele_name, - path: binding.path, - pos: binding.path.node.start, - }) - } - deps.push({ - name: glo_fn_name, - path: path, - pos: path.node.start, - }) - return { - glo_fn_name: glo_fn_name, - glo_fn_path: path, - deps: deps, - } -} - -/** - * Template: - * ```javascript - * var __globalObject = {getGlobalFnName}() || {}; - * var __TextDecoder = __globalObject["TextDecoder"]; - * var __Uint8Array = __globalObject["Uint8Array"]; - * var __Buffer = __globalObject["Buffer"]; - * var __String = __globalObject["String"] || String; - * var __Array = __globalObject["Array"] || Array; - * ``` - */ -function findGlobalFnRef(obj) { - const path = obj.glo_fn_path - const glo_fn_name = obj.glo_fn_name - let binding = path.parentPath.scope.getBinding(glo_fn_name) - let glo_fn_ref = binding.referencePaths[0] - while (!glo_fn_ref.isAssignmentExpression()) { - glo_fn_ref = glo_fn_ref.parentPath - } - const glo_obj_name = glo_fn_ref.node.left.name - obj.glo_obj_name = glo_obj_name - obj.glo_obj_path = glo_fn_ref - obj.glo_obj_ref = {} - insertDepItemVar(obj.deps, glo_obj_name, glo_fn_ref) - binding = glo_fn_ref.scope.getBinding(glo_obj_name) - for (const ref of binding.referencePaths) { - const prop = safeGetName(ref.parentPath.get('property')) - if (!prop) { - continue - } - let root = ref - while (!root.isAssignmentExpression()) { - root = root.parentPath - } - const ref_name = safeGetName(root.get('left')) - obj.glo_obj_ref[prop] = ref_name - insertDepItemVar(obj.deps, ref_name, root) - } - return -} - -/** - * Template: - * ```javascript - * var utf8ArrayToStr = (function () { - * // ... - * })(); - * function bufferToStringName () { - * if(typeof __TextDecoder !== "undefined" && __TextDecoder) { - * return new __TextDecoder()["decode"](new __Uint8Array(buffer)); - * } else if(typeof __Buffer !== "undefined" && __Buffer) { - * return __Buffer["from"](buffer)["toString"]("utf-8"); - * } else { - * return utf8ArrayToStr(buffer); - * } - * } - * ``` - */ -function findBufferToString(obj) { - const path = obj.glo_obj_path - const ref_array = obj.glo_obj_ref['Array'] - let binding = path.scope.getBinding(ref_array) - for (const ref of binding.referencePaths) { - if (ref.key !== 'callee') { - continue - } - let a2s_path = ref.getFunctionParent() - while (!a2s_path.isAssignmentExpression()) { - a2s_path = a2s_path.parentPath - } - obj.a2s_name = safeGetName(a2s_path.get('left')) - obj.a2s_path = a2s_path - insertDepItemVar(obj.deps, obj.a2s_name, obj.a2s_path) - break - } - if (!obj.a2s_name) { - return false - } - binding = obj.a2s_path.scope.getBinding(obj.a2s_name) - const b2s_path = binding.referencePaths[0].getFunctionParent() - obj.b2s_name = safeGetName(b2s_path.get('id')) - obj.b2s_path = b2s_path - obj.deps.push({ - name: obj.b2s_name, - path: b2s_path, - pos: b2s_path.node.start, - }) - binding = b2s_path.parentPath.scope.getBinding(obj.b2s_name) - const child = [] - for (const ref of binding.referencePaths) { - const decode_fn = ref.getFunctionParent() - let valid = false - decode_fn.traverse({ - StringLiteral(path) { - if (path.node.value.length === 91) { - valid = true - path.stop() - } - }, - }) - if (!valid) { - return false - } - child.push({ - name: decode_fn.node.id.name, - decoder: decode_fn, - }) - } - obj.child = child - return true -} - -function generatorStringConcealingDepCode(obj) { - obj.deps.sort((a, b) => { - return a.pos - b.pos - }) - const dep_node = t.program([]) - for (const item of obj.deps) { - if (item.node) { - dep_node.body.push(item.node) - } else { - dep_node.body.push(item.path.node) - } - } - obj.dep_code = generator(dep_node).code -} - -function renameProperty(member) { - const obj_name = safeGetName(member.get('object')) - const prop_name = safeGetName(member.get('property')) - const new_name = member.scope.generateUidIdentifier(`_tmp_local_`) - const binding = member.scope.getBinding(obj_name) - let first = true - for (const ref of binding.referencePaths) { - const item = ref.parentPath - const prop = safeGetName(item.get('property')) - if (prop !== prop_name) { - continue - } - if (first) { - let body = item - while (body.listKey !== 'body') { - body = body.parentPath - } - body.container.unshift( - t.variableDeclaration('var', [t.variableDeclarator(new_name)]) - ) - body.scope.crawl() - first = false - } - item.replaceWith(new_name) - } - member.scope.crawl() -} - -/** - * Template: - * ```javascript - * var cacheName = [], arrayName = [] - * // Below will appear multiple times - * var getterFnName = (x, y, z, a, b)=>{ - * if ( x !== y ) { - * return b[x] || (b[x] = a(arrayName[x])) - * } - * // Add fake ifStatements - * if(typeof a === "undefined") { - * a = decodeFn - * } - * if(typeof b === "undefined") { - * b = cacheName - * } - * } - * // Base91 Algo - * function decodeFn (str){ - * var table = {__strTable__}; - * var raw = "" + (str || ""); - * var len = raw.length; - * var ret = []; - * var b = 0; - * var n = 0; - * var v = -1; - * for (var i = 0; i < len; i++) { - * var p = table.indexOf(raw[i]); - * if (p === -1) continue; - * if (v < 0) { - * v = p; - * } else { - * v += p * 91; - * b |= v << n; - * n += (v & 8191) > 88 ? 13 : 14; - * do { - * ret.push(b & 0xff); - * b >>= 8; - * n -= 8; - * } while (n > 7); - * v = -1; - * } - * } - * if (v > -1) { - * ret.push((b | (v << n)) & 0xff); - * } - * return bufferToStringName(ret); - * } - * ``` - */ -function processSingleGetter(obj, decoder_name, decoder_path) { - const decoder_code = generator(decoder_path.node).code - let binding = decoder_path.parentPath.scope.getBinding(decoder_name) - let getter_path = binding.referencePaths[0].getFunctionParent() - while ( - !getter_path.isAssignmentExpression() && - !getter_path.isVariableDeclarator() - ) { - getter_path = getter_path.parentPath - } - let getter_name - if (getter_path.isAssignmentExpression()) { - if (getter_path.get('left').isMemberExpression()) { - renameProperty(getter_path.get('left')) - } - getter_name = safeGetName(getter_path.get('left')) - } else { - getter_name = safeGetName(getter_path.get('id')) - } - console.log( - `[StringConcealing] getter: ${getter_name} decoder: ${decoder_name}` - ) - const getter_code = 'var ' + generator(getter_path.node).code - binding = getter_path.scope.getBinding(getter_name) - if (getter_path.isAssignmentExpression()) { - getter_path.get('right').replaceWith(t.numericLiteral(0)) - } else { - getter_path.get('init').replaceWith(t.numericLiteral(0)) - } - binding.scope.crawl() - binding = getter_path.scope.getBinding(getter_name) - let complete = false - while (!complete) { - complete = true - const vm = isolate.createContextSync() - vm.evalSync(obj.dep_code) - try { - for (const ref of binding.referencePaths) { - if (ref.findParent((path) => path.removed)) { - continue - } - let repl_path = ref.parentPath - if (repl_path.isCallExpression()) { - const args = repl_path.node.arguments - if (args.length !== 1 || !t.isLiteral(args[0])) { - console.warn(`[StringConcealing] Unexpected call: ${repl_path}`) - continue - } - } else if (repl_path.isMemberExpression()) { - repl_path = repl_path.parentPath - } else { - console.warn(`[StringConcealing] Unexpected ref: ${repl_path}`) - continue - } - const eval_code = generator(repl_path.node).code - // The name of getter can be the same as other dep functions - const value = vm.evalSync( - `(function (){${decoder_code}\n${getter_code}\nreturn ${eval_code}})()` - ) - safeReplace(repl_path, value) - } - } catch (e) { - if (e.name !== 'ReferenceError') { - console.warn(`[StringConcealing] Unexpected exception: ${e.message}`) - return - } - complete = false - const lost = e.message.split(' ')[0] - const binding = getter_path.scope.getBinding(lost) - if (!binding) { - console.warn(`[StringConcealing] Missing cache or array: ${lost}`) - return - } - let count = binding.constantViolations.length - if (count) { - console.warn(`[StringConcealing] Invalid violations ${lost} : ${count}`) - return - } - count = binding.path.node.init.elements.length - if (count) { - console.log(`[StringConcealing] Add array : ${lost}`) - obj.array_name = lost - obj.array_path = binding.path - } else { - console.log(`[StringConcealing] Add cache : ${lost}`) - obj.cache_name = lost - obj.cache_path = binding.path - } - insertDepItemVar(obj.deps, lost, binding.path) - generatorStringConcealingDepCode(obj) - } - } - safeDeleteNode(getter_name, getter_path) - safeDeleteNode(decoder_name, decoder_path) -} - -const deStringConcealing = { - FunctionDeclaration(path) { - const obj = findGlobalFn(path) - if (!obj) { - return null - } - if (obj.glo_fn_path.parentPath.getFunctionParent()) { - return null - } - findGlobalFnRef(obj) - if (!findBufferToString(obj)) { - return - } - generatorStringConcealingDepCode(obj) - for (const item of obj.child) { - processSingleGetter(obj, item.name, item.decoder) - } - safeDeleteNode(obj.array_name, obj.array_path) - safeDeleteNode(obj.cache_name, obj.cache_path) - // a2s and b2s are pairs - if (safeDeleteNode(obj.b2s_name, obj.b2s_path)) { - obj.a2s_path.remove() - obj.a2s_path.scope.crawl() - } - }, -} - -function tryStringConcealingPlace(path) { - const parent = path.parentPath - if (!parent.isAssignmentExpression()) { - return - } - const name = safeGetName(parent.get('left')) - let binding = parent.scope.getBinding(name) - if (binding?.constantViolations?.length !== 1) { - return - } - const code = generator(parent.node).code - const vm = isolate.createContextSync() - vm.evalSync('var ' + code) - for (const ref of binding.referencePaths) { - if (ref.key !== 'object') { - continue - } - const test = generator(ref.parent).code - const res = vm.evalSync(test) - safeReplace(ref.parentPath, res) - } - safeDeleteNode(name, parent) -} - -const deStringConcealingPlace = { - StringLiteral(path) { - if (path.key !== 'right' || !path.parentPath.isAssignmentExpression()) { - return - } - const name = safeGetName(path.parentPath.get('left')) - if (!name) { - return - } - const binding = path.scope.getBinding(name) - if (binding.constantViolations.length !== 1) { - return - } - for (const ref of binding.referencePaths) { - if (ref.node.start < path.node.start) { - continue - } - ref.replaceWith(path.node) - } - safeDeleteNode(name, path.parentPath) - }, - ArrayExpression(path) { - let valid = true - if (path.node.elements.length === 0) { - return - } - for (const ele of path.node.elements) { - if (!t.isStringLiteral(ele)) { - valid = false - break - } - } - if (!valid) { - return - } - tryStringConcealingPlace(path) - }, - ObjectExpression(path) { - let valid = true - if (path.node.properties.length === 0) { - return - } - for (const ele of path.node.properties) { - if (!t.isStringLiteral(ele.value)) { - valid = false - break - } - } - if (!valid) { - return - } - tryStringConcealingPlace(path) - }, -} - -function checkOpaqueObject(path) { - const parent = path.parentPath - if (!parent.isAssignmentExpression()) { - return null - } - const tmp_name = safeGetName(parent.get('left')) - const func_path = parent.getFunctionParent() - if ( - !func_path || - func_path.key !== 'callee' || - !func_path.parentPath.isCallExpression() - ) { - return null - } - const func_body = func_path.node.body?.body - if (!func_body || func_body.length < 2) { - return null - } - const last_node = func_body[func_body.length - 1] - if ( - !t.isReturnStatement(last_node) || - last_node.argument?.name !== tmp_name - ) { - return null - } - const root_path = func_path.parentPath.parentPath - if (!root_path.isAssignmentExpression()) { - return null - } - const pred_name = safeGetName(root_path.get('left')) - const obj = { - pred_name: pred_name, - pred_path: root_path, - props: {}, - } - for (const prop of path.node.properties) { - const key = prop.key.name - const value = prop.value - if (t.isNumericLiteral(value)) { - obj.props[key] = { - type: 'number', - } - continue - } - if (t.isStringLiteral(value)) { - obj.props[key] = { - type: 'string', - } - continue - } - if (t.isArrayExpression(value)) { - if (value.elements.length === 0) { - obj.props[key] = { - type: 'array_dep', - } - } - continue - } - if (t.isArrowFunctionExpression(value) || t.isFunctionExpression(value)) { - const param = value.params?.[0]?.left?.name - if (!param) { - continue - } - const code = generator(value).code - const template = - `(${param}=){if(${pred_name}[0])${pred_name}push()` + - `return${pred_name}${param}}` - if (checkPattern(code, template)) { - obj.props[key] = { - type: 'array', - } - } - continue - } - } - return obj -} - -/** - * Template: - * ```javascript - * // This is defined in the global space - * var predicateName = (function () { - * var tempName = { - * prop_array_1: [], - * prop_array: function (paramName = 'length') { - * if (!predicateName[prop_array_1][0]) { - * predicateName[prop_array_1][0].push(rand1) - * } - * return predicateName[prop_array_1][paramName] - * }, - * prop_number: rand2, - * prop_string: rand_str, - * } - * return tempName - * })() - * // Below will appear multiple times - * predicateName[prop_array]() ? test : fake - * predicateName[prop_number] > rand3 ? test : fake - * predicateName[prop_string].charAt(index) == real_char ? test : fake - * predicateName[prop_string].charCodeAt(index) == real_char ? test : fake - * ``` - */ -const deOpaquePredicates = { - ObjectExpression(path) { - const obj = checkOpaqueObject(path) - if (!obj) { - return - } - console.log(`[OpaquePredicates] predicateName : ${obj.pred_name}`) - const vm = isolate.createContextSync() - const code = generator(obj.pred_path.node).code - vm.evalSync('var ' + code) - obj.pred_path.get('right').replaceWith(t.numericLiteral(0)) - let binding = obj.pred_path.scope.getBinding(obj.pred_name) - binding.scope.crawl() - binding = binding.scope.getBinding(obj.pred_name) - for (const ref of binding.referencePaths) { - if (ref.key !== 'object') { - continue - } - const member = ref.parentPath - const prop = member.get('property') - if (!prop || !Object.prototype.hasOwnProperty.call(obj.props, prop)) { - continue - } - let expr = member - while ( - expr.parentPath.isCallExpression() || - expr.parentPath.isMemberExpression() - ) { - expr = expr.parentPath - } - const test = generator(expr.node).code - const res = vm.evalSync(test) - safeReplace(expr, res) - } - safeDeleteNode(obj.pred_name, obj.pred_path) - }, -} - -function findGlobalVar(glo_name, glo_path) { - let tmp_path = glo_path.parentPath.getFunctionParent() - if ( - !tmp_path || - !tmp_path.parentPath.isMemberExpression() || - !tmp_path.parentPath.parentPath.isCallExpression() - ) { - return null - } - const tmp_body = tmp_path.node.body.body - tmp_path = tmp_path.parentPath.parentPath - const ret_node = tmp_body[tmp_body.length - 1] - if ( - !t.isReturnStatement(ret_node) || - !t.isAssignmentExpression(ret_node.argument) - ) { - return null - } - const code = generator(ret_node.argument.right).code - const template = `${glo_name}call(this)` - if (!checkPattern(code, template)) { - return null - } - const glo_var = ret_node.argument.left.name - const binding = glo_path.scope.getBinding(glo_var) - for (const ref of binding.referencePaths) { - if ( - !ref.parentPath.isMemberExpression() || - !ref.parentPath.parentPath.isReturnStatement() - ) { - continue - } - const func_path = ref.getFunctionParent() - const func_name = func_path.node.id.name - return { - glo_var: glo_var, - tmp_path: tmp_path, - glo_fn_name: func_name, - glo_fn_path: func_path, - } - } - return null -} - -function getGlobalConcealingNames(glo_fn_path) { - const obj = {} - glo_fn_path.traverse({ - SwitchCase(path) { - const key = parseInt(generator(path.node.test).code) - let consequent = path.node.consequent[0] - if (t.isReturnStatement(consequent)) { - obj[key] = consequent.argument.property.value - } else { - if (t.isExpressionStatement(consequent)) { - consequent = consequent.expression - } - obj[key] = consequent.right.left.value - } - }, - }) - return obj -} - -/** - * Hide the global vars found by module GlobalAnalysis - * - * Template: - * ```javascript - * // Add to head: - * var globalVar, tempVar = function () { - * getGlobalVariableFnName = createGetGlobalTemplate() - * return globalVar = getGlobalVariableFnName.call(this) - * }["call"]() - * // Add to foot: - * function globalFn (indexParamName) { - * var returnName - * switch (indexParamName) { - * case state_x: { - * return globalVar[name] - * } - * case state_y: { - * returnName = name || globalVar[name] - * break - * } - * } - * return globalVar[returnName] - * } - * // References: - * // name -> globalFn(state) - * ``` - */ -const deGlobalConcealing = { - FunctionDeclaration(path) { - const glo_obj = findGlobalFn(path) - if (!glo_obj) { - return null - } - const obj = findGlobalVar(glo_obj.glo_fn_name, glo_obj.glo_fn_path) - if (!obj) { - return null - } - console.log(`[GlobalConcealing] globalVar: ${obj.glo_var}`) - const glo_vars = getGlobalConcealingNames(obj.glo_fn_path) - console.log(`[GlobalConcealing] globalFn: ${obj.glo_fn_name}`) - let binding = obj.glo_fn_path.parentPath.scope.getBinding(obj.glo_fn_name) - for (const ref of binding.referencePaths) { - const repl_path = ref.parentPath - if (ref.key !== 'callee' || !repl_path.isCallExpression()) { - continue - } - const key = parseInt(generator(repl_path.node.arguments[0]).code) - repl_path.replaceWith(t.identifier(glo_vars[key])) - } - if (safeDeleteNode(obj.glo_fn_name, obj.glo_fn_path)) { - obj.tmp_path.remove() - } - }, -} - -function checkControlVar(path) { - const parent = path.parentPath - if (path.key !== 'right' || !parent.isAssignmentExpression()) { - return false - } - const var_path = parent.get('left') - const var_name = var_path.node?.name - if (!var_name) { - return false - } - let root_path = parent.parentPath - if (root_path.isExpressionStatement) { - root_path = root_path.parentPath - } - const binding = parent.scope.getBinding(var_name) - for (const ref of binding.referencePaths) { - if (ref === var_path) { - continue - } - let cur = ref - let valid = false - while (cur && cur !== root_path) { - if (cur.isSwitchCase() || cur === path) { - valid = true - break - } - cur = cur.parentPath - } - if (!valid) { - return false - } - if (ref.key === 'object') { - const prop = ref.parentPath.get('property') - if (!prop.isLiteral() && !prop.isIdentifier()) { - return false - } - continue - } - if (ref.key === 'right') { - const left = ref.parentPath.get('left') - if (!left.isMemberExpression()) { - return false - } - const obj = safeGetName(left.get('object')) - if (obj !== var_name) { - return false - } - continue - } - } - return true -} - -/** - * Process the constant properties in the controlVar - * - * Template: - * ```javascript - * controlVar = { - * // strings - * key_string: 'StringLiteral', - * // numbers - * key_number: 'NumericLiteral', - * } - * ``` - * - * Some kinds of deadCode may in inserted to the fake chunks: - * - * ```javascript - * controlVar = false - * controlVar = undefined - * controlVar[randomControlKey] = undefined - * delete controlVar[randomControlKey] - * ``` - */ -const deControlFlowFlatteningStateless = { - ObjectExpression(path) { - if (!checkControlVar(path)) { - return - } - const parent = path.parentPath - const var_name = parent.get('left').node?.name - console.log(`[ControlFlowFlattening] parse stateless in obj: ${var_name}`) - const props = {} - const prop_num = path.node.properties.length - for (let i = 0; i < prop_num; ++i) { - const prop = path.get(`properties.${i}`) - const key = safeGetName(prop.get('key')) - const value = safeGetLiteral(prop.get('value')) - if (!key || !value) { - continue - } - props[key] = value - } - const binding = parent.scope.getBinding(var_name) - for (const ref of binding.referencePaths) { - if (ref.key !== 'object') { - continue - } - const prop = safeGetName(ref.parentPath.get('property')) - if (!prop) { - continue - } - if (!Object.prototype.hasOwnProperty.call(props, prop)) { - continue - } - const upper = ref.parentPath - if (upper.key === 'left' && upper.parentPath.isAssignmentExpression()) { - // this is in the fake chunk - ref.parentPath.parentPath.remove() - continue - } - safeReplace(ref.parentPath, props[prop]) - } - binding.scope.crawl() - }, -} +const jcAntiTooling = require('../visitor/jsconfuser/anti-tooling') +const jcControlFlow = require('../visitor/jsconfuser/control-flow') +const jcDuplicateLiteral = require('../visitor/jsconfuser/duplicate-literal') +const jcGlobalConcealing = require('../visitor/jsconfuser/global-concealing') +const jcMinifyInit = require('../visitor/jsconfuser/minify') +const jcOpaquePredicates = require('../visitor/jsconfuser/opaque-predicates') +const jcStackInit = require('../visitor/jsconfuser/stack') +const jcStringCompression = require('../visitor/jsconfuser/string-compression') +const jcStringConceal = require('../visitor/jsconfuser/string-concealing') module.exports = function (code) { let ast @@ -1709,32 +23,35 @@ module.exports = function (code) { return null } // AntiTooling - traverse(ast, deAntiTooling) + traverse(ast, jcAntiTooling) // Minify - traverse(ast, deMinifyArrow) + const jcMinify = jcMinifyInit() + traverse(ast, jcMinify.deMinifyArrow) // DuplicateLiteralsRemoval - traverse(ast, deDuplicateLiteral) + traverse(ast, jcDuplicateLiteral) // Stack - traverse(ast, deStackFuncLen) - traverse(ast, deStackFuncOther) + const jcStack = jcStackInit(jcMinify.arrowFunc) + traverse(ast, jcStack.deStackFuncLen) + traverse(ast, jcStack.deStackFuncOther) // StringCompression - traverse(ast, deStringCompression) + traverse(ast, jcStringCompression) // StringConcealing - traverse(ast, deStringConcealing) - traverse(ast, deStringConcealingPlace) + traverse(ast, jcStringConceal.deStringConcealing) + traverse(ast, jcStringConceal.deStringConcealingPlace) // StringSplitting traverse(ast, calculateConstantExp) // Stack (run again) - traverse(ast, deStackFuncOther) + traverse(ast, jcStack.deStackFuncOther) // OpaquePredicates - traverse(ast, deOpaquePredicates) + traverse(ast, jcOpaquePredicates) traverse(ast, calculateConstantExp) traverse(ast, pruneIfBranch) // GlobalConcealing - traverse(ast, deGlobalConcealing) + traverse(ast, jcGlobalConcealing) // ControlFlowFlattening - traverse(ast, deControlFlowFlatteningStateless) + traverse(ast, jcControlFlow.deControlFlowFlatteningStateless) traverse(ast, calculateConstantExp) + // ExpressionObfuscation code = generator(ast, { comments: false, jsescOption: { minimal: true }, diff --git a/src/utility/check-func.js b/src/utility/check-func.js new file mode 100644 index 0000000..3f047f1 --- /dev/null +++ b/src/utility/check-func.js @@ -0,0 +1,15 @@ +function checkPattern(code, pattern) { + let i = 0 + let j = 0 + while (i < code.length && j < pattern.length) { + if (code[i] == pattern[j]) { + ++j + } + ++i + } + return j == pattern.length +} + +module.exports = { + checkPattern, +} diff --git a/src/utility/safe-func.js b/src/utility/safe-func.js new file mode 100644 index 0000000..d5183b8 --- /dev/null +++ b/src/utility/safe-func.js @@ -0,0 +1,72 @@ +const t = require('@babel/types') + +function safeDeleteNode(name, path) { + let binding + if (path.isFunctionDeclaration()) { + binding = path.parentPath.scope.getBinding(name) + } else { + binding = path.scope.getBinding(name) + } + if (!binding) { + return false + } + binding.scope.crawl() + binding = binding.scope.getBinding(name) + if (binding.references) { + return false + } + for (const item of binding.constantViolations) { + item.remove() + } + const decl = binding.path + if (decl.removed) { + return true + } + if (!decl.isVariableDeclarator() && !decl.isFunctionDeclaration()) { + return true + } + binding.path.remove() + return true +} + +function safeGetLiteral(path) { + if (path.isUnaryExpression()) { + if (path.node.operator === '-' && path.get('argument').isNumericLiteral()) { + return -1 * path.get('argument').node.value + } + return null + } + if (path.isLiteral()) { + return path.node.value + } + return null +} + +function safeGetName(path) { + if (path.isIdentifier()) { + return path.node.name + } + if (path.isLiteral()) { + return path.node.value + } + return null +} + +function safeReplace(path, value) { + if (typeof value === 'string') { + path.replaceWith(t.stringLiteral(value)) + return + } + if (typeof value === 'number') { + path.replaceWith(t.numericLiteral(value)) + return + } + path.replaceWithSourceString(value) +} + +module.exports = { + safeDeleteNode, + safeGetLiteral, + safeGetName, + safeReplace, +} diff --git a/src/visitor/jsconfuser/anti-tooling.js b/src/visitor/jsconfuser/anti-tooling.js new file mode 100644 index 0000000..3dd9efb --- /dev/null +++ b/src/visitor/jsconfuser/anti-tooling.js @@ -0,0 +1,53 @@ +const t = require('@babel/types') + +function deAntiToolingCheckFunc(path) { + if (path.node.params.length) { + return false + } + const body = path.node.body + if (!t.isBlockStatement(body)) { + return false + } + if (body.body.length) { + return false + } + return true +} + +function deAntiToolingExtract(path, func_name) { + let binding = path.scope.getBinding(func_name) + for (let ref of binding.referencePaths) { + if (!ref.parentPath.isCallExpression() || !ref.key === 'callee') { + continue + } + const call = ref.parentPath + if (!call.listKey === 'body') { + continue + } + for (let node of call.node.arguments) { + call.insertBefore(node) + } + call.remove() + } + binding.scope.crawl() + binding = path.scope.getBinding(func_name) + if (binding.references === 0) { + path.remove() + } +} + +const deAntiTooling = { + FunctionDeclaration(path) { + const func_name = path.node.id?.name + if (!func_name) { + return + } + if (!deAntiToolingCheckFunc(path)) { + return + } + console.log(`AntiTooling Func Name: ${func_name}`) + deAntiToolingExtract(path, func_name) + }, +} + +module.exports = deAntiTooling diff --git a/src/visitor/jsconfuser/control-flow.js b/src/visitor/jsconfuser/control-flow.js new file mode 100644 index 0000000..1be1042 --- /dev/null +++ b/src/visitor/jsconfuser/control-flow.js @@ -0,0 +1,182 @@ +const safeFunc = require('../../utility/safe-func') +const safeGetLiteral = safeFunc.safeGetLiteral +const safeGetName = safeFunc.safeGetName +const safeReplace = safeFunc.safeReplace + +function checkControlVar(path) { + const parent = path.parentPath + if (path.key !== 'right' || !parent.isAssignmentExpression()) { + return false + } + const var_path = parent.get('left') + const var_name = var_path.node?.name + if (!var_name) { + return false + } + let root_path = parent.parentPath + if (root_path.isExpressionStatement) { + root_path = root_path.parentPath + } + const binding = parent.scope.getBinding(var_name) + for (const ref of binding.referencePaths) { + if (ref === var_path) { + continue + } + let cur = ref + let valid = false + while (cur && cur !== root_path) { + if (cur.isSwitchCase() || cur === path) { + valid = true + break + } + cur = cur.parentPath + } + if (!valid) { + return false + } + if (ref.key === 'object') { + const prop = ref.parentPath.get('property') + if (!prop.isLiteral() && !prop.isIdentifier()) { + return false + } + continue + } + if (ref.key === 'right') { + const left = ref.parentPath.get('left') + if (!left.isMemberExpression()) { + return false + } + const obj = safeGetName(left.get('object')) + if (obj !== var_name) { + return false + } + continue + } + } + return true +} + +/** + * Process the constant properties in the controlVar + * + * Template: + * ```javascript + * controlVar = { + * // strings + * key_string: 'StringLiteral', + * // numbers + * key_number: 'NumericLiteral', + * } + * ``` + * + * Some kinds of deadCode may in inserted to the fake chunks: + * + * ```javascript + * controlVar = false + * controlVar = undefined + * controlVar[randomControlKey] = undefined + * delete controlVar[randomControlKey] + * ``` + */ +const deControlFlowFlatteningStateless = { + ObjectExpression(path) { + if (!checkControlVar(path)) { + return + } + const parent = path.parentPath + const var_name = parent.get('left').node?.name + console.log(`[ControlFlowFlattening] parse stateless in obj: ${var_name}`) + const props = {} + const prop_num = path.node.properties.length + for (let i = 0; i < prop_num; ++i) { + const prop = path.get(`properties.${i}`) + const key = safeGetName(prop.get('key')) + const value = safeGetLiteral(prop.get('value')) + if (!key || !value) { + continue + } + props[key] = value + } + const binding = parent.scope.getBinding(var_name) + for (const ref of binding.referencePaths) { + if (ref.key !== 'object') { + continue + } + const prop = safeGetName(ref.parentPath.get('property')) + if (!prop) { + continue + } + if (!Object.prototype.hasOwnProperty.call(props, prop)) { + continue + } + const upper = ref.parentPath + if (upper.key === 'left' && upper.parentPath.isAssignmentExpression()) { + // this is in the fake chunk + ref.parentPath.parentPath.remove() + continue + } + safeReplace(ref.parentPath, props[prop]) + } + binding.scope.crawl() + }, +} + +/** + * + * Template: + * ```javascript + * flaggedLabels = { + * currentLabel: { flagKey: 'xxx', flagValue : 'true or false' } + * } + * labelToStates[chunk[i].label] = stateValues: [] => caseStates[i] + * initStateValues = labelToStates[startLabel] + * endState + * chunks = [ + * { + * body: [ + * { + * type: "GotoStatement", + * label: "END_LABEL", + * } + * ], + * } + * { + * label: "END_LABEL", + * body: [], + * } + * ] + * while (stateVars) { + * switch (stateVars) { + * // fake assignment expression + * case fake_assignment: { + * stateVar = 'rand' + * // 'GotoStatement label' + * } + * // clone chunks + * case fake_clone: { + * // contain a real chunk + * } + * // fake jumps + * case real_1: { + * if (false) { + * // 'GotoStatement label' + * } + * // follow with real statements + * } + * } + * } + * The key may exist in its parent's map + * ``` + */ +const deControlFlowFlatteningState = { + ObjectExpression(path) { + if (!checkControlVar(path)) { + return + } + }, +} + +module.exports = { + deControlFlowFlatteningStateless, + deControlFlowFlatteningState, +} diff --git a/src/visitor/jsconfuser/duplicate-literal.js b/src/visitor/jsconfuser/duplicate-literal.js new file mode 100644 index 0000000..c46cd28 --- /dev/null +++ b/src/visitor/jsconfuser/duplicate-literal.js @@ -0,0 +1,120 @@ +const generator = require('@babel/generator').default +const t = require('@babel/types') + +const ivm = require('isolated-vm') +const isolate = new ivm.Isolate() + +const safeFunc = require('../../utility/safe-func') +const safeReplace = safeFunc.safeReplace + +function checkArrayName(path) { + if (path.key !== 'argument') { + return null + } + const ret_path = path.parentPath + if (!ret_path.isReturnStatement() || ret_path.key !== 0) { + return null + } + const array_fn_path = ret_path.getFunctionParent() + const array_fn_name = array_fn_path.node.id?.name + if (!array_fn_name) { + return null + } + const binding = array_fn_path.parentPath.scope.bindings[array_fn_name] + if (binding.references !== 1) { + return null + } + let ref = binding.referencePaths[0] + while (ref && !ref.isAssignmentExpression()) { + ref = ref.parentPath + } + if (!ref) { + return null + } + const array_name = ref.node.left?.name + if (!array_name) { + return null + } + return { + func_name: array_fn_name, + func_path: array_fn_path, + array_name: array_name, + array_path: ref, + } +} + +function parseArrayWarp(vm, path) { + let func = path.getFunctionParent(path) + let name = null + let binding = null + if (func.isArrowFunctionExpression()) { + func = func.parentPath + name = func.node.id.name + binding = func.scope.getBinding(name) + } else { + name = func.node.id.name + binding = func.parentPath.scope.getBinding(name) + } + console.log(`Process array warp function: ${name}`) + vm.evalSync(generator(func.node).code) + for (const ref of binding.referencePaths) { + const call = ref.parentPath + if (ref.key !== 'callee') { + console.warn(`Unexpected ref of array warp function: ${call}`) + continue + } + const value = vm.evalSync(generator(call.node).code) + safeReplace(call, value) + } + binding.scope.crawl() + binding = binding.scope.getBinding(name) + if (!binding.references) { + func.remove() + } +} + +/** + * Template: + * ```javascript + * var arrayName = getArrayFn() + * function getArrayFn (){ + * return [...arrayExpression] + * } + * ``` + */ +const deDuplicateLiteral = { + ArrayExpression(path) { + let obj = checkArrayName(path) + if (!obj) { + return + } + console.log(`Find arrayName: ${obj.array_name}`) + let decl_node = t.variableDeclarator( + obj.array_path.node.left, + obj.array_path.node.right + ) + decl_node = t.variableDeclaration('var', [decl_node]) + const code = [generator(obj.func_path.node).code, generator(decl_node).code] + let binding = obj.array_path.scope.getBinding(obj.array_name) + for (const ref of binding.referencePaths) { + const vm = isolate.createContextSync() + vm.evalSync(code[0]) + vm.evalSync(code[1]) + parseArrayWarp(vm, ref) + } + binding.scope.crawl() + binding = binding.scope.bindings[obj.array_name] + if (!binding.references) { + obj.array_path.remove() + binding.path.remove() + } + binding = obj.func_path.parentPath.scope.getBinding(obj.func_name) + binding.scope.crawl() + binding = binding.scope.getBinding(obj.func_name) + if (!binding.references) { + obj.func_path.remove() + } + }, +} + +module.exports = deDuplicateLiteral diff --git a/src/visitor/jsconfuser/global-concealing.js b/src/visitor/jsconfuser/global-concealing.js new file mode 100644 index 0000000..84a7a10 --- /dev/null +++ b/src/visitor/jsconfuser/global-concealing.js @@ -0,0 +1,129 @@ +const generator = require('@babel/generator').default +const t = require('@babel/types') + +const findGlobalFn = require('./global') +const safeFunc = require('../../utility/safe-func') +const safeDeleteNode = safeFunc.safeDeleteNode +const checkFunc = require('../../utility/check-func') +const checkPattern = checkFunc.checkPattern + +function findGlobalVar(glo_name, glo_path) { + let tmp_path = glo_path.parentPath.getFunctionParent() + if ( + !tmp_path || + !tmp_path.parentPath.isMemberExpression() || + !tmp_path.parentPath.parentPath.isCallExpression() + ) { + return null + } + const tmp_body = tmp_path.node.body.body + tmp_path = tmp_path.parentPath.parentPath + const ret_node = tmp_body[tmp_body.length - 1] + if ( + !t.isReturnStatement(ret_node) || + !t.isAssignmentExpression(ret_node.argument) + ) { + return null + } + const code = generator(ret_node.argument.right).code + const template = `${glo_name}call(this)` + if (!checkPattern(code, template)) { + return null + } + const glo_var = ret_node.argument.left.name + const binding = glo_path.scope.getBinding(glo_var) + for (const ref of binding.referencePaths) { + if ( + !ref.parentPath.isMemberExpression() || + !ref.parentPath.parentPath.isReturnStatement() + ) { + continue + } + const func_path = ref.getFunctionParent() + const func_name = func_path.node.id.name + return { + glo_var: glo_var, + tmp_path: tmp_path, + glo_fn_name: func_name, + glo_fn_path: func_path, + } + } + return null +} + +function getGlobalConcealingNames(glo_fn_path) { + const obj = {} + glo_fn_path.traverse({ + SwitchCase(path) { + const key = parseInt(generator(path.node.test).code) + let consequent = path.node.consequent[0] + if (t.isReturnStatement(consequent)) { + obj[key] = consequent.argument.property.value + } else { + if (t.isExpressionStatement(consequent)) { + consequent = consequent.expression + } + obj[key] = consequent.right.left.value + } + }, + }) + return obj +} + +/** + * Hide the global vars found by module GlobalAnalysis + * + * Template: + * ```javascript + * // Add to head: + * var globalVar, tempVar = function () { + * getGlobalVariableFnName = createGetGlobalTemplate() + * return globalVar = getGlobalVariableFnName.call(this) + * }["call"]() + * // Add to foot: + * function globalFn (indexParamName) { + * var returnName + * switch (indexParamName) { + * case state_x: { + * return globalVar[name] + * } + * case state_y: { + * returnName = name || globalVar[name] + * break + * } + * } + * return globalVar[returnName] + * } + * // References: + * // name -> globalFn(state) + * ``` + */ +const deGlobalConcealing = { + FunctionDeclaration(path) { + const glo_obj = findGlobalFn(path) + if (!glo_obj) { + return null + } + const obj = findGlobalVar(glo_obj.glo_fn_name, glo_obj.glo_fn_path) + if (!obj) { + return null + } + console.log(`[GlobalConcealing] globalVar: ${obj.glo_var}`) + const glo_vars = getGlobalConcealingNames(obj.glo_fn_path) + console.log(`[GlobalConcealing] globalFn: ${obj.glo_fn_name}`) + let binding = obj.glo_fn_path.parentPath.scope.getBinding(obj.glo_fn_name) + for (const ref of binding.referencePaths) { + const repl_path = ref.parentPath + if (ref.key !== 'callee' || !repl_path.isCallExpression()) { + continue + } + const key = parseInt(generator(repl_path.node.arguments[0]).code) + repl_path.replaceWith(t.identifier(glo_vars[key])) + } + if (safeDeleteNode(obj.glo_fn_name, obj.glo_fn_path)) { + obj.tmp_path.remove() + } + }, +} + +module.exports = deGlobalConcealing diff --git a/src/visitor/jsconfuser/global.js b/src/visitor/jsconfuser/global.js new file mode 100644 index 0000000..46ec263 --- /dev/null +++ b/src/visitor/jsconfuser/global.js @@ -0,0 +1,83 @@ +const generator = require('@babel/generator').default +const t = require('@babel/types') + +const safeFunc = require('../../utility/safe-func') +const safeGetName = safeFunc.safeGetName +const checkFunc = require('../../utility/check-func') +const checkPattern = checkFunc.checkPattern + +/** + * GlobalTemplate 1 (currently not support): + * ```javascript + * function {getGlobalFnName}(){ + * var localVar = false; + * eval(${transform.jsConfuserVar("localVar")} + " = true") + * if (!localVar) { + * {countermeasures} + * } + * const root = eval("this"); + * return root; + * } + * ``` + * GlobalTemplate 2: + * ```javascript + * function {getGlobalFnName}(array = [a, b, c, d]){ + * var bestMatch + * var itemsToSearch = [] + * try { + * bestMatch = Object + * itemsToSearch["push"](("")["__proto__"]["constructor"]["name"]) + * } catch(e) { + * } + * // ... + * return bestMatch || this; + * } + * ``` + */ +function findGlobalFn(path) { + const glo_fn_name = path.node.id?.name + if (!glo_fn_name) { + return null + } + let node = path.node.params?.[0] + if ( + !node || + !t.isAssignmentPattern(node) || + !t.isArrayExpression(node.right) || + node.right.elements.length !== 4 + ) { + return null + } + const array_name = node.left.name + const code = generator(path.node.body).code + const template = + 'try{=Objectpush(__proto__constructorname)}catch{}' + + `:for(;<${array_name}length;)try{=${array_name}[]()` + + 'for()if(typeof)continue}catch{}return||this' + if (!checkPattern(code, template)) { + return + } + const deps = [] + const array = path.get('params.0.right') + for (let i = 0; i < 4; ++i) { + const ele_name = safeGetName(array.get(`elements.${i}`)) + const binding = path.scope.getBinding(ele_name) + deps.push({ + name: ele_name, + path: binding.path, + pos: binding.path.node.start, + }) + } + deps.push({ + name: glo_fn_name, + path: path, + pos: path.node.start, + }) + return { + glo_fn_name: glo_fn_name, + glo_fn_path: path, + deps: deps, + } +} + +module.exports = findGlobalFn diff --git a/src/visitor/jsconfuser/minify.js b/src/visitor/jsconfuser/minify.js new file mode 100644 index 0000000..378953c --- /dev/null +++ b/src/visitor/jsconfuser/minify.js @@ -0,0 +1,92 @@ +const generator = require('@babel/generator').default + +function checkArrowWrap(path) { + if (path.node?.name !== 'arguments') { + return null + } + if (!path.parentPath.isSpreadElement()) { + return null + } + const call = path.parentPath.parentPath + if (path.parentPath.listKey !== 'arguments' || !call.isCallExpression()) { + return null + } + if (call.key !== 'argument' || !call.parentPath.isReturnStatement()) { + return null + } + const func_name = call.node.callee?.name + if (!func_name) { + return null + } + let wrap = call.getFunctionParent() + if (wrap.key !== 'init') { + return null + } + wrap = wrap.parentPath + const wrap_name = wrap.node.id?.name + wrap = wrap.parentPath + if ( + wrap.listKey !== 'body' || + wrap.key !== 0 || + wrap.container.length !== 2 + ) { + return null + } + const str = generator(wrap.container[1]).code + if (str.indexOf(wrap_name) === -1) { + return null + } + wrap = wrap.getFunctionParent() + const arrow_name = wrap.node?.id?.name + if (!arrow_name || wrap.node.params?.[0]?.name !== func_name) { + return null + } + return { + name: arrow_name, + path: wrap, + } +} + +/** + * Template: + * ```javascript + * function arrowFunctionName (arrowFn, functionLength = 0){ + * var functionObject = function(){ return arrowFn(...arguments) }; + * return Object.defineProperty(functionObject, "length", { + * "value": functionLength, + * "configurable": true + * }); + * } + * ``` + */ +module.exports = function () { + let arrowFunc = null + const deMinifyArrow = { + Identifier(path) { + let obj = checkArrowWrap(path) + if (!obj) { + return + } + arrowFunc = obj.name + console.log(`Find arrowFunctionName: ${obj.name}`) + let binding = obj.path.parentPath.scope.bindings[obj.name] + for (const ref of binding.referencePaths) { + if (ref.key !== 'callee') { + console.warn(`Unexpected ref of arrowFunctionName: ${obj.name}`) + continue + } + const repl_path = ref.parentPath + repl_path.replaceWith(repl_path.node.arguments[0]) + } + binding.scope.crawl() + binding = obj.path.parentPath.scope.bindings[obj.name] + if (!binding.references) { + obj.path.remove() + } + }, + } + return { + arrowFunc, + deMinifyArrow, + } +} diff --git a/src/visitor/jsconfuser/opaque-predicates.js b/src/visitor/jsconfuser/opaque-predicates.js new file mode 100644 index 0000000..de79387 --- /dev/null +++ b/src/visitor/jsconfuser/opaque-predicates.js @@ -0,0 +1,155 @@ +const generator = require('@babel/generator').default +const t = require('@babel/types') + +const ivm = require('isolated-vm') +const isolate = new ivm.Isolate() + +const safeFunc = require('../../utility/safe-func') +const safeDeleteNode = safeFunc.safeDeleteNode +const safeGetName = safeFunc.safeGetName +const safeReplace = safeFunc.safeReplace +const checkFunc = require('../../utility/check-func') +const checkPattern = checkFunc.checkPattern + +function checkOpaqueObject(path) { + const parent = path.parentPath + if (!parent.isAssignmentExpression()) { + return null + } + const tmp_name = safeGetName(parent.get('left')) + const func_path = parent.getFunctionParent() + if ( + !func_path || + func_path.key !== 'callee' || + !func_path.parentPath.isCallExpression() + ) { + return null + } + const func_body = func_path.node.body?.body + if (!func_body || func_body.length < 2) { + return null + } + const last_node = func_body[func_body.length - 1] + if ( + !t.isReturnStatement(last_node) || + last_node.argument?.name !== tmp_name + ) { + return null + } + const root_path = func_path.parentPath.parentPath + if (!root_path.isAssignmentExpression()) { + return null + } + const pred_name = safeGetName(root_path.get('left')) + const obj = { + pred_name: pred_name, + pred_path: root_path, + props: {}, + } + for (const prop of path.node.properties) { + const key = prop.key.name + const value = prop.value + if (t.isNumericLiteral(value)) { + obj.props[key] = { + type: 'number', + } + continue + } + if (t.isStringLiteral(value)) { + obj.props[key] = { + type: 'string', + } + continue + } + if (t.isArrayExpression(value)) { + if (value.elements.length === 0) { + obj.props[key] = { + type: 'array_dep', + } + } + continue + } + if (t.isArrowFunctionExpression(value) || t.isFunctionExpression(value)) { + const param = value.params?.[0]?.left?.name + if (!param) { + continue + } + const code = generator(value).code + const template = + `(${param}=){if(${pred_name}[0])${pred_name}push()` + + `return${pred_name}${param}}` + if (checkPattern(code, template)) { + obj.props[key] = { + type: 'array', + } + } + continue + } + } + return obj +} + +/** + * Template: + * ```javascript + * // This is defined in the global space + * var predicateName = (function () { + * var tempName = { + * prop_array_1: [], + * prop_array: function (paramName = 'length') { + * if (!predicateName[prop_array_1][0]) { + * predicateName[prop_array_1][0].push(rand1) + * } + * return predicateName[prop_array_1][paramName] + * }, + * prop_number: rand2, + * prop_string: rand_str, + * } + * return tempName + * })() + * // Below will appear multiple times + * predicateName[prop_array]() ? test : fake + * predicateName[prop_number] > rand3 ? test : fake + * predicateName[prop_string].charAt(index) == real_char ? test : fake + * predicateName[prop_string].charCodeAt(index) == real_char ? test : fake + * ``` + */ +const deOpaquePredicates = { + ObjectExpression(path) { + const obj = checkOpaqueObject(path) + if (!obj) { + return + } + console.log(`[OpaquePredicates] predicateName : ${obj.pred_name}`) + const vm = isolate.createContextSync() + const code = generator(obj.pred_path.node).code + vm.evalSync('var ' + code) + obj.pred_path.get('right').replaceWith(t.numericLiteral(0)) + let binding = obj.pred_path.scope.getBinding(obj.pred_name) + binding.scope.crawl() + binding = binding.scope.getBinding(obj.pred_name) + for (const ref of binding.referencePaths) { + if (ref.key !== 'object') { + continue + } + const member = ref.parentPath + const prop = member.get('property') + if (!prop || !Object.prototype.hasOwnProperty.call(obj.props, prop)) { + continue + } + let expr = member + while ( + expr.parentPath.isCallExpression() || + expr.parentPath.isMemberExpression() + ) { + expr = expr.parentPath + } + const test = generator(expr.node).code + const res = vm.evalSync(test) + safeReplace(expr, res) + } + safeDeleteNode(obj.pred_name, obj.pred_path) + }, +} + +module.exports = deOpaquePredicates diff --git a/src/visitor/jsconfuser/stack.js b/src/visitor/jsconfuser/stack.js new file mode 100644 index 0000000..54871ca --- /dev/null +++ b/src/visitor/jsconfuser/stack.js @@ -0,0 +1,335 @@ +const generator = require('@babel/generator').default +const t = require('@babel/types') + +const ivm = require('isolated-vm') +const isolate = new ivm.Isolate() + +const calculateConstantExp = require('../calculate-constant-exp') + +const safeFunc = require('../../utility/safe-func') +const safeGetName = safeFunc.safeGetName +const safeReplace = safeFunc.safeReplace + +let arrowFunc = null + +function checkFuncLen(path) { + if (path.node?.name !== 'configurable' || path.key !== 'key') { + return null + } + const prop = path.parentPath + if (!prop.isObjectProperty() || prop.key !== 1) { + return null + } + const obj = prop.parentPath + if (obj.node.properties.length !== 2) { + return null + } + if (obj.node.properties[0]?.key?.name !== 'value') { + return null + } + if (obj.listKey !== 'arguments' || obj.key !== 2) { + return null + } + const func_name = obj.container[0]?.name + const warp = obj.getFunctionParent() + if (warp.node.params?.[0]?.name !== func_name) { + return null + } + const func_len_name = warp.node?.id?.name + if (!func_len_name || func_len_name === arrowFunc) { + return null + } + return { + name: func_len_name, + path: warp, + } +} + +/** + * type: param, value, ref, invalid + */ +function initStackCache(len) { + const cache = {} + for (let i = 0; i < len; ++i) { + cache[i] = { + type: 'param', + } + } + return cache +} + +function processAssignLeft(vm, cache, path, prop_name, stk_name) { + const father = path.parentPath + const right = father.get('right') + if (right.isBinaryExpression()) { + cache[prop_name] = { + type: 'invalid', + } + return + } + if (right.isLiteral()) { + vm.evalSync(generator(father.node).code) + cache[prop_name] = { + type: 'value', + value: right.node, + } + return + } + if (right.isArrayExpression()) { + const elements = right.node.elements + if (elements.length === 1 && elements[0]?.value === 'charCodeAt') { + cache[prop_name] = { + type: 'value', + value: right.node, + } + return + } + } + if (right.isUnaryExpression() && right.node.operator === '-') { + vm.evalSync(generator(father.node).code) + cache[prop_name] = { + type: 'value', + value: right.node, + } + return + } + if (right.isMemberExpression() && right.node.object?.name === stk_name) { + const right_prop = right.get('property') + if (right_prop.isBinaryExpression()) { + return + } + let ref = safeGetName(right_prop) + if (!Object.prototype.hasOwnProperty.call(cache, ref)) { + cache[prop_name] = { + type: 'invalid', + } + return + } + while (cache[ref].type === 'ref') { + ref = cache[ref].value + } + if (cache[ref].type === 'value') { + safeReplace(right, cache[ref].value) + vm.evalSync(generator(father.node).code) + cache[prop_name] = { + type: 'value', + value: cache[ref].value, + } + } else { + cache[prop_name] = { + type: 'ref', + value: ref, + } + } + return + } + cache[prop_name] = { + type: 'invalid', + } +} + +function processAssignInvalid(cache, path, prop_name) { + cache[prop_name] = { + type: 'invalid', + } +} + +function processReplace(cache, path, prop_name) { + const value = cache[prop_name].value + const type = cache[prop_name].type + if (type === 'ref') { + path.node.computed = true + safeReplace(path.get('property'), value) + return true + } + if (type === 'value') { + path.replaceWith(value) + return true + } + return false +} + +function checkStackInvalid(path) { + const stk_name = path.node.params[0].argument.name + const body_path = path.get('body') + const obj = {} + body_path.traverse({ + MemberExpression: { + exit(path) { + if (path.node.object.name !== stk_name) { + return + } + const father = path.parentPath + const prop = path.get('property') + const prop_name = safeGetName(prop) + if (father.isUpdateExpression()) { + obj[prop_name] = 1 + return + } + if (body_path.scope == father.scope) { + return + } + if (!father.isAssignmentExpression() || path.key !== 'left') { + return + } + obj[prop_name] = 1 + }, + }, + }) + return obj +} + +function tryStackReplace(path, len, invalid) { + const stk_name = path.node.params[0].argument.name + const body_path = path.get('body') + const cache = initStackCache(len) + const vm = isolate.createContextSync() + vm.evalSync(`var ${stk_name} = []`) + let changed = false + body_path.traverse({ + MemberExpression: { + exit(path) { + if (path.node.object.name !== stk_name) { + return + } + const prop = path.get('property') + if (prop.isBinaryExpression()) { + return + } + const prop_name = safeGetName(prop) + if (!prop_name) { + return + } + if (Object.prototype.hasOwnProperty.call(invalid, prop_name)) { + processAssignInvalid(cache, path, prop_name) + return + } + const exist = Object.prototype.hasOwnProperty.call(cache, prop_name) + if (exist && cache[prop_name].type === 'param') { + return + } + const father = path.parentPath + if (father.isAssignmentExpression() && path.key === 'left') { + processAssignLeft(vm, cache, path, prop_name, stk_name) + } else if (exist) { + changed |= processReplace(cache, path, prop_name) + } + }, + }, + }) + const binding = body_path.scope.getBinding(stk_name) + binding.scope.crawl() + return changed +} + +function getStackParamLen(path) { + const stk_name = path.node.params?.[0]?.argument?.name + if (!stk_name) { + return 'unknown' + } + const body_path = path.get('body') + let len = 'unknown' + body_path.traverse({ + MemberExpression: { + exit(path) { + if (path.node.object.name !== stk_name) { + return + } + const prop = path.get('property') + if (prop.isBinaryExpression()) { + return + } + const prop_name = safeGetName(prop) + if (!prop_name || prop_name !== 'length') { + return + } + const father = path.parentPath + if (!father.isAssignmentExpression() || path.key !== 'left') { + return + } + const right = father.get('right') + if (right.isBinaryExpression()) { + return + } + if (!right.isLiteral()) { + return + } + len = right.node.value + path.stop() + }, + }, + }) + return len +} + +function processStackParam(path, len) { + if (path.isArrowFunctionExpression()) { + console.log(`Process arrowFunctionExpression, len: ${len}`) + } else if (path.isFunctionExpression()) { + console.log(`Process functionExpression, len: ${len}`) + } else { + console.log(`Process Function ${path.node.id.name}, len: ${len}`) + } + let changed = true + const invalid = checkStackInvalid(path) + while (changed) { + changed = tryStackReplace(path, len, invalid) + path.traverse(calculateConstantExp) + } +} + +const deStackFuncLen = { + Identifier(path) { + let obj = checkFuncLen(path) + if (!obj) { + return + } + console.log(`Find functionLengthName: ${obj.name}`) + let binding = obj.path.parentPath.scope.bindings[obj.name] + for (const ref of binding.referencePaths) { + if (ref.key !== 'callee') { + console.warn(`Unexpected ref of functionLengthName: ${obj.name}`) + continue + } + const repl_path = ref.parentPath + const arg = repl_path.node.arguments[0] + const len = repl_path.node.arguments[1].value + if (t.isIdentifier(arg)) { + const func_name = arg.name + const func_decl = repl_path.scope.getBinding(func_name).path + processStackParam(func_decl, len) + repl_path.remove() + } else { + repl_path.replaceWith(arg) + processStackParam(repl_path, len) + } + } + binding.scope.crawl() + binding = obj.path.parentPath.scope.bindings[obj.name] + if (!binding.references) { + obj.path.remove() + } + }, +} + +const deStackFuncOther = { + RestElement(path) { + if (path.listKey !== 'params') { + return + } + const func = path.getFunctionParent() + const len = getStackParamLen(func) + if (len === 'unknown') { + return + } + processStackParam(func, len) + }, +} + +module.exports = function (func) { + arrowFunc = func + return { + deStackFuncLen, + deStackFuncOther, + } +} diff --git a/src/visitor/jsconfuser/string-compression.js b/src/visitor/jsconfuser/string-compression.js new file mode 100644 index 0000000..c4e6b93 --- /dev/null +++ b/src/visitor/jsconfuser/string-compression.js @@ -0,0 +1,182 @@ +const generator = require('@babel/generator').default + +const ivm = require('isolated-vm') +const isolate = new ivm.Isolate() + +const safeFunc = require('../../utility/safe-func') +const safeReplace = safeFunc.safeReplace +const checkFunc = require('../../utility/check-func') +const checkPattern = checkFunc.checkPattern + +function findStringDecoder(path) { + if (path.node?.name !== 'charCodeAt' || path.key !== 'property') { + return null + } + let loop = path + while (loop && !loop.isForStatement()) { + loop = loop.parentPath + } + const i = loop?.node?.update?.argument?.name + if (!i) { + return null + } + const func = loop.getFunctionParent() + const param = func.node.params?.[0]?.name + if (!param) { + return null + } + const code = generator(func.node).code + const template = + `function(${param}){var=${param}.split()for(${i}=1;${i}<.length;${i}++)` + + `[${i}].charCodeAt(0)[${i}].push().charAt(0)return.join().split()}` + if (!checkPattern(code, template)) { + return null + } + return { + name: func.node.id.name, + path: func, + } +} + +function findStringGet(path) { + const decoder_name = path.node.id.name + let binding = path.parentPath.scope.getBinding(decoder_name) + if (!binding || binding.references !== 1) { + return null + } + const ref = binding.referencePaths[0] + if (ref.key !== 1 || ref.listKey !== 'arguments') { + return null + } + const get_ref_path = ref.parentPath.get('arguments.0') + const get_name = get_ref_path.node?.name + if (!get_name) { + return null + } + binding = get_ref_path.scope.getBinding(get_name) + return { + name: get_name, + path: binding.path, + ref: get_ref_path, + } +} + +function findStringSplit(path) { + while (path && !path.isAssignmentExpression()) { + path = path.parentPath + } + const split_name = path?.node?.left?.name + if (!split_name) { + return null + } + const binding = path.scope.getBinding(split_name) + return { + name: split_name, + path: path, + def: binding.path, + } +} + +function findStringFn(path, name) { + const binding = path.scope.getBinding(name) + const ref = binding.referencePaths?.[0] + if (!ref) { + return null + } + const fn_path = ref.getFunctionParent(name) + const fn_name = fn_path.node.id.name + return { + name: fn_name, + path: fn_path, + } +} + +/** + * Template: + * ```javascript + * var split = (function (getStringParamName, decoderParamName) { + * return decoderParamName(getStringParamName()) + * })(getStringName, decoder) + * function getStringName () { + * var str = splits[0] + * var objectToTest = {} + * if ('testingFor' in objectToTest) { + * str += splits[1] + * } + * return str + * } + * function decoder (b) { + * // DecodeTemplate + * } + * function fnName (index) { + * return split[index] + * } + * ``` + */ +const deStringCompression = { + Identifier(path) { + const decoder_obj = findStringDecoder(path) + if (!decoder_obj) { + return + } + const get_obj = findStringGet(decoder_obj.path) + if (!get_obj) { + return + } + const split_obj = findStringSplit(get_obj.ref) + if (!get_obj) { + return + } + const fn_obj = findStringFn(split_obj.path, split_obj.name) + if (!get_obj) { + return + } + console.log(`Find stringCompression Fn: ${fn_obj.name}`) + const vm = isolate.createContextSync() + vm.evalSync(generator(decoder_obj.path.node).code) + vm.evalSync(generator(get_obj.path.node).code) + vm.evalSync('var ' + generator(split_obj.path.node).code) + vm.evalSync(generator(fn_obj.path.node).code) + let binding = fn_obj.path.parentPath.scope.getBinding(fn_obj.name) + for (const ref of binding.referencePaths) { + if (ref.key !== 'callee') { + console.warn( + `Unexpected ref of stringCompression Fn: ${ref.parentPath}` + ) + continue + } + const repl_path = ref.parentPath + try { + const value = vm.evalSync(generator(repl_path.node).code) + safeReplace(repl_path, value) + } catch (e) { + console.warn( + `Unexpected ref of stringCompression Fn: ${ref.parentPath}` + ) + } + } + binding.scope.crawl() + binding = binding.scope.bindings[fn_obj.name] + if (!binding.references) { + fn_obj.path.remove() + } + binding.scope.crawl() + binding = split_obj.path.scope.getBinding(split_obj.name) + if (!binding.references) { + split_obj.path.remove() + split_obj.def.remove() + } + binding.scope.crawl() + binding = get_obj.path.scope.getBinding(get_obj.name) + if (!binding.references) { + get_obj.path.remove() + } + binding.scope.crawl() + binding = decoder_obj.path.scope.getBinding(decoder_obj.name) + if (!binding.references) { + decoder_obj.path.remove() + } + }, +} + +module.exports = deStringCompression diff --git a/src/visitor/jsconfuser/string-concealing.js b/src/visitor/jsconfuser/string-concealing.js new file mode 100644 index 0000000..95f2c8d --- /dev/null +++ b/src/visitor/jsconfuser/string-concealing.js @@ -0,0 +1,447 @@ +const generator = require('@babel/generator').default +const t = require('@babel/types') + +const ivm = require('isolated-vm') +const isolate = new ivm.Isolate() + +const findGlobalFn = require('./global') +const safeFunc = require('../../utility/safe-func') +const safeDeleteNode = safeFunc.safeDeleteNode +const safeGetName = safeFunc.safeGetName +const safeReplace = safeFunc.safeReplace + +function insertDepItemVar(deps, name, path) { + const binding = path.scope.getBinding(name) + if (binding.path === path) { + deps.push({ + name: name, + path: binding.path, + node: t.variableDeclaration('var', [binding.path.node]), + pos: binding.path.node.start, + }) + return + } + deps.push({ + name: name, + path: path, + pos: path.node.start, + }) + deps.push({ + name: name, + path: binding.path, + node: t.variableDeclaration('var', [binding.path.node]), + pos: binding.path.node.start, + }) +} + +/** + * Template: + * ```javascript + * var __globalObject = {getGlobalFnName}() || {}; + * var __TextDecoder = __globalObject["TextDecoder"]; + * var __Uint8Array = __globalObject["Uint8Array"]; + * var __Buffer = __globalObject["Buffer"]; + * var __String = __globalObject["String"] || String; + * var __Array = __globalObject["Array"] || Array; + * ``` + */ +function findGlobalFnRef(obj) { + const path = obj.glo_fn_path + const glo_fn_name = obj.glo_fn_name + let binding = path.parentPath.scope.getBinding(glo_fn_name) + let glo_fn_ref = binding.referencePaths[0] + while (!glo_fn_ref.isAssignmentExpression()) { + glo_fn_ref = glo_fn_ref.parentPath + } + const glo_obj_name = glo_fn_ref.node.left.name + obj.glo_obj_name = glo_obj_name + obj.glo_obj_path = glo_fn_ref + obj.glo_obj_ref = {} + insertDepItemVar(obj.deps, glo_obj_name, glo_fn_ref) + binding = glo_fn_ref.scope.getBinding(glo_obj_name) + for (const ref of binding.referencePaths) { + const prop = safeGetName(ref.parentPath.get('property')) + if (!prop) { + continue + } + let root = ref + while (!root.isAssignmentExpression()) { + root = root.parentPath + } + const ref_name = safeGetName(root.get('left')) + obj.glo_obj_ref[prop] = ref_name + insertDepItemVar(obj.deps, ref_name, root) + } + return +} + +/** + * Template: + * ```javascript + * var utf8ArrayToStr = (function () { + * // ... + * })(); + * function bufferToStringName () { + * if(typeof __TextDecoder !== "undefined" && __TextDecoder) { + * return new __TextDecoder()["decode"](new __Uint8Array(buffer)); + * } else if(typeof __Buffer !== "undefined" && __Buffer) { + * return __Buffer["from"](buffer)["toString"]("utf-8"); + * } else { + * return utf8ArrayToStr(buffer); + * } + * } + * ``` + */ +function findBufferToString(obj) { + const path = obj.glo_obj_path + const ref_array = obj.glo_obj_ref['Array'] + let binding = path.scope.getBinding(ref_array) + for (const ref of binding.referencePaths) { + if (ref.key !== 'callee') { + continue + } + let a2s_path = ref.getFunctionParent() + while (!a2s_path.isAssignmentExpression()) { + a2s_path = a2s_path.parentPath + } + obj.a2s_name = safeGetName(a2s_path.get('left')) + obj.a2s_path = a2s_path + insertDepItemVar(obj.deps, obj.a2s_name, obj.a2s_path) + break + } + if (!obj.a2s_name) { + return false + } + binding = obj.a2s_path.scope.getBinding(obj.a2s_name) + const b2s_path = binding.referencePaths[0].getFunctionParent() + obj.b2s_name = safeGetName(b2s_path.get('id')) + obj.b2s_path = b2s_path + obj.deps.push({ + name: obj.b2s_name, + path: b2s_path, + pos: b2s_path.node.start, + }) + binding = b2s_path.parentPath.scope.getBinding(obj.b2s_name) + const child = [] + for (const ref of binding.referencePaths) { + const decode_fn = ref.getFunctionParent() + let valid = false + decode_fn.traverse({ + StringLiteral(path) { + if (path.node.value.length === 91) { + valid = true + path.stop() + } + }, + }) + if (!valid) { + return false + } + child.push({ + name: decode_fn.node.id.name, + decoder: decode_fn, + }) + } + obj.child = child + return true +} + +function generatorStringConcealingDepCode(obj) { + obj.deps.sort((a, b) => { + return a.pos - b.pos + }) + const dep_node = t.program([]) + for (const item of obj.deps) { + if (item.node) { + dep_node.body.push(item.node) + } else { + dep_node.body.push(item.path.node) + } + } + obj.dep_code = generator(dep_node).code +} + +function renameProperty(member) { + const obj_name = safeGetName(member.get('object')) + const prop_name = safeGetName(member.get('property')) + const new_name = member.scope.generateUidIdentifier(`_tmp_local_`) + const binding = member.scope.getBinding(obj_name) + let first = true + for (const ref of binding.referencePaths) { + const item = ref.parentPath + const prop = safeGetName(item.get('property')) + if (prop !== prop_name) { + continue + } + if (first) { + let body = item + while (body.listKey !== 'body') { + body = body.parentPath + } + body.container.unshift( + t.variableDeclaration('var', [t.variableDeclarator(new_name)]) + ) + body.scope.crawl() + first = false + } + item.replaceWith(new_name) + } + member.scope.crawl() +} + +/** + * Template: + * ```javascript + * var cacheName = [], arrayName = [] + * // Below will appear multiple times + * var getterFnName = (x, y, z, a, b)=>{ + * if ( x !== y ) { + * return b[x] || (b[x] = a(arrayName[x])) + * } + * // Add fake ifStatements + * if(typeof a === "undefined") { + * a = decodeFn + * } + * if(typeof b === "undefined") { + * b = cacheName + * } + * } + * // Base91 Algo + * function decodeFn (str){ + * var table = {__strTable__}; + * var raw = "" + (str || ""); + * var len = raw.length; + * var ret = []; + * var b = 0; + * var n = 0; + * var v = -1; + * for (var i = 0; i < len; i++) { + * var p = table.indexOf(raw[i]); + * if (p === -1) continue; + * if (v < 0) { + * v = p; + * } else { + * v += p * 91; + * b |= v << n; + * n += (v & 8191) > 88 ? 13 : 14; + * do { + * ret.push(b & 0xff); + * b >>= 8; + * n -= 8; + * } while (n > 7); + * v = -1; + * } + * } + * if (v > -1) { + * ret.push((b | (v << n)) & 0xff); + * } + * return bufferToStringName(ret); + * } + * ``` + */ +function processSingleGetter(obj, decoder_name, decoder_path) { + const decoder_code = generator(decoder_path.node).code + let binding = decoder_path.parentPath.scope.getBinding(decoder_name) + let getter_path = binding.referencePaths[0].getFunctionParent() + while ( + !getter_path.isAssignmentExpression() && + !getter_path.isVariableDeclarator() + ) { + getter_path = getter_path.parentPath + } + let getter_name + if (getter_path.isAssignmentExpression()) { + if (getter_path.get('left').isMemberExpression()) { + renameProperty(getter_path.get('left')) + } + getter_name = safeGetName(getter_path.get('left')) + } else { + getter_name = safeGetName(getter_path.get('id')) + } + console.log( + `[StringConcealing] getter: ${getter_name} decoder: ${decoder_name}` + ) + const getter_code = 'var ' + generator(getter_path.node).code + binding = getter_path.scope.getBinding(getter_name) + if (getter_path.isAssignmentExpression()) { + getter_path.get('right').replaceWith(t.numericLiteral(0)) + } else { + getter_path.get('init').replaceWith(t.numericLiteral(0)) + } + binding.scope.crawl() + binding = getter_path.scope.getBinding(getter_name) + let complete = false + while (!complete) { + complete = true + const vm = isolate.createContextSync() + vm.evalSync(obj.dep_code) + try { + for (const ref of binding.referencePaths) { + if (ref.findParent((path) => path.removed)) { + continue + } + let repl_path = ref.parentPath + if (repl_path.isCallExpression()) { + const args = repl_path.node.arguments + if (args.length !== 1 || !t.isLiteral(args[0])) { + console.warn(`[StringConcealing] Unexpected call: ${repl_path}`) + continue + } + } else if (repl_path.isMemberExpression()) { + repl_path = repl_path.parentPath + } else { + console.warn(`[StringConcealing] Unexpected ref: ${repl_path}`) + continue + } + const eval_code = generator(repl_path.node).code + // The name of getter can be the same as other dep functions + const value = vm.evalSync( + `(function (){${decoder_code}\n${getter_code}\nreturn ${eval_code}})()` + ) + safeReplace(repl_path, value) + } + } catch (e) { + if (e.name !== 'ReferenceError') { + console.warn(`[StringConcealing] Unexpected exception: ${e.message}`) + return + } + complete = false + const lost = e.message.split(' ')[0] + const binding = getter_path.scope.getBinding(lost) + if (!binding) { + console.warn(`[StringConcealing] Missing cache or array: ${lost}`) + return + } + let count = binding.constantViolations.length + if (count) { + console.warn(`[StringConcealing] Invalid violations ${lost} : ${count}`) + return + } + count = binding.path.node.init.elements.length + if (count) { + console.log(`[StringConcealing] Add array : ${lost}`) + obj.array_name = lost + obj.array_path = binding.path + } else { + console.log(`[StringConcealing] Add cache : ${lost}`) + obj.cache_name = lost + obj.cache_path = binding.path + } + insertDepItemVar(obj.deps, lost, binding.path) + generatorStringConcealingDepCode(obj) + } + } + safeDeleteNode(getter_name, getter_path) + safeDeleteNode(decoder_name, decoder_path) +} + +const deStringConcealing = { + FunctionDeclaration(path) { + const obj = findGlobalFn(path) + if (!obj) { + return null + } + if (obj.glo_fn_path.parentPath.getFunctionParent()) { + return null + } + findGlobalFnRef(obj) + if (!findBufferToString(obj)) { + return + } + generatorStringConcealingDepCode(obj) + for (const item of obj.child) { + processSingleGetter(obj, item.name, item.decoder) + } + safeDeleteNode(obj.array_name, obj.array_path) + safeDeleteNode(obj.cache_name, obj.cache_path) + // a2s and b2s are pairs + if (safeDeleteNode(obj.b2s_name, obj.b2s_path)) { + obj.a2s_path.remove() + obj.a2s_path.scope.crawl() + } + }, +} + +function tryStringConcealingPlace(path) { + const parent = path.parentPath + if (!parent.isAssignmentExpression()) { + return + } + const name = safeGetName(parent.get('left')) + let binding = parent.scope.getBinding(name) + if (binding?.constantViolations?.length !== 1) { + return + } + const code = generator(parent.node).code + const vm = isolate.createContextSync() + vm.evalSync('var ' + code) + for (const ref of binding.referencePaths) { + if (ref.key !== 'object') { + continue + } + const test = generator(ref.parent).code + const res = vm.evalSync(test) + safeReplace(ref.parentPath, res) + } + safeDeleteNode(name, parent) +} + +const deStringConcealingPlace = { + StringLiteral(path) { + if (path.key !== 'right' || !path.parentPath.isAssignmentExpression()) { + return + } + const name = safeGetName(path.parentPath.get('left')) + if (!name) { + return + } + const binding = path.scope.getBinding(name) + if (binding.constantViolations.length !== 1) { + return + } + for (const ref of binding.referencePaths) { + if (ref.node.start < path.node.start) { + continue + } + ref.replaceWith(path.node) + } + safeDeleteNode(name, path.parentPath) + }, + ArrayExpression(path) { + let valid = true + if (path.node.elements.length === 0) { + return + } + for (const ele of path.node.elements) { + if (!t.isStringLiteral(ele)) { + valid = false + break + } + } + if (!valid) { + return + } + tryStringConcealingPlace(path) + }, + ObjectExpression(path) { + let valid = true + if (path.node.properties.length === 0) { + return + } + for (const ele of path.node.properties) { + if (!t.isStringLiteral(ele.value)) { + valid = false + break + } + } + if (!valid) { + return + } + tryStringConcealingPlace(path) + }, +} + +module.exports = { + deStringConcealing, + deStringConcealingPlace, +} From 5c34906a88d87c0ef4e659d3e97a7c91aad1c33d Mon Sep 17 00:00:00 2001 From: echo094 <20028238+echo094@users.noreply.github.com> Date: Sun, 1 Dec 2024 12:43:34 +0800 Subject: [PATCH 19/21] fix bug --- src/visitor/jsconfuser/stack.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/visitor/jsconfuser/stack.js b/src/visitor/jsconfuser/stack.js index 54871ca..0d3431c 100644 --- a/src/visitor/jsconfuser/stack.js +++ b/src/visitor/jsconfuser/stack.js @@ -109,7 +109,7 @@ function processAssignLeft(vm, cache, path, prop_name, stk_name) { ref = cache[ref].value } if (cache[ref].type === 'value') { - safeReplace(right, cache[ref].value) + right.replaceWith(cache[ref].value) vm.evalSync(generator(father.node).code) cache[prop_name] = { type: 'value', From 73cfbcf43331f4aad7e072e3b4216202bc1c79a7 Mon Sep 17 00:00:00 2001 From: echo094 <20028238+echo094@users.noreply.github.com> Date: Sun, 1 Dec 2024 14:29:16 +0800 Subject: [PATCH 20/21] fix bug --- src/visitor/jsconfuser/stack.js | 51 +++++++++++++++------ src/visitor/jsconfuser/string-concealing.js | 5 ++ 2 files changed, 41 insertions(+), 15 deletions(-) diff --git a/src/visitor/jsconfuser/stack.js b/src/visitor/jsconfuser/stack.js index 0d3431c..4d8e96c 100644 --- a/src/visitor/jsconfuser/stack.js +++ b/src/visitor/jsconfuser/stack.js @@ -27,10 +27,14 @@ function checkFuncLen(path) { if (obj.node.properties[0]?.key?.name !== 'value') { return null } - if (obj.listKey !== 'arguments' || obj.key !== 2) { + if (obj.listKey !== 'arguments') { return null } - const func_name = obj.container[0]?.name + const arg_num = obj.container.length + if (obj.key !== arg_num - 1) { + return null + } + const func_name = obj.container[arg_num - 3]?.name const warp = obj.getFunctionParent() if (warp.node.params?.[0]?.name !== func_name) { return null @@ -149,10 +153,9 @@ function processReplace(cache, path, prop_name) { return false } -function checkStackInvalid(path) { +function checkStackInvalid(path, invalid) { const stk_name = path.node.params[0].argument.name const body_path = path.get('body') - const obj = {} body_path.traverse({ MemberExpression: { exit(path) { @@ -163,7 +166,7 @@ function checkStackInvalid(path) { const prop = path.get('property') const prop_name = safeGetName(prop) if (father.isUpdateExpression()) { - obj[prop_name] = 1 + invalid[prop_name] = 1 return } if (body_path.scope == father.scope) { @@ -172,14 +175,24 @@ function checkStackInvalid(path) { if (!father.isAssignmentExpression() || path.key !== 'left') { return } - obj[prop_name] = 1 + invalid[prop_name] = 1 }, }, }) - return obj + return invalid } -function tryStackReplace(path, len, invalid) { +function checkChangeValid(invalid, used) { + let valid = true + Object.keys(used).forEach(function(key) { + if (Object.prototype.hasOwnProperty.call(invalid, key)) { + valid = false + } + }) + return valid +} + +function tryStackReplace(path, len, invalid, used) { const stk_name = path.node.params[0].argument.name const body_path = path.get('body') const cache = initStackCache(len) @@ -212,6 +225,7 @@ function tryStackReplace(path, len, invalid) { if (father.isAssignmentExpression() && path.key === 'left') { processAssignLeft(vm, cache, path, prop_name, stk_name) } else if (exist) { + used[prop_name] = 1 changed |= processReplace(cache, path, prop_name) } }, @@ -264,16 +278,23 @@ function getStackParamLen(path) { function processStackParam(path, len) { if (path.isArrowFunctionExpression()) { - console.log(`Process arrowFunctionExpression, len: ${len}`) + console.log(`[Stack] Process arrowFunctionExpression, len: ${len}`) } else if (path.isFunctionExpression()) { - console.log(`Process functionExpression, len: ${len}`) + console.log(`[Stack] Process functionExpression, len: ${len}`) } else { - console.log(`Process Function ${path.node.id.name}, len: ${len}`) + console.log(`[Stack] Process Function ${path.node.id.name}, len: ${len}`) } + const orig_code = generator(path.node).code let changed = true - const invalid = checkStackInvalid(path) + const invalid = {} + let used = {} while (changed) { - changed = tryStackReplace(path, len, invalid) + checkStackInvalid(path, invalid) + if (!checkChangeValid(invalid, used)) { + path.replaceWith(prase(orig_code)) + used = {} + } + changed = tryStackReplace(path, len, invalid, used) path.traverse(calculateConstantExp) } } @@ -284,11 +305,11 @@ const deStackFuncLen = { if (!obj) { return } - console.log(`Find functionLengthName: ${obj.name}`) + console.log(`[Stack] Find functionLengthName: ${obj.name}`) let binding = obj.path.parentPath.scope.bindings[obj.name] for (const ref of binding.referencePaths) { if (ref.key !== 'callee') { - console.warn(`Unexpected ref of functionLengthName: ${obj.name}`) + console.warn(`[Stack] Unexpected ref of functionLengthName: ${obj.name}`) continue } const repl_path = ref.parentPath diff --git a/src/visitor/jsconfuser/string-concealing.js b/src/visitor/jsconfuser/string-concealing.js index 95f2c8d..16a4142 100644 --- a/src/visitor/jsconfuser/string-concealing.js +++ b/src/visitor/jsconfuser/string-concealing.js @@ -372,6 +372,11 @@ function tryStringConcealingPlace(path) { if (binding?.constantViolations?.length !== 1) { return } + for (const ref of binding.referencePaths) { + if (ref.key !== 'object' || ref.parentPath.key === 'callee') { + return + } + } const code = generator(parent.node).code const vm = isolate.createContextSync() vm.evalSync('var ' + code) From 12288c9aa7b0c65fdef9fe24752eddd30288873a Mon Sep 17 00:00:00 2001 From: echo094 <20028238+echo094@users.noreply.github.com> Date: Thu, 16 Jan 2025 23:10:52 +0800 Subject: [PATCH 21/21] fix Stack --- src/visitor/jsconfuser/stack.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/visitor/jsconfuser/stack.js b/src/visitor/jsconfuser/stack.js index 4d8e96c..7446697 100644 --- a/src/visitor/jsconfuser/stack.js +++ b/src/visitor/jsconfuser/stack.js @@ -1,3 +1,4 @@ +const { parse } = require('@babel/parser') const generator = require('@babel/generator').default const t = require('@babel/types') @@ -184,7 +185,7 @@ function checkStackInvalid(path, invalid) { function checkChangeValid(invalid, used) { let valid = true - Object.keys(used).forEach(function(key) { + Object.keys(used).forEach(function (key) { if (Object.prototype.hasOwnProperty.call(invalid, key)) { valid = false } @@ -291,7 +292,7 @@ function processStackParam(path, len) { while (changed) { checkStackInvalid(path, invalid) if (!checkChangeValid(invalid, used)) { - path.replaceWith(prase(orig_code)) + path.replaceWith(parse(orig_code)) used = {} } changed = tryStackReplace(path, len, invalid, used) @@ -309,7 +310,9 @@ const deStackFuncLen = { let binding = obj.path.parentPath.scope.bindings[obj.name] for (const ref of binding.referencePaths) { if (ref.key !== 'callee') { - console.warn(`[Stack] Unexpected ref of functionLengthName: ${obj.name}`) + console.warn( + `[Stack] Unexpected ref of functionLengthName: ${obj.name}` + ) continue } const repl_path = ref.parentPath