概述
题目入口:http://ctf.pediy.com/game-fight-36.htm
本题是安卓cm,目测肯定需要调试so。
准备工具:
- ApkIde改之理(其他类似的也行,能够反编译apk,得到jar,so等)
- IDA(用于调试so),需要6.x以上,忘了是x几,我用的6.6
- adb(ApkIde改之理就有)
反编译
将6-Ericky kanxue.apk拖进ApkIDE改之理,等待编译(没有加壳),ok。
在右侧树结构栏中,找到smali->android->com->miss->rfchen,列表中就是java层的主要函数。
点击MainActivity.smali,然后点击工具栏中jd-gui.exe,抓到java源码查看。
1 |
|
这混淆的函数名我也是醉了,但这都不重要。输入key之后,然后点击按钮,进入OnClick,调用了上面代码中第二个函数(什么?我怎么知道的,因为它们哪个…点号…的函数名相同!!)。
然后调用了utils.check来验证,成功提示!这里成功和错误提示的字符串做过变换,通过utils.dbcb解密,不细看了,不重要!
进入utils.java,看到加载了so,调用的是这个so的导出函数,看反编译目录lib/armeabi-v7a(只提供了arm的so,要有个x86的好了),知道这个so是librf-chen.so。
1 | //典型的NDK调用,查查就知道了! |
那么重点来了,要分析librf-chen.so的check函数,才能搞定此题。
准备调试
早上提前学习了一下so调试方法,找到了看雪安卓大神的教程,就是参考中的IDA动态调试技术,然后用上了,很好用!
跟着走
下面开始照着做。
- 连上手机(或者模拟器),使用adb devices看看成功连上没有
- adb push ../dbgsrv/android_server /sdcard/sv,教程是直接放入/data/data,一般权限不够
- 然后进入shell,adb shell,输入su,获得root权限,然后cp /sdcard/sv /data/data/sv
- 修改sv权限,chmod 777 /data/data/sv
- 运行sv,/data/data/sv,默认监听到23946端口,Listening on port #23946。这步有个细节,不能直接adb shell /data/data/sv,这样权限不够,无法读取到进程信息,需要adb shell; su; /data/data/sv
- 再开一个cmd,然后运行adb forward tcp:23946 tcp:23946
- 运行一个idaq.exe,然后在菜单debugger->attach->remote Armlinux/android debugger,输入localhost, 23946,ok
- 弹出进程框,按下Alt+T,输入chen,搜索到1808 [32] com.miss.rfchen,ok
- F9运行
1 | \ApkIDEz> .\adb.exe shell |
在界面中输入key,然后点击按钮,此时librf-chen.so才加载,然后ctrl+s,alt+t,输入librf找到librf-chen.so的基地址信息(记为base),记下来。
用另一个ida打开librf-chen.so,找到check导出函数的偏移地址00002814,计算base+00002814,然后g在IDA调试器中输入该地址,加上断点。
1 | check(_JNIEnv *,_jclass *,_jstring *) 00002814 |
IDA基本调试快捷键和OD一样:1
2
3F9: 运行
F8: 步过
F7:步入
F9,跑起来,然后再次点击按钮,就断下来,进入了check。
下面就是跟和调试的过程了,看数据,看流程,分析算法!
arm汇编基础
得提前有个准备,看看arm指令,了解基本的指令,函数调用方式,下面列几个,更多的就看参考中的文章了
1 | MOVS 同x86的mov |
然后最主要的,函数调用的参数传递。arm默认使用的fastcall,通过r0,r1,r2,r3传递参数,超过4个参数,使用堆栈传递,r0也保存返回值。
关键点跟踪
在check断下之后,先是一段数据初始化,先滤过,然后blt sub_2874,进入关键函数
然后看到通过MOVS,STR将一些字符放入了内存。
1 | .text:0000288A 000 01 60 STR R1, [R0] |
接着就看让我恐惧的一幕,b loc_2898开始各种跳转,指令操作,然后刚跳完又是一个b xxx,接着各种跳转,毫无疑问,这是一段花指令了。
1 | .text:0000289E B loc_2898 |
花指令结构
经过多次跟踪,恶心到快吐的时候,终于看出话指令的基本结构了:
1 | .text:00002BE8 PUSH.W {R4-R10,LR} |
特征:
- 每跳转一个分支,基本都要一段花(记为A段),就是从上面代码中注释开始的问题
- 进行几个跳转后,到了结束位置,跳入有效代码
- 有效代码开头一般也有加一段花(记为B段)
- 在A段话指令中,指令地址是向下增长的,也就是A开始往下拉一段,就能找到结束位置
- B端一般无跳转,但是对称代码有多又少
所以根据特征,去除话指令也挺方便,我使用的IDA的patch功能手工去花的,脚本牛可以写个脚本。
所有花指令填充的00 bf(NOP),然后就可以F5了。
关键点跟踪2
然后接着调试跟踪。
接着上面,后续会接着向该段内存填充字符(非直接填充,还有个段算法,根据初始话的0x20的值来做的),我没有仔细跟踪算法了,通过对些内存关键点下断,然后跳出循环位置下断,下面0000357A就是循环位置,如此多次之后,循环结束。1
2.text:00003576 000 B4 F1 FF 3F CMP.W R4, #0xFFFFFFFF
.text:0000357A 000 3F F7 74 AD BGT.W loc_3066
查看该内存数据:1
25F019020 4A 00 79 00 75 00 33 00 43 00 4A 00 6C 00 56 00 J.y.u.3.C.J.l.V.
5F019030 44 00 53 00 47 00 51 00 21 00 0A 00 00 00 00 00 D.S.G.Q.!.......
接着跳过一段花之后,调用了bl sub_19FC,跟入,发现结果和刚才那段基本一直,也是将字符写入内存,并且内存就是刚才那段,只是每次都有一个1偏移。1
2
3
4
5
6.text:0000364A 000 FE F7 D7 F9 BL sub_19FC
...
librf_chen.so:5EFFB52E ORR.W R3, LR, R2,LSL#1
librf_chen.so:5EFFB532 LDRB.W R0, [R8,R5,LSL#1]
librf_chen.so:5EFFB536 ADDS R2, #1
librf_chen.so:5EFFB538 STRB.W R0, [R12,R3] ;也是前面的位置,但是加了个1偏移
同样,结束之后,查看内存,通过后面分析,知道这段字符就是key加密变换之后要对比的字符串。
1 | 5ED12020 4A 50 79 6A 75 70 33 65 43 79 4A 6A 6C 6B 56 36 JPyjup3eCyJjlkV6 |
子过程返回之后,接着b进入另一段。调了这么久,我们输入的key去哪里了?下面来了!
1 | text:00003680 000 D9 F8 00 00 LDR.W R0, [R9] 之前传入的参_JNIEnv |
先来看看check接口:1
check(_JNIEnv *,_jclass *,_jstring *) 00002814
check参数在刚进入就被保存了,现在在00003680位置取出来,返回了我们输入的key到R0中(看注释)。
1 | 5DC4BEC0 31 32 33 34 35 36 00 40 10 00 00 00 4B 00 00 00 123456.@....K... |
然后,又调用了一个子过程来处理key,我这里先没有跟入,直解F8,看了返回值
1 | .text:00003792 000 16 F0 09 FB BL sub_19DA8 |
1 | 65 4B 2F 30 36 38 71 52 00 00 00 00 C0 BE C4 5D eK/068qR |
基本确认是加密函数,然后又把该结果和JPyjup3eCyJjlkV6DmSmGHQ=!!进行对比。
1 | .text:000038CE 000 78 44 ADD R0, PC ; 保存了JPyjup3eCyJjlkV6DmSmGHQ=!! |
取出一个字符进行比较,不同则跳转,相同R4加1,继续比价直到超过0x18(也就是加密结果长度0x18),都相同了R0=1
看看不同时跳转的代码,sub_27C8是一个类似鱼strstr的代码,我本以为加密之后结果可以部分匹配也行,结果我错了,作者坑人,因为这个sub_27C8就算返回1,也就是部分匹配成功了,也会进入00003C26,R0=0。1
2.text:00003A1A 000 78 44 ADD R0, PC ; result
.text:00003A1C 000 FE F7 D4 FE BL sub_27C8 ; 在result中找key,找到匹配的一段,返回匹配位置,否则返回0
所以加密结果必须是0x18,和JPyjup3eCyJjlkV6DmSmGHQ=!!完全匹配(0x18字节)
算法
现在重新跟入加密子过程sub_19DA8,看看是怎么个算法。
1 | .text:00019DA8 sub_19DA8 ; CODE XREF: sub_2874+F1E |
先通过sub_1A31C子函数返回了一串字符199319124851!,算法和生成JPyjup3eCyJjlkV6DmSmGHQ=!!字符类似,不再细说。
1 | .text:00019F80 428 20 46 MOV R0, R4 ; size |
然后分配了一段内存,用于保存第一次加密的key结果。
调用sub_55E4,将199310124851!通过变换放入一个8字节+0x100*4的数组(初始化为0-0x100)空间,挺绕的,由于这个函数跟key没有多大关系,所以咩必要细究是怎么做的,可以直接将计算后内存dump出来用后面的逆运算(其实我没用上)。
1 | .text:0001A13A 428 EA F7 A0 FA BL sub_467E;第一次加密变换 |
然后sub_467E进行第一次加密变换,将key和前面的8字节+0x100*4的数组组队的xor,细节直接看代码(完整的我会放idb):1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20v4 = p->unk_0;
v5 = p->unk_4;
if ( key_len >> 3 ) // 8 >> 3 = 1
{
v6 = -(key_len >> 3); // -2
v7 = pKeyResult + 8 * (key_len >> 3); // 2*8
key1 = key;
do
{
++v6;
v9 = (unsigned __int8)(v4 + 1); // 1
v10 = p->index[v9]; // p->Index[1]
v11 = v5 + v10; // 0+p->Index[1]
v12 = p->index[v11];
p->index[v9] = v12;
p->index[v11] = v10;
*(_BYTE *)pKeyResult = p->index[(unsigned __int8)(v10 + v12)] ^ *(_BYTE *)key1;
v13 = (unsigned __int8)(v4 + 2); // 2
v14 = p->index[v13]; // p->Index[2]
...
这里我没有暂时没有渗入理解,直接进入第二次加密运算。
1 | .text:0001A222 428 01 44 ADD R1, R0 ;长度 |
进入sub_5AFC,将key每3个字节一组,进行<<8
拼接,也就是a1<<16+a2<<8+a3
,举个例子0xaa,0xbb,0xcc=>0xaabbcc
然后拼接结果v15再左移,
如果是3个字符拼接的,这里v16是3,v19=v15 << 8 * (3 - v16)
也就左移0,也就是不左移;
如果是两个字符或者一个字符拼接的,这里就需要左移8或者16位,说白了就是需要构成0x112233的结构。
然后v19进行4次移位,取aAbcdefghijklmn字符放入结果内存中。其实就是v19按6位进行分割(分别右移0x12,0xc,0x6,0x0,&03f),分割的值作为index,去aAbcdefghijklmn中对应字符,保存。
如果v16<3
,也就是此次拼接没有3个字符,这里index=0x40
,也就是增加额外的”=”用于结果。
1 | if ( _R10 > 0 ) // len>0 |
逆向算法
算法大致明白了,结果又是JPyjup3eCyJjlkV6DmSmGHQ=(取了0x18字节)。那么将第二次加密进行求逆。
先找JPyjup3eCyJjlkV6DmSmGHQ=每字节在’ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=’中的index。
1 | k = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=' |
结果是:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
241: J 9 9
2: P 15 f
3: y 50 32
4: j 35 23
5: u 46 2e
6: p 41 29
7: 3 55 37
8: e 30 1e
9: C 2 2
10: y 50 32
11: J 9 9
12: j 35 23
13: l 37 25
14: k 36 24
15: V 21 15
16: 6 58 3a
17: D 3 3
18: m 38 26
19: S 18 12
20: m 38 26
21: G 6 6
22: H 7 7
23: Q 16 10
24: = 64 40
然后每4个index一组,来自于v19的4次右移,那么反过来4个一组,左移相加就是v19
1 | for i in range(0, len(idd), 4): |
得到结果:
1 | 0: 24fca3 |
然后我们又知道v19其实是v15拼接的,所以拆开就得到v15(第一次加密结果),可以看到key长度应该是17。1
24 fc a3 ba 9d de 0b 22 63 96 45 7a 0e 64 a6 18 74
然后接着求第一次加密的逆运算,看代码,好多啊,怎么办,难道要求逆,好难!
好吧,不装了,其实不难,我们看前面说的第一次加密其实就是分组xor!
xor好啊,xor好啊…我们知道xor两次会将结果还原,想到了什么?!
是的,既然我们拿到第一次加密结果,那让他再和哪个8字节+0x100*4的数组再xor一次不久可以了,但是要重写这个加密代码貌似也挺麻烦的,怎么办?!
这里我是这么做的,在调试中,第一次加密前,将key的值(本来是输入)修改为上面得到的第一次加密结果,然后开始第一次加密运算,这样不就完美的完成了一次求逆吗,哈哈!
具体操作,对1A13A下断,输入key(必须是17位,否则修改内存时可能会挂),确认,断下来,此时r2就是key
1 | 5E127B20 31 32 33 34 35 36 37 38 39 30 31 32 33 34 35 36 1234567890123456 |
然后在hex窗口,f2修改内存,输入上面的24 fc…,然后f2确认修改。1
25E127B20 24 FC A3 BA 9D DE 0B 22 63 96 45 7A 0E 64 A6 18 $.
5E127B30 74 A9 12 5E 0F 00 1F 00 FF FF 1F 00 0F 00 00 t..^..
然后f8。看看结果:1
25E127B38 6D 61 64 65 62 79 65 72 69 63 6B 79 39 34 35 32 madebyericky9452
5E127B48 38 00 73 00 11 10 00 00 62 00 69 00 6C 00 69 00 8.s.....b.i.l.i.
答案就是:madebyericky94528
转载请注明出处:https://anhkgg.github.io/kxctf2017-writeup6
参考:
- 安卓APP动态调试技术–以IDA为例
- http://luleimi.blog.163.com/blog/static/175219645201210922139272/
- http://blog.csdn.net/zhangmiaoping23/article/details/43445797
- http://www.cnblogs.com/liujiahi/archive/2011/03/22/2196401.html
- http://cncc.bingj.com/cache.aspx?q=arm++IT+EQ&d=4981012666125942&mkt=zh-CN&setlang=zh-CN&w=YEX3ioizXLDZGmlpVDBGFh_dhhHpfnYj