4. 京东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 环境,来为自动解混淆做准备,步骤如下:

  1. 安装 nodejs 环境

  2. 安装 babel 相关库

npm install @babel/parser
npm install @babel/traverse
npm install @babel/types
npm install @babel/generator
  1. 安装下载库,用于下载 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 混淆的特征,因此我们的大致开发思路为:

  1. 定义一个解密配置,类型为大数组

  2. 获取加密数组

  3. 获取加密数组对应的偏移函数

  4. 获取加密数组对应的解密函数

  5. 将前三点组装成一个解密配置解密配置数组中

  6. 获取嵌套使用加密函数的新的加密函数,将其放入解密配置数组中

  7. 循环所有的解密配置数组,开始解密

具体定位我们需要借助 ast 代码分析工具,来分析具体的加密数组的特征,工具我们可以使用 https://github.com/cilame/v_jstools

例如我把其中一个加密数组放入工具中,就可以看到他的结构

获取解密配置

具体步骤如下:

  1. 遍历所有变量定义处

  2. 如果符合加密数组特征,则获取加密数组的所有引用

  3. 如果引用符合解密函数特征,则定义为解密函数

  4. 如果引用符合偏移特征,则定义为偏移函数

  5. 加入解密配置

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 混淆

具体步骤如下:

  1. 循环上一步获取得到的解密配置

  2. 使用 eval 定义好环境

  3. 获取解密函数所有引用的 path,并循环遍历

  4. 动态替换节点

  5. 判断是否为嵌套函数,如果是,则加入解密配置

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++
                }
            }


        })
    }

}

无效引用对象

具体步骤如下

  1. 遍历所有的 FunctionDeclaration|AssignmentExpression|VariableDeclarator

  2. 如果他们没有被引用,则删除

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)

}

移除花指令

具体步骤如下:

  1. 遍历所有变量定义的地方

  2. 如果为定义对象,则遍历这个对象的所有属性

  3. 获取对象属性的真实节点

  4. 获取对象属性的引用处

  5. 判断应用处的调用方式,使用相应的步骤去处理还原,新建节点并替换

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 控制流平坦化

具体步骤如下:

  1. 遍历所有 for 循环,判断是否符合 switch 控制流特征

  2. 获取 case 内的分发器

  3. 重组代码,替换 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

}

其他插件

还可以开发一些其他的插件,例如逗号表达式,二项式简单处理等等,这边不做过多的介绍

完整源码

效果展示

最后更新于

这有帮助吗?