IDAPython 讓你的生活更滋潤 part1 and part2

wyzsk發表於2020-08-19
作者: 蒸米 · 2016/01/06 9:11

0x00 序


今天在網上看到平底鍋blog上Josh Grunzweig發表了一系列關於利用IDAPython分析malware的教程。感覺內容非常不錯,於是翻譯成中文與大家一起分享。原文地址:

Part1: http://researchcenter.paloaltonetworks.com/2015/12/using-idapython-to-make-your-life-easier-part-1/

Part2: http://researchcenter.paloaltonetworks.com/2015/12/using-idapython-to-make-your-life-easier-part-2/

0x01 背景


作為一名malware逆向工程師,我的日常活動就是使用IDA Pro。這並不奇怪,因為IDA Pro可以說是行業標配(儘管它的替代品,如radare2和hopper也越來越受歡迎)。IDA其中一個非常強大的功能就是可以使用Python指令碼(又被稱為IDAPython)。使用者可以透過IDAPython呼叫大量的IDA API。當然,使用者還可以透過使用IDAPython獲取到指令碼語言提供的各種功能。

不幸的是,只有少量的關於IDAPython的資料,僅有的一些資料如下:

  • Chris Eagle的The IDA Pro Book
  • Alex Hanel的The Beginner’s Guide to IDAPython
  • Magic Lantern 的IDAPython Wiki

0x02 利用IDAPython解決字串加密問題


為了能提供更多的教程給分析師,我準備寫一篇帶例子的分析文章供大家學習。在本系列的第一部分,我將教大家編寫一個指令碼用來解決一個malware樣本的多處字串混淆問題。

在逆向分析一個病毒樣本的時候,我遇到了這樣一個函式:

p1 圖片1 字串解密函式

根據以往的經驗,我懷疑這個函式是用來進行解密的。關於這個函式大量的引用證實了我的猜想。

p2 圖2 大量的對可疑函式的引用

在圖2中,我們可以看到有116處對這個函式的引用。每當這個函式被呼叫時,都有一段資料作為引數透過ESI暫存器提供給這個函式。

p3 圖3可疑的函式 (405BF0) 被呼叫的例項

在這個時候,我已經非常肯定這個函式是malware用來在執行時進行字串解密的函式了。當我們面臨這種情況時,我們一般有如下幾種選擇:

  1. 我可以手動解密然後重新命名這些字串。
  2. 我可以動態除錯這個樣本然後重新命名遇到的字串
  3. 我可以寫一個指令碼用來解密並且重新命名這些字串

如果malware只解密了很少的幾個字串的話,我會選擇第一種或者第二種方法。但是,根據之前確認的,這個函式被呼叫了116次,所以採用IDAPython指令碼來解決問題會更靠譜一些。

解決字串混淆問題的第一步是確認和重寫解密函式。幸運的是,這個解密函式非常的簡單。這個函式只是把資料的第一個字元當做XOR演算法的key用來解密剩餘的資料。

E4 91 96 88 89 8B 8A CA 80 88 88

在上面這個例子中,我們把E4作為key來異或剩餘的資料。最後的結果是”urlmon.dll”。在Python中,我們可以把這個解密函式重寫為:

#!python
def decrypt(data):
    length = len(data)
    c = 1
    o = ""
    while c < length:
        o += chr(ord(data[0]) ^ ord(data[c][/c]))
        c += 1
    return o

可以看到,我們的測試指令碼可以得到我們所期望的結果:

#!bash
>>> from binascii import *
>>> d = unhexlify("E4 91 96 88 89 8B 8A CA 80 88 88".replace(" ",''))
>>> decrypt(d)
'urlmon.dll'

我們要做的下一步工作就是確認哪些程式碼引用了這個解密函式,並且提取作為引數的資料。獲取到函式的引用非常的簡單,只需要使用XrefsTo()這個API函式就能達到我們的目的。在這個指令碼中,我將會在指令碼中硬編碼這個地址。作為測試,我先將這些地址用16進位制列印出來:

#!python
for addr in XrefsTo(0x00405BF0, flags=0):
    print hex(addr.frm)

Result:
0x401009L
0x40101eL
0x401037L
0x401046L
0x401059L
0x40106cL
0x40107fL
<truncated>

獲取到這些交叉引用的引數並且提取這些原始資料需要一些技巧,但絕非很難。第一件我們想要做的事是獲取”mov esi, offset unk_??”指令中的偏移地址,因為這個指令會把引數傳遞給解密函式。為了做到這點,我們需要找到呼叫解密函式指令的前一個指令。找到這個指令後,我們可以使用GetOperandValue() 這個指令得到這個偏移地址的值。如下面的程式碼所示:

#!python
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()這個API函式,但是在這個問題中這些字串都是原始的二進位制資料,因此使用這個API可能不太合適。解決方案是我們自己編寫一個函式,然後一個字元一個字元的讀取資料直到碰到空的終止符為止。程式碼如下:

#!python
def get_string(addr):
  out = ""
  while True:
    if Byte(addr) != 0:
      out += chr(Byte(addr))
    else:
      break
    addr += 1
  return out

最後,我們將所有的程式碼放在一起:

#!python
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]))
    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()這個API函式。增加這樣兩行程式碼就會給程式加入必要的註釋:

#!python
MakeComm(x.frm, dec)
MakeComm(ref, dec)

增加了這一步後,我們能夠非常清晰的看到交叉引用的資料。如下圖所示,我們可以很輕鬆的分辨出哪些字串被引用了:

p4 圖4 執行完指令碼後的字串交叉引用介面

除此之外,我們在反彙編程式碼中也能看到這些解密後的字串作為註釋:

p5 圖5 執行完指令碼後的反彙編程式碼

0x03 利用IDAPython解決函式/庫呼叫的雜湊混淆問題


在反編譯中我們經常會見到shellcode和malware使用雜湊演算法來混淆載入的函式或者庫。比如逆向工程師們經常會在shellcode中看到混淆後的函式名。總的來說,整個過程是很簡單的。程式碼在執行時會先載入knerel32.dll。然後,它會用這個載入的映象去識別並儲存LoadLibraryA函式,這函式是用來載入更多的庫和函式的。這種特定的技術一般採用某種雜湊演算法來識別函式的。最常用的雜湊演算法一般是CRC32,當然,其他的一些變種演算法,如ROR13,也是非常常見的。

比如說,當我逆向一個malware的某一部分內容的時候,我看到了如下的程式碼:

p6 圖片6 malware使用CRC32雜湊演算法來動態的載入函式

因為0xEDB88320這個常數是CRC32演算法的常用引數。所以我們可以判斷出這個例子使用了CRC32雜湊演算法。

p7 圖片7 確認CRC32演算法

透過圖7我們可以確定這個演算法是CRC32演算法。現在,演算法和函式已經確定了。我們可以透過交叉引用(ida中按x)的數量來確定這個函式被被呼叫了多少次。可以看到這個函式一共被呼叫了190次。顯然,手動的解密並重新命名這些雜湊值並不是我們想要的。因此,我們可以使用IDAPython來幫我們解決。

第一步實際上並不需要IDAPython,但是它用到了Python。為了驗證哪個雜湊值對應哪個函式,我們需要生成一個windows通用函式雜湊列表。想要做到這點,我們只需要獲取一個windows通用庫的列表,然後遍歷這些庫的函式列表。程式碼如下:

#!python
def get_functions(dll_path):
  pe = pefile.PE(dll_path)
  if ((not hasattr(pe, 'DIRECTORY_ENTRY_EXPORT')) or (pe.DIRECTORY_ENTRY_EXPORT is None)):
    print "[*] No exports for %s" % dll_path
    return []
  else:
    expname = []
    for exp in pe.DIRECTORY_ENTRY_EXPORT.symbols:
      if exp.name:
        expname.append(exp.name)
    return expname

我們隨後可以得到函式名字的列表,然後計算他們的CRC32的雜湊值。程式碼如下:

#!python
def calc_crc32(string): 
  return int(binascii.crc32(string) & 0xFFFFFFFF)

最後我們將結果寫入一個JSON格式的檔案中,並且命名為"output.json"。這個JSON檔案包含了一個非常大的字典,採用如下的格式:

#!bash
HASH => NAME

完整版的程式碼如下:

https://github.com/pan-unit42/public_tools/blob/master/ida_scripts/gen_function_json.py

當這個檔案生成之後,我們可以返回IDA,並且繼續編寫我們的IDAPython指令碼。我們指令碼第一步要做的事情是讀取我們之前建立的'output.json'這個JOSON資料檔案。不幸的是,JSON物件並不支援整數作為key,因此當資料被載入後,我們需要手動把key從字串轉換為整數。程式碼如下:

#!python
for k,v in json_data.iteritems():
  json_data[int(k)] = json_data.pop(k)

當這些資料被載入後,我們將會建立一個列舉物件儲存了雜湊值與函式名的對應關係。(想要了解更多的關於列舉物件的資訊,我推薦你閱讀這篇教程:

http://www.cprogramming.com/tutorial/enum.html

使用列舉物件,我們可以找到一個整數對應的字串,比如說CRC32雜湊值對應的函式名。為了在IDA中建立新的列舉物件,我們可以使用AddEnum()這個函式。為了讓這個指令碼更加健壯,我們先使用GetEnum()函式來檢測用來列舉的值是否已經存在。

#!python
enumeration = GetEnum("crc32_functions")
if enumeration == 0xFFFFFFFF:
    enumeration = AddEnum(0, "crc32_functions", idaapi.hexflag())

這個列舉的值隨後將會被修改。下一步要乾的事情是根據函式的雜湊值來確定真實的函式地址。這一部分看起來很像第一部分的內容。我們透過觀察這個函式的結構可以發現CRC32雜湊值是這個載入函式的第二個引數。

p8 圖片8 傳遞給load_function()的引數

同樣的,我們還是列舉之前的指令來尋找函式的第二個引數。當我們找到後,我們透過output.json中的JSON資料來進行檢測,並且確保有一個函式名對應了這個雜湊值。程式碼如下:

#!python
for x in XrefsTo(load_function_address, flags=0):
    current_address = x.frm
    addr_minus_20 = current_address-20
    push_count = 0
    while current_address >= addr_minus_20:
      current_address = PrevHead(current_address)
      if GetMnem(current_address) == "push":
        push_count += 1
        data = GetOperandValue(current_address, 0)
        if push_count == 2:
          if data in json_data:
            name = json_data[data]

這個時候,我們使用AddConstEx()這個函式將CRC32雜湊和函式名加入我們之前建立的列舉物件中。

#!python
AddConstEx(enumeration, str(name), int(data), -1)

當這個資料加入到列舉物件中後,我們可以將CRC32的雜湊值轉換為對應的列舉名字了。下面的兩個函式一個是用來將一個整數轉換成對應的列舉資料,另一個是用來將某個地址的資料轉換成對應的列舉資料。

#!python
def get_enum(constant):
  all_enums = GetEnumQty()
  for i in range(0, all_enums):
    enum_id = GetnEnum(i)
    enum_constant = GetFirstConst(enum_id, -1)
    name = GetConstName(GetConstEx(enum_id, enum_constant, 0, -1))
    if int(enum_constant) == constant: return [name, enum_id]
    while True:
      enum_constant = GetNextConst(enum_id, enum_constant, -1)
      name = GetConstName(GetConstEx(enum_id, enum_constant, 0, -1))
      if enum_constant == 0xFFFFFFFF:
        break
      if int(enum_constant) == constant: return [name, enum_id]
  return None

def convert_offset_to_enum(addr):
  constant = GetOperandValue(addr, 0)
  enum_data = get_enum(constant)
  if enum_data:
    name, enum_id = enum_data
    OpEnumEx(addr, 0, enum_id, 0)
    return True
  else:
    return False

當我們把這個列舉轉換完成後,我們來研究一下如何修改DWORD處的值,因為DWORD處的值儲存了載入後的函式地址。

p9 圖片9 當載入完函式後,程式將函式地址儲存到了DWORD地址

為了做到這一點,我們不光需要遍歷之前的指令,還要查詢之後的指令,也就是將eax儲存到一個DWORD地址的指令。當我們發現這條指令之後,我們可以給這個DWORD地址重新命名成正確的函式名。為了防止衝突,我們在函式名前加上一個”d_”字串。

#!python
address = current_address
while address <= address_plus_30:
  address = NextHead(address)
  if GetMnem(address) == "mov":
    if 'dword' in GetOpnd(address, 0) and 'eax' in GetOpnd(address, 1):
      operand_value = GetOperandValue(address, 0)
      MakeName(operand_value, str("d_"+name))

等這一切都做完後,我們會發現原來很難讀懂的彙編程式碼變得很好理解了。如圖所示:

p10 圖片10 執行完指令碼後的變化

現在,當我們看到DOWRDS列表的時候,就已經能得到真實的函式名字了。並且這些資料能夠很好的幫助我們進行靜態分析。

p11

完整的程式碼如下:

#!python
import json

def get_enum(constant):
  all_enums = GetEnumQty()
  for i in range(0, all_enums):
    enum_id = GetnEnum(i)
    enum_constant = GetFirstConst(enum_id, -1)
    name = GetConstName(GetConstEx(enum_id, enum_constant, 0, -1))
    if int(enum_constant) == constant: return [name, enum_id]
    while True:
      enum_constant = GetNextConst(enum_id, enum_constant, -1)
      name = GetConstName(GetConstEx(enum_id, enum_constant, 0, -1))
      if enum_constant == 0xFFFFFFFF:
        break
      if int(enum_constant) == constant: return [name, enum_id]
  return None


def convert_offset_to_enum(addr):
  constant = GetOperandValue(addr, 0)
  enum_data = get_enum(constant)
  if enum_data:
    name, enum_id = enum_data
    OpEnumEx(addr, 0, enum_id, 0)
    return True
  else:
    return False


def enum_for_xrefs(load_function_address, json_data, enumeration):
  for x in XrefsTo(load_function_address, flags=0):
    current_address = x.frm
    addr_minus_20 = current_address-20

    push_count = 0
    while current_address >= addr_minus_20:
      current_address = PrevHead(current_address)
      if GetMnem(current_address) == "push":
        push_count += 1
        data = GetOperandValue(current_address, 0)
        if push_count == 2:
          if data in json_data:
            name = json_data[data]
            AddConstEx(enumeration, str(name), int(data), -1)
            if convert_offset_to_enum(current_address):
              print "[+] Converted 0x%x to %s enumeration" % (current_address, name)
              address_plus_30 = current_address+30
              address = current_address
              while address <= address_plus_30:
                address = NextHead(address)
                if GetMnem(address) == "mov":
                  if 'dword' in GetOpnd(address, 0) and 'eax' in GetOpnd(address, 1):
                    operand_value = GetOperandValue(address, 0)
                    MakeName(operand_value, str("d_"+name))


fh = open("output.json", 'rb')
d = fh.read()
json_data = json.loads(d)
fh.close()

# JSON objects don't allow using integers as dict keys. Little workaround for
# this issue. 
for k,v in json_data.iteritems():
  json_data[int(k)] = json_data.pop(k)

conversion_function = 0x00405680
enumeration = GetEnum("crc32_functions")
if enumeration == 0xFFFFFFFF:
  enumeration = AddEnum(0, "crc32_functions", idaapi.hexflag())
enum_for_xrefs(conversion_function, json_data, enumeration)

0x04 總結


在上一節中,我們利用IDAPython成功的解決了一個雜湊混淆的問題,在這個問題中我們用到了列舉物件。列舉物件對我們分析這類問題會很有幫助,能夠節省我們大量的時間。並且這個物件可以很容易的在IDA工程中提取或者載入,這對我們進行批次的逆向分析會很有幫助。

本文章來源於烏雲知識庫,此映象為了方便大家學習研究,文章版權歸烏雲知識庫!

相關文章