unidgb学习笔记

第一课绿洲

搜索导出函数

image.png
说明不是静态绑定
去 jni_Onload 看看
image.png
被混淆了。

上 unidgb

用 findhash 找下算法

  • 把 findhash.xml 和 findhash.py 扔到 ida plugins 目录下
  • ida -edit-plugin-findhash
  • image.png

image.png

第二课微博

java 层三个参数

image.png

so 层 5 个参数

Java_com_sina_weibo_security_WeiboSecurityUtils_calculateS
image.png

WeiboSecurityUtils_calculateS(int a1, int a2, int a3, int a4, int a5)
// 第一个参数是 env
// 第二个参数,实例方法是 jobject,静态方法是 jclazz,直接填 0,一般用不到。

识别 JNIEnv

**在第一个 int 参数上按 y,输入 JNIEnv* **, jni 的 api 就被识别出来了
按 N 在其他参数上重命名

查看静态绑定的地址+1:

image.png
Thumb 指令需要+1,否则执行会报错。
image.png
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

运行报错

image.png
运行下。0x2c8d 按 G 定位查看
image.png
按 x 查看交叉应用定位到 sub_1C60

unidgb 打 patch 绕过签名校验

image.png
这个判断疑似在校验签名
根据 ARM 调用约定,入参前四个分别通过 R0-R3 调用,返回值通过 R0 返回
mov r0,1 https://armconverter.com/一下 4FF00100
ida textview 查看偏移地址:image.png
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 跟踪算法的具体方法
image.png
右键image.png
查看image.png 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/
image.png

最终结果:
339be88b

总结:

java 层的方法到了 so 层后,前面会多两个参数,一个是 JNIEnv ,一个是无关紧要的参数

识别 JNIEnv

在第一个int 参数上按y,输入JNIEnv* , jni的api就被识别出来了

重命名参数

在参数上按n

Thumb 指令需要+1

unidgb 如何打 patch

unidgb 如何使用 hookzz hook 打印参数和返回值

第三课最右 V2-Sign

image.png
可以看到用了动态注册
静态注册不走 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

这个问题后续再分析。。继续运行

image.png

这个是由于一些常见的、系统的 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 方法:
image.png

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
image.png
继续补环境,依葫芦画瓢,把缺少的环境都给补了。

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>

image.png
搞定~

算法还原

因为返回值总是 32 位长度,且明文不变时输出也不变,很容易让人想到哈希算法,尤其是 MD5 算法。但是,样本经过了一定程度的 OLLVM 混淆,很难自上而下或者自下而上逐个模块分析代码逻辑,所以我们需要借助一下工具,当当当, FIndHash 试一下。
按 G 跳转到 65540

unidgb hook 65540 打印入参
不确定三个参数是指针还是数值,所以先全部做为数值处理,作为 long 类型看待,防止整数溢出。
image.png
参数 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");

接下来分析算法:
image.png
按 H 键将这 4 个数转为 16 进制
说它疑似 MD5 主要有两个依据

  • 输出结果是 32 位,MD5 恰好也是 32 位长度。
  • 有四个 IV,MD5 就有四个 IV

但是他不是标准的 md5
image.png
标准的 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 跳转到地址查看
image.png

但我们在用 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
image.png
发现 5 个常量,按 H 转为 16 进制

第五课

nonce=68D04064-17A1-4705-8F47-534740723D9A&timestamp=1646985629263&devicetoken=F1517503-9779-32B7-9C78-F5EF501102BC&sign=C08F72C468C86B764EFF3B782B766DA8

nonce=68D04064-17A1-4705-8F47-534740723D9A&timestamp=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 行。。
image.png
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
image.png

如果输入大于 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
image.png
0x30443836 我们转换下 36384430, cyberchef 中看一下
image.png

nonce=68D04064-17A1-4705-8F47-534740723D9A&timestamp=1647001027020&devicetoken=F1517503-9779-32B7-9C78-F5EF501102BC&sign=E9C515DC7FAF190C20AB4E7E3C9869E1

第一个明文块:0x44303945 -> 45393044 -> 68D0

第二个明文块:0x446884aa-> aa846844-> 4064
image.png
nonce=1F90A568-DBEE-4BD6-93A4-F4ABC2E324F0&timestamp=1647000109156&devicetoken=F1517503-9779-32B7-9C78-F5EF501102BC&sign=A40A56811AE194D2FCAE9A04998AC77F

0x30394631
31463930


unidgb学习笔记
http://blog.uzilol.cn/2022/02/10/yuque/unidgb%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/
作者
ive_e (leoli)
发布于
2022年2月10日
许可协议