某里231------AST解混淆流程
- 前情提要
- 移除void运算符
- 三元表达式转if/else结构
- 将if/else转为switch结构
- 三重switch转为一重switch
- 删除虚假的case
- 删除不存在的Bi赋值
- 处理同一Bi赋值的代码
- 处理if恒等
- 删除IIFE
- [还原被拆散的 if-else 结构](#还原被拆散的 if-else 结构)
- 还原while结构
- 当个出口合并
- 还原for结构
- 还原成顺序结构
- 展示最后效果图
前情提要
这里只是分享我的解混淆流程思路,由于我没有去再去梳理一遍流程,可能显得比较乱以及可能有些步骤冗余或者不够简洁,同时注意一下,在本章,我只展示如何去把最大的控制流(switch (ai))去还原成顺序结构,同时js的版本为1.231.69,如果后续网站更新了版本我无法保证流程一致,但应该大差不差。
对于其它的短小的控制流,比如如下图的,直接去使用Ai来还原即可。

这里温馨提示一下每还原一个步骤最好去替换一下网站文件去验证一下是否能正常运行。
移除void运算符
在原始代码中有大量的void运算符,可以去删除它,对于代码的正常运行没有任何副作用


三元表达式转if/else结构
在移除void运算符之后,可以发现代码中有大量的三元表达式


如果你觉得一次无法还原全部(无法将各种情况考虑到),可以一部分一部分的编写还原
如下图就是我编写的还原插件,其实大部分的代码都是重复的,只是当时编写时发些有些没有还原,只是重新去匹配没有还原的代码再把它转换

还原之后的效果如下

将if/else转为switch结构
通过分析还原之后的if/else其实不难发现这是一个二分查找式
以如下的小部分代码为例子
javascript
if (211 == Wi) {
for (Yd = 0; Yd < ca.length; Yd++)
if ((tE = ca[Yd] === ya)) {
Wt = Yd;
id = 1;
}
if (id) {
Bi = 29;
} else {
Bi = 5839616;
}
} else if (Wi < 211) { // 但判断到这时,Wi的取值范围只能是210与212
if ((wa = 1 === (La = 1 & ga))) { // then分支执行只能是Wi = 210
Bi = 4856064;
} else {
Bi = 10886656;
}
} else { // else分支执行只能是212
if (ca) {
Bi = 6885120;
} else {
Bi = 5902080;
}
}
如果不是能够理解,可以去这个网站去可视化理解一下二分查找算法演示工具
这个时候你就可以把这一段的if/else结构转换为switch结构

三重switch转为一重switch
通过如上的步骤还原,现在我们得到了三个switch(switch (ai)、switch (pi)、switch (Wi))
同时发现主要通过Bi = xxxx;来决定下一个执行,这是就可以把这三个switch转为一个switch(Bi)
现在要根据源代码中的分发器去分析
javascript
for (var Bi = 2235904; void 0 !== Bi; ) {
var ai = 255 & Bi,
Ni = Bi >> 8,
pi = 255 & Ni,
Ci = Ni >> 8,
Wi = 255 & Ci;
}
通过对还原之后的代码分析,可以知道switch (pi)、switch (Wi)只出现在switch (ai)的case 0中,而switch (ai)除case 0之外的其它case只需要Bi等于多少就会执行到与之对应的case
如果要执行到switch (ai)、switch (pi)、switch (Wi)这三个switch的case 0
就需要ai、pi、Wi分别等于0
可以根据如上的公式去逆运算出Bi的值,这时运算出的Bi的值值就是switch(Bi)中case的index
以Bi = 2235904为例子,当Bi = 2235904时Ni = Bi >> 8 => Ni = 8734; pi = 255 & Ni => pi = 30; Ci = Ni >> 8 => Ci = 34; Wi = 255 & Ci => Wi = 34;执行的是switch (ai) => case 0、switch (pi) => case 30、switch (Wi) => case 34
现在就是根据这个三个值(0、30、34)去逆运算出Bi = 2235904
最后通过ai + (pi << 8) + (Wi << (8 * 2))即可逆运算出Bi的值,最后根据这个公式可以直接计算出这三个switch中要运行每个case所需要Bi的值,最后转为一个switch,当Bi等于多少是直接运行与之对应的case。
最后的转换效果为如下图

到这一步基本的前期准备工作就到位了,后续的步骤需要你去自己去分析实践一下,也就是后续的步骤是每个步骤相当于一个模块,每个模块对应不同的功能,你需要自己去分析一下,就像搭积木一样。
删除虚假的case
通过上一步骤之后可以得到大量的case,但是呢其中有一些是无法运行到的虚假的case,这个时候呢就需要去构造一个函数来辅助我们,然后插装。
javascript
window.yxsx_exec = [];
function save_case(caseValue) {
// 记录该 case 被执行了 (如果不希望重复记录,可以加 indexOf 判断)
// 这里仅仅是 push 进去,方便查看执行顺序轨迹
window.yxsx_exec.push(caseValue);
// 如果你只想知道哪些 case 跑过(去重),可以用下面这种写法:
if (window.yxsx_exec.indexOf(caseValue) === -1) {
window.yxsx_exec.push(caseValue);
}
}
需要编写一个插件去实现把如上的代码去插入源代码中,然后把对每个case插入调用函数代码,如下图所示

这时候把这个插装完成的代码替换到网站去运行一下,使用不同的浏览器去运行一下去收集到所有运行到case_index数组,然后通过这个数组去删减没有运行过的case
效果如下图所示


删除不存在的Bi赋值
在上一步,删除了大量的虚假case之后,代码总还存有一些IF语句如下所示
javascript
(function () {
if ((Hx = dx)) {
Bi = 8129280;
} else {
Bi = 4132352; // 假如case 4132352被我们当作虚假case删掉,那么Bi = 4132352;就是一个没有意义的代码,需要删掉
}
}).call(this);
// 变成如下
(function () {
Hx = dx; // 注意要保留表达式,不要轻易删掉
Bi = 8129280;
}).call(this);
处理同一Bi赋值的代码
在代码中还存这一些共同出口的代码

要把它转为如下图的代码

处理if恒等
在源代码中还有一些代码如图所示

javascript
(function () {
n = v !== s; // n的值只有两个true或者false
KL = 223;
if ((B = (n *= n) > -63)) { // 无论n为true或者false,结果只有两个0或1,无论那个都大于-63
Bi = 1969664;
} else {
Bi = 11802880;
}
}).call(this);
源代码中还有许多类似这种的代码
需要去处理
转换效果

删除IIFE
在源代码中有大量的IIFE表达式

但是去除要注意局部变量与全局变量,放置被污染
去除之后的效果

接下来的步骤都需要你对图论有一定的了解
还原被拆散的 if-else 结构
在代码中有如下的结构
javascript
switch (Bi) {
case 7936512:
if (vL) {
Bi = 8526336; // 无论是否执不执行then分支,最后下一next都是Bi = 5243904;
} else {
Bi = 5243904;
}
break;
case 8526336:
tf += 4;
Bi = 5243904;
break;
// 最后可以转为
case 7936512:
if (vL) {
tf += 4;
}
Bi = 5243904;
break;
}
要还原这个代码需要一些图论的算法
首先你需要创建两个map,一个存储每个case的有效语句,另一个存储每个case的下一指向
然后构建控制流图
最后运用BFS算法与DFS算法去转到公共汇合点
最后根据分支特征分类为菱形、三角形或空判断来进行还原
效果如下


还原while结构
接下来是还原while结构,在源代码中有许多这样类似的结构
javascript
switch (Bi) {
case 1185280:
if (Ka < dd.length) {
Bi = 8657152;
} else {
Bi = 10755328;
}
break;
case 8657152:
yL = dd.charCodeAt(Ka) - 242;
Xa += String.fromCharCode(yL);
Bi = 10296832;
break;
case 10296832:
Ka++;
Bi = 1185280;
break;
// 可以转为
case 1185280:
while (Ka < dd.length) {
yL = dd.charCodeAt(Ka) - 242;
Xa += String.fromCharCode(yL);
Ka++;
}
Bi = 10755328;
break;
}
这个转换的方法与上一步骤相似,都需要构建控制流图
还原效果如下


当个出口合并
在代码中存在这大量的单一出口的case,可以把这些case合并为一个case
到现在如果你之前的步骤都成功实现的话,现在你的case大概只有100多个
效果如下


还原for结构
在源代码中还有一些for结构的特征
比如下面的部分代码
javascript
switch (Bi) {
case 3343360:
if (QE) {
EE = LT + wT;
vt += EE;
}
LT++;
if (LT < 100) {
Bi = 4456704;
} else {
Bi = 3610368;
}
break;
case 4456704:
QE = !1;
EE = 0;
yT = DE;
if (EE < 3) {
Bi = 2823936;
} else {
Bi = 3343360;
}
break;
case 2823936:
_L = pL[PT];
if ((uL = _L[(aL = (uL = 3 * LT) + EE)])) {
if (!(_L = uL[yT] != dN[EE])) {
_L = uL[bE] != Gx[EE];
}
aL = _L;
if (!(_L = QE)) {
_L = aL;
}
QE = _L;
}
EE++;
if (EE < 3) {
Bi = 2823936;
} else {
Bi = 3343360;
}
break;
}
还原之后的效果

还原成顺序结构
通过如上的步骤之后你的case将会锐减到两位数

此时去分析代码你会你会发现剩下的case用图论去分析只有if/else结构,我也建议你在最后的还原成顺序结构时,先把代码中的所有循环解决,只留下if/else结构这样就可以很容易的还原成顺序结构。
此时代码中的case关系均为if/else结构,但是如果你单纯的只时按照这种关系还原成顺序结构,你会发现代码会爆内存,只是因为代码中有大量的case共用一个相同的函数功能,如果不提取出来,还原之后的代码会成指数级暴增,导致运行内存爆炸。

到此,已经成功还原成顺序结构,至于之后的字符串变量合并等就不在赘述。
展示最后效果图





