This commit is contained in:
smallfawn
2024-06-27 21:29:59 +08:00
parent 76fd1a3ac6
commit 644ea3a59d
21 changed files with 6974 additions and 2 deletions

2
.gitattributes vendored
View File

@@ -1,2 +0,0 @@
# Auto detect text files and perform LF normalization
* text=auto

32
.github/workflows/decode.yml vendored Normal file
View File

@@ -0,0 +1,32 @@
name: Decode JavaScript File
on:
push:
branches:
- main
jobs:
decode:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: List files in root directory
run: ls -l
- name: Install dependencies and run decode
run: |
npm install
npm run decode -- -t obfuscator [-i input.js] [-o output.js]
- name: Configure Git author
run: |
git config --local user.email "action@github.com"
git config --local user.name "action"
- name: Save decoded output to repository
run: |
git status
git add output.js
git status
git commit -m "Add decoded output file"
git push

1
input.js Normal file
View File

@@ -0,0 +1 @@
(function(_0x444058,_0x570471){function _0x339db3(_0x1d0c2f,_0x3b1f5a,_0x4874cc,_0x2145a8,_0x27afc4){return _0x5263(_0x27afc4-0x195,_0x3b1f5a);}function _0x56d139(_0x27325d,_0x183565,_0x4fee98,_0x4ebe7d,_0x1e9086){return _0x5263(_0x1e9086-0x368,_0x4fee98);}var _0x200cf6=_0x444058();function _0x5eaa44(_0x13fd33,_0x266fba,_0xdf817,_0x2f810d,_0x287e1a){return _0x5263(_0x13fd33-0x1d8,_0xdf817);}function _0x46004f(_0x1a7d2d,_0x4b6fc0,_0x40aa99,_0x48f6da,_0x1a9c65){return _0x5263(_0x4b6fc0-0x338,_0x1a9c65);}function _0x4bf04b(_0x5b4f4e,_0xd2f654,_0x5f381a,_0x4e29c8,_0x9db713){return _0x5263(_0x9db713-0xd7,_0x5f381a);}while(!![]){try{var _0x179890=-parseInt(_0x339db3(0x2d5,0x2d1,0x2cc,0x2d5,0x2d4))/(0x136c*-0x1+0x548*0x1+0x4b7*0x3)*(-parseInt(_0x56d139(0x4a3,0x49e,0x4aa,0x4a1,0x4a4))/(-0x11*-0x85+-0x1358+-0x1*-0xa85))+-parseInt(_0x339db3(0x2d8,0x2d0,0x2d4,0x2d5,0x2d5))/(0x2272+0x12f7+-0x3566)*(parseInt(_0x56d139(0x4a7,0x4ad,0x4aa,0x4a8,0x4aa))/(-0x1*0x1ebb+0x85*-0x21+-0x994*-0x5))+parseInt(_0x5eaa44(0x322,0x323,0x326,0x31c,0x31b))/(0x4c*0x67+-0x6*-0xc0+0x5*-0x703)+parseInt(_0x4bf04b(0x21f,0x218,0x218,0x224,0x21f))/(-0x1d4+-0x6ae*0x4+0x9f*0x2e)+-parseInt(_0x4bf04b(0x21a,0x224,0x220,0x218,0x21e))/(0x16d6+-0x1f0*-0x1+-0x18bf)+parseInt(_0x46004f(0x472,0x479,0x477,0x480,0x477))/(-0xfe5+0x3a8+-0x1*-0xc45)*(-parseInt(_0x56d139(0x4b0,0x4b3,0x4a5,0x4b3,0x4ac))/(-0x1*-0x21dc+-0x145+-0x56d*0x6))+parseInt(_0x4bf04b(0x21a,0x20d,0x213,0x20f,0x214))/(0x23e8+-0x2379+-0x65)*(-parseInt(_0x46004f(0x479,0x47e,0x481,0x477,0x480))/(0xc88*0x1+-0x224e+0x15d1));if(_0x179890===_0x570471)break;else _0x200cf6['push'](_0x200cf6['shift']());}catch(_0x2cf0ac){_0x200cf6['push'](_0x200cf6['shift']());}}}(_0x4f47,-0x466*0x494+0x999da*0x1+0x1511b5));function _0x4f47(){var _0x3b4ba0=['PbtJQ','1UboPxT','3hjknQS','24dkllqJ','889964CuSUOU','log','992808dMSJWQ','Hello','5687PqdMXP','4884243rTzFPz','5881650hdzsay','\x20Worl','4674680kJDtDQ','2632414JrYWaV','24950NsVBxV'];_0x4f47=function(){return _0x3b4ba0;};return _0x4f47();}function hi(){function _0x3d370d(_0x2101a2,_0x21af60,_0x26c689,_0x44e706,_0x4f9c6c){return _0x5263(_0x21af60- -0x3ba,_0x4f9c6c);}function _0x3df3dc(_0x5b0f06,_0x1ad90a,_0x1fab14,_0x3cecd9,_0x1e9d24){return _0x5263(_0x1e9d24- -0xd3,_0x1fab14);}function _0x5e4abf(_0x363beb,_0x36d681,_0x15bd4d,_0x2e3669,_0x24fb12){return _0x5263(_0x2e3669-0x19,_0x15bd4d);}var _0x3c3202={};function _0x144232(_0x432acb,_0x3a5371,_0x499003,_0x5bf0fe,_0x591a73){return _0x5263(_0x5bf0fe- -0x4d,_0x432acb);}_0x3c3202[_0x3df3dc(0x6e,0x70,0x64,0x6f,0x6b)]=_0xe3fb9f(-0x1f9,-0x1ff,-0x200,-0x1fc,-0x202)+_0xe3fb9f(-0x1f8,-0x1fb,-0x200,-0x1ff,-0x1ff)+'d!';function _0xe3fb9f(_0x4ef0bf,_0x5d2307,_0x5b6659,_0x526774,_0x4dc04e){return _0x5263(_0x5d2307- -0x344,_0x526774);}var _0xb13bfe=_0x3c3202;console[_0xe3fb9f(-0x207,-0x201,-0x207,-0x201,-0x208)](_0xb13bfe[_0x5e4abf(0x15b,0x150,0x153,0x157,0x15c)]);}function _0x5263(_0x49a732,_0x2f0df8){var _0x58f2ba=_0x4f47();return _0x5263=function(_0x498a53,_0x357ad3){_0x498a53=_0x498a53-(0x152*-0xa+-0x1f1b+-0x59*-0x83);var _0x511ecd=_0x58f2ba[_0x498a53];return _0x511ecd;},_0x5263(_0x49a732,_0x2f0df8);}hi();

0
output.js Normal file
View File

3283
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

21
package.json Normal file
View File

@@ -0,0 +1,21 @@
{
"name": "decode-js",
"scripts": {
"decode": "node src/main.js",
"deob": "node src/main.js -t obfuscator",
"deso": "node src/main.js -t sojson",
"desov7": "node src/main.js -t sojsonv7",
"lint": "eslint --ext .js --fix src"
},
"dependencies": {
"@babel/generator": "^7.17.10",
"@babel/parser": "^7.17.10",
"@babel/traverse": "^7.17.10",
"@babel/types": "^7.17.10",
"eslint": "^8.23.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier": "^4.2.1",
"isolated-vm": "^4.7.2",
"prettier": "^2.7.1"
}
}

50
src/main.js Normal file
View File

@@ -0,0 +1,50 @@
const fs = require('fs')
const PluginCommon = require('./plugin/common.js')
const PluginJjencode = require('./plugin/jjencode.js')
const PluginSojson = require('./plugin/sojson.js')
const PluginSojsonV7 = require('./plugin/sojsonv7.js')
const PluginObfuscator = require('./plugin/obfuscator.js')
const PluginAwsc = require('./plugin/awsc.js')
// 读取参数
let type = 'common'
let encodeFile = 'input.js'
let decodeFile = 'output.js'
for (let i = 2; i < process.argv.length; i += 2) {
if (process.argv[i] === '-t') {
type = process.argv[i + 1]
}
if (process.argv[i] === '-i') {
encodeFile = process.argv[i + 1]
}
if (process.argv[i] === '-o') {
decodeFile = process.argv[i + 1]
}
}
console.log(`类型: ${type}`)
console.log(`输入: ${encodeFile}`)
console.log(`输出: ${decodeFile}`)
// 读取源代码
const sourceCode = fs.readFileSync(encodeFile, { encoding: 'utf-8' })
// 净化源代码
let code
if (type === 'sojson') {
code = PluginSojson(sourceCode)
} else if (type === 'sojsonv7') {
code = PluginSojsonV7(sourceCode)
} else if (type === 'obfuscator') {
code = PluginObfuscator(sourceCode)
} else if (type === 'awsc') {
code = PluginAwsc(sourceCode)
} else if (type === 'jjencode') {
code = PluginJjencode(sourceCode)
} else {
code = PluginCommon(sourceCode)
}
// 输出代码
if (code) {
fs.writeFile(decodeFile, code, () => {})
}

244
src/plugin/awsc.js Normal file
View File

@@ -0,0 +1,244 @@
/**
* Reference
* * [某宝登录bx-ua参数逆向思路(fireyejs 225算法)](https://zhuanlan.zhihu.com/p/626187669)
*/
const { parse } = require('@babel/parser')
const generator = require('@babel/generator').default
const traverse = require('@babel/traverse').default
const t = require('@babel/types')
function RemoveVoid(path) {
if (path.node.operator === 'void') {
path.replaceWith(path.node.argument)
}
}
function LintConditionalAssign(path) {
if (!t.isAssignmentExpression(path?.parent)) {
return
}
let { test, consequent, alternate } = path.node
let { operator, left } = path.parent
consequent = t.assignmentExpression(operator, left, consequent)
alternate = t.assignmentExpression(operator, left, alternate)
path.parentPath.replaceWith(
t.conditionalExpression(test, consequent, alternate)
)
}
function LintConditionalIf(ast) {
function conditional(path) {
let { test, consequent, alternate } = path.node
// console.log(generator(test, { minified: true }).code)
if (t.isSequenceExpression(path.parent)) {
if (!sequence(path.parentPath)) {
path.stop()
}
return
}
if (t.isLogicalExpression(path.parent)) {
if (!logical(path.parentPath)) {
path.stop()
}
return
}
if (!t.isExpressionStatement(path.parent)) {
console.error(`Unexpected parent type: ${path.parent.type}`)
path.stop()
return
}
consequent = t.expressionStatement(consequent)
alternate = t.expressionStatement(alternate)
let statement = t.ifStatement(test, consequent, alternate)
path.replaceWithMultiple(statement)
}
function sequence(path) {
if (t.isLogicalExpression(path.parent)) {
return logical(path.parentPath)
}
let body = []
for (const item of path.node.expressions) {
body.push(t.expressionStatement(item))
}
let node = t.blockStatement(body, [])
let replace_path = path
if (t.isExpressionStatement(path.parent)) {
replace_path = path.parentPath
} else if (!t.isBlockStatement(path.parent)) {
console.error(`Unexpected parent type: ${path.parent.type}`)
return false
}
replace_path.replaceWith(node)
return true
}
function logical(path) {
let { operator, left, right } = path.node
if (operator !== '&&') {
console.error(`Unexpected logical operator: ${operator}`)
return false
}
if (!t.isExpressionStatement(path.parent)) {
console.error(`Unexpected parent type: ${path.parent.type}`)
return false
}
let node = t.ifStatement(left, t.expressionStatement(right))
path.parentPath.replaceWith(node)
return true
}
traverse(ast, {
ConditionalExpression: { enter: conditional },
})
}
function LintLogicalIf(path) {
let { operator, left, right } = path.node
if (operator !== '&&') {
// console.warn(`Unexpected logical operator: ${operator}`)
return
}
if (!t.isExpressionStatement(path.parent)) {
console.warn(`Unexpected parent type: ${path.parent.type}`)
return
}
let node = t.ifStatement(left, t.expressionStatement(right))
path.parentPath.replaceWith(node)
return
}
function LintIfStatement(path) {
let { test, consequent, alternate } = path.node
let changed = false
if (!t.isBlockStatement(consequent)) {
consequent = t.blockStatement([consequent])
changed = true
}
if (alternate && !t.isBlockStatement(alternate)) {
alternate = t.blockStatement([alternate])
changed = true
}
if (!changed) {
return
}
path.replaceWith(t.ifStatement(test, consequent, alternate))
}
function LintIfTest(path) {
let { test, consequent, alternate } = path.node
if (!t.isSequenceExpression(test)) {
return
}
if (!t.isBlockStatement(path.parent)) {
return
}
let body = test.expressions
let last = body.pop()
let before = t.expressionStatement(t.sequenceExpression(body))
path.insertBefore(before)
path.replaceWith(t.ifStatement(last, consequent, alternate))
}
function LintSwitchCase(path) {
let { test, consequent } = path.node
if (consequent.length == 1 && t.isBlockStatement(consequent[0])) {
return
}
let block = t.blockStatement(consequent)
path.replaceWith(t.switchCase(test, [block]))
}
function LintReturn(path) {
let { argument } = path.node
if (!t.isSequenceExpression(argument)) {
return
}
if (!t.isBlockStatement(path.parent)) {
return
}
let body = argument.expressions
let last = body.pop()
let before = t.expressionStatement(t.sequenceExpression(body))
path.insertBefore(before)
path.replaceWith(t.returnStatement(last))
}
function LintSequence(path) {
let body = []
for (const item of path.node.expressions) {
body.push(t.expressionStatement(item))
}
let node = t.blockStatement(body, [])
let replace_path = path
if (t.isExpressionStatement(path.parent)) {
replace_path = path.parentPath
} else if (!t.isBlockStatement(path.parent)) {
console.warn(`Unexpected parent type: ${path.parent.type}`)
return
}
replace_path.replaceWith(node)
return
}
function LintBlock(path) {
let { body } = path.node
if (!body.length) {
return
}
let changed = false
let arr = []
for (const item of body) {
if (!t.isBlockStatement(item)) {
arr.push(item)
continue
}
changed = true
for (const sub of item.body) {
arr.push(sub)
}
}
if (!changed) {
return
}
path.replaceWith(t.blockStatement(arr))
}
module.exports = function (code) {
let ast = parse(code)
// Lint
traverse(ast, {
UnaryExpression: RemoveVoid,
})
traverse(ast, {
ConditionalExpression: { exit: LintConditionalAssign },
})
LintConditionalIf(ast)
traverse(ast, {
LogicalExpression: { exit: LintLogicalIf },
})
traverse(ast, {
IfStatement: { exit: LintIfStatement },
})
traverse(ast, {
IfStatement: { enter: LintIfTest },
})
traverse(ast, {
SwitchCase: { enter: LintSwitchCase },
})
traverse(ast, {
ReturnStatement: { enter: LintReturn },
})
traverse(ast, {
SequenceExpression: { exit: LintSequence },
})
traverse(ast, {
BlockStatement: { exit: LintBlock },
})
code = generator(ast, {
comments: false,
jsescOption: { minimal: true },
}).code
return code
}

21
src/plugin/common.js Normal file
View File

@@ -0,0 +1,21 @@
const { parse } = require('@babel/parser')
const generator = require('@babel/generator').default
const traverse = require('@babel/traverse').default
module.exports = function (code) {
let ast
try {
ast = parse(code, { errorRecovery: true })
} catch (e) {
console.error(`Cannot parse code: ${e.reasonCode}`)
return null
}
const deleteExtra = require('../visitor/delete-extra')
traverse(ast, deleteExtra)
const calculateBinary = require('../visitor/calculate-binary')
traverse(ast, calculateBinary)
const calculateRString = require('../visitor/calculate-rstring')
traverse(ast, calculateRString)
code = generator(ast).code
return code
}

52
src/plugin/eval.js Normal file
View File

@@ -0,0 +1,52 @@
const { parse } = require('@babel/parser')
const generator = require('@babel/generator').default
const traverse = require('@babel/traverse').default
const t = require('@babel/types')
function unpack(code) {
let ast = parse(code, { errorRecovery: true })
let lines = ast.program.body
let data = null
for (let line of lines) {
if (t.isEmptyStatement(line)) {
continue
}
if (data) {
return null
}
if (
t.isCallExpression(line?.expression) &&
line.expression.callee?.name === 'eval' &&
line.expression.arguments.length === 1 &&
t.isCallExpression(line.expression.arguments[0])
) {
data = t.expressionStatement(line.expression.arguments[0])
continue
}
return null
}
if (!data) {
return null
}
code = generator(data, { minified: true }).code
return eval(code)
}
function pack(code) {
let ast1 = parse('(function(){}())')
let ast2 = parse(code)
traverse(ast1, {
FunctionExpression(path) {
let body = t.blockStatement(ast2.program.body)
path.replaceWith(t.functionExpression(null, [], body))
path.stop()
},
})
code = generator(ast1, { minified: false }).code
return code
}
module.exports = {
unpack,
pack,
}

66
src/plugin/jjencode.js Normal file
View File

@@ -0,0 +1,66 @@
/**
* Check the format and decode if possible
*
* @param {string} code the encoded code
* @returns null or string
*/
function getCode(code) {
// split the code by semicolon
let blocks = []
for (let line of code.split(';')) {
if (line.length && line !== '\n') {
blocks.push(line)
}
}
if (blocks.length !== 6) {
console.error('The number of code blocks is incorrect!')
return null
}
// try to get the global variable name
const line1 = blocks[0].split('=')
if (line1.length !== 2 || line1[1].indexOf('~[]') === -1) {
console.error('Cannot find variable name!')
return null
}
// extract the target code
const target = blocks[5]
const variable = line1[0]
const left = `${variable}.$(${variable}.$(${variable}.$$+"\\""+`
let i = 0
let s = 0
while (i < left.length && s < target.length) {
if (left[i] === target[s]) {
++i
}
++s
}
const right = '"\\"")())()'
let j = right.length - 1
let e = target.length - 1
while (j >= 0 && e >= 0) {
if (right[j] === target[e]) {
--j
}
--e
}
if (s >= e) {
console.error('Cannot find the target code!')
return null
}
const selected = target.substring(s, e)
blocks[5] = `${variable}.$(${variable}.$$+"\\""+${selected}+"\\"")()`
const result = eval(blocks.join(';'))
return result
}
/**
* This encoding method originates from http://utf-8.jp/public/jjencode.html,
* and it does not change the original code (encoder, not obfuscation).
*/
module.exports = function (code) {
code = getCode(code)
if (!code) {
return null
}
return code
}

1109
src/plugin/obfuscator.js Normal file

File diff suppressed because it is too large Load Diff

657
src/plugin/sojson.js Normal file
View File

@@ -0,0 +1,657 @@
/**
* 在 babel_asttool.js 的基础上修改而来
*/
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 PluginEval = require('./eval.js')
const isolate = new ivm.Isolate()
const globalContext = isolate.createContextSync()
function virtualGlobalEval(jsStr) {
return globalContext.evalSync(String(jsStr))
}
function decodeGlobal(ast) {
// 清理空语句
let i = 0
while (i < ast.program.body.length) {
if (t.isEmptyStatement(ast.program.body[i])) {
ast.program.body.splice(i, 1)
} else {
++i
}
}
// 前3句非空语句分别为签名信息、预处理函数、解密函数。
if (i < 3) {
console.log('Error: code too short')
return false
}
// 分离解密语句与内容语句
let decrypt_code = ast.program.body.slice(0, 3)
if (!t.isVariableDeclaration(decrypt_code[0])) {
console.log('Error: line 1 is not variable declaration')
return false
}
let decrypt_fun = decrypt_code[2]
if (t.isExpressionStatement(decrypt_fun)) {
decrypt_fun = decrypt_code[1]
}
let decrypt_val
if (t.isVariableDeclaration(decrypt_fun)) {
decrypt_val = decrypt_fun.declarations[0].id.name
} else if (t.isFunctionDeclaration(decrypt_fun)) {
decrypt_val = decrypt_fun.id.name
} else {
console.log('Error: cannot find decrypt variable')
return false
}
console.log(`主加密变量: ${decrypt_val}`)
let content_code = ast.program.body.slice(3)
// 运行解密语句
ast.program.body = decrypt_code
let { code } = generator(ast, {
compact: true,
})
virtualGlobalEval(code)
// 遍历内容语句
function funToStr(path) {
let node = path.node
if (!t.isIdentifier(node.callee, { name: decrypt_val })) {
return
}
let tmp = path.toString()
let value = virtualGlobalEval(tmp)
// console.log(`还原前:${tmp} 还原后:${value}`)
path.replaceWith(t.valueToNode(value))
}
function memToStr(path) {
let node = path.node
if (!t.isIdentifier(node.object, { name: decrypt_val })) {
return
}
let tmp = path.toString()
let value = virtualGlobalEval(tmp)
// console.log(`还原前:${tmp} 还原后:${value}`)
path.replaceWith(t.valueToNode(value))
}
ast.program.body = content_code
traverse(ast, {
CallExpression: funToStr,
MemberExpression: memToStr,
})
return ast
}
function purifyBoolean(path) {
// 简化 ![] 和 !![]
const node0 = path.node
if (node0.operator !== '!') {
return
}
const node1 = node0.argument
if (t.isArrayExpression(node1) && node1.elements.length === 0) {
path.replaceWith(t.booleanLiteral(false))
return
}
if (!t.isUnaryExpression(node1) || node1.operator !== '!') {
return
}
const node2 = node1.argument
if (t.isArrayExpression(node2) && node2.elements.length === 0) {
path.replaceWith(t.booleanLiteral(true))
}
}
function cleanIFCode(path) {
function clear(path, toggle) {
// 判定成立
if (toggle) {
if (path.node.consequent.type == 'BlockStatement') {
path.replaceWithMultiple(path.node.consequent.body)
} else {
path.replaceWith(path.node.consequent)
}
return
}
// 判定不成立
if (!path.node.alternate) {
path.remove()
return
}
if (path.node.alternate.type == 'BlockStatement') {
path.replaceWithMultiple(path.node.alternate.body)
} else {
path.replaceWith(path.node.alternate)
}
}
// 判断判定是否恒定
const test = path.node.test
const types = ['StringLiteral', 'NumericLiteral', 'BooleanLiteral']
if (test.type === 'BinaryExpression') {
if (
types.indexOf(test.left.type) !== -1 &&
types.indexOf(test.right.type) !== -1
) {
const left = JSON.stringify(test.left.value)
const right = JSON.stringify(test.right.value)
clear(path, eval(left + test.operator + right))
}
} else if (types.indexOf(test.type) !== -1) {
clear(path, eval(JSON.stringify(test.value)))
}
}
function cleanSwitchCode(path) {
// 扁平控制:
// 会使用一个恒为true的while语句包裹一个switch语句
// switch语句的执行顺序又while语句上方的字符串决定
// 首先碰断是否符合这种情况
const node = path.node
if (!(t.isBooleanLiteral(node.test) || t.isUnaryExpression(node.test))) {
return
}
if (!(node.test.prefix || node.test.value)) {
return
}
if (!t.isBlockStatement(node.body)) {
return
}
const body = node.body.body
if (
!t.isSwitchStatement(body[0]) ||
!t.isMemberExpression(body[0].discriminant) ||
!t.isBreakStatement(body[1])
) {
return
}
// switch语句的两个变量
const swithStm = body[0]
const arrName = swithStm.discriminant.object.name
const argName = swithStm.discriminant.property.argument.name
console.log(`扁平化还原: ${arrName}[${argName}]`)
// 在while上面的节点寻找这两个变量
let arr = []
path.getAllPrevSiblings().forEach((pre_path) => {
const { declarations } = pre_path.node
let { id, init } = declarations[0]
if (arrName == id.name) {
arr = init.callee.object.value.split('|')
pre_path.remove()
}
if (argName == id.name) {
pre_path.remove()
}
})
// 重建代码块
const caseList = swithStm.cases
let resultBody = []
arr.map((targetIdx) => {
// 从当前序号开始直到遇到continue
let valid = true
targetIdx = parseInt(targetIdx)
while (valid && targetIdx < caseList.length) {
const targetBody = caseList[targetIdx].consequent
const test = caseList[targetIdx].test
if (!t.isStringLiteral(test) || parseInt(test.value) !== targetIdx) {
console.log(`switch中出现乱序的序号: ${test.value}:${targetIdx}`)
}
for (let i = 0; i < targetBody.length; ++i) {
const s = targetBody[i]
if (t.isContinueStatement(s)) {
valid = false
break
}
if (t.isReturnStatement(s)) {
valid = false
resultBody.push(s)
break
}
if (t.isBreakStatement(s)) {
console.log(`switch中出现意外的break: ${arrName}[${argName}]`)
} else {
resultBody.push(s)
}
}
targetIdx++
}
})
// 替换整个while语句
path.replaceInline(resultBody)
}
function cleanDeadCode(ast) {
traverse(ast, { UnaryExpression: purifyBoolean })
traverse(ast, { IfStatement: cleanIFCode })
traverse(ast, { ConditionalExpression: cleanIFCode })
traverse(ast, { WhileStatement: { exit: cleanSwitchCode } })
return ast
}
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
}
/**
* Two RegExp tests will be conducted here:
* * If '\n' exists (code formatted)
* * If '\u' or '\x' does not exist (literal formatted)
*
* An infinite call stack will appear if either of the test fails.
* (by replacing the 'e' with '\u0435')
*/
const deleteSelfDefendingCode = {
VariableDeclarator(path) {
const { id, init } = path.node
const selfName = id.name
if (!t.isCallExpression(init)) {
return
}
if (!t.isIdentifier(init.callee)) {
return
}
const callName = init.callee.name
const args = init.arguments
if (
args.length != 2 ||
!t.isThisExpression(args[0]) ||
!t.isFunctionExpression(args[1])
) {
return
}
const block = generator(args[1]).code
const pattern = `RegExp()return.test(.toString())RegExp()return.test(.toString())\u0435\u0435`
if (!checkPattern(block, pattern)) {
return
}
const refs = path.scope.bindings[selfName].referencePaths
for (let ref of refs) {
if (ref.key == 'callee') {
ref.parentPath.remove()
break
}
}
path.remove()
console.info(`Remove SelfDefendingFunc: ${selfName}`)
const scope = path.scope.getBinding(callName).scope
scope.crawl()
const bind = scope.bindings[callName]
if (bind.referenced) {
console.error(`Call func ${callName} unexpected ref!`)
}
bind.path.remove()
console.info(`Remove CallFunc: ${callName}`)
},
}
/**
* A "debugger" will be inserted by:
* * v5: directly.
* * v6: calling Function constructor twice.
*/
const deleteDebugProtectionCode = {
FunctionDeclaration(path) {
const { id, params, body } = path.node
if (
!t.isIdentifier(id) ||
params.length !== 1 ||
!t.isIdentifier(params[0]) ||
!t.isBlockStatement(body) ||
body.body.length !== 2 ||
!t.isFunctionDeclaration(body.body[0]) ||
!t.isTryStatement(body.body[1])
) {
return
}
const debugName = id.name
const ret = params[0].name
const subNode = body.body[0]
if (
!t.isIdentifier(subNode.id) ||
subNode.params.length !== 1 ||
!t.isIdentifier(subNode.params[0])
) {
return
}
const subName = subNode.id.name
const counter = subNode.params[0].name
const code = generator(body).code
const pattern = `function${subName}(${counter}){${counter}debug${subName}(++${counter})}try{if(${ret})return${subName}${subName}(0)}catch(){}`
if (!checkPattern(code, pattern)) {
return
}
const scope1 = path.parentPath.scope
const refs = scope1.bindings[debugName].referencePaths
for (let ref of refs) {
if (ref.findParent((path) => path.removed)) {
continue
}
let parent = ref.getFunctionParent()
if (parent.key == 0) {
// DebugProtectionFunctionInterval
// window.setInterval(Function(), ...)
const rm = parent.parentPath
rm.remove()
continue
}
// DebugProtectionFunctionCall
const callName = parent.parent.callee.name
const up2 = parent.getFunctionParent().parentPath
const scope2 = up2.scope.getBinding(callName).scope
up2.remove()
scope1.crawl()
scope2.crawl()
const bind = scope2.bindings[callName]
bind.path.remove()
console.info(`Remove CallFunc: ${callName}`)
}
path.remove()
console.info(`Remove DebugProtectionFunc: ${debugName}`)
},
}
const deleteConsoleOutputCode = {
VariableDeclarator(path) {
const { id, init } = path.node
const selfName = id.name
if (!t.isCallExpression(init)) {
return
}
if (!t.isIdentifier(init.callee)) {
return
}
const callName = init.callee.name
const args = init.arguments
if (
args.length != 2 ||
!t.isThisExpression(args[0]) ||
!t.isFunctionExpression(args[1])
) {
return
}
const body = args[1].body.body
if (body.length !== 3) {
return
}
if (
!t.isVariableDeclaration(body[0]) ||
!t.isVariableDeclaration(body[1]) ||
!t.isIfStatement(body[2])
) {
return
}
const feature = [
[],
['window', 'process', 'require', 'global'],
[
'console',
'log',
'warn',
'debug',
'info',
'error',
'exception',
'trace',
],
]
let valid = true
for (let i = 1; i < 3; ++i) {
const { code } = generator(body[i])
feature[i].map((key) => {
if (code.indexOf(key) == -1) {
valid = false
}
})
}
if (!valid) {
return
}
const refs = path.scope.bindings[selfName].referencePaths
for (let ref of refs) {
if (ref.key == 'callee') {
ref.parentPath.remove()
break
}
}
path.remove()
console.info(`Remove ConsoleOutputFunc: ${selfName}`)
const scope = path.scope.getBinding(callName).scope
scope.crawl()
const bind = scope.bindings[callName]
if (bind.referenced) {
console.error(`Call func ${callName} unexpected ref!`)
}
bind.path.remove()
console.info(`Remove CallFunc: ${callName}`)
},
}
const deleteVersionCheck = {
StringLiteral(path) {
const msg = '删除版本号js会定期弹窗还请支持我们的工作'
if (path.node.value !== msg) {
return
}
let fnPath = path.getFunctionParent().parentPath
if (!fnPath.isCallExpression()) {
return
}
fnPath.remove()
console.log('Remove VersionCheck')
},
}
function unlockEnv(ast) {
// 查找并删除`自卫模式`函数
traverse(ast, deleteSelfDefendingCode)
// 查找并删除`禁止控制台调试`函数
traverse(ast, deleteDebugProtectionCode)
// 清空`禁止控制台输出`函数
traverse(ast, deleteConsoleOutputCode)
// 删除版本号检测
traverse(ast, deleteVersionCheck)
return ast
}
function purifyFunction(path) {
const node = path.node
if (!t.isIdentifier(node.left) || !t.isFunctionExpression(node.right)) {
return
}
const name = node.left.name
if (node.right.body.body.length !== 1) {
return
}
let retStmt = node.right.body.body[0]
if (!t.isReturnStatement(retStmt)) {
return
}
if (!t.isBinaryExpression(retStmt.argument, { operator: '+' })) {
return
}
try {
const fnPath = path.getFunctionParent() || path.scope.path
fnPath.traverse({
CallExpression: function (_path) {
const _node = _path.node.callee
if (!t.isIdentifier(_node, { name: name })) {
return
}
let args = _path.node.arguments
_path.replaceWith(t.binaryExpression('+', args[0], args[1]))
},
})
path.remove()
console.log(`拼接类函数: ${name}`)
} catch {
let code = generator(path.node, { minified: true }).code
console.warn('Purify function failed: ' + code)
}
}
function purifyCode(ast) {
// 净化拼接字符串的函数
traverse(ast, { AssignmentExpression: purifyFunction })
// 净化变量定义中的常量数值
function purifyDecl(path) {
if (t.isNumericLiteral(path.node.init)) {
return
}
const name = path.node.id.name
const { code } = generator(
{
type: 'Program',
body: [path.node.init],
},
{
compact: true,
}
)
const valid = /^[-+*/%!<>&|~^ 0-9;]+$/.test(code)
if (!valid) {
return
}
if (/^[-][0-9]*$/.test(code)) {
return
}
const value = eval(code)
const node = t.valueToNode(value)
path.replaceWith(t.variableDeclarator(path.node.id, node))
console.log(`替换 ${name}: ${code} -> ${value}`)
}
traverse(ast, { VariableDeclarator: purifyDecl })
// 合并字符串
let end = false
function combineString(path) {
const op = path.node.operator
if (op !== '+') {
return
}
const left = path.node.left
const right = path.node.right
if (!t.isStringLiteral(left) || !t.isStringLiteral(right)) {
return
}
end = false
path.replaceWith(t.StringLiteral(eval(path + '')))
console.log(`合并字符串: ${path.node.value}`)
}
while (!end) {
end = true
traverse(ast, { BinaryExpression: combineString })
}
// 替换索引器
function FormatMember(path) {
// _0x19882c['removeCookie']['toString']()
// |
// |
// |
// v
// _0x19882c.removeCookie.toString()
let curNode = path.node
if (!t.isStringLiteral(curNode.property)) {
return
}
if (curNode.computed === undefined || !curNode.computed === true) {
return
}
if (!/^[a-zA-Z_$][0-9a-zA-Z_$]*$/.test(curNode.property.value)) {
return
}
curNode.property = t.identifier(curNode.property.value)
curNode.computed = false
}
traverse(ast, { MemberExpression: FormatMember })
// 分割表达式
function removeComma(path) {
// a = 1, b = ddd(), c = null;
// |
// |
// |
// v
// a = 1;
// b = ddd();
// c = null;
let { expression } = path.node
if (!t.isSequenceExpression(expression)) {
return
}
let body = []
expression.expressions.forEach((express) => {
body.push(t.expressionStatement(express))
})
path.replaceInline(body)
}
traverse(ast, { ExpressionStatement: removeComma })
// 删除空语句
traverse(ast, {
EmptyStatement: (path) => {
path.remove()
},
})
// 删除未使用的变量
const deleteUnusedVar = require('../visitor/delete-unused-var')
traverse(ast, deleteUnusedVar)
return ast
}
module.exports = function (code) {
let ret = PluginEval.unpack(code)
let global_eval = false
if (ret) {
global_eval = true
code = ret
}
let ast = parse(code)
// 清理二进制显示内容
traverse(ast, {
StringLiteral: ({ node }) => {
delete node.extra
},
})
traverse(ast, {
NumericLiteral: ({ node }) => {
delete node.extra
},
})
console.log('处理全局加密...')
ast = decodeGlobal(ast)
if (!ast) {
return null
}
console.log('处理代码块加密...')
const parseControlFlowStorage = require('../visitor/parse-control-flow-storage')
traverse(ast, parseControlFlowStorage)
console.log('清理死代码...')
ast = cleanDeadCode(ast)
// 刷新代码
ast = parse(
generator(ast, {
comments: false,
jsescOption: { minimal: true },
}).code
)
console.log('提高代码可读性...')
ast = purifyCode(ast)
console.log('解除环境限制...')
ast = unlockEnv(ast)
console.log('净化完成')
code = generator(ast, {
comments: false,
jsescOption: { minimal: true },
}).code
if (global_eval) {
code = PluginEval.pack(code)
}
return code
}

914
src/plugin/sojsonv7.js Normal file
View File

@@ -0,0 +1,914 @@
/**
* For jsjiami.com.v7
*/
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 PluginEval = require('./eval.js')
const isolate = new ivm.Isolate()
const globalContext = isolate.createContextSync()
function virtualGlobalEval(jsStr) {
return globalContext.evalSync(String(jsStr))
}
function decodeGlobal(ast) {
// 清理空语句
let i = 0
while (i < ast.program.body.length) {
if (t.isEmptyStatement(ast.program.body[i])) {
ast.program.body.splice(i, 1)
} else {
++i
}
}
// line 1: version and string table
// line x: preprocessing function of string table
// line y: main encode function containing the var of string table
if (i < 3) {
console.log('Error: code too short')
return false
}
// split the first line
traverse(ast, {
Program(path) {
path.stop()
const l1 = path.get('body.0')
if (!l1.isVariableDeclaration()) {
return
}
const defs = l1.node.declarations
const kind = l1.node.kind
for (let i = defs.length - 1; i; --i) {
l1.insertAfter(t.VariableDeclaration(kind, [defs[i]]))
l1.get(`declarations.${i}`).remove()
}
l1.scope.crawl()
},
})
// find the main encode function
// [version, string-array, call, ...]
let decrypt_code = []
for (let i = 0; i < 3; ++i) {
decrypt_code.push(t.EmptyStatement())
}
const first_line = ast.program.body[0]
let var_version
if (t.isVariableDeclaration(first_line)) {
if (first_line.declarations.length) {
var_version = first_line.declarations[0].id.name
}
} else if (t.isCallExpression(first_line?.expression)) {
let call_func = first_line.expression.callee?.name
let i = ast.program.body.length
let find = false
while (--i) {
let part = ast.program.body[i]
if (!t.isFunctionDeclaration(part) || part?.id?.name !== call_func) {
continue
}
if (find) {
// remove duplicate definition
ast.program.body[i] = t.emptyStatement()
continue
}
find = true
let obj = part.body.body[0]?.expression?.left
if (!obj || !t.isMemberExpression(obj) || obj.object?.name !== 'global') {
break
}
var_version = obj.property?.value
decrypt_code.push(part)
ast.program.body[i] = t.emptyStatement()
continue
}
}
if (!var_version) {
console.error('Line 1 is not version variable!')
return false
}
console.info(`Version var: ${var_version}`)
decrypt_code[0] = first_line
ast.program.body.shift()
// iterate and classify all refs of var_version
const refs = {
string_var: null,
string_path: null,
def: [],
}
traverse(ast, {
Identifier: (path) => {
const name = path.node.name
if (name !== var_version) {
return
}
const up1 = path.parentPath
if (up1.isVariableDeclarator()) {
refs.def.push(path)
} else if (up1.isArrayExpression()) {
let node_table = path.getFunctionParent()
while (node_table.getFunctionParent()) {
node_table = node_table.getFunctionParent()
}
let var_string_table = null
if (node_table.node.id) {
var_string_table = node_table.node.id.name
} else {
while (!node_table.isVariableDeclarator()) {
node_table = node_table.parentPath
}
var_string_table = node_table.node.id.name
}
let valid = true
up1.traverse({
MemberExpression(path) {
valid = false
path.stop()
},
})
if (valid) {
refs.string_var = var_string_table
refs.string_path = node_table
} else {
console.info(`Drop string table: ${var_string_table}`)
}
} else if (up1.isAssignmentExpression() && path.key === 'left') {
// We don't need to execute this reference
// Instead, we can delete it directly
const up2 = up1.parentPath
up2.replaceWith(up2.node.left)
} else {
console.warn(`Unexpected ref var_version: ${up1}`)
}
},
})
// check if contains string table
let var_string_table = refs.string_var
if (!var_string_table) {
console.error('Cannot find string table')
return false
}
// check if contains rotate function and decrypt variable
let decrypt_val
let decrypt_path
let binds = refs.string_path.scope.getBinding(var_string_table)
function parse_main_call(path) {
decrypt_path = path
const node = path.node
const copy = t.functionDeclaration(node.id, node.params, node.body)
node.body = t.blockStatement([])
return copy
}
// remove path of string table
if (refs.string_path.isVariableDeclarator()) {
decrypt_code[1] = t.variableDeclaration('var', [refs.string_path.node])
} else {
decrypt_code[1] = refs.string_path.node
}
refs.string_path.remove()
// iterate refs
let cache = undefined
for (let bind of binds.referencePaths) {
if (bind.findParent((path) => path.removed)) {
continue
}
const parent = bind.parentPath
if (parent.isCallExpression() && bind.listKey === 'arguments') {
// This is the rotate function.
// If it's in a sequence expression, it can be handled together.
// Or, we should handle it after this iteration.
cache = parent
continue
}
if (parent.isSequenceExpression()) {
// rotate function
decrypt_code.push(t.expressionStatement(parent.node))
const up2 = parent.parentPath
if (up2.isIfStatement()) {
// In the new version, rotate function will be enclosed by an
// empty IfStatement
up2.remove()
} else {
parent.remove()
}
continue
}
if (parent.isVariableDeclarator()) {
// main decrypt val
let top = parent.getFunctionParent()
while (top.getFunctionParent()) {
top = top.getFunctionParent()
}
decrypt_code[2] = parse_main_call(top)
decrypt_val = top.node.id.name
continue
}
if (parent.isCallExpression() && !parent.node.arguments.length) {
// main decrypt val
if (!t.isVariableDeclarator(parent.parentPath.node)) {
continue
}
let top = parent.getFunctionParent()
while (top.getFunctionParent()) {
top = top.getFunctionParent()
}
decrypt_code[2] = parse_main_call(top)
decrypt_val = top.node.id.name
continue
}
if (parent.isExpressionStatement()) {
parent.remove()
continue
}
console.warn(`Unexpected ref var_string_table: ${parent}`)
}
// If rotateFunction is detected but not handled, we should handle it now.
if (decrypt_code.length === 3 && cache) {
if (cache.parentPath.isExpressionStatement()) {
decrypt_code.push(cache.parent)
cache = cache.parentPath
} else {
decrypt_code.push(t.expressionStatement(cache.node))
}
cache.remove()
}
decrypt_path.parentPath.scope.crawl()
if (!decrypt_val) {
console.error('Cannot find decrypt variable')
return
}
console.log(`Main call wrapper name: ${decrypt_val}`)
// 运行解密语句
let content_code = ast.program.body
ast.program.body = decrypt_code
let { code } = generator(ast, {
compact: true,
})
virtualGlobalEval(code)
// 遍历内容语句
ast.program.body = content_code
function funToStr(path) {
let tmp = path.toString()
let value = virtualGlobalEval(tmp)
// console.log(`还原前:${tmp} 还原后:${value}`)
path.replaceWith(t.valueToNode(value))
}
function memToStr(path) {
let tmp = path.toString()
let value = virtualGlobalEval(tmp)
// console.log(`还原前:${tmp} 还原后:${value}`)
path.replaceWith(t.valueToNode(value))
}
function dfs(stk, item) {
stk.push(item)
const cur_val = item.name
console.log(`Enter sub ${stk.length}:${cur_val}`)
let pfx = ''
for (let parent of stk) {
pfx += parent.code + ';'
}
virtualGlobalEval(pfx)
let scope = item.path.scope
if (item.path.isFunctionDeclaration()) {
scope = item.path.parentPath.scope
}
const refs = scope.bindings[cur_val].referencePaths
const refs_next = []
for (let ref of refs) {
const parent = ref.parentPath
if (ref.key === 'init') {
// VariableDeclarator
refs_next.push({
name: parent.node.id.name,
path: parent,
code: 'var ' + parent,
})
} else if (ref.key === 'object') {
// MemberExpression
memToStr(parent)
} else if (ref.key === 'callee') {
// CallExpression
funToStr(parent)
}
}
for (let ref of refs_next) {
dfs(stk, ref)
}
scope.crawl()
item.path.remove()
scope.crawl()
console.log(`Exit sub ${stk.length}:${cur_val}`)
stk.pop()
}
const root = {
name: decrypt_val,
path: decrypt_path,
code: '',
}
dfs([], root)
return ast
}
function purifyBoolean(path) {
// 简化 ![] 和 !![]
const node0 = path.node
if (node0.operator !== '!') {
return
}
const node1 = node0.argument
if (t.isArrayExpression(node1) && node1.elements.length === 0) {
path.replaceWith(t.booleanLiteral(false))
return
}
if (!t.isUnaryExpression(node1) || node1.operator !== '!') {
return
}
const node2 = node1.argument
if (t.isArrayExpression(node2) && node2.elements.length === 0) {
path.replaceWith(t.booleanLiteral(true))
}
}
function cleanIFCode(path) {
function clear(path, toggle) {
// 判定成立
if (toggle) {
if (path.node.consequent.type == 'BlockStatement') {
path.replaceWithMultiple(path.node.consequent.body)
} else {
path.replaceWith(path.node.consequent)
}
return
}
// 判定不成立
if (!path.node.alternate) {
path.remove()
return
}
if (path.node.alternate.type == 'BlockStatement') {
path.replaceWithMultiple(path.node.alternate.body)
} else {
path.replaceWith(path.node.alternate)
}
}
// 判断判定是否恒定
const test = path.node.test
const types = ['StringLiteral', 'NumericLiteral', 'BooleanLiteral']
if (test.type === 'BinaryExpression') {
if (
types.indexOf(test.left.type) !== -1 &&
types.indexOf(test.right.type) !== -1
) {
const left = JSON.stringify(test.left.value)
const right = JSON.stringify(test.right.value)
clear(path, eval(left + test.operator + right))
}
} else if (types.indexOf(test.type) !== -1) {
clear(path, eval(JSON.stringify(test.value)))
}
}
function cleanSwitchCode1(path) {
// 扁平控制:
// 会使用一个恒为true的while语句包裹一个switch语句
// switch语句的执行顺序又while语句上方的字符串决定
// 首先碰断是否符合这种情况
const node = path.node
if (!(t.isBooleanLiteral(node.test) || t.isUnaryExpression(node.test))) {
return
}
if (!(node.test.prefix || node.test.value)) {
return
}
if (!t.isBlockStatement(node.body)) {
return
}
const body = node.body.body
if (
!t.isSwitchStatement(body[0]) ||
!t.isMemberExpression(body[0].discriminant) ||
!t.isBreakStatement(body[1])
) {
return
}
// switch语句的两个变量
const swithStm = body[0]
const arrName = swithStm.discriminant.object.name
const argName = swithStm.discriminant.property.argument.name
console.log(`扁平化还原: ${arrName}[${argName}]`)
// 在while上面的节点寻找这两个变量
let arr = []
path.getAllPrevSiblings().forEach((pre_path) => {
const { declarations } = pre_path.node
let { id, init } = declarations[0]
if (arrName == id.name) {
arr = init.callee.object.value.split('|')
pre_path.remove()
}
if (argName == id.name) {
pre_path.remove()
}
})
// 重建代码块
const caseList = swithStm.cases
let resultBody = []
arr.map((targetIdx) => {
// 从当前序号开始直到遇到continue
let valid = true
targetIdx = parseInt(targetIdx)
while (valid && targetIdx < caseList.length) {
const targetBody = caseList[targetIdx].consequent
const test = caseList[targetIdx].test
if (!t.isStringLiteral(test) || parseInt(test.value) !== targetIdx) {
console.log(`switch中出现乱序的序号: ${test.value}:${targetIdx}`)
}
for (let i = 0; i < targetBody.length; ++i) {
const s = targetBody[i]
if (t.isContinueStatement(s)) {
valid = false
break
}
if (t.isReturnStatement(s)) {
valid = false
resultBody.push(s)
break
}
if (t.isBreakStatement(s)) {
console.log(`switch中出现意外的break: ${arrName}[${argName}]`)
} else {
resultBody.push(s)
}
}
targetIdx++
}
})
// 替换整个while语句
path.replaceInline(resultBody)
}
function cleanSwitchCode2(path) {
// 扁平控制:
// 会使用一个空的for语句包裹一个switch语句
// switch语句的执行顺序由for语句上方的字符串决定
// 首先判断是否符合这种情况
const node = path.node
if (node.init || node.test || node.update) {
return
}
if (!t.isBlockStatement(node.body)) {
return
}
const body = node.body.body
if (
!t.isSwitchStatement(body[0]) ||
!t.isMemberExpression(body[0].discriminant) ||
!t.isBreakStatement(body[1])
) {
return
}
// switch语句的两个变量
const swithStm = body[0]
const arrName = swithStm.discriminant.object.name
const argName = swithStm.discriminant.property.argument.name
// 在while上面的节点寻找这两个变量
let arr = null
for (let pre_path of path.getAllPrevSiblings()) {
if (!pre_path.isVariableDeclaration()) {
continue
}
let test = '' + pre_path
try {
arr = eval(test + `;${arrName}`)
} catch {
//
}
}
if (!arr) {
return
}
console.log(`扁平化还原: ${arrName}[${argName}]`)
// 重建代码块
const caseMap = {}
for (let item of swithStm.cases) {
caseMap[item.test.value] = item.consequent
}
let resultBody = []
arr.map((targetIdx) => {
// 从当前序号开始直到遇到continue
let valid = true
while (valid && targetIdx < arr.length) {
const targetBody = caseMap[targetIdx]
for (let i = 0; i < targetBody.length; ++i) {
const s = targetBody[i]
if (t.isContinueStatement(s)) {
valid = false
break
}
if (t.isReturnStatement(s)) {
valid = false
resultBody.push(s)
break
}
if (t.isBreakStatement(s)) {
console.log(`switch中出现意外的break: ${arrName}[${argName}]`)
} else {
resultBody.push(s)
}
}
targetIdx++
}
})
// 替换整个while语句
path.replaceInline(resultBody)
}
function cleanDeadCode(ast) {
traverse(ast, { UnaryExpression: purifyBoolean })
traverse(ast, { IfStatement: cleanIFCode })
traverse(ast, { ConditionalExpression: cleanIFCode })
traverse(ast, { WhileStatement: { exit: cleanSwitchCode1 } })
traverse(ast, { ForStatement: { exit: cleanSwitchCode2 } })
return ast
}
function removeUniqueCall(path) {
let up1 = path.parentPath
let decorator = up1.node.callee.name
console.info(`Remove decorator: ${decorator}`)
let bind1 = up1.scope.getBinding(decorator)
bind1.path.remove()
if (up1.key === 'callee') {
up1.parentPath.remove()
} else if (up1.key === 'init') {
let up2 = up1.parentPath
let call = up2.node.id.name
console.info(`Remove call: ${call}`)
let bind2 = up2.scope.getBinding(call)
up2.remove()
for (let ref of bind2.referencePaths) {
if (ref.findParent((path) => path.removed)) {
continue
}
if (ref.key === 'callee') {
let rm = ref.parentPath
if (rm.key === 'expression') {
rm = rm.parentPath
}
rm.remove()
} else {
console.warn(`Unexpected ref key: ${ref.key}`)
}
}
}
}
function unlockDebugger(path) {
const decl_path = path.getFunctionParent()?.getFunctionParent()
if (!decl_path) {
return
}
// Check if contains inf-loop
let valid = false
path.getFunctionParent().traverse({
WhileStatement(path) {
if (t.isBooleanLiteral(path.node.test) && path.node.test) {
valid = true
}
},
})
if (!valid) {
return
}
const name = decl_path.node.id.name
const bind = decl_path.scope.getBinding(name)
console.info(`Debug test and inf-loop: ${name}`)
for (let ref of bind.referencePaths) {
if (ref.findParent((path) => path.removed)) {
continue
}
if (ref.listKey === 'arguments') {
// setInterval
let rm = ref.getFunctionParent().parentPath
if (rm.key === 'expression') {
rm = rm.parentPath
}
rm.remove()
} else if (ref.key === 'callee') {
// lint test for this method
let rm = ref.getFunctionParent()
removeUniqueCall(rm)
} else {
console.warn(`Unexpected ref key: ${ref.key}`)
}
}
decl_path.remove()
path.stop()
}
function unlockConsole(path) {
if (!t.isArrayExpression(path.node.init)) {
return
}
let pattern = 'log|warn|debug|info|error|exception|table|trace'
let count = 0
for (let ele of path.node.init.elements) {
if (~pattern.indexOf(ele.value)) {
++count
continue
}
return
}
if (count < 5) {
return
}
let left1 = path.getSibling(0)
const code = generator(left1.node, { minified: true }).code
pattern = ['window', 'process', 'require', 'global']
pattern.map((key) => {
if (code.indexOf(key) == -1) {
return
}
})
let rm = path.getFunctionParent()
removeUniqueCall(rm)
}
function unlockLint(path) {
if (path.findParent((up) => up.removed)) {
return
}
if (path.node.value !== '(((.+)+)+)+$') {
return
}
let rm = path.getFunctionParent()
removeUniqueCall(rm)
}
function unlockDomainLock(path) {
const array_list = [
'[7,116,5,101,3,117,0,100]',
'[5,110,0,100]',
'[7,110,0,108]',
'[7,101,0,104]',
]
const checkArray = (node) => {
const trim = node.split(' ').join('')
for (let i = 0; i < 4; ++i) {
if (array_list[i] == trim) {
return i + 1
}
}
return 0
}
if (path.findParent((up) => up.removed)) {
return
}
let mask = 1 << checkArray('' + path)
if (mask == 1) {
return
}
let rm = path.getFunctionParent()
rm.traverse({
ArrayExpression: function (item) {
mask = mask | (1 << checkArray('' + item))
},
})
if (mask & 0b11110) {
console.info('Find domain lock')
removeUniqueCall(rm)
}
}
function unlockEnv(ast) {
// 删除`禁止控制台调试`函数
traverse(ast, { DebuggerStatement: unlockDebugger })
// 删除`禁止控制台输出`函数
traverse(ast, { VariableDeclarator: unlockConsole })
// 删除`禁止换行`函数
traverse(ast, { StringLiteral: unlockLint })
// 删除`安全域名`函数
traverse(ast, { ArrayExpression: unlockDomainLock })
}
/**
* If a function acts as follows:
* A = function (p1, p2) { return p1 + p2 }
*
* Convert its call to a binary expression:
* A(a, b) => a + b
*/
function purifyFunction(path) {
const left = path.get('left')
const right = path.get('right')
if (!left.isIdentifier() || !right.isFunctionExpression()) {
return
}
const name = left.node.name
const params = right.node.params
if (params.length !== 2) {
return
}
const name1 = params[0].name
const name2 = params[1].name
if (right.node.body.body.length !== 1) {
return
}
let retStmt = right.node.body.body[0]
if (!t.isReturnStatement(retStmt)) {
return
}
if (!t.isBinaryExpression(retStmt.argument, { operator: '+' })) {
return
}
if (
retStmt.argument.left?.name !== name1 ||
retStmt.argument.right?.name !== name2
) {
return
}
const fnPath = path.getFunctionParent() || path.scope.path
fnPath.traverse({
CallExpression: function (_path) {
const _node = _path.node.callee
if (!t.isIdentifier(_node, { name: name })) {
return
}
let args = _path.node.arguments
_path.replaceWith(t.binaryExpression('+', args[0], args[1]))
},
})
path.remove()
console.log(`拼接类函数: ${name}`)
}
function purifyCode(ast) {
// 净化拼接字符串的函数
traverse(ast, { AssignmentExpression: purifyFunction })
// 净化变量定义中的常量数值
function purifyDecl(path) {
if (t.isNumericLiteral(path.node.init)) {
return
}
const name = path.node.id.name
const { code } = generator(
{
type: 'Program',
body: [path.node.init],
},
{
compact: true,
}
)
const valid = /^[-+*/%!<>&|~^ 0-9;]+$/.test(code)
if (!valid) {
return
}
if (/^[-][0-9]*$/.test(code)) {
return
}
const value = eval(code)
const node = t.valueToNode(value)
path.replaceWith(t.variableDeclarator(path.node.id, node))
console.log(`替换 ${name}: ${code} -> ${value}`)
}
traverse(ast, { VariableDeclarator: purifyDecl })
// 合并字符串
let end = false
function combineString(path) {
const op = path.node.operator
if (op !== '+') {
return
}
const left = path.node.left
const right = path.node.right
if (!t.isStringLiteral(left) || !t.isStringLiteral(right)) {
return
}
end = false
path.replaceWith(t.StringLiteral(eval(path + '')))
console.log(`合并字符串: ${path.node.value}`)
}
while (!end) {
end = true
traverse(ast, { BinaryExpression: combineString })
}
// 替换索引器
function FormatMember(path) {
// _0x19882c['removeCookie']['toString']()
// |
// |
// |
// v
// _0x19882c.removeCookie.toString()
let curNode = path.node
if (!t.isStringLiteral(curNode.property)) {
return
}
if (curNode.computed === undefined || !curNode.computed === true) {
return
}
if (!/^[a-zA-Z_$][0-9a-zA-Z_$]*$/.test(curNode.property.value)) {
return
}
curNode.property = t.identifier(curNode.property.value)
curNode.computed = false
}
traverse(ast, { MemberExpression: FormatMember })
// 分割表达式
function removeComma(path) {
// a = 1, b = ddd(), c = null;
// |
// |
// |
// v
// a = 1;
// b = ddd();
// c = null;
if (!t.isExpressionStatement(path.parent)) {
return
}
let replace_path = path.parentPath
if (replace_path.listKey !== 'body') {
return
}
for (const item of path.node.expressions) {
replace_path.insertBefore(t.expressionStatement(item))
}
replace_path.remove()
}
traverse(ast, { SequenceExpression: { exit: removeComma } })
// 删除空语句
traverse(ast, {
EmptyStatement: (path) => {
path.remove()
},
})
// 删除未使用的变量
const deleteUnusedVar = require('../visitor/delete-unused-var')
traverse(ast, deleteUnusedVar)
}
module.exports = function (code) {
let ret = PluginEval.unpack(code)
let global_eval = false
if (ret) {
global_eval = true
code = ret
}
let ast
try {
ast = parse(code, { errorRecovery: true })
} catch (e) {
console.error(`Cannot parse code: ${e.reasonCode}`)
return null
}
// IllegalReturn
const deleteIllegalReturn = require('../visitor/delete-illegal-return')
traverse(ast, deleteIllegalReturn)
// 清理二进制显示内容
traverse(ast, {
StringLiteral: ({ node }) => {
delete node.extra
},
})
traverse(ast, {
NumericLiteral: ({ node }) => {
delete node.extra
},
})
console.log('处理全局加密...')
ast = decodeGlobal(ast)
if (!ast) {
return null
}
console.log('处理代码块加密...')
const parseControlFlowStorage = require('../visitor/parse-control-flow-storage')
traverse(ast, parseControlFlowStorage)
console.log('清理死代码...')
ast = cleanDeadCode(ast)
// 刷新代码
ast = parse(
generator(ast, {
comments: false,
jsescOption: { minimal: true },
}).code
)
console.log('提高代码可读性...')
purifyCode(ast)
ast = parse(generator(ast, { comments: false }).code)
console.log('解除环境限制...')
unlockEnv(ast)
console.log('净化完成')
code = generator(ast, {
comments: false,
jsescOption: { minimal: true },
}).code
if (global_eval) {
code = PluginEval.pack(code)
}
return code
}

View File

@@ -0,0 +1,24 @@
const generator = require('@babel/generator').default
const t = require('@babel/types')
/**
* Calculate BinaryExpression if left and right are both literals.
* Otherwise, the expression can't be simplified.
*
* For example, `typeof window` can be calculated but it's not constant.
*/
module.exports = {
BinaryExpression(path) {
const { left, right } = path.node
if (!t.isLiteral(left) || !t.isLiteral(right)) {
return
}
const code = generator(path.node).code
try {
const ret = eval(code)
path.replaceWithSourceString(ret)
} catch {
//
}
},
}

View File

@@ -0,0 +1,36 @@
const generator = require('@babel/generator').default
const t = require('@babel/types')
/**
* "sh".split("").reverse().join("") -> "hs"
*/
module.exports = {
StringLiteral(path) {
if (path.key !== 'object') {
return
}
let root = path
let count = 6
while (root.parentPath && count) {
if (
root.parentPath.isMemberExpression() ||
root.parentPath.isCallExpression()
) {
root = root.parentPath
--count
} else {
break
}
}
if (count) {
return
}
const code = generator(root.node).code
try {
const ret = eval(code)
root.replaceWith(t.stringLiteral(ret))
} catch {
//
}
},
}

View File

@@ -0,0 +1,12 @@
/**
* 0x10 -> 16, "\u0058" -> "X"
* not ASCII-safe (disable jsescOption:minimal to keep ASCII-safe)
*/
module.exports = {
StringLiteral: ({ node }) => {
delete node.extra
},
NumericLiteral: ({ node }) => {
delete node.extra
},
}

View File

@@ -0,0 +1,10 @@
/**
* delete ReturnStatement in Program scope
*/
module.exports = {
ReturnStatement(path) {
if (!path.getFunctionParent()) {
path.remove()
}
},
}

View File

@@ -0,0 +1,35 @@
const t = require('@babel/types')
/**
* Delete unused variables with the following exceptions:
*
* - ForOfStatement
* - ForInStatement
*
*/
module.exports = {
VariableDeclarator: (path) => {
const { node, scope } = path
const name = node.id.name
const binding = scope.getBinding(name)
if (!binding || binding.referenced || !binding.constant) {
return
}
const up1 = path.parentPath
const up2 = up1?.parentPath
if (t.isForOfStatement(up2)) {
return
}
if (t.isForInStatement(up2)) {
return
}
console.log(`Unused variable: ${name}`)
if (up1.node.declarations.length === 1) {
up1.remove()
up1.scope.crawl()
} else {
path.remove()
scope.crawl()
}
},
}

211
src/visitor/merge-object.js Normal file
View File

@@ -0,0 +1,211 @@
const t = require('@babel/types')
function mergeObject(path) {
const { id, init } = path.node
if (!t.isObjectExpression(init)) {
// 判断是否是定义对象
return
}
let name = id.name
let scope = path.scope
let binding = scope.getBinding(name)
const start = path.node.end
let end = -1
let violation = null
if (!binding.constant) {
// Find the first constantViolation after this declaration
for (let item of binding.constantViolations) {
if (item.node.start <= start) {
continue
}
if (item.isVariableDeclarator()) {
end = item.node.start
violation = item
break
}
if (item.isAssignmentExpression()) {
end = item.node.start
violation = item
break
}
return
}
}
// 添加已有的key
let keys = {}
let properties = init.properties
for (let prop of properties) {
let key = null
if (t.isStringLiteral(prop.key)) {
key = prop.key.value
}
if (t.isIdentifier(prop.key)) {
key = prop.key.name
}
if (key) {
keys[key] = true
}
}
// 遍历作用域检测是否含有局部混淆特征并合并成员
let merges = []
const container = path.parentPath.parentPath
let cur = 0
let valid = true
// Check references in sequence
while (cur < binding.references) {
const ref = binding.referencePaths[cur]
// Ignore the references before this declaration
if (ref.node.start <= start) {
++cur
continue
}
// Ignore the references after the first constantViolation
if (end >= 0 && ref.node.end >= end) {
break
}
if (ref.key !== 'object' || !ref.parentPath.isMemberExpression()) {
break
}
const me = ref.parentPath
if (me.key !== 'left' || !me.parentPath.isAssignmentExpression()) {
break
}
const ae = me.parentPath
let bk = ae
let stop = false
while (bk.parentPath !== container) {
if (
bk.parentPath.isSequenceExpression() ||
bk.parentPath.isVariableDeclarator() ||
bk.parentPath.isVariableDeclaration() ||
bk.parentPath.isExpressionStatement()
) {
bk = bk.parentPath
continue
}
stop = true
break
}
if (stop) {
break
}
const property = me.node.property
let key = null
if (t.isStringLiteral(property)) {
key = property.value
}
if (t.isIdentifier(property)) {
key = property.name
}
if (!key) {
valid = false
break
}
// 不允许出现重定义
if (Object.prototype.hasOwnProperty.call(keys, key)) {
valid = false
break
}
// 添加到列表
properties.push(t.ObjectProperty(t.valueToNode(key), ae.node.right))
keys[key] = true
merges.push(ae)
++cur
}
if (!merges.length || !valid) {
return
}
// Remove code
console.log(`尝试性合并: ${name}`)
for (let ref of merges) {
const left = ref.node.left
if (ref.parentPath.isSequenceExpression() && ref.container.length === 1) {
ref = ref.parentPath
}
if (
ref.parentPath.isVariableDeclarator() ||
ref.parentPath.isAssignmentExpression()
) {
ref.replaceWith(left)
} else {
ref.remove()
}
}
// Check the remaining references
const ref1 = binding.referencePaths[cur++]
if (!ref1) {
scope.crawl()
return
}
const ref2 = binding.referencePaths[cur]
// Don't replace the declarator if there exists more than one reference
if (ref2 && ref2.node.end < end) {
scope.crawl()
return
}
// Check if the only reference is an assignment
let key = ref1.key
let up1 = ref1.parentPath
if (up1.isSequenceExpression() && ref1.container.length === 1) {
key = up1.key
up1 = up1.parentPath
}
if (!up1.isVariableDeclarator() || key !== 'init') {
scope.crawl()
return
}
// Move the definition to its reference
up1.node.init = path.node.init
// Delete the original definition
if (violation?.isAssignmentExpression()) {
path.node.init = undefined
} else {
path.remove()
}
scope.crawl()
}
/**
* Collect the properties of one object and move it back to the declaration.
*
* One example made by ObjectExpressionKeysTransformer:
*
* ```javascript
* var _0xb28de8 = {};
* _0xb28de8["abcd"] = function(_0x22293f, _0x5a165e) {
* return _0x22293f == _0x5a165e;
* };
* _0xb28de8.dbca = function(_0xfbac1e, _0x23462f, _0x556555) {
* return _0xfbac1e(_0x23462f, _0x556555);
* };
* _0xb28de8.aaa = function(_0x57e640) {
* return _0x57e640();
* };
* _0xb28de8["bbb"] = "eee";
* var _0x15e145 = _0xb28de8;
* ```
*
* The result:
*
* ```javascript
* var _0x15e145 = {
* "abcd": function (_0x22293f, _0x5a165e) {
* return _0x22293f == _0x5a165e;
* },
* "dbca": function (_0xfbac1e, _0x23462f, _0x556555) {
* return _0xfbac1e(_0x23462f, _0x556555);
* },
* "aaa": function (_0x57e640) {
* return _0x57e640();
* },
* "bbb": "eee"
* };
* ```
*
* Note:
* - Constant objects in the original code can be splitted
* - AssignmentExpression can be moved to ReturnStatement
*/
module.exports = {
VariableDeclarator: mergeObject,
}

View File

@@ -0,0 +1,196 @@
const generator = require('@babel/generator').default
const t = require('@babel/types')
function parseObject(path) {
let node = path.node
// 变量必须定义为Object类型才可能是代码块加密内容
if (!t.isObjectExpression(node.init)) {
return
}
let objPropertiesList = node.init.properties
if (objPropertiesList.length == 0) {
return
}
// 遍历Object 判断每个元素是否符合格式
let objName = node.id.name
let objKeys = {}
// 有时会有重复的定义
let replCount = 0
objPropertiesList.map(function (prop) {
if (!t.isObjectProperty(prop)) {
return
}
let key
if (t.isIdentifier(prop.key)) {
key = prop.key.name
} else {
key = prop.key.value
}
if (t.isFunctionExpression(prop.value)) {
// 符合要求的函数必须有且仅有一条return语句
if (prop.value.body.body.length !== 1) {
return
}
let retStmt = prop.value.body.body[0]
if (!t.isReturnStatement(retStmt)) {
return
}
// 检测是否是3种格式之一
let repfunc = null
if (t.isBinaryExpression(retStmt.argument)) {
// 二元运算类型
repfunc = function (_path, args) {
_path.replaceWith(
t.binaryExpression(retStmt.argument.operator, args[0], args[1])
)
}
} else if (t.isLogicalExpression(retStmt.argument)) {
// 逻辑判断类型
repfunc = function (_path, args) {
_path.replaceWith(
t.logicalExpression(retStmt.argument.operator, args[0], args[1])
)
}
} else if (t.isCallExpression(retStmt.argument)) {
// 函数调用类型 调用的函数必须是传入的第一个参数
if (!t.isIdentifier(retStmt.argument.callee)) {
return
}
if (retStmt.argument.callee.name !== prop.value.params[0]?.name) {
return
}
repfunc = function (_path, args) {
_path.replaceWith(t.callExpression(args[0], args.slice(1)))
}
}
if (repfunc) {
objKeys[key] = repfunc
++replCount
}
} else if (t.isStringLiteral(prop.value)) {
let retStmt = prop.value.value
objKeys[key] = function (_path) {
_path.replaceWith(t.stringLiteral(retStmt))
}
++replCount
} else if (t.isMemberExpression(prop.value)) {
let retStmt = prop.value
objKeys[key] = function (_path) {
_path.replaceWith(retStmt)
}
++replCount
}
})
// 如果Object内的元素不全符合要求 很有可能是普通的字符串类型 不需要替换
if (!replCount) {
return
}
if (objPropertiesList.length !== replCount) {
console.log(
`不完整替换: ${objName} ${replCount}/${objPropertiesList.length}`
)
return
}
// 遍历作用域进行替换 分为函数调用和字符串调用
console.log(`处理代码块: ${objName}`)
let objUsed = {}
function getReplaceFunc(_node) {
// 这里开始所有的调用应该都在列表中
let key = null
if (t.isStringLiteral(_node.property)) {
key = _node.property.value
} else if (t.isIdentifier(_node.property)) {
key = _node.property.name
} else {
// Maybe the code was obfuscated more than once
const code = generator(_node.property, { minified: true }).code
console.log(`意外的调用: ${objName}[${code}]`)
return null
}
if (!Object.prototype.hasOwnProperty.call(objKeys, key)) {
// 这里应该是在死代码中 因为key不存在
return null
}
objUsed[key] = true
return objKeys[key]
}
let bind = path.scope.getBinding(objName)?.referencePaths
let usedCount = 0
// Replace reversely to handle nested cases correctly
for (let i = bind.length - 1; i >= 0; --i) {
let ref = bind[i]
let up1 = ref.parentPath
if (up1.isMemberExpression() && ref.key === 'object') {
if (up1.key === 'left' && t.isAssignmentExpression(up1.parent)) {
continue
}
let func = getReplaceFunc(up1.node)
if (!func) {
continue
}
++usedCount
let up2 = up1.parentPath
if (up1.key === 'callee') {
func(up2, up2.node.arguments)
} else {
func(up1)
}
}
}
// 如果没有全部使用 就先不删除
if (usedCount !== bind.length) {
console.log(`不完整使用: ${objName} ${usedCount}/${bind.length}`)
} else {
path.remove()
}
}
/**
* Parse control flow object
*
* Several kinds of expressions are collected, transformed, and merged into
* the controlFlowStorage object by method FunctionControlFlowTransformer:
*
* - BinaryExpression
* - CallExpression
* - LogicalExpression
* - Literal
*
* ```javascript
* var _0xb28de8 = {
* "abcd": function(_0x22293f, _0x5a165e) {
* return _0x22293f == _0x5a165e;
* },
* "dbca": function(_0xfbac1e, _0x23462f, _0x556555) {
* return _0xfbac1e(_0x23462f, _0x556555);
* },
* "aaa": function(_0x57e640) {
* return _0x57e640();
* },
* "bbb": "eee",
* "ccc": A[x][y][...]
* };
* ```
*
* This visitor can parse such objects and undo the transformation.
*
* ```javascript
* // From
* var aa = _0xb28de8["abcd"](123, 456);
* var bb = _0xb28de8["dbca"](bcd, 11, 22);
* var cc = _0xb28de8["aaa"](dcb);
* var dd = _0xb28de8["bbb"];
* var ee = _0xb28de8["ccc"];
* // To
* var aa = 123 == 456;
* var bb = bcd(11, 22);
* var cc = dcb();
* var dd = "eee";
* var ee = A[x][y][...];
* ```
*/
module.exports = {
VariableDeclarator: {
exit: parseObject,
},
}