反混淆"控制流平坦化"

问题背景

业务逻辑非常依赖前端的安全产品,比如极验顶象-无感验证、瑞数 等都会对前端代码做混淆。其中有一种强度较高的混淆方式,叫”控制流平坦化”。

“控制流平坦化”是将一些顺序执行的代码,变化成阅读难度更大的while-switch-case或者for-switch-case代码。可以见ControlFlow这个项目的例子。

如果能够”反混淆”这种”控制流平坦化”混淆后的代码,就能让代码变得更容易阅读一些。

本文介绍”反控制流平坦化”的思路和实践过程,最终可以反混淆ControlFlowhttps://obfuscator.io/两个工具的”控制流平坦化”。

反混淆思路:

  • 按照”while-switch-case”执行顺序,拼接case语句

实现过程

  • 怎么实现?

    分为两步:

    1
    2
    * 第一步获取case块的执行顺序
    * 第二部根据case块的执行顺序,拼接case块
  • 怎么获取”case块的执行顺序”?

    思路是在每个case块中”注入代码”,然后在浏览器中执行js代码,执行过程”注入代码”就可以记录执行顺序。我理解这个类似”软件测试”中计算”代码覆盖率”时用到的插桩。

    原始代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    while (!![]){
    switch (...){
    case "0":
    ...
    case "1":
    ...
    }
    }

    插桩后,代码如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    // 利用"插桩"记录while-switch-case执行顺序
    // 集合 = {while id: [case1节点, case2节点,case3节点,case2节点]}

    let while_id_stack = [] // 栈

    let collection = {}

    while (!![]){
    while_statement_id = 111
    while_id_stack.push(while_statement_id)
    collection[while_statement_id] = []

    switch (...){
    case "0":
    case_statement_id = 222
    while_id_on_stack_op = while_id_stack[while_id_stack.length-1]
    collection[while_id_on_stack_op].push(case_statement_id)
    ...
    case "1":
    ...
    }

    while_id_stack.pop()
    }

    record(collection)

    浏览器运行后,集合中就存放有case节点的执行顺序。

    上面都是伪代码,具体实现见 hook.js

  • 怎么拼接case块?

    伪代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    集合 = {while id: [case1节点, case2节点,case3节点,case2节点]}

    函数1:

    遍历ast,如果遇到while语句 {
    得到当前while id

    if while id in 集合:
    顺序拼接各个case块内容

    用"拼接后的顺序语句"替换"while语句"

    从集合中删除 while id记录
    }

    while 集合中还有元素:

    调用 函数1

    具体实现见 simple_decode.js

实现结果

可以将”while-switch-case代码块”变成”顺序执行的代码块”。

比如 PlayWithObfuscator/tests/encoded_js/simple.js 中的混淆后的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
while (!![]) {
switch (_0xcgcd3c[_0x3922ce++]) {
case "0":
if (_0x33g15b = _0x5153cd.charCodeAt(_0xeea791 += .75), _0x33g15b > 255) throw new t("'btoa' failed: The string to be encoded contains characters outside of the Latin1 range.");
continue;

case "1":
_0xed1fcd = _0xed1fcd << 8 | _0x33g15b;
continue;
}

break;
}

转换成 阅读性更好一些的代码

1
2
if (_0x33g15b = _0x5153cd.charCodeAt(_0xeea791 += .75), _0x33g15b > 255) throw new t("'btoa' failed: The string to be encoded contains characters outside of the Latin1 range.");
_0xed1fcd = _0xed1fcd << 8 | _0x33g15b;

总结

目前属于demo阶段,可以对以下工具的”控制流平坦化”做反混淆:

测试结果在 tests目录 看到

参考

JS反混淆-控制流平展(一)