本文介紹瞭如何通過 IDA Python 腳本來實現對棧溢出漏洞的檢測,並以 ascii_easy 一道 PWN 基礎題為例來實戰。
介紹
Python資源共享群:626017123
IDAPython 是一個用於複雜逆向工程任務的強大的自動化工具。儘管有很多文章介紹了用 IDAPython 來簡化基本的逆向任務,但很少有提及使用 IDAPython 來審計二進制漏洞的方法。
因為這不是一個新的方法( Halvar Flake 在2001年做過關於 IDA 腳本自動分析漏洞的研究),但令人驚訝的是,這個話題沒有被更多的說明。這可能是因為在現代操作系統上想要利用漏洞日漸複雜困難。然而,這對於自動化部分的漏洞研究還是有價值的。
在這篇文章中,我們將介紹使用基本的 IDAPython 來檢測程序中出現的能導致棧溢出問題的地方。在這篇文章中,我會用自動化探測方法實戰 pwnable.kr 中的 ascii_easy 二進制題目。儘管這個二進制文件小到我們可以手動整個去分析它,它仍然是一個很好的學習案例以便我們使用相同的 IDAPython 技術取分析更大更復雜的二進制文件。
開始
在我們寫任何 IDAPython 腳本前,我們先要決定我們想讓我們的腳本做什麼。
這裡,我們選擇了簡單漏洞中的一個棧溢出漏洞,其可由 strcpy 函數導致將用戶可以控制的字符串拷貝到棧緩存區中。既然我們知道了我們要尋找什麼,我們可以開始考慮如何取自動化尋找這種漏洞。
我們分為兩步:
1. 定位可能導致棧溢出的函數(這裡我們選取 strcpy 函數)
2. 分析這些函數的調用來決定這個這個調用我們是否感興趣(即是否可能導致漏洞)
定位函數調用
為了尋找任何調用 strcpy 的地方,我們需要首先定位 strcpy 這個函數本身。
使用 IDAPython API 很容易做到這一點。使用如下的代碼片段來打印二進制文件中所有的函數名:
for functionAddr in Functions():
(GetFunctionName(functionAddr))
(注:可以在 IDA 底部的 python 命令控制窗口中輸入命令)
本文介紹瞭如何通過 IDA Python 腳本來實現對棧溢出漏洞的檢測,並以 ascii_easy 一道 PWN 基礎題為例來實戰。
介紹
Python資源共享群:626017123
IDAPython 是一個用於複雜逆向工程任務的強大的自動化工具。儘管有很多文章介紹了用 IDAPython 來簡化基本的逆向任務,但很少有提及使用 IDAPython 來審計二進制漏洞的方法。
因為這不是一個新的方法( Halvar Flake 在2001年做過關於 IDA 腳本自動分析漏洞的研究),但令人驚訝的是,這個話題沒有被更多的說明。這可能是因為在現代操作系統上想要利用漏洞日漸複雜困難。然而,這對於自動化部分的漏洞研究還是有價值的。
在這篇文章中,我們將介紹使用基本的 IDAPython 來檢測程序中出現的能導致棧溢出問題的地方。在這篇文章中,我會用自動化探測方法實戰 pwnable.kr 中的 ascii_easy 二進制題目。儘管這個二進制文件小到我們可以手動整個去分析它,它仍然是一個很好的學習案例以便我們使用相同的 IDAPython 技術取分析更大更復雜的二進制文件。
開始
在我們寫任何 IDAPython 腳本前,我們先要決定我們想讓我們的腳本做什麼。
這裡,我們選擇了簡單漏洞中的一個棧溢出漏洞,其可由 strcpy 函數導致將用戶可以控制的字符串拷貝到棧緩存區中。既然我們知道了我們要尋找什麼,我們可以開始考慮如何取自動化尋找這種漏洞。
我們分為兩步:
1. 定位可能導致棧溢出的函數(這裡我們選取 strcpy 函數)
2. 分析這些函數的調用來決定這個這個調用我們是否感興趣(即是否可能導致漏洞)
定位函數調用
為了尋找任何調用 strcpy 的地方,我們需要首先定位 strcpy 這個函數本身。
使用 IDAPython API 很容易做到這一點。使用如下的代碼片段來打印二進制文件中所有的函數名:
for functionAddr in Functions():
(GetFunctionName(functionAddr))
(注:可以在 IDA 底部的 python 命令控制窗口中輸入命令)
我們可以看到,所有的函數名都被輸出了。
本文介紹瞭如何通過 IDA Python 腳本來實現對棧溢出漏洞的檢測,並以 ascii_easy 一道 PWN 基礎題為例來實戰。
介紹
Python資源共享群:626017123
IDAPython 是一個用於複雜逆向工程任務的強大的自動化工具。儘管有很多文章介紹了用 IDAPython 來簡化基本的逆向任務,但很少有提及使用 IDAPython 來審計二進制漏洞的方法。
因為這不是一個新的方法( Halvar Flake 在2001年做過關於 IDA 腳本自動分析漏洞的研究),但令人驚訝的是,這個話題沒有被更多的說明。這可能是因為在現代操作系統上想要利用漏洞日漸複雜困難。然而,這對於自動化部分的漏洞研究還是有價值的。
在這篇文章中,我們將介紹使用基本的 IDAPython 來檢測程序中出現的能導致棧溢出問題的地方。在這篇文章中,我會用自動化探測方法實戰 pwnable.kr 中的 ascii_easy 二進制題目。儘管這個二進制文件小到我們可以手動整個去分析它,它仍然是一個很好的學習案例以便我們使用相同的 IDAPython 技術取分析更大更復雜的二進制文件。
開始
在我們寫任何 IDAPython 腳本前,我們先要決定我們想讓我們的腳本做什麼。
這裡,我們選擇了簡單漏洞中的一個棧溢出漏洞,其可由 strcpy 函數導致將用戶可以控制的字符串拷貝到棧緩存區中。既然我們知道了我們要尋找什麼,我們可以開始考慮如何取自動化尋找這種漏洞。
我們分為兩步:
1. 定位可能導致棧溢出的函數(這裡我們選取 strcpy 函數)
2. 分析這些函數的調用來決定這個這個調用我們是否感興趣(即是否可能導致漏洞)
定位函數調用
為了尋找任何調用 strcpy 的地方,我們需要首先定位 strcpy 這個函數本身。
使用 IDAPython API 很容易做到這一點。使用如下的代碼片段來打印二進制文件中所有的函數名:
for functionAddr in Functions():
(GetFunctionName(functionAddr))
(注:可以在 IDA 底部的 python 命令控制窗口中輸入命令)
我們可以看到,所有的函數名都被輸出了。
然後,我們要添加過濾取尋找我們感興趣的 strcpy 函數。簡單的字符串比較我們就能達到效果。但因為我們常常需要處理一些函數名相似但仍有區別的情況(例如 _strcpy, 這取決於導入函數的命名),我們最好檢查子字符串。
在前面的基礎上,我們有下面的代碼:
for functionAddr in Functions():
if “strcpy” in GetFunctionName(functionAddr):
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" :
hex(xref)
對 ascii_easy 二進制文件運行此腳本後我們得到如下結果:
本文介紹瞭如何通過 IDA Python 腳本來實現對棧溢出漏洞的檢測,並以 ascii_easy 一道 PWN 基礎題為例來實戰。
介紹
Python資源共享群:626017123
IDAPython 是一個用於複雜逆向工程任務的強大的自動化工具。儘管有很多文章介紹了用 IDAPython 來簡化基本的逆向任務,但很少有提及使用 IDAPython 來審計二進制漏洞的方法。
因為這不是一個新的方法( Halvar Flake 在2001年做過關於 IDA 腳本自動分析漏洞的研究),但令人驚訝的是,這個話題沒有被更多的說明。這可能是因為在現代操作系統上想要利用漏洞日漸複雜困難。然而,這對於自動化部分的漏洞研究還是有價值的。
在這篇文章中,我們將介紹使用基本的 IDAPython 來檢測程序中出現的能導致棧溢出問題的地方。在這篇文章中,我會用自動化探測方法實戰 pwnable.kr 中的 ascii_easy 二進制題目。儘管這個二進制文件小到我們可以手動整個去分析它,它仍然是一個很好的學習案例以便我們使用相同的 IDAPython 技術取分析更大更復雜的二進制文件。
開始
在我們寫任何 IDAPython 腳本前,我們先要決定我們想讓我們的腳本做什麼。
這裡,我們選擇了簡單漏洞中的一個棧溢出漏洞,其可由 strcpy 函數導致將用戶可以控制的字符串拷貝到棧緩存區中。既然我們知道了我們要尋找什麼,我們可以開始考慮如何取自動化尋找這種漏洞。
我們分為兩步:
1. 定位可能導致棧溢出的函數(這裡我們選取 strcpy 函數)
2. 分析這些函數的調用來決定這個這個調用我們是否感興趣(即是否可能導致漏洞)
定位函數調用
為了尋找任何調用 strcpy 的地方,我們需要首先定位 strcpy 這個函數本身。
使用 IDAPython API 很容易做到這一點。使用如下的代碼片段來打印二進制文件中所有的函數名:
for functionAddr in Functions():
(GetFunctionName(functionAddr))
(注:可以在 IDA 底部的 python 命令控制窗口中輸入命令)
我們可以看到,所有的函數名都被輸出了。
然後,我們要添加過濾取尋找我們感興趣的 strcpy 函數。簡單的字符串比較我們就能達到效果。但因為我們常常需要處理一些函數名相似但仍有區別的情況(例如 _strcpy, 這取決於導入函數的命名),我們最好檢查子字符串。
在前面的基礎上,我們有下面的代碼:
for functionAddr in Functions():
if “strcpy” in GetFunctionName(functionAddr):
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" :
hex(xref)
對 ascii_easy 二進制文件運行此腳本後我們得到如下結果:
即我們找到了 0x451b2c 目標地址。
分析函數調用
現在,通過上面的代碼,我們知道了如何獲取所有程序中調用 strcpy 的地方。而 ascii_easy 恰好就只有這一個調用 strcpy 的地方(也恰好可利用),很多程序有很多調用 strcpy 的地方(很多都不可被利用),因此我們需要一些方法取分析對 strcpy 的調用來根據可利用的可能性進行排序。
一個緩存區溢出漏洞的常見特點是它們往往涉及棧上的緩衝區。儘管在堆上或者別的地方的緩存區溢出也是有可能的,棧溢出是一個更簡單的利用方式。
這涉及一些對 strcpy 函數的目的地參數( destination )的分析,我們知道目的地參數是 strcpy 函數的第一個參數,而且我們可以通過瀏覽函數的反彙編得到這個參數。這個調用 strcpy 函數的反彙編如下:
本文介紹瞭如何通過 IDA Python 腳本來實現對棧溢出漏洞的檢測,並以 ascii_easy 一道 PWN 基礎題為例來實戰。
介紹
Python資源共享群:626017123
IDAPython 是一個用於複雜逆向工程任務的強大的自動化工具。儘管有很多文章介紹了用 IDAPython 來簡化基本的逆向任務,但很少有提及使用 IDAPython 來審計二進制漏洞的方法。
因為這不是一個新的方法( Halvar Flake 在2001年做過關於 IDA 腳本自動分析漏洞的研究),但令人驚訝的是,這個話題沒有被更多的說明。這可能是因為在現代操作系統上想要利用漏洞日漸複雜困難。然而,這對於自動化部分的漏洞研究還是有價值的。
在這篇文章中,我們將介紹使用基本的 IDAPython 來檢測程序中出現的能導致棧溢出問題的地方。在這篇文章中,我會用自動化探測方法實戰 pwnable.kr 中的 ascii_easy 二進制題目。儘管這個二進制文件小到我們可以手動整個去分析它,它仍然是一個很好的學習案例以便我們使用相同的 IDAPython 技術取分析更大更復雜的二進制文件。
開始
在我們寫任何 IDAPython 腳本前,我們先要決定我們想讓我們的腳本做什麼。
這裡,我們選擇了簡單漏洞中的一個棧溢出漏洞,其可由 strcpy 函數導致將用戶可以控制的字符串拷貝到棧緩存區中。既然我們知道了我們要尋找什麼,我們可以開始考慮如何取自動化尋找這種漏洞。
我們分為兩步:
1. 定位可能導致棧溢出的函數(這裡我們選取 strcpy 函數)
2. 分析這些函數的調用來決定這個這個調用我們是否感興趣(即是否可能導致漏洞)
定位函數調用
為了尋找任何調用 strcpy 的地方,我們需要首先定位 strcpy 這個函數本身。
使用 IDAPython API 很容易做到這一點。使用如下的代碼片段來打印二進制文件中所有的函數名:
for functionAddr in Functions():
(GetFunctionName(functionAddr))
(注:可以在 IDA 底部的 python 命令控制窗口中輸入命令)
我們可以看到,所有的函數名都被輸出了。
然後,我們要添加過濾取尋找我們感興趣的 strcpy 函數。簡單的字符串比較我們就能達到效果。但因為我們常常需要處理一些函數名相似但仍有區別的情況(例如 _strcpy, 這取決於導入函數的命名),我們最好檢查子字符串。
在前面的基礎上,我們有下面的代碼:
for functionAddr in Functions():
if “strcpy” in GetFunctionName(functionAddr):
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" :
hex(xref)
對 ascii_easy 二進制文件運行此腳本後我們得到如下結果:
即我們找到了 0x451b2c 目標地址。
分析函數調用
現在,通過上面的代碼,我們知道了如何獲取所有程序中調用 strcpy 的地方。而 ascii_easy 恰好就只有這一個調用 strcpy 的地方(也恰好可利用),很多程序有很多調用 strcpy 的地方(很多都不可被利用),因此我們需要一些方法取分析對 strcpy 的調用來根據可利用的可能性進行排序。
一個緩存區溢出漏洞的常見特點是它們往往涉及棧上的緩衝區。儘管在堆上或者別的地方的緩存區溢出也是有可能的,棧溢出是一個更簡單的利用方式。
這涉及一些對 strcpy 函數的目的地參數( destination )的分析,我們知道目的地參數是 strcpy 函數的第一個參數,而且我們可以通過瀏覽函數的反彙編得到這個參數。這個調用 strcpy 函數的反彙編如下:
分析上面的代碼,有兩種可以找到 _strcpy 函數的目的地參數。
第一種方法是依賴 IDA 自動分析後對已知函數的註釋。上圖中, IDA 已經自動檢測到了 _strcpy 函數的 dest 參數並標記。
另一個方法是從函數調用前開始尋找 push 指令。每當我們找到一個這樣的指令,我們可以自增一個計數器直到我們定位到了參數的索引。這裡,既然我們要找的 dest 參數是第一個參數,這個方法將會在先前一個 push 指令停下。
在這些情況中,當我們遍歷這些代碼時,我們要注意某些可以打破函數執行流的指令。例如 ret 或 jmp 這樣改變執行流的指令會使精確分析參數變得困難。另外,我們需要確保我們不遍歷那些當前所處函數之前的地方。現在,我們將只是在搜索參數時識別非順序執行代碼流的地方。如果找到任何非順序代碼流實例,則停止搜索。
我們將使用第二種查找參數的方法(查找被推送到堆棧的參數)。為了幫助我們以這種方式查找參數,我們創建一個 helper 函數。此函數將從函數調用的地址向向前查找,跟蹤 push 到堆棧的參數並返回我們指定參數對應的操作數。
對於上面的例子,helper函數將返回eax寄存器的值,因為eax寄存器保存了strcpy的目標參數dest。結合一些基本的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
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()
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
0
)
為了判斷 eax 是否指向在棧中的緩存區 buffer ,當它被 push 入棧時,我們應該繼續跟蹤 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 in
function_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
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
上面的代碼展示了我們搜索彙編指令以找到保存 dest buffer 的過程。它也展示了很多檢查,例如確保我們沒有沒有搜索到函數開頭之前的地址或任何可能改變執行流的指令。它也嘗試追蹤其他寄存器。例如,此代碼嘗試解釋下面演示的情況。
...
lea ebx [ebp-0x24]
...
mov eax, ebx
...
push eax
...
另外,在上面的代碼中,我們使用了函數 is_stack_buffer ,這個函數是這個腳本的最後一部分,有些東西沒有在 IDA API 中定義。這個函數的目的很簡單,給定指令的地址和操作數索引,檢查這個變量是否是一個棧上的 buffer 。雖然 IDA API 沒有給我們直接提供這樣的函數,但我們可以通過其他方法。通過是同 get_stkvar 函數並檢查返回值是 None 還是一個 object, 我們可以有效檢查操作數是否是一個棧上變量。函數實現如下:
def is_stack_buffer (addr, idx) :
inst = DecodeInstruction(addr)
return
None
注意這個函數和 IDA7 API 並不兼容。在下篇文章中我們會提到檢測棧上 buffer 的新的方法並保持和最近 IDA API 的兼容。
我們現在可以把他們組合成一個腳本來尋找 strcpy 導致的棧溢出漏洞了。通過上述技巧我們可以拓展到不止支持 strcpy, 還可以是 strcat、sprintf 等函數(可以參考 Microsoft Banned Functions List )
完整的腳本:
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
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()
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
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
運行結果
本文介紹瞭如何通過 IDA Python 腳本來實現對棧溢出漏洞的檢測,並以 ascii_easy 一道 PWN 基礎題為例來實戰。
介紹
Python資源共享群:626017123
IDAPython 是一個用於複雜逆向工程任務的強大的自動化工具。儘管有很多文章介紹了用 IDAPython 來簡化基本的逆向任務,但很少有提及使用 IDAPython 來審計二進制漏洞的方法。
因為這不是一個新的方法( Halvar Flake 在2001年做過關於 IDA 腳本自動分析漏洞的研究),但令人驚訝的是,這個話題沒有被更多的說明。這可能是因為在現代操作系統上想要利用漏洞日漸複雜困難。然而,這對於自動化部分的漏洞研究還是有價值的。
在這篇文章中,我們將介紹使用基本的 IDAPython 來檢測程序中出現的能導致棧溢出問題的地方。在這篇文章中,我會用自動化探測方法實戰 pwnable.kr 中的 ascii_easy 二進制題目。儘管這個二進制文件小到我們可以手動整個去分析它,它仍然是一個很好的學習案例以便我們使用相同的 IDAPython 技術取分析更大更復雜的二進制文件。
開始
在我們寫任何 IDAPython 腳本前,我們先要決定我們想讓我們的腳本做什麼。
這裡,我們選擇了簡單漏洞中的一個棧溢出漏洞,其可由 strcpy 函數導致將用戶可以控制的字符串拷貝到棧緩存區中。既然我們知道了我們要尋找什麼,我們可以開始考慮如何取自動化尋找這種漏洞。
我們分為兩步:
1. 定位可能導致棧溢出的函數(這裡我們選取 strcpy 函數)
2. 分析這些函數的調用來決定這個這個調用我們是否感興趣(即是否可能導致漏洞)
定位函數調用
為了尋找任何調用 strcpy 的地方,我們需要首先定位 strcpy 這個函數本身。
使用 IDAPython API 很容易做到這一點。使用如下的代碼片段來打印二進制文件中所有的函數名:
for functionAddr in Functions():
(GetFunctionName(functionAddr))
(注:可以在 IDA 底部的 python 命令控制窗口中輸入命令)
我們可以看到,所有的函數名都被輸出了。
然後,我們要添加過濾取尋找我們感興趣的 strcpy 函數。簡單的字符串比較我們就能達到效果。但因為我們常常需要處理一些函數名相似但仍有區別的情況(例如 _strcpy, 這取決於導入函數的命名),我們最好檢查子字符串。
在前面的基礎上,我們有下面的代碼:
for functionAddr in Functions():
if “strcpy” in GetFunctionName(functionAddr):
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" :
hex(xref)
對 ascii_easy 二進制文件運行此腳本後我們得到如下結果:
即我們找到了 0x451b2c 目標地址。
分析函數調用
現在,通過上面的代碼,我們知道了如何獲取所有程序中調用 strcpy 的地方。而 ascii_easy 恰好就只有這一個調用 strcpy 的地方(也恰好可利用),很多程序有很多調用 strcpy 的地方(很多都不可被利用),因此我們需要一些方法取分析對 strcpy 的調用來根據可利用的可能性進行排序。
一個緩存區溢出漏洞的常見特點是它們往往涉及棧上的緩衝區。儘管在堆上或者別的地方的緩存區溢出也是有可能的,棧溢出是一個更簡單的利用方式。
這涉及一些對 strcpy 函數的目的地參數( destination )的分析,我們知道目的地參數是 strcpy 函數的第一個參數,而且我們可以通過瀏覽函數的反彙編得到這個參數。這個調用 strcpy 函數的反彙編如下:
分析上面的代碼,有兩種可以找到 _strcpy 函數的目的地參數。
第一種方法是依賴 IDA 自動分析後對已知函數的註釋。上圖中, IDA 已經自動檢測到了 _strcpy 函數的 dest 參數並標記。
另一個方法是從函數調用前開始尋找 push 指令。每當我們找到一個這樣的指令,我們可以自增一個計數器直到我們定位到了參數的索引。這裡,既然我們要找的 dest 參數是第一個參數,這個方法將會在先前一個 push 指令停下。
在這些情況中,當我們遍歷這些代碼時,我們要注意某些可以打破函數執行流的指令。例如 ret 或 jmp 這樣改變執行流的指令會使精確分析參數變得困難。另外,我們需要確保我們不遍歷那些當前所處函數之前的地方。現在,我們將只是在搜索參數時識別非順序執行代碼流的地方。如果找到任何非順序代碼流實例,則停止搜索。
我們將使用第二種查找參數的方法(查找被推送到堆棧的參數)。為了幫助我們以這種方式查找參數,我們創建一個 helper 函數。此函數將從函數調用的地址向向前查找,跟蹤 push 到堆棧的參數並返回我們指定參數對應的操作數。
對於上面的例子,helper函數將返回eax寄存器的值,因為eax寄存器保存了strcpy的目標參數dest。結合一些基本的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
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()
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
0
)
為了判斷 eax 是否指向在棧中的緩存區 buffer ,當它被 push 入棧時,我們應該繼續跟蹤 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 in
function_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
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
上面的代碼展示了我們搜索彙編指令以找到保存 dest buffer 的過程。它也展示了很多檢查,例如確保我們沒有沒有搜索到函數開頭之前的地址或任何可能改變執行流的指令。它也嘗試追蹤其他寄存器。例如,此代碼嘗試解釋下面演示的情況。
...
lea ebx [ebp-0x24]
...
mov eax, ebx
...
push eax
...
另外,在上面的代碼中,我們使用了函數 is_stack_buffer ,這個函數是這個腳本的最後一部分,有些東西沒有在 IDA API 中定義。這個函數的目的很簡單,給定指令的地址和操作數索引,檢查這個變量是否是一個棧上的 buffer 。雖然 IDA API 沒有給我們直接提供這樣的函數,但我們可以通過其他方法。通過是同 get_stkvar 函數並檢查返回值是 None 還是一個 object, 我們可以有效檢查操作數是否是一個棧上變量。函數實現如下:
def is_stack_buffer (addr, idx) :
inst = DecodeInstruction(addr)
return
None
注意這個函數和 IDA7 API 並不兼容。在下篇文章中我們會提到檢測棧上 buffer 的新的方法並保持和最近 IDA API 的兼容。
我們現在可以把他們組合成一個腳本來尋找 strcpy 導致的棧溢出漏洞了。通過上述技巧我們可以拓展到不止支持 strcpy, 還可以是 strcat、sprintf 等函數(可以參考 Microsoft Banned Functions List )
完整的腳本:
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
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()
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
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
運行結果
這樣,我們就找到了存在問題的函數地址 0x8048528。
本文介紹瞭如何通過 IDA Python 腳本來實現對棧溢出漏洞的檢測,並以 ascii_easy 一道 PWN 基礎題為例來實戰。
介紹
Python資源共享群:626017123
IDAPython 是一個用於複雜逆向工程任務的強大的自動化工具。儘管有很多文章介紹了用 IDAPython 來簡化基本的逆向任務,但很少有提及使用 IDAPython 來審計二進制漏洞的方法。
因為這不是一個新的方法( Halvar Flake 在2001年做過關於 IDA 腳本自動分析漏洞的研究),但令人驚訝的是,這個話題沒有被更多的說明。這可能是因為在現代操作系統上想要利用漏洞日漸複雜困難。然而,這對於自動化部分的漏洞研究還是有價值的。
在這篇文章中,我們將介紹使用基本的 IDAPython 來檢測程序中出現的能導致棧溢出問題的地方。在這篇文章中,我會用自動化探測方法實戰 pwnable.kr 中的 ascii_easy 二進制題目。儘管這個二進制文件小到我們可以手動整個去分析它,它仍然是一個很好的學習案例以便我們使用相同的 IDAPython 技術取分析更大更復雜的二進制文件。
開始
在我們寫任何 IDAPython 腳本前,我們先要決定我們想讓我們的腳本做什麼。
這裡,我們選擇了簡單漏洞中的一個棧溢出漏洞,其可由 strcpy 函數導致將用戶可以控制的字符串拷貝到棧緩存區中。既然我們知道了我們要尋找什麼,我們可以開始考慮如何取自動化尋找這種漏洞。
我們分為兩步:
1. 定位可能導致棧溢出的函數(這裡我們選取 strcpy 函數)
2. 分析這些函數的調用來決定這個這個調用我們是否感興趣(即是否可能導致漏洞)
定位函數調用
為了尋找任何調用 strcpy 的地方,我們需要首先定位 strcpy 這個函數本身。
使用 IDAPython API 很容易做到這一點。使用如下的代碼片段來打印二進制文件中所有的函數名:
for functionAddr in Functions():
(GetFunctionName(functionAddr))
(注:可以在 IDA 底部的 python 命令控制窗口中輸入命令)
我們可以看到,所有的函數名都被輸出了。
然後,我們要添加過濾取尋找我們感興趣的 strcpy 函數。簡單的字符串比較我們就能達到效果。但因為我們常常需要處理一些函數名相似但仍有區別的情況(例如 _strcpy, 這取決於導入函數的命名),我們最好檢查子字符串。
在前面的基礎上,我們有下面的代碼:
for functionAddr in Functions():
if “strcpy” in GetFunctionName(functionAddr):
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" :
hex(xref)
對 ascii_easy 二進制文件運行此腳本後我們得到如下結果:
即我們找到了 0x451b2c 目標地址。
分析函數調用
現在,通過上面的代碼,我們知道了如何獲取所有程序中調用 strcpy 的地方。而 ascii_easy 恰好就只有這一個調用 strcpy 的地方(也恰好可利用),很多程序有很多調用 strcpy 的地方(很多都不可被利用),因此我們需要一些方法取分析對 strcpy 的調用來根據可利用的可能性進行排序。
一個緩存區溢出漏洞的常見特點是它們往往涉及棧上的緩衝區。儘管在堆上或者別的地方的緩存區溢出也是有可能的,棧溢出是一個更簡單的利用方式。
這涉及一些對 strcpy 函數的目的地參數( destination )的分析,我們知道目的地參數是 strcpy 函數的第一個參數,而且我們可以通過瀏覽函數的反彙編得到這個參數。這個調用 strcpy 函數的反彙編如下:
分析上面的代碼,有兩種可以找到 _strcpy 函數的目的地參數。
第一種方法是依賴 IDA 自動分析後對已知函數的註釋。上圖中, IDA 已經自動檢測到了 _strcpy 函數的 dest 參數並標記。
另一個方法是從函數調用前開始尋找 push 指令。每當我們找到一個這樣的指令,我們可以自增一個計數器直到我們定位到了參數的索引。這裡,既然我們要找的 dest 參數是第一個參數,這個方法將會在先前一個 push 指令停下。
在這些情況中,當我們遍歷這些代碼時,我們要注意某些可以打破函數執行流的指令。例如 ret 或 jmp 這樣改變執行流的指令會使精確分析參數變得困難。另外,我們需要確保我們不遍歷那些當前所處函數之前的地方。現在,我們將只是在搜索參數時識別非順序執行代碼流的地方。如果找到任何非順序代碼流實例,則停止搜索。
我們將使用第二種查找參數的方法(查找被推送到堆棧的參數)。為了幫助我們以這種方式查找參數,我們創建一個 helper 函數。此函數將從函數調用的地址向向前查找,跟蹤 push 到堆棧的參數並返回我們指定參數對應的操作數。
對於上面的例子,helper函數將返回eax寄存器的值,因為eax寄存器保存了strcpy的目標參數dest。結合一些基本的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
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()
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
0
)
為了判斷 eax 是否指向在棧中的緩存區 buffer ,當它被 push 入棧時,我們應該繼續跟蹤 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 in
function_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
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
上面的代碼展示了我們搜索彙編指令以找到保存 dest buffer 的過程。它也展示了很多檢查,例如確保我們沒有沒有搜索到函數開頭之前的地址或任何可能改變執行流的指令。它也嘗試追蹤其他寄存器。例如,此代碼嘗試解釋下面演示的情況。
...
lea ebx [ebp-0x24]
...
mov eax, ebx
...
push eax
...
另外,在上面的代碼中,我們使用了函數 is_stack_buffer ,這個函數是這個腳本的最後一部分,有些東西沒有在 IDA API 中定義。這個函數的目的很簡單,給定指令的地址和操作數索引,檢查這個變量是否是一個棧上的 buffer 。雖然 IDA API 沒有給我們直接提供這樣的函數,但我們可以通過其他方法。通過是同 get_stkvar 函數並檢查返回值是 None 還是一個 object, 我們可以有效檢查操作數是否是一個棧上變量。函數實現如下:
def is_stack_buffer (addr, idx) :
inst = DecodeInstruction(addr)
return
None
注意這個函數和 IDA7 API 並不兼容。在下篇文章中我們會提到檢測棧上 buffer 的新的方法並保持和最近 IDA API 的兼容。
我們現在可以把他們組合成一個腳本來尋找 strcpy 導致的棧溢出漏洞了。通過上述技巧我們可以拓展到不止支持 strcpy, 還可以是 strcat、sprintf 等函數(可以參考 Microsoft Banned Functions List )
完整的腳本:
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
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()
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
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
運行結果
這樣,我們就找到了存在問題的函數地址 0x8048528。