概述
- 基础知识:AST抽象树,js,浏览器调试技巧,熟悉浏览器环境,基础不牢,地动山摇
- 参考文章:https://www.52pojie.cn/thread-1619464-1-1.html,文章生成的伪代码不能执行生成base64码,在实际操作过程中,补好类浏览器环境后才能走正确的流程,并且顺带生成了结果
- 文章按照遇到问题的顺序书写
旧版本还原过程
思路
参考文章写了部分分支的AST代码,代码检测及算法部分的还原代码没有
打个日志断点,走到正确的流程,将寄存器pc值,操作数,case值放入数组,与本地node环境对比即可
流程进入循环或与浏览器不一致
使用AST还原代码执行逻辑生成伪代码(环境检测及参数加密),缺少环境会影响代码执行顺序(pc寄存器控制流程),还原不了加密位置代码时,还是需要补环境,走入正确的流程
注意全局变量中值的赋值,不仅要还原代码块,还要保存变量(全局,本地,调用栈),以便得到正确的流程
初始化代码
get_0x07_blockstatement函数初始化,注意入参是md5值,并且保存在my_k[0]
function get_0x07_blockstatement(pc, md5_value) {
let order = [];
let Stack = []; //保存代码块,用于生成伪代码
let Stack_2 = []; //用于运行代码,保存变量值
let blockstatement_0x07 = [];
let my_r = [0, 0, 0, 0]; //全局变量,改变pc需要用到
let my_k = [md5_value]; //本地变量,存放eval执行结果
let is_change_pc = false;
某分支代码示例
参考文章代码编写case1AST还原代码,注意保存变量即可,一个用于代码正常执行,一个用于生成伪代码
case 1:
t = (2048 & opcode[pc]) >> 11;
s = (1536 & opcode[pc]) >> 9;
i = 511 & opcode[pc];
h = 511 & opcode[pc];
//console.log('11111', 't=' + t, 's=' + s, 'i=' + i, 'h=' + h);
switch (t) {
case 0:
//e.r[s] = i; //e.r 全局变量
my_r[s] = i;
blockstatement_0x07.push(
types.expressionStatement(
types.assignmentExpression(
"=",
types.identifier('global_' + s),
types.numericLiteral(i) //types.identifier('i'),这里需要放一个数字
)));
pc++;
break;
case 1:
//e.r[s] = e.k[h]
my_r[s] = my_k[h];
blockstatement_0x07.push(types.expressionStatement(types.assignmentExpression("=", types.identifier('global_' + s), types.identifier('local_' + h))));
pc++;
}
break;
被卡住的点-补环境
getOwnPropertyDescriptor方法浏览器返回undefined,node返回对象,hook此方法返回undefined,并且改写toString方法
Object.getOwnPropertyDescriptor = function getOwnPropertyDescriptor () {
return undefined;
}
toString = function () {
if (this.name == "getOwnPropertyDescriptor")
return "function getOwnPropertyDescriptor() { [native code] }"
return Function('prototype')['toString'].call(this)
}
在用到toString的地方改成自己的toString方法
case 8:
s = opcode[pc]>> 10 & 3;
i = opcode[pc] >> 2 & 255;
t = 3 & opcode[pc];
switch (t) {
case 1:
/*for (var r = e.f.pop(), i = [], o = 0; o < this.i; o++)
i.unshift(e.f.pop());
e.r[3] = e.r[this.s][r](i[0], i[1]);*/
var r = Stack.pop(),
r_2 = Stack_2.pop();
for (i_1 = [], i_2 = [], o = 0; o < i; o++){
i_1.unshift(Stack.pop());
i_2.unshift(Stack_2.pop());}
//console.log('zxcvb', my_r,r_2, r, 'ii', i_2, Stack_2, i)
if (my_r[0].name == 'toString')
my_r[0] = toString;
魔改base64算法还原
我还原的加密过程代码没有写成文章中的for循环,是按顺序执行,代码长度自然更长(但是代码是一样的),一共执行了11次(循环了11次),在调试过程中没有遇到for的地方,不知道原作者是否是自己手动写成for循环的,AST还原对逻辑的要求挺高,没有尝试写for的代码块
查看伪代码及结合浏览器调试能看出规律
- 32位md5值补全为33位,倒序编码
- 与42进行异或操作,需要断点调试,找到异或的位置,位置如下(下标):32 28 24 20 16 12 8 4 0
python补全为33位后的MD5值:e3\x0024ee71e4c25016ea52e71a02bc8639
e 3 \x00 2 4 e e 7 1 e 4 c 2 5 0 1 6 e a 5 2 e 7 1 a 0 2 b c 8 6 3 9’)
30 31 32 27 28 29 24 25 26 21 22 23 18 19 20 15 16 17 12 13 14 9 10 11 6 7 8 3 4 5 0 1 2
刚好是作者写的每组的2,4,6的位置,是真大佬,佩服 - js代码是生成了ASSCI码后直接在字符表里面取字符,生成base64编码
找到这个规律后还是能够使用python代码还原
新版代码分析
指令分析
在合适的位置下断点,找到正常流程时指令的执行顺序,注意在保存影响流程执行顺序的变量,缺少浏览器环境也会走到错误的分支,补环境不是必须的(但是是js逆向的基础),我在这里花了不少时间,明明是还原算法,在浏览器调试一波,打印关键日志信息(有关加密的赋值,计算的代码)即可,也不是非要看伪代码,伪代码也不是很直观
所有指令都是来自以下14组指令,case编号为倒数第二条指令的值,如case 453
352,300,368,360,426,453,440, //453分支下2条指令,即为453的子分支case 400, 440/445
352,300,368,360,426,458,254, //458 254/278/266
352,300,368,360,426,448,455, 471/455/465/475/ 第一次分析漏了461
352,300,368,360,426,418,
352,300,368,360,426,413,
352,300,368,360,426,483,350,
352,300,368,360,426,428,
352,300,368,360,426,423,108, 78/64/168/50/71/80/57/161/108/138/94/154/140/131/ 第一次分析漏了87/110 就这个分支多一些,16条
352,300,368,360,426,463,466, 476/466/ 第一次分析漏了486加密时走
352,300,368,360,426,438,
352,300,368,360,426,443,
352,300,368,360,426,433,
352,300,368,360,426,408,344
352,300,368,360,426,478,
jsdom补环境
既然补了环境,我还是记录一下,毕竟也是踩了不少坑。没有使用jsdom时,走到alert函数是报错了, 这个函数不会补,还忘大佬指教。
问题描述:Object.getPrototypeOf(window.alert)与浏览器不一致,不会给alert函数添加属性
const jsdom = require("jsdom");
const { JSDOM } = jsdom;
const dom = new JSDOM(`<!DOCTYPE html><p>Hello world</p>`);
window = dom.window;
document = window.document;
window._resourceLoader = undefined;
window._sessionHistory = undefined;
alert = window.alert;
window.__ZH__ = {'zse':{}};
var createElement = function createElement(tag){
return canvas = {
'getContext': function(){
return {"direction": "ltr",
"toString": function(){
return "[object CanvasRenderingContext2D]"
}
}
}
};
}
Object.assign(document,{
'createElement': createElement,
"toString": function(){
return "[object HTMLDocument]"
}
});
navigator = {
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.102 Safari/537.36",
"webdriver": false,
"toString": function(){
return "[object Navigator]"
}
};
location = {
"href":"https://zhuanlan.zhihu.com/p/37359861",
"toString": function () {
return this.href
}
};
history = {
"toString": function(){
return "[object History]"
}
};
screen = {"toString": function(){
return "[object Screen]"
}};
window.__proto__.constructor.toString = function (){
return "function Window() { [native code] }"
}
base64算法还原
补MD5,在旧版的33位基础上+1位随机值(随机值乘以127向下取整)放在0位置,补14个14(来源:34/16 16-2),一共48位
[
13, 0, 54, 51, 57, 98, 99, 56, 97, 48, 50,
101, 55, 49, 97, 53, 50, 49, 54, 101, 50, 53,
48, 101, 52, 99, 101, 55, 49, 50, 52, 101, 101,
51, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14,
14, 14, 14, 14
]
- 对应位置异或再与42异或
48位的前16位
413....... C_0 [
13, 0, 54, 51, 57, 98, 99, 56, 97, 48, 50,
101, 55, 49, 97, 53
] false
S[0] [
48, 53, 57, 48, 53, 51,
102, 55, 100, 49, 53, 101,
48, 49, 100, 55
]初始化时生成
- __g.r调用上一步生成的数组
465......... C_0 { x: [Function: x], r: [Function: r], _encrypt: false } r [
23, 31, 37, 41, 38, 123,
47, 37, 47, 43, 45, 42,
45, 42, 47, 40
]
返回值:C_3 [
240, 14, 73, 31, 20, 163,
8, 23, 192, 26, 63, 208,
233, 54, 248, 57
]
- 调用__g.x
参数1:补全的48位MD5的后32位[
50, 49, 54, 101, 50, 53, 48, 101, 52,
99, 101, 55, 49, 50, 52, 101, 101, 51,
14, 14, 14, 14, 14, 14, 14, 14, 14,
14, 14, 14, 14, 14
]
参数2:__g.r的返回值[
240, 14, 73, 31, 20, 163,
8, 23, 192, 26, 63, 208,
233, 54, 248, 57
]
返回值
[
29, 1, 136, 145, 185, 79, 14, 246,
223, 35, 93, 10, 254, 58, 47, 239,
85, 157, 160, 72, 201, 36, 72, 82,
251, 39, 203, 207, 241, 10, 176, 214
]
- 生成48位数组:__g.r的返回值+__g.x的返回值
[
240, 14, 73, 31, 20, 163, 8, 23, 192, 26,
63, 208, 233, 54, 248, 57, 29, 1, 136, 145,
185, 79, 14, 246, 223, 35, 93, 10, 254, 58,
47, 239, 85, 157, 160, 72, 201, 36, 72, 82,
251, 39, 203, 207, 241, 10, 176, 214
]
- 每三位倒序编码,编出4位字符
新版本与参考文章版本的区别
- 搜索column.app.1552bd8b4930aa16be5e.js文件有两处x96,这次走的是case很多的模块函数,之前的加密也在只是不知道在哪里被调用了
- 旧版本中是十四个分支都是规矩的躺在不同的函数里面
- 同一个MD5的加密结果不一样,加了随机值,编码表位65位,测试后第65位没有用到,但是我不能按照参考文章作者使用的python方式正确还原(还原出来部分位置一致),于是使用js还原了算法,__g.r,__g.x都参与了48位值的生成(直接抠出来),旧版本是没有的,使用execjs调用js也是不错的方式。
写在最后
搞了好几天才能做到这个程度,现在是离职状态,又受疫情影响,这几天在乡坝头看电脑,虫就往电脑上,灯上飞,现在脸上过敏,嘴也是麻的,疫情也不好去医院看病,好在有一个也被困在村里的医生给了几颗药。
真的从外包辞职后,转行爬虫这个知识涉及面广的方向,现在看来真的不太好,不知道什么时候能好起来。