工具 | 如何使用 IDAPython 尋找漏洞

IT168GB發表於2018-07-19

概覽

IDAPython是一個強大的工具,可用於自動化繁瑣複雜的逆向工程任務。雖然已經有很多關於使用IDAPython來簡化基本的逆向工程的文章,但是很少有關於使用IDAPython來幫助審查二進位制檔案以發現漏洞的文章。因為這不是一個新想法(HalvarFlake在2001年提出了關於使用IDA指令碼自動化漏洞研究的文章),所以沒有更多關於這個主題的文章是有點令人驚訝的。這可能部分是因為在現代作業系統上執行利用操作所需的複雜性日益增加。但是,能夠將部分漏洞研究過程自動化仍然很有價值。

在這篇文章中,我們將開始介紹如何使用基本的IDAPython技術來檢測危險的程式碼,它們常常導致堆疊緩衝區溢位。在這篇博文中,我將使用 中的“ascii_easy”二進位制檔案自動檢測基本堆疊緩衝區溢位。雖然這個二進位制檔案足夠小,可以完全手動逆向,但它是一個很好的示例,可以將相同的IDAPython技術應用到更大、更復雜的二進位制檔案中。

 

開始

在開始編寫IDAPython之前,我們必須首先確定希望指令碼查詢什麼內容。在本例中,我選擇了具有最簡單型別漏洞之一的二進位制檔案,這是由使用“strcpy”將使用者控制的字串複製到堆疊緩衝區所造成的堆疊緩衝區溢位。既然我們已經知道了我們要尋找什麼,我們就可以開始考慮如何自動查詢這些型別的漏洞了。

為了達到目的,我們將把它分成兩個步驟:

  1. 查詢可能導致堆疊緩衝區溢位的所有函式呼叫(在本例中是”strcpy”)

  2. 分析函式呼叫的使用以確定使用是否符合條件(可能導致可利用的溢位)

 

查詢函式呼叫

為了找到對“strcpy”函式的所有呼叫,我們必須首先定位“strcpy”函式本身。使用IDAPython API提供的功能很容易做到這一點。使用下面的程式碼,我們可以列印出二進位制檔案中的所有函式名:

for functionAddr in Functions():    
   print(GetFunctionName(functionAddr))

在ascii_easy二進位制檔案上執行這個IDAPython指令碼會給出以下輸出。我們可以看到所有的函式名都列印在IDA Pro的輸出視窗中。

接下來,我們新增程式碼來過濾函式列表,以便找到我們感興趣的‘strcpy’函式。簡單的字串比較將在這裡發揮作用。由於我們通常處理的函式類似,但由於匯入函式的命名方式略有不同(例如示例程式中的“strcpy” vs“_strcpy”),所以最好檢查子串,而不是確切的字串。

在前面的程式碼的基礎上,我們現在有了以下程式碼:

for functionAddr in Functions():    
    if “strcpy” in GetFunctionName(functionAddr):        
        print hex(functionAddr)

現在我們找到了要找的函式,我們必須確定所有呼叫它的位置。這涉及到幾個步驟。首先,我們得到所有對“strcpy”的交叉引用,然後檢查每個交叉引用,找出哪些交叉引用是實際的`strcpy’函式呼叫。把所有這些放在一起,我們就會得到下面這段程式碼:

for functionAddr in Functions():    
    # Check each function to look for strcpy        
    if "strcpy" in GetFunctionName(functionAddr): 
        xrefs = CodeRefsTo(functionAddr, False)                
        # Iterate over each cross-reference
        for xref in xrefs:                            
            # Check to see if this cross-reference is a function call                            
            if GetMnem(xref).lower() == "call":           
                print hex(xref)

對ascii_easy二進位制檔案執行這個命令將生成二進位制檔案中所有的“strcpy”呼叫。結果如下:

 

函式呼叫分析

現在,透過上面的程式碼,我們知道如何在程式中獲取所有呼叫的地址。雖然在ascii_easy應用程式中,只有一個對“strcpy”的呼叫(碰巧它也是易受攻擊的),但許多應用程式都會有大量對“strcpy”的呼叫(大量的呼叫並不容易受到攻擊),因此我們需要某種方法來分析對“strcpy”的呼叫,以便對更容易受到攻擊的函式呼叫進行優先順序排序。

可利用緩衝區溢位的一個常見特徵是,它們常常涉及堆疊緩衝區。雖然利用堆和其他地方的緩衝區溢位是可能的,但是堆疊緩衝區溢位是一種更簡單的利用途徑。

這涉及到對strcpy函式的目標引數的一些分析。我們知道目標引數是strcpy函式的第一個引數,我們可以從函式呼叫的反彙編中找到這個引數。以下是對strcpy呼叫的反彙編。

在分析上面的程式碼時,有兩種方法可以找到_strcpy函式的目標引數。第一種方法是依賴自動IDA Pro分析,它自動註釋已知的函式引數。正如我們在上面的截圖中所看到的,IDA Pro自動檢測到了_strcpy函式的“dest”引數,並在將引數推送到堆疊中的指令處用註釋將其標記為dest引數。

檢測函式引數的另一種簡單方法是向後移動彙編程式碼,從函式呼叫開始尋找“push”指令。每當我們找到一條指令,我們就可以增加一個計數器,直到找到我們正在尋找的引數的索引為止。在這種情況下,由於我們正在尋找恰巧是第一個引數的“dest”引數,該方法將在函式呼叫之前的“push”指令的第一個例項處停止。

在這兩種情況下,當我們向後遍歷程式碼時,我們必須小心識別破壞順序程式碼流的某些指令。諸如“ret”和“jmp”之類的指令會導致程式碼流的更改,從而難以準確識別引數。此外,我們還必須確保不會在當前函式的開始處向後遍歷程式碼。現在,我們將在搜尋引數時簡單地識別非順序程式碼流的例項,如果找到任何非順序程式碼流例項,則停止搜尋。

我們將使用第二種方法查詢引數(尋找被推到堆疊中的引數)。為了以這種方式幫助我們找到引數,我們應該建立一個幫助函式,這個函式將從函式呼叫的地址向後跟蹤推送到堆疊中的引數,並返回與指定引數對應的運算元。

因此,對於上面呼叫ascii_easy中的_strcpy的示例,我們的幫助函式將返回值“eax”,因為“eax”暫存器在將strcpy作為引數推送到堆疊中時,儲存它的目標引數為_strcpy。結合使用一些基本的python和IDAPython API,我們可以構建一個函式來實現這一點,如下所示。

def find_arg(addr, arg_num):
   # Get the start address of the function that we are in
   function_head = GetFunctionAttr(addr, idc.FUNCATTR_START)    
   steps = 0
   arg_count = 0
   # It is unlikely the arguments are 100 instructions away, include this as a safety check
   while steps < 100:    
       steps = steps + 1
       # Get the previous instruction
       addr = idc.PrevHead(addr)  
       # Get the name of the previous instruction
       op = GetMnem(addr).lower()         
       # Check to ensure that we haven’t reached anything that breaks sequential code flow        
       if op in ("ret", "retn", "jmp", "b") or addr < function_head:           return
       if op == "push":
           arg_count = arg_count + 1
           if arg_count == arg_num:               # Return the operand that was pushed to the stack
               return GetOpnd(addr, 0)

使用這個幫助函式,我們能夠確定在呼叫_strcpy之前使用了“eax”暫存器來儲存目標引數。為了確定eax在被推入堆疊時是否指向堆疊緩衝區,我們現在必須繼續嘗試跟蹤“eax”中的值來自何處。為了做到這一點,我們使用了類似於以前幫助函式中使用的搜尋迴圈:

# Assume _addr is the address of the call to _strcpy # Assume opnd is “eax” # Find the start address of the function that we are searching infunction_head = GetFunctionAttr(_addr, idc.FUNCATTR_START)
addr = _addr 
while True:
   _addr = idc.PrevHead(_addr)
   _op = GetMnem(_addr).lower()    
   if _op in ("ret", "retn", "jmp", "b") or _addr < function_head:       break
   elif _op == "lea" and GetOpnd(_addr, 0) == opnd:       # We found the destination buffer, check to see if it is in the stack
       if is_stack_buffer(_addr, 1):           print "STACK BUFFER STRCOPY FOUND at 0x%X" % addr           break
   # If we detect that the register that we are trying to locate comes from some other register
   # then we update our loop to begin looking for the source of the data in that other register
   elif _op == "mov" and GetOpnd(_addr, 0) == opnd:
       op_type = GetOpType(_addr, 1)       if op_type == o_reg:
           opnd = GetOpnd(_addr, 1)
           addr = _addr       else:           break

在上面的程式碼中,我們透過彙編程式碼執行向後搜尋,查詢儲存目標緩衝區的暫存器獲取其值的指令。程式碼還執行許多其他檢查,比如檢查,以確保我們沒有搜尋過函式的開始,也沒有執行任何可能導致程式碼流更改的指令。程式碼還試圖追溯任何其他暫存器的值,這些暫存器可能是我們最初搜尋的暫存器的來源。例如,程式碼試圖說明下面演示的情況。

... lea ebx [ebp-0x24] 
... mov eax, ebx
...
push eax
...

此外,在上面的程式碼中,我們引用了函式is_stack_buffer()。這個函式是這個指令碼的最後一部分,在IDA API中沒有定義。這是一個額外的幫助函式,我們將編寫它來幫助我們尋找bug。這個函式的目的非常簡單:給定指令的地址和運算元的索引,報告變數是否是堆疊緩衝區。雖然IDA API沒有直接為我們提供這種功能,但它確實為我們提供了透過其他方式檢查這一功能的能力。使用get_stkvar函式並檢查結果是否為None或物件,我們能夠有效地檢查運算元是否是堆疊變數。我們可以在下面的程式碼中看到我們的幫助函式:

def is_stack_buffer(addr, idx):
   inst = DecodeInstruction(addr)   return get_stkvar(inst[idx], inst[idx].addr) != None

請注意,上面的幫助函式與IDA7 API不相容。在我們的下一篇博文中,我們將介紹一種新的方法來檢查引數是否是堆疊緩衝區,同時保持與所有最新版本的IDA API的相容性。

現在,我們可以將所有這些放到一個指令碼中,如下所示,以便找到使用strcpy的所有例項,以便將資料複製到堆疊緩衝區中。有了這些,我們就可以將這些功能擴充套件到除了strcpy之外,還可以擴充套件到類似的功能,如strcat、printf等(請參閱 ),以及向我們的指令碼新增額外的分析。這個指令碼的完整版在文章的底部可以找到。執行指令碼可以成功地找到易受攻擊的strcpy,如下所示。

 

指令碼

def is_stack_buffer(addr, idx):
   inst = DecodeInstruction(addr)   return get_stkvar(inst[idx], inst[idx].addr) != None def find_arg(addr, arg_num):
   # Get the start address of the function that we are in
   function_head = GetFunctionAttr(addr, idc.FUNCATTR_START)    
   steps = 0
   arg_count = 0
   # It is unlikely the arguments are 100 instructions away, include this as a safety check
   while steps < 100:    
       steps = steps + 1
       # Get the previous instruction
       addr = idc.PrevHead(addr)  
       # Get the name of the previous instruction        
       op = GetMnem(addr).lower() 
       # Check to ensure that we havent reached anything that breaks sequential code flow        
       if op in ("ret", "retn", "jmp", "b") or addr < function_head:            
           return
       if op == "push":
           arg_count = arg_count + 1
           if arg_count == arg_num:               #Return the operand that was pushed to the stack 
               return GetOpnd(addr, 0) 
for functionAddr in Functions():   # Check each function to look for strcpy
   if "strcpy" in GetFunctionName(functionAddr): 
       xrefs = CodeRefsTo(functionAddr, False)       # Iterate over each cross-reference
       for xref in xrefs:           # Check to see if this cross-reference is a function call
           if GetMnem(xref).lower() == "call":               # Since the dest is the first argument of strcpy
               opnd = find_arg(xref, 1) 
               function_head = GetFunctionAttr(xref, idc.FUNCATTR_START)
               addr = xref
               _addr = xref                
               while True:
                   _addr = idc.PrevHead(_addr)
                   _op = GetMnem(_addr).lower()                    
                   if _op in ("ret", "retn", "jmp", "b") or _addr < function_head:                       break
                   elif _op == "lea" and GetOpnd(_addr, 0) == opnd:                       # We found the destination buffer, check to see if it is in the stack
                       if is_stack_buffer(_addr, 1):                           print "STACK BUFFER STRCOPY FOUND at 0x%X" % addr                            break
                   # If we detect that the register that we are trying to locate comes from some other register
                   # then we update our loop to begin looking for the source of the data in that other register
                   elif _op == "mov" and GetOpnd(_addr, 0) == opnd:
                       op_type = GetOpType(_addr, 1)                       if op_type == o_reg:
                           opnd = GetOpnd(_addr, 1)
                           addr = _addr                       else:                           break

完整的指令碼在: https://github.com/Somerset-Recon/blog/blob/master/into_vr_script.py[](https://github.com/Somerset-Recon/blog/blob/master/into_vr_script.py)


本文系轉載文章,由“安全客”翻譯自 somersetrecon.com 原文連結  。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31510736/viewspace-2158153/,如需轉載,請註明出處,否則將追究法律責任。

相關文章