JS逆向-JSVMP

​ JSVMP(JavaScript Virtual Machine-Based Protect)JS虚拟机保护,VMP技术就是将原有的代码,转化为专用的字节码,由自写解释器进行解释执行,以此大幅提升逆向分析的成本,同样的,由于代码量的增加,运行效率也会大打折扣。

​ 这里谈一谈一个概念,解释执行,就是将源代码逐行转换为机器码然后再执行。类似于JAVA的JVM,将字节码转换成机器码再执行,但是在JSVMP中,这里的的机器码就不再是可供CPU执行的x86/ARM指令,而是我们自定义的虚拟指令序列,实质上还是JS代码。

​ 之前在windows软件、安卓native层中也遇到过VMP加壳,原理完全相同,比较亲切,但是在保护的对象和实际技术应用上,有着很大的区别,这里就不细谈了,等后续博客更新到安卓逆向或者windows 逆向,可能会拿在一起详细讲讲(??哪来的鸽子叫)。

JSVMP识别

​ JSVMP加密技术:原理、实现与攻防策略解析

https://comate.baidu.com/zh/page/hde0g2brg2j

​ 这篇文章讲的就很不错了,聊个题外话,我一直认为,不论是写技术博客,还是分享CTF的WP等等分享,都需要写一点自己的内容,哪怕你看完别人的文章,按照别人的思路,自己重新复述一遍,那么这篇文章就是有价值的。不要为了完成一篇文章,去写一些自己不清楚的东西,复制粘贴更甚,不要这么做,要真真正正留下来点东西,最不重要的就是毫无意义的文字和浪费掉的时间。

​ 接下来我将写一个简单的JSVMP,来演示一下他的执行流程,就最简单的,二数加法运算。实现之前,让我来讲一讲大体思路:

  • 将二数加法操作拆解成三条指令,加载数到栈LOAD、栈上二数相加ADD、和值返回RET,建立指二数加法指令序列
  • 创建指令序列解释器

​ 先准备一下指令序列,指令序列按照一定的规则进行保存,可以是特定的编码,这里规定操作码和参数都以join(“/“)进行拼接。先生成一个指令序列以便后续解释器使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
var instructions = [
{op : "LOAD", arg : "a"},
{op : "LOAD", arg : "b"},
{op : "ADD"},
{op : "RET"}
]

//上面就是我们加法所需要的指令,但现在还是不具有混淆的模样,直接观察就能看出我们的指令序列是一个堆栈操作的加法运算,
// 再对我们的操作码进行一次混淆

var opcode = {
LOAD : "_0x01d2c1",
ADD : "_0x01d2c2",
RET:"_0x01d2c3"
}

//通过这样的一个映射,将指令序列进行混淆


function compileByInstruction(instructions){
const bytecode = [];
for(let instruction of instructions){
bytecode.push(opcode[instruction.op])
if(instruction.arg !== undefined){
bytecode.push(instruction.arg.charCodeAt());
}
}
return bytecode.join("/");
}

//将操作码进行混淆,并且将arg进行ASCCII码转换,使用斜杠进行拼接

console.log(compileByInstruction(instructions));
1
2
(base) ➜  node demo.js 
_0x01d2c1/97/_0x01d2c1/98/_0x01d2c2/_0x01d2c3

​ 这样一个混淆后的ADD指令集就生成好了,接下来实现一下最关键的指令集解析部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// 创建堆栈和指令集,指令集加一个混淆,通过this.process进行动态加载,这里先置空,js中使用list来模拟stack非常的方便
function JSVMP(){
this.stack = [];
this.opcode = {
}
}

//在execute外定义ADD指令的逻辑
JSVMP.prototype.add = function(_0xa1b2c1, _0x1ab2c2){
return _0x1ab2c2 + _0xa1b2c1;
}

//指令集加载
JSVMP.prototype.process = function($opcode){
let opcodeArry = $opcode.split("/");
for(let i = 0; i < opcodeArry.length; i++){
$key = opcodeArry[i];
i++;
$value = opcodeArry[i];
this.opcode[$key] = $value;
}
}


//JSVMP核心执行入口,指令序列写死了,指令集也写在这里面
JSVMP.prototype.execute = function(content){
let instructions = "_0x01d2c1/97/_0x01d2c1/98/_0x01d2c2/_0x01d2c3";
let instructArry = instructions.split("/");
let $opcode = "LOAD/_0x01d2c1/ADD/_0x01d2c2/RET/_0x01d2c3"
this.process($opcode);
for(let i = 0; i < instructArry.length; i++){
switch (instructArry[i]){
case this.opcode.LOAD:
i++;
this.stack.push(content[String.fromCharCode(instructArry[i])]);
break;
case this.opcode.ADD:
let arg1 = this.stack.pop();
let arg2 = this.stack.pop();
this.stack.push(this.add(arg1, arg2));
break;
case this.opcode.RET:
return this.stack.pop()

}
}
}

//实例化一下,传入参数,打印结果
var jsvmp = new JSVMP();
console.log(jsvmp.execute({a:3, b:2}));
1
2
(base) ➜  jsvmp node demo.js
5

​ 结果是对的,这个简单的ADD VMP就已经实现了,自定义了指令集、解释器,还具备了一定的混淆。

​ JSVMP的特征就是:

存在以上结构
存在JSVMP字样

入参传入很长的一串字符串

如何处理JSVMP

​ 根据前人的经验,JSVMP大概有三种方法进行处理,第一种就是直接补环境,第二种就是纯算还原,第三种就是使用RPC进行远程调用。补环境比较方便,直接来像之前那样来补环境吧。

https://www.toutiao.com/

今日头条搜索

​ 拿一个字节的网站来看看他们是怎么做的JSVMP,关注到,这里有一个_signature参数,来看一下这个是怎么生成的,打断点调试一下。

signature生成

​ 调试到,是有一个windows.byted_acrawler.sign函数用来生成这个signature值,走进去看一看有什么东西。

JSVMP代码

​ 很明显,这里的signature生成逻辑已经被JSVMP混淆隐藏了,要在本地运行这个sign函数,可以将整个JSVMP代码全部扣下来,然后进行补环境。根据这个文件名可以发现,这个acrawler.js文件,是存在反爬的,可能会有环境检测,根据经验,进行补环境的过程中需要做一些特别的处理。

​ 浏览器补环境,在node环境中要特别注意一下两个东西,一个是exports,一个是module,这两个在浏览器环境中是undefined,但是在node环境中不是,这里反爬会在这里区别一下环境,防止本地运行,需要手动调整一下代码。

1
2
3
"undefined" != typeof exports ? exports : void 0, "undefined" != typeof module ? module : void 0,
//这里的三目运算符可以改成
void 0, void 0

length处理

​ 补环境中经常会遇到这样的length缺失,这就需要找对应上一步的A参数是什么,在对应的地方下一个log断点,和一个条件断点,查看length上一步是做了什么操作,然后再通过条件断点查看需要补什么环境。

断点调试

​ 然后使用调试,查看上一步的参数是protocol

protocol

​ 重新设置条件断点 A==”protocol”,查看需要补的参数,通过断点发现,这个protocol是location里面的东西,直接从浏览器中拿就好

查看protocol归属

​ node环境中又出现了错误,这里可以打印出所有的A参数,查看哪些环境是我们忽略掉的,导致位数不一样,浏览器中的signature参数的位数是147,这里是47,显然补环境还没有结束

localsessionStorage

​ 经过一步一步的S、R、A参数分析,发现缺少了localStorage和sessionStorage参数,以及对应的操作方法。这里遇到的问题比较多,有一段时间,我以为是我node版本的问题,结果重新走了一遍,通了。总之,要耐心,一步一步来,缺啥补啥。依旧补环境

​ 扔一个补的环境代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
var document = {
referrer: "https://www.toutiao.com/"
}

var window = globalThis;

var location = {
href : "https://www.toutiao.com/?wid=1657855126033",
protocol: "https:"
}

localStorage = {
"__pwa_push_show_count": "3",
"__tea_cache_first_24": "1",
"__click_close_download_panel_ts__": "1768906192550",
"tt_scid": "X-K2RhNMYuvyG8ixJ2bcVnTvdOwlrpi8bSZuvi3OMJx6jKSGDtiT1qvXoJcGL0xq80bd",
"xmst": "-KuT0_WjztH_2l4g7SBmGR2cLG5uqneDAX1dgWBCATcTCTgf_J5cZwdKwzqb4t-5-1PATg09gUdMUgCu0Kr4KVdZxhMudDDuIUwQi2xyy9MPIjN6IX5Za5vwiMU4n7bl",
"__tea_cache_tokens_24": "{\"web_id\":\"7592452048437610022\",\"user_unique_id\":\"7592452048437610022\",\"timestamp\":1768901790381,\"_type_\":\"default\"}",
"__pwa_push_show_time": "1768550589920",
"_byted_param_sw": "uOv3hoKTqsvlugnaOtw=",
"__tea_cache_refer_24": "{\"refer_key\":\"\",\"refer_title\":\"今日头条\",\"refer_manual_key\":\"\",\"routeChange\":false}",
"web_runtime_security_uid": "f413de2d-2d08-41db-a6c6-30c5810374de",
"__is_visited_home": "1",
"SLARDARtoutiao_web_pc": "JTdCJTIydXNlcklkJTIyOiUyMjc1OTI0NTIwNDg0Mzc2MTAwMjIlMjIsJTIyZGV2aWNlSWQlMjI6JTIyNzU5MjQ1MjA0ODQzNzYxMDAyMiUyMiwlMjJleHBpcmVzJTIyOjE3NzY2Nzc3OTE4MDMlN0Q=",
"ttcid": "846493ba57fa4d43815d5de2d6d1b8ee20"
}

sessionStorage = {
"__tea_session_id_24": "{\"sessionId\":\"47e8012b-f317-47ff-96d7-81eaedf56e20\",\"timestamp\":1768906615958}",
"_tea_cache_duration": "{\"duration\":11491611,\"page_title\":\"今日头条\"}",
"_byted_param_sw": "uOv3hoKTqsvlugnaOtw=",
"/": "1",
"tt_scid": "X-K2RhNMYuvyG8ixJ2bcVnTvdOwlrpi8bSZuvi3OMJx6jKSGDtiT1qvXoJcGL0xq80bd"
}

sessionStorage.getItem = function (arg){
return sessionStorage[arg];
}

localStorage.getItem = function (arg){
return localStorage[arg];
}

​ 这里有很多补环境用的Proxy自吐脚本,扔一个,调试起来比较方便

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function getEnv(proxy_array) {
for(let i=0; i<proxy_array.length; i++){
handler = `{
get: function(target, property, receiver) {
console.log('方法:get',' 对象:${proxy_array[i]}',' 属性:',property,' 属性类型:',typeof property,' 属性值类型:',typeof target[property]);
return target[property];
},
set: function(target, property, value, receiver){
console.log('方法:set',' 对象:${proxy_array[i]}',' 属性:',property,' 属性类型:',typeof property,' 属性值类型:',typeof target[property]);
return Reflect.set(...arguments);
}
}`;
eval(`
try{
${proxy_array[i]};
${proxy_array[i]} = new Proxy(${proxy_array[i]},${handler});
}catch(e){
${proxy_array[i]}={};
${proxy_array[i]} = new Proxy(${proxy_array[i]},${handler});
}
`);
}
}
proxy_array = ['window','document','locaion', 'navigator', 'history', 'screen', 'history']
getEnv(proxy_array);

//偷自 https://www.cnblogs.com/a438842265/p/18332426,感谢大佬分享

​ 至于纯算嘛,扔个链接:https://mp.weixin.qq.com/s/YAL4iT3ECQeK7vZRsLRFaA ,日后研究(!??哪来的鸽子叫)

​ 还得扔点啥,哦,RPC,自个搜搜Chrome RPC 或者 JsRPC怎么用吧。

https://github.com/jxhczhl/JsRpc

JS逆向-OB混淆

​ 讲OB混淆前,先要讲讲什么是混淆,混淆就是在不影响原本的代码的逻辑情况下对代码的形式和结构进行变动,使代码变得难以阅读和理解,增加代码逆向的难度。

​ 在JavaScript中,OB混淆一般是指的网站 https://obfuscator.io 提供的代码混淆,这里面提供了一个JavaScript代码混淆工具,可进行多种类型的代码混淆。

OB混淆识别

​ OB混淆基本上由四个部分组成:

1
2
3
4
- 一个大数组或者有一个大数组的函数 //用于存储加密的字符串和关键数据
- 一个自执行函数 //数组混淆器,用于打乱数组的顺序
- 解密函数 //数组访问器
- 加密后的函数 //原始逻辑

​ 这是OB混淆代码的基本构成,OB混淆有如下识别特征:

1
2
3
4
5
6
7
8
9
10
11
//变量名和函数名混淆 ,形如 _0xab1cd2。由"_0x"开头、无规律字母数字开头,后面跟上1-6位数字字母组合。
var _0xabcd = (function() {
var _0x1 = function(_0x2, _0x3) {
return _0x1234[_0x2 - 0x0];
};
_0x1['XyzAbc'] = function(_0x4, _0x5) {
return atob(_0x4);
};
return _0x1;
}());
//自执行函数中有push shift 字样

​ 纸上得来终觉浅,动手试一试才能知道OB混淆前后的改动:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
//混淆前的代码
!function(e){
console.log("Hello ", e);
}("Maododo")

//混淆后的代码
function _0x6cd1(_0x2fe9d3, _0x578f74) {
_0x2fe9d3 = _0x2fe9d3 - 0xd0;
var _0x4838f2 = _0x4838();
var _0x6cd151 = _0x4838f2[_0x2fe9d3];
return _0x6cd151;
}

var _0x158908 = _0x6cd1;

function _0x4838() {
var _0xce77dd = ['3856821yyeMCS', '5493156EGSXls', 'Hello\x20', 'Maododo', '346717LaHyhs', '39063eTpdiF', '7841840ZMbEsb', '4810420OBpuHU', '52ZUnfxK', '128SJfwCf', '17811900VqAsEG'];
_0x4838 = function () {
return _0xce77dd;
};
return _0x4838();
}

(function (_0x122f90, _0x42566e) {
var _0x5937d2 = _0x6cd1, _0x219585 = _0x122f90();
while (!![]) {
try {
var _0x28d2fe = -parseInt(_0x5937d2(0xd5)) / 0x1 * (parseInt(_0x5937d2(0xd8)) / 0x2) + parseInt(_0x5937d2(0xd0)) / 0x3 + parseInt(_0x5937d2(0xd7)) / 0x4 + parseInt(_0x5937d2(0xd6)) / 0x5 + -parseInt(_0x5937d2(0xd1)) / 0x6 + -parseInt(_0x5937d2(0xd4)) / 0x7 * (-parseInt(_0x5937d2(0xd9)) / 0x8) + -parseInt(_0x5937d2(0xda)) / 0x9;
if (_0x28d2fe === _0x42566e) break; else _0x219585['push'](_0x219585['shift']());
} catch (_0x342f06) {
_0x219585['push'](_0x219585['shift']());
}
}
}(_0x4838, 0xe533c),
!function (_0x44a1d9) {
var _0x355098 = _0x6cd1;
console['log'](_0x355098(0xd2), _0x44a1d9);
}(_0x158908(0xd3)));

​ 上面的代码就是一个标准的四个模块,让我来给拆解一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//第一块是解密函数,可以看到里面直接调用了大数组函数
function _0x6cd1(_0x2fe9d3, _0x578f74) {
_0x2fe9d3 = _0x2fe9d3 - 0xd0;
var _0x4838f2 = _0x4838();
var _0x6cd151 = _0x4838f2[_0x2fe9d3];
return _0x6cd151;
}

var _0x158908 = _0x6cd1;
// 很明显的大数组,用于存放变量,可以看到我们的常量数据‘Maododo’和‘hello’也在里面
function _0x4838() {
var _0xce77dd = ['3856821yyeMCS', '5493156EGSXls', 'Hello\x20', 'Maododo', '346717LaHyhs', '39063eTpdiF', '7841840ZMbEsb', '4810420OBpuHU', '52ZUnfxK', '128SJfwCf', '17811900VqAsEG'];
_0x4838 = function () {
return _0xce77dd;
};
return _0x4838();
}
//自执行函数,这就是就是数组混淆器,里面也有push和shift字样
(function (_0x122f90, _0x42566e) {
var _0x5937d2 = _0x6cd1, _0x219585 = _0x122f90();
while (!![]) {
try {
var _0x28d2fe = -parseInt(_0x5937d2(0xd5)) / 0x1 * (parseInt(_0x5937d2(0xd8)) / 0x2) + parseInt(_0x5937d2(0xd0)) / 0x3 + parseInt(_0x5937d2(0xd7)) / 0x4 + parseInt(_0x5937d2(0xd6)) / 0x5 + -parseInt(_0x5937d2(0xd1)) / 0x6 + -parseInt(_0x5937d2(0xd4)) / 0x7 * (-parseInt(_0x5937d2(0xd9)) / 0x8) + -parseInt(_0x5937d2(0xda)) / 0x9;
if (_0x28d2fe === _0x42566e) break; else _0x219585['push'](_0x219585['shift']());
} catch (_0x342f06) {
_0x219585['push'](_0x219585['shift']());
}
}
}(_0x4838, 0xe533c), //逗号表达式,下面的是第四块,也就是混淆后的执行逻辑,也是很清楚能看到这里,将变量和常量给替换成了'_0xab1cd2'格式的变量了
!function (_0x44a1d9) {
var _0x355098 = _0x6cd1;
console['log'](_0x355098(0xd2), _0x44a1d9);
}(_0x158908(0xd3)));

如何处理OB混淆?

解OB混淆工具、网站

​ 既然有工具能够直接生成OB混淆,那也有工具能直接解OB混淆

https://thanhle.io.vn/de4js/

https://github.com/Tsaiboss/decodeObfuscator

​ 这个工具整一下午没整好,有点失望,我后续将补充这部分内容,暂且搁置。

手动补OB混淆环境

​ 这里就跟OB混淆关系不是很大了,只需要将OB混淆后的部分,全部copy下来,简单替换几个函数就可以了,主要是耐心,

例子

​ OB混淆只不过是混淆之万一,用的多了,将规律记录下来,下次遇到便会得心应手。

某公众号功能

某公众号功能

​ 这里用的是真机adb调试Chrome inspect调试,针对不能开启f12的公众号、内嵌webview的APP还是挺好用的,公众号使用的话,还需要在微信输入并点击:

1
http://debugxweb.qq.com/?inspector=true

打开浏览器调试能力

​ 然后就可以进去直接调试,还需要再绕过一下无限debugger,这个网上文章有很多,这里用的是这个修改原型链:

1
2
3
4
5
6
7
8
var _constructor = constructor;
Function.prototype.constructor = function(s) {
if (s == "debugger"){
console.log(s);
return null;
}
return _constructor(s);
}

请求荷载

​ 可以发现这里,请求荷载被加密了,打断点跟一下,看看在哪里处理加密解密的

加mac函数

​ 来看看这个函数,使用了经典的平坦流控制进行关键函数的混淆,也就是while true 和 switch 进行代码执行流程的混淆,只需要打断点,然后把执行的代码,一行一行拼一起,不过我们只在乎请求报文加密,这里可以忽视。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
function encryptParamNEW(_$H, _$l, _$n) {
var Hg = a053b76H5;
var _$W = {
'Hbpen': Hg(0x1f1),
'ZFdMc': function(_$U, _$h) {
return _$U === _$h;
},
'XFqac': function(_$U, _$h) {
return _$U(_$h);
},
'LFjsF': function(_$U, _$h) {
return _$U + _$h;
},
'fgiYN': function(_$U, _$h) {
return _$U * _$h;
},
'EeZUw': function(_$U, _$h, _$L) {
return _$U(_$h, _$L);
}
};
var _$c = _$W[Hg(0x239)][Hg(0x296)]('|');
var _$w = 0x0;
while (!![]) {
switch (_$c[_$w++]) {
case '0':
if (_$W[Hg(0x307)](_$n, '1')) {
_$W[Hg(0x226)](encryptJSONSM, _$H);
} else {
var _$D = _$W['XFqac'](parseInt, _$W[Hg(0x245)](_$W[Hg(0x25f)](0x55d4a7f, Math[Hg(0x2fa)]()), 0x989680)) + '';
_$H = _$W[Hg(0x226)](wrapParam, _$H);
_$W['EeZUw'](encryptJSON, _$H, _$D);
}
continue;
case '1':
_$l['macKey'] = _$T;
continue;
case '2':
var _$T = '';
continue;
case '3':
var _$P = encryptParam(_$H);
continue;
case '4':
return _$H;
case '5':
_$T = JSON['parse'](_$P[Hg(0x305)]).mac;
continue;
}
break;
}
}

​ 简单看一下,这个函数是用来加mac的,进行报文防篡改的,可以pass了。

报文加密函数

报文加密函数加密后

​ 很清楚的看到,报文被加密了,这个ob混淆很简单呢,加密函数一下就能找到,也不用处理大数组等东西,算是一个失败的例子。

然后再进行代码本地化。

补环境01

​ 补环境过程中,遇到这种本地不能被解析的,可以直接去调试,看看这里是什么,直接替换成明文。因为这个getPublicKey函数比较短,环境不变的情况下,一般只有一个返回值,这里就可以自行判断,只留下return的值,也是个不错的补环境方法。

​ 其中有一个反调试函数,可以尝试将它删除

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
function _$o(_$H) {
var l1 = a053b76H5;
var _$l = {
'vtNxu': function(_$W, _$c) {
return _$W + _$c;
},
'wBNvU': function(_$W, _$c) {
return _$W > _$c;
},
'vWIAu': l1(0x22b),
'wetVp': l1(0x22a),
'SIxJk': function(_$W, _$c) {
return _$W !== _$c;
},
'BFLGu': function(_$W, _$c) {
return _$W / _$c;
},
'Kjmsr': l1(0x2cc),
'PgkEJ': function(_$W, _$c) {
return _$W + _$c;
},
'cjBVC': l1(0x2f1),
'GBnkh': l1(0x273),
'IuCnL': l1(0x259),
'YwJsb': function(_$W, _$c) {
return _$W > _$c;
}
};
function _$n(_$W) {

}
try {
if (_$H) {
return _$n;
} else {
_$n(0x0);
}
} catch (_$W) {}
}


//还有调用这个函数的自执行函数
(function() {
var g = a053b76n;
var _$H = {
'iwjOC': function(_$W, _$c) {
return _$W(_$c);
},
'tSvXd': function(_$W, _$c) {
return _$W + _$c;
}
};
var _$l = function() {
var K = a053b76n;
var _$W;
try {
_$W = _$H[K(0x228)](Function, _$H[K(0x22e)](K(0x31b) + '{}.constructor(\x22return\x20this\x22)(\x20)', ');'))();
} catch (_$c) {
_$W = typeof window !== K(0x2ac) ? window : this;
}
return _$W;
};
var _$n = _$l();
_$n[g(0x204)](_$o, 0xfa0);
}());

​ 将这两个部分的代码注释掉就可以,接下来就是很基础的补环境,还有ob混淆的补环境。大体就是这个流程。

报文加密

JS逆向-WebPack

​ 纸老虎,看着挺多挺唬人,其实很简单,麻烦一点,要耐心。

Webpack 识别

​ 是一个javascript静态打包工具,他的作用是将js代码进行分割,模块化,提高js的加载效率。

​ 下面是webpack的特征:

1
2
3
4
5
6
7
8
//文件开头存明显的webpack字样
(this.webpackJsonp = this.webpackJsonp || []).push([["chunk-83ba"], {xxx}
//文件开头为自执行函数
!function(xxx){xxx}
(function(xxx){xxx})
//存在大量闭包函数
"xxxx1": function(xxx){xxxx},
"xxxx2": function(xxx){xxxx}

​ Webpack使用闭包函数都会用到加载器,下面来讲一讲加载器长什么样:

1
2
3
4
5
!function(e) {...}([...]);
//!function("形参"){"加载器"}(["模块"]);

//1.自执行函数 !让引擎识别为函数表达式,而不是函数声明,其中形参e就是最后括号里的内容。
//2.其中模块有多种形式,可以是数组格式,可以是json格式(键值对)。其中每个元素都是一个函数。

​ 再写一个更为详细的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
!function(e){
//存放加载器
var c = {};

function n(t) {
var a = c[t] = {
i:t,
l:!1,
exports: {}
};
console.log(a);
return e[t].call(a.exports, a, a.exports, n),
a.l = 0,
a.exports
}

n.m = e;
//入口函数
n(2);
}([
function(){
console.log("初始化模块");
},
function(){
console.log("加密模块");
},
function(){
console.log("解密模块");
}
])

如何逆向Webpack代码?

​ 逆向webpack代码的痛点在于,代码执行流程不清晰,webpack将模块包装在闭包中,模块间复杂的函数依赖。解决上述问题就需要:

  • 找到需要导出的函数
  • 找到加载器
  • 找到模块存放及其嵌套依赖

​ 上面写的例子就比较简单了,代码执行流程比较容易分析,真实情况可能是,存在多个JS文件,每个JS文件中都有很多个模块,加载器不一定只在存在于模块上方。

​ 找到加载器和模块后,按照加载器加载模块,将需要用到的模块填入传参中,再用一个变量将加载器导出,就能直接调用函数链了。

例子

网址

https://synconhub.coscoshipping.com/

用到的工具环境:

IDE: WebStorm (方便折叠代码,vscode、notepad++、notepadNext都可以)
浏览器:Chrome Devtools

登录页参数加密

​ 这里看到传入的密码是加密的,打断点进行调试,最终找到加密是在app.939ea5e9.js中“MuMZ”模块中的r函数。找到我们的目标函数了。

目标函数位置

​ 接下来找加载器位置,往上翻一翻,根据前人的经验,遇到xxx.m就可能是加载器了,直接去看c。

寻找加载器

​ 就是这个,是不是很像一开始的例子。

加载器

把整个代码复制出来,折叠一下,就一目了然了。

折叠代码

​ 删减代码,然后尝试加载所需的模块,存在报错,在加载器中加一段调试代码,看看缺少哪些模块。

调试所需模块

​ 这样就能知道,问题出在哪里。

调试所需模块

​ 补了一共51个模块,不报错了,最后查看一下模块内部的导出名,是b,运行一下,没报错。

本地化webpack

🎨