前几天逛论坛看到一篇AST的还原某验三代。感觉写的不甚详细。作为小白的我看的云里雾里。因此也一篇文章,记录还原的过程。让小白也可以手把手还原混淆。
二、混淆文件解析。
三代有三个混淆js文件,分别是slide.7.9.3.js,fullpage.9.2.0-guwyxh.js,gct.js。发包的w值在slide.7.9.3.js中,三个文件大差不差,因此我们只看slide.7.9.3.js。

1.png (54.18 KB, 下载次数: 4)
下载附件
2025-9-5 14:34 上传
代码结构
可以看到,代码由四个函数和一个自执行大数组组成。
通过观察如下代码,可以看到其中lACSb是还原字符的代码。自执行函数就是业务逻辑代码。
[JavaScript] 纯文本查看 复制代码var $_DAGJB = lACSb.$_Ce
, $_DAGIo = ['$_DAHCK'].concat($_DAGJB)
, $_DAHAM = $_DAGIo[1];
$_DAGIo.shift();
var $_DAHBL = $_DAGIo[0];
(0,
this[$_DAHAM(490)])($_DAGJB(824))[$_DAGJB(198)]();
}
}),
$_BAV(ie[$_CJFi(232)], ue[$_CJEB(232)]),
其中$_DAGJB = lACSb.$_Ce类似的代码$_DAGJB(824)是一种字符赋值。
[JavaScript] 纯文本查看 复制代码var $_DAGJB = lACSb.$_Ce
, $_DAGIo = ['$_DAHCK'].concat($_DAGJB)
, $_DAHAM = $_DAGIo[1];
$_DAGIo.shift();
var $_DAHBL = $_DAGIo[0];
如上是第二种赋值。先赋值$_DAGJB,在通过concat合成一个数组,在取$_DAGIo[1]下标为2的数组,其实就是取lACSb.$_Ce。然后shift移除第一个数组。而var $_DAHBL = $_DAGIo[0];是个假植,就是没有用到的东西。
因此字符还原,我们需要处理两种情况。
接下来是控制流,代码如下
[JavaScript] 纯文本查看 复制代码function o() {
var $_DBGEn = lACSb.$_DN()[6][16];
for (; $_DBGEn !== lACSb.$_DN()[9][14];) {
switch ($_DBGEn) {
case lACSb.$_DN()[9][16]:
var t = n(".wrap")["$_BFHo"]();
$_DBGEn = lACSb.$_DN()[12][15];
break;
case lACSb.$_DN()[9][15]:
r === t && 0 !== r || 5
我们可以看到var $_DBGEn = lACSb.$_DN()[6][16];其中lACSb.$_DN取了某些值,赋值给了$_DBGEn,然后for循环判断了取得另一些值,在通过switch中case的值进行判断。因此可以看出来他的控制流不是类似其他ob的那种数组一样。其实就是在循环中按顺序执行,在结果相等的情况下跳出。只需要我们把所有的值写道map中,然后循环判断,写入写出即可。
接下来我们开始AST还原
三、还原模板。
每次还原前我们直接在模板里面写,代码如下
[JavaScript] 纯文本查看 复制代码const fs = require("fs");
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default;
const types = require('@babel/types');
const generator = require("@babel/generator").default;
let jscode = fs.readFileSync('slide.7.9.3.js', 'utf8');
let ast = parser.parse(jscode);
/////////
还原代码
////////
let {code} = generator(ast); //{}解包的意思 等价于let code=generator(ast).code
fs.writeFile('还原.js', code, (err) => {
});
然后先写入两个必备的通用还原代码,
[JavaScript] 纯文本查看 复制代码traverse(ast, {
NumericLiteral({node}) {
if (node.extra && /^0[obx]/i.test(node.extra.raw)) {
node.extra = undefined;
}
},
StringLiteral({node}) {
if (node.extra && /\\[ux]/gi.test(node.extra.raw)) {
node.extra = undefined;
}
},
});
traverse(ast, {
NumericLiteral: function (path) {
if (path.node.extra && path.node.value) {
const numstr = path.node.extra;
const numstring = path.node.value;
path.replaceWith(types.numericLiteral(numstring));
}
},
}, opts = {});
这两个通用的可以还原如下编码,使代码清晰

2.png (72.34 KB, 下载次数: 4)
下载附件
2025-9-5 14:34 上传
编码
接下来还原字符混淆
四、混淆字符还原。
我们把代码复制到ast的解析站https://astexplorer.net/

3.png (284.96 KB, 下载次数: 4)
下载附件
2025-9-5 14:35 上传
ast解析
可以看到var $_DAGJB = lACSb.$_Ce的type是VariableDeclaration,因此,我们需要先过滤VariableDeclaration
[JavaScript] 纯文本查看 复制代码traverse(ast,{
VariableDeclarator(path) {
})
我们需要先找到lACSb.$_Ce代码。可以看到是在init里面,类型是MemberExpression

4.png (276.85 KB, 下载次数: 4)
下载附件
2025-9-5 14:35 上传
lACSb.$_Ce
所以我们先判断type类型是MemberExpression的在放行
if(types.isMemberExpression(path.node.init)) {
}
然后我们在看一下init的代码,用generator.code
[JavaScript] 纯文本查看 复制代码let namecode=generator(path.node.init).code;
console.log(namecode);

5.png (16.74 KB, 下载次数: 4)
下载附件
2025-9-5 14:35 上传
namecode
可以看到筛选出来了一堆,我们直接namecode==="lACSb.$_Ce"判断过滤
接下来我们需要找到赋值的地方,可以看到赋值时id的name参数

6.png (199.42 KB, 下载次数: 3)
下载附件
2025-9-5 14:35 上传
astname
我们直接取path.node.init.name,可以看到结果是正确的

7.png (26.96 KB, 下载次数: 4)
下载附件
2025-9-5 14:36 上传
name参数
接下来我们找这个参数的绑定,取他的path作用域
我们定义两个值,并循环取到的path,在取path的parent,顺便打印一下parentPath的值
[JavaScript] 纯文本查看 复制代码let bd=path.scope.getBinding(path.node.id.name);
let repath=bd &&bd.referencePaths
for (let i = 0; i
日志如下
[JavaScript] 纯文本查看 复制代码$_DAGEI(75)
$_DAGEI(75)
$_DAGEI(986)
$_DAGEI(986)
$_DAGEI(1080)
['$_DAHCK'].concat($_DAGJB)
$_DAGJB(824)
可以看到取到了两种类型的值,这两种就是我们上面说到的,两种情况,我们分开处理。因此需要判断下,分成两种情况处理。
[JavaScript] 纯文本查看 复制代码if(rep.toString().includes("concat")){}
else{}
我们先处理else的情况,当else的时候,那么就是$_DAGEI(1080),我们此时就只需要提取里面的参数传入lACSb.$_Ce然后替换即可。
[JavaScript] 纯文本查看 复制代码argsm=lACSb.$_Ce.call(null,rep.node.arguments[0].value)
rep.replaceWith(types.valueToNode(argsm));
此时我们第一种就还原完成

8.png (72.82 KB, 下载次数: 4)
下载附件
2025-9-5 14:36 上传
第一种还原完成
接下来我们看第二种
我们先前判断了if(rep.toString().includes("concat")){},接下来我们就在这里面继续
先看结构,我们现在已经获取了['$_DAHCK'].concat($_DAGJB),那么接下来就是获取$_DAGIo的值
[JavaScript] 纯文本查看 复制代码var $_DAGJB = lACSb.$_Ce
, $_DAGIo = ['$_DAHCK'].concat($_DAGJB)
, $_DAHAM = $_DAGIo[1];
直接跟上面一样,获取name,然后获取binding和父路径
[JavaScript] 纯文本查看 复制代码let cat=rep.parent.id.name
let catbind=path.scope.getBinding(cat)
let catpath=catbind &&catbind.referencePaths
for (let i = 0; i
这样就获取到了$_DAGIo[1],然后我们继续获取name,相同的操作
[JavaScript] 纯文本查看 复制代码let bdcat=path.scope.getBinding(catid);
let pathcat=bdcat &&bdcat.referencePaths
for (let i = 0; i
到这一步就已经获取到了所有的参数,然后我们直接取值,替换即可
[JavaScript] 纯文本查看 复制代码argsmcat=lACSb.$_Ce.call(null,repcat.node.arguments[0].value)
repcat.replaceWith(types.valueToNode(argsmcat));

9.png (64.31 KB, 下载次数: 3)
下载附件
2025-9-5 14:36 上传
替换结果
我们到还原代码看一下

10.png (136.79 KB, 下载次数: 5)
下载附件
2025-9-5 14:37 上传
还原完成
可以看到此时已经把字符还原的差不多了。