unidgb 经验

参数

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

Hash 算法

一个哈希算法,可以简单划分成填充和加密两部分

多次测试发现不论输入明文多长,都输出固定长度结果,所以疑似哈希算法。
输出恒为 40 位,疑似哈希算法中的 SHA1 算法。
输出恒为 32 位,疑似哈希算法中的 MD5 算法。

SHA1 算法的运算部分是由什么组成?SHA1 和 MD5 采用了相同的结构,每 512 比特分组需要一轮运算,我们的输入长度不超过一个分组的长度,所以只用考虑一轮运算。一轮运算是 80 步,每隔 20 步是一种模式。

MD5 特征

  • 输出结果是 32 位。
  • 有四个 IV,MD5 就有四个 IV
  • 1.是 0x67452301 0xefcdab89 等四个魔术,但单靠这四个数证明不了是 MD5,也可能是别的哈希算法,除此之外,算法可能魔改常数。
  • MD5 的 64 个 K,K1-K64 是 MD5 独特的标志,简单的魔改也不会改 K 值。(其实 K 表也可以随便改,但一般的开发人员也不懂 K 的意义,不敢乱改。)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 魔数
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]

python MD5 源码

1
https://blog.csdn.net/qq_38851536/article/details/117533396

SHA1 算法

Thumb 指令

ARM32 有 Thumb 和 ARM 两种指令模式,ida 查看到的地址是 thumb 模式
ida 里面查看的指令地址需要+1
unidgb 输出的地址不需要+1

何判断是 Thumb 还是 Arm 模式:
ARM 模式指令总是 4 字节长度,Thumb 指令长度多数为 2 字节,少部分指令是 4 字节。

Hook

Unidbg 内嵌了多种 Hook 工具,目前主要是四种

  • Dobby
  • HookZz
  • xHook
  • Whale

xHook 是爱奇艺开源的基于 PLT HOOK 的 Hook 框架,它无法 Hook 不在符号表里的函数,也不支持 inline hook,这在我们的逆向分析中是无法忍受的,所以在这里不去理会它。

Whale 在 Unidbg 的测试用例中只有对符号表函数的 Hook,没看到 Inline Hook 或者 非导出函数的 Hook,所以也不去考虑。

HookZz 是 Dobby 的前身,两者都可以 Hook 非导出表中的函数,即 IDA 中显示为 sub_xxx 的函数,也都可以进行 inline hook,所以二选一就行了。我喜欢 HookZz 这个名字,所以就 HookZz 了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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));
}
});

通常会有个参数是 buffer,在执行前 preCall 将其 push 保存,执行完毕 postCal 再 pop 取出

1
// push ctx.push(ctx.getR2Pointer());
1
2
// pop 取出 Pointer output = ctx.pop(); Inspector.inspect(output.getByteArray(0,
0x10), "Arg3 after function");

打 PATCH 一

地址:0x1E86
汇编:MOV R0,1
查看“mov r0,1”的机器码,使用ARMConvert 转换成 4FF00100

1
2
3
4
public void patchVerify(){
int patchCode = 0x4FF00100; //
emulator.getMemory().pointer(module.base + 0x1E86).setInt(0,patchCode);
}

打 PATCH 二

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void patchVerify1(){
Pointer pointer = UnidbgPointer.pointer(emulator, module.base + 0x1E86);
assert pointer != null;
byte[] code = pointer.getByteArray(0, 4);
if (!Arrays.equals(code, new byte[]{ (byte)0xFF, (byte) 0xF7, (byte) 0xEB, (byte) 0xFE })) { // BL sub_1C60
throw new IllegalStateException(Inspector.inspectString(code, "patch32 code=" + Arrays.toString(code)));
}
try (Keystone keystone = new Keystone(KeystoneArchitecture.Arm, KeystoneMode.ArmThumb)) {
KeystoneEncoded encoded = keystone.assemble("mov r0,1");
byte[] patch = encoded.getMachineCode();
if (patch.length != code.length) {
throw new IllegalStateException(Inspector.inspectString(patch, "patch32 length=" + patch.length));
}
pointer.write(0, patch, 0, patch.length);
}
};

打印地址所指向的内存

打印地址所指向的内存,其效果类似于 frida 中 hexdump。

1
2
Inspector.inspect(ctx.getR0Pointer().getByteArray(0, 0x10), "Arg1");

treemap 参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
TreeMap<String, String> keymap = new TreeMap<String, String>();
keymap.put("ad_extra", "E1133C23F36571A3F1FDE6B325B17419AAD45287455E5292A19CF51300EAF0F2664C808E2C407FBD9E50BD48F8ED17334F4E2D3A07153630BF62F10DC5E53C42E32274C6076A5593C23EE6587F453F57B8457654CB3DCE90FAE943E2AF5FFAE78E574D02B8BBDFE640AE98B8F0247EC0970D2FD46D84B958E877628A8E90F7181CC16DD22A41AE9E1C2B9CB993F33B65E0B287312E8351ADC4A9515123966ACF8031FF4440EC4C472C78C8B0C6C8D5EA9AB9E579966AD4B9D23F65C40661A73958130E4D71F564B27C4533C14335EA64DD6E28C29CD92D5A8037DCD04C8CCEAEBECCE10EAAE0FAC91C788ECD424D8473CAA67D424450431467491B34A1450A781F341ABB8073C68DBCCC9863F829457C74DBD89C7A867C8B619EBB21F313D3021007D23D3776DA083A7E09CBA5A9875944C745BB691971BFE943BD468138BD727BF861869A68EA274719D66276BD2C3BB57867F45B11D6B1A778E7051B317967F8A5EAF132607242B12C9020328C80A1BBBF28E2E228C8C7CDACD1F6CC7500A08BA24C4B9E4BC9B69E039216AA8B0566B0C50A07F65255CE38F92124CB91D1C1C39A3C5F7D50E57DCD25C6684A57E1F56489AE39BDBC5CFE13C540CA025C42A3F0F3DA9882F2A1D0B5B1B36F020935FD64D58A47EF83213949130B956F12DB92B0546DADC1B605D9A3ED242C8D7EF02433A6C8E3C402C669447A7F151866E66383172A8A846CE49ACE61AD00C1E42223");
keymap.put("appkey", "1d8b6e7d45233436");
keymap.put("autoplay_card", "11");
keymap.put("banner_hash", "10687342131252771522");
keymap.put("build", "6180500");
keymap.put("c_locale", "zh_CN");
keymap.put("channel", "shenma117");
keymap.put("column", "2");
keymap.put("device_name", "MIX2S");
keymap.put("device_type", "0");
keymap.put("flush", "6");
keymap.put("ts", "1612693177");


DvmClass Map = vm.resolveClass("java/util/Map");
DvmClass AbstractMap = vm.resolveClass("java/util/AbstractMap",Map);
DvmObject<?> input_map = vm.resolveClass("java/util/TreeMap", AbstractMap).newObject(keymap);
list.add(vm.addLocalObject(input_map));
Number number = module.callFunction(emulator, 0x1c97, list.toArray())[0];
DvmObject result = vm.getObject(number.intValue());

需要注意的是,代码中补齐了 treeMap 的继承关系:map→AbstractMap→TreeMap,这么做是必要的,否则在有些情况下会报错

补环境

如果缺失的环境比较多,来源于某个 java 类。

  • 不继承自 AbstractJni
  • vm.setJni(this);改成 vm.setDvmClassFactory(new ProxyClassFactory());

然后根据提示复制相关的类到指定位置

对象数组参数怎么构造

image.png

1
2
3
4
5
6
7
8
StringObject input2_1 = new StringObject(vm, "9b69f861-e054-4bc4-9daf-d36ae205ed3e");
ByteArray input2_2 = new ByteArray(vm, "GET /aggroup/homepage/display __r0ysue".getBytes(StandardCharsets.UTF_8));
DvmInteger input2_3 = DvmInteger.valueOf(vm, 2);
vm.addLocalObject(input2_1);
vm.addLocalObject(input2_2);
vm.addLocalObject(input2_3);
// 完整的参数2
list.add(vm.addLocalObject(new ArrayObject(input2_1, input2_2, input2_3)));

补文件访问

方法 1

image.png
unidbg 对文件做了重定向,打印虚拟目录日志,把缺失的文件放入虚拟目录, 不存在的目录创建即可。

方法 2:

使用代码补

1
2
3
4
5
6
7
public FileResult resolve(Emulator emulator, String pathname, int oflags) {
if (("/data/app/com.sankuai.meituan-TEfTAIBttUmUzuVbwRK1DQ==/base.apk").equals(pathname)) {
// 填入想要重定位的文件
return FileResult.success(new SimpleFileIO(oflags, new File("unidbg-android\\src\\test\\java\\com\\lession10\\mt.apk"), pathname));
}
return null;
}

JNItrace

jnitrace 可以跟踪所有的 jni 调用,特别适合 unidgb 补环境使用。因为 so 层会有很多通过 jni 来调用 java 层的方法。

1
jnitrace -l libmtguard.so com.sankuai.meituan

SO 依赖

报错

1
2
[14:48:53 207] INFO [com.github.unidbg.linux.AndroidElfLoader]
(AndroidElfLoader:459) - libscmain.so load dependency libandroid.so failed

解决方法:

1
2
3
4
5
一种是hook,我们可以在libscmain中hook libandroid.so的函数,或者不管三七二十一,直接把那个
SO加载进来,然后hook 其中的各种函数,反正就是Hook,然后自己实现这些个函数的逻辑,给予正确
的返回值。
另一种方法是使用Unidbg提供的VirtualModule机制,创建一个虚拟SO,手动实现其中的函数,然后加
载进内存。(底层应该也用到了hook 回调)
1
2
3
4
5
6
7
8
9
10
DalvikModule dm_shared = vm.loadLibrary(new File("unidbg-
android/src/test/resources/lession2/libc++_shared.so"),true);
dm_shared.callJNI_OnLoad(emulator);
DalvikModule dm_libUserEnv = vm.loadLibrary(new File("unidbg-
android/src/test/resources/lession2/libUserEnv.so"),true);
dm_libUserEnv.callJNI_OnLoad(emulator);
DalvikModule dm = vm.loadLibrary(new File("unidbg-
android/src/test/resources/lession2/libpdd_secure.so"), true);
module = dm.getModule();
dm.callJNI_OnLoad(emulator);

注意:加顺序,依赖项必须较

  1. 先加载加载顺序,依赖项必须较加载顺序,依赖项必须较先加载
  2. 如果依赖库缺失环境报错,只要我们的目标 so 没用用到该函数,可以不用管。用到了就必须要补。

第二种 unidgb 提供了案例参考:
image.png

IO 重定向 、系统调用、文件访问(星球样本 1)

报错:

1
2
INFO [com.github.unidbg.linux.ARM32SyscallHandler] (ARM32SyscallHandler:1906) -
openat dirfd=-100, pathname=proc/9720/status, oflags=0x0, mode=0

openat 的系统调用 ,打开 proc/9720/status 文件
proc 文件系统由内核提供,系统中正在运行的每个进程都有对应的一个目录在 proc 下,其以进程的 PID 号为目录名,这个目录是读取进程信息的接口。 所以说“proc/9720/status”,就是读取 9720 这个进程的相关信息
在 Android 系统中,cmdline 里是应用的进程名,而 Status,则包含的信息非常多:可执行文件名、当前 状态、PID 和 PPID、实际及有效的 UID 和 GID、内存使用情况、以及其他。

可以用 adb shell 进入文件目录验证: cat status

利用 unidgb IO 重定向,补文件访问

1
2
3

emulator.getSyscallHandler().addIOResolver(this);

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
@Override
public FileResult resolve(Emulator emulator, String pathname, int oflags) {
if (("proc/"+emulator.getPid()+"/cmdline").equals(pathname)) {
// return FileResult.success(new ByteArrayFileIO(oflags, pathname,
"ctrip.android.view".getBytes()));
return FileResult.success(new SimpleFileIO(oflags, new
File("D:\\unidbg-teach\\unidbg android\\src\\test\\java\\com\\lession1\\cmdline"), pathname));
}
if (("proc/" + emulator.getPid() + "/status").equals(pathname)) {
return FileResult.success(new ByteArrayFileIO(oflags, pathname,
("Name: ip.android.view\n" +
"State: R (running)\n" +
"Tgid: "+emulator.getPid()+"\n" +
"Pid: "+emulator.getPid()+"\n" +
"PPid: 3000\n" +
"TracerPid: 0\n" +
"Uid: 10163 10163 10163 10163\n" +
"Gid: 10163 10163 10163 10163\n" +
"FDSize: 512\n" +
"Groups: 3002 3003 9997 20163 50163\n" +
"VmPeak: 2319784 kB\n" +
"VmSize: 2240148 kB\n" +
"VmLck: 0 kB\n" +
"VmPin: 0 kB\n" +
"VmHWM: 413060 kB\n" +
"VmRSS: 310988 kB\n" +
"VmData: 427160 kB\n" +
"VmStk: 8192 kB\n" +
"VmExe: 20 kB\n" +
"VmLib: 200676 kB\n" +
"VmPTE: 2100 kB\n" +
"VmSwap: 3356 kB\n" +
"Threads: 149\n" +
"SigQ: 1/6517\n" +
"SigPnd: 0000000000000000\n" +
"ShdPnd: 0000000000000000\n" +
"SigBlk: 0000000000001204\n" +
"SigIgn: 0000000000000000\n" +
"SigCgt: 00000006400096fc\n" +
"CapInh: 0000000000000000\n" +
"CapPrm: 0000000000000000\n" +
"CapEff: 0000000000000000\n" +
"CapBnd: 0000000000000000\n" +
"CapAmb: 0000000000000000\n" +
"Seccomp: 2\n" +
"Cpus_allowed: 0f\n" +
"Cpus_allowed_list: 0-3\n" +
"Mems_allowed: 1\n" +
"Mems_allowed_list: 0\n" +
"voluntary_ctxt_switches: 6918\n" +
"nonvoluntary_ctxt_switches: 4988").getBytes()));
}
return null;


补 Application

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
public DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject, String
signature, VaList vaList) {
switch (signature){
case "android/app/ActivityThread-
>getApplication()Landroid/app/Application;":{
return vm.resolveClass("android/app/Application",
vm.resolveClass("android/content/ContextWrapper",
vm.resolveClass("android/content/Context"))).newObject(signature);
}
}
return super.callObjectMethodV(vm, dvmObject, signature, vaList);
}

继承关系

android/content/Context → android/content/ContextWrapper→android/app/Application

returnvm.resolveClass(“android/app/returnvm.resolveClass(“android/app/Application”).newObject(signature);Application”).newObject(signature);

为什么要补继承关系?

1
2
3
4
5
访问实例方法的三步骤
1.获取类。表示需要访问哪一个JAVA类(FindClass)
2.获取方法ID,表示要访问类中的哪个实例方法 (GetMethodID)
3.调用方法 (CallxxxMethodxx),参数1是实例,参数2是方法ID,后面的参数是所调用的方法其
参数。
1
2
3
我们自己开发的时候可以不用管继承关系:
开启debug日志,当报错的时候直接检索methodID,看是哪个超类生成的ID,别管它在”真实代码逻辑“中是太爷爷、爷爷还是爸爸,
直接安排它做爸爸

设置日志为 debug

1
2
Logger.getLogger(DalvikVM.class).setLevel(Level.DEBUG);
Logger.getLogger(BaseVM.class).setLevel(Level.DEBUG);


unidgb 经验
http://blog.uzilol.cn/2022/03/10/yuque/unidgb%20%E7%BB%8F%E9%AA%8C/
作者
ive_e (leoli)
发布于
2022年3月10日
许可协议