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
2
3
4
5
6
7
int ret1 = decrypt(AAA);
int ret2 = decrypt(BBB);
if(ret1 && ret2){
xxxxx
}else{
Log("You changed the signature!");
}

这里尝试了很多方法,最后实在不行patch反调试后,开始debug,结果发现ret1和ret2都是非0的、而且执行到了正确解压的分支,靠!不可能啊啊啊啊,这题有毒啊,我执行解密解压是正确的!我也很绝望啊!所以,我也不知道为什么4.4.4是无法运行的。

好吧,那我们先放下这个问题不管,看一下native的逻辑。

先看init_array,进来就是一个反调试,新开一个线程,不停fork、sleep、kill。

再看看JNI_OnLoad,跳到了0x18D8 JNI_OnLoad_init。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
unsigned int __fastcall JNI_OnLoad_init(JNIEnv *env)
{
decode_string(env, &unk_5150, 25, 38, &res_1);
decode_string(env, &unk_516A, 5, 97, &res_2);
decode_string(env, &unk_5170, 56, 255, &res_3);
p_obj = &res_2;
p_type = &res_3;
p_function = JNI_decode_check;
clazz = ((*env)->FindClass)(env, &res_1);
if ( clazz )
result = (((*env)->RegisterNatives)(env, clazz, &p_obj, 1) >> 31) ^ 1;
else
result = 0;
return result;
}

先解密了3个string,再注册方法,八九不离十就是那个Decode.check(),跟过去看一下。

0x1810是JNI_Decode_check的入口,先把字符串拿一下,然后代入0x1628 main_check,第一个参数是input_string,第二个参数是md5,第三个参数是返回的result。

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
int __fastcall main_check(char *input, char *md5, int ret_string)
{

pipe(pipedes);
*dl_name = 0x909A858D;
*&dl_name[4] = 0x84;
*&dl_name[8] = 0;
//解密后是 "dysym"
if ( !fork()
(pipes(pipedes[1]),
v6 = read(pipedes[0], &v13, 0x100u),
*(&v13 + v6) = 0,
_aeabi_memcpy(ret_string, &v13, v6 + 1),
result = _stack_chk_guard - v19,
_stack_chk_guard != v19) )
//这里大概是多进程通讯返回result
{
ptrace(0);
pipes(pipedes[0]);
p_fun1 = decrypt_with_md5(decrypt_fun1, 0x16F7u, md5);
p_fun2 = decrypt_with_md5(decrypt_fun2, 0x90Fu, md5);
//这里解密了2个函数
if ( p_fun1 && p_fun2 )
{
if ( strlen(dl_name) )
{
dl_name[0] = 100;
if ( strlen(dl_name) >= 2 )
{
i = 1;
do
{
dl_name[i] ^= 0xE9u;
++i;
}
while ( i < strlen(dl_name) );
}
}
v13 = p_fun1;
para1 = 0x100000;
para2 = dlsym(-1, dl_name);
para3 = input;
//注意这里!!!
v17 = pipedes;
(p_fun2)(&v13);
//注意这里!!!
exit(0);
}
write(pipedes[1], "You changed the signature!", 0x1Au);
exit(1);
}
return result;
}

逻辑比较清晰,注释也在代码里写好,挑主要的说,两个难点,一个是解密一堆数据,另一个是解密后的调用。

先看0x1580 decrypt_with_md5 ,输入是待解密数据、长度、密钥,逻辑也比较简单,直接按字节xor即可拿到解密后的数据,后面调用了zlib的uncompress,解密一下可以得到2组数据。

file命令看一下

1
2
3
file dec*
dec1: ELF 32-bit LSB shared object, ARM, EABI5 version 1 (SYSV), dynamically linked, stripped
dec2: SysEx File -

第一个是ELF文件,第二应该是一段汇编,可能是启动命令吧。手动patch掉,patch的方法是覆盖掉dec1本来的数据,再按P来显示反汇编。

之后就是比较精彩的部分了,代码里调用了 (p_fun2)(&v13); 我们解密后看一下 p_func2 到底是在做些什么。

修好后的fix.so的0x30e8就是要解密后的代码了,看起来是用llvm写的,很丑,其实我也没大看懂,前几句是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int __fastcall sub_30E8(int *p_buf){
p_fun1 = *p_buf;
const_10000 = p_buf[1];
p_dlsym = p_buf[2];
v53 = 0;
v52 = 0;
v51 = 'nia';
v49 = 0;
v50 = 'm_os';
ii = 0;
v48 = 0;
v47 = 'tcet';
mprotect = 'orpm';
p_mprotect = (void *)((int (__fastcall *)(signed int, int *))p_dlsym)(-1, &mprotect);
((void (__fastcall *)(int, int, signed int))p_mprotect)(p_fun1, const_10000, 7);
v3 = 0x701021AC;
v4 = 0x80B66581;
v5 = 0x85B7D48F;
}

大概呢就是拿到调用之前的一些数据,总共有3个有用的:

  1. p_func1,指向那个ELF文件
  2. const_10000,一个数字0x10000
  3. 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void __fastcall __noreturn so_main(__int64 *input)
{
__int64 p_input; // kr00_8@1
signed int input_len; // r5@1
int enc_input; // [sp+0h] [bp-214h]@3
char enc_key[276]; // [sp+100h] [bp-114h]@3

p_input = *input;
dword_4004 = (unsigned __int64)*input >> 32;
ptrace(0);
input_len = strlen((const char *)p_input) + 1;
if ( input_len > 256 )
check_fail();
init_key_rc4((SBox *)enc_key, "pwnhub{this_is_not_flag_lolzzzzzzz}", 32);
enc_or_dec_data(enc_key, input_len, (char *)p_input, &enc_input);
if ( !memcmp(&enc_input, target, input_len) )
check_success();
else
check_fail();
exit(0);
}

新建了个结构体,也不知道前两个字段是干嘛的

1
2
3
4
5
00000000 SBox struc ; (sizeof=0x108, mappedto_25) ; XREF: test_enc/r
00000000 field_0 DCD ?
00000004 field_4 DCD ?
00000008 box256 DCB 256 dup(?)
00000108 SBox ends

分析一下,发现是一个RC4的加密,根据输入的内容,以及32位的key,加密后与预期进行对比。

先写个python看一下到底是不是RC4,因为check_success和check_fail都调用了这个解密方法,很容易验证,不过flag解密估计是一堆乱码,毕竟可能2次加密后的结果。

写个py脚本,先验证一下check_success时的结果,再解密一下被加密过的flag。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from Crypto.Cipher import ARC4

rc4_key = "pwnhub{this_is_not_flag_lolzzzzzzz}"[0:32]
target = [0xD8, 0x77, 0x7D, 0xEC, 0x14, 0xFB, 0x60, 0x22, 0x45, 0xF8, 0x9F, 0xEB, 0x81, 0xBA, 0x36, 0xCA, 0x6B, 0x67,
0x80, 0x51, 0xE1, 0xF4, 0xC6, 5, 0x22, 0xA, 0xDC, 0x22, 0x33, 0xD, 0xE, 0x2B, 0x4A, 0x7E]
target = [0xEB, 0x6F, 0x7D, 0xE3, 0x13, 0xF8, 0x6F, 0x31, 0x45, 0xF8, 0x8C, 0xB8, 0xA6, 0xE4, 8, 0xB5, 0x62, 0x49, 0x81,
0x18, 0xB5, 0xC1, 0xF1, 0x40, 9, 3, 0xCB, 0x31, 0xA, 0x18, 0x5F, 0x1A, 0x34, 0x47, 0xB4, 0xF2, 0x45, 0x10,
0x5E, 0x7F, 0x1B, 0, ]
cipher = ARC4.new(rc4_key)
rc4_dec = cipher.decrypt(''.join(chr(i) for i in target))
print rc4_dec.encode('hex')
print rc4_dec
# 436f6e67726174756c6174696f6e7320796f7520676f742074686520666c61672100
# Congratulations you got the flag!
# 70776e6875627b666c61673a48304d5f70417469335a43655f6172335f7930565f3975597321217d0096
# pwnhub{flag:H0M_pAti3ZCe_ar3_y0V_9uYs!!}

卧槽?居然有明文?好吧,这样就莫名其妙拿到了flag。。。

我还以为那堆llvm里有第一层加密,结果没有什么卵用,嗯就这样愉快的结束了。。。

最后还有两个疑点:

  1. 为什么4.4.4不成功,至今不明白,猜不透(搞不懂)
  2. 那堆llvm代码是干嘛的(懒的看)