WHCTF2017 android writeup

WHCTF 2017,flappy大佬们出的题,感觉不错,两道apk,有一个拿了全场一血,分享一下writeup。拖了很久忘了发了,惭愧惭愧

转载请联系本人,否则作侵权处理。

相关文件链接:https://github.com/LeadroyaL/attachment_repo/tree/master/whctf2017/(FindMyMorse.apk, LoopCrpyto.apk)

Find My Morse

打开之后随便点击几下屏幕,偶尔绿偶尔红,Logcat也会有输出,猜测是根据点击屏幕的时间长短来处理。

C++写的native activity,manifest里什么东西都没有,dex里只有support的包,逻辑全都在libnative.so里。

看一下导入表,大概率是使用EGL去绘制一些东西,以及gettimeofday来计时。

看一下导出表,入口在android_main注册了2个函数,sub_F3ECBF70看起来是onCreate,初始化EGLsub_F3ECC154看起来是onTouchcheck

猜测:AMotionEvent_getAction拿到的是onKeyDownonKeyUp的事件,分别调用gettimeofday,拿到按压的持续时长,并且按下时*(v3+40)=0,会将屏幕主动调整为颜色A。大于0.2ms1,小于0.2ms0,这与题目说的Morse很像。之后index = 7 * (*(v3 + 52) % 4) + *(v3 + 52) / 32byte_F3ED0008[index]xor,最后bit相同时候认为本次check成功,*(v3+40)=0颜色调整为Abyte_F3ED0008[index] >>= 1 *(v3 + 52)++,一旦*(v3 + 52)大于224,就认为全部判定成功。若本次判定失败,则重置*(v3 + 52) = 0,设定颜色*(v3 + 44) = 0x3F800000为颜色B。最后,如果上文一直没有return,就会复制一段rodatabss段上,将byte_F3ED0008[28]初始化。

综上,*(v3 + 52)最初为0,经过224次变化,每次变化会加一,而index = 7 * (*(v3 + 52) % 4) + *(v3 + 52) / 32dumpbyte_F3ED0008[index]index的序列,即可还原出flagbit顺序。

之后遇到一个小问题,还原出来的224bit = 28 * 8bit = 32*7bit,而0对应0还是1,所以共有4种组合,最后得到32个字符最合理。

 

LoopCrypto

Java层的字符串都是加密的,具体方式不知道,但解密函数已经存在于dex了,复制粘贴出来可以直接用,这里就不细说了。

Java层,取签名的md5,转为hexString,小写作为参数带入JNI;输入的字符串作为参数带入JNI。即Decode.check(input, md5)。

之后进入native,init_array里有1个函数,用于反调试,先对bss段的数据进行xor 0xe9的操作,得到明文后继续,解密结果是

fork一个进程出来,读一下主进程是否被Trace,从而kill进程,并且check-sleep-check-sleep循环。绕过方法很多,例如patch掉so文件,kill掉这个被fork出来的进程。

然后是JNI_OnLoad,首先使用Java层的函数解密3个String,可以用之前写好的JVM来解密掉,分别是

之后注册JNI方法,就是我们的入口了,逻辑很简单,拿值,进check,返回值存在第三个参数里。

又是要fork线程,原来的线程使用pipe的一端进行读,fork出来的线程执行check,并且向另一端写返回值,主要逻辑在fork出来的线程里

ida也没有好的调试方法,只能动态将eip改为fork出来的进程然后跟一遍。ptrace自己,防止被调试,然后解密一段code,需要使用md5,解密后解压,然后执行,参数是input

解密函数先mmap一段可执行的内存map_ptr,再malloc一段heap_ptr,将密文与md5进行xor,存入heap_ptr,再进行1.2.11版本的zlib解压,解压出来的内容存入map_ptr。此时map_ptr就是真正的check逻辑了。

我们同样patchso文件,alt+G切换为thumb,就可以使用F5

主要是最上面的一堆赋值和常量比较难猜,最后猜出来是48-40-40-32byte,然后对其中后3buffer进行了赋值操作,第一个bufferinput输入进去的。

逻辑是:将input存放到input_buffer里,最后添加0,最长长度为0x27,算上\0的话0x28。经过一个骚操作,将加密后的数据写入input_buffer,该骚操作是8byte一个block,分成2int32,循环0x20次。之后与常量进行对比,先比1byte,再比input_lenbyte。全对走一个流程,有错走另一个流程,这里猜测是输出successfail

这个算法看起来无法用肉眼去逆,而且有几个可疑的数字,和4个可疑的字符串,查阅资料发现是TEA算法,以前听说过这种加密,还是Space@ROIS大佬很久之前和我提起过QQ用的这种加密方式,但没有研究过,于是找一份解密代码,解密一下enc_successenc_fail,成功了。去解密enc_flag,得到flag

flag{LOoK|N9_An_3@&9_s%Lue?!?!}

 

后记:

最开始zlib解压那段数据时候,一直失败,后来是dump出来的,留下了一点遗憾。详细研究一下这个uncompress函数,是静态编译进去的1.2.11版的zlib,而安卓里、python里使用的是1.2.8,对比一下源码里的函数。

之前参数不同,从手机里拖出来的zlib函数:
j_inflateInit_((int)&v8, (int)”1.2.8″, 56)
j_inflateInit2_(a1, 15, a2, a3);
j_inflateInit2_(v5, 31, (int)”1.2.8″, 56);

libcheck.so里的zlib
inflateInit_(&v13, -15, “1.2.11”, ‘8’);

+15 和 -15表示windowBit,最后都进入了
j_inflateReset2(v10, -15/+15);

里面对第二个数字进行了判断,如果为负数则取绝对值,正数的话计算取一个东西,所以其实+15和-15其实没有区别。

但1.2.8的解压规则和1.2.11规则不一样,所以使用正常的1.2.11,参数给默认,按理说就可以解压 出来了,然而我没这个闲情逸致去尝试,感兴趣的朋友可以去解密一下那段dump看看。

 

 


=============================================================
随着访客的增多,LeadroyaL在本站流量的开支越来越多了,曾经1元能用1个月,现在1元只能用3天。如果觉得本文帮到了你,希望能够为服务器的流量稍微打赏一点,谢谢!

发表评论

电子邮件地址不会被公开。 必填项已用*标注

*

code