Hello World

​ 又一次问世了,我的博客,不知写什么为好。总之,你好,世界!

WEB-无境SRC靶场

​ 小六推荐了这个靶场,好久没玩玩靶场了,扫了一眼,题目都是比较感兴趣的,打一打,顺便总结整理一下平时挖洞时思路与技巧。前段时间心血来潮在看GoF设计模式,花费了很多时间用来学习和练习,看这个进度也是要等好久才能输出关于设计模式的文章了,安卓逆向进度也放一放,慢慢来吧。

​ 既然是SRC,那这个系列就点到为止,尽量不使用自动化工具,手工证明即可,不以拿flag为目标。

支付漏洞

​ 这里的支付漏洞还是相当简单的,也是面试中的常被问到的点。

简单支付漏洞之整数溢出

​ 做这道题之前,需要了解一些基础的数值的数据类型:

类型 符号性 字节数 数值范围
int8 有符号 1 -128 ~ 127
uint8 无符号 1 0 ~ 255
int16 有符号 2 -32768 ~ 32767
uint16 无符号 2 0 ~ 65535
int32 有符号 4 -2147483648 ~ 2147483647
uint32 无符号 4 0 ~ 4294967295
int64 有符号 8 -9223372036854775808 ~ 9223372036854775807
uint64 无符号 8 0 ~ 18446744073709551615

​ 可以参考一下月神的这个科普视频: 【最大值溢出漏洞挖掘科普-月神】

​ 就拿int8来举例子吧,有符号的数是通过补码的最高位来表示正负,1是负数,0是正数,那么int8类型的最大值就是0111 1111,如果在这个基础上+1的话,就变成了1000 0000,就是负数中的最小值,通过补码转换就是-128,所以就会出现,int8类型的127+1=-128,然后还有一种情况,这种是正常的补码运算,就是-1 + 1,也就是1111 1111 + 0000 0001 = 1 0000 0000,进位舍弃,结果为0000 0000,也就是0。所以,整数数值就相当于首尾相连。

​ 来分析分析这个场景吧:

​ 现在我们有1000元,目标商品是2400元,通过金额整数溢出来达成购买商品。

个人中心

订单结算

数据包1

​ 分析一下,这里上传了数量,我们能控制的有用参数貌似只有数量,商品单价是2400,看一下参数有什么限制。

数据包2

​ 这里限制死了,只能上传大于等于0的参数,这里尝试上传一个大数
数据包3

​ 推测这里使用的是int32,月神那个视频里也提及了,微信支付使用的也是int32。反复测试一下,总金额没有超过2147483647,尝试进行刚刚好溢出,可能付款的值就小于当前值,数量为q, 2400*q -> 2147483647+2147483648,计算出q的值取整然后+1。

整数溢出

简单支付漏洞之优惠券数量

​ 这个漏洞比较简单啊,成因是上送了优惠券的个数并进行计算,未对购买者持有优惠券数量进行校验。

优惠券数量更改

简单支付漏洞之金额修改

​ 这个漏洞也比较简单,上送了支付金额,推荐后端开发时,不进行上送利率、金额、总价等,只上送产品的id、个数并进行严格的过滤。

金额修改

订单记录

简单支付漏洞之订单编号

​ 这个的思路也十分的清晰,他的交易逻辑就是先上送产品id和数量,生成订单号,然后通过订单号进行扣费,这里能改的也就是数量,测试一下数量是否做了校验。

订单创建

支付订单

订单信息

简单支付漏洞之四舍五入

​ 感觉没有记录的必要。。。 支付记录

简单支付漏洞之优惠券遍历

​ 有问题啊这个靶场,领不了优惠券,但是可以用订单编号那个过,有点失望。。。

越权漏洞

JS不能分析数据又加密,咋整

​ 很简单啊,id越权,然后解JWT,这算非预期?

jwt

get_profile

简单越权漏洞之ID越权

​ 很简单的一个id越权,简单的洞,但是很常见。

订单查询

简单越权漏洞之JS逆向

​ 本靶场需要进行简单的js逆向,挖掘越权查看他人敏感信息漏洞。

​ 来,让我看看是什么js逆向,我觉得都难不倒我。

加密报文

​ 这里一看就是一个对称加密,找到算法和key就好了。

​ 找到了加密文件:

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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
;(function(_0x2f4d, _0x5b7e, _0x8bf6) {
if(false) return;
var _0x5e6c = function(_0x4d21) {
return function(_0x3a18) {
var _0x7c09 = '', _0x1d0f = _0x3a18.split('');
for (var _0x2e50 = 0; _0x2e50 < _0x1d0f.length; _0x2e50++) {
_0x7c09 += _0x1d0f[_0x2e50];
}
return _0x4d21(_0x7c09);
};
};

var _0x3471 = ['b','t','o','a', 'a','t','o','b'];
var _0x6d91 = String.fromCharCode(98,116,111,97);
var _0x8e76 = String.fromCharCode(97,116,111,98);
var _0x9c54 = _0x5e6c(function(_0x6b2d){ return _0x2f4d[_0x6d91](_0x6b2d); });
var _0x3b92 = _0x5e6c(function(_0x1a43){ return _0x2f4d[_0x8e76](_0x1a43); });

var _0x7f38 = function(_0x2c7e) { return typeof _0x2c7e === 'string' ? _0x2c7e : ''; };

var _0x1da4 = function(_0x4f21, _0x7d39) {
var _0x3c5b = 0;
return function(_0x18d2) {
var _0x4e87 = _0x4f21(_0x18d2);
_0x3c5b++;
return _0x7d39 ? _0x7d39(_0x4e87) : _0x4e87;
};
};

var _0x5c82 = (function() {
var _0x6e3d = '';
var _0x9a47 = [67,48,109,112,108,51,120,95,75,51,121,95,70,48,114,95,48,112,51,110,49,100,95,51,110,99,114,121,112,116,49,48,110];
for (var _0x5f21 = 0; _0x5f21 < _0x9a47.length; _0x5f21++) {
_0x6e3d += String.fromCharCode(_0x9a47[_0x5f21]);
}
return _0x6e3d;
})();

var _0x5f34 = function(_0x2d31, _0x4a7e) {
return _0x2d31 ^ (_0x4a7e || 1);
};

var _0x7a92 = function(_0x9e21) {
return _0x9e21.split('').reverse().join('');
};

var _0x4e72 = function(_0x1c94) {
return function(_0x3d76) {
return _0x1c94(_0x3d76);
};
};

var _0x3c8f = function(_0x6e29) {
return _0x6e29.toString(parseInt('100', 4));
};

// 故意添加混淆代码
var _0x7c32 = [];
for(var _0x2e31 = 0; _0x2e31 < 10; _0x2e31++) {
_0x7c32.push(function() { return Math.random(); });
}

var _0x9c21 = function(_0x3e21) {
if(_0x3e21 % 2 === 0) {
return function(_0x7e21) { return _0x7e21 + 1; };
} else {
return function(_0x7e21) { return _0x7e21 - 1; };
}
};

var _0x4a32 = _0x9c21(Math.floor(Math.random() * 10));

_0x2f4d['encryptOpenid'] = (function() {
var _0x3e21 = _0x7c32[3]();
var _0x7e21 = _0x4a32(100);

return function(_0x5d31) {
if (!_0x5d31) return '';

var _0x2e43 = _0x9c54(_0x7f38(_0x5d31));
var _0x7c82 = _0x7a92(_0x2e43);

var _0x9e32 = '';
var _0x1a52 = 0, _0x6c73 = 0;

// 添加无意义的控制流
if(_0x3e21 > 0.5) {
while (_0x1a52 < _0x7c82.length) {
var _0x8e93 = _0x7c82.charCodeAt(_0x1a52);
var _0x4a21 = _0x5c82.charCodeAt(_0x1a52 % _0x5c82.length);
_0x9e32 += String.fromCharCode(_0x5f34(_0x8e93, _0x4a21));
_0x1a52 = ++_0x6c73;
}
} else {
for(_0x1a52 = 0; _0x1a52 < _0x7c82.length; _0x1a52++) {
var _0x8e93 = _0x7c82.charCodeAt(_0x1a52);
var _0x4a21 = _0x5c82.charCodeAt(_0x1a52 % _0x5c82.length);
_0x9e32 += String.fromCharCode(_0x5f34(_0x8e93, _0x4a21));
}
}

var _0x2c83 = _0x9c54(_0x9e32);

// 使用eval增加混淆
var _0x4e92 = eval("Date[String.fromCharCode(110,111,119)]().toString(36)");
var _0x7a21 = eval("Math[String.fromCharCode(114,97,110,100,111,109)]().toString(36).substring(2, 8)");

return _0x9c54(_0x7a21 + _0x2c83 + _0x4e92);
};
})();

_0x2f4d['decryptOpenid'] = (function() {
var _0x3e21 = _0x7c32[7]();
var _0x7e21 = _0x4a32(50);

return function(_0x3e72) {
if (!_0x3e72) return '';

try {
var _0x9a32 = _0x3b92(_0x7f38(_0x3e72));
var _0x1c83 = _0x9a32.substring(6, _0x9a32.length - 8);
var _0x5e92 = _0x3b92(_0x1c83);

var _0x8c32 = '';

// 添加无意义的控制流
if(_0x3e21 < 0.5) {
for (var _0x4a73 = 0; _0x4a73 < _0x5e92.length; _0x4a73++) {
var _0x6e29 = _0x5e92.charCodeAt(_0x4a73);
var _0x2c74 = _0x5c82.charCodeAt(_0x4a73 % _0x5c82.length);
_0x8c32 += String.fromCharCode(_0x5f34(_0x6e29, _0x2c74));
}
} else {
var _0x4a73 = 0;
while(_0x4a73 < _0x5e92.length) {
var _0x6e29 = _0x5e92.charCodeAt(_0x4a73);
var _0x2c74 = _0x5c82.charCodeAt(_0x4a73 % _0x5c82.length);
_0x8c32 += String.fromCharCode(_0x5f34(_0x6e29, _0x2c74));
_0x4a73++;
}
}

var _0x4e73 = _0x7a92(_0x8c32);

return _0x3b92(_0x4e73);
} catch (_0x9c21) {
(function(_0x6e33) {
_0x2f4d.console[String.fromCharCode(101,114,114,111,114)]('n', _0x6e33);
})(_0x9c21);
return '';
}
};
})();

var _0x2e43 = function() {
var _0x7c82 = [];
for(var i = 0; i < 5; i++) {
_0x7c82.push(function(x) { return x * x; });
}
return _0x7c82;
}();

if(![]) {
_0x2f4d['encryptOpenid'] = function() { return ''; };
_0x2f4d['decryptOpenid'] = function() { return ''; };
}

})(this || window, document, function() { return; });

js逆向

​ 传入的openid是_0x5d31,在78行打断点,然后将其修改成1即可进行越权查询。

逻辑漏洞

简单漏洞之组合环境一

靶场目标:买下会员专属商品

由于需要手机号验证码登录,所以做了一个验证码平台在8081端口

提示:此环境组合了两种手法,包括一种基础的手法与前面的靶场:《有个接口隐藏了,找到它》的练习手法,靶场不存在弱口令与验证码爆破,请勿浪费性能

重要:验证码平台不参与漏洞逻辑,仅提供接码功能,预期解不涉及验证码平台本身,也不根据验证码平台能提供的信息去猜测

​ 这题的信息如上,反正我是没找到隐藏接口,太菜了,这种拼接谁能想到啊,将8081的接口拼到80的api上,我服了。

拼接接口

​ 然后用获取到的糖糖账号,登录,更改获取手机验证码的接口参数。

验证码获取

​ 登录后购买,这谁能想到啊,我要给差评。

getflag

简单-任意用户登录二

简单的任意用户登录

滴滴滴滴,你收到了一个电话,来自13900812231,哦原来是四川成都xx单位的客户,他发给你了一个网站,让你测试有没有漏洞?

登录界面

​ 既然题目都给出了任意用户登录了,我猜应该是响应包文直接替换就能绕过,尝试了一下,替换并不能直接进去,也不存在未授权,那么接下来看一下这个忘记密码功能和注册功能,任意用户登录在这里也容易出现。

​ 确实可以通过修改响应包来实现跨步,直接修改账号的密码。

重置密码

​ 然后我尝试修改了题目中给出的手机号,可以修改,但是登录仍然显示封禁中,在我没有思路的时候,我去看了一眼评论区的提示,手机号可以爆破出可以使用的账号

账号枚举

​ 按照刚才的思路,重置一下这个账号,并进行登录

主页

简单逻辑漏洞之任意用户登录

篡改响应包

篡改响应包

​ 篡改响应包,在请求包中加入主页下方的管理员手机号,别问我为什么这样绕,问出题人棉花糖吧。

getflag

SQL注入

真实报告之sql注入

排序注入

​ 一眼排序注入

排序注入

排序注入2

​ 拿flag的话直接拿sqlmap跑吧,这里证明一下存在注入点就可以走了。

XSS

xss+csrf的简单模拟

圣杯页面

​ 依旧圣杯页,注册一个账号

评论页

​ 这个棉花糖怎么这么不正经。。。

​ 这里说是xss+csrf,接管账号的话,直接去找重置密码接口,看看密码重置接口需要的参数是什么。

重置密码

​ 这里重置密码做的很敷衍啊,很简单,直接开始构造xss吧。

csrf

​ 重新创建一个账号,看看是否存在csrf。确实可以更改

评论区

​ 这里也存在糖糖的账号,直接登录试试。

mht个人中心

​ 也是成功登录上糖糖的账号。

信息泄露

有个接口隐藏了,找到它

​ 直接扫目录,扫到了swagger,拼接接口(这个我是真的想不到得拼接admin,看了评论区才知道要这样整,还是太菜了)

拼接接口

总结

​ 这个SRC靶场就这么打完了,这个靶场还是比较简单的,但有些点确实想不到,多练吧,没别的说的,菜就多练。

Android逆向-NDK开发初试

何为NDK

​ 安卓开发分SDK和NDK,SDK运行在ART/Dalvik虚拟机中,NDK运行在原生系统中,虚拟机执行效率不如原生效率高,要谈两者的区别,还是直接放一个SDK调用NDK的图,通过实际的调用流程来了解一下吧。

SDK调用NDK Zygote 是安卓系统启动时就会创建这么一个进程,它是所有安卓应用的父进程。Zygote在启动时,会加载系统核心框架库(如framework.jar,包含Activity、Service等核心类的实现),每启动一个应用,Zygote就会通过Fork自身来生成新进程,前面那些预加载的资源就会被所有子进程共享,避免重复加载。

​ 所以Zygote是创建ART/Dalvik虚拟机的进程,它会完成虚拟机的初始化,确保fork出来的应用进程都继承一个准备就绪的、标准化的虚拟机环境。应用启动后,就会在这个虚拟机中加载APK中的Dex码,进行初始化Application,然后就是四大组件的初始化。

​ 那图中的JNI又是个啥呢,在安卓中,JNI 是指 Java 原生接口,是一个规范。它定义了 Android 从受管理代码(使用 Java 或 Kotlin 编程语言编写)编译的字节码与原生代码(使用 C/C++ 编写)互动的方式。SDK调用NDK,是通过JNI与NDK进行调用映射和数据类型转换等操作,完成JAVA层到NATIVE层的通信。

​ 话说回来,NDK就是为了高运行效率、跨平台复用、底层逻辑操作,按照JNI规范在原生空间中运行编译C\C++代码的Android原生开发工具集。

NDK开发初试

​ 扔一点命令

shell
1
2
3
4
5
6
7
8
//查看当前界面Activity
adb shell dumpsys window | grep mCurrentFocus
//查看应用完整包名
adb shell pm list package
//列出所有应用包,并对应完整路径
adb shell pm list package -f
//显示系统详细信息
adb shell uname -a

​ 通过Android Studio创建一个Native开发项目,在java层中写进行声明

1
2
3
4
5
public static native String mdString(String string);
//如果使用static进行声明,那么native层中的this对象要使用jclass类型,反之使用jobject类型
//并且,访问其对象也应使用对应的方法
//非 static 方法(jobject):可通过env->GetObjectField/CallObjectMethod访问实例成员;
//static 方法(jclass):需通过env->GetStaticObjectField/CallStaticObjectMethod访问静态成员。

​ 对应的,native层的定义(C版)

1
2
3
4
5
6
7
8
JNIEXPORT jstring JNICALL
Java_com_example_myndk_MainActivity_stringFromJNI(
JNIEnv* env, // JNI环境指针,提供JNI操作函数
jobject thiz) { // 对应Java中的this对象
const char* hello = "Hello from C Native Code!";
// 把C字符串转为Java的String对象
return (*env)->NewStringUTF(env, hello);
}

​ 其中,如果是已CPP进行native层的编写,就需要在最开始进行一个name mangling,因为CPP可能会进行重载,抽空去研究一下编译区别(咕咕咕)。

1
2
3
4
5
6
7
8
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_myndk_MainActivity_stringFromJNI(
JNIEnv* env,
jobject thiz) {
std::string hello = "Hello from C++ Native Code!";
// C++中env直接调用方法,无需(*env)->
return env->NewStringUTF(hello.c_str());
}

1. JNI 基本类型对照表 (Java Primitive Types)

Java 基本类型 JNI 对应类型 C/C++ 等效类型 占用字节 说明
boolean jboolean unsigned char 1 0=false,非0=true
byte jbyte signed char 1 8位有符号整数
char jchar unsigned short 2 UTF-16编码的字符
short jshort short 2 16位有符号整数
int jint int 4 32位有符号整数
long jlong long long / int64_t 8 64位有符号整数
float jfloat float 4 32位浮点数
double jdouble double 8 64位浮点数

2. JNI 引用类型对照表 (Java Reference Types)

Java 引用类型 JNI 对应类型 说明/使用场景
Object jobject 所有引用类型的父类
Class jclass 对应 Java 的 Class 对象 (用于静态方法/反射)
String jstring 对应 Java 的 String
Throwable jthrowable 对应 Java 的异常类
Object[] jobjectArray 对象数组 (如 String[], User[])
boolean[] jbooleanArray 布尔数组
byte[] jbyteArray 字节数组 (常用作二进制数据传输)
char[] jcharArray 字符数组
short[] jshortArray 短整型数组
int[] jintArray 整型数组
long[] jlongArray 长整型数组
float[] jfloatArray 浮点数组
double[] jdoubleArray 双精度浮点数组
自定义类/接口 jobject 统统映射为 jobject

3. JNI 环境与 ID 类型 (JNI Special Types)

用途 JNI 类型 说明
JNI 环境指针 JNIEnv* 核心接口指针 (C使用 (*env)->, C++使用 env->)
虚拟机指针 JavaVM* 跨线程获取 JNIEnv 时使用
字段 ID jfieldID 访问 Java 属性的凭据
方法 ID jmethodID 调用 Java 方法的凭据

​ 直接抄仨对照表扔这,用到的时候再看。

​ 写完对应的代码,再扔一个命令行编译的命令

1
2
3
./gradlew assembleDebug

#编译好后,通过 find . -name "*.apk" 找到对应的debug文件

JNI_HOOK

这里使用frida进行native层的被动调用,需要找到要hook的libso名称,还有要hook的函数名称,以及入参和出参类型,使用以下命令进行查找。

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
Using USB device `KB2000`
Agent injected and responds ok!

_ _ _ _
___| |_|_|___ ___| |_|_|___ ___
| . | . | | -_| _| _| | . | |
|___|___| |___|___|_| |_|___|_|_|
|___|(object)inject(ion) v1.11.0

Runtime Mobile Exploration
by: @leonjza from @sensepost

[tab] for command suggestions
com.example.ezmd5 on (OnePlus: 11) [usb] # memory list modules
libezmd5.so 0x718b26b000 45056 (44.0 KiB) /data/app/~~fmgadrx_ut4G29FifjXEmw==/com.example.ezmd5-vLBLnfw0npqVhTLBZNLi...
//查找加载的libso名称

com.example.ezmd5 on (OnePlus: 11) [usb] # memory list exports libezmd5.so
Save the output by adding `--json exports.json` to this command
Type Name Address
-------- -------------------------------------------- ------------
function Java_com_example_ezmd5_MainActivity_mdString 0x71da5e9a10
function _ZN7_JNIEnv17GetStringUTFCharsEP8_jstringPh 0x71da5e9b30
function _Z7MD5InitP7MD5_CTX 0x71da5e9b6c
function _Z9MD5UpdateP7MD5_CTXPhj 0x71da5e9bcc
function _Z8MD5FinalP7MD5_CTX 0x71da5e9d8c
function _ZN7_JNIEnv12NewStringUTFEPKc 0x71da5ea0bc

//查看libso文件的导出表,找到导出的函数名

//使用c++filt工具,查看函数传参类型
➜ frida-scripts # c++filt -n _ZN7_JNIEnv17GetStringUTFCharsEP8_jstringPh
_JNIEnv::GetStringUTFChars(_jstring*, unsigned char*)

找到我们想hook的JNI,通过ida或者c++filt工具查看传参

ida

​ 由于该jni是在启动时后就进行直接调用的,这里改用了lasting_yang师傅写的 frida_dump去hookdlopen,非常给力的fridahook项目,抽空研究研究(咕咕咕~)

https://github.com/lasting-yang/frida_dump

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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
var can_hook_libart = false;

function hook_dlopen() {
Interceptor.attach(Module.findExportByName(null, "dlopen"), {
onEnter: function(args) {
var pathptr = args[0];
this.targeet = false;
if (pathptr !== undefined && pathptr != null) {
var path = ptr(pathptr).readCString();
//console.log("dlopen:", path);
if (path.indexOf("libezmd5.so") >= 0) {
this.target = true;
console.log("[dlopen:]", path);
}
}
},
onLeave: function(retval) {
if(this.target){
console.log("find Exports => libezmd5.so", JSON.stringify(Module.enumerateSymbols()));
}

// if (this.can_hook_libart && !is_hook_libart) {
// dump_dex();
// is_hook_libart = true;
// }
}
})

Interceptor.attach(Module.findExportByName(null, "android_dlopen_ext"), {
onEnter: function(args) {
var pathptr = args[0];
if (pathptr !== undefined && pathptr != null) {
var path = ptr(pathptr).readCString();
console.log("android_dlopen_ext:", path);
if(path.indexOf("libezmd5.so") >= 0){
this.target = true;
}



// if (path.indexOf("libart.so") >= 0) {
// this.can_hook_libart = true;
// console.log("[android_dlopen_ext:]", path);
// }
}
},
onLeave: function(retval) {

if(this.target){
// console.log("find Exports => libezmd5.so", JSON.stringify(Module.enumerateSymbols()));

console.log("find Exports => ", JSON.stringify(Module.enumerateExports("libezmd5.so")));
const matched_export = Module.enumerateExports("libezmd5.so").filter(function(exported) {
return exported.name.indexOf("Java_com_example_ezmd5_MainActivity_mdString") >= 0;
});
const matched_addr = matched_export[0]["address"];
console.log("find Export addr => ", matched_addr);
var md5String = new NativeFunction(matched_addr, 'pointer', ['pointer', 'pointer', 'pointer']);
Interceptor.replace(matched_addr, new NativeCallback((env, jclass, jstring)=>{
console.log("111>")
console.log("jstring =>", Java.vm.getEnv().getStringUtfChars(jstring, null).readCString())
var string1 = "ghbbhc";
var jstring1 = Java.vm.getEnv().newStringUtf(string1);
var retval = md5String(env, jclass, jstring1);
console.log("new md5 => ", Java.vm.getEnv().getStringUtfChars(retval,null).readCString());
return retval;

}, 'pointer', ['pointer', 'pointer', 'pointer']))

}
// if (this.can_hook_libart && !is_hook_libart) {
// dump_dex();
// is_hook_libart = true;
// }
}
});
}


setImmediate(hook_dlopen);

fridahook

JNI主动调用

1
2
3
4
5
6
7
8
9
10
11
setTimeout(()=>{
Java.perform(()=>{
var native = Module.findExportByName("libezmd5.so", "Java_com_example_ezmd5_MainActivity_mdString");
console.log("find native address =>", native);
var md5String = new NativeFunction(native, 'pointer', ['pointer', 'pointer', 'pointer']);
console.log("============= 主动调用 =============");
console.log("String =>", "123456");
var result = md5String(Java.vm.tryGetEnv(), Java.vm.tryGetEnv(), Java.vm.getEnv().newStringUtf("123456"));
console.log("MD5 =>", Java.vm.getEnv().getStringUtfChars(result, null).readCString());
})
}, 1000)

ab26fd33d8d12a11db2544bce6e5a076

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

🎨