看雪2019CTF-变形金刚WP
前言
之前一直没有系统整理Android逆向方面的内容,这次借着这道题备份一下相关内容。
主要包括安卓动态调试,环境模拟等
Java层坑点
Apk
在这里MainActivity
拓展的并不是常见的AppCompatActivity
,而是出题人自己编写的AppCompiatActivity
。
关键在于出题人在MainActivity
中定义onCreate(),然后在AppCompiatActivity
中定义onStart()。带来的效果是onCreate()先执行然后被onStart()所覆盖。于是真正执行的逻辑位于AppCompatActivity
中而非障眼法MainActivity
。
Native层坑点
核心的密码校验位于liboo000oo.so
,但是打开之后导出的函数只有两个:
查看.init_array
发现:
于是查看做了什么初始化操作:
对于库内.data
段内数据进行异或还原,这里实际上是解密了字符串。但在还不清楚的情况下可以自行进行还原。
加密还原
Radare2
Radare2对于这种情况有专用指令wox
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| Usage: wo[asmdxoArl24] [hexpairs] @ addr[!bsize] | wo[24aAdlmorwx] without hexpair values, clipboard is used | wo2 [val] 2= 2 byte endian swap | wo4 [val] 4= 4 byte endian swap | woa [val] += addition (f.ex: woa 0102) | woA [val] &= and | wod [val] /= divide | woD[algo] [key] [IV] decrypt current block with given algo and key | woe [from to] [step] [wsz=1] .. create sequence | woE [algo] [key] [IV] encrypt current block with given algo and key | wol [val] <<= shift left | wom [val] *= multiply | woo [val] |= or | wop[DO] [arg] De Bruijn Patterns | wor [val] >>= shift right | woR random bytes (alias for 'wr $b') | wos [val] -= substraction | wow [val] == write looped value (alias for 'wb') | wox [val] ^= xor (f.ex: wox 0x90)
|
意思是write over,加上x之后就是对相应的地址进行异或处理,注意在r2的命令行参数中要加上-w
参数来进行写入。
命令样例wox 0xa5 @ 0x4050!0x42
:其中0xa5
是进行异或的值,感叹号之后跟的是重复次数。
在理解解密流程的基础上不断重复就行了,要查看的话用px 0x25 @ 0x4020
,不再解释。
QEMU
使用QEMU也可以模拟arm环境让它自己进行解密,使用了别人已经做好兼容的项目环境:
https://github.com/AeonLucid/AndroidNativeEmu
1 2 3 4 5 6 7 8 9 10 11 12
| from unicorn import UC_HOOK_CODE from unicorn.arm_const import * from androidemu.emulator import Emulator
emulator = Emulator() lib_module = emulator.load_library("liboo000oo.so")
emulator.call_symbol(lib_module, '.datadiv_decode5009363700628197108')
cont = emulator.mu.mem_read( 0xcbbcb000+ 0x4010,0xd5) print(''.join('{:02x} '.format(x) for x in cont))
|
核心代码部分如上,执行了初始化函数,之后按照计算好的偏移dump出对应的数据,保存之后覆写即可。
当然自带的库中还有更多的方便函数可以尝试。
1 2 3 4 5 6 7 8 9 10 11 12
| => Undefined external symbol: __cxa_finalize => Undefined external symbol: __cxa_atexit => Undefined external symbol: __aeabi_memclr => Undefined external symbol: __aeabi_memcpy8 => Undefined external symbol: __stack_chk_fail => Undefined external symbol: __stack_chk_guard => Undefined external symbol: malloc => Undefined external symbol: strlen => Undefined external symbol: raise => Undefined external symbol: abort => Undefined external symbol: memcpy a0 f0 bc cb ca f0 bc cb d0 f0 bc cb 85 b7 bc cb 36 35 30 66 39 30 39 63 2d 37 32 31 37 2d 33 36 34 37 2d 39 33 33 31 2d 63 38 32 64 66 38 62 39 38 65 39 38 00 00 00 00 00 00 00 00 00 00 00 00 21 3a 23 24 25 26 28 29 2b 2d 2a 2f 60 7e 5f 5b 5d 7b 7d 3f 3c 3e 2c 2e 40 5e 61 62 63 64 65 66 67 68 69 6a 6b 6c 6d 6e 6f 70 71 72 73 74 75 76 77 78 79 7a 30 31 32 33 34 35 36 37 38 39 5c 27 3b 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 61 6e 64 72 6f 69 64 2f 73 75 70 70 6f 72 74 2f 76 37 2f 61 70 70 2f 41 70 70 43 6f 6d 70 69 61 74 41 63 74 69 76 69 74 79 00 65 71 00 00 00 00 28 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69 6e 67 3b 29 5a
|
这些是回显。
函数动态注册
解密完成,但是没有eq函数的话程序是怎么执行的?
答案是出题人在JNI_OnLoad函数中动态注册,通过RegisterNatives
函数:
该函数的签名如下:
1
| ``art::JNI::RegisterNatives(_JNIEnv*, _jclass*, JNINativeMethod const*, int)
|
其中第三个参数是实际注册的Native方法,查看安卓源代码如下:
1 2 3 4 5 6
| typedef struct { const char* name; const char* signature; void* fnPtr; } JNINativeMethod;
|
分别是函数名,函数签名和函数指针,都是CString和C风格函数指针,对应在题目中就位于off_4014
上。
查看对应的结构题,第一个0xCBBCF0CA对应函数名,可以看到是eq没有问题(注意之前QEMU部分提到过的偏移计算)。那么对应的第三个地址的位置就是eq函数的实际所在了。
Frida
在没有查明结构题定义之前绕了弯路,但是也学到了Frida这个工具。 Frida类似于pin之于x86调试,只不过实际的函数定义使用javascript,有些不适应。
Frida支持直接附加安卓程序进程,也可以从Frida进行创建之后先行hook再恢复程序执行。但是在直接root的小米5x上一旦调用spawn就会卡顿重启,于是只好刷机换成更加原生的系统。感谢室友和XDA论坛的帮助,先刷入TWRP,再三清之后刷入Pixel Experience这个ROM,还要刷入ROM提供的包来避免启动黑屏,最后刷入Magisk管理root。
至此安卓9.0原生开发机准备完毕!
Frida的阅读资料推荐这两篇
https://www.slideshare.net/ssusercf6665/frida-107244825
https://www.frida.re/docs/javascript-api/#memory
实际工作是预先在安卓端部署frida server在/data/local/tmp,赋予755权限之后执行。电脑端就可以通过CLI或者Python进行交互,Python中获取usb连接设备然后从Frida创建进程,附加,执行js脚本完成hook,最后恢复进程执行。
实际脚本中的函数签名参考开发机中的libart.so
进行过调整,使用objdump -t
工具即可看到mangled之后的函数名。之后使用Frida提供的objdump
函数打印对应地址数据,坑点在于arm体系结构中有ARM指令集和Thumb指令集2种,其中ARM指令为32位指令,按照4字节对齐存储,一条指令必须从4的整数倍地址来取;Thumb指令为16位指令,按2字节对齐存储,一条指令必须从偶数地址来取。
而结构体中的地址末尾是5,在执行时会自动对齐到4,所以直接dump出的指令数据有缺失所以无法正常解析!
Frida script
运行结果如下:
1 2 3 4 5 6 7 8 9 10 11 12
| frida % python ./native.py fx-ti@DELL [*] Running CTF RegisterNativeMethods is at 0xe9869241 android.support.v7.app.AppCompiatActivity ceab1785 b5 03 af 2d e9 00 0f ad f5 07 7d be 49 79 44 09 ...-......}.IyD. ceab1795 68 03 91 09 68 86 91 01 68 d1 f8 a4 32 11 46 00 h...h...h...2.F. ceab17a5 22 98 47 05 90 b9 48 78 44 ff f7 84 ef 82 46 ff ".G...HxD.....F. ceab17b5 f7 86 ef 06 46 50 46 ff f7 82 ef 80 46 50 46 ff ....FPF.....FPF. ceab17c5 f7 7e ef 83 46 30 46 51 46 ff f7 80 ef 40 46 51 .~..F0FQF....@FQ ceab17d5 46 ff f7 7c ef 58 46 51 46 ff f7 78 ef ba f1 00 F..|.XFQF..x.... ceab17e5 0f 46 d0 aa 49 00 20 52 46 79 44 0b 78 01 31 2d .F..I. RFyD.x.1- ceab17f5 2b 1c bf 08 f8 00 30 01 30 01 3a f6 d1 01 28 37 +.....0.0.:...(7
|
dump出了对应位置的函数数据。
之后尝试过的GDB动态调试选为备用,可以参考这篇
https://blog.csdn.net/zhu929033262/article/details/76064044
更好的IDA动态调试可以参考这篇,重点推荐,可能会买本书看看
https://blog.csdn.net/jiangwei0910410003/article/details/82812941
总结
题目实际解法参考这个,变形RC4+Base64
https://bbs.pediy.com/thread-250376.htm
一道题目走完了Frida,QEMU,GDB,IDA,感觉学到了不少,新发布的JEB3.0新加入native层反编译也很强。好想学编译啊!