【AST 还原实战】某招聘 zp_stoken 平坦流 还原(一)

流程平坦化

它使用了 两种平坦流,这里还原第一种

分析结构

每次执行 case 时,才更新控制器的值,这种 平坦流 还是挺常见的,但 该 Js 做了一点处理

var sCS = 16; // 分发器初始值 
while (!![]) {
    switch (sCS) {
        case 1:
            ...
            sCS += 22;
            break;
        case 2:
            ...
            sCS += 3;
                    // 该 分支没有 break ,直接进入下一个 case
        case 3:
            ...
            sCS += 12;
            break;
        case 4:
            ...
            sCS += 2;
            break;
        case 5:
            ...
            sCS += 12;
            break;
        ...
    }

mark

还原思路

1、通过 特征 判断,确定平坦流 代码

2、获取 分发器初始值(shufferVar

3、遍历 while-case映射出 case 值 和 case 代码块 ({'case值': 代码块})

4、再次遍历 while-case,构造 分支流程(自己说的词),表示 流程的前后分支关系,当然没有 break 的需要特殊处理一下,形如:[[2, 3],[1, 2],[3, 4]]

5、通过分支流程前后关系,构造出 分发器数组

6、遍历 分发器数组shufferArr),根据 数组 元素 顺序 获取映射的代码块,同时判断 break并删除 break更新流程节点,添加到 父节点的 body 里面,添加前 删除 分发器节点 和 while 节点

1、判断特征

平坦流都在函数节点里面, 声明节点 + whilw 节点

traverse(ast, {
    'FunctionDeclaration|FunctionExpression'(path) {
        path.traverse({
            // 只处理分发器是单个声明的
            VariableDeclaration(p) {
                // 通过 特征 定位 定位分发器位置  var rbD = 0;
                let declarations = p.node.declarations;
                if (!(declarations.length === 1
                    && type.isNumericLiteral(declarations[0].init))
                ) return;
                let whilePath = p.getSibling(p.key + 1)
                if (!type.isWhileStatement(whilePath)) return; // 声明语句后不是 while 节点
            }
        })
    }
})

mark

2、获取分发器

判断完成,获取分发器 变量名初始值,变量名 用来更新 分发器值时判断

 // 获取 分发器 
let shufferVar = {name: declarations[0].id.name, value: declarations[0].init.value} // 分发器起始值
console.info('分发器其起始值:', shufferVar)

3、映射 case

映射为了 方面获取 case 代码块

// 映射 case 和 代码块
let _case = {}
whilePath.node.body.body[0].cases.map((v) => {
    let switchCase = v.consequent; // 获取 cases 里的代码块 数组
    _case[v.test.value] = switchCase
})

4、构造分支流程

构造分支流程的思路是,既然进入该 case,那么分发器现在的值就是该 case 的值,那么下一步的流程更新代码,就在该case代码块里面,也就能构造前后分支的关系数组

// 构造 分支 流程
let shufferFlow = [], // 分支流程 数组
    flag = false; // 没有 break 标识
whilePath.node.body.body[0].cases.map((v) => {
    let n = v.consequent.slice(-1)[0] // 获取最后一个代码块  大多数是 break ,不是 break 的话,该 cases 执行完就 跳到下一个 case
    if (type.isBreakStatement(n)) {
        let caseNum = v.test.value
        let nextNode = v.consequent.slice(-2)[0]
        if (nextNode.expression.left.name === shufferVar.name) { // 变量名 与 分发器起始值一样 更新
            let nextNum = caseNum;
            eval(`nextNum  // 下一次 流程
            ${nextNode.expression.operator}
            ${nextNode.expression.right.value}`)
            flag && (nextNum -= 1) && (flag = false) // 特殊处理就是 下一次节点 -1
            shufferFlow.push([caseNum, nextNum])
        }
    } else {
        if (!type.isReturnStatement(n)) {
            // 不是 return 执行完直接到 下一个 cases,但 下一个 case 需要单独处理
            let caseNum = v.test.value
            let nextNode = caseNum + 1
            flag = true // 标识 记录
            shufferFlow.push([caseNum, nextNode])
        }
    }
})

mark

5、还原 分发器

上一步构造出来的 前后分支关系顺序乱的没有还原代码的前后执行关系,所以在还原分发器之前,还需要 通过分发器初始值,还原出代码的前后执行关系。得到代码的前后执行流程后,还原出分发器时,这里取巧的使用了 函数,它原本是用来数组聚合的

// 构造 前后分支关系
let shufferFlow = [], // 前后分支关系
    flag = false; // 没有 break 标识
whilePath.node.body.body[0].cases.map((v) => {
    let n = v.consequent.slice(-1)[0] // 获取最后一个代码块  大多数是 break ,不是 break 的话,该 cases 执行完就 跳到下一个 case
    if (type.isBreakStatement(n)) {
        let caseNum = v.test.value
        let nextNode = v.consequent.slice(-2)[0]
        if (nextNode.expression.left.name === shufferVar.name) { // 变量名 与 分发器起始值一样 更新
            let nextNum = caseNum;
            eval(`nextNum  // 下一次 流程
            ${nextNode.expression.operator}
            ${nextNode.expression.right.value}`)
            flag && (nextNum -= 1) && (flag = false) // 特殊处理就是 下一次节点 -1
            shufferFlow.push([caseNum, nextNum])
        }
    } else {
        if (!type.isReturnStatement(n)) {
            // 不是 return 执行完直接到 下一个 cases,但 下一个 case 需要单独处理
            let caseNum = v.test.value
            let nextNode = caseNum + 1
            flag = true // 标识 记录
            shufferFlow.push([caseNum, nextNode])
        }
    }
})
console.log('前后分支关系:', JSON.stringify(shufferFlow), shufferFlow.length)
// 通过 起始节点 流程还原
let shufferFlow_ = []
shufferFlow.map((v1, i1) => {
    if (shufferVar.value === v1[0]) {
        shufferFlow_.push(v1) // 寻找起头
        for (let i = 0; i < shufferFlow.length; i++) {
            for (let j = 0; j < shufferFlow.length; j++) {
                if (shufferFlow_[i][1] === shufferFlow[j][0]) {
                    shufferFlow_.push(shufferFlow[j]);
                    break
                }
            }
        }
    }
})
console.log('分支执行流程:', JSON.stringify(shufferFlow_))
// 构造分发器
let shufferArr = [];
shufferFlow_.reduce((k1, k2) => {
    k1 && shufferArr.push(...k1)
    k2[1] > 0 && shufferArr.push(k2[1])
})

console.log('分发器数组:', JSON.stringify(shufferArr), shufferArr.length)

mark

6、整体还原

获取完整 分发器后,只需要把 每个 case 里面代码,按照分发器 提取,添加到 父节点的 body 里面就好,这个过程使用判断 删除一点代码就好了

p.remove() // 删除分发器初始值
whilePath.remove() // 删除 while 节点
let parentPath = whilePath.parent
shufferArr.map((v) => {
    if (type.isBreakStatement(_case[v].slice(-1)[0])) {
        _case[v].pop() // 删除 break
        _case[v].pop() // 删除 流程更新节点
    }
    parentPath.body.push(..._case[v]) // 添加到 path 节点
})
console.log('父节点 body 数:', parentPath.body.length)

验证

可以看到已经还原了

mark

mark)

发表评论 / Comment

用心评论~