本文距离上次更新已过去 0 天,部分内容可能已经过时,请注意甄别。
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 (); } }
运行结果 报了一个 ”不合法的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(); dm.callJNI_OnLoad(emulator); } public String getMakeUrl () { List<Object> list = new ArrayList <>(10 ); list.add(vm.getJNIEnv()); 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 ); 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); } }
运行后直接出结果 可以发现,结果由四部分组成,前三个参数是我们 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 ); }
运行计数总共九十一万行, 不超过100w行的执行流,要么程序没怎么混淆,要么逻辑不 太复杂。两者任意一个复杂度高一些,都不会只有100w行汇编以内。 使用findcrypt插件再确认一下样本大概使用了哪些加密算法 SO中至少存在 AES/BASE64,至于我们的函数中用了什么?这得具体分析,毕竟Findcrypt只是一个静态 的、加密特征匹配插件。
目标函数可能用了AES/Base64,说“可能”是上述算法可能用于SO中其他函数而非目标函数。
目标函数可能用了AES和Base64之外的其他加密算法,因为FIndCrypt提供了静态的、有限的分析,很容易遗漏。
使用Unidbg处理算法,一般而言,自下而上分析更省时省力,这得益于Unidbg两方面的能力
这让我们可以逆流而上,自结果推来源,分析算法和数据块十分轻松。 重新看运行结果图 运算结果来自于NewStringUTF,这个JString从哪里来的? 日志提示调用处在0x336f,这个地址实际上是LR(返回地址),所以NewStringUTF函数调用是 0x336f 的上一条 0x336C。 在 0x336C 下断点 回顾一下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 );
从下往上寻找对内存最晚的操作,可以发现,这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 逐步拼接的结果 对来源 0xbffff69b 做traceWrite,千万记得加后缀L。 发现依然来自于libc,这不是好事,说明我们还没到第一现场。 从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 ; } }); }
运行发现,程序逻辑上逐两个字节进行拷贝 140处调用。看的头疼,所以我尝试性的搜索了下3c179e17e8e9b06d7b18c68555b92220,期待某次 memcpy可以看到它,那么我们就能找到它的产生之处了。 打印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&cli0010 : 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=1673232015v0030 : 71 34 37 48 64 39 4A 55 67 66 44 43 79 74 43 q47Hd9JUgfDCytC
vq47Hd9JUgfDCytC 这十个字节是未知的,其余三个字段是传进来的,我们结合上面的MD5会产生一种 明悟,这不就是加盐MD5吗?传进来的参数拼接后加上”vq47Hd9JUgfDCytC“,MD5后传出去。 那么现在问题就变成了,vq47Hd9JUgfDCytC是哪里来的? 对0xbffff500L 做traceWrite
ida打开libmakeurl.so,看一下来源0x8a88,十六个字节都来自这里
前面我们用过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)
参数2像是buffer,存放加密结果。blr用于在函数返回处下断点,然后c继续跑,在函数运行结束后再次 查看参数2指向的内存。
可以发现确实是我们要分析的十六个字节。至于参数1是什么意思,硬看似乎看不出来。 因此可以判断,sub_8228生成了我们要分析的十六个字节,而且它像AES的执行逻辑。 AES 加密还是解密?什么工作模式?明文是什么?Key是什么?一概不知。我们得到sub_8228上层去看看。 重新运行程序,bt 打印调用栈 跳到 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; int v7; int v8; signed int v10; signed int v11; char v12[280 ]; int v13; 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 进行断点查看参数 密钥是30292827262524232221000000000000,暂时不知道是加密还是解密,结果是 767134374864394a5567664443797443(vq47Hd9JUgfDCytC) 看看加密过程对的上吗 因为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()); 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()); emulator.attach().addBreakPoint(registerContext.getLRPointer().peer, new BreakPointCallback () { @Override public boolean onHit (Emulator<?> emulator, long address) { 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; int v3; int i; int v6; int v7; _QWORD v8[2 ]; char v9[20 ]; _QWORD v10[2 ]; char v11[20 ]; int v12; 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 , 0x100 u); 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:0004568 A A4 DCB 0xA4 .rodata:0004568B 42 DCB 0x42 ; B .rodata:0004568 C 23 DCB 0x23 ; # .rodata:0004568 D 4F DCB 0x4F ; O .rodata:0004568 E 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 0 D DCB 0xD .rodata:00045694 4F DCB 0x4F ; O .rodata:00045695 68 DCB 0x68 ; h .rodata:00045696 5 C 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:00045 CF0 10 unk_45CF0 DCB 0x10 ; DATA XREF: sub_A298+24 ↑o .rodata:00045 CF0 ; sub_A298+2 C↑o .rodata:00045 CF0 ; .text:off_A428↑o .rodata:00045 CF0 ; sub_A430+50 ↑o .rodata:00045 CF0 ; sub_A430+5 E↑o .rodata:00045 CF0 ; .text:off_A5D0↑o .rodata:00045 CF0 ; sub_A5D8+4 E↑o .rodata:00045 CF0 ; sub_A5D8+5 C↑o .rodata:00045 CF0 ; .text:off_A770↑o .rodata:00045 CF0 ; sub_A778+4 E↑o .rodata:00045 CF0 ; sub_A778+5 C↑o .rodata:00045 CF0 ; .text:off_A910↑o .rodata:00045 CF0 ; sub_A918+50 ↑o .rodata:00045 CF0 ; sub_A918+5 E↑o .rodata:00045 CF0 ; .text:off_AAA4↑o ... .rodata:00045 CF1 09 DCB 9 .rodata:00045 CF2 08 DCB 8 .rodata:00045 CF3 07 DCB 7 .rodata:00045 CF4 06 DCB 6 .rodata:00045 CF5 05 DCB 5 .rodata:00045 CF6 04 DCB 4 .rodata:00045 CF7 03 DCB 3 .rodata:00045 CF8 02 unk_45CF8 DCB 2 .rodata:00045 CF9 01 DCB 1 .rodata:00045 CFA 20 DCB 0x20 .rodata:00045 CFB 20 DCB 0x20 .rodata:00045 CFC 20 DCB 0x20 .rodata:00045 CFD 20 DCB 0x20 .rodata:00045 CFE 20 DCB 0x20 .rodata:00045 CFF 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,而异或的另外一方,因为不存在未知数,编译器会直接优化计算出结果
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); emulator.attach().addBreakPoint(module .base + 0x8B3C ); } public String getMakeUrl () { List<Object> list = new ArrayList <>(10 ); list.add(vm.getJNIEnv()); 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 ); 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()); emulator.attach().addBreakPoint(registerContext.getLRPointer().peer, new BreakPointCallback () { @Override public boolean onHit (Emulator<?> emulator, long address) { Inspector.inspect(v12.getByteArray(8 , 176 ), "Round Key " +v12.toString()); return true ; } }); return true ; } }); } public static void main (String[] args) { DouYu douYu = new DouYu (); String makeUrl = douYu.getMakeUrl(); System.out.println("result:" + makeUrl); } }