該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
