Group of Software Security In Progress

GoSSIP @ LoCCS.Shanghai Jiao Tong University

2015移动安全挑战赛(阿里&看雪主办)全程回顾(4)

APK界面

题目下载

本次移动安全挑战赛的第四题和第三题一样,是一个包含了加壳dex的APK文件,我们使用同解决上一题一样的方法,用InDroid得到原始dex文件的dexdump结果:

使用InDroid进行脱壳的演示视频:

Dex整体处理过程和上一题也类似,使用handleMessage处理最后的判断输入成功与否,只有sendEmptyMessage(0)后,触发除以0的异常才能成功。不过这一题将用户输入转成byte后,传给一个native的方法:ali$aM$j方法,另外参数还包括一个常数48和Handler。看样子逆向native库势在必行了。这一题的lib文件夹下文件和上一题是一样的,有三个文件,其中libmobisecy.so其实是个zip文件,解压后是个classes.dex,直接反汇编后,类和方法的名字都在,只是里面的代码都是

1
throw new RuntimeException();

libmobisecz.so直接就是一堆binary数据,猜测应该是运行时会被解密,通过某种方式映射到为真正的代码执行。因此我们的目标就是libmobisec.so这个ELF文件。 直接用IDA打开libmobisec.so,发现IDA会崩溃。用readelf发现正常的节区头数据都被破坏了,因此应该这个so本身也被加过壳了,很多数据只有在动态运行时才会解开,所以直接使用动态的方法,先运行这个程序后,直接在内存中把这个so dump出来。 首先需要在输入框中随便输入些数据后,点击确定,保证用户输入数据执行到native方法里后再做dump。我们使用的方法是查看maps后,使用dd命令把整个so都dump出来。 输入命令:

1
2
3
4
5
6
7
8
9
10
11
12
root@maguro:/ # ps | grep crackme.a4
u0_a73    1935  126   512204 48276 ffffffff 400dc408 S crackme.a4
root@maguro:/ # cat /proc/1935/maps
5e0f2000-5e283000 r-xp 00000000 103:04 741132    /data/app-lib/crackme.a4-1/libmobisec.so
5e283000-5e466000 r-xp 00000000 00:00 0 
5e466000-5e467000 rwxp 00000000 00:00 0 
5e467000-5e479000 rw-p 00000000 00:00 0
5e479000-5e490000 r-xp 00191000 103:04 741132    /data/app-lib/crackme.a4-1/libmobisec.so
5e490000-5e491000 rwxp 001a8000 103:04 741132    /data/app-lib/crackme.a4-1/libmobisec.so
5e491000-5e492000 rw-p 001a9000 103:04 741132    /data/app-lib/crackme.a4-1/libmobisec.so
5e492000-5e493000 rwxp 001aa000 103:04 741132    /data/app-lib/crackme.a4-1/libmobisec.so
5e493000-5e4c1000 rw-p 001ab000 103:04 741132    /data/app-lib/crackme.a4-1/libmobisec.so

使用dd命令将libmobisec.so的内存dump出来

1
root@maguro:/ # dd if=/proc/1935/mem of=/sdcard/alimsc4 skip=1578049536 ibs=1 count=3993600

dd命令使用的数字都是十进制的,skip就是libmobisec.so的起始地址,count是总长度。 为了让IDA还能够识别libmobisec里的libc函数,我们还需要把libc也载入到IDA中,libc就直接从system/lib里拖出来就行了。

1
adb pull /system/lib/libc.so ./

用IDA先打开libc,调整好是在内存中的偏移即rebase program,再在load additional binary里载入dd出来的libmobisec.so,通过maps里的偏移后载入。接下来的任务就是在其中找到M\$j这个函数的地址。 一开始尝试直接在dd出来的ELF文件中找这个M\$j这个函数名,类似的名字会被处理成

1
Java_ali_00024a_M_00024j

类似下图:

图1

不过我没找到这个M\$j这个名字,逆过JNI库的都知道,如果符号表里找不到这个函数名,说明在JNI_Onload的时候,使用RegisterNatives函数重新将一个JNI函数映射为Native函数了。

正当一筹莫展的时候,我再次想起了InDroid系统。在Dalvik中,每个方法都是一个Method的结构体,其中当这个方法是native的时候,Method的insns这个指针会指向native方法的起始地址。因此我们修改了下InDroid,让Dalvik在执行M\$j这个方法前,去打印了M\$j方法的insns指针。这时我们得到了一个指向另一片内存区域的值,既不在libdvm中,也不在libmobisec中,并且这片内存页被映射成了rwx,由此推断里面也极有可能是代码,我们继而又dd出了这块内存,用IDA打开,使用ARM平台反汇编,发现该处就一条指令,是LOAD PC到另一个地址,而这个地址恰好在libmobisec中。于是我们直接到IDA中跳到这个地址,发现正好是个压栈指令,印证了我们的想法,此处就是M$j函数,于是在在IDA里该地址指令处,右击选择create function,让IDA识别这一段汇编指令为函数指令后,就可以通过F5查看看反编译的C代码了。

这个函数本身做了一些控制流混淆,同时还有很多字符串加解密的功能函数,一些简单的如异或操作,也被展开成与和或的组合等更长更复杂的表达式形式。另外还看到一些变形过的RC4,等等。不过因为我们已经是dump出来执行过的数据,所以必要的数据都已经解密了。如下图:

图2

通过查看反编译的C代码,我发现程序中是直接通过JNI方法调用了Java中的bh类的方法a(在图2常量中也可以看到)。 再次回到dex层查看a方法,该方法是不断的将输入传递给不同的函数进行处理,先是cda方法,cCa方法,pa方法,xa方法,ali$aM$d方法(native),aSa方法,xa方法,ali$aM&z方法(native),cda方法,cCa方法,每一个方法都是些简单的数学运算,编码,以及密码学处理等可逆的操作,结合逆向和Indroid对输入输出的监控,都可以轻松确定每个Java函数的作用,具体过程如下代码显示:

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
invoke-static {}, LbKn;.a:()Z // method@08a1
move-result v3
invoke-static {v3}, LbKn;.b:(I)V // method@08a2
add-int/lit8 v0, v5, #int 1 // #01
invoke-static {v4, v5}, Lcd;.a:([BI)[B // method@0b23
move-result-object v1
add-int/lit8 v2, v0, #int 1 // #01
invoke-static {v1, v0}, LcC;.a:([BI)[B // method@0a30
move-result-object v0
add-int/lit8 v1, v2, #int -1 // #ff
invoke-static {v0, v2}, Lp;.a:([BI)[B // method@0e8d
move-result-object v0
invoke-static {v0, v1}, Lx;.a:([BI)[B // method@0ede
move-result-object v0
add-int/lit8 v2, v1, #int -1 // #ff
invoke-static {v0, v1}, Lali$a;.M$d:([BI)[B // method@03d3
move-result-object v0
add-int/lit8 v1, v2, #int 1 // #01
invoke-static {v0, v2}, LaS;.a:([BI)[B // method@022e
move-result-object v0
invoke-static {v0, v1}, Lx;.a:([BI)[B // method@0ede
move-result-object v0
add-int/lit8 v2, v1, #int 1 // #01
invoke-static {v0, v1}, Lali$a;.M$z:([BI)[B // method@0440
move-result-object v0
add-int/lit8 v1, v2, #int 1 // #01
invoke-static {v0, v2}, Lcd;.a:([BI)[B // method@0b23
move-result-object v0
add-int/lit8 v2, v1, #int 1 // #01
invoke-static {v0, v1}, LcC;.a:([BI)[B // method@0a30
move-result-object v0
return-object v0

值得注意的是,其中有两个native的方法,因为InDroid还可以监控调用native方法的参数以及返回值,我们发现这几个native都没有对输入做复杂的处理,只有M\$d对输入的第四个字节做了减8的处理。

做了这些逆变换以后我们其实并没有找到最终比较的处理,不过在解密过的数据中(图2),不仅有之前需要调用的各种方法和类,还可以发现有个十分可疑的Base64的字符串。

1
aJTCZnf6NyBPYJfbrBuLu0wOhRFbPtvqpYjiby5J81M=

并且在native的M\$z方法的反汇编代码中,可以看到有对这个Base64字符串的长度比较,由于我们并没有找到真正的比较函数,因此得到这个字符串后,我们直接从M\$z开始向上逆推之前的变换就得到了的答案。

具体解密代码如下:

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
 #!/usr/bin/env python
# encoding: utf-8

from Crypto.Cipher import AES

def Lcda(s):
    return ''.join(map(lambda x: chr((ord(x) + 3) & 0xff), s))
def de_Lcda(s):
    return ''.join(map(lambda x: chr((ord(x) - 3) & 0xff), s))

def LcCa(s, a):
    return ''.join([chr(((ord(s[i]) ^ a) + i) & 0xff) for i in xrange(len(s))])
def de_LcCa(s, a):
    return ''.join([chr(((ord(s[i]) - i) & 0xff) ^ a) for i in xrange(len(s))])

def Lpa(s):
    return s[1:] + s[0]
def de_Lpa(s):
    return s[-1] + s[:-1]

def Lxa(s):
    return s.encode("base64")[:-1]
def de_Lxa(s):
    return s.decode("base64")

def LaliaMd(s):
    return s[:3] + chr((ord(s[3]) - 8) & 0xff) + s[4:]
def de_LaliaMd(s):
    return s[:3] + chr((ord(s[3]) + 8) & 0xff) + s[4:]

def LaSa(s):
    BS = 16
    pad = lambda s: s + (BS - len(s) % BS) * chr(BS - len(s) % BS)
    cc = AES.new("qqVJwt11yyLm7hVK1iI2aw==".decode("base64"), AES.MODE_ECB)
    cipher = cc.encrypt(pad(s))
    return cipher
def de_LaSa(s):
    cc = AES.new("qqVJwt11yyLm7hVK1iI2aw==".decode("base64"), AES.MODE_ECB)
    cipher = cc.decrypt(s)
    return cipher

res = "aJTCZnf6NyBPYJfbrBuLu0wOhRFbPtvqpYjiby5J81M="

flag = de_Lcda(de_LcCa(de_Lpa(de_Lxa(de_LaliaMd(de_LaSa(de_Lxa(res))))), 49))
print flag

结果为:

1
alilaba2345ba

这里还需要提一下如何寻找M\$dM\$z两个函数在so库中的地址的方法,不过这个方法是一些经验的总结,原因是整个native ELF文件的节区结构是被修改过的。这两个方法和M\$j不太一样,因为在dump出的libmobisec里可以找到M\$z的函数名,证明这个方法没有使用RegiterNatives来做变换,因此我们可以通过符号表来找这个函数与文件头部的偏移。方法是找M\$z和字符串表的偏移,如0x03FE,然后穷搜整个文件:

图3

因为符号表应该会把字符串表偏移作为一项,这块区域的结构体,我们对照ELF结构发现并不是标准的符号表,但还是可以大概看出结构体的内容,包括索引,字符串表偏移,以及ELF特殊的标志数,因此推测0x57BE4偏移是M\$z函数。该地址也正好是个压栈的指令,证明了我们的猜想。