【公众号】:小鱼神1024
【作者主页】:小鱼神1024
【知识星球】:小鱼神的逆向编程圈
【擅长领域】:JS逆向、小程序逆向、AST还原、验证码突防、Python开发、浏览器插件开发、React前端开发、NestJS后端开发等等
前言
快速掌握纯算还原的技巧,就是要熟悉标准算法日志特点。上一篇讲到 魔改base64
日志分析,想必伙伴们应该对日志有了一定的熟悉感,接下来我们分析 rc4
算法。
插桩
在js代码中,找到 运算符
位置以及 apply
插桩点,结合插桩日志框架,代码插桩如下:
jsvmp源码:https://t.zsxq.com/oUnbQ
日志分析
随着分析的算法难度越来越高,日志的长度也越来越长,这里我们只贴出关键日志,方便大家分析,完整日志可以运行jsvmp源码生成
代码执行后,部分日志如下:
第 1 条: 函数: [Function: apply] 调用者: [Function: fromCharCode] 参数: [ null, [ 0.00390625, 1, 8 ] ] 结果:
第 2 条: 0 + 0 ====> 0
第 3 条: 0 % 3 ====> 0
第 4 条: 函数: [Function: charCodeAt] 调用者: 参数: [ 0 ] 结果: 0
第 5 条: 0 + 0 ====> 0
第 6 条: 0 % 256 ====> 0
第 7 条: 0 + 1 ====> 1
第 8 条: 1 % 3 ====> 1
第 9 条: 函数: [Function: charCodeAt] 调用者: 参数: [ 1 ] 结果: 1
第 10 条: 1 + 1 ====> 2
第 11 条: 2 % 256 ====> 2
第 12 条: 2 + 1 ====> 3
第 13 条: 2 % 3 ====> 2
第 14 条: 函数: [Function: charCodeAt] 调用者: 参数: [ 2 ] 结果: 8
第 15 条: 3 + 8 ====> 11
第 16 条: 11 % 256 ====> 11
第 17 条: 11 + 3 ====> 14
第 18 条: 3 % 3 ====> 0
第 19 条: 函数: [Function: charCodeAt] 调用者: 参数: [ 0 ] 结果: 0
第 20 条: 14 + 0 ====> 14
第 21 条: 14 % 256 ====> 14
第 22 条: 14 + 4 ====> 18
第 23 条: 4 % 3 ====> 1
第 24 条: 函数: [Function: charCodeAt] 调用者: 参数: [ 1 ] 结果: 1
第 25 条: 18 + 1 ====> 19
第 26 条: 19 % 256 ====> 19
第 27 条: 19 + 5 ====> 24
第 28 条: 5 % 3 ====> 2
第 29 条: 函数: [Function: charCodeAt] 调用者: 参数: [ 2 ] 结果: 8
第 30 条: 24 + 8 ====> 32
第 31 条: 32 % 256 ====> 32
第 32 条: 32 + 6 ====> 38
第 33 条: 6 % 3 ====> 0
第 34 条: 函数: [Function: charCodeAt] 调用者: 参数: [ 0 ] 结果: 0
第 35 条: 38 + 0 ====> 38
第 36 条: 38 % 256 ====> 38
第 37 条: 38 + 7 ====> 45
第 38 条: 7 % 3 ====> 1
第 39 条: 函数: [Function: charCodeAt] 调用者: 参数: [ 1 ] 结果: 1
第 40 条: 45 + 1 ====> 46
第 41 条: 46 % 256 ====> 46
第 42 条: 46 + 8 ====> 54
第 43 条: 8 % 3 ====> 2
第 44 条: 函数: [Function: charCodeAt] 调用者: 参数: [ 2 ] 结果: 8
第 45 条: 54 + 8 ====> 62
第 46 条: 62 % 256 ====> 62
第 47 条: 62 + 9 ====> 71
第 48 条: 9 % 3 ====> 0
第 49 条: 函数: [Function: charCodeAt] 调用者: 参数: [ 0 ] 结果: 0
第 50 条: 71 + 0 ====> 71
第 51 条: 71 % 256 ====> 71
第 52 条: 71 + 10 ====> 81
第 53 条: 10 % 3 ====> 1
第 54 条: 函数: [Function: charCodeAt] 调用者: 参数: [ 1 ] 结果: 1
其中:
第 1 条: 函数: [Function: apply] 调用者: [Function: fromCharCode] 参数: [ null, [ 0.00390625, 1, 8 ] ] 结果:
这个日志,轻松还原代码如下:
const key = String.fromCharCode.apply(null, [0.00390625,1,8])
然后往下分析:
第 3 条: 0 % 3 ====> 0
第 8 条: 1 % 3 ====> 1
第 13 条: 2 % 3 ====> 2
第 18 条: 3 % 3 ====> 0
第 23 条: 4 % 3 ====> 1
...
第 1273 条: 254 % 3 ====> 2
第 1278 条: 255 % 3 ====> 0
从这几条日志里,可以推断出,这部分代码是一个循环,而且每次递增 1
, 从 0
循环到 255
继续分析:
第 6 条: 0 % 256 ====> 0
第 7 条: 0 + 1 ====> 1
第 11 条: 2 % 256 ====> 2
第 12 条: 2 + 1 ====> 3
第 16 条: 11 % 256 ====> 11
第 17 条: 11 + 3 ====> 14
第 21 条: 14 % 256 ====> 14
第 22 条: 14 + 4 ====> 18
从这几条日志里,可以发现,每次 % 256
后作为循环的结束,同时得到的值作为下一次循环的起始值。综合这些,尝试写出循环代码:
var s = [];
for (var i = 0; i < 256; i++) {
s[i] = i;
}
var j = 0;
for (var i = 0; i < 256; i++) {
j = (j + s[i] + key.charCodeAt(i % key.length)) % 256;
var temp = s[i];
s[i] = s[j];
s[j] = temp;
}
继续分析:
第 1282 条: 0 + 1 ====> 1
第 1283 条: 1 % 256 ====> 1
第 1284 条: 0 + 21 ====> 21
第 1285 条: 21 % 256 ====> 21
第 1286 条: 216 + 21 ====> 237
第 1287 条: 237 % 256 ====> 237
第 1288 条: 函数: [Function: charCodeAt] 调用者: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 参数: [ 0 ] 结果: 77
第 1289 条: 58 ^ 77 ====> 119
第 1290 条: 函数: [Function: fromCharCode] 调用者: [Function: String] 参数: [ 119 ] 结果: w
第 1291 条: 函数: [Function: push] 调用者: [ 'w' ] 参数: [ 'w' ] 结果: 1
第 1292 条: 1 + 1 ====> 2
第 1293 条: 2 % 256 ====> 2
第 1294 条: 21 + 11 ====> 32
第 1295 条: 32 % 256 ====> 32
第 1296 条: 81 + 11 ====> 92
第 1297 条: 92 % 256 ====> 92
第 1298 条: 函数: [Function: charCodeAt] 调用者: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 参数: [ 1 ] 结果: 111
第 1299 条: 224 ^ 111 ====> 143
第 1300 条: 函数: [Function: fromCharCode] 调用者: [Function: String] 参数: [ 143 ] 结果:
第 1301 条: 函数: [Function: push] 调用者: [ 'w', '\x8F' ] 参数: [ '\x8F' ] 结果: 2
第 1302 条: 2 + 1 ====> 3
第 1303 条: 3 % 256 ====> 3
第 1304 条: 32 + 14 ====> 46
第 1305 条: 46 % 256 ====> 46
第 1306 条: 70 + 14 ====> 84
第 1307 条: 84 % 256 ====> 84
第 1308 条: 函数: [Function: charCodeAt] 调用者: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 参数: [ 2 ] 结果: 122
第 1309 条: 211 ^ 122 ====> 169
第 1310 条: 函数: [Function: fromCharCode] 调用者: [Function: String] 参数: [ 169 ] 结果: ©
第 1311 条: 函数: [Function: push] 调用者: [ 'w', '\x8F', '©' ] 参数: [ '©' ] 结果: 3
第 1312 条: 3 + 1 ====> 4
第 1313 条: 4 % 256 ====> 4
第 1314 条: 46 + 100 ====> 146
第 1315 条: 146 % 256 ====> 146
第 1316 条: 47 + 100 ====> 147
第 1317 条: 147 % 256 ====> 147
第 1318 条: 函数: [Function: charCodeAt] 调用者: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 参数: [ 3 ] 结果: 105
第 1319 条: 37 ^ 105 ====> 76
第 1320 条: 函数: [Function: fromCharCode] 调用者: [Function: String] 参数: [ 76 ] 结果: L
第 1321 条: 函数: [Function: push] 调用者: [ 'w', '\x8F', '©', 'L' ] 参数: [ 'L' ] 结果: 4
第 1322 条: 4 + 1 ====> 5
第 1323 条: 5 % 256 ====> 5
第 1324 条: 146 + 138 ====> 284
第 1325 条: 284 % 256 ====> 28
第 1326 条: 193 + 138 ====> 331
第 1327 条: 331 % 256 ====> 75
第 1328 条: 函数: [Function: charCodeAt] 调用者: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 参数: [ 4 ] 结果: 108
第 1329 条: 198 ^ 108 ====> 170
第 1330 条: 函数: [Function: fromCharCode] 调用者: [Function: String] 参数: [ 170 ] 结果: ª
第 1331 条: 函数: [Function: push] 调用者: [ 'w', '\x8F', '©', 'L', 'ª' ] 参数: [ 'ª' ] 结果: 5
第 1332 条: 5 + 1 ====> 6
第 1333 条: 6 % 256 ====> 6
第 1334 条: 28 + 38 ====> 66
第 1335 条: 66 % 256 ====> 66
第 1336 条: 33 + 38 ====> 71
第 1337 条: 71 % 256 ====> 71
第 1338 条: 函数: [Function: charCodeAt] 调用者: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 参数: [ 5 ] 结果: 108
当上述循环结束后,好像又进入了另外一个循环:
第 1288 条: 函数: [Function: charCodeAt] 调用者: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 参数: [ 0 ] 结果: 77
第 1298 条: 函数: [Function: charCodeAt] 调用者: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 参数: [ 1 ] 结果: 111
第 1308 条: 函数: [Function: charCodeAt] 调用者: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 参数: [ 2 ] 结果: 122
...
第 2388 条: 函数: [Function: charCodeAt] 调用者: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 参数: [ 110 ] 结果: 54
从参数 0
到 110
可以看出,它也是一个循环,而且每次递增 1
。
那 110
代表什么呢?为啥会循环到 110
呢?
经过测试发现,Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36
的长度刚好为 111
,因为从 0
开始,所以循环到 110
就结束了。
我们发现,分析纯算时,不要盲目按部就班直接运算还原,一定要看是否有循环,如果有,找到循环开始和结束尤其重要。
继续分析:
第 1282 条: 0 + 1 ====> 1
第 1283 条: 1 % 256 ====> 1
第 1284 条: 0 + 21 ====> 21
第 1285 条: 21 % 256 ====> 21
第 1286 条: 216 + 21 ====> 237
第 1287 条: 237 % 256 ====> 237
第 1288 条: 函数: [Function: charCodeAt] 调用者: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 参数: [ 0 ] 结果: 77
第 1289 条: 58 ^ 77 ====> 119
第 1290 条: 函数: [Function: fromCharCode] 调用者: [Function: String] 参数: [ 119 ] 结果: w
第 1291 条: 函数: [Function: push] 调用者: [ 'w' ] 参数: [ 'w' ] 结果: 1
这个就是第一次完整循环的日志,这个也比较简单,唯一比较难理解的地方是:
第 1284 条: 0 + 21 ====> 21
其中 21
从哪里来的呢?
对于这种情况,对于缺少经验的伙伴,是非常头疼的了。
还有上述数组交换位置,也是比较难理解的。
这就需要用到监听数组访问和赋值的技巧了。
为了解决这个普遍的通用问题,我封装一个函数配合插桩框架使用,很容易就能找到出 21
是从哪里来的以及如何交换位置的。
多维数组代理-监听数组访问和赋值:https://t.zsxq.com/HQqm9
好了,基于上述日志,我们就可以还原出整个 rc4
加密函数了:
function rc4(plaintext, key) {
var s = [];
for (var i = 0; i < 256; i++) {
s[i] = i;
}
var j = 0;
for (var i = 0; i < 256; i++) {
j = (j + s[i] + key.charCodeAt(i % key.length)) % 256;
var temp = s[i];
s[i] = s[j];
s[j] = temp;
}
var i = 0;
var j = 0;
var cipher = [];
for (var k = 0; k < plaintext.length; k++) {
i = (i + 1) % 256;
j = (j + s[i]) % 256;
var temp = s[i];
s[i] = s[j];
s[j] = temp;
var t = (s[i] + s[j]) % 256;
cipher.push(String.fromCharCode(s[t] ^ plaintext.charCodeAt(k)));
}
return cipher.join("");
}
const key = String.fromCharCode.apply(null, [0.00390625, 1, 8]); // 密钥
const plaintext =
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"; // 明文
// 加密
const ciphertext = rc4(plaintext, key);
console.log("Ciphertext:", ciphertext);
rc4日志特点总结
-
有两个循环,第一个循环是从
0
到255
,第二个循环是从0
到明文长度
。 -
第一个循环中,每次结束都是以
% 256
结束,得到的值作为下一次循环的起始值。 -
第二个循环中,每次结束都是以两个数字
_ ^ _
结束。
满足这几个条件,基本可以尝试套用 rc4
算法模版了。
为了加深 rc4
算法和其变种算法特点,下一篇,我们再分析一个 rc4
变种算法。