某乎base64算法还原

概述

  • 基础知识: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   就这个分支多一些,16352,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(`

Hello world

`
); 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
]
  1. 对应位置异或再与42异或
48位的前16413....... 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
]初始化时生成
  1. __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
]
  1. 调用__g.x
参数1:补全的48MD5的后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
]
  1. 生成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
]
  1. 每三位倒序编码,编出4位字符

新版本与参考文章版本的区别

  1. 搜索column.app.1552bd8b4930aa16be5e.js文件有两处x96,这次走的是case很多的模块函数,之前的加密也在只是不知道在哪里被调用了
  2. 旧版本中是十四个分支都是规矩的躺在不同的函数里面
  3. 同一个MD5的加密结果不一样,加了随机值,编码表位65位,测试后第65位没有用到,但是我不能按照参考文章作者使用的python方式正确还原(还原出来部分位置一致),于是使用js还原了算法,__g.r,__g.x都参与了48位值的生成(直接抠出来),旧版本是没有的,使用execjs调用js也是不错的方式。

写在最后

搞了好几天才能做到这个程度,现在是离职状态,又受疫情影响,这几天在乡坝头看电脑,虫就往电脑上,灯上飞,现在脸上过敏,嘴也是麻的,疫情也不好去医院看病,好在有一个也被困在村里的医生给了几颗药。
真的从外包辞职后,转行爬虫这个知识涉及面广的方向,现在看来真的不太好,不知道什么时候能好起来。

你可能感兴趣的:(爬虫,python,js,爬虫)