前言
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的加密,直接耶稣也解不开的配置拉满被。。先把形如\x5d这种编码的字符串还原,随便找个格式化网站美化一下去掉\x..。。。
我太懒了,呜呜呜,对不起自己。
好,我们观察一下代码:以加密函数为界,我们将代码分为两个部分,第一部分为加密部分,第二部分为原本的逻辑部分。
我们先把第一部分剪出来。大概160行。我尝试过了将第一部分转为AST处理,但是貌似开启了代码保护模式,无法转换为AST树。无妨,那这部分我们手动处理。
然后看下面的_0xf3ad3函数,一个while循环,貌似做了把大数组按某种序列重新排序的操作。继续替换,将函数名_0xf3ad3替换sort_list,参数_0x35e21d的引用只有在这里,不管他也无妨。然后继续往下看
看到这个方法返回了false,看到这里你是不是想去找false执行的逻辑了?如果真的这样去做,那就掉进了作者挖的坑里了。仔细观察你会发现这段正则里是没有匹配换行的,初始加密的代码也是没有换行,所有代码都在一行里。然而我们输入到浏览器的是格式化之后的代码,有大量的换行,必定是返回false啊。
那我们接着就看返回ture的逻辑吧:
至此,第一个自执行函数执行完毕。什么setCookie,removeCookie都是吓唬你的。 然后看第二个函数,也就是我们的加密函数。
然后在大数组里查找序号为arg1的元素赋值给argument。看到这里大家已经有眉目了吧,这个大数组就是和用到的参数映射起来的啊,不过是在映射的时候加了一层加密。
接下来是rc4的加密逻辑了,没必要管他,直接跳过:
到这里代码的第一部分,加密部分就分析完了。我们把整段JS压成一行试下,随便拿一个调用输出试试:
下一步要请出AST了,解释一下第一部分为什么我没有用AST,一是代码逻辑比较零散,用AST处理事倍功半,每行代码都去找节点更麻烦,二是加入了代码保护,无法顺利的转为AST树,需要另外处理。于是直接手动分析了,也不是很麻烦,这样也能学会一些v5加密的逻辑。
然后是处理代码:
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下篇还没写。。。好吧,这次保证不会鸽,毕竟我也想看看自己能把这个加密还原成什么样~~~当然,无论结果如何,我都会把我最后取得的进度和过程分享出来,希望和需要的人一同进步。
最后
声明:本文分析过程仅供学习,并无任何个人以及商业或其他用途。如有不慎侵权,请联系我删除。