Ast解ob混淆
一、引言
ast作为一种解开混淆的手段,广泛运用于逆向圈子。最开始是由蔡老板发现可以用来做混淆对抗,后面就开始被广泛使用了。废话不多说,我们直接开始。
注意!这篇文章默认你已经掌握ast的基础,我不会用过多的话语去解释一些基础api。
来吧!开始吧😎
抽象而烧脑,只能做参考。具体哪里绕,需要你思考。🫡
二、基础框架
桥豆麻袋,开始之前,先把基础框架搭起来。这样会避免遗忘。
以及将代码树可视化的网站——https://astexplorer.net/
// fs模块 用于操作文件的读写const fs = require("fs");// @babel/parser 用于将JavaScript代码转换为ast树const parser = require("@babel/parser");// @babel/traverse 用于遍历各个节点的函数const traverse = require("@babel/traverse").default;// @babel/types 节点的类型判断及构造等操作const types = require("@babel/types");// @babel/generator 将处理完毕的AST转换成JavaScript源代码const generator = require("@babel/generator").default;
// 混淆的js代码文件const encode_file = "./encode.js"// 反混淆的js代码文件const decode_file = "./decode.js"
// 读取混淆的js文件let jsCode = fs.readFileSync(encode_file, {encoding: "utf-8"});// 将javascript代码转换为ast树(json结构)let ast = parser.parse(jsCode)
// 这里留着编写ast插件
// 将处理后的ast转换为js代码(反混淆后的代码)let {code} = generator(ast);// 保存代码fs.writeFile('decode.js', code, (err)=>{});众所周知啊,我们的ob混淆说简单点无非就是大数组,数组位移函数,自执行函数,业务函数。这些东西被ob混淆成了恶心的代码结构,导致难以窥探,以下介绍常见处理ob混淆的流程。
三、解密函数调用还原
解密函数长啥样子?如下。你会发现很多代码都需要用到这个函数解密,函数调用后会返回真正起作用的值或者函数,所以是解密函数。
var $_0x525d = function (_0x6038e2, _0x525d05){ //解密逻辑}//以下是某个函数里的内容,他就用到了解密函数$_0x525d('\x30\x78\x31', '\x33\x62\x56\x66')//函数调用后会返回真正起作用的值或者函数。var _0x13b2d2 = { '\x6e\x56\x54\x44\x55': function(_0x24e2b4, _0x3e2149) { return _0x24e2b4 !== _0x3e2149; }, '\x76\x4e\x70\x50\x59': $_0x525d('\x30\x78\x31', '\x33\x62\x56\x66'),}那么如何去还原呢?很简单。
我们先来看看结构$_0x525d('\x30\x78\x31', '\x33\x62\x56\x66')如下图,照着树操作即可

//1.还原所有的解密函数"$_0x525d"调用//把大数组和解密函数扒下来。var $_0x6038 = ['\x77\x71\x58\x43\x72\x52\x37\x43\x70\x63\x4f\x7a', '\x77\x36\x35\x2b\x77\x71\x37\x43\x68\x79\x45\x3d',············];var $_0x525d = function (_0x6038e2, _0x525d05){解密逻辑。}visitor1 = { CallExpression: function (path) { if (path.node.callee && path.node.callee.name === "$_0x525d") { args = path.node.arguments.map(arg => arg.value); const result = $_0x525d(args[0], args[1]); // console.log(`解密: ${path.toString()} → "${result}"`); path.replaceWith(types.stringLiteral(result)) } }}traverse(ast, visitor1)记住!ast只是文本解析!要想执行函数,还得把大数组和解密函数放到内存里!这里我直接放到了上面
四、还原对象取属性,且属性为字符串
还原对象取属性是什么意思呢?
我们有一个对象,他有很多属性,这里我们先看属性值为字符串的。
比如'\x4c\x42\x67\x72\x59'这个属性。
var _0x13b2d2 = { '\x6e\x56\x54\x44\x55': function(_0x24e2b4, _0x3e2149) { return _0x24e2b4 !== _0x3e2149; }, '\x76\x4e\x70\x50\x59': $_0x525d('\x30\x78\x31', '\x33\x62\x56\x66'), '\x4c\x66\x56\x62\x48': function(_0x5cb74e, _0x5e6b30) { return _0x5cb74e + _0x5e6b30; }, '\x4c\x42\x67\x72\x59': '\x64\x65\x62\x75', }然后这个属性在下面被调用了。(栗子简单,这里你可能还看得明白,但是一旦调用多起来就体现出解混淆的优势了!)
qtYxEc['\x4c\x42\x67\x72\x59'], '\x67\x67\x65\x72')我们来还原这个属性。还原三个对象,是我因为我在项目中遇到三个要还原的,懒得改啦😋
这里得通过path.scope.getBinding来拿到对象作用域,然后就可以查找替换它的属性了。
这种找作用域的方法只适合如下,这种直接赋值属性给对象类型的 。
a = { b:1, c:2, d:3}要还原的ast结构如下。

拿到的作用域绑定对象即binding如下。

//3.还原_0x13b2d2,_0x146ace,_0x4ab93c直接取属性,且属性值为字符串visitor3 = { MemberExpression: { exit: function (path) { if (path.node.object.name === "_0x13b2d2"||path.node.object.name ==="_0x146ace"||path.node.object.name ==="_0x4ab93c") { if(path.node.object.name === "_0x13b2d2") { binding = path.scope.getBinding("_0x13b2d2"); } if(path.node.object.name ==="_0x146ace"){ binding =path.scope.getBinding("_0x146ace"); } if(path.node.object.name ==="_0x4ab93c"){ binding =path.scope.getBinding("_0x4ab93c"); } const objNodes = binding.path.node.init.properties; // 还原属性字符串 key = path.node.property.value; if (!key) return; objNodes.forEach(function (prop) { if (prop.key.value && prop.key.value === key && prop.value.type === "StringLiteral") { val = prop.value.value; path.replaceWith(types.stringLiteral(val)) } }) } } }}traverse(ast, visitor3)五、还原对象取属性,并且属性为函数调用
var _0x13b2d2 = {'\x64\x57\x72\x6a\x7a': function(_0x4407e0, _0x255c41) { return _0x4407e0 + _0x255c41; }, '\x70\x76\x4d\x58\x7a': function(_0xfb37b7, _0x17c2f7, _0x364f86, _0x1ce4f8, _0xcb03b3, _0x4e20c2, _0x91d9af, _0x39515b) { return _0xfb37b7(_0x17c2f7, _0x364f86, _0x1ce4f8, _0xcb03b3, _0x4e20c2, _0x91d9af, _0x39515b); },}可以看到,属性里的函数有二元操作符类型的,也有函数调用类型的
比如下面这种调用。
'\x69\x4f\x46\x57\x75': function(_0x1884ee, _0xfb2ec) { return _0x1884ee >> _0xfb2ec; }
blks[_0x13b2d2['\x69\x4f\x46\x57\x75'](i, 0x2)] |= _0x13b2d2['\x43\x42\x41\x66\x67'](0x80, _0x13b2d2[$_0x525d('\x30\x78\x33\x33', '\x6b\x34\x79\x64')](_0x13b2d2['\x72\x41\x73\x46\x64'](i, 0x4), 0x8));
值类型为函数调用也是大同小异,如下一并还原。
//4.还原_0x13b2d2,_0x146ace,_0x4ab93c的函数属性调用visitor4 = { CallExpression: { exit: function (path) { if (path.node.callee.type === "MemberExpression" && (path.node.callee.object.name === "_0x13b2d2"||path.node.callee.object.name==="_0x146ace"||path.node.callee.object.name==="_0x4ab93c")) { // 检查 _0x13b2d2,_0x146ace,_0x4ab93c 在当前作用域是否存在绑定 if(path.node.callee.object.name === "_0x13b2d2") { binding = path.scope.getBinding('_0x13b2d2'); } if(path.node.callee.object.name==="_0x146ace"){ binding = path.scope.getBinding('_0x146ace'); } if(path.node.callee.object.name==="_0x4ab93c"){ binding = path.scope.getBinding('_0x4ab93c'); } key = path.node.callee.property.value; let arr = []; if (binding) { const objNodes = binding.path.node.init.properties; args = path.node.arguments; objNodes.forEach(function (prop) { if (prop.value.type === "FunctionExpression" && prop.key.value === key) { //处理操作符形式的函数 if (prop.value.body.body[0].argument.type === "BinaryExpression") { operator = prop.value.body.body[0].argument.operator; path.replaceWith(types.binaryExpression(operator, args[0], args[1])) } //处理返回函数调用 if (prop.value.body.body[0].argument.type === "CallExpression") { args.forEach(function (ton) { arr.push(ton); }) funcname = arr.shift(); path.replaceWith(types.callExpression(funcname,arr)) } } })
} } } }}traverse(ast, visitor4)如果是非直接赋值对象类型,而是增加属性那种,比如。
a = {};a['d']=1a['f']=function(){}这样的我们可以用一个哈希表来存储键值映射,说白了还是构造上面那个对象形式啦。
这里可能会出现报错,根据报错信息排查即可!🫡
// 第一步:建一个仓库,存所有对象属性const objectPropertiesMap = {};
// 先全量遍历一次,专门收集 “对象[属性] = 值” 这种赋值traverse(ast, { AssignmentExpression(path) { const left = path.node.left; // 必须是 obj["key"] 或 obj['key'] 这种computed赋值 if (left.type === "MemberExpression" && left.computed) { const objName = left.object.name; const propName = left.property.value; const rightVal = path.node.right;
// 只收集我们要的目标对象:_0x4472a2 if ((objName === "_0x4472a2") && propName) { // 把属性和对应的值节点存起来 objectPropertiesMap[propName] = rightVal; } } }});
visitor3 = { MemberExpression: { exit: function (path) { if (path.node.object.name === "_0x4472a2"||path.node.object.name ==="_0x494ecf") { // ✅ 关键:跳过赋值语句的左边! //_0x4472a2["ZnPaJ"] = "HQUgk" 等号左边 ❌ 不能!这是赋值目标 // var a = _0x4472a2["ZnPaJ"] 等号右边 ✅ 可以!这是读取值 if (path.parent.type === 'AssignmentExpression' && path.parent.left === path.node) { return; } // 还原属性字符串 key = path.node.property.value; if(objectPropertiesMap[key]&&objectPropertiesMap[key].type==='StringLiteral'){ value = objectPropertiesMap[key]["value"] path.replaceWith(types.stringLiteral(value)) }
} } }}
traverse(ast, visitor3)
//4.还原函数调用visitor4 = { CallExpression:{ exit:function(path){ if(path.node.callee.object&&(path.node.callee.object.name === "_0x4472a2"||path.node.callee.object.name ==="_0x494ecf")){ key = path.node.callee.property.value; if(objectPropertiesMap[key]&&objectPropertiesMap[key].type==="FunctionExpression"){ if(objectPropertiesMap[key]["body"]["body"][0]["argument"]["operator"]) { operator = objectPropertiesMap[key]["body"]["body"][0]["argument"]["operator"]; arg1 = path.node.arguments[0]; arg2 = path.node.arguments[1]; path.replaceWith(types.binaryExpression(operator, arg1, arg2)) } //处理第一个作为函数名的调用 else if(objectPropertiesMap[key]["body"]["body"][0]["argument"]["type"]==="CallExpression") { let arr = [] args = path.node.arguments; args.forEach(function (ton) { arr.push(ton) }) funcname = arr.shift(); path.replaceWith(types.callExpression(funcname, arr)) } } } } }}traverse(ast, visitor4)六、控制流平坦化还原
什么是控制流平坦化。
常见的就是用一个while无限循环套一个switch case语句来制造混乱的代码流,让逆向者进入迷宫,实现鬼打墙的效果。
比如来个简单的
while (!![]) { switch (_0x440812[_0x2cc7fe++]) { case '\x30': blks = new Array(_0x13b2d2[$_0x525d('\x30\x78\x32\x62', '\x32\x57\x28\x36')](nblk, 0x10)); continue; case '\x31': blks[_0x13b2d2['\x69\x4f\x46\x57\x75'](i, 0x2)] |= _0x13b2d2['\x43\x42\x41\x66\x67'](0x80, _0x13b2d2[$_0x525d('\x30\x78\x33\x33', '\x6b\x34\x79\x64')](_0x13b2d2['\x72\x41\x73\x46\x64'](i, 0x4), 0x8)); continue; case '\x32': for (i = 0x0; i < _0x45def1[$_0x525d('\x30\x78\x33\x34', '\x63\x63\x6e\x4f')]; i++) blks[_0x13b2d2[$_0x525d('\x30\x78\x33\x35', '\x67\x70\x30\x4d')](i, 0x2)] |= _0x13b2d2[$_0x525d('\x30\x78\x33\x36', '\x25\x26\x5a\x65')](_0x45def1[$_0x525d('\x30\x78\x33\x37', '\x75\x23\x25\x50')](i), _0x13b2d2[$_0x525d('\x30\x78\x33\x38', '\x68\x32\x24\x55')](i % 0x4, 0x8)); continue; case '\x33': return blks; case '\x34': blks[_0x13b2d2[$_0x525d('\x30\x78\x33\x39', '\x31\x77\x21\x65')](_0x13b2d2[$_0x525d('\x30\x78\x33\x61', '\x51\x44\x48\x41')](nblk, 0x10), 0x2)] = _0x13b2d2[$_0x525d('\x30\x78\x33\x62', '\x6d\x49\x4a\x4c')](_0x45def1[$_0x525d('\x30\x78\x33\x63', '\x4e\x39\x6b\x33')], 0x8); continue; case '\x35': nblk = _0x13b2d2['\x4d\x64\x78\x73\x6b'](_0x13b2d2[$_0x525d('\x30\x78\x33\x64', '\x75\x23\x25\x50')](_0x13b2d2['\x4d\x64\x78\x73\x6b'](_0x45def1[$_0x525d('\x30\x78\x33\x65', '\x68\x32\x24\x55')], 0x8), 0x6), 0x1); continue; case '\x36': for (i = 0x0; i < _0x13b2d2['\x65\x4a\x49\x63\x56'](nblk, 0x10); i++) blks[i] = 0x0; continue; } break; }
我们得首先拿到指令数组,然后采集case 的条件值。
//生成blks的控制流平坦化var instructionArray = "5|0|6|2|1|4|3".split('|');var instructmentarray = "0|2|5|3|1|4|6".split("|")visitor5={ SwitchStatement:{ exit:function(path){ if((path.node.discriminant.object.name==="_0x440812"&&path.node.discriminant.property.argument.name==="_0x2cc7fe")||(path.node.discriminant.object.name==="_0x47723f"&&path.node.discriminant.property.argument.name==="_0x301fd3")){ let casemap= {}; if(path.node.discriminant.object.name==="_0x47723f"){ instructionArray=instructmentarray; } //构建指令映射 path.node.cases.forEach(caseNode=>{ const key = caseNode.test.value; const stmts = caseNode.consequent.filter(s=>s.type!== 'ContinueStatement') casemap[key]=stmts; }) //按指令顺序拼接 const orderStatement = []; for(let i=0;i<instructionArray.length;i++){ const key=instructionArray[i] if(casemap[key]){ //这里的casemap[key]是个数组,里面还是个数组(内容是代码块),所以我们要用...把数组拆开两次 orderStatement.push(...casemap[key]) console.log(casemap[key]) } } path.parentPath.replaceWithMultiple(orderStatement); console.log('✅ 控制流还原完成!'); } } }}traverse(ast,visitor5)上面用了很多exit,这个类似于二叉树的后序遍历,从子树开始遍历到根,可以模拟实现从内向外一一解开混淆。
为什么从内向外?如果你直接从外面干,可能会把里面的结构破坏,导致不再会被traverse遍历!
七、结语
ast多练,自然就掌握了。
混淆通常不具备普适性,所以我们常常得针对不同的网站来写不同的解混淆脚本代码。
嘻嘻🫡
部分信息可能已经过时









