1 概述
该crackme的核心验证程序在libqihoo.so中。这个动态库经过了加密、清除elf文件的节区信息、花指令等处理,无法直接使用IDA进行静态分析。所以需要使用动态调试。不过如果仅仅使用动态跟踪的话,是很难分析出最后结果的。所以应当想法获得正确的libqihoo.so文件,然后结合动态调试、静态分析,最终分析出验证机制。
2 破解方法
2.1 获取较为正确的libqihoo.so文件
这里之所以使用较为正确,是因为我从内存dump出来的libqihoo.so文件并不完全正确,不过核心代码均有了,只是IDA没能自动识别出export函数(malloc,memset等),需要我们分析的时候自己加以判断,只要熟悉这些函数就很容易判断出来。
获取so文件的方法很简单:使用动态调试附加上crackme之后,运行几次确保so已经加载到内存,然后ctrl+s查看so在内存中的起始位置,再到hex-view窗口中dump出这段内存,另存为libqihoo.so,然后另起一个IDA静态分析即可。IDA分析后的可以发现在Function widows有了我们关心的JNI_OnLoad和verify函数:
如图所示:

2 开始分析
通过分析发现JNI_OnLoad函数将JAVA层的verify函数同so中的verify函数进行了关联。所以我们直接开始分析verify函数即可。Ps:方便大家查看,我将我分析过程中提取的关键代码也放出来,在360crackmeARMcode.c中,大家可以参照着看,那里面有详细注释。其中逻辑简单的我保留了汇编代码,逻辑复杂的就手工转换成了c代码。
2.1 JAVA层传递的参数
JAVA层传递三个参数username, emailaddr, serialNum。这三个参数均为String类型。
2.2 so文件的花指令模式
Libqihoo.so中加入了大量的花指令,这无疑增加了逆向分析的工作量。不过只要理清了它的花指令模式,我们完全可以一劳永逸地解决这个麻烦。它的花指令模式如下:
LDMFD SP!, {R0} 真正指令 STMFD SP!, {R0} ADRL R0, LOC_XXX SUB R0, R0, #4 BX R0 为了方便分析,我是手工将花指令代码中的真正指令提取出来,然后进行分析的。看起来很费劲,其实是大大缩减了分析时间。
解决了花指令问题,就开始真正分析了。
2.3 本地验证流程
1)在verify函数中调用__GNU_Unwind_8,将三个参数(String型的字符串)转换为native层的char* username, * emailaddr, * serialNum。
2)在verify函数中调用__GNU_Unwind_6。此函数完成验证的核心功能。 下面开始对__GNU_Unwind_6函数进行分析:
①新建一个结构体(命名为inputinfo),该结构体如下:
代码:
typedef struct inputInfo{ char *username; char *emailaddr; char *serialNum; }inputInfo;
然后给inputInfo内的三个成员赋值。
②判断serialNum长度是否为8,是就继续,否则退出。
③调用__GNU_Unwind_1。在__GNU_Unwind_1中依次调用__GNU_Unwind_2、__GNU_Unwind_3、__GNU_armfini_29、__GNU_Unwind_4、__GNU_Unwind_11、__GNU_Unwind_10。下面对这几个函数加以说明:
__GNU_Unwind_2: 此函数主要调用__GNU_armfini_25和__GNU_armfini_23两个函数来获取com/qihoo/qhcrackme/MainActivity;->show(Ljava/lang/String;)V方法id。__GNU_armfini_25是一个解密函数,解密出一个base64字符串,__GNU_armfini_23是一个base64解码函数,将__GNU_armfini_25解出的字符串转换成明文。
__GNU_Unwind_3:完成一个功能:sprintf(info, "%s%s", input.username, input.emailaddr);
★__GNU_armfini_29:这是本程序中最重要的两个函数之一。该函数首先将username和emailaddr连接在一起(这里为方便称之为info),然后调用__arm_aeabi_6和__gnu_arm_message完成对info信息的变换。__arm_aeabi_6用于构造一个0x102字节的转换表 ,__gnu_arm_message用于将input的前4字节进行变换(使用前面生成的转换表)。这里需要特别说明:由于程序固定转换info的前4字节,所以如果我们输入的username+emailaddr的字节数小于4的话就会发生预料不到的错误,因此一定要确保两者长度之和大于4。
__GNU_Unwind_4:sha1_hash函数,对变换后的info进行hash,得到sha1_result ★__GNU_Unwind_11:本程序最重要的两个函数之二。该函数功能如下:
serialNum[0] = sha1_result[0]; serialNum [1] = sha1_result [2]; serialNum [2] = sha1_result [5]; serialNum [3] = sha1_result [9]; serialNum [4] = sha1_result [14]; serialNum [5] = sha1_result [20]; serialNum [6] = sha1_result [27]; serialNum [7] = sha1_result [35];
依次进行对比。
__GNU_Unwind_10:根据__GNU_Unwind_11的对比结果来进行不同处理,如果对比成功,就调用前面获得的show方法,显示”you passed…”,否则就什么都不做。
至此整个程序的验证逻辑分析完毕。下面开始编写注册机代码:
呃…算了,时间有限…大家直接看附件createSerialNum中的代码吧~~
Ps:这个crackme有两个版本,我分析的是最新的版本(现今官方指定版)。听说第一个版本对sha1进行了部分变异,原帖参考http://bbs.pediy.com/showthread.php?...代码即可~()
附一组验证通过的序列号:
Username:wanchouchou
Emaildaar:[email protected]
SerialNum: 56c0e1fb
3 总结
以前都是静态分析,这是我第一次动态分析so,第一次遇到花指令,第一次搞这么复杂的代码,断断续续花了6天的时间才搞定….不过收获还是很大的,通过360的第一个题,学习了jni的编写,第3个题初识了so的动态调试。不过还是有一个疑惑,360是如何清除elf的节区信息,使得直接IDA静态分析失效的呢?清除过后,又是如何在加载这个so文件的时候,还原的呢?望大牛们不吝赐教啊!谢谢!
转换表:
代码:
5 4b e1 9f 78 eb 90 98 a5 a8 2c dd af a0 46 1 43 49 c1 79 d4 70 4d 3b 28 d7 19 37 da 30 15 e5 9b 10 c8 2f 59 bb 3d 3 e4 45 ee fc be b3 6b 31 2d a2 b7 93 96 ae 17 a9 a4 a c0 e2 53 82 1f e0 61 5e c2 68 d6 67 e9 86 94 ce 47 8 14 b de 6f 55 4a 58 52 c 3c 32 27 74 e8 62 95 c3 f5 c4 97 7e 1c 64 38 bd 22 57 84 2b ad 80 b5 e7 8c 36 cf 89 65 c9 6a f9 d2 25 0 2a 4 ca 1a 8d ff c5 b6 4c b8 f3 df 2e ec 5f fd 6d 7d 7 5d 9e 35 8b 8a b9 d1 56 69 29 cd 60 d9 ed 54 81 40 d5 8e d3 e6 cb a3 b1 9c dc 9d 24 f0 9 1b 77 b2 26 db 71 2 83 a1 39 b4 72 fe 4e d8 3f f6 fb 50 21 18 6 23 bf 1e 85 87 6c fa d0 16 bc 42 9a 76 51 13 92 34 44 7f a7 ea b0 ba 5b f7 cc 63 75 41 e 7c 73 f4 12 48 11 3e 5c 8f 99 66 c6 3a f a6 5a 6e 7b f1 7a f2 aa 91 f8 ac 33 88 20 e3 1d ab ef c7 d 4f 0 0
注:本帖由看雪论坛志愿者PEstone 重新将文档整理排版,若和原文有出入,以原作者附件为准
|