首页
社区
课程
招聘
[原创]F5 Shape最新版逆向分析-加解密和补环境
2024-4-24 18:51 1809

[原创]F5 Shape最新版逆向分析-加解密和补环境

2024-4-24 18:51
1809

F5 Shape最新版逆向分析-加解密和补环境

前言

F5 Networks 收购了Shape Security又叫F5 shape,谷歌可以找到该公司相关资料。很多国外站都使用了该公司的产品作为登录接口的反机器人方案。

比如:美西南、xbk等,下文称之为Shape

1
url: aHR0cHM6Ly93d3cuc291dGh3ZXN0LmNvbS8=

Shape是应该是jsvmp的业界天花板了。自定义指令集有200多个。业务代码都编译成了code流。其中指令顺序,js虚拟机函数调用栈,变量存储都是随机的,即使原始收集浏览器信息的函数闭包的调用顺序也会被编译器随机位置。

早在3年前,本人就研究过这个东西,当时没有任何参考资料。纯靠一股子蛮力分析清楚了加解密相关算法。最近看到了一些讨论Shape的帖子,激起了兴趣,又分析下最新版的Shape,有一点点经验,分享出来供大家参考。

JS逆向和安全防护de研究交流可以私聊我

jsvmp

官方资料或者chatgpt的回答,我就懒得抄了,读者可以自行搜索。只说下个人的理解,还有分析jsvmp的大概思路。

PC端的vmp。鼎鼎大名,读者应该都用过或者听过。所谓vmp就是把简单的几行代码,通过自定义指令的方式,把代码膨胀几百倍,让逆向者找不到头绪。

举个简单例子:

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
C++:
void funcAdd(a, b){
    return a+b;
}
 
asm:
.text:0000000140001000 sub_140001000   proc near               ; CODE XREF: sub_140001020+E↓p
.text:0000000140001000                                         ; sub_140001020+21↓p
.text:0000000140001000
.text:0000000140001000 arg_0           = dword ptr  8
.text:0000000140001000 arg_8           = dword ptr  10h
.text:0000000140001000
.text:0000000140001000                 mov     [rsp+arg_8], edx
.text:0000000140001004                 mov     [rsp+arg_0], ecx
.text:0000000140001008                 mov     eax, [rsp+arg_8]
.text:000000014000100C                 mov     ecx, [rsp+arg_0]
.text:0000000140001010                 add     ecx, eax
.text:0000000140001012                 mov     eax, ecx
.text:0000000140001014                 retn
.text:0000000140001014 sub_140001000   endp
 
vmp:
自己搞一个编译器,把汇编指令转为自定义指令,比如把+这个指令对应的add汇编指令搞成一个函数,然后里面各种弯弯绕。
自己维护一个函数的调用栈,变量栈。
上面几行汇编就在vmp这个虚拟机里跑成千上万条,那对逆向分析的人来说,就很难搞清楚这个成千上万到底跑了个啥。
 
jsvmp:
与PC端vmp类似,也是把简单的指令复杂化,具体到Shape的+指令就对应于好几个函数
    , function(c) {
        c.x[c.x.length - 2] = c.x[c.x.length - 2] + c.x[c.x.length - 1]; //c.x就是局部变量栈
        c.x.length -= 1
    }
    , function(c) {
        var E = j[c.W];
        var F = j[c.W + 1];
        c.W += 2;
        var T = c.y.c(E);
        var a = T[F];
        var i = c.x[c.x.length - 1];
        c.x[c.x.length - 1] = i + a
    }
其他各种运算符,比如- & | > < == ! 等等都是如此,对应于一个函数。这样做的目的就是可以自行维护一个函数调用栈和变量栈

所以Shape这个jsvmp也是一个所谓的栈式虚拟机

其实当下前端安全领域,为了隐藏加解密核心代码或者隐藏收集了哪些浏览器信息,VM的种类和实现已经成千上万了。经常分析这类型的人应该是对此一点都不陌生了。

处理jsvmp的思路

一般朴素的处理思路,当然是想办法还原原始代码,这个也确实应该是一劳永逸的终极解决办法。比如AST语法树还原。我也见过使用该方案处理简单VM的实例。但是对于稍微复杂点的VM,这个办法要么工作量很大,要么连理论上的可能性都没有了。对于Shape,我的认知来说,还原原始代码应该是绝无可能。

回过头来讲,我们一般只需要分析清楚这段VM执行了什么就行了,所以不还原也是无所谓的。那要搞清楚VM执行了什么,最好的办法就是插装打日志了。输出所有操作到console控制台。通过分析日志搞清楚VM执行了什么。

再回过头来说,有时候我们都不需要知道这段VM执行了什么。我们只需要调用这段VM跑我们需要的结果就行了。那对于纯算法的VM直接跑就行了。对于收集浏览器信息的vm,就要使用补环境大法了。

实战Shape

1.找到shape在哪

chrome浏览器,F12打开开发者工具,打开美西南url,Login登录随便输入账号密码进行登录,触发一个请求,看抓包,请求头里Ee30zvqlwf-A,Ee30zvqlwf-B,Ee30zvqlwf-C,Ee30zvqlwf-D,Ee30zvqlwf-F,Ee30zvqlwf-Z就是Shape加密得到的参数了,不带这几个参数请求是无法成功取到预期结果的。

image

查看启动器也就是请求的调用堆栈即可找到Shape所在的js

image

Shape的特征就是带有如下js

image

image

2.分析Shape执行了什么

插装打日志大法配合F8 F10 F11单步调试,当你差不多按坏了一个键盘,投入至少个把月的时间精力,大概能搞清楚执行了什么。

这里直接说答案:收集浏览器信息,然后加密成上面请求里的各个参数。收集的浏览器信息多到只有你想不到的,没有他们不采集的。

美西南收集了66个浏览器相关的信息

1
2
3
(obj.member=value) ->   (66) [{…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, Array(2),
{…}, {…}, {…}, {…}, {…}, {…}, Array(2), {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…},
{…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}] 34 (2) [Array(0), Array(13)]

这里简单拿收集信息里的66份之一做个演示。

成员29收集浏览器基础信息

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
下面是收集信息的明文结果(这里贴的是我直接通过Ee30zvqlwf-A参数,反向解密取到的,因为Shape是一边序列化一边加密的,所以是断不到明文瞬间的。我这里用反向解密拿到)
解密成员:29
Binary size: 308
35 2E 30 20 28 57 69 6E 64 6F 77 73 20 4E 54 20     53  46  48  32  40  87  105 110 100 111 119 115 32  78  84  32      5.0.(Windows.NT.
31 30 2E 30 3B 20 57 69 6E 36 34 3B 20 78 36 34     49  48  46  48  59  32  87  105 110 54  52  59  32  120 54  52      10.0;.Win64;.x64
29 20 41 70 70 6C 65 57 65 62 4B 69 74 2F 35 33     41  32  65  112 112 108 101 87  101 98  75  105 116 47  53  51      ).AppleWebKit/53
37 2E 33 36 20 28 4B 48 54 4D 4C 2C 20 6C 69 6B     55  46  51  54  32  40  75  72  84  77  76  44  32  108 105 107     7.36.(KHTML,.lik
65 20 47 65 63 6B 6F 29 20 43 68 72 6F 6D 65 2F     101 32  71  101 99  107 111 41  32  67  104 114 111 109 101 47      e.Gecko).Chrome/
31 32 32 2E 30 2E 30 2E 30 20 53 61 66 61 72 69     49  50  50  46  48  46  48  46  48  32  83  97  102 97  114 105     122.0.0.0.Safari
2F 35 33 37 2E 33 36 00 84 32 30 30 33 30 31 30     47  53  51  55  46  51  54  0   132 50  48  48  51  48  49  48      /537.36..2003010
37 00 47 65 63 6B 6F 00 37 1F 88 28 03 4D 6F 7A     55  0   71  101 99  107 111 0   55  31  136 40  3   77  111 122     7.Gecko.7..(.Moz
69 6C 6C 61 00 00 80 00 00 00 00 00 00 F8 3F 18     105 108 108 97  0   0   128 0   0   0   0   0   0   248 63  24      illa..........?.
39 23 18 93 31 2F 4E 65 74 73 63 61 70 65 00 3C     57  35  24  147 49  47  78  101 116 115 99  97  112 101 0   60      9#..1/Netscape.<
0B 47 6F 6F 67 6C 65 20 49 6E 63 2E 00 20 2D 20     11  71  111 111 103 108 101 32  73  110 99  46  0   32  45  32      .Google.Inc...-.
50 38 2B 20 50 4D 6F 7A 69 6C 6C 61 2F 35 2E 30     80  56  43  32  80  77  111 122 105 108 108 97  47  53  46  48      P8+.PMozilla/5.0
20 28 57 69 6E 64 6F 77 73 20 4E 54 20 31 30 2E     32  40  87  105 110 100 111 119 115 32  78  84  32  49  48  46      .(Windows.NT.10.
30 3B 20 57 69 6E 36 34 3B 20 78 36 34 29 20 41     48  59  32  87  105 110 54  52  59  32  120 54  52  41  32  65      0;.Win64;.x64).A
70 70 6C 65 57 65 62 4B 69 74 2F 35 33 37 2E 33     112 112 108 101 87  101 98  75  105 116 47  53  51  55  46  51      ppleWebKit/537.3
36 20 28 4B 48 54 4D 4C 2C 20 6C 69 6B 65 20 47     54  32  40  75  72  84  77  76  44  32  108 105 107 101 32  71      6.(KHTML,.like.G
65 63 6B 6F 29 20 43 68 72 6F 6D 65 2F 31 32 32     101 99  107 111 41  32  67  104 114 111 109 101 47  49  50  50      ecko).Chrome/122
2E 30 2E 30 2E 30 20 53 61 66 61 72 69 2F 35 33     46  48  46  48  46  48  32  83  97  102 97  114 105 47  53  51      .0.0.0.Safari/53
37 2E 33 36 00 57 69 6E 33 32 00 22 2F A0 01 07     55  46  51  54  0   87  105 110 51  50  0   34  47  160 1   7       7.36.Win32."/...
14 00 A5 01                                         20  0   165 1                                                       ....
 
这个成员在收集的65个成员里占到第29位,当然这个到底占数组哪个位置不是固定的!
这也是Shape最难处理的地方,不同时段获取到的Shape.js对应的这个占位是不同的!
跟code流直接相关。意味着必须把code跑一遍才能拿到这个位置。
 
收集过程可以打日志分析函数调用取到
(call) ->   45170 Arguments(3) ['5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.…KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36', Array(4), 3, callee: (...), Symbol(Symbol.iterator): ƒ]
(call) ->   45170 Arguments(3) ['20030107', Array(109), 108, callee: (...), Symbol(Symbol.iterator): ƒ]
(call) ->   45170 Arguments(3) ['Gecko', Array(118), 117, callee: (...), Symbol(Symbol.iterator): ƒ]
(call) ->   68549 Arguments(4) [ƒ, 1028, Array(124), 123, callee: (...), Symbol(Symbol.iterator): ƒ]
(call) ->   68549 Arguments(4) [ƒ, -21334, Array(127), 126, callee: (...), Symbol(Symbol.iterator): ƒ]
(call) ->   45170 Arguments(3) ['Mozilla', Array(130), 129, callee: (...), Symbol(Symbol.iterator): ƒ]
(call) ->   45170 Arguments(3) ['', Array(138), 137, callee: (...), Symbol(Symbol.iterator): ƒ]
(call) ->   68549 Arguments(4) [ƒ, 1.5, Array(139), 138, callee: (...), Symbol(Symbol.iterator): ƒ]
(call) ->   68549 Arguments(4) [ƒ, 24, Array(148), 147, callee: (...), Symbol(Symbol.iterator): ƒ]
(call) ->   68549 Arguments(4) [ƒ, 27, Array(149), 148, callee: (...), Symbol(Symbol.iterator): ƒ]
(call) ->   68549 Arguments(4) [ƒ, 24, Array(150), 149, callee: (...), Symbol(Symbol.iterator): ƒ]
(call) ->   68549 Arguments(4) [ƒ, 159, Array(152), 151, callee: (...), Symbol(Symbol.iterator): ƒ]
(call) ->   45170 Arguments(3) ['Netscape', Array(154), 153, callee: (...), Symbol(Symbol.iterator): ƒ]
(call) ->   68549 Arguments(4) [ƒ, -21334, Array(163), 162, callee: (...), Symbol(Symbol.iterator): ƒ]
(call) ->   45170 Arguments(3) ['Google Inc.', Array(166), 165, callee: (...), Symbol(Symbol.iterator): ƒ]
(call) ->   68549 Arguments(4) [ƒ, 1440, Array(178), 177, callee: (...), Symbol(Symbol.iterator): ƒ]
(call) ->   68549 Arguments(4) [ƒ, 2560, Array(180), 179, callee: (...), Symbol(Symbol.iterator): ƒ]
(call) ->   68549 Arguments(4) [ƒ, 1400, Array(182), 181, callee: (...), Symbol(Symbol.iterator): ƒ]
(call) ->   68549 Arguments(4) [ƒ, 2560, Array(184), 183, callee: (...), Symbol(Symbol.iterator): ƒ]
(call) ->   45170 Arguments(3) ['Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWeb…KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36', Array(186), 185, callee: (...), Symbol(Symbol.iterator): ƒ]
(call) ->   45170 Arguments(3) ['Win32', Array(298), 297, callee: (...), Symbol(Symbol.iterator): ƒ]
(call) ->   68549 Arguments(4) [ƒ, 1515, Array(304), 303, callee: (...), Symbol(Symbol.iterator): ƒ]
(call) ->   68549 Arguments(4) [ƒ, 20, Array(309), 308, callee: (...), Symbol(Symbol.iterator): ƒ]
(call) ->   68549 Arguments(4) [ƒ, 0, Array(310), 309, callee: (...), Symbol(Symbol.iterator): ƒ]
 
上面每一次函数call。都会把字符串或者其他类型数字序列化为字节集,这个时候就直接进行异或加密了。最终取到一个加密后的字节集
 
[35, 73, 182, 48, 124, 183, 46, 145, 161, 62, 250, 241, 195, 207, 25, 135, 17, 139, 212, 207, 240, 106, 59, 222, 246, 54, 55, 207,
3, 188, 64, 9, 71, 111, 178, 37, 183, 163, 194, 142, 193, 236, 143, 68, 199, 4, 150, 174, 243, 184, 245, 139, 123, 139, 242, 254,
161, 52, 53, 91, 198, 227, 165, 104, 86, 14, 163, 81, 77, 91, 255, 254, 69, 174, 12, 63, 45, 21, 148, 34, 121, 161, 38, 11, 226,
216, 52, 222, 4, 98, 22, 20, 52, 185, 150, 169, 205, 214, 3, 105, 95, 33, 238, 41, 35, 86, 32, 98, 129, 103, 254, 216, 228, 214,
159, 8, 80, 95, 218, 39, 74, 59, 233, 41, 34, 62, 234, 131, 152, 161, 113, 235, 197, 58, 105, 142, 184, 112, 195, 125, 99, 6, 229,
227, 248, 134, 54, 147, 17, 38, 159, 222, 44, 249, 78, 108, 233, 2, 155, 172, 26, 65, 192, 154, 78, 150, 144, 31, 205, 140, 229,
157, 23, 183, 32, 237, 214, 122, 127, 223, 53, 169, 175, 171, 181, 198, 25, 149, 150, 233, 30, 41, 224, 120, 114, 193, 216, 164,
184, 47, 196, 45, 207, 76, 85, 45, 165, 229, 224, 204, 233, 250, 70, 99, 191, 171, 225, 120, 162, 111, 231, 254, 193, 174, 1, 241,
37, 38, 16, 55, 89, 238, 28, 47, 58, 183, 169, 162, 45, 130, 127, 163, 95, 26, 66, 198, 137, 183, 102, 173, 55, 77, 203, 101, 100,
164, 131, 53, 161, 146, 137, 83, 153, 250, 173, 189, 242, 79, 146, 78, 212, 85, 144, 228, 238, 169, 93, 19, 154, 107, 54, 49, 51,
214, 120, 233, 129, 254, 31, 118, 15, 157, 149, 113, 4, 157, 129, 125, 168, 81, 23, 239, 253, 37, 101, 89, 119, 197, 31, 31, 61,
48]
 
因为是异或加密的结果,而且异或加密使用了随机数作为起始算子,所以这个字节集的结果不是固定的。
这里的关键就是Shape直接边序列化变进行了异或加密。分析这里真的废键盘!

异或加密

熟悉加解密的应该都见过异或加密,对于那种动态生成的数据流,异或加密是比较合适的加密方案,搞一个异或加密器,每生成一个字节就从加密器取一个异或的字节,一边序列化一边加密。Shape即是使用此法。具体流程我相信大家F8大法应该能调试清楚

image

上图即可实现一个异或加密器,初始参数s3是js自带字符串校验所得

image

初始参数s4就比较变态了,是随机数 * js浮点数表里的位置不固定的值。正是由于这个浮点数的位置不定,所以也决定了必须跑一遍Code流才行。

image

进一步加密

你以为一个简单的异或加密就完了么?不不不!异或加密只是对收集的65个成员单个成员进行了第一轮的加密。这65个成员还要组合在一起,然后进一步进行加密,这个加密才是最难分析清楚的部分。也是最废键盘的部分。

同样!插装打日志大法配合F8 F10 F11单步调试,当你差不多按坏了一个键盘,再投入至少个把月的时间精力,大概能搞清楚执行了什么。这里直接说答案:一个非标准的加密算法,一个16个成员的整数型数组,通过复杂的运算得到一个新的16个成员的整数型数组,和前面的结果进行分组异或。每组完毕后,再次进行复杂的运算,得到一个不同的新的16个成员的整数型数组,继续下一组的异或。如此循环,得到最终的加密后的字节集。

这个加密算法3年前就是如此,现在也完全没变。

image

变异Base64加密

通过上一步加密得到的字节集大概就是几千个字节的加密字节,这个数组当然不能直接作为请求参数,所以又进行了一层变异Base64加密。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//t就是编码表 Shape是js里自带的
function VariantBase64(str,t) {
    var len = str.length;
    var r = new Uint8Array(Math.floor(len * 3 / 4));
    var m1, m2, m3, m4, k1, k2, k3;
    for (var i = 0, j = 0; i < len; i += 4, j += 3) {
        m1 = t.indexOf(str[i]);
        m2 = t.indexOf(str[i + 1]);
        m3 = t.indexOf(str[i + 2]);
        m4 = t.indexOf(str[i + 3]);
        k1 = m1 << 2 | m2 >> 4;
        k2 = (m2 & 15) << 4 | m3 >> 2;
        k3 = (m3 & 3) << 6 | m4;
        r[j] = k1;
        if (i + 2 < len) {
            r[j + 1] = k2
        }
        if (i + 3 < len) {
            r[j + 2] = k3
        }
    }
    return r
}

image

至此请求头里的参数A就出来了

3.请求头其他几个参数

1
2
3
4
5
6
7
8
9
10
11
12
13
a: 收集的浏览器信息加密的
 
b: 参数f+参数a的校验
 
c: code里字符串解密跑出来的
"LqUN19-tNV_7vDBqS3BjTwImSWByl9bgefykao7k2F7_PcfDTk8bWGEkCs63HonbNBwcao9mMxGoqGUH0X7iyZx6jgn6umUPuysJU0Df-Es8mzMYSJKzjd6xV_k,306"
:"AEBwWCuOAQAAIDGyOzYDrlXg7UWJqv7slOCdALh1laeWUHYLsA_5IphsAz9I"
 
d: code里字符串解密跑出来的
qZEp4vGadEbZjgpzMEtMYCQXWVdjxZiFT824Upytvz2FWqfKHDQ6Z3hMCqqXGt7XJERkUb1mfk-cuxU-51TkxqMTriqPv0wB0wgoNzT0w2J7unIpdtHp7-mnJtkHZiK605vjebwd5QDZU8Ydx4R11TCiiiheFmpwjB8BEu_31G9QCozrAJOV1iVMrgPVOSkeHOoQ4zpcOFTkR8Q,118
: "ABaAhIDBCKGFgQGAAYIQgISigaIAwBGAzvpizi_33wcP-SKYbAM_SP____-_HhmNAL5xVFHc5SurCp9zHqMVMDk"
 
f: js里带的

字符串解密过程无需分析,因为无法脱离code流。也是异或,异或用的字符串在加密字符串表中的位置也是code流里写死的。

补环境

通过上面的分析,可以看到有很多处必须把code流跑一遍的地方。这就意味着脱离这个随时间变化的Shape.js。实际是无法直接伪造参数的。加密用的参数很多都是直接编译到了code流里面。不跑一遍拿不到!那么要拿到这些参数,就只能想办法跑一遍了。无头浏览器是一个办法,但是执行效率堪忧。补环境是一个更好的办法。B站看点教程,然后慢慢补吧。直到随便拿来一个Shape.js都可以在v8里跑出一个可用的请求头参数即可。

1
我参考的B站教程:https://www.bilibili.com/video/BV11o4y1D7m6?p=37&vd_source=e8f93da27c5f30218e12cae3fb0ccd1b

image

补环境也是掉头发的活儿!尤其Shape收集的东西太多了!!!!!!抓狂呀。

展示

image

总结

  1. Shape加密的分析真的是一个极其掉头发的活儿。根源就是他的原始明文js就很复杂,收集的浏览器信息又非常多。那编译成code流在VM里跑,光执行自定义指令就有几百万次。
  2. 这个东西只是繁杂,只要投入精力还是能搞清楚的。


[培训]二进制漏洞攻防(第3期);满10人开班;模糊测试与工具使用二次开发;网络协议漏洞挖掘;Linux内核漏洞挖掘与利用;AOSP漏洞挖掘与利用;代码审计。

最后于 2024-4-25 18:01 被rushmaster编辑 ,原因:
收藏
点赞3
打赏
分享
最新回复 (2)
雪    币: 19634
活跃值: (29304)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
秋狝 2024-4-25 09:18
2
1
感谢分享
雪    币: 3710
活跃值: (3924)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
caolinkai 2024-4-25 12:54
3
0
感谢分享
游客
登录 | 注册 方可回帖
返回