写点什么

JS 逆向实战:AST 技术让你轻松破解 OB 混淆保护!

作者:LLLibra146
  • 2025-01-27
    北京
  • 本文字数:4154 字

    阅读完需:约 14 分钟

引言

还是原来的爬虫练习平台,本文的重点是 JS 逆向中的 OB 混淆处理。


spa13

spa13 地址:https://spa13.scrape.center/


spa13 说明:


NBA 球星数据网站,数据纯前端渲染,Token 经过加密处理, JavaScript 经过 JavaScript Obfuscator 混淆,适合 JavaScript 逆向分析。


老规矩,先打开 main.js 看看:



初步看还是可以看出来代码结构的,但是似乎所有的字符串都被转成十六进制了,并且有的字符串还被加密了,要调用一个特定的函数才能被解密。

OB 混淆

这个就是 OB 混淆,比较明显的特征就是开头会创建一个很大的数组,里面是被加密后的字符串,然后会对大数组进行反转操作,一般这里面还会掺杂一些格式化检测或者其他的检测,最后就是提供一个函数供解密用。

AST 解混淆

AST 是源代码的抽象语法结构的树状表现形式,它以一种结构化的方式来呈现代码的语法结构 。在这个树状结构里,每个节点都代表着源代码中的一种语法结构,比如变量声明、函数调用、运算符表达式等。


例如,对于简单的 JavaScript 语句 let num = 1 + 2;,AST 会将其解析为一个包含变量声明节点、赋值运算符节点、数字字面量节点和加法运算符节点的树状结构。变量声明节点表示 let num,赋值运算符节点表示 =,两个数字字面量节点分别表示 12,加法运算符节点表示 +,通过这些节点之间的层级关系和连接,精确地描述了代码的语法构成。


AST 可以让我们从结构化的角度来审视混淆后的代码。通过解析混淆代码生成 AST,我们能够清晰地看到代码的真实逻辑结构,不受变量名或代码顺序变化的干扰,这就为我们解码 OB 混淆提供了很大的帮助,我们可以借助 AST 来分析代码结构,并且还可以动态的修改原始代码,去除或还原被混淆的部分代码。


为了方便写 AST 解混淆的代码,我们需要一个网站来帮助我们生成 AST 语法树作为参考,我比较常用的是:https://astexplorer.net/


先试试刚才的混淆代码:



上图就是 AST 语法树,左侧是我们的原始代码,右侧是编译后的语法树结构,点击左侧的任意代码,右边就会自动跳转到对应的 AST 语法树节点上。现在图上显示的是一个 ArrayExpression 结构,这是一个数组节点,数组节点中有很多的 StringLiteral 节点,这个节点就是字符串节点,还有很多种节点我这里就不多介绍了,通过代码和语法树的对比我们就可以知道哪一行代码对应哪种结构,并不需要知道所有的节点类型。


为了使用 AST 来解混淆,我们除了使用网页来解混淆,还需要在本地安装 nodejs 运行环境,并且使用 npm 安装对应的 babel 库,它能将我们输入的混淆 JS 编译成语法树并且动态的修改语法树,并且将修改后的语法树重新输出成代码。

还原十六进制字符串

接下来我们使用 AST 来还原代码中的十六进制字符串来感受一下 AST 强大的功能吧。


为了知道如何还原字符串,我们需要先比较一下正常的字符串和十六进制编码的字符串在 AST 语法树中的区别是什么,然后通过 JS 代码将混淆的语法树转换成非混淆的形式即可,几乎所有的 AST 解混淆都是这样的逻辑。




通过对比图片上的两段代码,我们可以看出来,十六进制的字符串它的 extra 属性中的 raw 属性是十六进制的形式,正常的字符串的 extra 属性的 raw 属性是正常的字符串,根据以上区别,我们来写代码尝试还原一下。


新建一个 ast.js,写入以下代码:


const fs = require('fs'); //导入需要的库const parser = require("@babel/parser");const traverse = require("@babel/traverse").default;const generator = require("@babel/generator").default;
encodeFile = "main.js";//定义输入以及输出文件decodeFile = "main_decode.js";
//将源代码解析成 AST对象let ast = parser.parse(fs.readFileSync(encodeFile, {encoding: "utf-8"}));
//修改 AST 语法树let hex_decode = { //遍历说中的字符串节点,只需要写一遍,框架会自动遍历所有的节点 StringLiteral({node}) { if (node.value !== node.extra.raw) { node.extra.raw = "'" + node.value + "'"; } },}//执行实际的修改traverse(ast, hex_decode);
//将 AST 语法树还原成代码let {code} = generator(ast, opts = { "jsescOption": {"minimal": true},});//将生成好的代码写入新的文件fs.writeFile(decodeFile, code, (err) => {});
复制代码



运行 ast.js,查看新生成的 decode 文件,可以看到,十六进制的字符串已经被我们还原了,只需要几行代码就可以还原,可见 AST 有多么方便,接下来我们还原被函数加密的字符串。

还原加密字符串

如何还原加密的字符串呢,我们通过分析代码结构可以看出来,JS 代码会在运行时动态的生成一个大的数组,然后调用函数传入一个类似于索引值的字符串,拿到数组中的某个值后返回。那要如何使用 AST 来获取数组的值呢?抱歉,做不到,AST 只能修改语法树,无法执行具体的解密函数。


但是 ast.js 也是一个 JS 文件,我们可以使用扣代码的方式,将解密的函数放到 ast.js 中执行,然后获取所有的加密函数节点,获取函数的参数,直接调用解密函数,将解密函数的返回结果替换之前的加密函数节点,让它变成字符串节点,这样不就能解密字符串了吗,思路有了,那如何找到函数节点呢,来对照看看:



通过上图可以看到,函数节点叫 CallExpression,它的函数保存在 arguments 中,我们只需要遍历所有的 CallExpression 节点,并且获取到 arguments 中的参数值调用解密函数即可。


在此之前,别忘了将解密函数搬过来。


const fs = require('fs'); //导入需要的库const parser = require("@babel/parser");const types = require("@babel/types");const traverse = require("@babel/traverse").default;const generator = require("@babel/generator").default;
encodeFile = "main.js";//定义输入以及输出文件decodeFile = "main_decode.js";
//将源代码解析成 AST对象let ast = parser.parse(fs.readFileSync(encodeFile, {encoding: "utf-8"}));
//修改 AST 语法树let hex_decode = { //遍历说中的字符串节点,只需要写一遍,框架会自动遍历所有的节点 StringLiteral({node}) { if (node.value !== node.extra.raw) { node.extra.raw = "'" + node.value + "'"; } },}//执行实际的修改traverse(ast, hex_decode);
// 解密函数开始//const _0x4afa = ['1993-03-11', '79.4KG', '1984-05-29', 'stringify', '128.8KG', '1991-06-29', '198cm', 'davis.png', '208cm', '卡尔-安东尼-唐斯', '188cm', '196cm', 'antetokounmpo.png', '83.9KG', '112.5KG', 'toString', 'embiid.png', '88.5KG', '114.8KG', '203cm', '206cm', '斯蒂芬-库里', '1988-03-14', 'JD8wgBMgVjdQbBUVbMarpZMAadLD7yvfzVV', 'Base64', '考瓦伊-莱昂纳德', '扬尼斯-安特托昆博', 'leonard.png', '安东尼-戴维斯', '达米安-利拉德', '109.8KG', 'harden.png', '99.8KG', 'durant.png', '102.1KG', 'paul.png', '1989-08-26', '1985-05-06', 'key', 'parse', '201cm', '113.4KG', '108.9KG', '1988-11-12', 'Utf8', '90.7KG', '尼科拉-约基奇', '213cm', 'pad', 'enc', '卡梅罗-安东尼', 'westbrook.png', 'encrypt', '127.0KG', 'thompson.png', '1994-12-06', 'irving.png', '185cm', 'lillard.png', '拉塞尔-威斯布鲁克', '1990-02-08', 'anthony.png', '191cm'];(function (_0x35db0b, _0x4afab2) { const _0x343162 = function (_0x6f5802) { while (--_0x6f5802) { _0x35db0b['push'](_0x35db0b['shift']()); } }; _0x343162(++_0x4afab2);})(_0x4afa, 0xed);const _0x3431 = function (_0x35db0b, _0x4afab2) { _0x35db0b = _0x35db0b - 0x0; let _0x343162 = _0x4afa[_0x35db0b]; return _0x343162;};//解密函数结束//

traverse(ast, { CallExpression(path) { let {node} = path; //别忘了判断一下是不是我们要的函数,如果不是我们需要,则直接返回不做任何处理,不然会破坏其他正常的函数 if (node.arguments.length !== 1 || node.callee.name !== '_0x5e920f') { return; } let args = node.arguments[0].value; //获取函数参数,例如:0x30 let result = _0x3431(args)//调用实际的解密函数 console.log(result) path.replaceWith(types.stringLiteral(result));//构造一个字符串节点,替换原来的节点 }});
//将 AST 语法树还原成代码let {code} = generator(ast, opts = { "jsescOption": {"minimal": true},});//将生成好的代码写入新的文件fs.writeFile(decodeFile, code, (err) => {});
复制代码



可以看到,数据都被还原了,为了方便的拿到数据,我们还可以使用 AST 来删除后续的 new Vue 代码,防止在 nodejs 中执行报错。

删除 new


来看一下 new Vue 的代码语法树长什么样子,原来是 NewExpression 节点,因为我们的代码中只有一个 NewExpression 节点,那么我们就删除所有的 NewExpression 节点就好了。在原来的 ast.js 文件中添加以下代码:


//删除 NewExpression节点traverse(ast, {    NewExpression(path) {        path.remove();    }})
复制代码


看效果:



代码只剩下 100 多行了,现在可以直接使用 nodejs 来执行上面的代码,然后打印 players 变量的值了。至此,我们使用 AST 技术还原了 OB 混淆。


完整代码见:https://github.com/libra146/learnscrapy/tree/main/js/spa13

总结

使用 AST 语法树的方式来还原类似的混淆是非常方便的,再也不用使用全局替换或者正则的方式来还原代码了,也不用顶着十六进制的恶心代码在浏览器中调试了,使用 AST 的方式就类似于一个手术刀对符合要求的代码进行精准分析和切割,不用怕误伤其他的代码,非常的方便。


另外,其实不止 JS 有 AST 语法树,只有是编程语言,基本上都有语法树,语法树是编译原理中很重要的一环,只有有了语法树,编译器才能对代码进行一些优化,优化的原理其实和我们解混淆其实差不多,都是对特定的节点进行特定的操作。


本文章首发于个人博客 LLLibra146's blog

本文作者:LLLibra146

更多文章请关注公众号:

本文链接https://blog.d77.xyz/archives/10bf322b.html

发布于: 刚刚阅读数: 4
用户头像

LLLibra146

关注

还未添加个人签名 2018-09-17 加入

还未添加个人简介

评论

发布
暂无评论
JS逆向实战:AST技术让你轻松破解OB混淆保护!_js_LLLibra146_InfoQ写作社区