IDAPython脚本编写指南(一)
介绍
IDA
可以说是最好的静态反汇编工具,无论是在漏洞研究,软件逆向和病毒分析等领域,都是非常重要的工具,最近分析病毒感觉平常的使用并没有领悟到这款神器的精髓,在逆向时需要花费大量的时间,效率是非常重要的,如果能通过一些脚本将普通的分析过程实现自动化或者半自动化那就是节省了生命了,IDAPython
就是一个非常好的脚本工具,而且由于Python语言的特性,只要是有一些编程基础的人那么学习成本也会非常的低。因为在学习过程中感觉现在网上的资料很少,所以这里记录自己的学习过程并做一下分享。
工具版本
-
IDA版本:IDA7.0
-
Python版本:Python 2.7.16
注意:目前IDAPython只支持Python [2.5.1, 2.6.1, 2.7]
,如果电脑上只有Python3
版本的话会报错
- IDAPython源码地址 https://github.com/idapython/src 目前IDAPython支持IDA6.6 - IDA7.4
基础
这里的演示全部以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']
从输出中我们可以看到startEA
和endEA
函数。它们用于访问函数的开始和结束。这些属性只适用于当前函数。如果我们想要访问周围的函数,我们可以使用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
注意,ret
或leave
不是最后一条指令。
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)
在拿到一个样本时先跑一下上面的脚本,把自己想要查看的函数信息拿出来,就不需要再一个个每个窗口去寻找了,下一篇会分享关于助记符的函数使用。