IDAPython:讓你的生活更美好(一)
IDAPython是IDA的一個功能強大的擴展特性,對外提供了大量的IDA API調用。另外,還能在使用python 腳本語言的過程中獲得能力提升,所以我強烈推薦所有的逆向工程師使用它。
然而不幸的是,除了下面這幾項,關于IDAPython的信息和教程實在太少了。
· “The IDA Pro Book” by Chris Eagle · “The Beginner’s Guide to IDAPython” by Alex Hanel · “IDAPython Wiki” by Magic Lantern
為了增加IDAPython相關的教程資料,在該系列中我將會提供我寫的一些有趣的實例代碼。 而在第一部分,我會通過編寫腳本來解碼一個惡意軟件里面的大量被混淆字的符串。
背景
在逆向惡意樣本的過程中,我遇到了下面這個函數:
基于過去的經驗,我覺得這個函數應該是用來解密二進制數據的。這個函數的交叉引用次數證明了我的猜測:
正如圖2所示,這個特殊的函數被調用了116次。這個函數的每一次調用,都有一個二進制數據對象通過ESI寄存器作為參數傳入。
通過這一分析,我更加確定這個函數是惡意軟件運行時用來解密字符串的。面對這一情況,我可以選擇下面這幾種解決方案:
1. 手動解密并重命名這些被混淆的字符串 2. 運行這些樣本,遇到這些字符串的時候進行重命名 3. 編寫一個腳本來解密并重命名這些字符串
如果惡意軟件只是解密少量的字符串,我會選擇第一種或者第二種方案。然而,正如我們前面了解到的,這個函數被調用了116次,所以編寫一個腳本似乎更有意義。
編寫IDAPYTHON腳本
解決混淆字符串問題的第一個步驟是找到并重寫解密函數。幸運的是,這里的解密函數比較簡單。這個函數簡單的將二進制數組中的第一個字符與剩下的數據逐字節的進行異或。
E4 91 96 88 89 8B 8A CA 80 88 88
在上面這個例子中,取出0XE4跟剩下的其他數據進行異或。解密的結果是’urlmon.dll’。我們可以用python這樣實現:
def decrypt(data): length = len(data) c = 1 o = "" while c < length: o += chr(ord(data[0]) ^ ord(data[c])) c += 1 return o
運行這段代碼,我們得到了我們預期的結果
>>> from binascii import * >>> d = unhexlify("E4 91 96 88 89 8B 8A CA 80 88 88".replace(" ",'')) >>> decrypt(d) 'urlmon.dll'
接下來就是找出代碼中引用了解密函數的地方,提取作為參數出入的數據。 通過IDA找到函數的引用比較簡單,IDA提供的API 函數XrefsTo()完美的解決了這個問題。下面這個腳本中,我將地址硬編碼到解密腳本中。 下面的代碼能夠找出解密函數的引用地址。下面這個測試,我簡單的將地址用16進制的格式打印出來。
for addr in XrefsTo(0x00405BF0, flags=0): print hex(addr.frm) Result: 0x401009L 0x40101eL 0x401037L 0x401046L 0x401059L 0x40106cL 0x40107fL <truncated>
從交叉引用處識別參數并提取原始數據稍微復雜一點,但顯然不是不可能的。首先我們要得到字符串解密函數調用點之前最近的一個 ‘mov esi, offset unk_??’指令的偏移地址。為了達到這個目的,我們會對字符串解密函數的每一處調用,逐條指令的回溯去查找 ‘mov esi, offset [addr]’ 指令。我們可以使用GetOperandValue()函數(API)來獲取真正的偏移地址。
下面是代碼實現:
def find_function_arg(addr): while True: addr = idc.PrevHead(addr) if GetMnem(addr) == "mov" and "esi" in GetOpnd(addr, 0): print “We found it at 0x%x” % GetOperandValue(addr, 1) break Example Results: Python>find_function_arg(0x00401009) We found it at 0x418be0
現在我們只要簡單的將偏移地址處的字符串提取出來。 通常我們會使用GetString()函數,然而,由于混淆過的字串是原始的二進制數據,這個函數無法得到我們期望的結果。 所以我們通過逐字節的重復讀取,直到遇到null(0×00)結束符為止。
下面是代碼實現:
def get_string(addr): out = "" while True: if Byte(addr) != 0: out += chr(Byte(addr)) else: break addr += 1 return out
接下來就是將之前實現的功能整合到一起:
def find_function_arg(addr): while True: addr = idc.PrevHead(addr) if GetMnem(addr) == "mov" and "esi" in GetOpnd(addr, 0): return GetOperandValue(addr, 1) return "" def get_string(addr): out = "" while True: if Byte(addr) != 0: out += chr(Byte(addr)) else: break addr += 1 return out def decrypt(data): length = len(data) c = 1 o = "" while c < length: o += chr(ord(data[0]) ^ ord(data[c])) c += 1 return o print "[*] Attempting to decrypt strings in malware" for x in XrefsTo(0x00405BF0, flags=0): ref = find_function_arg(x.frm) string = get_string(ref) dec = decrypt(string) print "Ref Addr: 0x%x | Decrypted: %s" % (x.frm, dec) Results: [*] Attempting to decrypt strings in malware Ref Addr: 0x401009 | Decrypted: urlmon.dll Ref Addr: 0x40101e | Decrypted: URLDownloadToFileA Ref Addr: 0x401037 | Decrypted: wininet.dll Ref Addr: 0x401046 | Decrypted: InternetOpenA Ref Addr: 0x401059 | Decrypted: InternetOpenUrlA Ref Addr: 0x40106c | Decrypted: InternetReadFile <truncated>
我們可以不用運行惡意軟件也能看到所有加密后的字符串。下一個步驟我們可以將解密后的字符串以注釋的形式寫到引用處,讓字符串明文與密文同時存在,這樣就很便于分析了。我們使用 MakeComm() 函數來實現這一功能。將下面這兩行代碼加入到上面代碼print語句的后面:
MakeComm(x.frm, dec) MakeComm(ref, dec)
如下圖所示,我們能看到的,通過這一額外的步驟,我們可以結合交叉引用看得更清楚。現在我們能夠很容易的找到被引用的特殊字符串。
另外的,通過反編譯,我們可以看到解密后的字符串作為注釋存在與代碼中。
總結
通過使用IDAPython,我們完成一個困難的任務—惡意二進制樣本中的161個加密字符串的解密。正如我們看到的,IDAPython對于逆向工程來說是一款強大的工具,簡化任務,節省寶貴的時間。
*原文鏈接: researchcenter.paloaltonetworks ,東二門陳冠希/編譯,轉載請注明來自FreeBuf黑客與極客(FreeBuf.COM)