该 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://www.cnblogs.com/goodhacker/p/8395240.html