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为基址。