蓝狮注册逆向分析:基于 JS 字节码的保护技术

现在流行使用 JS 字节码对 JavaScript 源码进行保护。我怎么感觉技术也是轮回发展的呢?字节码与 JavaScript 源码的关系,就像汇编与 PE 或 ELF 一样,感觉又回到了 N 年前用 IDA+OD 做逆向的时代。但时代不同了,逆向的程序越来越大,工作量越来越大,需要更有效的自动化手段来降低逆向的工作量。当然,再自动的方法也只能是辅助工具,因为逆向就是个体力活。

我的思路:既然 V8 是 JavaScript 运行的宿主机,那么在 V8 中能看到 JavaScript 的一切行为,那我应该在 V8 中加入必要的监控手段来调试 JS 和字节码,我在 V8 中加入的监控分为以下两类:

(1) 监控每个 JS api,准确地说是所有的 ECMAScript API。

(2) V8中内置 API,这些 API 主要用于为 ECMAScript API 提供基础库,蓝狮官网例如,读取本地时间等一些加解密常用的操作。

这 (2) 点非常用重要,如果不做这一点,我们只能看到字节码表面的信息,数据在字节码内部的流转或操作就捕捉不到了。

这和我一直在研究的 Chromium-powered Taint Tracking 是同样的技术,只是应用场景不同而已。我的最终目标是以 V8 为基础实现一个可监控的、有字节码断点功能的调试器,简单地说就是一个可以动态调试字节码的 OD。

这个目标的原理很简单,但工程量很大,过去的大半年里,我做了不少的逆向工作,在实际中逐渐完善,目前算基本完工了。通过字节码保护 JavaScript,再稍微多加一些无用的字节码(花指令)就可以达到人工难以分析的工作量。我觉得一定要用动态的方法,这样可以跳过大量的干扰指令,在字节码执行过程中去观察一些关键信息,这是我为什么要在 V8 中开发调试器的原因。

两个小例子
下面的代码很简单,hello world,明文代码,没加密。

000001D800293976 @ 0 : 13 00 LdaConstant [0]
000001D800293978 @ 2 : c3 Star1
000001D800293979 @ 3 : 19 fe f8 Mov , r2
000001D80029397C @ 6 : 65 59 01 f9 02 CallRuntime [DeclareGlobals], r1-r2
000001D800293981 @ 11 : 13 01 LdaConstant [1]
000001D800293983 @ 13 : 23 02 00 StaGlobal [2], [0]
000001D800293986 @ 16 : 13 03 LdaConstant [3]
000001D800293988 @ 18 : 23 04 02 StaGlobal [4], [2]
000001D80029398B @ 21 : 21 05 04 LdaGlobal [5], [4]
000001D80029398E @ 24 : c2 Star2
000001D80029398F @ 25 : 2d f8 06 06 GetNamedProperty r2, [6], [6]
000001D800293993 @ 29 : c3 Star1
000001D800293994 @ 30 : 21 02 09 LdaGlobal [2], [9]
000001D800293997 @ 33 : c1 Star3
000001D800293998 @ 34 : 21 04 0b LdaGlobal [4], [11]
000001D80029399B @ 37 : 39 f7 08 Add r3, [8]
000001D80029399E @ 40 : c1 Star3
000001D80029399F @ 41 : 5e f9 f8 f7 0d CallProperty1 r1, r2, r3, [13]
000001D8002939A4 @ 46 : c4 Star0
000001D8002939A5 @ 47 : a9 Return
Constant pool (size = 7)
000001D800293931: [FixedArray] in OldSpace

  • map: 0x01d800002229
  • length: 7
    0: 0x01d800293921
    1: 0x01d8002938a5
    2: 0x01d800003fd5
    3: 0x01d8002938b9
    4: 0x01d800003fe5
    5: 0x01d8000059b5
    6: 0x01d8002027a9
    Handler Table (size = 0)
    Source Position Table (size = 0)
    代码末尾的常量池给我们提供了重要的参考信息,再配合字节码,我们可以确认这个程序是把两个字符串拼接并输出。

下面的代码是 JSFuck,也是 hello world,蓝狮注册但没有任何信息可以帮助我们分析。

[generated bytecode for function: (0x0269002938b1 )]
Bytecode length: 28229
Parameter count 1
Register count 22
Frame size 176
Bytecode age: 0
00000269002947CA @ 0 : 7b 00 CreateEmptyArrayLiteral [0]
00000269002947CC @ 2 : c0 Star4
00000269002947CD @ 3 : 7b 02 CreateEmptyArrayLiteral [2]
00000269002947CF @ 5 : 55 ToBooleanLogicalNot
00000269002947D0 @ 6 : bf Star5
00000269002947D1 @ 7 : 7b 03 CreateEmptyArrayLiteral [3]
00000269002947D3 @ 9 : 39 f5 01 Add r5, [1]
00000269002947D6 @ 12 : bf Star5
00000269002947D7 @ 13 : 7b 04 CreateEmptyArrayLiteral [4]
//……省略………….
Constant pool (size = 134)
0000026900294589: [FixedArray] in OldSpace

  • map: 0x026900002229
  • length: 134
    0: 0x026900293905 >
    1: 0x02690029391d >
    2: 0x026900293935 >
    3: 0x02690029394d >
    4: 0x026900293965 >
    5: 0x02690029397d >
    //……省略………
    上面的代码中,我看不到常量池的内容,也看不到的 JS API,面对这样的逆向,我认为用 V8 做动态调试是一劳永逸的方法。

最后
JavaScript 会越来越复杂,这样的工具使逆向过程变得轻松多了,工欲善其事,必先利其器。但逆向依旧就是体力,该干的事是一点也没有少。

又是好几个月,我天天在啃字节码,做逆向,做 V8 逆向工具。

0 Comments
Leave a Reply