pwnhub端午节公开赛 writeup
端午节时候pwnhub终于出了一期android逆向题目,抽空去练练手。整体来看是嵌套ELF的玩法,挺不错的。
转载请联系本人,否则作侵权处理。
相关文件链接:https://github.com/LeadroyaL/attachment_repo/tree/master/pwnhub-duanwu (shudu.apk, dec1, dec1.idb, fix.idb, fix.so )
一开始是个数独,先玩一下吧,也没看逻辑,说不定会动态修改什么数据,于是找到一个在线的解题网站 http://www.llang.net/sudoku/calsudoku.html 。
把数独解掉了,有一个输入flag的EditText和check的button。
这时候返回去看一下数独的逻辑,好像就是一个简单的数独,没有存放太多的全局变量,再全部填写完毕而且正确的情况下自己会dismiss,之后绘制一个新的界面、就是我们的check界面了。比较有意思的就是里面大部分的String都被加密处理掉了,在Decode类的某个方法,看起来是自己写的解密,也没仔细看逻辑,就是输入char[]
,输出String的一个解密器。
Java层的逻辑大概就这么多了,先过一个游戏(当然也可以修改smali来直接调用d.setSelectTile()来直接结束游戏显示入口),之后就是正式入口了,没有设置什么陷阱。
JNI的入口是public native String Decode.check(String input, String md5_sign)
看起来是需要签名的md5做什么事情,不要擅自篡改。
这时候随便输入一点东西,点确定,发现我的Nexus5报错了,说签名被修改?拿室友的小米4.4.4也说签名不对?还好我的Nexus 6P没事,说flag错误,如图。。。
查一下为什么会发生这样的事情,这里大概尝试了N个小时为什么4.4.4不能运行,主要逻辑是这样
1 | int ret1 = decrypt(AAA); |
这里尝试了很多方法,最后实在不行patch反调试后,开始debug,结果发现ret1和ret2都是非0的、而且执行到了正确解压的分支,靠!不可能啊啊啊啊,这题有毒啊,我执行解密解压是正确的!我也很绝望啊!所以,我也不知道为什么4.4.4是无法运行的。
好吧,那我们先放下这个问题不管,看一下native的逻辑。
先看init_array,进来就是一个反调试,新开一个线程,不停fork、sleep、kill。
再看看JNI_OnLoad,跳到了0x18D8 JNI_OnLoad_init。
1 | unsigned int __fastcall JNI_OnLoad_init(JNIEnv *env) |
先解密了3个string,再注册方法,八九不离十就是那个Decode.check(),跟过去看一下。
0x1810是JNI_Decode_check的入口,先把字符串拿一下,然后代入0x1628 main_check,第一个参数是input_string,第二个参数是md5,第三个参数是返回的result。
1 | int __fastcall main_check(char *input, char *md5, int ret_string) |
逻辑比较清晰,注释也在代码里写好,挑主要的说,两个难点,一个是解密一堆数据,另一个是解密后的调用。
先看0x1580 decrypt_with_md5 ,输入是待解密数据、长度、密钥,逻辑也比较简单,直接按字节xor即可拿到解密后的数据,后面调用了zlib的uncompress,解密一下可以得到2组数据。
file命令看一下
1 | file dec* |
第一个是ELF文件,第二应该是一段汇编,可能是启动命令吧。手动patch掉,patch的方法是覆盖掉dec1本来的数据,再按P来显示反汇编。
之后就是比较精彩的部分了,代码里调用了 (p_fun2)(&v13);
我们解密后看一下 p_func2
到底是在做些什么。
修好后的fix.so的0x30e8就是要解密后的代码了,看起来是用llvm写的,很丑,其实我也没大看懂,前几句是
1 | int __fastcall sub_30E8(int *p_buf){ |
大概呢就是拿到调用之前的一些数据,总共有3个有用的:
- p_func1,指向那个ELF文件
- const_10000,一个数字0x10000
- p_dlsym,指向之前调用dlsym的地址 其中有个so_main,和mprotect,猜一下意思,是去调用解密出来的ELF里的这个函数,中间那一大堆llvm的东西可能是对字符串的第一次加密,我们暂且先不管。。。
果然在跳出一大堆翔一样的代码后有这么一句
1 | ((void (__fastcall *)(int *, int *, signed int, signed int))v43)(p_buf + 3, v43, v7, -1366112933); |
就是调用so_main,传递的参数暂时还比较迷,没太懂。那么就开始分析so_main吧!
1 | void __fastcall __noreturn so_main(__int64 *input) |
新建了个结构体,也不知道前两个字段是干嘛的
1 | 00000000 SBox struc ; (sizeof=0x108, mappedto_25) ; XREF: test_enc/r |
分析一下,发现是一个RC4的加密,根据输入的内容,以及32位的key,加密后与预期进行对比。
先写个python看一下到底是不是RC4,因为check_success和check_fail都调用了这个解密方法,很容易验证,不过flag解密估计是一堆乱码,毕竟可能2次加密后的结果。
写个py脚本,先验证一下check_success时的结果,再解密一下被加密过的flag。
1 | from Crypto.Cipher import ARC4 |
卧槽?居然有明文?好吧,这样就莫名其妙拿到了flag。。。
我还以为那堆llvm里有第一层加密,结果没有什么卵用,嗯就这样愉快的结束了。。。
最后还有两个疑点:
- 为什么4.4.4不成功,至今不明白,猜不透(搞不懂)
- 那堆llvm代码是干嘛的(懒的看)