4. 京东AST
AST 自动解混淆
最后更新于
AST 自动解混淆
最后更新于
本文讲解如何使用 ast
技术来处理京东 3.1
版本的混淆代码, 实现自动去除混淆、无效引用对象,花指令、平坦控制流等等
在本文开始之前我们首先需要获取混淆之后的 js
代码,该代码的下载地址为:js_security_v3_0.1.5.js
下载好混淆后的代码之后我们来逐步分析一下应该如何处理该代码
OB
混淆OB
混淆的特征一般是:
代码内部定义了一个大数组,大数组内部是一些加密好的字符串
代码内部定义了一个偏移函数,会操作这个大数组,进行偏移操作(可选)
代码内部定义了一个解码函数,调用解码函数传入对应的参数,就会将某个位置的字符串解码为原本的字符串
代码内部大量使用的 解码函数(相关参数)
的格式来代替原来的属性值 / 常量值
首先我们可以看到,原来的代码格式化后是有 9500+
行代码的,通过查看代码, 发现里面很多代码有是使用的函数调用来占位的,例如:
Mw[(Bw = 643, Lw = 629, Dw(Bw - -14, Lw))] = !1
简单分析一下,这边是将 Mw
的某个属性设置为 false
, 而属性值使用的 (Bw = 643, Lw = 629, Dw(Bw - -14, Lw))
进行占位,因此要获取该属性值,我们就需要获取到这个 Dw 函数, 以及传入的参数,简单整理一下,属性值就是:Dw(643+14, 629)
,而 Dw
函数如下图所示:
这个函数简单看起来就是从 n
里面获取某个值,然后经过一些解码操作,最后得到真实的值,而 n
又来源于 zw()
,继续跟紧发现 zw()
又来源于一个大数组,如下图
这种就是我们常见的 OB
混淆了
除了上面所说的 OB
混淆,里面还有一种常见的混淆手段,我们称之为花指令,简单来说,花指令可以分为字符串花指令和函数花指令
花指令的特征一般是:
代码内部定义了一个对象,对象内定义了很多方法 (可选)
代码内部定义了很多函数,函数之间一次引用
原来的操作方法会被定义好的函数 / 对象属性替代
如上图所示,在代码内,fv
为一个对象,变量 _
指向了 fv
这个的某个属性值,这边就是利用了花指令和 ob
混淆来降低代码可读性
Switch
控制流为了降低代码可读性,常常还会加入大量的 switch
控制流, 将本来顺序执行的代码拆分成多块,然后使用 switch case
语句进行执行,如下图所示
在上文中,我们已经分析完了京东的 js
里面几种常见的混淆手段,下面我们需要准备 ast
环境,来为自动解混淆做准备,步骤如下:
安装 nodejs
环境
安装 babel
相关库
npm install @babel/parser
npm install @babel/traverse
npm install @babel/types
npm install @babel/generator
安装下载库,用于下载 js
文件(可选)
npm install axios
接下来我们正式开始编写 ast
插件来自动化处理京东的混淆代码
为了提升开发效率,我个人封装了一个 ast
的工具类
该工具类的功能如下:
genAst
:根据路径生成 ast
对象,路径可以是 url / 文件路径 / 代码
reGenAST
: 将 ast
对象重新生成 ast
对象
getBinding
: 获取当前 path
下的变量为 name
的绑定信息(可以获取到定义处)
getReferencePaths
: 获取当前 path
下的变量为 name
的引用 path
getRealNode
: 获取当前 path
下的节点为 node
的真实节点(不完美,有 Bug
)
getRealValue
:获取当前 path
下的节点为 node
的真实值
getOuterFuncPath
:获取当前 path
的外部函数 path
toCode
:将 ast
对象转为 code
代码
toFile
: 将 ast
对象存储至文件内
makeNode
:创建一个新的节点
OB
混淆接下来我们来编写插件用于解 OB
混淆,根据上文分析,我们已经得知了在京东的 js
文件中,OB
混淆的特征,因此我们的大致开发思路为:
定义一个解密配置,类型为大数组
获取加密数组
获取加密数组对应的偏移函数
获取加密数组对应的解密函数
将前三点组装成一个解密配置解密配置数组中
获取嵌套使用加密函数的新的加密函数,将其放入解密配置数组中
循环所有的解密配置数组,开始解密
具体定位我们需要借助 ast
代码分析工具,来分析具体的加密数组的特征,工具我们可以使用 https://github.com/cilame/v_jstools
例如我把其中一个加密数组放入工具中,就可以看到他的结构
具体步骤如下:
遍历所有变量定义处
如果符合加密数组特征,则获取加密数组的所有引用
如果引用符合解密函数特征,则定义为解密函数
如果引用符合偏移特征,则定义为偏移函数
加入解密配置
function getDecodeConfig(ast, log) {
let decodeConfig = []
AST.traverse(ast, {
"VariableDeclaration"(path) {
// 加密数组特征
if (path.node.kind === "var" && path.node.declarations.length === 1 && path.node.declarations[0].init && path.node.declarations[0].init.type === "ArrayExpression" && path.getSibling(path.key + 1).node.type === "ReturnStatement" && path.parentPath.node.type === "BlockStatement" && path.parentPath.parentPath.node.type === "FunctionDeclaration") {
let item = {}
// 找到加密数组之后, 获取最外层的函数, 这个函数就可以生成加密数组
let encodeArrPath = AST.getOuterFuncPath(path)
if (encodeArrPath) {
item.encodeArr = encodeArrPath
log && console.log(`[加密数组] \n${item.encodeArr}`)
// 找到引用这个数组的地方
// 只有两个地方
// 1. 数组偏移
// 2. 解密函数
AST.getReferencePaths(encodeArrPath, encodeArrPath.node.id.name).forEach(p => {
// 解密函数特征
if (p.key === "callee" && p.parentPath.type === "CallExpression" && p.parentPath.node.arguments.length === 0) {
item.decodeFun = AST.getOuterFuncPath(p)
log && console.log(`[解密函数] \n${item.decodeFun}`)
} else if (// 数组偏移函数特征
p.listKey === "arguments" && p.parentPath.type === "CallExpression") {
item.arrayOffsetFun = p.parentPath
log && console.log(`[数组偏移函数] \n${item.arrayOffsetFun}`)
}
})
decodeConfig.push(item)
}
}
}
})
return decodeConfig
}
OB
混淆具体步骤如下:
循环上一步获取得到的解密配置
使用 eval
定义好环境
获取解密函数所有引用的 path
,并循环遍历
动态替换节点
判断是否为嵌套函数,如果是,则加入解密配置
function solveConfusion(decodeConfig, log) {
for (let _index, index_ = 0; index_ < decodeConfig.length; index_++) {
_index = index_
let config = decodeConfig[index_];
let encodeArr = config.encodeArr
let decodeFun = config.decodeFun
let arrayOffsetFun = config.arrayOffsetFun
// 可能不存在编码数组, 因为有一些函数是嵌套节点
let encodeArrStr = encodeArr ? `${encodeArr};` : ""
// 可能不存在数组偏移函数, 因为有一些函数是嵌套节点
let arrayOffsetFunStr = arrayOffsetFun ? `!${arrayOffsetFun};` : ""
let decodeFunStr = `${decodeFun};`
// 要生成的 env 的代码
let envCode = `${encodeArrStr}${arrayOffsetFunStr}${decodeFunStr}`
// 先 eval 生成
eval(envCode)
AST.getReferencePaths(decodeFun, decodeFun.node.id.name).forEach(myPath => {
if (myPath.parentPath.type === "CallExpression") {
let rawCode = myPath.parentPath + ""
for (let i = 0; i < myPath.parentPath.node.arguments.length; i++) {
let argument = myPath.parentPath.node.arguments[i];
// 递归找到最原始的节点并替换掉
myPath.parentPath.node.arguments[i] = AST.getRealNode(myPath.parentPath, argument)
}
// 调用函数, 替换原来的 path
try {
myPath.parentPath.replaceWith(AST.makeNode({value: eval(myPath.parentPath + "")}))
log && console.log(`替换成功: ${rawCode}-> ${myPath.parentPath}`)
} catch (e) {
log && console.log(`替换失败: ${rawCode}-> ${myPath.parentPath}, 原因: ${e}`)
}
// 获取其外层的函数, 判断是不是嵌套节点
let parentFunc = AST.getOuterFuncPath(myPath)
if (parentFunc.node.body.body.length === 1) {
log && console.log(`[嵌套节点]\n ${parentFunc}`)
decodeConfig.splice(_index + 1, 0, {
decodeFun: parentFunc,
})
_index++
}
}
})
}
}
具体步骤如下
遍历所有的 FunctionDeclaration|AssignmentExpression|VariableDeclarator
如果他们没有被引用,则删除
function removeUnreferencedObjects(ast, log) {
let count = 0
ast = AST.reGenAST(ast)
while (true) {
AST.traverse(ast, {
"FunctionDeclaration|AssignmentExpression": (path) => {
let binding
switch (path.node.type) {
case "FunctionDeclaration":
// 这里用 1 是因为参数名和函数名一样, 就会获取错误
binding = AST.getBinding(path, path.node.id.name, 1)
break
case "AssignmentExpression":
binding = AST.getBinding(path, path.node.left.name, 1)
break
}
if (binding && !binding.referenced) {
try {
path.remove();
count++
} catch (e) {
// console.log(path + "")
}
}
}
})
AST.traverse(ast, {
"VariableDeclarator": (path) => {
let binding
// 最大的那个没有引用, 所以不删除
if (path.node.id.name === "ParamsSign") {
return
}
binding = AST.getBinding(path, path.node.id.name)
if (binding && !binding.referenced) {
count++
path.remove();
}
}
})
if (!count) {
break
} else {
log && console.log(`[移除引用数量]: ${count}`)
count = 0
ast = AST.reGenAST(ast)
}
}
return AST.reGenAST(ast)
}
具体步骤如下:
遍历所有变量定义的地方
如果为定义对象,则遍历这个对象的所有属性
获取对象属性的真实节点
获取对象属性的引用处
判断应用处的调用方式,使用相应的步骤去处理还原,新建节点并替换
function removeFlowerInstruction(ast, log) {
let flag
while (true) {
AST.traverse(ast, {
VariableDeclarator(path) {
if (AST.types.isObjectExpression(path.node.init)) {
let objectName = path.node.id.name
let properties = path.node.init.properties
for (let i = 0; i < properties.length; i++) {
let propertie = properties[i];
propertie.value = AST.getRealNode(path, propertie.value)
}
let references = AST.getReferencePaths(path, objectName)
for (let i = 0; i < references.length; i++) {
let reference = references[i];
if (AST.types.isMemberExpression(reference.parentPath.node)) {
let propertyName = reference.parentPath.node.property.name || reference.parentPath.node.property.value
let binding = AST.getBinding(reference.parentPath, objectName)
let propertie = binding.path.node.init.properties.find(p => p.key.name === propertyName)
if (propertie) {
let node
try {
switch (propertie.value.type) {
case "FunctionExpression":
let bodyLength = propertie.value.body.body.length
let returnBody = propertie.value.body.body.find(x => x.type === "ReturnStatement")
if (
1 <= bodyLength <= 2
&& returnBody
&& reference.parentPath.parentPath.node.type === "CallExpression"
&& reference.parentPath.parentPath.node.callee.type === "MemberExpression"
) {
let rawArgs, operator, left, right, leftIndex, rightIndex
switch (returnBody.argument.type) {
case "BinaryExpression":
operator = returnBody.argument.operator
left = returnBody.argument.left
right = returnBody.argument.right
rawArgs = propertie.value.params.map(x => x.name)
leftIndex = rawArgs.indexOf(left.name)
rightIndex = rawArgs.indexOf(right.name)
node = AST.makeNode({
"type": "binaryExpression",
"value": [operator, reference.parentPath.parentPath.node.arguments[leftIndex], reference.parentPath.parentPath.node.arguments[rightIndex]]
})
break
case "CallExpression":
// 调用表达式
let fun = returnBody.argument.callee
let args = returnBody.argument.arguments
rawArgs = propertie.value.params.map(x => x.name)
let argsIndex = args.map(x => rawArgs.indexOf(x.name))
// 将参数都筛选出来, 换成外部的参数
let newArgs = argsIndex.map(x => reference.parentPath.parentPath.node.arguments[x])
if (rawArgs !== newArgs.map(x => x.name)) {
// 将内部的函数换成外部的函数
let newFun = reference.parentPath.parentPath.node.arguments[rawArgs.indexOf(fun.name)]
node = AST.makeNode({
"type": "callExpression",
"value": [newFun, newArgs]
})
} else {
node = AST.makeNode({
"type": "callExpression",
"value": [returnBody.argument.callee, newArgs]
})
}
break
case "LogicalExpression":
operator = returnBody.argument.operator
left = returnBody.argument.left
right = returnBody.argument.right
rawArgs = propertie.value.params.map(x => x.name)
leftIndex = rawArgs.indexOf(left.name)
rightIndex = rawArgs.indexOf(right.name)
node = AST.makeNode({
"type": "logicalExpression",
"value": [
operator,
reference.parentPath.parentPath.node.arguments[leftIndex],
reference.parentPath.parentPath.node.arguments[rightIndex],]
})
break
default:
log && console.log(`[${returnBody.argument.type}] - ${reference.parentPath}`)
}
node && reference.parentPath.parentPath.replaceInline(node)
}
break
default:
if (propertie.value.type === "ObjectExpression" && propertie.key.name === "exports") {
break
}
let realValue = AST.getRealValue(reference.parentPath, propertie.value)
if (realValue.state) {
log && console.log(`[${propertie.value.type}] - ${reference.parentPath} -> ${realValue.value}`)
reference.parentPath.replaceInline(AST.makeNode({
"value": realValue.value
}))
}
}
} catch (e) {
if (e.message.indexOf("Container is falsy") > -1) {
flag = true
} else {
log && console.log(`[${propertie.value.type}] - ${reference.parentPath}`)
}
}
}
}
}
}
}
})
if (!flag) {
break
} else {
ast = AST.reGenAST(ast)
flag = false
}
}
return ast
}
switch
控制流平坦化具体步骤如下:
遍历所有 for
循环,判断是否符合 switch
控制流特征
获取 case
内的分发器
重组代码,替换 for
循环代码块
function switchFlatStreaming(ast) {
let flag = false
while (true) {
AST.traverse(ast, {
"ForStatement"(path) {
let body = path.node.body.body
let switchStatement = body && body.length >= 2 ? body.find(x => x.type === "SwitchStatement") : undefined
let breakStatement = body && body.length >= 2 && switchStatement ? body[body.indexOf(switchStatement) + 1] : undefined
let discriminant = switchStatement ? switchStatement.discriminant : undefined
let discriminantObj = switchStatement ? discriminant.object : undefined
if (
body
&& switchStatement
&& breakStatement
&& discriminantObj
) {
if ((path + "").indexOf('this["_storagetokenKey"] = ny(v = ""["concat"](this._storagetokenKey, "_"))["call"](') > -1) {
debugger
}
discriminantObj = AST.getRealNode(path, discriminantObj)
if (
discriminant.type === "MemberExpression"
&& discriminantObj.type === "CallExpression"
&& discriminantObj.callee.type === "MemberExpression"
&& discriminantObj.callee.object.type === "StringLiteral"
&& (discriminantObj.callee.property.value || discriminantObj.callee.property.name) === "split"
&& discriminant.property.type === "UpdateExpression"
&& discriminant.property.operator === "++"
&& AST.getRealNode(path, discriminant.property.argument).value === 0
) {
let dispatchers = discriminantObj.callee.object.value[discriminantObj.callee.property.value || discriminantObj.callee.property.name](discriminantObj.arguments[0].value)
let cases = switchStatement.cases
let myBlocks = []
let rawIndex
let rawBody
switch (path.parentPath.node.type) {
case "IfStatement":
if (path.parentPath.node.consequent.type !== "BlockStatement") {
path.parentPath.node.consequent = AST.makeNode({
"type": "BlockStatement",
"value": [[path.parentPath.node.consequent]]
})
rawBody = path.parentPath.node.consequent.body
} else {
rawBody = path.parentPath.node.consequent.body.body
}
break
case "BlockStatement":
rawBody = path.parentPath.node.body
break
default:
console.log(path.parentPath.node.type)
}
// try {
// console.log(path + "")
// console.log("rawBody: ", rawBody)
// console.log(rawBody.indexOf(path.node))
// } catch (e) {
// debugger
// }
rawIndex = rawBody.indexOf(path.node)
for (let i = 0; i < rawIndex; i++) {
myBlocks.push(rawBody[i])
}
rawBody.init && myBlocks.push(rawBody.init)
for (let i = 0; i < dispatchers.length; i++) {
let dispatcher = dispatchers[i];
let _case = cases.find(c => c.test.value === dispatcher)
for (let j = 0; j < _case.consequent.length; j++) {
let consequent = _case.consequent[j]
if (
consequent.type === "ContinueStatement"
|| consequent.type === "BreakStatement"
) {
break
}
myBlocks.push(consequent)
}
}
for (let i = rawIndex + 1; i < rawBody.length; i++) {
myBlocks.push(rawBody[i])
}
rawBody.splice(0, rawBody.length)
rawBody.push(...myBlocks)
flag = true
path.stop()
}
}
}
})
if (flag) {
ast = AST.reGenAST(ast)
flag = false
} else {
break
}
}
return ast
}
还可以开发一些其他的插件,例如逗号表达式,二项式简单处理等等,这边不做过多的介绍