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里有
1 Check.c(input_string, Check.b(MainActivity.this))
再看看Check.b是什么东西,双击一下,发现卡死了,赶紧停下来,发现卡在了方法Check.c上面,整体如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public static String b (Context arg6) { String v1 = null ; try { byte [] v2 = arg6.getPackageManager().getPackageInfo(arg6.getPackageName(), 64 ).signatures[0 ].toByteArray(); if (v2.length == 0 ) { return v1; } v1 = Check.a(v2); } catch (Exception v0) { v0.printStackTrace(); } return v1; } public static boolean c (String arg10, String arg11) throws Exception { }
显然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,总结流程。这里两种方法都讲一下。
先观察
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 .method public static synthetic c(Ljava/lang/String;Ljava/lang/String;)Z .locals 10 .annotation system Ldalvik/annotation/Throws; value = { Ljava/lang/Exception; } .end annotation const/4 v7, 0x0 invoke-virtual {p0}, Ljava/lang/String;->getBytes()[B move-result-object v2 const/4 v9, 0x1 const/4 v6, 0x0 invoke-static {v6, p1}, Lcom/ctf/crackme1/StringHolder;->get(ILjava/lang/String;)Ljava/lang/String; move-result-object v3 invoke-static {v3}, Ljava/lang/Class;->forName(Ljava/lang/String;)Ljava/lang/Class; move-result-object v0 const/4 v3, 0x3 invoke-static {v3, p1}, Lcom/ctf/crackme1/StringHolder;->get(ILjava/lang/String;)Ljava/lang/String; move-result-object v3 new-array v4, v9, [Ljava/lang/Class; const-class v5, [B aput-object v5, v4, v6 invoke-virtual {v0, v3, v4}, Ljava/lang/Class;->getDeclaredMethod(Ljava/lang/String;[Ljava/lang/Class;)Ljava/lang/reflect/Method; move-result-object v1 invoke-virtual {v0}, Ljava/lang/Class;->newInstance()Ljava/lang/Object; move-result-object v8 new-array v3, v9, [Ljava/lang/Object; aput-object v2, v3, v6 invoke-virtual {v1, v8, v3}, Ljava/lang/reflect/Method;->invoke(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object; const/4 v9, 0x1 const/4 v6, 0x0 invoke-static {v6, p1}, Lcom/ctf/crackme1/StringHolder;->get(ILjava/lang/String;)Ljava/lang/String; move-result-object v3 invoke-static {v3}, Ljava/lang/Class;->forName(Ljava/lang/String;)Ljava/lang/Class; move-result-object v0 const/4 v3, 0x4 invoke-static {v3, p1}, Lcom/ctf/crackme1/StringHolder;->get(ILjava/lang/String;)Ljava/lang/String; move-result-object v3 new-array v4, v9, [Ljava/lang/Class; const-class v5, [B aput-object v5, v4, v6 invoke-virtual {v0, v3, v4}, Ljava/lang/Class;->getDeclaredMethod(Ljava/lang/String;[Ljava/lang/Class;)Ljava/lang/reflect/Method; move-result-object v1 invoke-virtual {v0}, Ljava/lang/Class;->newInstance()Ljava/lang/Object; move-result-object v8 new-array v3, v9, [Ljava/lang/Object; aput-object v2, v3, v6 invoke-virtual {v1, v8, v3}, Ljava/lang/reflect/Method;->invoke(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;
一开始拿到了inputString.getBytes(),之后从StringHolder里拿String,forName拿到类,再拿String,再getDeclaredMethod反射拿方法,最后invoke。所以先看看StringHolder.get(int index, String des_key)的返回值吧,用index作为索引去查map,没查到就用md5值作为des_key去解密,得到的固定字符串后放入map并返回。所以我们手动写一下DES解密程序,结果如下:
int index
String dec
0
com.ctf.crackme1.util
1
android.os.Debug
2
isDebuggerConnected
3
b <--utils里的b,循环++
4
d <--utils里的d,循环xor
5
e <--utils里的e,高低换位
再看看utils类里的方法,方法a是一个反调试,但好像从来没有被调用过,可能是出题人随手debug时候留下的,同样c、f、g也是一样的,分别调用3种加密算法,出题人debug用。
我们继续看那段很长的代码,懒人方法先试一下,拉到最下面,发现还有一句
1 2 3 4 5 6 7 8 9 aput-object v2, v3, v6 invoke-virtual {v1, v8, v3}, Ljava/lang/reflect/Method;->invoke(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object; invoke-static {v2}, Lcom/ctf/crackme1/Check;->check([B)Z move-result v7 return v7 .end method
显然是将结果byet[]v2代入了Check.check()这个native方法中。
懒一点,在第10个invoke后面删掉,直到native check之前,再用apktools打包起来,丢进JEB里看看,哇,真是太感人了!!!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public static boolean c (String arg10, String arg11) throws Exception { byte [] v2 = arg10.getBytes(); Class v0 = Class.forName(StringHolder.get(0 , arg11)); v0.getDeclaredMethod(StringHolder.get(3 , arg11), byte [].class).invoke(v0.newInstance(), v2); v0 = Class.forName(StringHolder.get(0 , arg11)); v0.getDeclaredMethod(StringHolder.get(4 , arg11), byte [].class).invoke(v0.newInstance(), v2); v0 = Class.forName(StringHolder.get(0 , arg11)); v0.getDeclaredMethod(StringHolder.get(5 , arg11), byte [].class).invoke(v0.newInstance(), v2); v0 = Class.forName(StringHolder.get(0 , arg11)); v0.getDeclaredMethod(StringHolder.get(3 , arg11), byte [].class).invoke(v0.newInstance(), v2); v0 = Class.forName(StringHolder.get(0 , arg11)); v0.getDeclaredMethod(StringHolder.get(4 , arg11), byte [].class).invoke(v0.newInstance(), v2); v0 = Class.forName(StringHolder.get(0 , arg11)); v0.getDeclaredMethod(StringHolder.get(3 , arg11), byte [].class).invoke(v0.newInstance(), v2); v0 = Class.forName(StringHolder.get(0 , arg11)); v0.getDeclaredMethod(StringHolder.get(4 , arg11), byte [].class).invoke(v0.newInstance(), v2); v0 = Class.forName(StringHolder.get(0 , arg11)); v0.getDeclaredMethod(StringHolder.get(3 , arg11), byte [].class).invoke(v0.newInstance(), v2); return Check.check(v2); }
显然是在一直调用utils.b(input_bytes)、utils.d(input_bytes)、utils.e(input_bytes)对输入的字符串进行操作。
第二种,思路就是硬看smali咯,也不难,注释过的如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 const/4 v7, 0x0 invoke-virtual {p0}, Ljava/lang/String;->getBytes()[B move-result-object v2 // v2 = inputString.getBytes() const/4 v9, 0x1 const/4 v6, 0x0 invoke-static {v6, p1}, Lcom/ctf/crackme1/StringHolder;->get(ILjava/lang/String;)Ljava/lang/String; move-result-object v3 // v3 = StringHolder.get(0, key) invoke-static {v3}, Ljava/lang/Class;->forName(Ljava/lang/String;)Ljava/lang/Class; move-result-object v0 // v0 = Class.forName(v3) const/4 v3, 0x3 invoke-static {v3, p1}, Lcom/ctf/crackme1/StringHolder;->get(ILjava/lang/String;)Ljava/lang/String; move-result-object v3 // v3 = StringHolder.get(3, key) new-array v4, v9, [Ljava/lang/Class; // v4 = new Class[1] const-class v5, [B // v5 = byte[].class aput-object v5, v4, v6 // v4[0] = v5 invoke-virtual {v0, v3, v4}, Ljava/lang/Class;->getDeclaredMethod(Ljava/lang/String;[Ljava/lang/Class;)Ljava/lang/reflect/Method; move-result-object v1 // v1 = getDeclaredMethod(v0, v3, v4) invoke-virtual {v0}, Ljava/lang/Class;->newInstance()Ljava/lang/Object; move-result-object v8 // v8 = v0.newInstance() new-array v3, v9, [Ljava/lang/Object; aput-object v2, v3, v6 // v3[0] = inputBytes invoke-virtual {v1, v8, v3}, Ljava/lang/reflect/Method;->invoke(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object; // v1.invoke(v8, v3)
由于3、4、5是随机出现的,即bde也随机出现,所以需要写个正则来提取一下,我写了这样的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 import ref = open ("Check.smali" , 'rb' ) data = f.read() patter = re.compile ( r"move-result-object.*?const/4 .*?0x(\d).*?Lcom/ctf/crackme1/StringHolder;" , flags=re.DOTALL) res = patter.findall(data) print resfor i in res: if i == '0' or i == '1' : res.remove(i) for i in range (len (res)): if res[i] == '3' : res[i] = 'b' elif res[i] == '4' : res[i] = 'd' elif res[i] == '5' : res[i] = 'e' else : pass print resprint '' .join(res)print len (res)
结果是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。
1 2 3 4 5 6 7 8 9 10 11 12 13 void init_array_fun1 () { void *a2; void *v1; int v2; v1 = p_destroy; v2 = 0 ; j_j_sigaction(14 , (const struct sigaction *)&v1, 0 ); a2 = p_exit; j_j_j_bsd_signal(18 , (int )p_exit); j_j_j_bsd_signal(17 , (int )a2); }
init_2有2个功能,一个功能为解密一段函数并且执行,另一个功能为反调试。
1 2 3 4 5 6 7 8 9 int init_array_fun2 () { int pid; decrypt_function_and_call((int )p_224c, (int )off_A024); pid = j_j_getpid(); j_j_hook_got((int )&unk_A278, pid, (int )"libc." , (int )"fwrite" , (int )p_j_j_xor13, (int )p_xor13); return 0 ; }
0x29BC接收2个参数,分别为 “待解秘函数的地址” 和 “待解秘函数的长度”,解密方法比较简单,是直接进行取反。其实这一大段看不明白,有几个可疑的特征:
多处mprotect调用,
我们输入的是待解密的函数地址+1,这里会主动减去一个1,
待解密函数本身是一片乱码,无论如何P、Analyze,都是无法组成正常语句的,而且待解密函数存在xref
对长度进行了mod和div等操作,怀疑是为了对其或者别的原因,看起来就是在解密东西了,
期间对待解密的函数进行了写操作,是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的地方也有一个解密,我们顺便一起解掉好了。
1 2 3 4 5 for i in range (len (data)): if i >= 0x1CB8 and i < 0x1DB4 : data[i] = chr (~ord (data[i]) & 0xff ) if i >= 0x224C and i < 0x22FC : data[i] = chr (~ord (data[i]) & 0xff )
之后打开新的so文件,继续分析这个0x224c。
先将0xA254写为0~31,再调用0x26D0函数。
0x26D0是一个反调试函数,检测/proc/pid/status,不断fork自己,pthrace等骚操作,并且有sleep(),管他呢,反正我们也无法调试。
.init段分析完毕,下面我们看JNI_OnLoad。
很不幸,这个函数真的什么都没做,纯属逗你玩,当时我还研究了半天有什么玄机,后来发现好像真的没东西。。。
剩下的只有JNI_Check_check了,仔细分析一下其流程。
上来先定义好几个buffer,意义暂时未知,我们接着往下看。
栈上有0x20个字节,全部写为0
global 指针,0xA274 _BYTE* calc_ptr = malloc(0x20),之后全部赋值为0
strncpy将输入的flag复制到 calc_ptr上,长度最多为32
解密并且调用0x1cb8
调用0x55ec
将参与0x55ec计算后的结果赋值给calc_ptr[0:16]
将calc_ptr[16:32]
的元素,加等于其下标
进行fwrite(注意:此时触发xor13函数)
与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代码如下:
1 2 3 4 5 6 7 8 9 10 void __fastcall AES_enc (_BYTE *calc_ptr, int a2, int key) { void *huge_buffer; *(_DWORD *)off_9F18 = 4 ; *off_9F14 = 10 ; huge_buffer = j_j_malloc(44 * *off_9F10); passing_sbox((_BYTE *)key, (int )huge_buffer); enc_data((int )calc_ptr, a2, (int )huge_buffer); }
其中pssing_sbox函数第一句是计算轮数
key_round = (*off_9F14 + 1) * *off_9F10;
算出来的结果是44,44代表的就是128位AES加密,果然解密时候就成功了。
好了,之后的步骤就简单了,正常的赋值和加法,最后进行xor,就得到target啦!
呼!结束了!总结一下,我们需要过掉如下的加密:
Java层的1000次bde处理
RC4加密全部
将前16字节写为,AES-128加密后数据的前16字节
将后16字节加等于其下标
将全部字节xor13
与char target[32]进行比较
剩下的工作就是提取各种参数,写脚本的事情了,这里就不详细阐述了。
PPP能短时间解出这题,真心佩服,学习一个;一年后解出来,也算是一点小小的进步了,分享给大家咯。。。
请容许我小开心一下,一年内的安卓题,终于全都ak掉了,以后可能会分享一下这一年来遇到的一些有趣的安卓题吧,当然鸽掉的可能性很大,hhhhhhh
附:最终脚本(alictf_data.py从文章开头提供的链接下载)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 import alictf_datafrom Crypto.Cipher import AES, ARC4rc4_key = alictf_data.rc4_key aes_key = alictf_data.aes_key target = alictf_data.target xor_13 = 0x13 bde_list = alictf_data.bde_list for i in range (32 ): target[i] ^= xor_13 print '' .join(chr (i) for i in target).encode('hex' )cipher = AES.new('' .join(chr (i) for i in aes_key), AES.MODE_ECB) aes_dec = cipher.decrypt('' .join(chr (i) for i in target[0 :16 ])) print aes_dec.encode('hex' )for i in range (16 ): target[i] = ord (aes_dec[i]) for i in range (16 , 32 ): target[i] = (target[i] - i) & 0xff cipher = ARC4.new('' .join(chr (i) for i in rc4_key)) rc4_dec = cipher.decrypt('' .join(chr (i) for i in target[0 :32 ])) print rc4_dec.encode('hex' )target = rc4_dec.rstrip('\0' ) target = [ord (i) for i in target] def b_decrypt (in_data ): size = len (in_data) for ii in range (size - 1 , -1 , -1 ): if ii == 0 : in_data[ii] = (in_data[ii] - in_data[size - 1 ]) & 0xff else : in_data[ii] = (in_data[ii] - in_data[ii - 1 ]) & 0xff in_data[ii] = (in_data[ii] - 58 ) & 0xff return in_data def d_decrypt (in_data ): size = len (in_data) for ii in range (size - 1 , -1 , -1 ): if ii == 0 : in_data[ii] = (in_data[ii] ^ in_data[size - 1 ]) & 0xff else : in_data[ii] = (in_data[ii] ^ in_data[ii - 1 ]) & 0xff in_data[ii] ^= 150 return in_data def e_decrypt (in_data ): size = len (in_data) for ii in range (size): in_data[ii] = ((in_data[ii] & 0xf ) << 4 ) | ((in_data[ii] & 0xf0 ) >> 4 ) return in_data bde_list = bde_list[::-1 ] for i in range (len (bde_list)): cur = bde_list[i] if cur == 'b' : target = b_decrypt(target) elif cur == 'd' : target = d_decrypt(target) elif cur == 'e' : target = e_decrypt(target) else : print "ERROR" print targetprint '' .join(chr (i) for i in target)