某鱼直播软件使用unidbg算法分析
zsk Lv4
本文距离上次更新已过去 0 天,部分内容可能已经过时,请注意甄别。

image

Unidbg 模拟执行

首先模拟执行,先搭个架子

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
package com.douyu;

import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Module;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.AbstractJni;
import com.github.unidbg.linux.android.dvm.DalvikModule;
import com.github.unidbg.linux.android.dvm.VM;
import com.github.unidbg.memory.Memory;
import java.io.File;

public class DouYu extends AbstractJni {
private final AndroidEmulator emulator;
private final VM vm;
private final Module module;

public DouYu(){
emulator = AndroidEmulatorBuilder.for32Bit().build();
Memory memory = emulator.getMemory();
memory.setLibraryResolver(new AndroidResolver(23));
vm = emulator.createDalvikVM(new File("unidbg-android/src/test/resources/demo/douyu/douyu.apk"));
vm.setVerbose(true);
vm.setJni(this);
DalvikModule dm = vm.loadLibrary(new File("unidbg-android/src/test/resources/demo/douyu/libmakeurl2.5.0.so"), true);
module = dm.getModule();
dm.callJNI_OnLoad(emulator);
}

public static void main(String[] args) {
DouYu douYu = new DouYu();
}
}

运行结果

image
报了一个 ”不合法的JNI版本“ 错误,具体出错的原因有很多,一个常见的问题是SO的依赖库缺失。即程 序调用依赖库中某个函数时,因为这个依赖库没加载到Unidbg虚拟内存中,进而发生寻址错误,比如上 图就是 0x1664 地址访问失败。在Unidbg日志的第三行我们看到, libc++_shared.so 加载失败,即库缺失报错。 我们的目标SO依赖了 libc++_shared.so ,这个库是C++的支持库,但不在Unidbg默认支持的SO里。 我们要在apk的lib里把它拷贝出来。
在加载so文件前添加这两行,目标so依赖的其他so文件要在其前面加载

1
2
DalvikModule dm_shared = vm.loadLibrary(new File("unidbg-android/src/test/resources/demo/douyu/libc++_shared.so"), true);
dm_shared.callJNI_OnLoad(emulator);

再次运行就一切正常了
下面就到了今天的主角——native_makeUrl函数,Unidbg中先call它,参数很长,构造的很随意,因为只是学习用途。

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
package com.douyu;

import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Module;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.*;
import com.github.unidbg.linux.android.dvm.array.ArrayObject;
import com.github.unidbg.memory.Memory;
import java.io.File;
import java.util.ArrayList;
import java.util.List;

public class DouYu extends AbstractJni {
private final AndroidEmulator emulator;
private final VM vm;
private final Module module;

public DouYu(){
emulator = AndroidEmulatorBuilder.for32Bit().build();
Memory memory = emulator.getMemory();
memory.setLibraryResolver(new AndroidResolver(23));
vm = emulator.createDalvikVM(new File("unidbg-android/src/test/resources/demo/douyu/douyu.apk"));
vm.setVerbose(true);
vm.setJni(this);

DalvikModule dm_shared = vm.loadLibrary(new File("unidbg-android/src/test/resources/demo/douyu/libc++_shared.so"), true);
dm_shared.callJNI_OnLoad(emulator);

DalvikModule dm = vm.loadLibrary(new File("unidbg-android/src/test/resources/demo/douyu/libmakeurl2.5.0.so"), true);
module = dm.getModule();

// emulator.traceCode(module.base, module.base + module.size);
dm.callJNI_OnLoad(emulator);
}

public String getMakeUrl() {
// args list
List<Object> list = new ArrayList<>(10);
// arg1 env
list.add(vm.getJNIEnv());
// arg2 jobject/jclazz 一般用不到,直接填0
list.add(0);
DvmObject<?> context = vm.resolveClass("android/content/Context").newObject(null);
list.add(vm.addLocalObject(context));
list.add(vm.addLocalObject(new StringObject(vm, "")));
StringObject input3_1 = new StringObject(vm, "aid");
StringObject input3_2 = new StringObject(vm, "client_sys");
StringObject input3_3 = new StringObject(vm, "time");
vm.addLocalObject(input3_1);
vm.addLocalObject(input3_2);
vm.addLocalObject(input3_3);
list.add(vm.addLocalObject(new ArrayObject(input3_1, input3_2, input3_3)));
StringObject input4_1 = new StringObject(vm, "android1");
StringObject input4_2 = new StringObject(vm, "android");
StringObject input4_3 = new StringObject(vm, "1673232015");
vm.addLocalObject(input4_1);
vm.addLocalObject(input4_2);
vm.addLocalObject(input4_3);
list.add(vm.addLocalObject(new ArrayObject(input4_1, input4_2, input4_3)));
StringObject input5_1 = new StringObject(vm, "");
StringObject input5_2 = new StringObject(vm, "");
StringObject input5_3 = new StringObject(vm, "");
StringObject input5_4 = new StringObject(vm, "");
StringObject input5_5 = new StringObject(vm, "");
StringObject input5_6 = new StringObject(vm, "");
StringObject input5_7 = new StringObject(vm, "");
StringObject input5_8 = new StringObject(vm, "");
StringObject input5_9 = new StringObject(vm, "");
StringObject input5_10 = new StringObject(vm, "");
StringObject input5_11 = new StringObject(vm, "");
StringObject input5_12 = new StringObject(vm, "");
StringObject input5_13 = new StringObject(vm, "");
vm.addLocalObject(input5_1);
vm.addLocalObject(input5_2);
vm.addLocalObject(input5_3);
vm.addLocalObject(input5_4);
vm.addLocalObject(input5_5);
vm.addLocalObject(input5_6);
vm.addLocalObject(input5_7);
vm.addLocalObject(input5_8);
vm.addLocalObject(input5_9);
vm.addLocalObject(input5_10);
vm.addLocalObject(input5_11);
vm.addLocalObject(input5_12);
vm.addLocalObject(input5_13);
list.add(vm.addLocalObject(new ArrayObject(input5_1, input5_2, input5_3,input5_4, input5_5, input5_6,input5_7, input5_8, input5_9,input5_10, input5_11, input5_12,input5_13)));
StringObject input6_1 = new StringObject(vm, "");
StringObject input6_2 = new StringObject(vm, "");
StringObject input6_3 = new StringObject(vm, "");
StringObject input6_4 = new StringObject(vm, "");
StringObject input6_5 = new StringObject(vm, "");
StringObject input6_6 = new StringObject(vm, "");
StringObject input6_7 = new StringObject(vm, "");
StringObject input6_8 = new StringObject(vm, "");
StringObject input6_9 = new StringObject(vm, "");
StringObject input6_10 = new StringObject(vm, "");
vm.addLocalObject(input6_1);
vm.addLocalObject(input6_2);
vm.addLocalObject(input6_3);
vm.addLocalObject(input6_4);
vm.addLocalObject(input6_5);
vm.addLocalObject(input6_6);
vm.addLocalObject(input6_7);
vm.addLocalObject(input6_8);
vm.addLocalObject(input6_9);
vm.addLocalObject(input6_10);
list.add(vm.addLocalObject(new ArrayObject(input6_1, input6_2, input6_3,input6_4, input6_5, input6_6,input6_7, input6_8, input6_9,input6_10)));
list.add(0);
list.add(1);
// 参数准备完成
// call function
Number number = module.callFunction(emulator, 0x2f91, list.toArray());
return vm.getObject(number.intValue()).getValue().toString();
}

public static void main(String[] args) {
DouYu douYu = new DouYu();
String makeUrl = douYu.getMakeUrl();
System.out.println("result:"+ makeUrl);
}
}

运行后直接出结果
image
可以发现,结果由四部分组成,前三个参数是我们 input4 传进去的内容,所以需要分析的只有auth的 来源。 多次运行会发现,auth 的值,恒为 3c179e17e8e9b06d7b18c68555b92220,长度 32 位。

算法分析

首先,确认函数执行流的汇编长度,如果行数过多上千万甚至上亿行,就只能放弃。如果几十万行,那就还可以看看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public void traceLength() {
emulator.getBackend().hook_add_new(new CodeHook() {
int count = 0;

@Override
public void hook(Backend backend, long address, int size, Object user) {
count += 1;
System.out.println(count);
}

@Override
public void onAttach(UnHook unHook) {
}

@Override
public void detach() {
}
}, module.base, module.size + module.base, null);
}

image
运行计数总共九十一万行, 不超过100w行的执行流,要么程序没怎么混淆,要么逻辑不 太复杂。两者任意一个复杂度高一些,都不会只有100w行汇编以内。
使用findcrypt插件再确认一下样本大概使用了哪些加密算法

image
SO中至少存在 AES/BASE64,至于我们的函数中用了什么?这得具体分析,毕竟Findcrypt只是一个静态 的、加密特征匹配插件。

  • 目标函数可能用了AES/Base64,说“可能”是上述算法可能用于SO中其他函数而非目标函数。
  • 目标函数可能用了AES和Base64之外的其他加密算法,因为FIndCrypt提供了静态的、有限的分析,很容易遗漏。

使用Unidbg处理算法,一般而言,自下而上分析更省时省力,这得益于Unidbg两方面的能力

  • 强大方便的内存读写监控
  • 无地址随机化

这让我们可以逆流而上,自结果推来源,分析算法和数据块十分轻松。
重新看运行结果图
image
运算结果来自于NewStringUTF,这个JString从哪里来的?
日志提示调用处在0x336f,这个地址实际上是LR(返回地址),所以NewStringUTF函数调用是 0x336f 的上一条 0x336C。

image

在 0x336C 下断点

image

回顾一下NewStringUTF 这个JNI方法,数据来源就是参数二字符数组

1
jstring NewStringUTF(JNIEnv *env, const char *bytes);

数据从地址0x402d20a0开始,我们要监控auth=后面的数据,即从0x402d20a0+len(aid=android1&client_sys=android&time=1638452332&auth=)开始,数一下auth的32个字节所处的地址,监控对它的写入。

1
emulator.traceWrite(0x401D20D5, 0x401D20D5+0x20);

image

从下往上寻找对内存最晚的操作,可以发现,这32个字节的赋值发生在libc里。一般数据在libc里赋值, 指的是调用了libc中memcpy等库函数做拷贝、转换、比较等处理,而非数据生成的第一现场。
把Unidbg的libc.so拷贝一份出来,扔到IDA里。搜索0x17d3a,看具体是哪个函数。我们发现 是在strcat函数里,即做字符串拼接。
hook strcat 函数,进行追踪

1
char *strcat(char *dest, const char *src)
  • dest – 指向目标数组,该数组包含了一个 C 字符串,且足够容纳追加后的字符串。
  • src – 指向要追加的字符串,该字符串不会覆盖目标字符串。
1
2
3
4
5
6
7
8
9
10
11
12
public void hookStrCat(){
emulator.attach().addBreakPoint(module.findSymbolByName("strcat",
true).getAddress(), new BreakPointCallback() {
@Override
public boolean onHit(Emulator<?> emulator, long address) {
UnidbgPointer r1 = emulator.getContext().getPointerArg(1);
System.out.println("strcat:"+ r1);
System.out.println(r1.getString(0));
return true;
}
});
}

运行,可以发现结果的四个字段就是strcat 逐步拼接的结果

image

对来源 0xbffff69b 做traceWrite,千万记得加后缀L。
发现依然来自于libc,这不是好事,说明我们还没到第一现场。

image

从IDA跳到地址 0x176dc,看到是在_memcpy_base函数里

1
2
void __fastcall _memcpy_base(int a1, char *a2, unsigned int a3, int a4, int a5,
int a6)

它应该是memcpy函数内部的子函数,我们Hook一下memcpy,其原型如下

1
void *memcpy(void *str1, const void *str2, size_t n)
  • str1 – 指向用于存储复制内容的目标数组,类型强制转换为 void* 指针。
  • str2 – 指向要复制的数据源,类型强制转换为 void* 指针。
  • n – 要被复制的字节数。

我们打印str2,长度为n

1
2
3
4
5
6
7
8
9
10
11
12
13
public void hookMemcpy(){
emulator.attach().addBreakPoint(module.findSymbolByName("memcpy",
true).getAddress(), new BreakPointCallback() {
@Override
public boolean onHit(Emulator<?> emulator, long address) {
UnidbgPointer r1 = emulator.getContext().getPointerArg(1);
int length = emulator.getContext().getIntArg(2);
System.out.println("memcpy");
Inspector.inspect(r1.getByteArray(0, length), r1.toString());
return true;
}
});
}

运行发现,程序逻辑上逐两个字节进行拷贝

image

140处调用。看的头疼,所以我尝试性的搜索了下3c179e17e8e9b06d7b18c68555b92220,期待某次 memcpy可以看到它,那么我们就能找到它的产生之处了。

image

打印memcpy的str2时,我采用了Unidbg的Inspect API,它会在 打印内存块时,顺带打印数据的MD5值,这个设计主要是为了比较两个内存块是否全然等值,但这里却 帮到了我们。

1
2
3
4
0000: 61 69 64 3D 61 6E 64 72 6F 69 64 31 26 63 6C 69    aid=android1&cli
0010: 65 6E 74 5F 73 79 73 3D 61 6E 64 72 6F 69 64 26 ent_sys=android&
0020: 74 69 6D 65 3D 31 36 37 33 32 33 32 30 31 35 76 time=1673232015v
0030: 71 34 37 48 64 39 4A 55 67 66 44 43 79 74 43 q47Hd9JUgfDCytC

vq47Hd9JUgfDCytC 这十个字节是未知的,其余三个字段是传进来的,我们结合上面的MD5会产生一种 明悟,这不就是加盐MD5吗?传进来的参数拼接后加上”vq47Hd9JUgfDCytC“,MD5后传出去。
那么现在问题就变成了,vq47Hd9JUgfDCytC是哪里来的?

image

对0xbffff500L 做traceWrite

image

ida打开libmakeurl.so,看一下来源0x8a88,十六个字节都来自这里

image

前面我们用过Findcrypt,有看到RijnDael_AES_LONG_inv_45FC4,它自动将0x8a9e所位于的函数中,一个数组标记为AES的S逆盒。这告诉我们,十六字节的生成处是AES的运算逻辑。换而言之,这十六字节大概率是AES加密或解密的输出。
可是,样本使用了Ollvm,比如0x8a9e这一行,就是Ollvm中的指令替换。
我们先不要陷入函数的细节里,因为如果是标准AES,那根本不用 分析加密程序的内部,自然也就不用考虑这些混淆了。
0x8a88 位于 sub_8228 函数内,Hook sub_8228,顺利断下
观察两个参数

1
int __fastcall sub_8228(unsigned __int64 a1, _QWORD *a2)

image

参数2像是buffer,存放加密结果。blr用于在函数返回处下断点,然后c继续跑,在函数运行结束后再次 查看参数2指向的内存。
image

可以发现确实是我们要分析的十六个字节。至于参数1是什么意思,硬看似乎看不出来。
因此可以判断,sub_8228生成了我们要分析的十六个字节,而且它像AES的执行逻辑。
AES 加密还是解密?什么工作模式?明文是什么?Key是什么?一概不知。我们得到sub_8228上层去看看。
重新运行程序,bt 打印调用栈
image
跳到 0x08ba7 看一下

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
int __fastcall sub_8B3C(const char *a1, int a2, int a3)
{
signed int i; // r0
int v7; // r1
int v8; // r1
signed int v10; // [sp+4h] [bp-144h]
signed int v11; // [sp+Ch] [bp-13Ch]
char v12[280]; // [sp+10h] [bp-138h] BYREF
int v13; // [sp+128h] [bp-20h]

v10 = (strlen(a1) + 15) >> 4;
sub_72BC(v12, a2, 128);
for ( i = 0; ; i = v11 + 1 )
{
v7 = 1590846758;
while ( 1 )
{
v8 = v7 & 0x7FFFFFFF;
if ( v8 != 1590846758 )
break;
v11 = i;
v7 = 131555431;
if ( i < v10 )
v7 = 1574041125;
}
if ( v8 == 131555431 )
break;
if ( v8 != 1574041125 )
{
while ( 1 )
;
}
sub_8228(v12, a3 + 16 * v11, &a1[16 * v11]);
}
return _stack_chk_guard - v13;
}

首先我们知道,sub_8228是AES的具体运算程序,刚才Hook确认了这一点。而密钥编排一般发生在具 体运算前面,即早于sub_8228。整个函数体内,就只有sub_72BC 一个函数了,那也可能在sub_8B3C外层。更重要的线索是它的参数3,128。AES 存在128/192/256 三种密钥的规格,这里就是在指定AES的规格,并生成对应的轮密钥。 char v12[280]; v12是一个较大的数组,用于存放生成轮密钥的结果。
那么可以大胆猜测sub_8B3C的a2就是十六字节长的AES-128密钥。进而参数1就是 十六字节的输入。
对 sub_8B3C 进行断点查看参数

image

密钥是30292827262524232221000000000000,暂时不知道是加密还是解密,结果是 767134374864394a5567664443797443(vq47Hd9JUgfDCytC)
看看加密过程对的上吗

image

因为aes会对明文进行填充,它会自动按照PKCS7约定,再次填充一个分组的长度,输出也是两个分组的结果。 这里结果a7488462036f15054005472d6f487c67才是对的,后面是填充后的分组加密而来的可以不用管
跟我们上面的 sub_8B3C 的参数一是一致的,说明 vq47Hd9JUgfDCytC 是由明文a7488462036f15054005472d6f487c67,密钥30292827262524232221000000000000解密而来的

我们这里做一个讨论,如何从一个小的线索点,分析出AES的全貌。
以 sub_72BC(v12, a2, 128); 为例,我们猜测它是密钥编排函数,那么如何快速验证呢?
我Hook 入参时a2指向的十六字节,以及函数结束后v13指向的176字节(因为是AES-128,所以轮密钥 是4*44)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public void hook72bc(){
emulator.attach().addBreakPoint(module.base + 0x72bc, new BreakPointCallback() {
UnidbgPointer v12;
@Override
public boolean onHit(Emulator<?> emulator, long address) {
RegisterContext registerContext = emulator.getContext();
UnidbgPointer a2 = registerContext.getPointerArg(1);
v12 = registerContext.getPointerArg(0);
Inspector.inspect(a2.getByteArray(0, 0x10), "key " + a2.toString());
// 函数结束对v12hook查看
emulator.attach().addBreakPoint(registerContext.getLRPointer().peer, new BreakPointCallback() {
@Override
public boolean onHit(Emulator<?> emulator, long address) {
Inspector.inspect(v12.getByteArray(0, 176), "Round Key "+v12.toString());
return false;
}
});
return true;
}
});
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
>-----------------------------------------------------------------------------<
[17:01:35 327]key unidbg@0xbffff3d8, md5=037ff8eefc91404afaed9fa22e282e3f, hex=30292827262524232221000000000000
size: 16
0000: 30 29 28 27 26 25 24 23 22 21 00 00 00 00 00 00 0)('&%$#"!......
^-----------------------------------------------------------------------------^

>-----------------------------------------------------------------------------<
[17:01:35 336]Round Key unidbg@0xbffff290, md5=159aaceb29acbcbd8d87b091ce103546, hex=0a00000098f2ffbf30292827262524232221000000000000524a4b44746f6f67564e6f67564e6f677fe2cef50b8da1925dc3cef50b8da19226d081de2d5d204c709eeeb97b134f2b535470ff7e0950b30e97be0a7584f1211cf58d6262fcddd16c6b63db19ef92fae3baa0b681467d67ed2d1ebcf4c28c4686defa090798876eeab599d21e771594f387d87bf41f5f151eaac6c700ddd35329e13518ddfe6a0dc354accac3897f99b833db3665cdb13b
size: 176
0000: 0A 00 00 00 98 F2 FF BF 30 29 28 27 26 25 24 23 ........0)('&%$#
0010: 22 21 00 00 00 00 00 00 52 4A 4B 44 74 6F 6F 67 "!......RJKDtoog
0020: 56 4E 6F 67 56 4E 6F 67 7F E2 CE F5 0B 8D A1 92 VNogVNog........
0030: 5D C3 CE F5 0B 8D A1 92 26 D0 81 DE 2D 5D 20 4C ].......&...-] L
0040: 70 9E EE B9 7B 13 4F 2B 53 54 70 FF 7E 09 50 B3 p...{.O+STp.~.P.
0050: 0E 97 BE 0A 75 84 F1 21 1C F5 8D 62 62 FC DD D1 ....u..!...bb...
0060: 6C 6B 63 DB 19 EF 92 FA E3 BA A0 B6 81 46 7D 67 lkc..........F}g
0070: ED 2D 1E BC F4 C2 8C 46 86 DE FA 09 07 98 87 6E .-.....F.......n
0080: EA B5 99 D2 1E 77 15 94 F3 87 D8 7B F4 1F 5F 15 .....w.....{.._.
0090: 1E AA C6 C7 00 DD D3 53 29 E1 35 18 DD FE 6A 0D .......S).5...j.
00A0: C3 54 AC CA C3 89 7F 99 B8 33 DB 36 65 CD B1 3B .T.......3.6e..;
^-----------------------------------------------------------------------------^

RoundKey 的结果像是一个结构体,两个int组成,第一个是0x0000000a,即代表了AES-128的十轮运 算,第二个是指针,值为0xbffff298,是v12往后偏移八个字节。
我们不妨修改一下hook72bc,看一下0xbffff298具体打印什么

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public void hook72bc(){
emulator.attach().addBreakPoint(module.base + 0x72bc, new BreakPointCallback() {
UnidbgPointer v12;
@Override
public boolean onHit(Emulator<?> emulator, long address) {
RegisterContext registerContext = emulator.getContext();
UnidbgPointer a2 = registerContext.getPointerArg(1);
v12 = registerContext.getPointerArg(0);
Inspector.inspect(a2.getByteArray(0, 0x10), "key " + a2.toString());
// 函数结束对v12hook查看
emulator.attach().addBreakPoint(registerContext.getLRPointer().peer, new BreakPointCallback() {
@Override
public boolean onHit(Emulator<?> emulator, long address) {
//Inspector.inspect(v12.getByteArray(0, 176), "Round Key "+v12.toString());
Inspector.inspect(v12.getByteArray(8, 176), "Round Key "+v12.toString());
return true;
}
});
return true;
}
});
}

结果如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[17:20:19 949]key unidbg@0xbffff3d8, md5=037ff8eefc91404afaed9fa22e282e3f, hex=30292827262524232221000000000000
size: 16
0000: 30 29 28 27 26 25 24 23 22 21 00 00 00 00 00 00 0)('&%$#"!......
^-----------------------------------------------------------------------------^

>-----------------------------------------------------------------------------<
[17:20:19 957]Round Key unidbg@0xbffff290, md5=1a32868f8f948e426e209b5995588178, hex=30292827262524232221000000000000524a4b44746f6f67564e6f67564e6f677fe2cef50b8da1925dc3cef50b8da19226d081de2d5d204c709eeeb97b134f2b535470ff7e0950b30e97be0a7584f1211cf58d6262fcddd16c6b63db19ef92fae3baa0b681467d67ed2d1ebcf4c28c4686defa090798876eeab599d21e771594f387d87bf41f5f151eaac6c700ddd35329e13518ddfe6a0dc354accac3897f99b833db3665cdb13ba6991df165106268
size: 176
0000: 30 29 28 27 26 25 24 23 22 21 00 00 00 00 00 00 0)('&%$#"!......
0010: 52 4A 4B 44 74 6F 6F 67 56 4E 6F 67 56 4E 6F 67 RJKDtoogVNogVNog
0020: 7F E2 CE F5 0B 8D A1 92 5D C3 CE F5 0B 8D A1 92 ........].......
0030: 26 D0 81 DE 2D 5D 20 4C 70 9E EE B9 7B 13 4F 2B &...-] Lp...{.O+
0040: 53 54 70 FF 7E 09 50 B3 0E 97 BE 0A 75 84 F1 21 STp.~.P.....u..!
0050: 1C F5 8D 62 62 FC DD D1 6C 6B 63 DB 19 EF 92 FA ...bb...lkc.....
0060: E3 BA A0 B6 81 46 7D 67 ED 2D 1E BC F4 C2 8C 46 .....F}g.-.....F
0070: 86 DE FA 09 07 98 87 6E EA B5 99 D2 1E 77 15 94 .......n.....w..
0080: F3 87 D8 7B F4 1F 5F 15 1E AA C6 C7 00 DD D3 53 ...{.._........S
0090: 29 E1 35 18 DD FE 6A 0D C3 54 AC CA C3 89 7F 99 ).5...j..T......
00A0: B8 33 DB 36 65 CD B1 3B A6 99 1D F1 65 10 62 68 .3.6e..;....e.bh
^-----------------------------------------------------------------------------^

首先我们就可以确定,这就是密钥编排的结果,这是我们根据AES-128的编排性质推断出来的。

  • 轮密钥的前十六个字节就是主密钥,完全符合
  • 十六个字节后面的编排规则,以行为单位看的话,前四个字节较为复杂,后十二字节只是简单异 或。如下验证

密钥 30 29 28 27 26 25 24 23 22 21 00 00 00 00 00 00
按照密钥的编排 4个字节一组
W0 30 29 28 27
W1 26 25 24 23
W2 22 21 00 00
W3 00 00 00 00
根据结果我们也可以看出
W4 = 52 4A 4B 44
W5 = 74 6F 6F 67
验证可以得到 W4 xor W1 = W5

1
2
3
4
5
C:\Users\zsk>python
Python 3.8.10 (tags/v3.8.10:3d8993a, May 3 2021, 11:48:03) [MSC v.1928 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> hex(0x524A4B44^0x26252423)
'0x746f6f67'

确实符合编排的规律。

因此可以认定 72bc 就是密钥编排函数,并确定了密钥。怎么仅从这个线索,推出输入呢?

  • 如果是加密,那么对K0做traceRead可以定位到算法的输入,对K10做traceRead,其运算结果就是算法 的输出。
  • 如果是解密,那么对K0做traceRead可以定位算法的输出,对K10做traceRead,其运算结果就是算法的 输入。

换个情况,如果只知道算法的输入,该怎么确认密钥呢?

  • 如果是加密,那么对算法的输入做traceRead,可以定位到K0,在AES-128上意味着主密钥;
  • 如果是CBC 模式,那么定位到IV。 如果是解密,那么对算法的输入做traceRead,可以定位到K10,使用stark 逆推主密钥。

再换个情况,如果只知道算法的输出,该怎么确认其他要素?

  • 如果是加密过程,对算法的输出做traceWrite,运算的双方中有一方是K10。
  • 如果是解密过程,对算法的输出做traceWrite,运算的双方中有一方是K0。

下面考虑Key和密文哪里来的 在sub_8B3C打断点,查看堆栈,发现都位于 sub_A298

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
int __fastcall sub_A298(void *a1)
{
int v1; // r9
int v3; // r11
int i; // r0
int v6; // [sp+0h] [bp-78h]
int v7; // [sp+4h] [bp-74h]
_QWORD v8[2]; // [sp+8h] [bp-70h] BYREF
char v9[20]; // [sp+18h] [bp-60h] BYREF
_QWORD v10[2]; // [sp+30h] [bp-48h] BYREF
char v11[20]; // [sp+40h] [bp-38h] BYREF
int v12; // [sp+58h] [bp-20h]

v3 = 0;
v10[0] = unk_45688;
v10[1] = unk_45690;
strcpy(v11, " ");
v8[0] = unk_45CF0;
v8[1] = unk_45CF8;
strcpy(v9, " ");
memset(a1, 0, 0x100u);
for ( i = 1282341844; ; i = 1282341844 )
{
while ( i != 967467364 )
{
if ( i == 1282341844 )
{
v6 = v3;
i = 1618205161;
if ( v3 < 32 )
i = -1314423687;
if ( i <= 967467363 )
goto LABEL_15;
}
else
{
v1 = 0;
LABEL_14:
i = 967467364;
}
}
v7 = v1;
i = -688078044;
if ( v1 < 32 )
i = -1194610101;
LABEL_15:
if ( i != -1314423687 )
break;
*((_BYTE *)v8 + v6) = (*((_BYTE *)v8 + v6) & 0x8E | ~*((_BYTE *)v8 + v6) & 0x71) ^ 0x51;
v3 = v6 + 1;
}
if ( i == -1194610101 )
{
*((_BYTE *)v10 + v7) = (~*((_BYTE *)v10 + v7) & 0xE9 | *((_BYTE *)v10 + v7) & 0x16) ^ 0xC9;
v1 = v7 + 1;
goto LABEL_14;
}
sub_8B3C((const char *)v10, (int)v8, (int)a1);
return _stack_chk_guard - v12;
}

其中v10 , 前八个字节来自0x45688,后八个字节来自0x45690。因为这两个八字节是紧连着的,所以可 以一并看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
.rodata:00045688 87                            unk_45688 DCB 0x87                      ; DATA XREF: sub_A298+E↑o
.rodata:00045688 ; sub_A298+18↑o
.rodata:00045688 ; .text:off_A424↑o
.rodata:00045689 68 DCB 0x68 ; h
.rodata:0004568A A4 DCB 0xA4
.rodata:0004568B 42 DCB 0x42 ; B
.rodata:0004568C 23 DCB 0x23 ; #
.rodata:0004568D 4F DCB 0x4F ; O
.rodata:0004568E 35 DCB 0x35 ; 5
.rodata:0004568F 25 DCB 0x25 ; %
.rodata:00045690 60 unk_45690 DCB 0x60 ; `
.rodata:00045691 25 DCB 0x25 ; %
.rodata:00045692 67 DCB 0x67 ; g
.rodata:00045693 0D DCB 0xD
.rodata:00045694 4F DCB 0x4F ; O
.rodata:00045695 68 DCB 0x68 ; h
.rodata:00045696 5C DCB 0x5C ; \
.rodata:00045697 47 DCB 0x47 ; G

在逐字节经过如下处理后成为我们的密文

1
*((_BYTE *)v10 + v7) = (~*((_BYTE *)v10 + v7) & 0xE9 | *((_BYTE *)v10 + v7) & 0x16) ^ 0xC9;


而密钥也一样,前八个字节来自0x45CF0,后八个字节来自0x45CF8。

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
.rodata:00045CF0 10                            unk_45CF0 DCB 0x10                      ; DATA XREF: sub_A298+24↑o
.rodata:00045CF0 ; sub_A298+2C↑o
.rodata:00045CF0 ; .text:off_A428↑o
.rodata:00045CF0 ; sub_A430+50↑o
.rodata:00045CF0 ; sub_A430+5E↑o
.rodata:00045CF0 ; .text:off_A5D0↑o
.rodata:00045CF0 ; sub_A5D8+4E↑o
.rodata:00045CF0 ; sub_A5D8+5C↑o
.rodata:00045CF0 ; .text:off_A770↑o
.rodata:00045CF0 ; sub_A778+4E↑o
.rodata:00045CF0 ; sub_A778+5C↑o
.rodata:00045CF0 ; .text:off_A910↑o
.rodata:00045CF0 ; sub_A918+50↑o
.rodata:00045CF0 ; sub_A918+5E↑o
.rodata:00045CF0 ; .text:off_AAA4↑o ...
.rodata:00045CF1 09 DCB 9
.rodata:00045CF2 08 DCB 8
.rodata:00045CF3 07 DCB 7
.rodata:00045CF4 06 DCB 6
.rodata:00045CF5 05 DCB 5
.rodata:00045CF6 04 DCB 4
.rodata:00045CF7 03 DCB 3
.rodata:00045CF8 02 unk_45CF8 DCB 2
.rodata:00045CF9 01 DCB 1
.rodata:00045CFA 20 DCB 0x20
.rodata:00045CFB 20 DCB 0x20
.rodata:00045CFC 20 DCB 0x20
.rodata:00045CFD 20 DCB 0x20
.rodata:00045CFE 20 DCB 0x20
.rodata:00045CFF 20 DCB 0x20

它经过了如下逐字节的处理

1
*((_BYTE *)v8 + v6) = (*((_BYTE *)v8 + v6) & 0x8E | ~*((_BYTE *)v8 + v6) & 0x71) ^ 0x51;

看起来有些云里雾里的,这是Ollvm中指令替换的功劳。
真正功能上而言,只是SO中硬编码的两串十六进制字节,在异或0x20后,就成为了密文和Key,在运行 时AES解密出明文,作为MD5的盐。
我们以Key为例,它的完整流程如下(下面均为十六进制字节)
首先,Key是 30 29 28 27 26 25 24 23 22 21 00 00 00 00 00 00,开发者不希望硬编码在SO里,所以先 将它异或0x20,在SO中硬编码即 10 09 08 07 06 05 04 03 02 01 20 20 20 20 20 20。
然后在使用时,将这么一串异或0x20,因为异或两次等于自身,所以Key重新变成30 29 28 27 26 25 24 23 22 21 00 00 00 00 00 00,正常参与运算。

那么下面这两种运算,其功能都等价于单字节异或0x20,怎么变成这个样子了呢?

1
2
*((_BYTE *)v10 + v7) = (~*((_BYTE *)v10 + v7) & 0xE9 | *((_BYTE *)v10 + v7) & 0x16) ^ 0xC9;
*((_BYTE *)v8 + v6) = (*((_BYTE *)v8 + v6) & 0x8E | ~*((_BYTE *)v8 + v6) & 0x71) ^ 0x51;


这就是指令替换的目的,将简单的加减乘除、异或、与等运算,替换成等价但更复杂的指令序列。
演示一下这个过程
S = A ^ B
异或0不影响结果
S = A ^ B ^ 0
0可以展开成C ^ C
S = A ^ B ^ C ^ C
做一下简单的分配
S = (A^C)^(B^C)
两数异或时可以等价替换如下,可以自行验证。

1
a ^ b => (~a & b) | (a & ~b) 

那么

1
S = (~A & C) | (A & ~C) ^ (~B & C) | (B & ~C)

回到 S = A ^ B,假设A 就是我们的待处理数据,B是0x20,即将数据和0x20异或,我们再选择C为0xE9
1
S = (~A & 0xE9) | (A & ~0xE9) ^ (~0x20 & 0xE9) | (0x20 & ~0xE9)


0xE9 在取反后即 0x16,而异或的另外一方,因为不存在未知数,编译器会直接优化计算出结果

image

1
S = (~A & 0xE9) | (A & 0xE9) ^ 0xC9


A 代入 *((_BYTE *)v10 + v7) 不就是

1
*((_BYTE *)v10 + v7) = (~*((_BYTE *)v10 + v7) & 0xE9) | (*((_BYTE *)v10 + v7) & 0xE9) ^ 0xC9

A 代入 *((_BYTE *)v8 + v6) ,C 为0x71时,就是 另一个式子。
本质上,两者都是逐字节与0x20异或。

完整代码

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
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
package com.douyu;

import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Emulator;
import com.github.unidbg.Module;
import com.github.unidbg.arm.backend.Backend;
import com.github.unidbg.arm.backend.CodeHook;
import com.github.unidbg.arm.backend.UnHook;
import com.github.unidbg.arm.context.RegisterContext;
import com.github.unidbg.debugger.BreakPointCallback;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.*;
import com.github.unidbg.linux.android.dvm.array.ArrayObject;
import com.github.unidbg.memory.Memory;
import com.github.unidbg.pointer.UnidbgPointer;
import com.github.unidbg.utils.Inspector;

import java.io.File;
import java.util.ArrayList;
import java.util.List;

public class DouYu extends AbstractJni {
private final AndroidEmulator emulator;
private final VM vm;
private final Module module;

public DouYu(){
emulator = AndroidEmulatorBuilder.for32Bit().build();
Memory memory = emulator.getMemory();
memory.setLibraryResolver(new AndroidResolver(23));
vm = emulator.createDalvikVM(new File("unidbg-android/src/test/resources/demo/douyu/douyu.apk"));
vm.setVerbose(true);
vm.setJni(this);

DalvikModule dm_shared = vm.loadLibrary(new File("unidbg-android/src/test/resources/demo/douyu/libc++_shared.so"), true);
dm_shared.callJNI_OnLoad(emulator);

DalvikModule dm = vm.loadLibrary(new File("unidbg-android/src/test/resources/demo/douyu/libmakeurl2.5.0.so"), true);
module = dm.getModule();
dm.callJNI_OnLoad(emulator);

// NewStringUTF断点
//emulator.attach().addBreakPoint(module.base + 0x336c);
// 监控地址写入, auth的32个字节所处的地址, 401d20a0 + len(aid=android1&client_sys=android&time=1638452332&auth=)
//emulator.traceWrite(0x401D20D5, 0x401D20D5+0x20);

// 监控strcat函数,看结果是在哪里写入的
//emulator.traceWrite(0xbffff69bL, 0xbffff69bL+0x20);

// 对 vq47Hd9JUgfDCytC 做traceWrite
//emulator.traceWrite(0xbffff500L, 0xbffff69bL+0x20);

// 对 vq47Hd9JUgfDCytC 出现的位置函数 sub_8228(unsigned __int64 a1, _QWORD *a2) 进行断点
//emulator.attach().addBreakPoint(module.base + 0x8228);

// 对调用生成 vq47Hd9JUgfDCytC 函数的上一级断点
emulator.attach().addBreakPoint(module.base + 0x8B3C);
}

public String getMakeUrl() {
// args list
List<Object> list = new ArrayList<>(10);
// arg1 env
list.add(vm.getJNIEnv());
// arg2 jobject/jclazz 一般用不到,直接填0
list.add(0);
DvmObject<?> context = vm.resolveClass("android/content/Context").newObject(null);
list.add(vm.addLocalObject(context));
list.add(vm.addLocalObject(new StringObject(vm, "")));
StringObject input3_1 = new StringObject(vm, "aid");
StringObject input3_2 = new StringObject(vm, "client_sys");
StringObject input3_3 = new StringObject(vm, "time");
vm.addLocalObject(input3_1);
vm.addLocalObject(input3_2);
vm.addLocalObject(input3_3);
list.add(vm.addLocalObject(new ArrayObject(input3_1, input3_2, input3_3)));
StringObject input4_1 = new StringObject(vm, "android1");
StringObject input4_2 = new StringObject(vm, "android");
StringObject input4_3 = new StringObject(vm, "1673232015");
vm.addLocalObject(input4_1);
vm.addLocalObject(input4_2);
vm.addLocalObject(input4_3);
list.add(vm.addLocalObject(new ArrayObject(input4_1, input4_2, input4_3)));
StringObject input5_1 = new StringObject(vm, "");
StringObject input5_2 = new StringObject(vm, "");
StringObject input5_3 = new StringObject(vm, "");
StringObject input5_4 = new StringObject(vm, "");
StringObject input5_5 = new StringObject(vm, "");
StringObject input5_6 = new StringObject(vm, "");
StringObject input5_7 = new StringObject(vm, "");
StringObject input5_8 = new StringObject(vm, "");
StringObject input5_9 = new StringObject(vm, "");
StringObject input5_10 = new StringObject(vm, "");
StringObject input5_11 = new StringObject(vm, "");
StringObject input5_12 = new StringObject(vm, "");
StringObject input5_13 = new StringObject(vm, "");
vm.addLocalObject(input5_1);
vm.addLocalObject(input5_2);
vm.addLocalObject(input5_3);
vm.addLocalObject(input5_4);
vm.addLocalObject(input5_5);
vm.addLocalObject(input5_6);
vm.addLocalObject(input5_7);
vm.addLocalObject(input5_8);
vm.addLocalObject(input5_9);
vm.addLocalObject(input5_10);
vm.addLocalObject(input5_11);
vm.addLocalObject(input5_12);
vm.addLocalObject(input5_13);
list.add(vm.addLocalObject(new ArrayObject(input5_1, input5_2, input5_3,input5_4, input5_5, input5_6,input5_7, input5_8, input5_9,input5_10, input5_11, input5_12,input5_13)));
StringObject input6_1 = new StringObject(vm, "");
StringObject input6_2 = new StringObject(vm, "");
StringObject input6_3 = new StringObject(vm, "");
StringObject input6_4 = new StringObject(vm, "");
StringObject input6_5 = new StringObject(vm, "");
StringObject input6_6 = new StringObject(vm, "");
StringObject input6_7 = new StringObject(vm, "");
StringObject input6_8 = new StringObject(vm, "");
StringObject input6_9 = new StringObject(vm, "");
StringObject input6_10 = new StringObject(vm, "");
vm.addLocalObject(input6_1);
vm.addLocalObject(input6_2);
vm.addLocalObject(input6_3);
vm.addLocalObject(input6_4);
vm.addLocalObject(input6_5);
vm.addLocalObject(input6_6);
vm.addLocalObject(input6_7);
vm.addLocalObject(input6_8);
vm.addLocalObject(input6_9);
vm.addLocalObject(input6_10);
list.add(vm.addLocalObject(new ArrayObject(input6_1, input6_2, input6_3,input6_4, input6_5, input6_6,input6_7, input6_8, input6_9,input6_10)));
list.add(0);
list.add(1);
// 参数准备完成
// call function
Number number = module.callFunction(emulator, 0x2f91, list.toArray());
return vm.getObject(number.intValue()).getValue().toString();
}

public void traceLength() {
emulator.getBackend().hook_add_new(new CodeHook() {
int count = 0;

@Override
public void hook(Backend backend, long address, int size, Object user) {
count += 1;
System.out.println(count);
}

@Override
public void onAttach(UnHook unHook) {
}

@Override
public void detach() {
}
}, module.base, module.size + module.base, null);
}

public void hookStrCat(){
emulator.attach().addBreakPoint(module.findSymbolByName("strcat",
true).getAddress(), new BreakPointCallback() {
@Override
public boolean onHit(Emulator<?> emulator, long address) {
UnidbgPointer r1 = emulator.getContext().getPointerArg(1);
System.out.println("strcat:"+ r1);
System.out.println(r1.getString(0));
return true;
}
});
}

public void hookMemcpy(){
emulator.attach().addBreakPoint(module.findSymbolByName("memcpy", true).getAddress(), new BreakPointCallback() {
@Override
public boolean onHit(Emulator<?> emulator, long address) {
UnidbgPointer r1 = emulator.getContext().getPointerArg(1);
int length = emulator.getContext().getIntArg(2);
System.out.println("memcpy");
Inspector.inspect(r1.getByteArray(0, length), r1.toString());
return true;
}
});
}

public void hook72bc(){
emulator.attach().addBreakPoint(module.base + 0x72bc, new BreakPointCallback() {
UnidbgPointer v12;
@Override
public boolean onHit(Emulator<?> emulator, long address) {
RegisterContext registerContext = emulator.getContext();
UnidbgPointer a2 = registerContext.getPointerArg(1);
v12 = registerContext.getPointerArg(0);
Inspector.inspect(a2.getByteArray(0, 0x10), "key " + a2.toString());
// 函数结束对v12hook查看
emulator.attach().addBreakPoint(registerContext.getLRPointer().peer, new BreakPointCallback() {
@Override
public boolean onHit(Emulator<?> emulator, long address) {
//Inspector.inspect(v12.getByteArray(0, 176), "Round Key "+v12.toString());
Inspector.inspect(v12.getByteArray(8, 176), "Round Key "+v12.toString());
return true;
}
});
return true;
}
});
}

public static void main(String[] args) {
DouYu douYu = new DouYu();
//douYu.traceLength();
//douYu.hookStrCat();
//douYu.hookMemcpy();
//douYu.hook72bc();
String makeUrl = douYu.getMakeUrl();
System.out.println("result:"+ makeUrl);
}
}

 评论