今天我们来研究下,某头条的jsvmp逻辑层加密算法,其主要的目的是想在大家在接触此类算法时,给出点实质性的建议和思路。
0x01 分析加密
进入到目标网站通过分析请求会发现一个动态的 _signature 加密参数
0x02 定位加密
不同于以往,我们这次使用 ajax 请求拦截方式,来对这个参数进行拦截
通过堆栈的调用信息,得出以下结果
我们分析得知: _signature 参数由 window.byted_arawler.sign 方法实现,然而这个方法指向 acrawler.js 文件中一个莫名其妙的位置
所以我们目的也有了初步的明确:只要让 window 对象拥有 byted_acrawler 属性,那么整套算法就会被我们完全击破
我们在本地新建一个 HTML 文件把代码整体拷贝过来,然后用浏览器打开
我们惊奇的发现,这个 byted_acrawler 属性已经存在于 window 对象之中,但是当我们借用 node 去执行它的时候,却秒被现实打脸
我们先整体看下代码,除了 if-else 就是 else-if 活活的一个无限套娃形式,而且里面的逻辑,只用单一的字母表示,根本没有任何阅读性
虽然通篇都是 if-else 但它肯定也符合一定的逻辑规律,如若不然,它一定会报错。只不过官方这样做的目的,让它失去可读性,从而导致我们无法对分析。试想,我们可不可以,把它换一种形式进行表示出来,从而恢复它的可读性呢?
0x06 平坦化流程
这是一个全新的概念,也是对抗 jsvmp 算法必会的概念,虽然方式比较简单,但工程量比较庞大。我们需要将代码中所有的 if-else 替换成 switch-case 但问题在于 switch 哪个变量 case 那种情况?
观察一下这段代码:
var j = parseInt("" + b[O] + b[O + 1], 16);
验证一下,整体流程会不会因为j 变量的动态变化,进入到不同的逻辑分支?
不妨我们通过开篇所讲的条件断点试一下,每次 j=16 时会不会进入同一个逻辑分支中,如果能,说明 j 所有 if-else 的动态参数。如果不能,那就再去验证其他参数。
经验证,我们可以确定 j=16 时都会进入到上述的分支,所以我们不妨可以这样将它表示出来
switch (j) {
case 0:
// todo somethind
break;
case 16:
q = S[R--],
w = S[R--],
(A = S[R--]).x === G ? A.y >= 1 ? S[++R] = K(b, A.c, A.l, q, A.z, w, null, 1) : (S[++R] = K(b, A.c, A.l, q, A.z, w, null, 0),
A.y++) : S[++R] = A.apply(w, q);
break;
default:
//todo something
return;
}
最终我们梳理的结果是这样的
这里注意一下:x 会有一个偏移量计算,每次运行都会 x>>4 并且 x 和 A 的值相等
0x07 环境对比
等我们对代码平坦化流程之后,我们需要考虑之前的一个问题,为什么浏览器环境就可以拥有byted_acrawler 而 node 环境却是 undefined 呢?可能的原因是,它肯定存在某种环境的检测,导致 node 环境失效
我们先从运行的开端分析一下(这也是很多人会忽略的细节)
glb = "undefined" == typeof window ? global : window
这段代码放到浏览器运行,代表的是 window 对象
如果把它放到 node 环境运行,它代表却是 global 对象
所以我们需要改变下 node 环境,这里我采用的是 jsdom 来对环境进行一下伪装,这样的话,我们就改变了初始的环境
修改完环境之后,我们再次运行我们本地文件,我们会发现抛出一个错误
这是 case 26 流程所抛出的,所以我们直接在官方打条件断点进行比对
这是官方的结果
经过对比,官方 S 中拥有Object 函数而本地却是 undefined 所以我们要给它添加这种属性
window.Object = function (){
return ["native code"];
}
添加完之后自然有了这种属性
0x09 环境补全
鉴于这个对比过程太过漫长,我这里主要列出几个重要的属性,其他的请自行校验:
1.补齐 canvas 属性
window.HTMLCanvasElement.prototype.getContext = function () {
return {
fillText(text, x, y, maxWidth) {
return ["native code"];
},
arc(x, y, radius, startAngle, endAngle, anticlockwise) {
return ["native code"];
},
stroke() {
return ["native code"]
},
getExtension() {
return ["native code"]
},
getParameter() {
return ["native code"]
}
}
}
2.补齐 localStorage(这个会影响 _signature 生成的长度)
(function () {
let localStorage = {
"__tea_cache_tokens_2018": "{\"user_unique_id\":\"verify_kut5p98i_BUC0aRbH_QZ5B_4lu4_BQzz_F22NI2z6snKh\",\"web_id\":\"7018918857491351074\",\"timestamp\":1634353853331}",
"__tea_cache_tokens_24": "{\"web_id\":\"7018898163830441479\",\"ssid\":\"522d279c-ba4b-4c85-a61c-98b7f1a05b71\",\"user_unique_id\":\"7018898163830441479\",\"timestamp\":1634219409505}",
"__tea_cache_first_24": "1",
"tt_scid": "z1OscwP3.P6dJQt3WyFHyZ65WZCoGUYARVlW9x0SAYAXAN.mmr9m1v6FlmpA1M98f94d",
"__tea_cache_first_2018": "1",
"_byted_param_sw": "lmr6SespEoX9rv9+V/8="
}
for (var p in localStorage) {
window.localStorage.setItem(p, localStorage[p])
}
})()
resourceLoader = new jsdom.ResourceLoader({
strictSSL: false,
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36",
});
const jsdomConfig = {
resources: resourceLoader,
url: "https://www.toutiao.com/",
}
0x10 简洁化处理
如果细心的话会发现,每个分支的代码,只运行了一小部分就跳出了所在的分支,剩下的代码形同虚设般存在。之所以会这样,是因为分支内部的 A 值会影响代码的整体走势,所以我们只需要判断满足 A的代码就可以,其他的都是在尸位素餐
精简之前
case 25:
(A = x) > 8 ? (C = S[R--],
S[R] = typeof C) : A > 6 ? S[R] = --S[R] : A > 4 ? S[R -= 1] = S[R][S[R + 1]] : A > 2 && (q = S[R--],
(A = S[R]).x === G ? A.y >= 1 ? S[R] = K(b, A.c, A.l, [q], A.z, w, null, 1) : (S[R] = K(b, A.c, A.l, [q], A.z, w, null, 0),
A.y++) : S[R] = A(q));
break;
精简之后
case 25:
S[R -= 1] = S[R][S[R + 1]]
break;
0x11 总结
1.对于这种混淆的代码而言,流程平坦化是首选,但如何进行才是关键
2.代码不仅想知道你的运行环境,它更想知道这种环境是否真的适合它
最终效果:
✧✧ 本片文章只用于学习交流,切勿用于商业用途,如果有侵权行为,请与作者联系,本人会在第一时间将其删除。✧✧