端午公开赛Writeup

Posted by Pwnhub on 2017-06-01

各位胖友,大家好!又到了「胖哈勃电台」时间。

刚过完端午的胖友们是不是感觉上班困乏无力呀?没关系,一起来看看胖哈勃端午公开赛的 Writeup ,找回过端午的感觉吧!

端午公开了一道和安卓相关的 Reverse 题,首先感谢一下 @alset 胖友的倾情出题,打破了 Pwnhub 一直没有安卓题的状态,也让我们发现了几个安卓方向的好选手,比如下面要公布的 Writeup 的主人:@LeadroyaL 。(超级棒!)


一开始是个数独,先玩一下吧,也没看逻辑,说不定会动态修改什么数据,于是找到一个在线的解题网站http://www.llang.net/sudoku/calsudoku.html,把数独解掉了,有一个输入flag的框框和check的button。

这时候返回去看一下数独的逻辑,好像就是一个简单的数独,没有存放太多的全局变量,再全部填写完毕而且正确的情况下自己会dismiss,之后绘制一个新的界面、就是我们的check界面了。比较有意思的就是里面大部分的String都被加密处理掉了,在Decode类的某个方法,看起来是自己写的解密,也没仔细看逻辑,就是输入char[],输出String的一个解密器。

Java层的逻辑大概就这么多了,先过一个游戏(当然也可以修改smali来直接调用d.setSelectTile()来直接结束游戏显示入口),之后就是正式入口了。

public native String Decode.check(String input, String md5_sign)看起来是需要签名的md5做什么事情,不要擅自篡改。

这时候随便输入一点东西,点确定,发现我的 Nexus5 报错了,说签名被修改?拿室友的小米 4.4.4 也说签名不对?还好我的 Nexus 6P 没事,说 flag 错误,如图。。。

查一下为什么会发生这样的事情,这里大概尝试了N个小时为什么 4.4.4 不能运行,主要逻辑是这样

int ret1 = decrypt(AAA);
int ret2 = decrypt(BBB);
if(ret1 && ret2){
    xxxxx
}else{
    Log("You changed the signature!");
}

1.jpg

2.jpg

这里尝试了很多方法,最后实在不行 patch 反调试后,开始 debug ,结果发现 ret1 和 ret2 都是非 0 的、而且执行到了 strlen 那句,靠!不可能啊啊啊啊,这题有毒啊,我执行解密解压居然是正确的!我也很绝望啊!所以,我也不知道为什么 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 命令看一下

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

第一个是 ELF 文件,第二应该是一段汇编,可能是启动命令吧。

之后就是比较精彩的部分了,代码里调用了(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);
}

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

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 次加密后的结果。

1
2
3
4
5
6
7
8
9
10
11
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

pwnhub{flag:H0M_pAti3ZCe_ar3_y0V_9uYs!!}

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

3.jpg

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

最后还有两个疑点:

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

关于 @LeadroyaL 的两点疑问,胖哥只能解释第一点…经过和题主 @alset 的沟通,比赛开始时上线的 APK 包存在一点点兼容性问题,后来胖哥在题目中加了说明,希望没有影响到各位选手参赛。

经过这次公开赛,发现了平台上玩安卓的胖友,接下来的比赛,胖哥也会多多找些安卓题,也欢迎各位安卓大神踊跃投稿!

最后,本周末(6/3) Pwnhub 即将上线一道 Web 题,欢迎各位胖友来玩呀!