IDAPython腳本編寫指南(二)
關於指令
在上一篇已經學會了使用函數,現在可以繼續來學習指令了,如果我們有一個函數的地址,我們可以使用idautils.FuncItems(ea)
來獲得所有地址的列表。
Python>dism_addr = list(idautils.FuncItems(here()))
Python>type(dism_addr)
<type 'list'>
Python>print dism_addr
[4199264L, 4199265L, 4199267L, 4199268L, 4199275L, 4199276L, 4199278L, 4199281L, 4199284L, 4199290L, 4199293L, 4199299L, 4199302L, 4199307L, 4199310L, 4199316L, 4199318L, 4199325L, 4199327L, 4199329L, 4199330L, 4199335L, 4199336L, 4199337L, 4199339L, 4199344L, 4199347L, 4199350L, 4199352L, 4199354L, 4199356L, 4199358L, 4199364L, 4199366L, 4199369L, 4199372L, 4199377L, 4199379L, 4199383L, 4199386L, 4199389L, 4199391L, 4199393L, 4199397L, 4199400L, 4199402L, 4199403L, 4199406L, 4199408L, 4199410L, 4199412L, 4199413L, 4199414L, 4199417L, 4199418L, 4199423L, 4199429L, 4199434L, 4199437L, 4199439L, 4199441L, 4199444L, 4199446L, 4199450L, 4199452L, 4199456L, 4199460L, 4199463L, 4199465L, 4199466L, 4199467L]
Python>for line in dism_addr: print hex(line),idc.generate_disasm_line(line,0)
....
idautils.FuncItems(ea)
返回一個 iterator type
類型,但它被強制轉換為一個list
。該list
按連續順序包含每個指令的起始地址。現在我們已經有了一個遍歷段、函數和指令的良好基礎,下面讓我們展示一個有用的例子。有時,當逆向打包代碼時,只知道在某個地方動態調試是很有用的。動態調試可以調試調用call
和跳轉jmp
,例如call eax
或者 jmp edi
。
python>for func in idautils.Functions(): # 獲取已知函數list
flags = idc.get_func_attr(func,FUNCATTR_FLAGS) # 獲取函數的標志
if flags & FUNC_LIB or flags & FUNC_THUNK: # 標志是否是FUNC_LIB 或者 FUNC_FLAGS
continue
dism_addr = list(idautils.FuncItems(func)) # 函數指令地址
for line in dism_addr:
m = idc.print_insn_mnem(line)
if m == 'call' or m == 'jmp':
op = idc.get_operand_type(line,0)
if op == o_reg:
print "0x%x %s" % (line,idc.generate_disasm_line(line,0))
我們使用idautils.Functions()
來獲得所有已知函數的list
,對於每個函數,我們通過調用idc.get_func_attr(ea, FUNCATTR_FLAGS)
檢索函數標志。如果函數是庫代碼或thunk
函數,則傳遞該函數。接下來,我們調用idautil.funcitems()
來獲取函數中的所有地址。我們使用for
循環遍歷list
。因為我們只對call
和jmp
指令感興趣,所以我們需要通過調用idc.print_insn_mnem()
來獲得助記符。然后,我們使用一個簡單的字符串比較來檢查助記符。如果助記符是call
或jmp
,我們通過調用idc.get_operand_type(ea,n)
來獲得操作數類型。這個函數返回一個內部稱為op_t.type
的整數。此值可用於確定操作數是否是寄存器、內存引用等。然后檢查op_t.type
是一個寄存器。如果是,則打印該行。將idautil.funcitems()
的返回值轉換成列表是很有用的,因為迭代器沒有len()
這樣的對象。通過將它轉換為一個list
,我們可以很容易地獲得一個函數中的行數或指令數。
Python>ea = here()
Python>len(idautils.FuncItems(ea))
Traceback (most recent call last):
File "<string>", line 1, in <module>
TypeError: object of type 'generator' has no len()
Python>len(list(idautils.FuncItems(ea)))
49
在前面的示例中,我們使用了一個包含函數中所有地址的list
。我們遍歷每個實例以訪問下一條指令。如果我們只有一個地址而且想要獲得下一條指令,該怎么辦?要,我們可以使用idc.next_head(ea)
移動到下一個指令地址,並使用idc.prev_head(ea)
獲得前一個指令地址。這些函數得到的是下一條指令的開始的位置,而不是下一個地址。要獲取下一個地址,我們使用idc.next_addr(ea)
,要獲取前一個地址,我們使用idc.prev_head(ea)
。
ea = here()
print hex(ea),idc.generate_disasm_line(ea,0)
next_instr = idc.next_head(ea)
print hex(next_instr),idc.generate_disasm_line(next_instr,0)
prev_instr = idc.prev_head(ea)
print hex(prev_instr),idc.generate_disasm_line(prev_instr,0)
print hex(idc.next_addr(ea))
print hex(idc.prev_addr(ea))
在動態調試的示例中,IDAPython代碼依賴於使用jmp
和call
的字符串比較,我們也可以使用idaapi.decode_insn(ea)
來解碼指令,而不是使用字符串比較,對一條指令進行解碼是更加好的方法,因為使用整型指令表示可以更快、更少出錯。不幸的是,整數表示是特定於IDA
的,無法方便的移植到其它反匯編工具,下面是使用idaapi.decode_insn(ea
並比較整數表示形式的相同示例。
Python>JMPS = [idaapi.NN_jmp,idaapi.NN_jmpfi,idaapi.NN_jmpni]
Python>CALLS = [idaapi.NN_call,idaapi.NN_callfi,idaapi.NN_callni]
# 使用另外一種表示方法來表示上面相同的示例
for func in idautils.Functions():
flags = idc.get_func_attr(func,FUNCATTR_FLAGS)
if flags & FUNC_LIB or flags & FUNC_THUNK: # 忽略庫函數和thunk
continue
dism_addr = list(idautils.FuncItems(func))
for line in dism_addr:
idaapi.decode_insn(line)
if idaapi.cmd.itype in CALLS or idaapi.cmd.itype in JMPS:
if idaapi.cmd.Op1.type == o_reg:
print "0x%x %s" % (line,idc.generate_disasm_line(line,0))
輸出和前面的示例相同,前兩行將jmp
和call
放入連個lists
中,由於我們沒有使用助記符字符串的表示形式。我們需要認識到,助記符(例如call
和jmp
)可以有多個值。例如:jmp
可以使用idaapi.NN_jmp
表示跳轉,idaapi.NN_jmpfi
表示間接遠跳,或者idaapi.NN_jmpni
表示間接近跳,X86
和X64
指令類型都以NN開頭。
找到這超過1700多個指令類型,我們可以在命令行中執行[name for name in dir(idaapi) if "NN"]
,或者在IDA的SDK文件allins.hpp中查看它們。一旦我們在列表中有了指令,我們使用idautil . functions()
和get_func_attr(ea, FUNCATTR_FLAGS)
的組合來獲得所有適用的函數,同時忽略庫和thunks
。我們通過調用idautil.funcitems (ea)
來獲取函數中的每條指令。這是調用新引入的函數idaapi.decode_insn(ea)
的地方。這個函數找到我們想要解碼指令的地址,一旦解碼成功,我們可以通過idaapi.cmd
訪問指令的不同屬性。
Python>dir(idaapi.cmd)
['Op1', 'Op2', 'Op3', 'Op4', 'Op5', 'Op6', 'Operands', '__class__', '__del__', '__delattr__', '__dict__', '__doc__', '__format__', '__get_auxpref__', '__get_operand__', '__get_ops__', '__getattribute__', '__getitem__', '__hash__', '__init__', '__iter__', '__module__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__set_auxpref__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__swig_destroy__', '__weakref__', 'add_cref', 'add_dref', 'add_off_drefs', 'assign', 'auxpref', 'create_op_data', 'create_stkvar', 'cs', 'ea', 'flags', 'get_canon_feature', 'get_canon_mnem', 'get_next_byte', 'get_next_dword', 'get_next_qword', 'get_next_word', 'insnpref', 'ip', 'is_canon_insn', 'is_macro', 'itype', 'ops', 'segpref', 'size', 'this', 'thisown']
可以從dir()
命令查看到idaapi.cmd
有很多的屬性,操作數類型通過idaapi.cmd.Op1.type
訪問。請注意,操作數索引從1開始,不同於IDC中get_operand_type(ea,n)的從0開始。
操作數
操作數類型是常用的,所以最好遍歷所有類型。如前所述,我們可以使用idc.get_operand_type(ea,n)
來獲取操作數類型。ea
是地址,n
是索引。有八種不同類型的操作數類型。
o_void
當一個指令沒有任何操作數時返回0。
Python>ea = here()
Python>print hex(ea), idc.generate_disasm_line(ea,0)
0x40142bL retn
Python>print idc.get_operand_type(ea,0)
0
o_reg
如果操作數是常規寄存器時,它將返回1。
Python>ea = here()
Python>print hex(ea), idc.generate_disasm_line(ea,0)
0x401429L pop ebx
Python>print idc.get_operand_type(ea,0) # 操作數是一個寄存器時返回1
1
o_mem
如果操作數是直接內存引用,它將返回2。這種類型對於查找對數據的引用很有用。
Python>ea = here()
Python>print hex(ea), idc.generate_disasm_line(ea,0)
0x401364L cmp dword_406C80, 0
Python>print idc.get_operand_type(ea,0)
2
o_phrase
如果操作數包含基址寄存器和/或標志寄存器,則返回此操作數。這個值在內部表示為3。
Python>print hex(ea), idc.generate_disasm_line(ea,1)
0x4013b0L mov al, [eax+ebx*2]
Python>print idc.get_operand_type(ea,1)
3
o_displ
如果操作數由寄存器和一個數字偏移時,則返回4。偏移是一個整數值,比如0x18
。當一條指令訪問一個結構中的值時,通常會出現這種情況。
Python>print hex(ea), idc.generate_disasm_line(ea,0)
0x40132dL lea edx, [esp+28h+Msg]
Python>print idc.get_operand_type(ea,1)
4
o_imm
當操作數是立即數時,返回5
Python>print hex(ea), idc.generate_disasm_line(ea,0)
0x401358L add esp, 1Ch
Python>print idc.get_operand_type(ea,1)
5
o_far
這個操作數在逆向x86
或者x64
時很不常見,用於查找當前遠跳地址的操作數,返回6
o_fear
這個操作數在逆向x86
或x86_64
時不是很常見。用於查找近跳地址的操作數,返回7
一個例子
有時,當逆向可執行文件的內存dump
時,操作數不能被識別為偏移
seg000:00BC1388 push 0Ch
seg000:00BC138A push 0BC10B8h
seg000:00BC138F push [esp+10h+arg_0]
seg000:00BC1393 call ds:_strnicmp
push
進的第二個數值是內存偏移,如果我們右鍵點擊它,把它變成一個數據類型,我們會看到一個字符串的偏移量。我們完全可以將這個過程自動化。
# 當操作數是立即數時
min = idc.get_inf_attr(INF_MIN_EA)
max = idc.get_inf_attr(INF_MAX_EA)
# 對於每個已知的函數
for func in idautils.Functions():
flags = idc.get_func_attr(func,FUNCATTR_FLAGS)
#忽略庫函數和thunk
if flags & FUNC_LIB or flags & FUNC_THUNK:
continue
dism_addr = list(idautils.FuncItems(func))
for curr_addr in dism_addr:
if idc.get_operand_type(curr_addr,0) == 5 and \
(min < idc.get_operand_value(curr_addr,0) < max):
idc.OpOff(curr_addr,0,0)
if idc.get_operand_type(curr_addr,1) == 5 and \
(min < idc.get_operand_value(curr_addr,1) < max):
idc.op_plain_offset(cur_addr,1,0)
運行以上代碼后,我們可以看到以下字符
seg000:00BC1388 push 0Ch
seg000:00BC138A push offset aNtoskrnl_exe ; "ntoskrnl.exe"
seg000:00BC138F push [esp+10h+arg_0]
seg000:00BC1393 call ds:_strnicmp
一開始,我們通過調用idc.get_inf_attr(INF_MIN_EA)
和idc.get_inf_attr(INF_MAX_EA)
來獲得最小和最大地址函數或指令,檢查操作數類型是否為o_imm
(5),找到這個值后,就通過調用idc.get_operand_value(ea,n)
來讀取該值,如果值在最小和最大地址的范圍內,使用idc.op_plain_offset(ea, n, base)
將操作數轉換為偏移量,第一個參數ea是地址,n是操作數索引,base是基址例子中是以0為基址。