作者:bin233 唐山师范学院/大四/DDCTF2019 Android方向 TOP2
第三届DDCTF高校闯关赛鸣锣开战,DDCTF是滴滴针对国内高校学生举办的网络安全技术竞技赛,由滴滴出行安全产品与技术部顶级安全专家出题,已成功举办两届。在过去两年,共有一万余名高校同学参加了挑战,其中部分优胜选手选择加入滴滴,参与到了解决出行领域安全问题的挑战中。通过这样的比赛,我们希望挖掘并培养更多的国际化创新型网络安全人才,共同守护亿万用户的出行安全。
Android第一题:Breaking LEM
首先将apk拖入JEB进行反编译,来到入口类并找到点击事件函数。
观察到Java层只负责传递输入内容到native层,因此直接分析so文件的Java_com_didictf_guesskey2019lorenz_MainActivity_stringFromJNI函数即可。
该函数首先会将输入内容与字符串”ddctf-android-lorenz-“比较,如果输入长度不足则直接失败,否则将进行截断操作(如输入ddctf-android-lorenz-XXX将截断为XXX)。
之后会对“XXX”进行逐字符验证,字符必须属于字符串”ABCDEFGHIJKLMNOPQRSTUVWXYZ123456″。接下来就是洛伦兹加密了,在GitHub找到该算法实现,发现该算法加解密是同一个函数。因此只需要拿到密文,再让apk跑一次就是明文。不出所料,洛伦兹加密只对XXX进行加密(设加密后为YYY)。随后会对YYY进行sha256运算。
分析发现是五层sha256算法进行加密,最后与shaCorrect进行比较。通过查找交叉引用便能找到shaCorrect的真实字符串(在init_array中进行初始化)。
接下来的任务便是暴力破解该sha256,比赛当晚我就跑完了7位及以下所有字符串。最后等到了提示,是8位字符串并告诉了前两位字符。因此,将其补齐为八位字符串,就能保证经过洛伦兹加密后的前两位密文是不变的,只需要暴力破解后六位字符串即可。最终运气爆棚,倒着爆一分钟就出来了。
将爆出来的结果拼接上ddctf-android-lorenz-,让apk自动为我们解密出明文。
Android第二题 Have Fun
首先拖入JEB发现标识符被混淆成了不可见字符,由于文件不大,直接手动重命名反混淆。
很容易追踪到对输入内容第一次加密的函数,o()、p()函数会将Assets中的dex文件释放到一个隐藏文件夹中,还偷偷改了字节码。
Apk使用到第一代加固保护技术,通过DEXClassLoader热加载dex文件,继续跟进dexLoader函数中。
为了更快更准确的拿到dex文件,使用IDA动态调试dex,便能直接得到dex文件路径以及即将被加载的dex文件(直接从assets中拿到的dex文件算法是错误的)。
正确的算法实现如下:
接下来程序会删除该dex文件,最后调用so层函数。So文件进行了section加密,但直接静态分析就够了。从JNI_Onload中得到动态注册的三元组,并找到具体函数位置。
程序会对输入内容进行16进制转换并与内存中的固定数据进行比较,该数据如下图所示。
解题脚本如下:
Android第三题 不一样的Service
本题使用了控制流平坦化,画面实在是太美,强行带混淆调试。首先JAVA层会开启一个service参与输入内容的验证,没有什么关键逻辑,重点关注so层。从JNI_OnLoad找到动态注册的函数如下:
很容易发现如下反调试检测的函数,这里先不去关心。
接下来留意到Parcel的处理函数,创建结构体方便后续分析,动态调试中重点关注readString的调用。
单步跟踪发现如下函数会使用到readString函数(偏移0x1DB50)。
跟到如上图位置(偏移0x10458),终于拿到java层输入的内容,接着进入到sendInput1函数(偏移0x1B0D4)。
这里会发现程序使用socket将输入内容发送了出去,接着进入recvResult函数(偏移0x1470C)。
发现recv的数据竟然与send的数据不同,而且调试多次发现每次recv到的内容还都不一样,暂且放下该问题。接着接收的数据进行分析,程序会将该内容与固定的内存数据(称之为enFlag)进行比较(偏移0x8540)。
后来想到还有一个service进程,开始调试service进程。跟踪到validate函数,发现如果输入长度为32位就会返回dd字符串(并且在主进程也有对recv的结果是否为ddd的验证,否则都不会接收到那个奇怪的内容)。
第一次加密操作:单步慢慢跟进很容易发现,这里使用python实现如下:
第二次加密操作:会先保存前两个元素,后面元素每两个进行异或,处理完后将刚才保存的元素放到最后。伪代码如下:
第三次加密操作:会再与某个内存数据(称之为key)进行逐位异或,最后send出去,这就是在主线程recv的数据与send的数据不同的原因(主进程与服务进程进行socket通信,因而之前IDA只能控制主进程空间)。
将key与enFlag逐位异或就完成了一次解密,但发现最后两个元素明显不处于ASCII码表中,所以推测自己得到了一个错误的key(印证了之前recv多个不同结果的现象)。
因此先随便输入32位字符串,自行实现第一二次加密操作进行加密,然后将其与主进程recv的数据进行异或,这样就得到了多组key,必然有一个key是真实的。
将这些key继续与enFlag异或,其中一个key异或结果如下图所示。
68对应字符‘D’,而69正好是第一次加密加了下标1导致的,因此也是‘D’(不正好像DDCTF吗?可以推断出自己已经得到了正确的数据)。接下来的问题就是破解“第二次加密”了,直接无脑爆破不太现实,这里提供两种解密方式:
逆向猜解法:
可以推测最后一个元素数据是“}”,那么“第一次加密后”他就是“}”+31=156。所以只需要猜解倒数第二个元素,然后逆着异或。具体脚本如下:
def myPrint(res):
ret=[]
for i in range(32):
ret+=chr(res[i]-i)
print "".join(ret)
for j in range(160):
ispass=0
flag=[ 1 , 18 , 15 , 215 , 22 , 254 , 12 , 9 , 42 , 21 , 20 , 50 , 232 , 22 , 242 , 204 , 1 , 248 , 2 , 246 , 244 , 248 , 4 , 251 , 221 , 202 , 22 , 3 , 27 , 210 , 68 , 69 ]
flag[30]=j
flag[31]=156
for i in range(1,31):
flag[30-i] = flag[32-i]^flag[30-i]
if(flag[30-i]<33 or flag[30-i]>160):
ispass=1
break
if(ispass==0):
flag[0]=68
flag[1]=69
myPrint(flag)
正向异或法:
既然我们已经看“DD”字符串了,那么后面必然是“CTF”,正向再异或一遍,具体脚本如下:
flag=[ 1 , 18 , 15 , 215 , 22 , 254 , 12 , 9 , 42 , 21 , 20 , 50 , 232 , 22 , 242 , 204 , 1 , 248 , 2 , 246 , 244 , 248 , 4 , 251 , 221 , 202 , 22 , 3 , 27 , 210 , 68 , 69 ]
tmp=[]
tmp.append(flag.pop(30))
tmp.append(flag.pop(30))
tmp+=flag
tmp[2]=ord('C')+2
tmp[3]=ord('T')+3
for i in range(0,30):
tmp[i+2]= tmp[i] ^ flag[i]
for i in range(32):
tmp[i]-=i
print "".join(map(lambda x:chr(x),tmp))
想了解更多 题目出题人视角解析,请关注:滴滴安全应急响应中心(DSRC)公众号查看:
发表评论
您还未登录,请先登录。
登录