第一课绿洲
搜索导出函数

说明不是静态绑定
去 jni_Onload 看看

被混淆了。
上 unidgb
用 findhash 找下算法
- 把 findhash.xml 和 findhash.py 扔到 ida plugins 目录下
- ida -edit-plugin-findhash


第二课微博
java 层三个参数

so 层 5 个参数
Java_com_sina_weibo_security_WeiboSecurityUtils_calculateS

WeiboSecurityUtils_calculateS(int a1, int a2, int a3, int a4, int a5)
// 第一个参数是 env
// 第二个参数,实例方法是 jobject,静态方法是 jclazz,直接填 0,一般用不到。
识别 JNIEnv
**在第一个 int 参数上按 y,输入 JNIEnv* **, jni 的 api 就被识别出来了
按 N 在其他参数上重命名
查看静态绑定的地址+1:

Thumb 指令需要+1,否则执行会报错。

0x1E7C + 1
context 如何构造
DvmObject<?> context = vm.resolveClass(“android/content/Context”).newObject(null);// context
字符串类型如何构造
list.add(vm.addLocalObject(context));
list.add(vm.addLocalObject(new StringObject(vm, “12345”)));
list.add(vm.addLocalObject(new StringObject(vm, “r0ysue”)));
除了基本类型,比如 int,long 等,其他的对象类型一律要手动 addLocalObject
运行报错

运行下。0x2c8d 按 G 定位查看

按 x 查看交叉应用定位到 sub_1C60
unidgb 打 patch 绕过签名校验

这个判断疑似在校验签名
根据 ARM 调用约定,入参前四个分别通过 R0-R3 调用,返回值通过 R0 返回
mov r0,1 https://armconverter.com/一下 4FF00100
ida textview 查看偏移地址:
module.base + 0x1E86
1 2
| int patchCode = 0x4FF00100; // emulator.getMemory().pointer(module.base + 0x1E86).setInt(0,patchCode);
|
需要注意的是,这儿地址可别+1 了,Thumb 的+1 只在运行和 Hook 时需要考虑,打 Patch 可别想。
借助 unidgb 分析算法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| //初始化2个字符串。key和bytes inputBytesKey = (*a1)->GetStringUTFChars(a1, inputKey, 0); inputBytesStr = (char *)(*a1)->GetStringUTFChars(a1, inputBytes, 0); v7 = j_strlen(inputBytesStr); v8 = v7 + j_strlen(inputBytesKey) + 1; inputBytesStr2 = j_malloc(v8); j_memset(inputBytesStr2, 0, v8); j_strcpy((char *)inputBytesStr2, inputBytesStr); j_strcat((char *)inputBytesStr2, inputBytesKey); //C 库函数 - strcat() 拼接key和bytestr result = (_BYTE *)MDStringOld(inputBytesStr2); //传入MD方法做运算 v11 = (char *)j_malloc(9u); //创建字符串v11 *v11 = result[1]; v11[1] = result[5]; v11[2] = result[2]; v11[3] = result[10]; v11[4] = result[17]; v11[5] = result[9]; v11[6] = result[25]; v12 = result[27]; v11[8] = 0; v11[7] = v12; //将结果的1,5,2,10,17,9,25,27位取出来 //下面创建NewByteArray把V11传进去 v21 = (*a1)->FindClass(a1, "java/lang/String"); v22 = (*a1)->GetMethodID(a1, v21, "<init >", "([BLjava/lang/String;)V"); v13 = j_strlen(v11); v19 = (*a1)->NewByteArray(a1, v13); v14 = j_strlen(v11); (*a1)->SetByteArrayRegion(a1, v19, 0, v14, v11); //将bytearray转string,utf-8编码 v15 = (*a1)->NewStringUTF(a1, "utf-8"); v16 = (*a1)->NewObject(a1, v21, v22, v19, v15); j_free(v11); j_free(inputBytesStr2); (*a1)->ReleaseStringUTFChars(a1, (jstring)inputBytes, inputBytesStr); return (*a1)->PopLocalFrame(a1, v16);</init >
|
F5 跟踪算法的具体方法

右键
查看
text view
得到偏移地址 0x1BD0+1
也可以按 TAB 键直接跳转
frida hook 代码:
1 2 3 4
| var baseAddr = Module.findBaseAddress("libutility.so") var MDStringOld = baseAddr.add(0x1BD0).add(0x1) Interceptor.attach(MDStringOld, { onEnter: function (args) { console.log("input:\n", hexdump(this.arg0)) }, onLeave: function (retval) { console.log("result:\n", hexdump(retval)) } })
|
使用 unidgb hook
1 2 3 4 5 6 7 8 9 10 11 12
| IHookZz hookZz = HookZz.getInstance(emulator); hookZz.wrap(module.base + 0x1BD0 + 1, new WrapCallback<HookZzArm32RegisterContext >() { // inline wrap导出函数 @Override // 类似于 frida onEnter public void preCall(Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) { // 类似于Frida args[0] Pointer input = ctx.getPointerArg(0); System.out.println("input:" + input.getString(0)); }; @Override // 类似于 frida onLeave public void postCall(Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) { Pointer result = ctx.getPointerArg(0); System.out.println("result:" + result.getString(0)); } });</HookZzArm32RegisterContext >
|
打印参数和返回值
input:12345r0ysue
result:439a333788b0cecfce1389d4b83ba1cb
结果是 32 位,猜测 md5,测试下。https://gchq.github.io/CyberChef/

最终结果:
339be88b
总结:
java 层的方法到了 so 层后,前面会多两个参数,一个是 JNIEnv ,一个是无关紧要的参数
识别 JNIEnv
在第一个int 参数上按y,输入JNIEnv* , jni的api就被识别出来了
重命名参数
在参数上按n
Thumb 指令需要+1
unidgb 如何打 patch
unidgb 如何使用 hookzz hook 打印参数和返回值
完
第三课最右 V2-Sign

可以看到用了动态注册
静态注册不走 RegisterNative 的
把要调用的两个函数的地址记录下来
0x4a069
RegisterNative(com/izuiyou/network/NetCrypto, native_init()V, RX@0x4004a069[libnet_crypto.so]0x4a069)
0x4a28d
RegisterNative(com/izuiyou/network/NetCrypto, sign(Ljava/lang/String;[B)Ljava/lang/String;, RX@0x4004a28d[libnet_crypto.so]0x4a28d)
如果加载 so 第二个参数设置为 false,则会出现乱码
说明 SO 做了字符串混淆,这个解密一般发生在 Init array 节或者 JNI OnLoad 中
Shift+F7 查看 segments
这个问题后续再分析。。继续运行

这个是由于一些常见的、系统的 Java 类和方法,Unidbg 作开发者已经做了处理,但不常使用的类库以及自定义 Java 类显然不在此列,我们需要补环境。
- 用户类里面补,因为我们的 zuiyou 类继承了 AbstractJNI
- AbstractJNI 里面补
我们选择在用户类里面补,缺什么补什么。这里却的是 com/izuiyou/common/base/BaseApplication->getAppContext()Landroid/content/Context;
我们 new 一个 android/content/Context
1
| return vm.resolveClass("android/content/Context").newObject(null);
|
接下来处理 sign 方法:

1 2 3 4 5 6 7 8 9 10
| private String callSign(){ // 准备入参 List<object> list = new ArrayList<>(10); list.add(vm.getJNIEnv()); // 第一个参数是env list.add(0); // 第二个参数,实例方法是jobject,静态方法是jclass,直接填0,一般用不到。 list.add(vm.addLocalObject(new StringObject(vm, "12345"))); ByteArray plainText = new ByteArray(vm, "r0ysue".getBytes(StandardCharsets.UTF_8)); list.add(vm.addLocalObject(plainText)); Number number = module.callFunction(emulator, 0x4a28D, list.toArray()); return vm.getObject(number.intValue()).getValue().toString(); }; </object>
|
前 2 个参数不用管, 最后一个 bytearray 构造方式就是 new 一个 byteArray

继续补环境,依葫芦画瓢,把缺少的环境都给补了。
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
| @Override public DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) { switch (signature) { case "android/content/Context->getClass()Ljava/lang/Class;": return dvmObject.getObjectType(); case "java/lang/Class->getSimpleName()Ljava/lang/String;": return new StringObject(vm, "AppController"); case "java/lang/String->getAbsolutePath()Ljava/lang/String;": case "android/content/Context->getFilesDir()Ljava/io/File;": return new StringObject(vm, "/data/user/0/cn.xiaochuankeji.tieba/files"); } return super.callObjectMethodV(vm, dvmObject, signature, vaList); } @Override public boolean callStaticBooleanMethodV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) { switch (signature) { case "android/os/Debug->isDebuggerConnected()Z": return false; } return super.callStaticBooleanMethodV(vm, dvmClass, signature, vaList); } @Override public int callStaticIntMethodV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) { switch (signature) { case "android/os/Process->myPid()I": return 0; } return super.callStaticIntMethodV(vm, dvmClass, signature, vaList); } private String callSign() { // 准备入参 List<object> list = new ArrayList<>(10); list.add(vm.getJNIEnv()); // 第一个参数是env list.add(0); // 第二个参数,实例方法是jobject,静态方法是jclass,直接填0,一般用不到。 list.add(vm.addLocalObject(new StringObject(vm, "12345"))); ByteArray plainText = new ByteArray(vm, "r0ysue".getBytes(StandardCharsets.UTF_8)); list.add(vm.addLocalObject(plainText)); Number number = module.callFunction(emulator, 0x4a28D, list.toArray()); return vm.getObject(number.intValue()).getValue().toString(); } </object>
|

搞定~
算法还原
因为返回值总是 32 位长度,且明文不变时输出也不变,很容易让人想到哈希算法,尤其是 MD5 算法。但是,样本经过了一定程度的 OLLVM 混淆,很难自上而下或者自下而上逐个模块分析代码逻辑,所以我们需要借助一下工具,当当当, FIndHash 试一下。
按 G 跳转到 65540
unidgb hook 65540 打印入参
不确定三个参数是指针还是数值,所以先全部做为数值处理,作为 long 类型看待,防止整数溢出。

参数 1 和 3 像是地址,参数 2 像是长度
打印内存:
1 2 3 4 5
| Inspector.inspect(ctx.getR0Pointer().getByteArray(0, 0x10), "Arg1"); System.out.println(ctx.getR1Long()); Inspector.inspect(ctx.getR2Pointer().getByteArray(0, 0x10), "Arg3"); }; @Override // 类似于 frida onLeave public void postCall(Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) { } }); }
|
1 2 3 4 5 6 7 8 9 10
| >-----------------------------------------------------------------------------< [20:45:00 882]Arg1, md5=433fc65838eefef90efcd0dbebe279c0, hex=723079737565004000f03f4018f7ffbf size: 16 0000: 72 30 79 73 75 65 00 40 00 F0 3F 40 18 F7 FF BF r0ysue.@..?@.... ^-----------------------------------------------------------------------------^ 6 >-----------------------------------------------------------------------------< [20:45:00 886]Arg3, md5=8ea9479bc177e0a5b093801cefd5f32e, hex=ecf6ffbf000000005bbf252988f6ffbf size: 16 0000: EC F6 FF BF 00 00 00 00 5B BF 25 29 88 F6 FF BF ........[.%)....
|
参数 1.就是字符串
参数 2 是长度刚好是 6
参数 3 一般都是存放结果的 buffer,c 开发者习惯,记住就好
打印结果:
preCall 存入,postCall 取出
1
| // push ctx.push(ctx.getR2Pointer());
|
1 2
| // pop 取出 Pointer output = ctx.pop(); Inspector.inspect(output.getByteArray(0, 0x10), "Arg3 after function");
|
接下来分析算法:

按 H 键将这 4 个数转为 16 进制
说它疑似 MD5 主要有两个依据
- 输出结果是 32 位,MD5 恰好也是 32 位长度。
- 有四个 IV,MD5 就有四个 IV
但是他不是标准的 md5

标准的 4 个常量值与他不同
哈希算法的魔改,最简单的修改点就是修改 IV,此处似乎采用了这种。我们测试下结果发现果然如此~结束
第四课
搭建基本架子跑起来~
1 2 3
| RegisterNative(com/mfw/tnative/AuthorizeHelper, xPreAuthencode(Landroid/content/Context;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;, RX@0x4002e301[libmfw.so]0x2e301)
|
得到地址:0x2e301
算法还原
测试发现,不论输入明文多长,都输出固定长度结果,所以疑似哈希算法,又因为输出恒为 40 位,所以又疑似哈希算法中的 SHA1 算法。
按 G 跳转到地址查看

但我们在用 Unidbg 模拟执行时,并没有感受到 native 调用 JAVA 签名校验的烦恼,这是因为我们传入了 APK,Unidbg 替我们处理了这部分签名校验,但 Unidbg 并不能处理所有情况下的签名校验,所以在之前的一些例子里,我们会 patch 掉签名校验函数。
开始 hook
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| public void hook_312E0(){ // 获取HookZz对象 IHookZz hookZz = HookZz.getInstance(emulator); // 加载HookZz,支持inline hook,文档看https://github.com/jmpews/HookZz // enable hook hookZz.enable_arm_arm64_b_branch(); // 测试enable_arm_arm64_b_branch,可有可无 // hook MDStringOld hookZz.wrap(module.base + 0x312E0 + 1, new WrapCallback<HookZzArm32RegisterContext >() { // inline wrap导出函数 @Override // 方法执行前 public void preCall(Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) { Pointer input = ctx.getPointerArg(0); byte[] inputhex = input.getByteArray(0, ctx.getR2Int()); Inspector.inspect(inputhex, "input"); Pointer out = ctx.getPointerArg(1); ctx.push(out); }; @Override // 方法执行后 public void postCall(Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) { Pointer output = ctx.pop(); byte[] outputhex = output.getByteArray(0, 20); Inspector.inspect(outputhex, "output"); } }); hookZz.disable_arm_arm64_b_branch(); };</HookZzArm32RegisterContext >
|
返回值就是我们的结果,那就是自定义函数 1 加密函数了。
进入 sub_312E0

发现 5 个常量,按 H 转为 16 进制
第五课
nonce=68D04064-17A1-4705-8F47-534740723D9A×tamp=1646985629263&devicetoken=F1517503-9779-32B7-9C78-F5EF501102BC&sign=C08F72C468C86B764EFF3B782B766DA8
nonce=68D04064-17A1-4705-8F47-534740723D9A×tamp=1647001027020&devicetoken=F1517503-9779-32B7-9C78-F5EF501102BC&sign=E9C515DC7FAF190C20AB4E7E3C9869E1
无法 f5,使用 findHash 试试无结果
打开 unidgb 的 traceCode 功能
emulator.traceCode(module.base, module.base + module.size);
参数是起始地址和终止地址
trace 的汇编执行流到文件中,方便查看
1
| emulator.traceCode(module.base, module.base + module.size);
|
结果有 11w 行。。

MD5 在前面的篇幅中已经讲了很多了,它有两组标志性的数可以用于确认自身身份。
1.是 0x67452301 0xefcdab89 等四个魔术,但单靠这四个数证明不了是 MD5,也可能是别的哈希算法,除此之外,算法可能魔改常数。
2.MD5 的 64 个 K,K1-K64 是 MD5 独特的标志,简单的魔改也不会改 K 值。(其实 K 表也可以随便改,但一般的开发人员也不懂 K 的意义,不敢乱改。)
1 2 3 4 5 6 7 8 9 10 11 12
| # 魔数 A = 0x67452301 B = 0xefcdab89 C = 0x98badcfe D = 0x10325476 # K表 Ktable = [0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee, 0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501, 0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be, 0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821, 0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa, 0xd62f105d, 0x2441453, 0xd8a1e681, 0xe7d3fbc8, 0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed, 0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a, 0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c, 0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70, 0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x4881d05, 0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665, 0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039, 0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1, 0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1, 0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391]
|
我们在 trace 里面搜索魔数和 k 表。说明他是一个魔改的 md5 算法。
接下来我们做两件事
- 从汇编 trace 中析出 MD5 的结果——用于确认输出是否与 MD5 有直接关系
- 从汇编 trace 中析出 MD5 的输入——用于确认函数的输入和 MD5 的输入的关系
首先做第一件事
找 0x67452301(魔数 A)最后和谁相加
0x844525cc
计算两者相加的结果(如果大于 0xffffffff 则取低的 32 比特) 即 EB8A48CD
EB8A48CD

如果输入大于 0xffffffff 比特,那么调整一下端序,CD488AEB,这就是 MD5 前 8 个数字的结果
搜索一下 EB8A48CD,发现后面还有它参与的运算,这说明明文长度超过一个分组长,需要进行第二个分组的运算
同样找 EB8A48CD 最后和谁相加
0xEB8A48CD+ 0xd8e846f3 = 1 C472 8FC0
取 C4728FC0 倒转端序 C08F72C4
我们发现这就是加密结果的前 8 个数,以此类推,第二第三第四部分,同理
C08F72C468C86B764EFF3B782B766DA8
我们通过这种方式确认了,MD5 的结果就是加密的结果。
那么做另一件事——Trace 汇编中析出 MD5 的明文
1 2 3 4
| 在MD5具体流程中,每轮运算都需要64步,每步的第三个操作是选取明文的一截进行加法运算,第四个操作是和K相加。我们无法定位第三个操作,但因为第四个操作的K都是已知的,所以可以这样描述“第四个操作上方第一个add运算就是明文的一截+中间结果” 但是呢。。这前四步其实并没有硬性的顺序要求,生成的汇编代码常常不遵照顺序。。 但好在第一个F(B,C,D)的结果是固定的0xffffffff,它是一个很好的“锚点“ 基于K值和这个锚点,我们可以在汇编trace中准确的析出明文——仅依靠trace汇编,不管OLLVM或者花指令将指令流变成10w行还是100w行,还是SO做了保护,明文不会完整出现在内存中,都不影响这个分析过程。
|
我们搜索 K 表第一个值:0xd76aa478

0x30443836 我们转换下 36384430, cyberchef 中看一下

nonce=68D04064-17A1-4705-8F47-534740723D9A×tamp=1647001027020&devicetoken=F1517503-9779-32B7-9C78-F5EF501102BC&sign=E9C515DC7FAF190C20AB4E7E3C9869E1
第一个明文块:0x44303945 -> 45393044 -> 68D0
第二个明文块:0x446884aa-> aa846844-> 4064

nonce=1F90A568-DBEE-4BD6-93A4-F4ABC2E324F0×tamp=1647000109156&devicetoken=F1517503-9779-32B7-9C78-F5EF501102BC&sign=A40A56811AE194D2FCAE9A04998AC77F
0x30394631
31463930