该crackme主要实现都在so中,用ida加载libqihoo.so,出现以下错误
第一个错误说明是节区头部表格的表项大小错误,第二个错误是指节区头部表格的大小或偏移值错误。不管它,点击“Yes”继续加载。找到JNI_OnLoad函数,发现该函数已经加密:
我们知道so 文件加载时首先会查看 .init 或 .init_array 段是否存在,如果存在那么就先运行这段的内容,如果不存在的话那么就检查是否存在JNI_OnLoad,存在则执行。所以JNI_OnLoad的解密可能在 .init 或 .init_array 段中。
因为 .init 或者 .init_array 在 IDA 动态调试的时候是不会显示出来的,所以需要静态分析出这两段的偏移量然后动态调试的时候计算出绝对位置,然后在 make code(快捷键:c),这样才可以看到该段内的代码内容。
查看 .init_array 段的地址有两种办法:
(1).可以使用 IDA 加载 .so 文件,按ctrl+s快捷键查看 “Segments” 视图,这里会列出不同类型的代码段信息,如下图所示。
(2).可以使用二进制工具 readelf 来查看 .so 文件的结构,在 OS X 上面可以使用 greadelf 代替。
从上述(1)可以看出,并没有显示该so的.init 或者 .init_array段,所以我们需要对该so进行修复。
因为节区头部表的偏移值e_shoff为0x2118c(在0x20h处),所以根据ELF文件的结构,从该偏移值开始到文件结尾的数据为整个节区头部表。由于节区头部表项的大小(e_shentsize)固定为0x28,所以我们可以由:(0x214fb - 0x2118c + 1)/0x28 = 0x16 得出真正的节区头部表项数目(e_shnum)为0x16。
下面再来看e_shstrndx字段,我们从ELF文件中可以明显地看出,字符串表节区为最后一个节区,所以它的索引值应当为0x15。
根据上述规则将对应数值修改好后保存文件,然后再次加载修复后的so文件,已经不会报错了,按ctrl+s快捷键也可以查看到.init_array段信息了:
IDA 定位到.init_array段,可以看到.init_array段会执行__gnu_armfini_26函数
该函数含义大量花指令,为了便于分析,我们在该函数下断点,对其进行动态调试。通过动态调试,我们会发现该so中花指令与真实指令的关系如下图所示:
__gnu_armfini_26函数清除花指令后的汇编代码如下:
BL __gnu_armfini_30 MOV R4, R0 MOV R0, #0x28 ; '(' BL sysconf ; 获取系统的cpu个数和可用的cpu个数 ADD R1, R4, #0x8A00 BIC R0, R4, #0xFF0 MOV R2, #7 ADD R1, R1, #0xEC BIC R0, R0, #0xF BL __gnu_arm_fini_06 MOV R1, #0x8A00 MOV R0, R4 ADD R1, R1, #0xEC BL __gnu_armfini_29 ADD R1, R4, #0x8A00 MOV R0, R4 ADD R1, R1, #0xEC BL sub_B6E37614
其中调用的__gnu_armfini_29函数比较可疑
_arm_aeabi_6去掉花指令后是
STMFD SP!, {R3-R8,R10,LR} MOV R4, R0 MOV R5, R1 ;0000000C MOV R8, R2 MOV R3, #0 :loc_B6EFDA74 STRB R3, [R8,R3] ;0x00 将r8开始的地址依次填充0-0x99 ADD R3, R3, #1 ;0+1 CMP R3, #0x100 BNE loc_B6EFDA74 MOV R3, #0 MOV R6, R3 STRB R3, [R8,#0x100] ;R8+#0x100地址处的内容置0 STRB R3, [R8,#0x101] ;R8+#0x101地址处的内容置0 MOV R7, R8 ADD R10, R8, #0x100 ;R8:BEF9C6F4, R10:BEF9C7F4 MOV R0, R3 :loc_B6EFD914 LDRB R2, [R4,R0] ;r2=0x96,0xE6,0x57 LDRB R3, [R7] ;R3=0x00, R3=0xC2,0x00 ADD R0, R0, #1 ;r0=0x01, 0x02, 0x03 MOV R1, R5 ;R1=R5=0x0C ADD R2, R2, R3 ;R2 = R2+R3=0xC6+0x00, R2 = R2+R3=0x96+0xC2=0x158, R2 = R2+R3=0xE6+0x00=0xE6 ADD R6, R2, R6 ;R6 = R2+R6=0xC6+0x00, R6 = R2+R6=0x158+0xC6=0x21E, R6 = R2+R6=0xE6+0x01E=0x104 AND R6, R6, #0xFF ;R6 = 0xC6, 0x01E, 0x004 LDRB R2, [R8,R6] ;r2=0x00, 0xEF, 0xE9 STRB R2, [R7],#1 ;STR R0,[R1], #8 将R0中的字数据写入以R1为地址的存储器 中,并将新地址R1+8写入R1。 STRB R3, [R8,R6] ;r3=0x00, 0xC2, 0x00 BL __aeabi_idivmod ;执行完后r1=r0=0x01,0x02,0x03 CMP R7, R10 ;r7=0xC2,r10=0x00 AND R0, R1, #0xFF ;r0:0x01 BNE loc_B6EFD914 LDMFD SP!, {R3-R8,R10,PC}
这实际上就是RC4算法的第一个步骤(参考:https://www.jianshu.com/p/fcfdcc3ff9d5):
/*
初始化状态向量S和临时向量T,供keyStream方法调用
*/
void initial() { for(int i=0;i<256;++i){ S[i]=i; T[i]=K[i%keylen]; } }
_gnu_arm_message函数具有rc4算法的典型特征:
_gnu_armfini_29这个函数解密从0x75EEC6DC地址开始,大小为0x8AEC内存区域的数据,实际上是对地址为“基址0x75ED5000+0x176dc”,大小为0x8AEC的内存区域数据进行解密。
解密完毕后将该so dump下来,dump的起始地址及大小可以通过ida的Modules菜单获取:
dump脚本:
static main(void) { auto fp, begin, end, ptr; fp = fopen("d:\\dump.so", "wb"); begin = 0x75ED5000; end = begin + 0x23000; for ( ptr = begin; ptr < end; ptr ++ ) fputc(Byte(ptr), fp); }
按快捷键“shift+F2”打开脚本编写窗口写入上述脚本代码,点击“Run"运行该代码就可以在D盘根目录看到dump下来的so文件dump.so。
用IDA打开dump.so,定位到JNI_OnLoad函数,按“C”键,将数据转换成代码,按”F5“查看反编译的伪代码,按“Y”键修正变量类型,得到如下的代码:
我们知道RegisterNatives的函数原型是:
jint RegisterNatives(jclass clazz, const JNINativeMethod* methods,jint nMethods)
第二个参数是JNINativeMethod结构体
typedef struct { const char* name; const char* signature; void* fnPtr; } JNINativeMethod;
结构体的第三个参数是函数指针,该结构体的偏移地址为0x22004,定位到该偏移地址处发现没有正常把函数指针解析出来,需要进一步调试。
重新打开libqihoo.so,在 __gnu_armfini_29函数处下断点,进行动态调试。按F9运行到这里后,函数会进行内存区块解密,解密完成之后,在Modules菜单中找到libqihoo.so,点开,我们就可以看到“verify”和“JNI_OnLoad”函数了,分别下断点(注意,一定要在解密完之后,即执行完__gnu_arm_message函数后再下断点),如图所示:
再按F9就来到了JNI_OnLoad函数处
定位到JNINativeMethod结构体地址,从ida中我们可以看到该结构体函数指针指向verify函数:
继续F9,执行到verify函数。
在verify函数中调用了__gnu_Unwind_8和__gnu_Unwind_6函数。
__gnu_Unwind_8函数的作用是将jstring转换成为c/c++中的char*
从java程序中传过去的String对象在本地方法中对应的是jstring类型,jstring类型和c中的char*不同,所以如果你直接当做char*使用的话,就会出错。因此在使用之前需要将jstring转换成为c/c++中的char*,这里使用JNIEnv提供的方法转换。
/* java jstring turn to c/c++ char* */ char* jstringToChar(JNIEnv* env, jstring jstr) { char* pStr = NULL; jclass jstrObj = (*env)->FindClass(env, "java/lang/String"); jstring encode = (*env)->NewStringUTF(env, "utf-8"); jmethodID methodId = (*env)->GetMethodID(env, jstrObj, "getBytes", "(Ljava/lang/String;)[B"); jbyteArray byteArray = (jbyteArray)(*env)->CallObjectMethod(env, jstr, methodId, encode); jsize strLen = (*env)->GetArrayLength(env, byteArray); jbyte *jBuf = (*env)->GetByteArrayElements(env, byteArray, JNI_FALSE); if (jBuf > 0) { pStr = (char*)malloc(strLen + 1); if (!pStr) { return NULL; } memcpy(pStr, jBuf, strLen); pStr[strLen] = 0; } env->ReleaseByteArrayElements(byteArray, jBuf, 0); return pStr; }
实际上上述转换的字符串就是用户输入的用户名、邮箱以及序列号。
在__gnu_Unwind_6函数中会判断用户输入的序列号长度是否为8。
调用__gnu_Unwind_1函数,此函数又有很多花指令
再次调用_gnu_armfini_29函数,通过前面的分析我们知道这是一个RC4加解密函数。
在这里实际上是取“用户名+邮箱”组成的字符串的的前四个字节进行加密,如图所示:
然后就是一个比较关键的函数__gnu_Unwind_4了
这个函数实际上是实现了SHA1加密算法,以下为该算法初始化缓冲区时的特征:
__gnu_Unwind_4将”用户名+邮箱“组成的字符串(包括前四个被RC4算法加密过的字符)进行SHA1加密后,通过__gnu_Unwind_11函数与用户输入的序列号进行对比,从而判断是否破解成功。
附:其它方法
我们重新打开一个ida,加载libqihoo.so,按快捷键“shift+F2”打开脚本编写窗口编写脚本对该内存区域进行解密。如下图所示:
这样得到的就是完全解密的so文件了。
脚本内容:
import idaapi def rc4(data, key): """RC4 encryption and decryption method.""" S, j, out = list(range(256)), 0, [] for i in range(256): j = (j + S[i] + ord(key[i % len(key)])) % 256 S[i], S[j] = S[j], S[i] i = j = 0 for ch in data: i = (i + 1) % 256 j = (j + S[i]) % 256 S[i], S[j] = S[j], S[i] out.append(chr(ord(ch) ^ S[(S[i] + S[j]) % 256])) return "".join(out) key=idaapi.get_many_bytes(0x20434,12) addr= 0x176dc; data=idaapi.get_many_bytes(addr,0x8aec) decode=rc4(data,key) idaapi.patch_bytes(addr, decode)
样本下载地址:
链接:https://pan.baidu.com/s/1pMXryhP 密码:clun