GoSSIP_SJTU · 2015/04/01 9:26java
Author: 上海交通大学密码与计算机安全实验室软件安全小组GoSSIPpython
题目下载android
本次比赛的第一个题目是一个APK文件,安装后,须要用户输入特定的密码,输入正确会显示破解成功。该题目的APK文件没有太多的保护,能够直接使用各类分析工具(如jeb等)反编译获得Java代码。git
得到正确注册码的代码逻辑为: 1. 从logo.png这张图片的偏移89473处,读取一个映射表,768字节编码成UTF-8,即256个中文表 2. 从偏移91265处读取18个字节编码的UTF-8(即6个中文字符)为最终比较的密码。而后经过输入的字符的转换,转换规则就是ASCII字符编码,去比较是否和最终密码相等。github
咱们在这里提供一种很是愉快的解法,不须要复杂的工具和分析,你们能够参见视频算法
打开app后,咱们使用adb logcat并加上这个app独有的 lil 标签过滤日志输出,发现app输出日志中有table,pw以及enPassword。随意输入字符串如123456789,发现enPassword中有对应的中文输出,根据输出反馈,能够知道有以下对应关系安全
经过观察Logcat输出可知,最终目标pw应为义弓么丸广之,根据上述table中的对应关系,咱们能够获得最终密码为:app
581026
复制代码
题目下载框架
本次比赛的第二个题目仍然是一个独立的APK文件,安装后,须要用户输入特定的密码,输入正确会显示成功。第二题APK在Java层代码中并无关键逻辑,将用户输入直接传给native so层中securityCheck这个native method(securityCheck方法在libcrackme.so中),由native code来决定返回正确与否。函数
用IDA工具打开libcrackme.so,首先看下程序的大体流程,能够看到在securityCheck这个方法调用前,在init_array段和JNI_Onload函数里程序都作了些处理,而在securityCheck方法的最后有一个判断,将用户输入和wojiushidaan作比较。尝试直接输入wojiushidaan,发现密码错误,所以能够猜想前面一大段逻辑的做用就是会把这个最终的字符串改掉。此时的思路是只需知道最终判断时候这个wojiushidaan地址上的变换后的值就好了。尝试使用IDA调试发现一旦attach上去,整个程序就退出,想必必定是在以前的代码中有反调试的代码。
同上一题同样,咱们提供一种很是巧妙的解法:
注意到在最终比较以前,程序使用了android_log_print函数,当咱们直接运行程序时,发现这里固定输出了
I/yaotong ( XXX): SecurityCheck Started...
复制代码
这时候咱们想,是否能够直接patch这个libcrackme.so,修改打印的内容,直接利用这个函数帮咱们输出此时真正须要比较的值。
咱们选择patch的方法是直接把这个log函数往下移,由于在0x12A4地址处正好有咱们须要的打印的数据地址赋值给了R2寄存器(原本是为了给后面作比较用的),所以将代码段从0x1284到0x129C的地方都用NOP改写,在0x12AC的地方调用log函数,同时为了避免影响R1的值,把0x12A0处的R1改为R3。
下面是对比patch前和patch后的图:
参考视频给出了完整的解决过程:
经过观察Logcat输出可知,最终密码为:
aiyou,bucuoo
复制代码
在介绍本次比赛第三道题目以前,首先要介绍一个咱们GoSSIP小组开发的基于Dalvik VM的插桩分析框架InDroid,其设计思想是直接修改AOSP上的Dalvik VM解释器,在解释器解释执行Dalvik字节码时,插入监控的代码,这样就能够获取到全部程序运行于Dalvik上的动态信息,如执行的指令、调用的方法信息、参数返回值、各类Java对象的数据等等。InDroid只须要修改AOSP的dalvik vm部分代码,编译以后,可直接将编译生成的新libdvm.so刷入任何AOSP支持的真机设备上(目前咱们主要使用Nexus系列机型特别是Nexus4和Galaxy Nexus)。在本次比赛的第三题和第四题分析过程当中,咱们使用该工具进行分析,大大提升了分析效率。具体细节能够参考咱们发表的CIT 2014论文DIAS: Automated Online Analysis for Android Applications
回到题目上,将第三题的APK进行反编译后发现代码使用了加壳保护,对付这类加壳的APK,最方便的方法就是使用InDroid来进行动态监控,由于静态加密的DEX必定会在执行时在Dalvik上时解密执行,这样咱们能够直接在InDroid框架里对解释执行过程当中释放出来的指令进行监控。在咱们本身使用的工具里,咱们开发了一个动态读取整个dex信息的接口,执行时去读DexFile这个结构,而后对其进行解析(解析时直接复用了Android自带的dexdump的代码)。这样,咱们的插桩工具在运行程序后,可以直接获得程序的dex信息,同未经保护时使用dexdump后获得的结果基本一致。虽然咱们获得的信息是dalvik字节码,没有直接反编译成Java代码那么友好,但因为程序不大,关键逻辑很少,所以对咱们的分析效率影响并不大。
使用InDroid进行脱壳的演示视频:
在获得脱壳以后的dexdump结果后,咱们能够对代码进行静态分析。咱们发现用户的输入会传递给继承自Class timertask的Class b,被Class b的run方法处理。在run方法中,若是sendEmptyMessage方法被调用时的参数为0,就会致使Class c的handleMessage这个方法中获得的message的what值为0,进而致使103除0跳入异常处理中,触发成功的提示。
继续分析这个run方法的逻辑,能够知道用户的输入会被传递到Class e的a方法中,作个相似摩尔斯译码的过程(其译码与标准的摩尔斯电码不太同样),而后通过下面一系列大量的混淆用的无用处理和不可能相等的比较后,将译码后获得的字符串送入到关键的判断中去。这个判断成功的条件比较复杂:对于译码后获得的字符串的前两个字节,要求使用hashcode方法的结果等于3618,而且这两个字节相加等于168,才会进入后面的比较。咱们穷搜索一下符合这类输入的字符串:
#!java
for ( size_t i = 33; i < 127; ++i )
{
for ( size_t j = 33; j < 127; ++j )
{
String x =String.valueOf((char)j)+String.valueOf((char)i);
if (x.hashCode()==3618 && (i+j) == 168)
{
System.out.println(x);
System.out.println(j+i);
}
}
}
复制代码
输出为:
s5
168
复制代码
也就是说只有s5知足hashcode为3618,而相加等于168这个条件。
肯定前两个字符后,后面还有四个字符须要同Class e和Class a的Annotation值比较。由于咱们作脱壳的时候直接使用了dexdump的代码,而dexdump即便到最新版里也没法很好地处理Annotations:
// TODO: Annotations.
复制代码
不过不要紧,咱们还有动态分析工具这一利器,由于最终目的是获得getAnnotation方法的返回值,依然能够用InDroid在Dalvik执行到getAnnotation方法时监控返回值,就能获得Annotation的具体值。使用InDroid获取具体信息的视频以下:
最后可知,符合程序需求的字符串是
s57e1p
复制代码
使用程序内部的对应表,对其进行逆变换,可以让程序输入成功提示的输入应该是:
… _____ ____. . ..___ .__.
复制代码
本次移动安全挑战赛的第四题和第三题同样,是一个包含了加壳dex的APK文件,咱们使用同解决上一题同样的方法,用InDroid获得原始dex文件的dexdump结果:
使用InDroid进行脱壳的演示视频:
Dex总体处理过程和上一题也相似,使用handleMessage
处理最后的判断输入成功与否,只有sendEmptyMessage(0)
后,触发除以0的异常才能成功。不过这一题将用户输入转成byte后,传给一个native的方法:ali$a
的M$j
方法,另外参数还包括一个常数48和Handler。看样子逆向native库势在必行了。这一题的lib文件夹下文件和上一题是同样的,有三个文件,其中libmobisecy.so
实际上是个zip文件,解压后是个classes.dex,直接反汇编后,类和方法的名字都在,只是里面的代码都是
throw new RuntimeException();
复制代码
而libmobisecz.so
直接就是一堆binary数据,猜想应该是运行时会被解密,经过某种方式映射到为真正的代码执行。所以咱们的目标就是libmobisec.so
这个ELF文件。
直接用IDA打开libmobisec.so
,发现IDA会崩溃。用readelf发现正常的节区头数据都被破坏了,所以应该这个so自己也被加过壳了,不少数据只有在动态运行时才会解开,因此直接使用动态的方法,先运行这个程序后,直接在内存中把这个so dump出来。
首先须要在输入框中随便输入些数据后,点击肯定,保证用户输入数据执行到native方法里后再作dump。咱们使用的方法是查看maps后,使用dd命令把整个so都dump出来。
输入命令:
[email protected]:/ # ps | grep crackme.a4 u0_a73 1935 126 512204 48276 ffffffff 400dc408 S crackme.a4 [email protected]:/ # 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出来
[email protected]:/ # 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里拖出来就好了。
adb pull /system/lib/libc.so ./
复制代码
用IDA先打开libc,调整好是在内存中的偏移即rebase program,再在load additional binary里载入dd出来的libmobisec.so,经过maps里的偏移后载入。接下来的任务就是在其中找到M\$j这个函数的地址。
一开始尝试直接在dd出来的ELF文件中找这个M\$j这个函数名,相似的名字会被处理成
Java_ali_00024a_M_00024j
复制代码
相似下图:
不过我没找到这个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出来执行过的数据,因此必要的数据都已经解密了。以下图:
经过查看反编译的C代码,我发现程序中是直接经过JNI方法调用了Java中的bh
类的方法a
(在图2常量中也能够看到)。 再次回到dex层查看a
方法,该方法是不断的将输入传递给不一样的函数进行处理,先是cd
的a
方法,cC
的a
方法,p
的a
方法,x
的a
方法,ali$a
的M$d
方法(native),aS
的a
方法,x
的a
方法,ali$a
的M&z
方法(native),cd
的a
方法,cC
的a
方法,每个方法都是些简单的数学运算,编码,以及密码学处理等可逆的操做,结合逆向和Indroid对输入输出的监控,均可以轻松肯定每一个Java函数的做用,具体过程以下代码显示:
invoke-static {}, LbKn;.a:()Z // [email protected] move-result v3 invoke-static {v3}, LbKn;.b:(I)V // [email protected] add-int/lit8 v0, v5, #int 1 // #01 invoke-static {v4, v5}, Lcd;.a:([BI)[B // [email protected] move-result-object v1 add-int/lit8 v2, v0, #int 1 // #01 invoke-static {v1, v0}, LcC;.a:([BI)[B // [email protected] move-result-object v0 add-int/lit8 v1, v2, #int -1 // #ff invoke-static {v0, v2}, Lp;.a:([BI)[B // [email protected] move-result-object v0 invoke-static {v0, v1}, Lx;.a:([BI)[B // [email protected] move-result-object v0 add-int/lit8 v2, v1, #int -1 // #ff invoke-static {v0, v1}, Lali$a;.M$d:([BI)[B // [email protected] move-result-object v0 add-int/lit8 v1, v2, #int 1 // #01 invoke-static {v0, v2}, LaS;.a:([BI)[B // [email protected] move-result-object v0 invoke-static {v0, v1}, Lx;.a:([BI)[B // [email protected] move-result-object v0 add-int/lit8 v2, v1, #int 1 // #01 invoke-static {v0, v1}, Lali$a;.M$z:([BI)[B // [email protected] move-result-object v0 add-int/lit8 v1, v2, #int 1 // #01 invoke-static {v0, v2}, Lcd;.a:([BI)[B // [email protected] move-result-object v0 add-int/lit8 v2, v1, #int 1 // #01 invoke-static {v0, v1}, LcC;.a:([BI)[B // [email protected] move-result-object v0 return-object v0 复制代码
值得注意的是,其中有两个native的方法,由于InDroid还能够监控调用native方法的参数以及返回值,咱们发现这几个native都没有对输入作复杂的处理,只有M\$d对输入的第四个字节作了减8的处理。
作了这些逆变换之后咱们其实并无找到最终比较的处理,不过在解密过的数据中(图2),不只有以前须要调用的各类方法和类,还能够发现有个十分可疑的Base64的字符串。
aJTCZnf6NyBPYJfbrBuLu0wOhRFbPtvqpYjiby5J81M=
复制代码
而且在native的M\$z方法的反汇编代码中,能够看到有对这个Base64字符串的长度比较,因为咱们并无找到真正的比较函数,所以获得这个字符串后,咱们直接从M\$z开始向上逆推以前的变换就获得了的答案。
具体解密代码以下:
#!python
#!/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
复制代码
结果为:
alilaba2345ba
复制代码
这里还须要提一下如何寻找M\$d
和M\$z
两个函数在so库中的地址的方法,不过这个方法是一些经验的总结,缘由是整个native ELF文件的节区结构是被修改过的。这两个方法和M\$j不太同样,由于在dump出的libmobisec里能够找到M\$z的函数名,证实这个方法没有使用RegiterNatives
来作变换,所以咱们能够经过符号表来找这个函数与文件头部的偏移。方法是找M\$z
和字符串表的偏移,如0x03FE,而后穷搜整个文件:
由于符号表应该会把字符串表偏移做为一项,这块区域的结构体,咱们对照ELF结构发现并非标准的符号表,但仍是能够大概看出结构体的内容,包括索引,字符串表偏移,以及ELF特殊的标志数,所以推测0x57BE4偏移是M\$z
函数。该地址也正好是个压栈的指令,证实了咱们的猜测。
2015年移动安全挑战赛的最后一道题目,在规定的比赛时间内,仅有来自咱们GoSSIP的wbyang一名选手解决了这道问题,今天咱们就来揭开这一道最高难度题目的神秘面纱。
先把名为AliCrackme_5.apk
的文件丢到JEB里看一看:
dex文件并无进行加壳和混淆,看上去是一个很是简单的程序,Java代码部分使用函数Register("Bomb_Atlantis", input)
对输入进行判断。因此须要分析的逻辑应该都在libcrackme.so
里的Register
函数中。
接下来咱们用IDA打开这个libcrackme.so
,不出所料的发现IDA彻底无法处理,应该是进行了强烈的混淆和加壳处理:
使用和解决前面题目相同的技巧,咱们继续使用dd
的方法来去处一部分的混淆和加壳。运行一次程序后,从/proc/self/maps
里找到libcrackme.so
在内存中的位置,使用dd
命令从/proc/self/mem
中提取出内存中的libcrackme.so
,接着使用在解决第四题时使用过的技巧,将libcrackme.so
和libc.so
一块儿加载到IDA里。
用IDA打开dump出的代码后,咱们发现仍然有大部分的代码没法被IDA识别,须要手动定位到须要分析的代码而后手工定义(IDA快捷键C)代码,同时因为代码会在THUMB指令集和ARM指令集之间切换,有时候须要用快捷键ALT+G来将T寄存器设置为不一样的值,设置正确后才能正确翻译出代码。这里咱们首先遇到的问题是没法定位Register
函数,一样使用第四题中的技巧,用InDroid监控到Register
函数的真实地址,就能够在该地址上开始分析。
在libcrackme.so
这个动态库里使用的一些混淆方法,对于处理了前面一些相似混淆后如今的咱们来讲已经不是问题(^_^)。经过分析代码,咱们定位了几个函数,这些函数的偏移在不一样的设备上应该是不一样的。总体的逻辑其实并不复杂,首先会有一个固定的字符串“Bomb_Atlantis”和一个固定的salt去进行一次md5运算,salt是动态生成的,不过因为dump内存的时候这些动态的值已经生成好了,因此可以直接发现这个salt(出于一些版权缘由咱们不便公布本题目的一些内部细节,所以该salt值请你们本身分析)
以后程序会将这个md5值和咱们的输入进行一些异或和计算的操做,通过几步比较简单、可逆的变换以后,进入一个比较复杂的函数,通过这个函数处理后直接和一个内存中的值进行比较,返回比较结果。
这里说一个咱们在作第五题时用到的分析方法——动态hook。因为libcrackme.so
中并无对调用自身的上层应用进行验证,这就致使了咱们能够本身写一个程序去加载这个so,调用其中的方法。这也致使了咱们在加载libcrackme.so
后,能够加载另外一个用于hook的so,这样咱们能够hook libcrackme.so
中的任意函数,从而知道任意函数的参数和返回值,这对于咱们理解程序有着很是大的帮助。这里咱们使用的hook框架是著名Android安全研究人员Collin Mulliner开发的Android平台上的一个二进制注入框架adbi。固然这道题目并不可以经过注入的方法将咱们的so注入进去,由于源程序禁掉了ptrace这样的系统调用。咱们对adbi稍做修改,使之成为一个能够手动加载的动态hook框架。同时因为咱们无法经过符号表来定位函数的地址,全部的hook地址都须要硬编码,而且要和运行这道题目程序的Android设备内存映射严格对应。
须要指出的是adbi中存在一个小bug,hook.c
这个文件的118行应该是
h->jumpt[0]=0x60
复制代码
而不是0x30,对应的thumb汇编应该是
push{r5,r6}
复制代码
而不是
push{r4,r5},
复制代码
这个小bug在解题过程当中会形成一些影响。使用adbi来hook这道题目的函数还须要注意一点,这题的代码中有一些函数使用的THUMB指令集,hook这些函数时,不要忘记人工的对hook地址+1。
经过hook的方法,咱们已经可以动态的分析libcrackme.so
,首先咱们验证了咱们对以前几步变换的分析结果。以后就是分析最后一个复杂的处理函数,经过静态分析+动态调试,咱们发现这是一个相似于白盒密码学的加密函数。咱们的输入进入函数后,首先通过几步相似DES的预处理,以后会进行若干轮的查表,经过查询一个巨大的表将咱们的输入进行某种加密,生成一段密文,再通过几回简单的处理后和最后内存中的一段常量(出于一些版权缘由咱们不便公布本题目的一些内部细节,所以该常量请你们本身分析)进行比较。
经过动态调试,咱们可以计算出加密算法最后应该输出的值,可是因为这个加密算法的密钥融入了整个置换表中,要找出一个逆置换表显然不太可能。咱们简单过滤了libcrackme.so
的其余函数,也没有发现用于解密的函数,想要正常解密密文是不太现实了。不过根据对加密算法的分析,咱们发现这若干轮的置换是相互独立的,而且每一轮的复杂度并不高,这就意味这咱们能够在能够接受的时间内对算法进行爆破。咱们一开始的想法是code reuse,直接在Android设备上爆破,可是发现速度太慢,最后只能用笨办法,经过hook从内存中dump出来置换表,用C代码重写了这个算法,有惊无险地在比赛结束前半小时搜索出结果。根据逆推算法推出正确输入是:
3EFoAdTxepVcVtGgdVDB6AA=
复制代码
好了,咱们的2015移动安全挑战赛全系列回顾就到此为止了!但愿你们能和咱们多多交流讨论,欢迎你们关注咱们的微博GoSSIP_SJTU,基本上天天都会有精彩的内容发布哦。