AST反混淆实战(一)

AST简介

抽象语法树Abstract Syntax Tree,AST),或简称语法树(Syntax tree),是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。之所以说语法是“抽象”的,是因为这里的语法并不会表示出真实语法中出现的每个细节。

使用到的第三方库

esprima:代码转语法树
estraverse:AST遍历辅助库(深度优先搜索),包含遍历及替换节点的功能
escodegen:语法树转代码
jsdom:获取模拟浏览器window对象(很多js会检测环境)

相关文件

混淆源文件待上传后改链接
反混淆脚本待上传后改链接

反混淆前后对比

-反混淆前

    _0x1dc76b[_0x1972('0xed')](typeof window, _0x1dc76b[_0x1972('0x229')]) && _0x1dc76b[_0x1972('0x147')](typeof window[_0x1972('0x38e')], _0x1dc76b[_0x1972('0x229')]) ? _0x1dc76b[_0x1972('0x44c')](typeof window[_0x1972('0x38e')][_0x1972('0x131')], _0x1dc76b[_0x1972('0x299')]) ? /phantomjs/[_0x1972('0x348')](window[_0x1972('0x38e')][_0x1972('0x131')][_0x1972('0x39e')]()) ? ss = _0x1dc76b[_0x1972('0x64')](ss, _0x1dc76b[_0x1972('0x13')](0x1, 0xf)) : window[_0x1972('0x38e')][_0x1972('0x301')] ? ss = _0x1dc76b[_0x1972('0x64')](ss, _0x1dc76b[_0x1972('0x311')](0x1, 0xf)) : ss = _0x1dc76b[_0x1972('0x64')](ss, _0x1dc76b[_0x1972('0x36c')](0x1, 0x1)) : _0x1dc76b[_0x1972('0x44c')](typeof window[_0x1972('0x38e')][_0x1972('0x3bd')], _0x1dc76b[_0x1972('0x21f')]) ? ss = _0x1dc76b[_0x1972('0x64')](ss, _0x1dc76b[_0x1972('0x36c')](0x1, 0xf)) : !window[_0x1972('0x38e')][_0x1972('0x3bd')] ? ss = _0x1dc76b[_0x1972('0x64')](ss, _0x1dc76b[_0x1972('0x323')](0x1, 0xf)) : ss = _0x1dc76b[_0x1972('0x28c')](ss, _0x1dc76b[_0x1972('0x323')](0x1, 0xf)) : ss = _0x1dc76b[_0x1972('0x27c')](ss, _0x1dc76b[_0x1972('0x323')](0x1, 0xf));
    _0x1dc76b[_0x1972('0x44c')](typeof window, _0x1dc76b[_0x1972('0x229')]) && _0x1dc76b[_0x1972('0x26')](typeof window[_0x1972('0x433')], _0x1dc76b[_0x1972('0x229')]) ? _0x1dc76b[_0x1972('0x20e')](_0x1dc76b[_0x1972('0x1d8')](window[_0x1972('0x433')][_0x1972('0x1da')], window[_0x1972('0x433')][_0x1972('0x176')]), 0x0) ? ss = _0x1dc76b[_0x1972('0x27c')](ss, _0x1dc76b[_0x1972('0x323')](0x1, 0x2)) : ss = _0x1dc76b[_0x1972('0x370')](ss, _0x1dc76b[_0x1972('0x323')](0x1, 0x10)) : ss = _0x1dc76b[_0x1972('0x34d')](ss, _0x1dc76b[_0x1972('0x323')](0x1, 0x10));
    _0x1dc76b[_0x1972('0x26')](typeof document, _0x1dc76b[_0x1972('0x229')]) && _0x1dc76b[_0x1972('0x26')](typeof document[_0x1972('0x208')], fff) ? !document[_0x1972('0x208')](_0x1dc76b[_0x1972('0x454')]) ? ss = _0x1dc76b[_0x1972('0x2c6')](ss, _0x1dc76b[_0x1972('0x323')](0x1, 0x3)) : ss = _0x1dc76b[_0x1972('0x3da')](ss, _0x1dc76b[_0x1972('0x323')](0x1, 0x11)) : ss = _0x1dc76b[_0x1972('0x3da')](ss, _0x1dc76b[_0x1972('0x1a1')](0x1, 0x11));
    _0x1dc76b[_0x1972('0x119')](typeof window, _0x1dc76b[_0x1972('0x229')]) ? _0x1dc76b[_0x1972('0x119')](typeof window[_0x1972('0x410')], _0x1dc76b[_0x1972('0x21f')]) ? ss = _0x1dc76b[_0x1972('0x3da')](ss, _0x1dc76b[_0x1972('0x1a1')](0x1, 0x4)) : ss = _0x1dc76b[_0x1972('0x3da')](ss, _0x1dc76b[_0x1972('0x18d')](0x1, 0x12)) : _0x1dc76b[_0x1972('0x32e')](typeof window, _0x1dc76b[_0x1972('0x229')]) && _0x1dc76b[_0x1972('0x363')](typeof window[_0x1972('0x197')], _0x1dc76b[_0x1972('0x21f')]) ? ss = _0x1dc76b[_0x1972('0x3da')](ss, _0x1dc76b[_0x1972('0x18d')](0x1, 0x12)) : ss = _0x1dc76b[_0x1972('0x3da')](ss, _0x1dc76b[_0x1972('0x18d')](0x1, 0x12));
image.png

反混淆后

typeof window == _0x1dc76b['WfJBW'] && typeof window['navigator'] == _0x1dc76b['WfJBW'] ? typeof window['navigator']['userAgent'] == _0x1dc76b['XaIwC'] ? /phantomjs/['test'](window['navigator']['userAgent']['toLowerCase']()) ? ss = ss | 1 << 15 : window['navigator']['webdriver'] ? ss = ss | 1 << 15 : ss = ss | 1 << 1 : typeof window['navigator']['appVersion'] == _0x1dc76b['DWvnG'] ? ss = ss | 1 << 15 : !window['navigator']['appVersion'] ? ss = ss | 1 << 15 : ss = ss | 1 << 15 : ss = ss | 1 << 15;
    typeof window == _0x1dc76b['WfJBW'] && typeof window['screen'] == _0x1dc76b['WfJBW'] ? 970 + 1680 > 0 ? ss = ss | 1 << 2 : ss = ss | 1 << 16 : ss = ss | 1 << 16;
    typeof document == _0x1dc76b['WfJBW'] && typeof document['getElementById'] == fff ? !document['getElementById'](_0x1dc76b['djBAH']) ? ss = ss | 1 << 3 : ss = ss | 1 << 17 : ss = ss | 1 << 17;
    typeof window == _0x1dc76b['WfJBW'] ? typeof window['callPhantom'] == _0x1dc76b['DWvnG'] ? ss = ss | 1 << 4 : ss = ss | 1 << 18 : typeof window == _0x1dc76b['WfJBW'] && typeof window['_phantom'] == _0x1dc76b['DWvnG'] ? ss = ss | 1 << 18 : ss = ss | 1 << 18;
image.png

说明

//类似这种调用方法,根据传入参数算真实值(做了反混淆,会处理)
_0x1972('0x229')
//运算符封装为方法的,都会进行反混淆
    _0x4d51e8['GNVJM'] = function (_0x399108, _0x2aefef) {
        return _0x399108 == _0x2aefef;
    };
//某个key等于字符串,(这种没处理,因为不是很影响读码了,就没处理。)
_0x4d51e8['WfJBW'] = 'object';

逆向过程

1.下载核心js文件,内容混淆严重。先做反混淆。分析代码,拆解为几种混淆规则,针对每种规则反混淆。这个文件梳理主要有3、4种混淆规则,这里做了主要的2种,已可以阅读七七八八了(主要就是几个混淆,然后又互相嵌套,AST遍历是深度优先遍历,会从内到外依次反混淆,可以处理嵌套)。

  • 通过某个方法运算得到的string
var _0x1972 = function (_0x129385, _0x5b4f79) {
// ...............
//.............某些计算规则,返回根据传入参数计算出来的string。
//.................
return s;
}
//多次重复出现类似的代码
_0x1972('0x49')

//=================================================================================================================================
//反混淆核心代码
var ast = esprima.parseScript(content.toString());
//_0x1972方法在前57行,这个通过eval函数加载这个方法
eval(content.split('\n').slice(0, 57).join(''));
ast = estraverse.replace(ast, {
    enter: function (node, parent) {
        if (node.type === "CallExpression") {
             //因为混淆文件会变化,变量名会变,函数名称长度为7的只有_0x1972这个方法
            if (node.callee.name && node.callee.name.length === 7 && node.arguments.length === 1) {
                //如果是这个函数,则直接用计算后的结果替换
                let data = eval(`${node.callee.name}('${node.arguments[0].value}')`);
                if (data && data !== 'undefined' && data !== 'Number' && data !== 'NaN') {
                    return {
                        type: "Literal",
                        value: data,
                        raw: `'${data}'`
                    };
                }
            }
        }
    },
});

//=================================================================================================================================
//反混淆后_0x1972('0x49')变为计算后的值WfJBW
WfJBW
  • 运算符混淆
_0x615b4b['uUzso'] = function (_0x59febf, _0x3b43de) {
    return _0x59febf & _0x3b43de;
};
//中间有一次重命名过程
var _0x2bb501 = _0x615b4b;
//多次出现的类似代码
_0x2bb501['uUzso'](_0xd72c68, 8)

//反混淆核心代码
ast = estraverse.replace(ast, {
            enter: function (node, parent) {
                if (node.type === "CallExpression"
                    && node.arguments.length === 2
                    && node.callee
                    && node.callee.property
                    && node.callee.property.value
                ) {
                    let operator;
                    let rname;
                    let rvalue = node.callee.property.value;
                   //寻找重命名前的真实名称
                    estraverse.traverse(ast, {
                        enter: function (find_node, parent) {

                            try {
                                if (find_node.type === "VariableDeclaration"
                                    && find_node.declarations[0].id.name === node.callee.object.name
                                ) {
                                    rname = find_node.declarations[0].init.name;
                                    this.break()
                                }
                            } catch (e) {
                                this.skip()
                            }
                        }
                    });
                    //寻找对应方法的运算符,赋值给operator
                    estraverse.traverse(ast, {
                        enter: function (find_node, parent) {
                            try {

                                if (find_node.type === "AssignmentExpression"
                                    && find_node.left.object.name === rname
                                    && find_node.left.property.value === rvalue
                                ) {
                                    operator = find_node.right.body.body[0].argument.operator;
                                    if (operator) {
                                        this.break()
                                    }
                                    this.skip()
                                }
                            } catch (e) {
                                this.skip()
                            }
                        }
                    });
                    //替换为简化的语法树
                    if (operator) {
                        node = {
                            "type": "BinaryExpression",
                            "operator": operator,
                            "left": node.arguments[0],
                            "right": node.arguments[1]
                        };
                        return node
                    }
                }
            }


        }
    );


//反混淆后代码
_0x615b4b['uUzso'] = function (_0x59febf, _0x3b43de) {
    return _0x59febf & _0x3b43de;
};
var _0x2bb501 = _0x615b4b;
//_0x2bb501['uUzso'](_0xd72c68, 8)     变为            _0xd72c68 & 8
_0xd72c68 & 8;

2、代码反混淆后已可以阅读,找到核心代码。

//加密入口函数
ABC['prototype']['z'](seed, ts)

//之后会用python调用,这里简单封装入口
function encrypt(seed, ts) {
    ts = Number(ts);
    return ABC['prototype']['z'](seed, ts);
}
//hook取两个真实参数,真实结果应为971bzdrUsH6rIWHBVAmYXWgD3LfYvX/ci0ZvZl1DAdpLwgm8rTU6n4iFR5UFL3dliWHPzK1oNJkfepDbUYXKr4ZeMTBkmYrZcjuFh8K6Z6LiCgJwowN9mPggIR+336cmJH9B
encrypt('pA2EJB+wiUR5EXY0ZDTsTSh2z4oxxDfBIGHbOzpdB6A=', 1589961615404);

3、nodejs环境,添加jsdom模拟浏览器环境,返回的结果总变。而在浏览器环境下,结果不变,且与之前hook取到的结果一致。判断反混淆未影响代码逻辑,但是代码中有判断浏览器环境

//添加window对象
const {JSDOM} = jsdom;
const {window} = new JSDOM(`...`)

//有一些判断环境的代码(这里给列出部分)
//如 global、process、child_process这些都是nodejs环境有,而浏览器没有。有50处左右对比环境的。
    if (typeof global != _0x3e1ab5['VMFpl'] || typeof process != _0x3e1ab5['VMFpl'] || typeof child_process != _0x3e1ab5['VMFpl'] || typeof Buffer != _0x3e1ab5['VMFpl'] || typeof sessionStorage == _0x3e1ab5['VMFpl']) {
        ss = ss | 1 << 12;
    }

4、根据上面找出的对比浏览器环境的代码,写出对应的处理代码如:

//导入jsdom,有部分检测window对象,及其属性
const jsdom = require("jsdom");
const {JSDOM} = jsdom;
//检测当前url
const {window} = new JSDOM(`...`,{
    url: "https://www.zhipin.com/job_detail/?query=",
});
// // or even
const {document} = (new JSDOM(`...`)).window;
//检测top
top = window
//检测setInterval方法toString的值,浏览器及nodejs环境有差异
setInterval['toString']= function () {
    return "function setInterval() { [native code] }"
}
//有检测部分差异变量
global = undefined
process = undefined
child_process = undefined
Buffer = undefined
sessionStorage = {}

5、还有部分不好在代码上覆盖,直接在反混淆后替换

//这里是调试时过无限debugger
    code = code.replace('debugger;', '')
//有一个eval函数内部也是无限debugger,这里用console.log替换eval,直接输出代码不执行
        .replace('eval', 'console.log')
//替换部分浏览器值(在4中代码最上方覆盖不生效,这里直接替换掉,待研究)
        .replace("window['screen']['availHeight']", '970')
        .replace("window['screen']['availWidth']", '1680')
        .replace("window['outerWidth']", '1680')
        .replace("window['innerWidth']", '480')
        .replace("window['outerHeight']", '967')
        .replace("window['innerHeight']", '856')

6、终于在nodejs环境执行后与浏览器环境一致,也与hook到的值一致。
7、这个混淆的js文件,会定期更新,各变量名及内部加密算法改变。这里的4和5的处理都加入反混淆脚本,在js文件改变时,必须反混淆后才能出现5中的代码。(1.因为这里是直接调用,所以不关心加密算法是什么。2.因为上面5的部分代码无法覆盖,所以新的文件还是要走反混淆这一步。如果5的部分可以类似于4的处理,那么之后新js文件可以不用反混淆,只需要将4部分放入js文件头即可)

后记

看似很复杂的混淆,其实都只是几种混淆规则、互相嵌套造成的。看着脑壳疼,其实只要分析一下,借助AST,完成部分反混淆规则,即可正常阅读代码。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342