JS逆向:AST还原极验混淆JS实战

4,931 阅读8分钟

本文仅供学习交流使用,请勿用于商业用途或不正当行为

如果侵犯到贵公司的隐私或权益,请联系我立即删除

AST是抽象语法树,名字感觉很高级,其实也不用怕,可以简单理解为是将JS代码归类后的JSON,并且提供了很多方法给你对这个JSON进行增删改查。


学习一个新东西,首先一定要搞清楚它有什么用,都不知道有啥用,学习它干啥,对吧


作为一个爬虫攻城狮,JS逆向也是家常便饭了,JS逆向经常会遇到各种混淆过后的代码,极其难以阅读,这时候就可以使用AST对这些混淆代码进行一定的还原处理,得到一个相对容易阅读的JS代码,方便我们进行JS分析。


阅读本文需要有一点AST的基础,不要害怕,只需要稍微学习一点AST知识就行了,并不需要掌握AST的全部知识才能看得懂,不过最起码要对AST定位节点有一点认识,关于AST的学习大家肯定找蔡老板啊


相信只要能认真阅读完本文内容,理解其中的概念,就能对AST还原混淆代码有一点点感觉了,并会发现AST其实也不是那么难。


本次反混淆的是下面这个文件

https://static.geetest.com/static/js/fullpage.8.9.5.js

保存到本地,收缩一下代码看看整体结构

简单看下这几个function

嗯, 啥也看不懂...


别慌,静下心来,一点一点分析,一点一点解决!


观察代码发现里有很多地方调用了AJgjJ.DAi,搜索出515个匹配结果,如下图

在匹配结果中有两类调用,如上图红框标示的

第一个:带括号的AJgjJ.DAi(79); 暂时不知道是在干啥;

第二个,不带括号的AJgjJ.DAi,可以理解为

mZtVWz = ['qhicV'].concat(AJgjJ.DAi)

concat()是合并数组的作用

也就是说AJgjJ.DAi应该是一个数组

控制台打印看看

果然类似数组,通过索引就能取出值来,返回值都是字符串类型。


那么问题来了:如何将代码里的AJgjJ.DAi()还原成对应的字符串?

很快就能想到,在JS代码里将字符串"AJgjJ.DAi()"逐个匹配出来,拿到括号里的索引数字,然后将数字值传入函数AJgjJ.DAi()计算得到字符串结果,然后将结果替换代码里的字符串"AJgjJ.DAi()",就实现了还原的目的了。


其实AST操作的话也差不多是这个思路,我们把代码复制到在线AST解析网站,看看在AST里是什么样子的

https://astexplorer.net/



能看到所有的AJgjJ.DAi都在CallExpression节点里,所以我们首先遍历CallExpression节点,然后定位到name属性得到值并判断是否为DAi,是的话就定位到value属性取出值,再将拿到的value传入到函数AJgjJ.DAi()进行计算,得到结果再使用AST操作替换。


刚才说到要将value传入到AJgjJ.DAi()里执行,所以首先需要将AJgjJ.DAi的代码从JS里扣过来放到Node里用于计算。

代码实现:

替换后的效果:

可以看到带括号的AJgjJ.DAi()原来有8个,处理后已经都被还原掉了

不带括号的AJgjJ.DAi还有519个

接下来我们来处理这些不带括号的AJgjJ.DAi

在处理之前,先分析一下

观察上图:

nxWF = AJgjJ.DAi;

concat是合并数组,所以

mZtVWz = ['qhicV'].concat(nxWF)

其实等于

mZtVWz = ['qhicV',AJgjJ.DAi]

oeXg = mZtVWz[1];

就等于

oeXg = AJgjJ.DAi;

图中所有红框的nxWF和绿框的oeXg,实际上都等于AJgjJ.DAi


这里我们的处理思路是:将所有的这两个位置的name全部遍历出来,放入一个数组,然后遍历判断所有类似oeXg(12)的name是否在数组里,是的话就替换



代码实现:

看下效果

能看到在数组nameArray里存在的函数已经被还原了,但是发现还存在一些冗余代码,我们看看如何将它们删除


继续分析AST结构

能看到左边这三组对应的是右边的三个节点,我们只需要定位到右边的这三个节点,然后删除就行

首先定位第一个节点,定位的方法就是判断VariableDeclaraction下面property下面的name是否为DAi

代码实现:

还原效果:

看起来清晰点了


继续分析,观察到代码里有很多unicode代码

在AST结构里,能看到unicode代码是在axtra里的,所以我们只需要将axtra给删掉,就能还原掉unicode了


代码实现:

效果:

处理完你会发现仍然有大量的unicode编码的字符串

这些是中文的unicode,解决办法是在generator的第二个参数添加

opts = {jsescOption:{"minimal":true}}

添加之后,重新还原一下

能看到中文unicode编码也很轻松就被还原了

剩下的37个匹配结果,都是正则里的,不影响,就不需要做处理了


另外在代码里还发现一些eval的代码

还原后,eval里面是字符串形式的JS代码

所以在反混淆之前大家可以先去浏览器执行一下eval里的这些函数,拿到值后格式化一下替换掉这些eval,这样可以在还原时一起将那些字符串形式的JS代码也一并还原掉,这里就不多说了。


继续分析,细心的你一定能看到极验的JS代码中有很多这种结构的代码,非常影响代码的阅读

这种结构的代码俗称控制流平坦化,简单来讲就是将代码块之间的关系打断,由一个分发器来控制代码块的跳转,找了个图如下


先引用一个简单的例子,这样清晰点

switch就是流程控制器,它会遍历arr数组,因为每一句case里面都有continue,也就是说switch每次从arr中取出值,就会把所有的case语句都执行一遍,这样去扰乱程序的执行流程,以增加我们分析代码的难度,等case都执行完后再从arr数组中取下一个值。直到遍历完arr,才退出循环。


所以arr数组就是程序的主要执行顺序,我们只需遍历它,然后执行对应case语句块的代码,就能达到简化流程的目的了。



极验跟上面这个例子还有点不同,它在每个case里会重新赋值改变控制流的判断条件值,分发器根据这个判断条件值来执行流程控制,所以重点是需要能知道下一个执行的是哪个case语句。


极验的控制流还原思路是先拿到流程控制的初始值和for循环的条件判断值,接着提取出所有的case,然后遍历每一个case,计算出case的条件判断值,对比这个条件判断值是否与初始值相同,相同的话,可以删除一下无用语句,将当前case语句块存放到一个专门用于存放case语句块的数组,更新修改下条件判断值,最后将case语句块的数组替换ForStatement,具体看代码


代码实现参考公众号的全文吧:JS逆向:AST还原极验混淆JS实战》


看看代码实现对应的JS和AST结构

// 获取上一个节点,也就是VariableDeclaration 
var PrevSibling = path.getPrevSibling();

当前的节点是ForStatement,它的上一个节点就是VariableDeclaration,也就是for语句上面的一个语句,它是控制的初始判断值;

// 获取控制流的初始值
var argNode = PrevSibling.container[0].declarations[0].init;
var init_arg_f = argNode.object.property.value;
var init_arg_s = argNode.property.value;
var init_arg = AJgjJ.EMf()[init_arg_f][init_arg_s];

// 提取for节点中的if判断参数的value作为判断参数
var break_arg_f = node.test.right.object.property.value;
var break_arg_s = node.test.right.property.value;
var break_arg = AJgjJ.EMf()[break_arg_f][break_arg_s];


// 提取并计算case后的条件判断的值
var case_arg_f = case_list[i].test.object.property.value;
var case_arg_s = case_list[i].test.property.value;
var case_init = AJgjJ.EMf()[case_arg_f][case_arg_s];

//当前case下的所有节点
var targetBody = case_list[i].consequent;


// 提取break节点的上一个节点AJgjJ.EMf()后面的两个索引值
var change_arg_f = targetBody[targetBody.length - 2].expression.right.object.property.value;
var change_arg_s = targetBody[targetBody.length - 2].expression.right.property.value;

// 修改控制流的初始值init_arg = AJgjJ.EMf()[change_arg_f][change_arg_s];

targetBody.pop(); // 删除
breaktargetBody.pop(); // 删除break节点的上一个节点

resultBody = resultBody.concat(targetBody);


对照着上面的代码和AST结构自行研究哈,注释也比较详细了


经过以上的处理之后,就能得到一个相对容易阅读的JS代码了,这样分析逆向的时候也会更容易一点


还原后的效果


今天的AST还原极验混淆代码实战到这里就全部完成了,大家如果有更好的思路,欢迎分享给我呀。


以上内容仅为个人学习总结记录,思路主要参考公众号:搞点逆向778,分享出来希望能帮到大家,如有错误或遗漏之处请见谅


公众号:一生向风