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

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

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


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

反編譯看看:

DecryptUnicodeStr 算法其實(shí)相同,只是因?yàn)樽止?jié)構(gòu)成不同,所以是兩個解密函數(shù)分開寫:

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

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


這里稍微解釋一下 make unicode str 時(shí)的操作:
- 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,對應(yīng)的 idc 函數(shù)是 idc.MakeStr(0。然而 ida.MakeStr() 函數(shù)默認(rèn)是生成 Ascii 字串的,要想生成 Unicode 字串,就需要調(diào)用 idc.SetLongPrm() 函數(shù)設(shè)置一下字符串的類型。

IDA 中支持的字符串類型如上圖,相應(yīng)地,在 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() 函數(shù)設(shè)置一下字符串類型。其中 idc.INF_STRTYPE 即代表字符串類型的常量,在 idc 庫中的定義如下:

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

而 Flare-Emu 就做的更方便了,他們直接在框架中實(shí)現(xiàn)了一些基礎(chǔ)的系統(tǒng) API,而不用自己手動實(shí)現(xiàn)并進(jìn)行 Hook 操作:

之所以提這么個問題,是因?yàn)檫@個 kemon.sys 樣本中的批量解密字串的過程中,涉及了對 memcpy 函數(shù)的調(diào)用:

這樣一來,直接用 Flare-Emu 來模擬執(zhí)行應(yīng)該是個更便捷的選項(xiàng)。
3.2.2 Patch IDB
第 2 點(diǎn)問題,將模擬結(jié)果寫回 IDB 文件,在 IDA 中顯示。
首要問題是如何獲模擬執(zhí)行成功后的結(jié)果——明文字符串。前面描述字串解密算法時(shí)說過,解密后的字節(jié)(Byte)會直接替換密文中的特定字節(jié),把密文的前 dataLen 個字節(jié)解密出來,就是明文字串。這個字節(jié)替換的操作,其實(shí)對應(yīng) Unicorn 指令模擬器中定義的 MEM_WRITE 操作,即寫內(nèi)存,而且,字串解密過程中也只有這個字串替換操作會寫內(nèi)存 。恰好,F(xiàn)lare-Emu 中提供了一個 memAccessHook() 接口(如下圖),可以 Hook 多種內(nèi)存操作:
- 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 的的內(nèi)存操作有以下幾個:

于是,我們 Hook 掉指令模擬過程中的 UC_MEM_WRITE 操作,即可獲取解密后的字節(jié),并將這些字節(jié)手動 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 的基本操作當(dāng)然是像前文中 IDAPython 腳本那樣,調(diào)用 idc.PatchXXX 函數(shù)寫入 IDB 文件。前面一個姿勢中,Patch IDB 文件,只調(diào)用了一個 idc.PatchByte() 函數(shù)。其實(shí),idc 庫中共有 4 個函數(shù)可以 Patch IDB:
- idc.PatchByte(): Patch 1 Byte;
- idc.PatchWord(): Patch 2 Bytes;
- idc.PatchDword(): Patch 4 Bytes;
- idc.PatchQword(): Patch 8 Bytes;
指令模擬器中執(zhí)行 Patch 的操作,并不只有 PatchByte 這一項(xiàng)。根據(jù)我 print 出來的指令模擬過程中寫內(nèi)存操作的細(xì)節(jié),可以看到共涉及 3 種 Patch 操作(如下圖):1 byte、2 Bytes 和 4 Bytes,所有才有了上面 mem_hook() 函數(shù)中的 3 種 memAccessSize。

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

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


具體操作方法就不細(xì)說了,有興趣的朋友可以自行探索。
4. 總結(jié)
文中介紹兩種基本方法,在 IDA 中批量解密 雙槍 木馬傳播中間環(huán)節(jié)的惡意驅(qū)動 kemon.sys 中的大量自定義加密字串:Python 實(shí)現(xiàn)解密函數(shù)和指令模擬解密函數(shù)。
原理都很簡單,介紹的有點(diǎn)啰嗦,希望把每個關(guān)鍵細(xì)節(jié)都描述清楚了。
兩種方法對應(yīng)的 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