alictf2016 Jumble writeup

2016年6月4日作为一个菜鸡参加了alictf2016,是安卓逆向的主场,记忆中有两道未解出的题目,一道是steady,第二次遇到时候做出来了;另一道是jumble,当时我就下定决心,将来有一天我一定要做出来,时隔一年,终于也被我刚出来了。

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

相关文件链接:https://github.com/LeadroyaL/attachment_repo/tree/master/alictf2016_jumble (Jumble.apk,fix.so,alictf.py,alictf_data.py)

下面说正事,当时是有两支队伍做出来的,PPP和40thieves,这题难度应该是偏高的,当时的压轴题吧,当初JEB打不开,而且解出来的人太少,就弃疗了。

整个apk分为Java和native两部分,均有加密。

首先看一下Java部分。

第一处反调试,MyApp.onCreate()里,直接检测Debug.isDebuggerConnected(),然后exit()。当然,这道题也不用去调试。顺便讲一下绕过方法把,不要用am start -D -n的方法去启动,这个反调试就绕过了。

入口很清晰,在MainActivity里有

再看看Check.b是什么东西,双击一下,发现卡死了,赶紧停下来,发现卡在了方法Check.c上面,整体如下

显然Check.b()的作用是拿到包的签名,再过一下Check.a(String s)方法,这是个MD5的过程,输出为hexString的MD5,好在出题人帮我们Log了一下这个MD5,免得我们手动去计算了。

得到的值为:37f531665ef482c4e1964fb56973a9de。

接下来就是Java层的难点了,这个无法被decompiled部分,当时题目描述有一句:”looooooong code”,显然这个是太长了,JEB无效。

用apktools解一下包,发现这个Check.smali确实很大,983K,只能肉眼读smali,大概理解一下意思。整体结构看起来非常工整,这里有两种思路,一种是删掉99%的smali代码,注意寄存器的变化,让一个程序缩短并且正确返回,再使用apktools打包,用JEB观察反编译出来的代码;另一种思路是硬刚,肉眼看smali,总结流程。这里两种方法都讲一下。

先观察

一开始拿到了inputString.getBytes(),之后从StringHolder里拿String,forName拿到类,再拿String,再getDeclaredMethod反射拿方法,最后invoke。所以先看看StringHolder.get(int index, String des_key)的返回值吧,用index作为索引去查map,没查到就用md5值作为des_key去解密,得到的固定字符串后放入map并返回。所以我们手动写一下DES解密程序,结果如下:

int indexString dec
0com.ctf.crackme1.util
1android.os.Debug
2isDebuggerConnected
3b <--utils里的b,循环++
4d <--utils里的d,循环xor
5e <--utils里的e,高低换位

再看看utils类里的方法,方法a是一个反调试,但好像从来没有被调用过,可能是出题人随手debug时候留下的,同样c、f、g也是一样的,分别调用3种加密算法,出题人debug用。

我们继续看那段很长的代码,懒人方法先试一下,拉到最下面,发现还有一句

显然是将结果byet[]v2代入了Check.check()这个native方法中。

懒一点,在第10个invoke后面删掉,直到native check之前,再用apktools打包起来,丢进JEB里看看,哇,真是太感人了!!!

显然是在一直调用utils.b(input_bytes)、utils.d(input_bytes)、utils.e(input_bytes)对输入的字符串进行操作。

第二种,思路就是硬看smali咯,也不难,注释过的如下

由于3、4、5是随机出现的,即bde也随机出现,所以需要写个正则来提取一下,我写了这样的

结果是bdebdbdbedebebdbdbdebdbdbdedbdedbebdbdedbebedebdbebdbdebebedebedebebebedbedebdbebdbdebdbebdededebebebedbedebebdbdbedebebdbdebdbebebedbededbdedbebebdebebededbedebdbebdedebededebdbdbdbedbedbdedbedbdbebebdedebebdedebebebebebdedebdedededebedbebedbdbedebebebebdbedebdbdebededbebebdbdebebdebedbebebdebdebebdebdbdbdbebdedebedbdbebebebdedebdbdedbdedebdbebedbdbdbdedbdbedbedbdebedbebedebdbdebdbdbebdebebedbebebebdbdbebedebedebebdedebebebebedbdbdbedbdebdededbdebdbedebedebdedebebdbdbebebdedbdedbdbebdedbdedbebdbdebdbdbedbedebdbdbebedbebedebebdbedbedbdedbdebedebebebdedebededbdbededbdebdedebdbdebdbdbdbdbebdebebebdebededbebebdbedbdbdebdebdedebdebededbdbebdbebdebdedebebdebdedbedededbebdedbedbdbedededebebdbebebdedbededbebdbedbdebdedbedbdbdebdebedebdbdbededbdedebdedebdbdedebedebebdbebebdedbdededbedebdbdedebdedededbebdedbedbdbededbdedbdbedededebebebebdedbdebebdbedbebdbdebdbededebebededbdbebedbebedbebdbdedbedbdebdbebdbdebdebedbdbededebdbdbdbebdedebdbededbebdedbdedededbedebdbdebebebdbedebebdebdedededbdbdbebdbe

Java部分到此结束,接下来将一下native的部分。

加载这个so,发现是没有section名字的,总得先看看.init段吧,只有两段可执行的,出了.text段肯定就是.init段了,通过readelf -a也可以看到.init段是在0x9de0的位置。上IDA!

这里有个恶心的地方,整个so都是用llvm去写的,很多逻辑其实都是连蒙带猜,靠逆向经验的积累吧,不过我尽量说的详尽一点。

init段有2个函数,0x2510和0x2588,分别记为init_1和init_2。

init_1用于反调试,定义了3个信号的处理,14、18、17,要么exit,要么破坏flag。

init_2有2个功能,一个功能为解密一段函数并且执行,另一个功能为反调试。

0x29BC接收2个参数,分别为 “待解秘函数的地址” 和 “待解秘函数的长度”,解密方法比较简单,是直接进行取反。其实这一大段看不明白,有几个可疑的特征:

  1. 多处mprotect调用,
  2. 我们输入的是待解密的函数地址+1,这里会主动减去一个1,
  3. 待解密函数本身是一片乱码,无论如何P、Analyze,都是无法组成正常语句的,而且待解密函数存在xref
  4. 对长度进行了mod和div等操作,怀疑是为了对其或者别的原因
    看起来就是在解密东西了,
  5. 期间对待解密的函数进行了写操作,是0x2D08的循环取反,参数刚好是buffer_ptr和length
    于是我们有理由相信,这个函数是用来解密的,在v23 == -1498352302的时候,会调用解密过后的funciont_ptr。

我们稍后再分析这个解密过后的函数0x224c。

init_2函数的第二部分是拿到pid,之后执行一个与”libc.”和”fwrite”有关的东西,并且后两个参数指向同一个函数,0x22FC,对某个global buffer进行xor13的操作,猜起来是进行GOT表的hook吧,反正逻辑太复杂,估计逆不出来。

init_2调用got_hook的时候,0x6040被调用,虽然不知道这个是干嘛的,反正可能触发exit()。

解密0x224c很简单,用python去简单处理一下即可,将范围内的全部取反即可(顺便,JNI_Check_check的地方也有一个解密,我们顺便一起解掉好了。

之后打开新的so文件,继续分析这个0x224c。

先将0xA254写为0~31,再调用0x26D0函数。

0x26D0是一个反调试函数,检测/proc/pid/status,不断fork自己,pthrace等骚操作,并且有sleep(),管他呢,反正我们也无法调试。

.init段分析完毕,下面我们看JNI_OnLoad。

很不幸,这个函数真的什么都没做,纯属逗你玩,当时我还研究了半天有什么玄机,后来发现好像真的没东西。。。

剩下的只有JNI_Check_check了,仔细分析一下其流程。

上来先定义好几个buffer,意义暂时未知,我们接着往下看。

  1. 栈上有0x20个字节,全部写为0
  2. global 指针,0xA274 _BYTE* calc_ptr = malloc(0x20),之后全部赋值为0
  3. strncpy将输入的flag复制到 calc_ptr上,长度最多为32
  4. 解密并且调用0x1cb8
  5. 调用0x55ec
  6. 将参与0x55ec计算后的结果赋值给calc_ptr[0:16]
  7. 将calc_ptr[16:32]的元素,加等于其下标
  8. 进行fwrite(注意:此时触发xor13函数)
  9. 与target 0xA004进行对比,32位完全相同则认为success

按顺序分析,解密过的0x1cb8。

先对0xA254(之前被赋值为0~31)进行32次交换,(也就是说什么都没干),之后调用0x2E90,参数分别为(JNI_Check_check,256,calc_ptr,0x20)。

函数0x2E90就是一坨翔。。。完全看不懂,看起来是将JNI_Check_check的raw-data作为_BYTE*去处理的,第二个是256,第三个是待处理数据,第四个也是长度,可能是一种加密算法。之后再仔细观察,最开始进行了memset(stack_buffer_1, 256)的操作,只有2个参数,这就很尴尬,初始数据都不知道,可能是0吧,之后在某次循环里,将raw-data的值分别给了stack_buffer_1,将stack_buffer_2的值赋为0~255。再猜一下,没有硬编码的sbox,与输入字符长度无关,长度为256,限制再255之内,很可能是流密码RC4。(结果好像还真是RC4)那么密钥就是从JNI_Check_check开始的256的字节,明文就是我们输入的String补全0,最后明文被全被覆写为密文。

接下来是check里的第二个加密函数,0x55ec,这个比较直观,因为里面乱点时候不小心点到一个global的数组,其值为0x63,0x7C,0x77,0x7B,刚好是256字节,而且是AES的Sbox。参数1为RC4加密过的数据,参数2为输出结果,参数3为key。这时候遇到另一个问题,AES是128还是192还是256,因为key给的是256位的key,我一开始想当然就解密去了,发现解出来的是错的。

这里可以选择观察加密轮数的方法,0x55ec代码如下:

其中pssing_sbox函数第一句是计算轮数

算出来的结果是44,44代表的就是128位AES加密,果然解密时候就成功了。

好了,之后的步骤就简单了,正常的赋值和加法,最后进行xor,就得到target啦!


呼!结束了!总结一下,我们需要过掉如下的加密:

  1. Java层的1000次bde处理
  2. RC4加密全部
  3. 将前16字节写为,AES-128加密后数据的前16字节
  4. 将后16字节加等于其下标
  5. 将全部字节xor13
  6. 与char target[32]进行比较

剩下的工作就是提取各种参数,写脚本的事情了,这里就不详细阐述了。

PPP能短时间解出这题,真心佩服,学习一个;一年后解出来,也算是一点小小的进步了,分享给大家咯。。。

请容许我小开心一下,一年内的安卓题,终于全都ak掉了,以后可能会分享一下这一年来遇到的一些有趣的安卓题吧,当然鸽掉的可能性很大,hhhhhhh


附:最终脚本(alictf_data.py从文章开头提供的链接下载)

 


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

《alictf2016 Jumble writeup》有3个想法

发表评论

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

*

code