来自高纬度的对抗:AST解密JS代码实战(上)

前言

AST全名"Abstract Syntax Code",抽象语法树。他能够将JS源代码通过词法分析、语法分析后,按代码结构转换为树状显示,每个节点代表代码中的一个结构。AST在前端构建中起了非常大的作用,可以说充分体现了JavaScript这门语言的精髓。很多前端技术例如ES6、TypeScript的发展都离不开AST。

使用在线结构化工具(https://astexplorer.net/)写一个简单的函数如下:


左边是输出hello world的简单代码,右边是经过结构化后的树状结构。这里面的节点说明,可以参照https://github.com/yacan8/blog/blob/master/posts/JavaScript%E6%8A%BD%E8%B1%A1%E8%AF%AD%E6%B3%95%E6%A0%91AST.md

本文的重点并不是介绍AST,如果有读者对AST不了解的话,强烈建议先学习下AST。

开始

这次用的代码是某V5的加密,直接耶稣也解不开的配置拉满被。。

瞄一眼代码:
除了能认出好像是JS以外,啥都看不出来。没关系,我们慢慢来分析。
先把形如\x5d这种编码的字符串还原,随便找个格式化网站美化一下去掉\x..。。。

我太懒了,呜呜呜,对不起自己。

好,我们观察一下代码:

可以看到,代码里有大量类似“_0x515a("0x4", "z#w7")”这种形式的代码,点过去看下,做了一层加密。那么执行这个方法,就可以还原真实的参数。
丢浏览器里执行试试:
执行之后得到解密字符串。这种混淆在我之前的文章模拟登录一号店时也遇到了,当时是写正则脚本调用加密函数处理。这次换一种方式,人力分析。

以加密函数为界,我们将代码分为两个部分,第一部分为加密部分,第二部分为原本的逻辑部分。
我们先把第一部分剪出来。大概160行。我尝试过了将第一部分转为AST处理,但是貌似开启了代码保护模式,无法转换为AST树。无妨,那这部分我们手动处理。

满篇的十六进制字符串确实不好分析,为了方便理清逻辑,我们用编译器对重点参数批量替换。
先把这个大数组替换一下:
查看这个大数组的引用:

就是这个数组正下方的自执行函数。从这个函数开始扒:
通过上面的查看调用处,我们可以知道这里第一个参数_0x501011就是大数组,_0x442532是一个数字,这里传入的106。我们将_0x501011重命名为argument_list,_0x442532不知道干嘛用了,先命名为argument_num。
然后看下面的_0xf3ad3函数,一个while循环,貌似做了把大数组按某种序列重新排序的操作。继续替换,将函数名_0xf3ad3替换sort_list,参数_0x35e21d的引用只有在这里,不管他也无妨。然后继续往下看
一个新的函数_0x1e0679,里面定义了一个对象_0x48a671,定义了一些cookie的操作。那我们就把_0x48a671命名为cookie_object。
然后我们可以先合上他不管了,接着看:
这段貌似东西有点多,先批量改个名~~
清晰多啦,首先是定义了一个update_cookie方法,里面用正则对cookie_object的removeCookie方法的字符串匹配。这里有一个大坑,我开始做的时候也没有发现,后来百度了一下,才找到这个坑。把代码放进浏览器里执行:

看到这个方法返回了false,看到这里你是不是想去找false执行的逻辑了?如果真的这样去做,那就掉进了作者挖的坑里了。仔细观察你会发现这段正则里是没有匹配换行的,初始加密的代码也是没有换行,所有代码都在一行里。然而我们输入到浏览器的是格式化之后的代码,有大量的换行,必定是返回false啊。

我们把代码压缩一下再试试:
!神奇的事情发生了,原来这里真正返回的是true。他在这里实际上是检测了代码是否被格式化过,如果格式化了出现换行,就代表有人尝试以其他方式在运行这段代码,那么后续就不走正常的逻辑。这里需要注意。
那我们接着就看返回ture的逻辑吧:
第一个参数为null,第二个参数为字符串“counter”,getCookie内部首先判断第一个参数是否为空,如果为空就返回调用它时传入的参数。也就是_0x1a5844方法会返回传入的字符串。为了方便理解,我们直接改下这个函数被:
然后对第二个参数“counter”做了一些处理。我们先看看哪里用到了运行结果_0x2d894f。
好像这个_0x2d894f之后也没什么用,整个getCookie得到的结果也没有再使用。那么这个东西就是在混淆视听。他实际上真正要执行的是下面这段代码:
那整个getCookie的逻辑已经很清晰了,改写一下:
这里面的sort_list方法就是上面对数组进行重排序的方法,argument_num是整个自执行函数传入的第二个参数,也就是106。
至此,第一个自执行函数执行完毕。什么setCookie,removeCookie都是吓唬你的。

然后看第二个函数,也就是我们的加密函数。

一步一步来,先把我们看到的出现频次较高的变量重命名,改成人类能理解的形式:
逐行解读,首先第一行把arg1-0,就是把arg1强制转换为整型,这个也是js的特点,类型自动转换,比如我们想要整数10变成字符串的"10",只需要10+""就可以了。
然后在大数组里查找序号为arg1的元素赋值给argument。看到这里大家已经有眉目了吧,这个大数组就是和用到的参数映射起来的啊,不过是在映射的时候加了一层加密。

if判断,不用说了吧,这个东西必定是undefined,肯定会走这里的逻辑。进去是一个自执行函数。改一下名:
window.abot是js解base64的方法,这里将这个方法重写。
接下来是rc4的加密逻辑了,没必要管他,直接跳过:
往下:
emmm,故技重施,一样的,直接走最里层if里的逻辑。又是一层乱七八糟的加密逻辑。不看了,头大。。感兴趣的同学可以分析一下。
到这里代码的第一部分,加密部分就分析完了。我们把整段JS压成一行试下,随便拿一个调用输出试试:
没问题~~

下一步要请出AST了,解释一下第一部分为什么我没有用AST,一是代码逻辑比较零散,用AST处理事倍功半,每行代码都去找节点更麻烦,二是加入了代码保护,无法顺利的转为AST树,需要另外处理。于是直接手动分析了,也不是很麻烦,这样也能学会一些v5加密的逻辑。

接下来的第一步,把所有加密参数都解密。我们先把逻辑部分的代码复制进AST结构化工具里观察,找一个调用加密代码的地方:
首先节点类型是CalleeExpression,也就是函数执行表达式,内部最明显的特征是callee节点中的name属性为"_0x515a",并且arguments,也就是实参个数为2。通过以上规则,我们可以确定一个节点是否调用了解密函数。编写代码,首先新建一个js文件,把原始js代码以字符串的形式导出。为什么?因为不想看见他,恶心!
再新建一个js文件,把解密函数导出,这里解密函数还是用最初没有改变量名的吧,天晓得我们改的时候会不会在某些地方改错了:

然后是处理代码:
const template = require("@babel/template").default; 
const traverse = require("@babel/traverse").default; 
const t = require("@babel/types"); 
const generator = require("@babel/generator").default;   

const path = require('path'); 
const fs = require('fs')

const {jscode} = require('./v51');
const {encrypt} = require('./encrypt1')
// 将代码解析为AST树
const ast = parser.parse(jscode);

// 遍历Literal节点
traverse(ast, {
    CallExpression: decryptStr
})

function decryptStr(path) {
    var node = path.node;
    // 筛选符合条件的节点
    if (!t.isIdentifier(node.callee))
        return;

    if (node.callee.name!="_0x515a")
        return;
    
    if (!node.arguments||node.arguments.length!=2)
        return;
    // 替换节点值
    var new_str = encrypt(node.arguments[0].value, node.arguments[1].value);
    path.replaceWith(t.stringLiteral(new_str));
        
}

let {code} = generator(ast);
console.log(unescape(code.replace(/\\u/g, '%u')));

还原结果:

调用解密函数0x515a的全部部分已经被替换了。

总结

你肯定想问,还没完呢,咋就总结了?已经11点啦··明天还要上班,姑且算是前篇,后篇我会在近两天找时间更新。

其实这篇并没有用到多少AST,代码第一部分没有用的原因也提到了。第二部分的解密也刚刚开始,AST崭露头角。
这篇大部分篇幅都在分析解密函数上,我的想法还是这样,通常对一个反爬方案或者加密方案的处理是比较容易找出来的,更多的时候不需要去分析他处理的逻辑,node黑盒调用的场景太多了。但是如果永远不去分析,就永远学不会。只是了解怎么应对,不去理解原理,是没法解决100%的问题的。V5是比较优(e)秀(xin)的JS加壳之一,能够通过简单的分析理解他初步的加密原理也是很大的收获。

面对严重的加密混淆,一定要稳住,看见乱七八糟的代码不要怕,慢慢找入口点,最开始我也被这些代码吓到了,尤其是第一部分无法转成AST树,并且第一部分格式化后在浏览器中运行内存拉满也运行不出结果。直到分析以后才发现,他对JS代码做了一层校验,我输入格式化后的代码,正中圈套。

后篇将针对剩余的代码逻辑,力求还原给人类看的代码。

额,发现好像最近挖的坑有点多。。想起爱奇艺的m3u8下篇还没写。。。好吧,这次保证不会鸽,毕竟我也想看看自己能把这个加密还原成什么样~~~当然,无论结果如何,我都会把我最后取得的进度和过程分享出来,希望和需要的人一同进步。

最后

声明:本文分析过程仅供学习,并无任何个人以及商业或其他用途。如有不慎侵权,请联系我删除。

你可能感兴趣的:(来自高纬度的对抗:AST解密JS代码实战(上))