IDAPython腳本編寫指南(一)


IDAPython腳本編寫指南(一)

介紹

IDA可以說是最好的靜態反匯編工具,無論是在漏洞研究,軟件逆向和病毒分析等領域,都是非常重要的工具,最近分析病毒感覺平常的使用並沒有領悟到這款神器的精髓,在逆向時需要花費大量的時間,效率是非常重要的,如果能通過一些腳本將普通的分析過程實現自動化或者半自動化那就是節省了生命了,IDAPython就是一個非常好的腳本工具,而且由於Python語言的特性,只要是有一些編程基礎的人那么學習成本也會非常的低。因為在學習過程中感覺現在網上的資料很少,所以這里記錄自己的學習過程並做一下分享。

工具版本

  • IDA版本:IDA7.0

  • Python版本:Python 2.7.16

注意:目前IDAPython只支持Python [2.5.1, 2.6.1, 2.7],如果電腦上只有Python3版本的話會報錯

基礎

這里的演示全部以IDA7.0 x32為例,點擊File選項

在輸入腳本命令的時候,簡單的直接可以在命令框中輸入,如果腳本功能比較多,直接Shift + F2

  • 基礎函數
Python>ea = idc.get_screen_ea()  # 獲取此時光標所在位置的地址
Python>ea = here()               # 同上
Python>print "0x%x %s" % (ea,ea) # 打印地址
Python>hex(idc.get_inf_attr(INF_MIN_EA))  # 輸出節中起始地址地址
Python>hex(idc.get_inf_attr(INF_MAX_EA))  # 輸出節中結束地址
Python>idc.get_segm_name(ea)           # 獲取所在的區段名稱字符串
.text
Python>idc.generate_disasm_line(ea,0)  # 獲取光標所在行的反匯編指令
Python>idc.print_insn_mnem(ea)         # 獲取助記符 add
Python>idc.print_operand(ea,0)         # 獲取第一個操作數
Python>idc.print_operand(ea,1)         # 獲取第二個操作數

關於節的函數

首先使用這樣一段腳本代碼來遍歷一下分析的程序有多少區段

for seg in idautils.Segments():
print idc.get_segm_name(seg),idc.get_segm_start(seg),idc.get_segm_end(seg)
.text 4198400 4214784
.idata 4214784 4215000
.rdata 4215000 4218880
.data 4218880 4231168

idautils.Segments()這個函數返回的是一個可以迭代的對象,使用for循環來遍歷它,idc.get_segm_name(seg)獲取迭代對象所在段的名稱字符串,idc.get_segm_start(seg) idc.get_segm_end(seg)從名字上就可以看出是獲取區段開始和結束位置的地址。

如果不想遍歷所有的段,只使用一個偏移量來找到下一個段的名稱。可以先獲取當前段內的一個地址ea = here(), ea可以是當前段內的任何地址,再使用idc.get_next_seg(ea)來找到下一個區段

idc.get_next_seg(ea)

函數的遍歷

既然我們知道了如何遍歷所有段,我們就更應該知道如何遍歷所有已知的函數。

Python>for func in idautils.Functions(): print hex(func), idc.get_func_name(func)
# 遍歷已知的函數
.....
0x4038e7L _free
0x403920L _strcpy
0x403930L _strcat
0x403a10L _malloc
0x403a22L __nh_malloc
0x403a4eL __heap_alloc
.....

idautils.Functions() 返回已知函數信息。該信息包含每個函數的起始地址和函數名。idautils.Functions() 返回值傳遞給一個范圍內進行搜索。如果我們想這樣做,我們使用idautils.Functions(start_addr, end_addr)得到開始和結束地址。要獲得函數名,我們使用idc.get_func_name(ea)ea可以是函數范圍內的任何地址。IDAPython包含大量用於處理函數的api。讓我們從一個簡單的函數開始。這個函數的語義並不重要,但是我們應該在心里記住這些地址。

要獲得函數的開始和結束,我們可以使用idaapi.get_func(ea)

Python>ea = here()
Python>func = idaapi.get_func(ea)     # 獲取地址所在函數的范圍
Python>type(func)
<class 'ida_funcs.func_t'>
Python>print "Start: 0x%x, End 0x%x" % (func.startEA,func.endEA)
Start: 0x402b6c, End 0x402b6d               # 輸出的結果查看上圖是正確的

idaapi.get_func(ea) 返回idaapi.func_t類,有時,如何使用返回的類調用函數並不總是很明顯。在Python還有一個有用的命令是dir(class)函數可以用來探索類。


Python>dir(func)   # 以下結果是以我的反匯編程序為例
['__class__', '__del__', '__delattr__', '__dict__', '__doc__', '__eq__', '__format__', '__getattribute__', '__gt__', '__hash__', '__init__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__swig_destroy__', '__weakref__', '_print', 'analyzed_sp', 'argsize', 'clear', 'color', 'compare', 'contains', 'does_return', 'empty', 'endEA', 'end_ea', 'extend', 'flags', 'fpd', 'frame', 'frregs', 'frsize', 'intersect', 'is_far', 'llabelqty', 'llabels', 'overlaps', 'owner', 'pntqty', 'points', 'referers', 'refqty', 'regargqty', 'regargs', 'regvarqty', 'regvars', 'size', 'startEA', 'start_ea', 'tailqty', 'tails', 'this', 'thisown']

從輸出中我們可以看到startEAendEA函數。它們用於訪問函數的開始和結束。這些屬性只適用於當前函數。如果我們想要訪問周圍的函數,我們可以使用idc.get_next_func(ea)idc.get_prev_func(ea)ea的值只需要是被分析函數邊界內的一個地址。關於枚舉函數的一個注意是:它只在IDA將代碼塊標識為函數時才有效。在代碼塊被標記為函數之前,它將在函數枚舉過程中被跳過。沒有標記為函數的代碼在圖例中被標記為紅色(在IDA的GUI的頂部有顏色條)。可以使用函數idc.create_insn(ea)手動修復或自動修復。

IDAPython有許多不同的方法來訪問相同的數據。訪問函數邊界的常用方法是使用idc.get_func_attr(ea, FUNCATTR_START)idc.FUNCATTR_END get_func_attr (ea)

Python>ea = here()
Python>start = idc.get_func_attr(ea, FUNCATTR_START)
Python>end = idc.get_func_attr(ea, FUNCATTR_END)
Python>cur_addr = start
Python>while cur_addr <= end: 
             print hex(cur_addr), idc.generate_disasm_line(cur_addr, 0)
             cur_addr = idc.next_head(cur_addr, end)

上圖是輸出的結果。

idc.get_func_attr(ea, attr)用於獲取函數的開始和結束。然后我們打印當前地址並使用idc.generate_disasm_line(ea, 0)進行反匯編。我們使用idc.next_head(eax)來開始下一條指令,並一直執行下去,直到這個函數結束。這種方法的一個缺陷是它依賴於包含在函數開始和結束邊界內的指令。如果跳轉到一個高於函數末尾的地址,則循環將提前退出。這種類型的跳轉在代碼轉換等混淆技術中非常常見。由於邊界可能不可靠,所以最好調用idautil.funcitems (ea)來遍歷函數中的地址。我們將在下一節詳細介紹這種方法。與idc相似。用於收集函數信息的另一個有用的函數是idc.get_func_attr(ea,FUNCATTR_FLAGS)。它可以用於檢索關於函數的信息,比如它是已知庫代碼還是函數沒有返回值。一個函數有9個可能的標志。如果要枚舉所有函數的所有標志,可以使用以下代碼。

Python> for func in idautils.Functions():
    flags = idc.get_func_attr(func,FUNCATTR_FLAGS)
    if flags & FUNC_NORET:
        print hex(func), "FUNC_NORET"
    if flags & FUNC_FAR:
        print hex(func), "FUNC_FAR"
    if flags & FUNC_LIB:
        print hex(func), "FUNC_LIB"
    if flags & FUNC_STATIC:
        print hex(func), "FUNC_STATIC"
    if flags & FUNC_FRAME:
        print hex(func), "FUNC_FRAME"
    if flags & FUNC_USERFAR:
        print hex(func), "FUNC_USERFAR"
    if flags & FUNC_HIDDEN:
        print hex(func), "FUNC_HIDDEN"
    if flags & FUNC_THUNK:
        print hex(func), "FUNC_THUNK"
    if flags & FUNC_LIB:
        print hex(func), "FUNC_BOTTOMBP"

我們使用idautil.functions()獲取所有已知函數地址的列表,然后使用idc.get_func_attr(ea,FUNCATTR_FLAGS)獲取標志。我們通過對返回的值使用邏輯和(&)操作來檢查該值。例如,要檢查函數是否沒有返回值,我們將使用以下比較if flags & FUNC_NORET現在讓我們檢查一下所有的函數標志。這些標志位有的很常見,有的很少見。

FUNC_NORET

此標志用於標識不執行返回指令的函數。它內部表示為1。下面是一個不返回值的函數示例。


CODE:004028F8 sub_4028F8 proc near
CODE:004028F8
CODE:004028F8 and eax, 7Fh
CODE:004028FB mov edx, [esp+0]
CODE:004028FE jmp sub_4028AC
CODE:004028FE sub_4028F8 endp

注意,retleave不是最后一條指令。

FUNC_FAR

除非逆向的軟件使用分段的內存,否則很少看見這個標志,它在內部表示為2的整數。

FUNC_USERFAR

這個標記很少被看到,並且幾乎沒有文檔。HexRays將這個標志描述為user has specified far-ness of the function(原文翻譯不過來,直接看原詞吧)。它的內部值是32。

FUNC_LIB

此標志用於查找庫代碼。標識庫代碼非常有用,因為在進行分析時通常可以忽略這些代碼。其內部表示為一個整數值4。下面是它的用法和功能的一個例子

Python>for func in idautils.Functions():
 	flags = idc.get_func_attr(func, FUNCATTR_FLAGS)
 	if flags & FUNC_LIB:
 		print hex(func), "FUNC_LIB", idc.get_func_name(func)

FUNC_STATIC

此標志用於標識已編譯為靜態函數的函數。在C語言中,靜態函數默認是全局的。如果作者將函數定義為靜態函數,則該函數只能被該文件中的其他函數訪問。在一定程度上,這可以用來幫助理解源代碼是如何構造的。

FUNC_FRAME

此函數用於標志用了ebp作為棧幀的函數,這個函數有典型的開頭,如下:

.text:1A716697 push ebp
.text:1A716698 mov ebp, esp
.text:1A71669A sub esp, 5Ch

FUNC_BOTTOMBP

與FUNC_FRAM類似,它標識棧底指針指向堆棧指針的函數

FUNC_HIDDEN

帶有FUNC_HIDDEN標標志的函數意味着它們是隱藏的,需要展開才能查看。如果我們訪問一個被標記為隱藏的函數的地址,它將自動展開。

FUNC_THUNK

此標志標識為thunk(中轉)函數,即使用了一個jmp的函數。

.text:1A710606 Process32Next proc near
.text:1A710606 jmp ds:__imp_Process32Next
.text:1A710606 Process32Next endp

應該注意的是,一個函數可以有多個標志組成。下面是一個具有多個標記的函數示例。

0x1a716697 FUNC_LIB
0x1a716697 FUNC_FRAME
0x1a716697 FUNC_HIDDEN
0x1a716697 FUNC_BOTTOMBP

有時需要將一段代碼或數據定義為一個函數。例如,下面的代碼沒有被定義為函數

.text:00407DC1
.text:00407DC1 mov ebp, esp
.text:00407DC3 sub esp, 48h
.text:00407DC6 push ebx

定義一個功能我們可以使用idc.MakeFunction(start, end)

Python>idc.MakeFunction(0x00407DC1, 0x00407E90)

idc.MakeFunction(start, end)是函數的開始地址,第二個是函數的結束地址。在許多情況下,結束地址是不需要的,而IDA會自動識別函數的結束。下面的程序集是執行上述代碼的輸出。

.text:00407DC1 sub_407DC1 proc near
.text:00407DC1
.text:00407DC1 SystemInfo= _SYSTEM_INFO ptr -48h
.text:00407DC1 Buffer = _MEMORY_BASIC_INFORMATION ptr -24h
.text:00407DC1 flOldProtect= dword ptr -8
.text:00407DC1 dwSize = dword ptr -4
.text:00407DC1
.text:00407DC1 mov ebp, esp
.text:00407DC3 sub esp, 48h
.text:00407DC6 push ebx

總結

通過以上這些函數,我們就可以很快的來查看一個程序的基礎信息,在拿到一個程序時只需要跑一下這個腳本,就很容易拿到程序的區段函數等信息,例如:

import idautils
# 查看區段
print "+++++++++++++++++++++++"
for seg in idautils.Segments():
    print idc.get_segm_name(seg),idc.get_segm_start(seg),idc.get_segm_end(seg),  "|"
# 查看已知庫函數信息
print "+++++++++++++++++++++++++"
for func in idautils.Functions():
    print hex(func),idc.get_func_name(func)

# 獲得光標所在地址的函數指令
print "-----------------------------"
ea = here()
start = idc.get_func_attr(ea, FUNCATTR_START)
end = idc.get_func_attr(ea, FUNCATTR_END)
cur_addr = start
while cur_addr <= end: 
    print hex(cur_addr), idc.generate_disasm_line(cur_addr, 0) 
    cur_addr = idc.next_head(cur_addr, end)

在拿到一個樣本時先跑一下上面的腳本,把自己想要查看的函數信息拿出來,就不需要再一個個每個窗口去尋找了,下一篇會分享關於助記符的函數使用。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM