本文为2016年bctf中的LostFlower的writeup。
LostFlower writeup
首先点我下载题目,直接使用jeb反编译,入口代码如下:
可以看到,Java逻辑十分简单,首先获取用户输入,然后调用Double.parseDouble
将其转为Double类型(这意味着输入数据必须为合法的Double数据),接着将其作为参数传递给native层的stringFromJNI
,如果返回值为6,则调用stringFromJNI2
,其返回值即为Flag。
使用IDA反汇编,结果如下:
可以看到,SO进行了高强度的混淆,加入了大量的while、if等无用指令。
对stringFromJNI
使用f5,奇怪的是,经过对f5伪代码的分析,并没有发现有对用户输入进行校验的地方,难道是程序做了处理,导致f5出现了错误?静态分析解决不了,我们就用动态调试。SO没有做任何反调试处理,而且发现动态调试时,stringFromJNI
的f5可以得到正确的伪代码。
由于汇编代码含有大量的无效跳转,因此我们选择直接在伪代码的基础上进行动态调试,经过几轮动态调试,发现stringFromJNI
包含3处关键代码(即有对输入数据进行判断或处理的地方),如下:
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
| ... v59 = a4; v58 = a3; v67 = _stack_chk_guard; v60 = j_j___aeabi_d2iz(a3, a4); ... else if ( (signed int)v6 > 814185195 ) { if ( v6 == (void *)814185196 ) { v6 = &unk_60130C76; if ( v65 <= 1000000000 ) v6 = (void *)306644219; } } ··· v13 = check1(*v62); v14 = 1; if ( !v13 ) v14 = 0; v63 = v14; v4 = (const char *)-439993678; } else if ( v4 == (const char *)-439993678 ) { v9 = (unsigned __int8)v63; v4 = (const char *)&unk_662944DE; goto LABEL_49; } ...
|
从上面的代码我们可以得出以下3点结论:
stringFromJNI
只对输入Double数据的整数部分做处理。
stringFromJNI
将整数部分和1000000000做比较,根据大小不同,进入不同的分支。
stringFromJNI
调用了check1
,参数为整数部分,根据返回值是否为1,进入不同的分支。
现在我们开始脑洞一番,一般情况下,我们测试的时候,不会输入一个大于1000000000的数据,基于这一点我们猜测输入数据应大于1000000000;程序员的逻辑中,返回1是真,0是假,因此我们猜测check1
的返回应为1。现在我们测试一番,首先在check1的后面一行下断点,接着输入一个大于1000000000的数据,如1000000001,点击提交按钮,程序断下来以后,可以看到r0为0,修改r0为1,最后让程序继续运行,可以发现,程序成功打印出了Flag,然而由于我们是直接修改的返回值,而输入依然是错误的,自然这个Flag也是错误的。不过,可以确定的是我们的猜测是正确的,即输入数据应大于1000000000并且check1
的返回值为1。因此现在的关键即为分析check1
,同样在动态分析时,对check1
进行f5得到伪代码,check1
的逻辑要更加简单一点,我们可以直接采用逆推的方法,也就是从出口(return)开始,往函数入口方向逆向推理的过程。逆推过程这里不再详述,只需要注意以下3点即可加快效率:
- 不要在意大于或小于的比较,我们只需要关注等于时的分支。
- 在IDA里,你可以选择一个值,此时所有这个值都会标黄,从而可以快速找到下一个值。
- 我们可以直接在IDA f5伪代码里下断点,也就是说可以直接以伪代码级别调试。
给出逆推的结果,代码如下:
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
| ... if ( v2 == (void *)-427046904 ) { LABEL_15: v2 = (void *)196778168; LOBYTE(v1) = 1; } ... } if ( (signed int)v2 > -109345567 ) break; if ( v2 == (void *)-242797719 ) goto LABEL_28; } if ( (signed int)v2 > -32671955 ) break; if ( v2 == (void *)-109345566 ) { sub_7509FAA4(v17 - 631823485 - v18 + 631823485); LABEL_28: v2 = (void *)56101389; } } if ( (signed int)v2 > 32634615 ) break; if ( v2 == (void *)-32671954 ) { v5 = j_j___aeabi_i2d(v3); v7 = j_j_pow_0(0, 1076101120, v5, v6); v9 = j_j___aeabi_d2iz(v7, v8); v10 = j_j___aeabi_idiv(v17, v9); v11 = j_j___modsi3(v10, 10); v12 = my_pow(v11); v18 = -(-v18 - v12); v2 = (void *)142850058; } } if ( (signed int)v2 > 56101388 ) break; ... if ( v2 == (void *)32634616 ) { v2 = (void *)-427046904; v4 = -1611885754; if ( !v29 ) goto LABEL_35; } ... if ( v2 == (void *)56101389 ) { v13 = sub_7509FAA4((char *)&unk_59357062 + v17 - v18 - 0x59357062); v14 = 1; if ( v13 >= 0 ) v14 = 0; v29 = v14; v2 = (void *)32634616; } ... return v1; ...
|
从上面注释可以知道,也就是需要sub_7509FAA4((char *)&unk_59357062 + v17 - v18 - 0x59357062);
返回负数,现在我们需要做的就是搞清v17和v18的值是什么,以及sub_7509FAA4
的逻辑。关键代码如下:
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
| ... v17 = a1; ... if ( v2 == &unk_4A2AC114 ) { v2 = (void *)-32671954; v4 = -242797719; if ( v3 >= 10 ) LABEL_35: v2 = (void *)v4; } ... if ( v2 == (void *)-1521320686 ) { ++v3; v2 = (void *)-1898177542; } ... if ( v2 == (void *)-32671954 ) { v5 = j_j___aeabi_i2d(v3); v7 = j_j_pow_0(0, 1076101120, v5, v6); v9 = j_j___aeabi_d2iz(v7, v8); v10 = j_j___aeabi_idiv(v17, v9); v11 = j_j___modsi3(v10, 10); v12 = my_pow(v11); v18 = -(-v18 - v12); v2 = (void *)142850058; } ...
|
v17为用户输入的Double数据的整数部分,v18在for循环里完成了赋值,那么这个for循环到底干了什么?我们先看前面几行代码:
1 2 3 4 5
| v5 = j_j___aeabi_i2d(v3); v7 = j_j_pow_0(0, 1076101120, v5, v6); v9 = j_j___aeabi_d2iz(v7, v8); v10 = j_j___aeabi_idiv(v17, v9); v11 = j_j___modsi3(v10, 10);
|
这里几个函数都是一些除法、求余的操作。经过几轮动态调试,事实上,这个for循环的前五行代码就是逆向取出一个10位整数的每一位。举个例子,输入为1234567890,每一轮得到的依次为0,9,8,7,6,5,4,3,2,1。
现在还剩下一个my_pow
,顾名思义这是个幂相关的函数。注意我们不需要去具体分析my_pow
,因为我们的输入只有0-9这10种可能,几轮测试过后,得到结论:my_pow
返回输入数据的十次幂。
用C代码重现下这个for循环:
1 2 3 4 5 6 7 8 9 10 11
| int i; int integer; int out; out = 0; for (i = 0; i < 10; ++i) { int x; x = integer / pow(10, i) % 10; out += pwo(x, 10); }
|
ok,现在sub_7509FAA4((char *)&unk_59357062 + v17 - v18 - 0x59357062);
中,我们知道v17为用户输入数据的整数部分,v18为上面的out,剩下的是分析sub_7509FAA4
的功能,f5得到伪代码,代码很短,而且逻辑也很简单,逻辑是这样的:如果参数(v17 - v18)为非负数直接返回该参数,如果参数为负数则求补之后返回。
这里遇到一个问题,由上面分析可知,需要sub_7509FAA4
返回负数,但是按照该函数逻辑,无论如何都会返回一个非负数!什么情况?经过1个小时的重新分析,排除了前面分析错误的情况,那么sub_7509FAA4
存在溢出?这个时候看伪代码已经没用了,通过对sub_7509FAA4
的汇编代码的分析,发现了溢出点:NEGS R1, R1
,当参数为负数时,程序使用NEGS
指令求补后返回,其中NEGS
的作用是这样的:将目的操作数的所有数据位取反加1。当参数为0x80000000(这是个负数)时,所有数据位取反后为0x8fffffff,再加1后发生溢出,最后值为0x80000000。也就是说,0x80000000经过NEGS
后仍然为0x80000000。
终上所述,现在给出结论:
- 输入数据为一个合法的十位Double数据,设其整数部分为integer。
- 对integer的每一位求10次幂,并全部加起来,结果为sum。
- integer -sum == 0x80000000。
最后给出计算输入的程序:
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
| int main() { int pow_table[] = {0, 1, 0x400, 0xe6a9, 0x100000, 0x9502f9, 0x39aa400, 0x10d63af1, 0x40000000, 0xcfd41b91}; int i; for (i = 1000000000; i <= 0x80000000; ++i) { int a0 = i % 10; int a1 = (i % 100) / 10; int a2 = (i % 1000) / 100; int a3 = (i % 10000) / 1000; int a4 = (i % 100000) / 10000; int a5 = (i % 1000000) / 100000; int a6 = (i % 10000000) / 1000000; int a7 = (i % 100000000) / 10000000; int a8 = (i % 1000000000) / 100000000; int a9 = i / 1000000000; int sum = pow_table[a0] + pow_table[a1] + pow_table[a2] + pow_table[a3] + pow_table[a4] + pow_table[a5] + pow_table[a6] + pow_table[a7] + pow_table[a8] + pow_table[a9]; if (((i - sum) & 0xffffffff) == 0x80000000) { printf("ok, %d\n", i); break; } if(i % 1000000 == 0) { printf("%d\n",i); } } return 0; }
|
程序跑一会就出来了,输入为1422445956
,最后的Flag为:BCTF{wrhav3f4nwxo}
。