【知识库】DDCTF2019官方Write Up——Android篇
官方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/
随时掌握互联网精彩
- 1 奋力打开改革发展新天地 7945061
- 2 中方回应特朗普威胁收回巴拿马运河 7982359
- 3 刘强东提前发年终奖 7853658
- 4 “冷资源”里的“热经济” 7720639
- 5 全球约有1.9亿妇女为内异症患者 7682638
- 6 国足原主帅李铁已上诉 7587382
- 7 保时捷断臂求生 7405042
- 8 山姆代购在厕所分装蛋糕 7399776
- 9 喝水后有4种表现提示肾有问题 7244492
- 10 男子闪婚生女后发现妻子结过7次婚 7191123