手把手带你AST还原某验三代(一)

查看 55|回复 8
作者:mysticz   
一、前言
前几天逛论坛看到一篇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 上传

还原完成
可以看到此时已经把字符还原的差不多了。

代码, 下载次数

zhangxg   

不明觉厉,让人想跟着大佬脚本来一遍
heigui520   

感谢分享
dapc   

究竟AST是什么??
aspartate aminotransferase?
llgxdnn   


dapc 发表于 2025-9-7 01:18
究竟AST是什么??
aspartate aminotransferase?

AST:Abstract Syntax Tree - 抽象语法树
ljl9090   

需要一定技术功底啊,小白表示看不懂。
nftion   

这里   let bdcat=path.scope.getBinding(catid);  catid 是什么呢  ,前面部分看着还行,后面 “concat“部分 有一些不完整。
dapc   

感谢分享
dapc   


涛之雨 发表于 2025-9-7 12:05
AST:Abstract Syntax Tree - 抽象语法树

抽象语法树, 谢谢解释。
您需要登录后才可以回帖 登录 | 立即注册

返回顶部