兩種姿勢批量解密惡意驅動中的上百條字串
作者:JiaYu 轉自公眾號:信口雜談
1. 概述
在 360Netlab 的舊文 《“雙槍”木馬的基礎設施更新及相應傳播方式的分析》 中,提到了 雙槍 木馬傳播過程中的一個惡意驅動程序 kemon.sys ,其中有經過自定義加密的 Ascii 字符串和 Unicode 字符串 100+ 條:

這在 雙槍 木馬的傳播鏈條中只是一個很小的技術點,所以文中也沒說具體是什么樣的加密算法以及怎樣解密,供分析員更方便地做樣本分析工作。但這個技術點還算有點意思,尤其是對逆向入門階段的朋友來說,可以參考一下解法。最近又碰到了這個驅動程序的變種,跟團隊的老司機討教了一番,索性寫篇短文記錄一下。
感謝老司機們解惑。也歡迎各路師傅不吝賜教,提一些更快準狠的解法。
2. 樣本概況
MD5: b001c32571dd72dc28fd4dba20027a88
2.1 字符串加密情況
驅動程序中用到的 100+ 條字符串都做了自定義加密處理,在設置完各 IRP 派遣函數和卸載例程之后,依次解密這些字符串。IDA 中打開樣本,部分解密過程如下:

整個解密過程的函數是 sub_100038 ,里面會多次調用兩個具體的解密函數:sub10003871 和 sub_10003898。前者解密 Ascii 字串,后者解密 Unicode 字串,都有兩個參數:arg1—>要解密的字符串地址;arg2—>字符串長度。后面會把這兩個函數分別命名為 DecryptAsciiStr 和 DecryptUnicodeStr 。這兩個函數在 IDA 中看到的 xrefs 狀況如下:


2.2 加密算法
前面說了,算法不復雜。以 DecryptAsciiStr 函數為例:

反編譯看看:

DecryptUnicodeStr 算法其實相同,只是因為字節構成不同,所以是兩個解密函數分開寫:

簡單總結起來,這套解密過程其實就是:把當前字節后面特定偏移處的字節與 0xC 異或,然后替換掉當前字節,把解密后的字節寫入到當前位置,即完成解密。本人對密碼學不熟,不知道這是不是已有名號的加密算法,看起來像是 凱撒密碼 的變形加強版?對此有了解的朋友歡迎指教。
3. 解密
了解了上面的情況之后,就該著手解密這百十多條字符串了。既然是用 IDA 來分析這個樣本,理想的狀況應該是把這些字串批量解出來,直接在 IDA 中呈現,然后就可以進行后續分析了。既然是要自動化批量解密,寫 IDAPython 應該算是最便捷的做法了。最終效果如圖:

3.1 姿勢 1——自行實現解密算法
首先想到的思路是:就兩個解密算法,而且不復雜,不妨直接寫個 IDAPython 腳本,實現這兩個解密算法。解密之后把明文字串直接寫到 IDB 文件中,在 IDA 中呈現。兩個解密算法的 Python 版本分別如下(附帶對 IDB 的 Patch 操作):


這里稍微解釋一下 make unicode str 時的操作:
- old_type = idc.GetLongPrm(INF_STRTYPE)
- idc.SetLongPrm(idc.INF_STRTYPE, idc.ASCSTR_UNICODE)
- idc.MakeStr(argv[0], argv[0]+(argv[1]*2))
- idc.SetLongPrm(idc.INF_STRTYPE, old_type)
在 IDA 的 UI 界面中,可以選擇生成的字符串的類型(如下圖),快捷鍵只有一個 A,對應的 idc 函數是 idc.MakeStr(0。然而 ida.MakeStr() 函數默認是生成 Ascii 字串的,要想生成 Unicode 字串,就需要調用 idc.SetLongPrm() 函數設置一下字符串的類型。

IDA 中支持的字符串類型如上圖,相應地,在 idc 庫中的定義如下:
- ASCSTR_C = idaapi.ASCSTR_TERMCHR # C-style ASCII string
- ASCSTR_PASCAL = idaapi.ASCSTR_PASCAL # Pascal-style ASCII string (length byte)
- ASCSTR_LEN2 = idaapi.ASCSTR_LEN2 # Pascal-style, length is 2 bytes
- ASCSTR_UNICODE = idaapi.ASCSTR_UNICODE # Unicode string
- ASCSTR_LEN4 = idaapi.ASCSTR_LEN4 # Pascal-style, length is 4 bytes
- ASCSTR_ULEN2 = idaapi.ASCSTR_ULEN2 # Pascal-style Unicode, length is 2 bytes
- ASCSTR_ULEN4 = idaapi.ASCSTR_ULEN4 # Pascal-style Unicode, length is 4 bytes
- ASCSTR_LAST = idaapi.ASCSTR_LAST # Last string type
所以,要生成 Unicode 格式的字串,需要先用 idc.SetLongPrm() 函數設置一下字符串類型。其中 idc.INF_STRTYPE 即代表字符串類型的常量,在 idc 庫中的定義如下:

用 Python 實現了解密函數之后,如何模擬這一波解密過程把這 100+ 條字串依次解密呢?這里可以結合 IDA 中的 xrefs 和 idc.PrevHead() 函數來實現:
- 先通過 xrefs 找到調用兩個解密函數的位置;
- 再通過 idc.PrevHead() 定位到兩個解密函數的參數地址,并解析出參數的值;
- 執行解密函數,將解密后的明文字串寫回 IDB 并 MakeStr。
3.2 姿勢 2——指令模擬
這個樣本中的字串解密算法并不復雜,所以可以輕松寫出 Python 版本,并直接用 IDAPython 腳本在 IDA 中將其批量解密。那如果字串解密算法比較復雜,用 Python 實現一版顯得吃力呢?
這時不妨考慮一下指令模擬器。
近幾年,Unicorn 作為新一代指令模擬器在業界大火。基于 Unicorn 的 IDA 指令模擬插件也不斷被開發出來,比如簡捷的 IdaEmu 和 FireEye 開發的功能強大的 Flare-Emu。指令模擬器可以模擬執行一段匯編指令,而 IDA 中的指令模擬插件可以在 IDA 中模擬執行指定的指令片段(需要手動指定起始指令地址和結束指令地址,并設置相關寄存器的初始狀態)。這樣一來,我們就可以在 IDA 中,利用指令模擬插件來模擬執行上面的批量解密指令,解密字串的匯編指令模擬執行結束,字串也就自然都給解密了。
本文 Case 的指令模擬姿勢基于 Flare-Emu。
不過,這個姿勢需要注意兩點問題:
- 指令模擬器無法模擬系統 API ,如果解密函數中有調用系統 API 的操作,那指令模擬這個姿勢就要費老勁了。
- 所謂模擬指令執行,真的只是模擬,而不會修改 IDA 中的任何數據。這樣一來,需要自己把指令模擬器執行結束后的明文字串 Patch 到 IDB 文件中,這樣才能在 IDA 中看到明文字串。
3.2.1 hook api
第 1 點問題,IdaEmu 中需要自己實現相關 API 的功能,并對指令片段中相應的 API 進行 Hook,才能順利模擬。比如下圖示例中,指令片段里調用了 _printf 函數,那么就需要我們手動實現 _printf 的功能并 Hook 掉指令片段中的 _printf 才行:

而 Flare-Emu 就做的更方便了,他們直接在框架中實現了一些基礎的系統 API,而不用自己手動實現并進行 Hook 操作:

之所以提這么個問題,是因為這個 kemon.sys 樣本中的批量解密字串的過程中,涉及了對 memcpy 函數的調用:

這樣一來,直接用 Flare-Emu 來模擬執行應該是個更便捷的選項。
3.2.2 Patch IDB
第 2 點問題,將模擬結果寫回 IDB 文件,在 IDA 中顯示。
首要問題是如何獲模擬執行成功后的結果——明文字符串。前面描述字串解密算法時說過,解密后的字節(Byte)會直接替換密文中的特定字節,把密文的前 dataLen 個字節解密出來,就是明文字串。這個字節替換的操作,其實對應 Unicorn 指令模擬器中定義的 MEM_WRITE 操作,即寫內存,而且,字串解密過程中也只有這個字串替換操作會寫內存 。恰好,Flare-Emu 中提供了一個 memAccessHook() 接口(如下圖),可以 Hook 多種內存操作:
- memAccessHook can be a function you define to be called whenever memory is accessed for reading or writing. It has the following prototype: memAccessHook(unicornObject, accessType, memAccessAddress, memAccessSize, memValue, userData).
Unicorn 支持 Hook 的的內存操作有以下幾個:

于是,我們 Hook 掉指令模擬過程中的 UC_MEM_WRITE 操作,即可獲取解密后的字節,并將這些字節手動 Patch 到 IDB 中:
- def mem_hook(unicornObject, accessType, memAccessAddress, memAccessSize, memValue, userData):
- #if accessType == UC.UC_MEM_READ:
- # print("Read: ", hex(memAccessAddress), memAccessSize, hex(memValue))
- if accessType == UC.UC_MEM_WRITE:
- #print("Write: ", hex(memAccessAddress), memAccessSize, hex(memValue))
- if memAccessSize == 1:
- idc.PatchByte(memAccessAddress, memValue)
- elif memAccessSize == 2:
- idc.PatchWord(memAccessAddress, memValue)
- elif memAccessSize == 4:
- idc.PatchDword(memAccessAddress, memValue)
Patch IDB 的基本操作當然是像前文中 IDAPython 腳本那樣,調用 idc.PatchXXX 函數寫入 IDB 文件。前面一個姿勢中,Patch IDB 文件,只調用了一個 idc.PatchByte() 函數。其實,idc 庫中共有 4 個函數可以 Patch IDB:
- idc.PatchByte(): Patch 1 Byte;
- idc.PatchWord(): Patch 2 Bytes;
- idc.PatchDword(): Patch 4 Bytes;
- idc.PatchQword(): Patch 8 Bytes;
指令模擬器中執行 Patch 的操作,并不只有 PatchByte 這一項。根據我 print 出來的指令模擬過程中寫內存操作的細節,可以看到共涉及 3 種 Patch 操作(如下圖):1 byte、2 Bytes 和 4 Bytes,所有才有了上面 mem_hook() 函數中的 3 種 memAccessSize。

明確并解決了「系統 API Hook」和「捕獲指令模擬結果并 Patch IDB」這兩點問題,就可以寫出準確無誤的 IDAPython 腳本了。
3.2.3 Radare2 ESIL 模擬
r2 上也有強大的指令模擬模塊,名為 ESIL( Evaluable Strings Intermediate Language):

在 r2 上用這個東西來模擬指令解密這一批字符串,就不用像 IDA 中那樣還要自己動手寫 IDAPython 腳本了,只需要通過 r2 指令配置好幾個相關參數即可。下面兩張圖是在 r2 中通過指令模擬批量解密這些字符串的前后對比:


具體操作方法就不細說了,有興趣的朋友可以自行探索。
4. 總結
文中介紹兩種基本方法,在 IDA 中批量解密 雙槍 木馬傳播中間環節的惡意驅動 kemon.sys 中的大量自定義加密字串:Python 實現解密函數和指令模擬解密函數。
原理都很簡單,介紹的有點啰嗦,希望把每個關鍵細節都描述清楚了。
兩種方法對應的 IDAPython 腳本,已上傳到 Github,以供參考:https://github.com/0xjiayu/decrypt_CypherStr_kemonsys
5. 參考資料
https://en.wikipedia.org/wiki/Caesar_cipher
https://github.com/tmr232/idapython/blob/master/python/idc.py
https://unicorn-engine.org
https://github.com/36hours/idaemu
https://github.com/fireeye/flare-emu
https://github.com/unicorn-engine/unicorn/blob/master/bindings/python/unicorn/unicorn_const.py#L64