【知识库】DDCTF2019官方Write Up——Android篇

百家 作者:滴滴安全应急响应中心 2019-04-22 13:13:00

官方Write Up发布时间线

对于没有被选手探索到的出题预设路径,出题人将现身说法,带来官方解读。

Android作者:bin233

唐山师范学院/大四/Android积分610 TOP2 

第一题: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自动为我们解密出明文。


第二题: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进制转换并与内存中的固定数据进行比较,该数据如下图所示。



解题脚本如下:



第三题:不一样的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))

————— End —————

    延伸阅读    

官网题目仍开放访问,点击“阅读原文“前往

    关于漏洞    

滴滴出行相关漏洞请提交至

http://sec.didichuxing.com/

关注公众号:拾黑(shiheibook)了解更多

[广告]赞助链接:

四季很好,只要有你,文娱排行榜:https://www.yaopaiming.com/
让资讯触达的更精准有趣:https://www.0xu.cn/

公众号 关注网络尖刀微信公众号
随时掌握互联网精彩
赞助链接