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

语言: CN / TW / HK

现在流行使用 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 <closure>, 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 <Map(FIXED_ARRAY_TYPE)>
 - length: 7
           0: 0x01d800293921 <FixedArray[2]>
           1: 0x01d8002938a5 <String[6]: #Hello >
           2: 0x01d800003fd5 <String[1]: #a>
           3: 0x01d8002938b9 <String[6]: #world!>
           4: 0x01d800003fe5 <String[1]: #b>
           5: 0x01d8000059b5 <String[7]: #console>
           6: 0x01d8002027a9 <String[3]: #log>
Handler Table (size = 0)
Source Position Table (size = 0)

代码末尾的常量池给我们提供了重要的参考信息,再配合字节码,我们可以确认这个程序是把两个字符串拼接并输出。

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

[generated bytecode for function:  (0x0269002938b1 <SharedFunctionInfo>)]
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 <Map(FIXED_ARRAY_TYPE)>
 - length: 134
           0: 0x026900293905 <ArrayBoilerplateDescription PACKED_SMI_ELEMENTS, 0x0269002938f9 <FixedArray[1]>>
           1: 0x02690029391d <ArrayBoilerplateDescription PACKED_SMI_ELEMENTS, 0x026900293911 <FixedArray[1]>>
           2: 0x026900293935 <ArrayBoilerplateDescription PACKED_SMI_ELEMENTS, 0x026900293929 <FixedArray[1]>>
           3: 0x02690029394d <ArrayBoilerplateDescription PACKED_SMI_ELEMENTS, 0x026900293941 <FixedArray[1]>>
           4: 0x026900293965 <ArrayBoilerplateDescription PACKED_SMI_ELEMENTS, 0x026900293959 <FixedArray[1]>>
           5: 0x02690029397d <ArrayBoilerplateDescription PACKED_SMI_ELEMENTS, 0x026900293971 <FixedArray[1]>>
//......省略.........

上面的代码中,我看不到常量池的内容,也看不到的 JS API,面对这样的逆向,我认为用 V8 做动态调试是一劳永逸的方法。

最后

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

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