F5 Networks 收购了Shape Security又叫F5 shape,谷歌可以找到该公司相关资料。很多国外站都使用了该公司的产品作为登录接口的反机器人方案。
Shape是应该是jsvmp的业界天花板了。自定义指令集有200多个。业务代码都编译成了code流。其中指令顺序,js虚拟机函数调用栈,变量存储都是随机的,即使原始收集浏览器信息的函数闭包的调用顺序也会被编译器随机位置。
早在3年前,本人就研究过这个东西,当时没有任何参考资料。纯靠一股子蛮力分析清楚了加解密相关算法。最近看到了一些讨论Shape的帖子,激起了兴趣,又分析下最新版的Shape,有一点点经验,分享出来供大家参考。
一般朴素的处理思路,当然是想办法还原原始代码,这个也确实应该是一劳永逸的终极解决办法。比如AST语法树还原。我也见过使用该方案处理简单VM的实例。但是对于稍微复杂点的VM,这个办法要么工作量很大,要么连理论上的可能性都没有了。对于Shape,我的认知来说,还原原始代码应该是绝无可能。
回过头来讲,我们一般只需要分析清楚这段VM执行了什么就行了,所以不还原也是无所谓的。那要搞清楚VM执行了什么,最好的办法就是插装打日志了。输出所有操作到console控制台。通过分析日志搞清楚VM执行了什么。
再回过头来说,有时候我们都不需要知道这段VM执行了什么。我们只需要调用这段VM跑我们需要的结果就行了。那对于纯算法的VM直接跑就行了。对于收集浏览器信息的vm,就要使用补环境大法了。
chrome浏览器,F12打开开发者工具,打开美西南url,Login登录随便输入账号密码进行登录,触发一个请求,看抓包,请求头里Ee30zvqlwf-A,Ee30zvqlwf-B,Ee30zvqlwf-C,Ee30zvqlwf-D,Ee30zvqlwf-F,Ee30zvqlwf-Z就是Shape加密得到的参数了,不带这几个参数请求是无法成功取到预期结果的。
熟悉加解密的应该都见过异或加密,对于那种动态生成的数据流,异或加密是比较合适的加密方案,搞一个异或加密器,每生成一个字节就从加密器取一个异或的字节,一边序列化一边加密。Shape即是使用此法。具体流程我相信大家F8大法应该能调试清楚
你以为一个简单的异或加密就完了么?不不不!异或加密只是对收集的65个成员单个成员进行了第一轮的加密。这65个成员还要组合在一起,然后进一步进行加密,这个加密才是最难分析清楚的部分。也是最废键盘的部分。
同样!插装打日志大法配合F8 F10 F11单步调试,当你差不多按坏了一个键盘,再投入至少个把月的时间精力,大概能搞清楚执行了什么。这里直接说答案:一个非标准的加密算法,一个16个成员的整数型数组,通过复杂的运算得到一个新的16个成员的整数型数组,和前面的结果进行分组异或。每组完毕后,再次进行复杂的运算,得到一个不同的新的16个成员的整数型数组,继续下一组的异或。如此循环,得到最终的加密后的字节集。
通过上面的分析,可以看到有很多处必须把code流跑一遍的地方。这就意味着脱离这个随时间变化的Shape.js。实际是无法直接伪造参数的。加密用的参数很多都是直接编译到了code流里面。不跑一遍拿不到!那么要拿到这些参数,就只能想办法跑一遍了。无头浏览器是一个办法,但是执行效率堪忧。补环境是一个更好的办法。B站看点教程,然后慢慢补吧。直到随便拿来一个Shape.js都可以在v8里跑出一个可用的请求头参数即可。
下面是收集信息的明文结果(这里贴的是我直接通过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直接边序列化变进行了异或加密。分析这里真的废键盘!