mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4mobile wallpaper 5mobile wallpaper 6
1073 字
3 分钟
利用Ast解ob混淆
2026-05-11

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')如下图,照着树操作即可

a

//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结构如下。

b

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

c

//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));

d

值类型为函数调用也是大同小异,如下一并还原。

//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']=1
a['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;
}

e

我们得首先拿到指令数组,然后采集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多练,自然就掌握了。

混淆通常不具备普适性,所以我们常常得针对不同的网站来写不同的解混淆脚本代码。

嘻嘻🫡

分享

如果这篇文章对你有帮助,欢迎分享给更多人!

利用Ast解ob混淆
https://fatdog.20060113.xyz/posts/ast-tree/
作者
神秘大胖狗
发布于
2026-05-11
许可协议
MIT

部分信息可能已经过时

封面
Sample Song
Sample Artist
封面
Sample Song
Sample Artist
0:00 / 0:00