最近在爬取twitter推出的grok3时发现接口对x-client-transaction-id做了校验,遂对生成代码逆向了一波,具体步骤包含了(ps:官方加密代码有两个版本一个是chrome86版本,一个是最新版本,本次逆向为旧版本,新版本会在下一期发出)
1 入口
首先我们全局搜索x-client-transaction-id
,可以清晰的看到o.headers["x-client-transaction-id"] = d.sent
就是生成的结果,在此处下断可以看到d是一个对象,它的arg/sent/_sent都是目标结果,逆着call stack往上推,最终在这个函数不断循环
function n(t, r, e, n, o, i, a) {
try {
var u = t[i](a)
, c = u.value
} catch (l) {
return void e(l)
}
u.done ? r(c) : Promise.resolve(c).then(n, o)
}
Promise.resolve(c).then(n, o)
会将c转换为一个Promise,并为其注册一个回调函数.then(n, o)
,如果成功则调用n函数失败调用o函数。u = t[i](a)
这段代码中t是一个生成器,会不断的从中取出Promise来获取其返回值。如果不理清这一段代码那么后面的定位将及其困难,因为你会发现断点一直在此处来回调用。
既然我们已经知道了现在有n个Promise放在生成器管道中,那我们就只需要追踪其中一个,进入到生成器代码中,找到var l = h(r, e, n);
这个变量就是生成Promise的位置,展开l的arg属性是一个Promise,我们进到源码位置(ondemand.s文件)打上断点,发现arguments就是请求接口名加上请求方式,如此可以猜到加密入参可能包含这两个参数,在继续往下走 又回到了ondemand.s文件,此时就需要注意加密入口可能就在这个文件,代码都是混淆后的,极其难看,但也要硬着头皮大致看一下,然后我们会发现一个熟悉的return n.t15 = n.sent
,sent属性就是我们上面提到的,发现这段代码包含在switch case语句中,是通过n对象的next属性来决定跳到哪里,我们打下断点,断下来后发现结果就在此处。
2 补js环境
找到了入口就成功了一半,接下来我们把ondemand.s
文件整个copy下来放到nodejs环境中。由于在浏览器中有完整的环境所以转到nodejs后我们还需要把浏览器的环境也补齐。这里引入jsdom
库,它是用于在 Node.js 中模拟浏览器的子集功能,以便在无真实浏览器环境下运行前端代码和脚本,在运行代码以前先把这一步做了。解决完这些还不够,浏览器中不同的js脚本之间互相引用,在执行到我们这一步之前已经做了太多工作,我们不可能把这些错综复杂的关系都补上,所以现在我们直接运行这段js代码,根据报错来补充环境。
2.1 入口函数暴露
观察这段代码的入口发现是放在一个很深的嵌套函数中,那怎么在最外部能调用到呢?首先这个代码是一个webpak,可以直接把主要函数体拿出来放入我们自定义的一个函数__dd
中,然后再观察入口函数发现它其实是通过一层一层的函数中return出来的,那这就简单了我们直接把最外层的函数return出来即可。
var __obj = __dd[0][358387]({id: 358387, exports: {}, loaded: false}, new Object(), function () {
console.info(arguments);
});
这样我们就拿到了目标函数,其接受两个参数第一个为接口名,第二个为请求方式,我们运行一下发现返回的还是一个函数,该函数接受一个对象,此时很多小伙伴可能已经不知道咋办了,这个对象该如何去造。此时我们就需要同步去浏览器执行该代码,查看该函数传入的对象并保存到我们的nodejs代码中。其实这种方式是我们补环境的核心,有时候发现我们计算出来的结果和浏览器中不一致就经常会使用这种方式来定位问题。对象造好后,我们直接执行
var __result = __obj()("/2/grok/add_response.json", "POST")({delegate:null, done:false, method:"next", next:0, prev:3});
2.2 jsdom添加HTML内容
执行到这一步我们迎来了第一个问题:TypeError: Cannot read properties of undefined (reading 'childNodes')
,可以清晰的看出这一步是在操作html,但是我们在nodejs环境中怎么能操作html呢?答案是可以的,在我们创建jsdom对象时可以给其传入一个模拟的html文本,我们回到浏览器刷新页面将返回的html整体复制过来注入到jsdom
const dom = new JSDOM('<!DOCTYPE html><html...
然后在执行就发现没有触发该错误了。
在a = nu[O("O@a6", 0, 0, -it)](pu)
打断点,我们发现该函数返回的是一个Uint8数组,该数组值非常重要,我们计算出来的值无法请求通接口原因之一就是它造成的
-
进入函数内部 Gu(Lu(iu(tu(0, 1556, 0, "sWuS") + vu(663, 0, "^Q]f"))[0], tu(0, 1428, 0, "S(!W") + "nt"))
,这个代码的混淆方式是通过tu
、vu
函数来加密字符串的,我们手动还原一下:Gu(Lu(iu("[name^=tw]")[0], "content"))
iu函数中使用css选择器提取html中name^=tw
的元素值即twitter-site-verification
元素的值。取到值后进入Gu函数生成uin8数组
Gu = function(n) {
return new fu(atob(n)[ju(948, "YJOw", 808, 963, 902)]("")[tu(0, 1383, 0, "OIXL")]((function(n) {
return n[tu(0, 1530, 0, "9I@S") + Qu(-416, 0, 0, 0, "O@a6")](0)
})))
}
// 还原之后
Gu = function(n) {
return new fu(atob(n)["split"]("")["map"]((function(n) {
return n["charCodeAt"](0)
})))
}
-
SVG计算:通过还原后可以知道这段代码读取了SVG元素的值,然后进行处理提取该字符串中的所有数字存入一个数组中
Ku = function(n, t) {
return Wu = Wu || Lu(Ju(iu(n))[t[5] % 4][ju(1104, "h^1P", 1124, 1169, 1180) + Fu("U#NX", 1018, 1120, 1240, 1094)][0][Qu(-309, 0, 0, 0, ")]6W") + Fu("YP6T", 947, 1056, 1052, 1037)][1], "d")[tu(0, 1571, 0, "S(!W") + ju(894, "cAs@", 843, 784, 1013)](9)[vu(441, 0, "ky]N")]("C")[Fu("!&pV", 1133, 1181, 1278, 1110)]((function(n) {
return n[vu(517, 0, "U#NX") + "ce"](/[^\d]+/g, " ")[vu(486, 0, "9B[1")]()[Fu("10I8", 1001, 1015, 951, 902)](" ")[Qu(-316, 0, 0, 0, "^ShB")](ou)
}))
}
// 还原之后
Ku = function(n, t) {
return Wu = Wu || Lu(Ju(iu(n))[t[5] % 4]["childNodes"][0]["childNodes"][1], "d")["substring"](9)["split"]("C")["map"]((function(n) {
return n["replace"](/[^\d]+/g, " ")["trim"]()["split"](" ")["map"](ou)
}
))
}
从这里我们可以体会到还原之后代码可读性提高了很多,稍后会继续介绍代码还原操作。
3 Animate实现
我们补环境到这里已经基本完成,我们直接运行代码发现可以拿到计算得到的结果,但是这个值并不能过接口监测,这是为什么呢?而且程序也没有报错,在这个代码中一定有缺少的某些环境并没有以报错的形式体现出来。我们通过nodejs环境和浏览器环境step by step调试,最终发现是由于jsdom缺少animate
函数导致最终生成的结果为假结果。
3.1 源码解析
首先定位到入口并反混淆后:var O = n["animate"](nu["aPbXX"](Mu, t), 4096);
,其中nu["aPbXX"]
函数会返回三个值分别是color、transform、easing,这三个值的计算和之前计算的SVG的值对应,计算后的结果会通过animate函数进行渲染,生成动画,紧接着会马上暂停动画,通过回溯一个固定时间点来计算该时间点的颜色以及旋转角度,最终生成类似{matrix:'matrix(0.986112,0.166080,-0.166080,0.986112,0,0)',color:'rgb(34, 94, 214)'}
3.2 实现Animate
入参:
{
color: ["#2060d5", "#532aef"],
transform: ["rotate(0deg)", "rotate(316deg)"],
easing: "cubic-bezier(0.53,0.85,0.93,-0.89)"}
结果:
{
matrix: "matrix(0.986112, 0.166080, -0.166080, 0.986112, 0, 0)",
color: "rgb(34, 94, 214)"
}
easing描述的是贝塞尔缓动函数的四个入参,其用于将动画的线性进度值映射到非线性进度值,以实现更自然的加速、减速效果,也就是说颜色的渐变以及角度的转换都需要遵循这一规律。如此我们便可以自实现或通过三方库实现动画并提取关键帧的模态,如此我们便可以通过纯代码伪造来过掉其检测点