当前位置:   article > 正文

idapython

idapython

对于许多用户来说,使用函数hex来打印地址是常见的做法。随着升级到IDA 7+,使用十六进制打印地址的用户将不再有可点击的地址。现在的地址类型是long而不是int。如果你需要打印的地址是可点击的,请使用 字符串格式化。下面的第一个打印地址是long,不能点击。打印的地址 使用字符串格式化的地址是可打印的。

Python>ea = idc.get_screen_ea() # get address of cursor 
Python>print(hex(ea)) # print unclickable address
0x407e3bL
Python>print("0x%x" % ea) # print clickable address
0x407e3b

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

基础

当使用 在使用IDAPython APIs时,最常见的传递变量是一个地址。在IDAPython 文档中,该地址被称为ea。

获取光标的地址

idc.get_screen_ea()
here()
返回int类型的地址
  • 1
  • 2
  • 3

获取当前基本快的范围

idc.get_inf_attr(INF_MIN_EA)
idc.get_inf_attr(INF_MAX_EA)
  • 1
  • 2
Python>ea = idc.get_screen_ea()
Python>print("0x%x %s" % (ea, ea))
0x401570 4199792
Python>ea = here()
Python>print("0x%x %s" % (ea, ea))
0x401570 419972
Python>print("0x%x" % idc.get_inf_attr(INF_MIN_EA))
0x401000
Python>print("0x%x" % idc.get_inf_attr(INF_MAX_EA))
0x41d000
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

各种元素

Python>idc.get_segm_name(ea) # get text
.text
Python>idc.generate_disasm_line(ea, 0) # get disassembly
lea eax, [ebp+arg_0]
Python>idc.print_insn_mnem(ea) # get mnemonic助记符
lea
Python>idc.print_operand(ea,0) # get first operand操作符
eax
Python>idc.print_operand(ea,1) # get second operand
[ebp+arg_0]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

为了获得段名的字符串表示,我们使用idc.get_segm_name(ea),其中ea 是该段内的一个地址。打印反汇编的字符串,可以使用 idc.generate_disasm_line(ea, 0)。参数是存储在ea中的地址和一个标志为 0. 标志0返回IDA在分析过程中发现的显示的反汇编。当标志0通过时,ea可以是指令偏移范围内的任何地址。要反汇编一个精确的偏移量 忽略IDA的分析,可以使用1的标志。要想获得助记符或指令名,我们可以 调用idc.print_insn_mnem(ea)。要获得助记符的操作数,我们将调用 idc.print_operand(ea, long n)。第一个参数是地址,第二个长的n是 操作数索引。第一个操作数是0,第二个是1,接下来的每个操作数都会递增 为1。

在某些情况下,验证一个地址的存在是很重要的。 idaapi.BADADDR、idc.BADADDR或 BADADDR可以用来检查有效的地址

Python>idaapi.BADADDR
4294967295
Python>print("0x%x" % idaapi.BADADDR)
0xffffffff
Python>if BADADDR != here(): print("valid address")
valid address

64位0xffffffffffffffff
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

打印一个单行并不是特别有用。IDAPython的力量来自于对所有指令的迭代,交叉引用地址和搜索代码或数据。后两者将在后面的章节中详细介绍。也就是说,遍历所有段是一个好的开始

Python>for seg in idautils.Segments():
	print("%s, 0x%x, 0x%x" % (idc.get_segm_name(seg), idc.get_segm_start(seg), 
idc.get_segm_end(seg)))
Python>
.textbss, 0x401000, 0x411000
.text, 0x411000, 0x418000
.rdata, 0x418000, 0x41b000
.data, 0x41b000, 0x41c000
.idata, 0x41c000, 0x41c228
.00cfg, 0x41d000, 0x41e000

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

idautils.Segments()返回一个迭代器类型的对象。我们可以通过使用for循环来循环浏览该对象。列表中的每一项都是一个段的起始地址。如果我们把这个地址作为参数传给idc.get_segm_name(ea),就可以用它来获得段名。通过调用idc.get_segm_start(ea)或idc.get_segm_end(ea)可以找到这些段的开始和结束。地址或ea需要在段的开始或结束的范围内。如果我们不想遍历所有段,而是想从一个偏移量找到下一个段,我们可以使用 idc.get_next_seg(ea)。所传递的地址可以是我们想找到下一个段的段内的任何地址。如果我们想通过名字获得一个段的起始地址,我们可以使用idc.get_segm_by_sel(idc.selector_by_name(str_SectionName))。该函数 idc.selector_by_name(segname)返回段选择器,并被传递一个段名的字符串参数。段落选择器是一个整数,从1开始,为可执行文件中的每个段(又称节)递增。idc.get_segm_by_sel(int)被传递段选择器并返回段的起始地址。

函数

Python>for func in idautils.Functions():
	       print("0x%x, %s" % (func, idc.get_func_name(func)))
Python>
0x401000, sub_401000
0x401006, w_vfprintf
0x401034, _main
…removed…
0x401c4d, terminate
0x401c53, IsProcessorFeaturePresent
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

idautils.Functions()返回一个已知函数的列表。该列表包含每个函数的起始地址。idautils.Functions()可以通过参数在一个范围内搜索。如果我们要做到这一点,我们将传递起始和结束地址idautils.Functions(start_addr, end_addr)。为了获得一个函数的名称,我们使用 idc.get_func_name(ea)。在函数边界内的任何地址。IDAPython包含了一大套用于处理函数的API。让我们从一个简单的函数开始。这个函数的语义并不重要,但我们应该在脑海中建立一个 记忆中的地址。

Python>func = idaapi.get_func(ea)
Python>type(func)
<class 'ida_funcs.func_t'>
Python>print("Start: 0x%x, End: 0x%x" % (func.start_ea, func.end_ea))
Start: 0x45c7c3, End: 0x45c7cd
  • 1
  • 2
  • 3
  • 4
  • 5

idaapi.get_func(ea) 返回 ida_funcs.func_t 的一个类。有时并不总是很明显 如何使用一个由函数调用返回的类。在 Python 中探索类的一个有用的命令是 dir(class) 函数

Python>dir(func)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', 
'__format__', '__ge__', '__get_points__', '__get_regvars__', '__get_tails__', 
'__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', 
'__le__', '__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', 'need_prolog_analysis', 'overlaps', 'owner', 'pntqty', 'points', 'referers', 'refqty', 'regargqty', 'regargs', 'regvarqty', 'regvars', 'size', 'startEA', 'start_ea', 'tailqty', 'tails', 'this','thisown']
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

从输出中我们可以看到函数start_ea和end_ea。这些是用来访问函数的开始和结束的。结束地址不是最后一条指令中的最后一个地址,而是在该指令之后的一个字节。这些属性只适用于当前的函数。如果我们想访问周围的函数,我们可以使用idc.get_next_func(ea)和idc.get_prev_func(ea)。ea的值只需要是被分析函数边界内的一个地址。枚举函数的一个注意事项是,它只在IDA已经确定了 的代码块作为一个函数。在代码块被标记为函数之前,它在函数枚举过程中被跳过。函数枚举过程中被跳过。没有被标记为函数的代码在图例中被标记为红色 (在IDA的GUI中顶部的彩色条)。这些代码可以手动修复,或者使用函数自动修复 idc.create_insn(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("0x%x %s" % (cur_addr, idc.generate_disasm_line(cur_addr, 0)))
 		   cur_addr = idc.next_head(cur_addr, end)
Python>
0x45c7c3 mov eax, [ebp-60h]
0x45c7c6 push eax ; void *
0x45c7c7 call w_delete
0x45c7cc retn
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

idc.get_func_attr(ea, attr) 被用来获取函数的开始和结束。然后我们通过使用 idc.generate_disasm_line(ea, 0) 打印当前地址和反汇编。我们使用 idc.next_head(ax)来获取下一条指令的开始,并继续下去,直到我们到达这个函数的结束。这个函数。这种方法的一个缺陷是它依赖于指令被包含在 边界内。如果有一个跳转到比函数结束更高的地址 循环就会过早地退出。这些类型的跳转在混淆技术中是很常见的 这些类型的跳转在混淆技术(如代码转换)中很常见。由于边界可能是不可靠的,所以最好的做法是调用idautils.FuncItems(ea)在一个函数中进行地址循环。我们将在下文中详细介绍

idc.get_func_attr(ea, FUNCATTR_FLAGS)

与 idc.get_func_attr(ea, attr) 类似,另一个收集函数信息的有用参数是 idc.get_func_attr(ea, FUNCATTR_FLAGS) 。FUNCATTR_FLAGS 可以用于 用来检索关于一个函数的信息,比如它是否是库代码,或者该函数是否不 返回一个值。一个函数有九个可能的标志。如果我们想列举所有函数的所有标志,我们可以使用 所有函数的所有标志,我们可以使用下面的代码。

Python>import idautils
Python>for func in idautils.Functions():
 	flags = idc.get_func_attr(func,FUNCATTR_FLAGS)
 	if flags & FUNC_NORET:
 		print("0x%x FUNC_NORET" % func)
 	if flags & FUNC_FAR:
 		print("0x%x FUNC_FAR" % func)
 	if flags & FUNC_LIB:
 		print("0x%x FUNC_LIB" % func)
 	if flags & FUNC_STATIC:
 		print("0x%x FUNC_STATIC" % func)
 	if flags & FUNC_FRAME:
 		print("0x%x FUNC_FRAME" % func)
 	if flags & FUNC_USERFAR:
 		print("0x%x FUNC_USERFAR" % func)
	if flags & FUNC_HIDDEN:
 		print("0x%x FUNC_HIDDEN" % func)
 	if flags & FUNC_THUNK:
 		print("0x%x FUNC_THUNK" % func)
 	if flags & FUNC_LIB:
 		print("0x%x FUNC_BOTTOMBP" % func)
Python>
0x401006 FUNC_FRAME
0x40107c FUNC_LIB
0x40107c FUNC_STATIC
…
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

我们使用 idautils.Functions() 得到一个所有已知函数地址的列表,然后我们使用 idc.get_func_attr(ea, FUNCATTR_FLAGS)来获取标志。我们通过使用一个 对返回值进行逻辑 AND (&) 操作来检查。例如,要检查该函数是否没有一个 返回值,我们将使用以下比较,如果flags & FUNC_NORET。现在让我们来看看 所有的函数标志。这些标志中有些是很常见的,而其他的则很罕见。

FUNC_NORET

请注意,ret或leave不是最后的指令。函数是否有返回. 表示1

FUNC_FAR

除非逆向使用分段内存的软件,否则很少看到这个标志。它在内部是 表示为一个2的整数。

FUNC_FAR

这个标志很少见,也没有什么文档。Hex-Rays将该标志描述为 “用户已经指定了 函数的far-ness”。它的内部值为32。

FUNC_LIB

这个标志是用来寻找库代码的。识别库代码是非常有用的,因为它是在做分析时通常可以忽略的代码。其内部表示为4的整数值。

Python>for func in idautils.Functions():
 flags = idc.get_func_attr(func, FUNCATTR_FLAGS)
 if flags & FUNC_LIB:
 print("0x%x FUNC_LIB %s" % (func,idc.get_func_name(func)))
Python>
0x40107c FUNC_LIB ?pre_c_initialization@@YAHXZ
0x40113a FUNC_LIB ?__scrt_common_main_seh@@YAHXZ
0x4012b2 FUNC_LIB start
0x4012bc FUNC_LIB ?find_pe_section@@YAPAU_IMAGE_SECTION_HEADER@@QAEI@Z
0x401300 FUNC_LIB ___scrt_acquire_startup_lock
0x401332 FUNC_LIB ___scrt_initialize_crt
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

FUNC_STATIC

这个标志用于识别具有基于静态ebp框架的库函数。

FUNC_FRAME

这个标志表示该函数使用了一个帧指针ebp。使用帧指针的函数通常 以标准函数的序言开始,用于设置堆栈框架

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

FUNC_BOTTOMBP

和FUNC_FRAM一样,这个标志也是用来跟踪帧指针的。它可以识别出基础指针指向堆栈指针的函数。

FUNC_HIDDEN

带有FUNC_HIDDEN标志的函数意味着它们是隐藏的,需要展开才能看到。如果我们进入一个被标记为隐藏的函数的地址,它将自动被 展开。

FUNC_THUNK

这个标志标识了属于thunk函数的函数。它们是跳转到另一个函数的简单函数。

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

应该注意的是,一个函数可以由多个标志组成。下面是一个函数的例子。

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

  • 1
  • 2
  • 3
  • 4
  • 5

有时,一段代码或数据需要被定义为一个函数。例如,下面的代码在分析阶段还没有被定义为一个函数,或者没有交叉引用。

.text:00407DC1 
.text:00407DC1 mov ebp, esp
.text:00407DC3 sub esp, 48h
.text:00407DC6 push ebx
  • 1
  • 2
  • 3
  • 4

要定义一个函数,我们可以使用 idc.add_func(start, end) 。

Python>idc.add_func(0x00407DC1, 0x00407E90)
  • 1

idc.add_func(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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

提取函数的参数

在IDAPython中,提取函数参数并不总是一个简单的任务。在许多情况下,需要确定一个函数的调用约定,并且必须使用反向追踪或类似的技术来手动解析参数。由于有大量的调用约定,这并不总是可行的通用实现。IDAPython确实包含一个名为 idaapi.get_arg_addrs(ea)的函数,可以用来获取参数的地址,如果IDA能够 识别出被调用函数的原型。这种识别并不总是存在的,但它在调用API或64位代码中经常可以看到。例如,在下面的汇编中,我们 可以看到API SendMessage有四个参数传递给它。

.text:000000014001B5FF js loc_14001B72B
.text:000000014001B605 mov rcx, cs:qword_14002D368 ; hWnd
.text:000000014001B60C xor r9d, r9d ; lParam
.text:000000014001B60F xor r8d, r8d ; wParam
.text:000000014001B612 mov edx, 0BDh ; '½' ; Msg
.text:000000014001B617 call cs:SendMessageW
.text:000000014001B61D xor esi, esi
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

通过idaapi.get_arg_addrs(ea)可以得到参数地址的列表

Python>ea = 0x00014001B617
Python>idaapi.get_arg_addrs(ea)
[0x14001b605, 0x14001b612, 0x14001b60f, 0x14001b60c]
  • 1
  • 2
  • 3

指令

idautils.FuncItem(here())返回func_item_iterator_t *类型的数据,用list转换格式得到指令地址的列表

Python>dism_addr = list(idautils.FuncItems(here()))
Python>type(dism_addr)
<type 'list'>
Python>print(dism_addr)
[4573123, 4573126, 4573127, 4573132]
Python>for line in dism_addr: print("0x%x %s" % (line, 
idc.generate_disasm_line(line, 0)))
0x45c7c3 mov eax, [ebp-60h]
0x45c7c6 push eax ; void *
0x45c7c7 call w_delete
0x45c7cc retn
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

idautils.FuncItems(ea)返回一个迭代器类型,但被转换为一个列表。该列表包含了每个指令的起始 顺序的每条指令的起始地址。现在我们已经有了一个很好的知识基础,可以通过段、函数和指令进行循环;让我们来看看一个有用的例子。有时当逆向打包的代码时,只知道动态调用发生的地方是有用的。一个动态调用是指对一个寄存器的操作数的调用或跳转,如调用eax或jmp edi。

Python>
for func in idautils.Functions():
	flags = idc.get_func_attr(func, FUNCATTR_FLAGS)
    if flags & FUNC_LIB or flags & FUNC_THUNK:
    	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)))
Python>
0x43ebde call eax ; VirtualProtect
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

我们调用idautils.Functions()来获取所有已知函数的列表。对于每个函数,我们通过调用 idc.get_func_attr(ea, FUNCATTR_FLAGS) 函数的标志,调用 idc.get_func_attr(ea,FUNCATTR_FLAGS) 。

如果该函数是库代码或thunk函数,则该函数被跳过。接下来,我们调用idautils.FuncItems(ea)来获得该函数中的所有地址。我们用for循环的方式循环浏览这个列表。由于我们只对调用和jmp指令感兴趣,我们需要通过调用idc.print_insn_mnem(ea)获得助记符。然后我们用一个简单的字符串比较来检查助记符。如果助记符是跳转或调用,我们通过调用 idc.get_operand_type(ea, n) 得到操作数类型。这个函数返回一个整数,内部称为op_t.type。这个值可以用来确定操作数是否是一个寄存器、内存引用等。然后我们检查op_t.type是否是一个 寄存器。如果是,我们就打印这一行。将 idautils.FuncItems(ea) 的返回值转换为一个列表是很有用的 因为迭代器没有 len() 这样的对象。通过将其转换为一个列表,我们可以很容易地得到 一个函数中的行数或指令数。

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)))
39
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

在前面的例子中,我们使用了一个包含一个函数内所有地址的列表。我们循环 来访问下一条指令。如果我们只有一个地址并想获得 下一条指令呢?我们可以使用idc.next_head(ea)来移动到下一条指令的地址,而 来获取上一条指令的地址,我们可以使用idc.prev_head(ea)。这些函数可以得到下一条指令的开始 但不是下一条指令的地址。要获得下一个地址,我们使用 idc.next_addr(ea)来获取下一条指令的起始地址,并使用idc.prev_head(ea)来获取上一条指令的地址。

Python>ea = here()
Python>print("0x%x %s" % (ea, idc.generate_disasm_line(ea, 0)))
0x10004f24 call sub_10004F32
Python>next_instr = idc.next_head(ea)
Python>print("0x%x %s" % (ea, idc.generate_disasm_line(next_instr, 0)))
0x10004f29 mov [esi], eax
Python>prev_instr = idc.prev_head(ea)
Python>print("0x%x %s" % (ea, idc.generate_disasm_line(prev_instr, 0)))
0x10004f1e mov [esi+98h], eax
Python>print("0x%x" % idc.next_addr(ea))
0x10004f25
Python>print("0x%x" % idc.prev_head(ea))
0x10004f23
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

在动态调用的例子中,IDAPython代码依赖于使用jmp和call的字符串比较。与其使用字符串比较,我们还可以使用以下方法对指令进行解码 idaapi.decode_insn(insn_t, ea)。第一个参数是一个来自ida_ua的insn_t类,该类是通过调用 ida_ua.insn_t() 创建。这个类被填充了属性,一旦 idaapi.decode_insn被调用后,这个类将被填充属性。第二个参数是要分析的地址。解码一个 指令可能是有利的,因为使用指令的整数表示法工作 可以更快、更不容易出错。不幸的是,整数表示法是IDA特有的,不能很容易地移植到其他磁盘上。不能轻易移植到其他反汇编工具上。下面是同一个例子,但使用 idaapi.decode_insn(insn_t, ea)和比较整数表示法。

Python>JMPS = [idaapi.NN_jmp, idaapi.NN_jmpfi, idaapi.NN_jmpni]
Python>CALLS = [idaapi.NN_call, idaapi.NN_callfi, idaapi.NN_callni]
Python>
for func in idautils.Functions():
	flags = idc.get_func_attr(func, FUNCATTR_FLAGS)
	if flags & FUNC_LIB or flags & FUNC_THUNK:
		continue
	dism_addr = list(idautils.FuncItems(func))
	for line in dism_addr:
		ins = ida_ua.insn_t()
		idaapi.decode_insn(ins, line)
		if ins.itype in CALLS or ins.itype in JMPS:
			if ins.Op1.type == o_reg:
				print("0x%x %s" % (line, idc.generate_disasm_line(line, 0))
Python>
0x43ebde call eax ; VirtualProtect

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

输出结果与前面的例子相同。前两行将jmp和call的常量放入两个列表中。由于我们不是在处理助记符的字符串表示法,我们需要认识到,一个助记符(如call或jmp)可能有多个值。例如 例如,jmp可以用idaapi.NN_jmp表示跳转,idaapi.NN_jmpfi表示间接远端跳转,或者idaapi.NN_jmpni表示间接近跳。X86和X64指令类型都 以NN开头。为了探索所有1700多种指令类型,我们可以执行[name for name in dir(idaapi) if “NN” in name] 的命令行,或者在IDA的SDK文件allins.hpp中查看它们。一旦我们有了列表中的指令,我们就使用idautils.Functions()的组合和 get_func_attr(ea, FUNCATTR_FLAGS)来获取所有适用的函数,同时忽略库 和thunks。我们通过调用 idautils.FuncItems(ea) 获得函数中的每条指令。在这新引入的函数 idaapi.decode_insn(ins, ea) 被调用。这个函数 接收我们想要解码的指令的地址。一旦它被解码,我们就可以通过访问指令的不同属性。

Python>dir(ins)
['Op1', 'Op2', 'Op3', 'Op4', 'Op5', 'Op6', 'Op7', 'Op8', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get_auxpref__', '__get_operand__', '__get_ops__', '__getattribute__', '__getitem__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__set_auxpref__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__swig_destroy__', '__weakref__', 'add_cref', 'add_dref', 'add_off_drefs', 'assign', 'auxpref', 'auxpref_u16', 'auxpref_u8', '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_64bit', 'is_canon_insn', 'is_macro', 'itype', 'ops', 'segpref', 'size', 'this', 'thisown']
  • 1
  • 2

操作符

idc.get_operand_type(ea,n)获取对应地址的操作符类型

o_void

如果一条指令没有操作符返回0

python>print("0x%x %s" % (ea, idc.generate_disasm_line(ea, 0)))
0xa09166 retn
Python>print(idc.get_operand_type(ea,0))
0
  • 1
  • 2
  • 3
  • 4

o_reg

通用寄存器返回1

Python>print("0x%x %s" % (ea, idc.generate_disasm_line(ea, 0)))
0xa09163 pop edi
Python>print(idc.get_operand_type(ea,0))
1
  • 1
  • 2
  • 3
  • 4

o_mem

如果一个操作数是直接的内存引用,则返回这种类型。这个值在内部表示为2,这个类型对于寻找对DATA的引用很有用。

Python>print("0x%x %s" % (ea, idc.generate_disasm_line(ea, 0)))
0xa05d86 cmp ds:dword_A152B8, 0
Python>print(idc.get_operand_type(ea,0))
2
  • 1
  • 2
  • 3
  • 4

o_phrase

如果操作数包括一个基数寄存器和/或一个索引寄存器,则返回这个操作数。这个值 内部表示为3。

Python>print("0x%x %s" % (ea, idc.generate_disasm_line(ea, 0)))
0x1000b8c2 mov [edi+ecx], eax
Python>print(idc.get_operand_type(ea,0))
3
  • 1
  • 2
  • 3
  • 4

o_displ

如果操作数由寄存器和一个位移值组成,则返回该操作数。该 位移是一个整数值,如0x18。它通常出现在指令访问结构中的值时。结构中的值时常见。在内部,它被表示为一个4的值。

Python>print("0x%x %s" % (ea, idc.generate_disasm_line(ea, 0)))
0xa05dc1 mov eax, [edi+18h]
Python>print(idc.get_operand_type(ea,1))
4
  • 1
  • 2
  • 3
  • 4

o_imm

立即数 5

Python>print("0x%x %s" % (ea, idc.generate_disasm_line(ea, 0)))
0xa05da1 add esp, 0Ch
Python>print(idc.get_operand_type(ea,1))
5
  • 1
  • 2
  • 3
  • 4

o_far

这个操作数在反转x86或x86_64时不常见。它被用来寻找正在访问远端地址的操作数 访问立即远端地址。它在内部被表示为6

o_near

这个操作数在反转x86或x86_64时不常见。它被用来寻找正在访问远端地址的操作数 访问即时近似地址的操作数。它在内部被表示为7

example

有时在反转一个可执行文件的内存转储时,操作数不能被识别为一个 偏移。

seg000:00BC1388 push 0Ch
seg000:00BC138A push 0BC10B8h
seg000:00BC138F push [esp+10h+arg_0]
seg000:00BC1393 call ds:_strnicmp
  • 1
  • 2
  • 3
  • 4

第二个被推送的值是一个内存偏移。如果我们在它上面点击右键并将其改为 数据类型;我们会看到一个字符串的偏移。这样做一次或两次是可以的,但在这之后我们不妨将这一过程自动化。

min = idc.get_inf_attr(INF_MIN_EA)
max = idc.get_inf_attr(INF_MAX_EA)
# for each known function
for func in idautils.Functions():
    flags = idc.get_func_attr(func, FUNCATTR_FLAGS)
    # skip library & thunk functions
    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(curr_addr, 1, 0)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

idc.get_inf_attr(INF_MIN_EA)和idc.get_inf_attr(INF_MAX_EA) 我们循环浏览所有函数和指令。对于每条指令,我们检查操作数类型是否为o_imm并且在内部表示为数字5。o_imm类型是整数或偏移量之类的值。一旦 我们通过调用idc.get_operand_value(ea,n)读取该值。然后,该值被 检查它是否在最小和最大地址的范围内。如果是的话,我们使用 idc.op_plain_offset(ea, n, base)来将操作数转换为偏移量。第一个参数 ea是地址,n是操作数索引,base是基地址。我们的例子只需要 有一个基数为0。

基本块

一个基本块是一个直线代码序列,它没有分支,由一个单一入口点 和一个退出点。在对程序的控制流进行分析时,基本块很有用。IDA的 在使用函数的图形反汇编视图时,通常可以看到基本块的表示。函数的图解视图时,通常会看到基本块的表示。使用基本块进行分析的一些明显的例子是识别循环或控制流混淆。流程的混淆。当一个基本块将控制权转移到另一个块时,下一个块被称为 继承者,而之前的块被称为前者。下面的流程图是一个函数 用单字节XOR来解密一个字符串。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SGo0544p-1666518039768)(C:\Users\86152\Desktop\学习资料\书籍\固件\IDApython\idapython.assets\image-20221017201651493.png)]

ea = 0x0401050
f = idaapi.get_func(ea)
fc = idaapi.FlowChart(f, flags=idaapi.FC_PREDS)
for block in fc:
    print("ID: %i Start: 0x%x End: 0x%x" % (block.id, block.start_ea, 
    block.end_ea)) 
    if block.start_ea <= ea < block.end_ea:
    	print(" Basic Block selected")
    successor = block.succs()
    for addr in successor:
    	print(" Successor: 0x%x" % addr.start_ea)
    pre = block.preds()
    for addr in pre:
    	print(" Predecessor: 0x%x" % addr.end_ea)
    if ida_gdl.is_ret_block(block.type):
    	print(" Return Block")
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

第一条指令将单字节的XOR偏移量分配给变量ea。函数 idaapi.FlowChart(f=None, bounds=None, flags=0)需要一个func_t的类作为第一个参数。作为第一个参数传递。为了得到这个类,我们调用 idaapi.get_func(ea)。该 参数bounds可以是一个元组,第一项是起始地址,第二项是结束地址。是结束地址 bounds=(start, end)。在IDA 7.4中,第三个参数flags必须被设置为 idaapi.FC_PREDS,如果要计算前身。变量fc包含一个 ida_gdl.FlowChart对象,可以通过循环来迭代所有的块。每个区块 包含以下属性。

  • id 每一个函数中每一块都有独立的编号,从0开始
  • type 类型描述基本块
    • fcb_normal 代表正常的块 0表示
    • fcb_indjump 以间接跳转结尾的块,1表示
    • fcb_ret 返回块,2表示。ida_gdl.is_ret_block(block.type)也可以判断该块是否是返回块
    • fcb_cndret 条件返回块 3表示
    • fcb_noret 无返回的块 4表示
    • fcb_enoret 一个没有返回的块,它不属于一个函数,并且有一个内部值为 5
    • fcb_extern是外部正常块,内部值为 6
    • fcb_error是一个通过函数尾端传递执行的块,并且具有内部值为 7
  • start_ea是基本块的起始地址
  • end_ea是基本块的结束地址。基本块的结束地址不是最后一个指令地址,而是它后面的偏移量。
  • preds是返回前驱地址的生成器的一个函数
  • succs 是一个函数,它返回一个包含所有后续地址的生成器。

在idaapi之后。调用FlowChart,迭代每个基本块。id、起始地址和打印结束地址。要定位ea所在的块,将ea比较为大于或通过比较块等于基本块的开始。start_ea,小于block.end_ea获得基本块。变量名块是任意选择的。要获得所有后续偏移量的生成器,我们调用block.succs()。成功中的每个项目生成器循环并打印。要获取前置所有偏移的生成器,我们可以调用block.preds()。preds生成器中的每个项目都会循环并打印。这个最后一个if语句调用ida_gdl.is_ret_block(btype)确定块是否为返回类型。

结构体

在编译过程中,将从代码中删除结构布局、结构名称和类型。重建结构并正确标记成员名称可以极大地帮助逆向过程。下面是x86外壳代码中常见的程序集片段。线程环境块(TEB)和进程环境块(PEB)中的完整代码遍历器结构用于查找kernel32.dll的基址

seg000:00000000 xor ecx, ecx
seg000:00000002 mov eax, fs:[ecx+30h]
seg000:00000006 mov eax, [eax+0Ch]
seg000:00000009 mov eax, [eax+14h]
  • 1
  • 2
  • 3
  • 4

通常观察到的下一步是遍历便携式可执行文件格式来查找Window APIs。这种技术最早是由The Last Stage of Delirium在他们的论文Win32 汇编组件中首次记录了这项技术,时间是2002年。由于所有不同的结构都被解析了,所以很容易迷失方向。除非标明结构的偏移量,否则很容易迷失方向。从下面的代码中可以看出,即使有几个 结构的标签也会有帮助。

seg000:00000000 xor ecx, ecx
seg000:00000002 mov eax, fs:[ecx+_TEB.ProcessEnvironmentBlock]
seg000:00000006 mov eax, [eax+PEB.Ldr]
seg000:00000009 mov eax, [eax+PEB_LDR_DATA.InMemoryOrderModuleList.Flink]
seg000:0000000C mov eax, [eax+ecx]
  • 1
  • 2
  • 3
  • 4
  • 5

对指定偏移添加标签

status = idc.add_default_til("ntapi")
if status:
    idc.import_type(-1, "_TEB")
    idc.import_type(-1, "PEB")
    idc.import_type(-1, "PEB_LDR_DATA")
    ea = 2
    teb_id = idc.get_struc_id("_TEB")
    idc.op_stroff(ea, 1, teb_id, 0)
    ea = idc.next_head(ea)
    peb_ldr_id = idc.get_struc_id("PEB_LDR_DATA")
    idc.op_stroff(ea, 1, peb_ldr_id, 0)
    ea = idc.next_head(ea)
    idc.op_stroff(ea, 1, peb_ldr_id, 0)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

第一行是通过调用 idc.add_default_til(name)加载类型库(TIL)。对于 不熟悉TIL的人,它们是IDA自己的C/C++头文件格式。它们包含 结构、枚举、联合体和其他数据类型的定义。不同的TIL可以通过打开类型库窗口(Type Library Window)(SHIFT+F11)手动探索 idc.add_default_til(name) 返回该库是否能被加载的状态。如果TIL可以被加载,它返回1(真)或 0 (False) 如果库被加载或未被加载。在你的代码中加入这个检查是个好习惯。IDA并不总是识别编译器来导入TIL或忘记我们手动加载TIL。在 TIL被加载后,TIL中的各个定义需要被导入到IDB中。为了导入单个定义,我们调用 idc.import_type(idx, type_name)。第一个参数是 idx,它是类型的索引。每个类型都有一个索引和id。idx为-1,表示该类型 应该被添加到IDA的导入类型列表的末尾。一个类型的索引可以改变,所以依靠索引并不总是可靠的。并不总是可靠的。idx为-1是最常用的参数。上面的三个类型是 在上面的代码中被添加到IDB的三种类型是_TEB、PEB和PEB_LDR_DATA。

变量ea被赋值为2。赋值后,我们通过调用idc.get_struc_id(string_name)获得导入类型的ID。字符串"_TEB "被传递给idc.get_struct_id,返回结构ID为整数。该结构ID被分配给teb_id。为了将成员名称 "ProcessEnvironmentBlock "应用于结构偏移量(0x30),我们可以 使用idc.op_stroff(ea, n, strid, delta)。op_stroff需要四个参数。第一个 参数是包含将被标记的偏移量的指令的地址(ea)。第二个参数n是操作数。在我们的例子中,由于我们要改变标签 mov eax, fs:[ecx+30h]中的0x30,我们需要为第二个操作数传递一个1的值。第三个参数是需要用于将偏移量转换为结构的类型ID。最后一个参数是结构基点和进入结构的指针之间的delta。这个delta值为0。函数idc.op_stroff被用来将结构名称添加到偏移量中。偏移量。然后,代码调用idc.next_head(ea)来获得下一条指令的地址,然后用 然后使用之前描述的相同过程来标记另外两个结构。

除了使用IDA内置的TIL来访问结构外,我们还可以创建自己的结构。在这个例子中,我们将假装IDA没有PEB_LDR_DATA的类型定义。我们没有使用IDA,而是使用Windbg转储类型定义,命令是dt nt!_PEB_LDR_DATA。这个命令的输出可以看到如下。

0:000> dt nt!_PEB_LDR_DATA
ntdll!_PEB_LDR_DATA
 +0x000 Length : Uint4B
 +0x004 Initialized : UChar
 +0x008 SsHandle : Ptr64 Void
 +0x010 InLoadOrderModuleList : _LIST_ENTRY
 +0x020 InMemoryOrderModuleList : _LIST_ENTRY
 +0x030 InInitializationOrderModuleList : _LIST_ENTRY
 +0x040 EntryInProgress : Ptr64 Void
 +0x048 ShutdownInProgress : UChar
 +0x050 ShutdownThreadId : Ptr64 Void
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
sid = idc.get_struc_id("my_peb_ldr_data")
if sid != idc.BADADDR:
	idc.del_struc(sid)//删除已经存在的自定义结构体
sid = idc.add_struc(-1, "my_peb_ldr_data", 0) 
idc.add_struc_member(sid, "length", 0, idc.FF_DWORD, -1, 4)
idc.add_struc_member(sid, "initialized", 4, idc.FF_DWORD, -1, 4)
idc.add_struc_member(sid, "ss_handle", -1, idc.FF_WORD, -1, 2)
idc.add_struc_member(sid, "in_load_order_module_list", -1, idc.FF_DATA, -1, 10) 
idc.add_struc_member(sid, "in_memory_order_module_list", -1, idc.FF_QWORD + 
idc.FF_WORD, -1, 10)
idc.add_struc_member(sid, "in_initialization_order_module_list", -1, idc.FF_QWORD + 
idc.FF_WORD, -1, 10)
idc.add_struc_member(sid, "entry_in_progress", -1, idc.FF_QWORD, -1, 8)
idc.add_struc_member(sid, "shutdown_in_progress", -1, idc.FF_WORD, -1, 2)
idc.add_struc_member(sid, "shutdown_thread_id", -1, idc.FF_QWORD, -1, 8)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

我们代码的第一步,调用idc.get_struc_id(struct_name)返回结构名称的id。如果存在名称为 "my_peb_ldr_data "的结构,idc.get_struct_id返回idc.BADADDR。如果结构ID不是idc.BADADDR,那么我们知道已经存在一个名称为 "my_peb_ldr_data "的结构。对于这个例子,我们通过调用idc.del_struc(sid)来删除该结构。它需要一个结构ID的参数。要创建一个结构,代码会调用idc.add_struc(index, name, is_union)。第一个参数是新结构的索引。与idc.import_type一样,最好的做法是传递一个-1的值,这指定IDA应该使用下一个最大的索引作为id。传递给idc.add_struc的第二个参数是结构名称。is_union的第三个参数是一个bool,定义了新创建的结构是否是一个union。在上面的代码中,我们传递一个0的值来指定它不是一个联合体。结构的成员可以通过调用idc.add_struc_member(sid, name, offset, flag, typeid, nbytes)进行标注。注意。idc.add_struc_member有更多的参数,但由于它们用于更复杂的定义。我们将不涉及它们。如果你对如何创建更复杂的定义感兴趣,我会 我建议你稍后深入研究一下IDAPython的源代码。第一个参数是结构ID 之前分配给变量sid的结构id。第二个参数是一个成员名称的字符串。第三个参数是偏移量。偏移量可以是-1,用于添加到结构的末端,也可以是一个整数,用于指定偏移量。值来指定一个偏移量。第四个参数是标志。标志指定了数据类型(字、浮点。等)。下面可以看到可用的flag数据类型。

FF_BYTE 0x00000000 // byte
FF_WORD 0x10000000 // word
FF_DWORD 0x20000000 // dword
FF_QWORD 0x30000000 // qword
FF_TBYTE 0x40000000 // tbyte
FF_STRLIT 0x50000000 // ASCII ?
FF_STRUCT 0x60000000 // Struct ?
FF_OWORD 0x70000000 // octaword (16 bytes/128 bits)
FF_FLOAT 0x80000000 // float
FF_DOUBLE 0x90000000 // double
FF_PACKREAL 0xA0000000 // packed decimal real
FF_ALIGN 0xB0000000 // alignment directive
FF_CUSTOM 0xD0000000 // custom data type
FF_YWORD 0xE0000000 // ymm word (32 bytes/256 bits)
FF_ZWORD 0xF0000000 // zmm word (64 bytes/512 bits)
FF_DATA 0x400 // data
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

第五个参数是typeid,用于更复杂的定义。对于我们的例子,它的值是-1。 最后一个参数是要分配的字节数(nbyte)。重要的是,flag和nbyte的大小是相等的。如果使用标志为idc.FF_DWORD的dword,必须指定大小为4。如果不是,IDA不会创建这个成员。这可能是一个棘手的错误,因为IDA不会抛出任何警告。可以使用各种标志的组合。例如,在创建 "in_memory_order_module_list "成员时,idc.FF_QWORD + idc.FF_WORD被用来指定大小为10。如果一个idc.FF_DATA的标志被传递,那么任何大小都可以被使用,而不需要结合和增加其他标志。如果我们在IDA结构窗口中查看新创建的结构,我们会看到下面的情况。

00000000 my_peb_ldr_data struc ; (sizeof=0x3A, mappedto_139)
00000000 length dd ?
00000004 initialized dd ?
00000008 ss_handle dw ?
0000000A in_load_order_module_list db 10 dup(?)
00000014 in_memory_order_module_list dt ?
0000001E in_initialization_order_module_list dt ?
00000028 entry_in_progress dq ?
00000030 shutdown_in_progress dw ?
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

枚举类型

对枚举类型的简化描述;是它是一种使用符号常量来表示有意义的名称的方式。枚举类型(又称Enums)在调用系统API时是很常见的。当在Windows上调用CreateFileA时,GENERIC_READ的期望访问被表示为常数0x80000000。不幸的是,这些名称在编译过程中被剥离了。用有意义的名字重新填充常量有助于逆向工程的进行。在逆向工程恶意软件中,经常会看到代表API名称的哈希值的常量。这种技术被用来混淆静态分析中的API调用。下面的代码是该技术的一个例子。

seg000:00000018 push 0CA2BD06Bh ; ROR 13 hash of CreateThread
seg000:0000001D push dword ptr [ebp-4]
seg000:00000020 call lookup_hash
seg000:00000025 push 0
seg000:00000027 push 0
seg000:00000029 push 0
seg000:0000002B push 4C30D0h ; StartAddress
seg000:00000030 push 0
seg000:00000032 push 0
seg000:00000034 call eax ; CreateThread
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

值0xCA2BD06B是 "CreateThread "的哈希值。散列是通过循环每个字符,使用ROR将字节的位数移到13,并存储结果来创建散列的组合。这种技术通常被称为Z0mbie散列或ROR-13。由于哈希值在某种程度上是 "CreateThread "的象征性名称,它是一个关于何时使用枚举的实际例子。

既然我们已经知道哈希值0xCA2BD06B是字符串 “CreateThread”,我们就可以直接创建这个枚举。如果我们不知道这个哈希值代表什么API名称呢?那么我们就需要某种方法来对某个Windows DLL中所有导出的符号名称进行散列。为了简洁起见,我们可以欺骗说这个DLL是kernel32.dll。为了导出kernel32.dll中的符号名称,我们可以使用pefile。请看附录中关于使用pefile的最常见使用情况的简短例子。然后我们需要一种方法来复制散列算法。对于下面的代码,我们将使用Rolf Rolles(见 "What’s Next "部分)对z0mbie hash和pefile的实现9的修改版本。该代码的设计使读者可以很容易地修改它,以匹配任何哈希值或添加所有哈希值。

import pefile
def ror32(val, amt):
	return ((val >> amt) & 0xffffffff) | ((val << (32 - amt)) & 0xffffffff)
def add32(val, amt):
	return (val + amt) & 0xffffffff
def z0mbie_hash(name):
	hash = 0
    for char in name:
    	hash = add32(ror32(hash, 13), ord(char) & 0xff)
    return hash
def get_name_from_hash(file_name, hash):
    pe = pefile.PE(file_name)
    for exp in pe.DIRECTORY_ENTRY_EXPORT.symbols:
        if z0mbie_hash(exp.name) == hash:
            return exp.name
    
api_name = get_name_from_hash("kernel32.dll", 0xCA2BD06B)
if api_name:
    id = idc.add_enum(-1, "z0mbie_hashes", ida_bytes.hex_flag())
    idc.add_enum_member(id, api_name, 0xCA2BD06B, -1)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

第一行将pefile导入IDA。两个函数ror32和add32负责复制ROR指令。函数z0mbie_hash(name)接收一个要被散列的字符串参数并返回散列值。最后一个函数get_name_from_hash(file_path, hash)需要两个参数。第一个参数是要被散列的DLL文件路径。第二个参数是我们要搜索的名称的哈希值。该函数返回字符串名称。这个函数的第一行调用pefile.PE(file_path)来加载和解析kernel32.dll。pefile的PE实例被保存到变量pe中。DLL中的每个符号都是通过循环浏览pe.DIRECTORY_ENTRY_EXPORT.symbols中的每一项来进行的。这个字段包含了DLL中每个导出的符号的名称、地址和其他属性。符号名称通过调用z0mbie_hash(exp.name)进行散列,然后进行比较。如果发生匹配,符号名称将被返回并分配给api_name。在代码的这一点上,创建和添加枚举的工作已经完成。添加枚举的第一步是创建枚举的ID。这是通过调用 idc.add_enum(idx, name, flag) 完成的。第一个参数是idx或新枚举的序列号。值为-1时,会分配给下一个可用的id。第二个参数是枚举的名称。最后一个参数是标志,即ida_bytes.hex_flag()。执行完代码后,如果我们在IDA中高亮显示数值0xCA2BD06B时按下快捷键M,我们会看到字符串 "CreateThread "作为一个符号常量选项。下面的代码就是我们之前看到的代码,哈希值现在是一个符号常量。

seg000:00000015 mov [ebp-4], ebx
seg000:00000018 push CreateThread ; ROR 13 hash of CreateThread
seg000:0000001D push dword ptr [ebp-4]
  • 1
  • 2
  • 3

Xrefs 交叉引用

能够找到数据或代码的交叉引用(又称Xrefs)是一项常见的分析任务。定位Xrefs是很重要的,因为它们提供了某些数据被使用的位置或一个函数被调用的位置。例如,如果我们想找到WriteFile被调用的所有地址。通过使用Xrefs,我们所需要做的就是通过名称定位WriteFile的地址,然后找到所有与之相关的Xrefs。

Python>wf_addr = idc.get_name_ea_simple("WriteFile")
Python>print("0x%x %s" % (wf_addr, idc.generate_disasm_line(wf_addr, 0)))
0x1000e1b8 extrn WriteFile:dword
Python>for addr in idautils.CodeRefsTo(wf_addr, 0):\
	print("0x%x %s" % (addr, idc.generate_disasm_line(addr, 0)))
0x10004932 call ds:WriteFile
0x10005c38 call ds:WriteFile
0x10007458 call ds:WriteFile
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

在第一行,我们通过使用 idc.get_name_ea_simple(str) 得到 API WriteFile 的地址。这个函数返回API的地址。我们打印出 WriteFile 的地址和它的字符串表示。然后通过调用 idautils.CodeRefsTo(ea, flow) 循环浏览所有代码交叉引用。它返回一个可以循环使用的迭代器。ea是我们希望交叉引用的地址。参数flow是一个bool。它用于指定是否遵循正常的代码流。然后显示每个交叉引用的地址。关于idc.get_name_ea_simple(str)的使用,有一点要注意。IDB中所有重命名的函数和API都可以通过调用idautils.Names()访问。这个函数返回一个迭代器对象,可以通过循环来打印或访问这些名称。每个命名的项目是一个(ea, str_name)的元组。

Python>[x for x in Names()]
[(268439552, 'SetEventCreateThread'), (268439615, 'StartAddress'), (268441102, 
'SetSleepClose'),....]
  • 1
  • 2
  • 3

如果我们想获得代码被引用的位置,我们将使用idautisl.CodeRefsFrom(ea,flow)。例如,让我们得到0x10004932被引用的地方的地址

Python>ea = 0x10004932
Python>print("0x%x %s" % (ea, idc.generate_disasm_line(ea, 0)))
0x10004932 call ds:WriteFile
Python>for addr in idautils.CodeRefsFrom(ea, 0):\
 print("0x%x %s" % (addr, idc.generate_disasm_line(addr, 0)))
Python>
0x1000e1b8 extrn WriteFile:dword
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

如果我们回顾idautils.CodeRefsTo(ea, flow)的例子,我们看到地址0x10004932是 idautils.CodeRefsTo(ea, flow)和idautils.CodeRefsFrom(ea, flow)是用来搜索WriteFile的地址。idautils.CodeRefsFrom(ea, flow)被用来搜索代码的交叉引用。A 使用idautils.CodeRefsTo(ea, flow)的一个局限性是,动态导入的API,然后手动重命名的API,并不代表它的名字。然后手动重命名的API,不会显示为代码交叉引用。假设我们手动重命名一个 dword地址更名为 “RtlCompareMemory”,使用idc.set_name(ea, name, SN_CHECK)。

Python>print("0x%x" % (ea)
0xa26c78
Python>idc.set_name(ea, "RtlCompareMemory", SN_CHECK)
True
Python>for addr in idautils.CodeRefsTo(ea, 0):\
	print("0x%x %s" % (addr, idc.generate_disasm_line(addr, 0)))
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

IDA并没有将这些API标记为代码交叉引用。稍后我们将描述一个通用的技术来获取所有的交叉引用。如果我们想搜索数据的交叉引用,我们可以使用 idautils.DataRefsTo(e) 或 idautils.DataRefsFrom(ea)。

Python>print("0x%x %s" % (ea, idc.generate_disasm_line(ea, 0)))
0x1000e3ec db 'vnc32',0
Python>for addr in idautils.DataRefsTo(ea):\
	print("0x%x %s" % (addr, idc.generate_disasm_line(addr, 0)))
0x100038ac push offset aVnc32 ; "vnc32"
  • 1
  • 2
  • 3
  • 4
  • 5

idautils.DataRefsFrom(ea)获取当前指令中包含的偏移地址

idautils.DataRefsTo(ea)获取到当前偏移的指令的地址

idautils.CodeRefsFrom(ea,flow)获取该地址指令引用到的地址

idautils.CodeRefsTo(ea,flow)获取到该地址的指令的地址

要做相反的事情,显示来自的地址,我们调用idautils.DataRefsFrom(ea),传递 地址作为一个参数。这将返回一个所有地址的迭代器,这些地址交叉引用到 数据的迭代器。代码和数据的不同用法会让人有些困惑。让我们来描述一个更通用的 技术。这种方法可以通过调用一个单一的 函数。我们可以使用idautils.XrefsTo(ea, flags=0)获得一个地址的所有引用对象。 并通过调用idautils.XrefsFrom(ea, flags=0)获得一个地址的所有跳转的地址。

Python>print("0x%x %s" % (ea, idc.generate_disasm_line(ea, 0)))
0x1000eee0 unicode 0, <Path>,0
Python>for xref in idautils.XrefsTo(ea, 1):
	print("%i %s 0x%x 0x%x %i" % (xref.type, idautils.XrefTypeName(xref.type), 
xref.frm, xref.to, xref.iscode))
Python>
1 Data_Offset 0x1000ac0d 0x1000eee0 0
Python>>print("0x%x %s" % (xref.frm, idc.generate_disasm_line(xref.frm, 0))
0x1000ac0d push offset KeyName ; "Path"
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

第一行显示我们的地址和一个名为 "Path "的字符串。我们使用idautils.XrefsTo(ea, 1)来获取所有与该字符串的交叉引用。然后我们使用xref.type来打印xrefs类型值。idautils.XrefTypeName(xref.type)用来打印这种类型的字符串表示。有十二个不同的记录的参考类型值。在左边可以看到该值,在下面可以看到其相应的名称。

0 = 'Data_Unknown'
1 = 'Data_Offset'
2 = 'Data_Write'
3 = 'Data_Read'
4 = 'Data_Text'
5 = 'Data_Informational'
16 = 'Code_Far_Call'
17 = 'Code_Near_Call'
18 = 'Code_Far_Jump'
19 = 'Code_Near_Jump'
20 = 'Code_User'
21 = 'Ordinary_Flow'
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

xref.frm打印出从地址,xref.to打印出两个地址。xref.iscodeprints如果xref在一个代码段中。在前面的例子中,我们把idautils.XrefsTo(ea, 1)的标志设置为值1。如果这个标志是0,那么任何交叉引用都会被显示出来。我们可以用下面的程序块来说明这一点。

.text:1000AAF6 jnb short loc_1000AB02 ; XREF
.text:1000AAF8 mov eax, [ebx+0Ch]
.text:1000AAFB mov ecx, [esi]
.text:1000AAFD sub eax, edi
.text:1000AAFF mov [edi+ecx], eax
.text:1000AB02
.text:1000AB02 loc_1000AB02: ; ea is here()
.text:1000AB02 mov byte ptr [ebx], 1

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

我们把光标放在0x1000AB02处。这个地址有一个来自0x1000AAF6的交叉引用,但它也有一个来自0x1000AAF6的交叉引用。也有第二个交叉引用到0x1000AAFF。

Python>print("0x%x %s" % (ea, idc.generate_disasm_line(ea, 0)))
0x1000ab02 mov byte ptr [ebx], 1
Python>for xref in idautils.XrefsTo(ea, 1):
	print("%i %s 0x%x 0x%x %i" % (xref.type, idautils.XrefTypeName(xref.type), 
xref.frm, xref.to, xref.iscode))
Python>
19 Code_Near_Jump 0x1000aaf6 0x1000ab02 1
Python>for xref in idautils.XrefsTo(ea, 0):
	print("%i %s 0x%x 0x%x %i" % (xref.type, idautils.XrefTypeName(xref.type), 
xref.frm, xref.to, xref.iscode))
Python>
21 Ordinary_Flow 0x1000aaff 0x1000ab02 1
19 Code_Near_Jump 0x1000aaf6 0x1000ab02 1

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

第二个交叉引用是从0x1000AAFF到0x1000AB02。交叉引用不一定是 分支指令造成的。它们也可以由正常的普通代码流引起。如果我们把 标志为1,普通代码流引用类型将不会被添加。现在回到我们的RtlCompareMemory 的例子。我们可以使用idautils.XrefsTo(ea, flow)来获得所有交叉引用。

Python>print("0x%x" % ea)
0xa26c78
Python>idc.set_name(ea, "RtlCompareMemory", SN_CHECK)
True
Python>for xref in idautils.XrefsTo(ea, 1):
	print("%i %s 0x%x 0x%x %i" % (xref.type, idautils.XrefTypeName(xref.type), 
xref.frm, xref.to, xref.iscode))
Python>
3 Data_Read 0xa142a3 0xa26c78 0
3 Data_Read 0xa143e8 0xa26c78 0
3 Data_Read 0xa162da 0xa26c78 0
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
Python>print("0x%x %s" % (ea, idc.generate_disasm_line(ea, 0)))
0xa21138 extrn GetProcessHeap:dword
Python> for xref in idautils.XrefsTo(ea, 1):
	print("%i %s 0x%x 0x%x %i" % (xref.type, idautils.XrefTypeName(xref.type), 
xref.frm, xref.to, xref.iscode))
Python>
17 Code_Near_Call 0xa143b0 0xa21138 1
17 Code_Near_Call 0xa1bb1b 0xa21138 1
3 Data_Read 0xa143b0 0xa21138 0
3 Data_Read 0xa1bb1b 0xa21138 0
Python>print(idc.generate_disasm_line(0xa143b0, 0))
call ds:GetProcessHeap
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

verboseness来自于Data_Read和Code_Near都被添加到xrefs中。获取 所有的地址并把它们添加到一个集合中,这对减少所有的地址很有用。

def get_to_xrefs(ea):
    xref_set = set([])
    for xref in idautils.XrefsTo(ea, 1):
    	xref_set.add(xref.frm)
    return xref_set
def get_frm_xrefs(ea):
	xref_set = set([])
	for xref in idautils.XrefsFrom(ea, 1):
		xref_set.add(xref.to)
	return xref_set
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
Python>print("0x%x %s" % (ea, idc.generate_disasm_line(ea, 0)))
0xa21138 extrn GetProcessHeap:dword
Python>get_to_xrefs(ea)
set([10568624, 10599195])
Python>[("0x%x" % x) for x in get_to_xrefs(ea)]
['0xa143b0', '0xa1bb1b']
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

查找

我们已经通过迭代所有已知的函数或指令进行了一些基本搜索。这很有用,但有时我们需要搜索特定的字节,如0x55 0x8B 0xEC。这个字节模式是经典的函数prologue push ebp, mov ebp, esp。 为了搜索字节或二进制模式,我们可以使用ida_search.find_binary(start, end, searchstr, radix, sflag)。 start和end定义了我们想要搜索的范围。 searchstr是我们正在搜索的模式。radix在编写处理器模块时使用。这个话题已经超出了本书的范围。我建议阅读Chris Eagle的The IDA Pro Book中的第19章。现在,radix字段被填充为16的值。sflag是方向或条件。有几种不同类型的标志。名称和数值见下图

SEARCH_UP = 0
SEARCH_DOWN = 1
SEARCH_NEXT = 2
SEARCH_CASE = 4
SEARCH_REGEX = 8
SEARCH_NOBRK = 16
SEARCH_NOSHOW = 32
SEARCH_IDENT = 128
SEARCH_BRK = 256
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

并非所有这些flag都值得讨论,但我们可以谈谈最常用的flag。

  • SEARCH_UP和SEARCH_DOWN用于选择我们希望搜索遵循的方向。
  • SEARCH_NEXT用于获取下一个找到的对象。
  • SEARCH_CASE用于指定大小写敏感性。
  • SEARCH_NOSHOW不显示搜索进度。

IDA以前的版本包含一个SEARCH_UNICODE的sflag来搜索Unicode字符串。在搜索字符时不再需要这个标志,因为IDA在默认情况下同时搜索ASCII和Unicode。让我们快速浏览一下前面提到的寻找函数序幕的字节模式。

pattern = '55 8B EC'
addr = idc.get_inf_attr(INF_MIN_EA)
for x in range(0, 5):
	addr = ida_search.find_binary(addr, idc.BADADDR, pattern, 16,ida_search.SEARCH_DOWN)
	if addr != idc.BADADDR:
 		print("0x%x %s" % (addr, idc.generate_disasm_line(addr, 0)))
Python>
0x401000 push ebp
0x401000 push ebp
0x401000 push ebp
0x401000 push ebp
0x401000 push ebp
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

在第一行我们定义我们的搜索模式。搜索模式可以是以0x开头的十六进制格式,如0x55 0x8B 0xEC或以字节形式出现在IDA的十六进制视图55 8B EC。除非我们使用ida_search.find_text(ea, y, x, searchstr, sflag),否则不能使用格式\x55\x8B\xEC。 idc.get_inf_attr(INF_MIN_EA)被用来获取可执行文件中的第一个地址。然后我们将使用 ida_search.find_binary(start, end, searchstr, radiux, sflag) 的回报分配给一个叫做 addr 的变量。

当搜索时,重要的是验证搜索是否找到了模式。这可以通过比较addr和idc.BADADDR来测试。然后我们打印地址和反汇编。注意到地址并没有增加吗?这是因为我们没有传递SEARCH_NEXT标志。如果没有传递这个标志,当前地址将被用来搜索模式。如果最后一个地址包含了我们的字节模式,那么搜索将永远不会通过它来增量。下面是更正后的版本,在SEARCH_DOWN之前加上SEARCH_NEXT标志。

pattern = '55 8B EC'
addr = idc.get_inf_attr(INF_MIN_EA)
for x in range(0, 5):
	addr = ida_search.find_binary(addr, idc.BADADDR, pattern, 16, 
ida_search.SEARCH_NEXT|ida_search.SEARCH_DOWN)
	if addr != idc.BADADDR:
		print("0x%x %s" % (addr, idc.generate_disasm_line(addr, 0))
Python>
0x401000 push ebp
0x401040 push ebp
0x401070 push ebp
0x4010e0 push ebp
0x401150 push ebp
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

搜索字节模式很有用,但有时我们可能想搜索字符串,如 “chrome.dll”。我们可以用[hex(y) for y in bytearray(“chrome.dll”)]将字符串转换为十六进制字节,但这有点难看。另外,如果字符串是Unicode,我们就必须考虑到这种编码。最简单的方法是使用 ida_search.find_text(ea, y, x, searchstr, sflag)。这些字段大部分看起来都很熟悉,因为它们与 ida_search.find_binary 相同。 ea 是起始地址。y 是 ea 处要搜索的行数,x 是该行的坐标。y和x这两个字段通常分配为0。 searchstr是要搜索的模式,sflag定义了要搜索的方向和类型。作为一个例子,我们可以搜索字符串 "Accept "的所有出现次数。字符串窗口中的任何字符串shift+F12都可以用于这个例子的搜索。

Python>cur_addr = idc.get_inf_attr(INF_MIN_EA)
for x in range(0, 5):
	cur_addr = ida_search.find_text(cur_addr, 0, 0, "Accept",ida_search.SEARCH_DOWN)
	if addr == idc.BADADDR:
		break
	print("0x%x %s" % (cur_addr, idc.generate_disasm_line(cur_addr, 0)))
	cur_addr = idc.next_head(cur_addr)
Python>
0x40da72 push offset aAcceptEncoding; "Accept-Encoding:\n"
0x40face push offset aHttp1_1Accept; " HTTP/1.1\r\nAccept: */* \r\n "
0x40fadf push offset aAcceptLanguage; "Accept-Language: ru \r\n"
...
0x423c00 db 'Accept',0
0x423c14 db 'Accept-Language',0
0x423c24 db 'Accept-Encoding',0
0x423ca4 db 'Accept-Ranges',0
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

我们使用 idc.get_inf_attr(INF_MIN_EA) 来获取最小地址,并将其分配给一个名为 cur_addr 的变量。同样的,对于最大的地址,也是通过调用 idc.get_inf_attr(INF_MAX_EA) 得到的,并将返回的结果分配给一个名为 end 的变量。由于我们不知道有多少个字符串的出现,我们需要检查搜索是否继续向下并且小于最大地址。然后我们将ida_search.find_text的返回值分配给当前地址。由于我们通过调用 idc.next_head(ea) 手动增加地址,我们不需要 SEARCH_NEXT 标志。我们手动将当前地址递增到下一行的原因是一个字符串可以在一行中出现多次。这可能会使获取下一个字符串的地址变得很棘手。除了前面描述的模式搜索外,还有几个函数可以用来查找其他类型。查找API的命名惯例使我们很容易推断其整体功能。在我们讨论寻找不同的类型之前,我们首先要讨论通过地址来识别类型。有一个以 "is "开头的API子集,可以用来确定一个地址的类型。这些API返回一个真或假的布尔值。

idc.is_code(f)

如果ida标记该地址为代码,则返回真

idc.is_data(f)

如果ida标记该地址为数据,返回真

idc.is_tail(f)

如果ida标记改地址为尾部,返回真

idc.is_unknown(f)

如果IDA将该地址标记为未知,则返回True。这种类型用于当IDA还没有 识别地址是代码还是数据时使用该类型。

idc.is_head(f)

如果IDA标记改地址为头部,返回真

这个f对我们来说是新的。我们不是传递地址,而是首先需要得到内部标志的表示,然后将其传递给我们的 idc.is_* 集合的函数。为了得到内部标志,我们使用idc.get_full_flags(ea)。现在我们对函数的使用方法和不同的类型有了基本的了解,让我们来做一个简单的例子。

Python>print("0x%x %s" % (ea, idc.generate_disasm_line(ea, 0)))
0x10001000 push ebp
Python>idc.is_code(idc.get_full_flags(ea))
True
  • 1
  • 2
  • 3
  • 4

ida_search.find_code(ea,flag)

它被用来寻找被标记为代码的下一个地址。如果我们想找到一个数据块的结尾,这就很有用。如果ea是一个已经被标记为代码的地址,它会返回下一个地址。该标志的使用与之前在ida_search.find_text中描述的一样。

Python>print("0x%x %s" % (ea, idc.generate_disasm_line(ea, 0)))
0x4140e8 dd offset dword_4140EC
Python>addr = ida_search.find_code(ea, SEARCH_DOWN|SEARCH_NEXT)
Python>print("0x%x %s" % (addr, idc.generate_disasm_line(addr, 0)))
0x41410c push ebx
  • 1
  • 2
  • 3
  • 4
  • 5

我们可以看到ea是一些数据的地址0x4140e8。我们将返回的 ida_search.find_code(ea, SEARCH_DOWN|SEARCH_NEXT)的返回值分配给addr。然后我们打印addr和 它的反汇编。通过调用这个单一的函数,我们跳过了36个字节的数据,得到了一个标记为代码的部分的开始。

ida_search.find_data(ea,flag)

返回下一个标记为数据块的地址

Python>print("0x%x %s" % (ea, idc.generate_disasm_line(ea, 0)))
0x41410c push ebx
Python>addr = ida_search.find_data(ea, SEARCH_UP|SEARCH_NEXT)
Python>print("0x%x %s" % (addr, idc.generate_disasm_line(addr, 0)))
0x4140ec dd 49540E0Eh, 746E6564h, 4570614Dh, 7972746Eh, 8, 1, 4010BCh
  • 1
  • 2
  • 3
  • 4
  • 5

ida_search.find_unknown(ea,flag)

这个功能是用来查找IDA没有识别为代码或数据的字节的地址。未知的类型需要进一步的人工分析,可以通过视觉或脚本来进行。

Python>print("0x%x %s" % (ea, idc.generate_disasm_line(ea, 0)))
0x406a05 jge short loc_406A3A
Python>addr = ida_search.find_unknown(ea, SEARCH_DOWN)
Python>print("0x%x %s" % (addr, idc.generate_disasm_line(addr, 0))))
0x41b004 db 0DFh ; ?
  • 1
  • 2
  • 3
  • 4
  • 5

ida_search.find_defined(ea,flag)

寻找ida识别为代码或数据的地址

0x41b900 db ? ;
Python>addr = ida_search.find_defined(ea, SEARCH_UP)
Python>print("0x%x %s" % (addr, idc.generate_disasm_line(addr, 0))))
0x41b5f4 dd ?
  • 1
  • 2
  • 3
  • 4

尽管看起来没有任何有用的数据,但我们可以通过查看引用可以看到他确实被用到了

Python>for xref in idautils.XrefsTo(addr, 1):
print("0x%x %s" % (xref.frm, idc.generate_disasm_line(addr, 0))))
Python>
0x4069c3 mov eax, dword_41B5F4[ecx*4]

  • 1
  • 2
  • 3
  • 4
  • 5

ida_search.find_imm(ea,flag,value)

比起搜索一个类型,我们可能想搜索一个特定的值。比如说,我们感觉代码调用rand来生成一个随机数,但我们找不到代码。如果我们知道 rand 使用值 0x343FD 作为种子,我们可以通过 ida_search.find_imm(get_inf_attr(INF_MIN_EA), SEARCH_DOWN, 0x343FD ) 搜索这个数字。

Python>addr = ida_search.find_imm(get_inf_attr(INF_MIN_EA), SEARCH_DOWN, 0x343FD )
Python>addr
[268453092, 0]
Python>print("0x%x %s %x" % (addr[0], idc.generate_disasm_line(addr[0], 0), 
addr[1]))
0x100044e4 imul eax, 343FDh 0
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

在第一行,我们通过 get_inf_attr(INF_MIN_EA) 传递最小地址,向下搜索,然后搜索值 0x343FD。而不是像以前的查找 API 中显示的那样返回一个地址,ida_search.find_imm 返回一个元组。元组中的第一项是地址,第二项是操作数。和 idc.print_operand 的返回一样,第一个操作数从零开始。当我们打印地址和反汇编时,我们可以看到值是第二个操作数。如果我们想搜索一个即时值的所有用途,我们可以做以下的工作。

Python>addr = idc.get_inf_attr(INF_MIN_EA)
while True:
	addr, operand = ida_search.find_imm(addr, SEARCH_DOWN | SEARCH_NEXT, 4)
	if addr == BADADDR:
		break
	print("0x%x %s Operand %i" % (addr,idc.generate_disasm_line(addr, 0), operand))
Python>
0x402434 dd 9, 0FF0Bh, 0Ch, 0FF0Dh, 0Dh, 0FF13h, 13h, 0FF1Bh, 1Bh Operand 0
0x40acee cmp eax, 7Ah Operand 1
0x40b943 push 7Ah Operand 0
0x424a91 cmp eax, 7Ah Operand 1
0x424b3d cmp eax, 7Ah Operand 1
0x425507 cmp eax, 7Ah Operand 1

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

大部分代码看起来很熟悉,但由于我们要搜索多个值,所以使用了一个while循环和SEARCH_DOWN|SEARCH_NEXT标志。在某些情况下,使用ida_search.find_*搜索会有点慢。Yara可以用来加快IDA的搜索速度。请参阅Yara一章,了解在IDA中使用Yara来加速搜索的更多细节。

选择数据

idc.read_selection_start()获取选择部分的起始地址

idc.read_selection_end()获取选择部分的结束地址

.text:00408E46 push ebp
.text:00408E47 mov ebp, esp
.text:00408E49 mov al, byte ptr dword_42A508
.text:00408E4E sub esp, 78h
.text:00408E51 test al, 10h
.text:00408E53 jz short loc_408E78
.text:00408E55 lea eax, [ebp+Data]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
Python>start = idc.read_selection_start()
Python>print("0x%x" % start)
0x408e46
Python>end = idc.read_selection_end()
Python>print("0x%x" % end)
0x408e58
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

需要注意的是结束地址不是最后一条指令的地址,而是最后一条指令下一条指令的地址

如果我们希望只做一个API调用,我们可以使用idaapi.read_selection()。

注释与重命名

在我们看一些例子之前,我们应该先讨论一下注释和重命名的基本知识。 有两种类型的注释。第一种是常规注释,第二种是可重复注释。一个普通的注释出现在地址0x041136B,作为文本普通注释。一个可重复的评论可以在地址0x0411372,0x0411386和0x0411392看到。只有最后一个注释是手动输入的注释。当一条指令引用一个包含可重复注释的地址(如分支条件)时,其他注释会出现。

00411365 mov [ebp+var_214], eax
0041136B cmp [ebp+var_214], 0 ; regular comment
00411372 jnz short loc_411392 ; repeatable comment
00411374 push offset sub_4110E0
00411379 call sub_40D060
0041137E add esp, 4
00411381 movzx edx, al
00411384 test edx, edx
00411386 jz short loc_411392 ; repeatable comment
00411388 mov dword_436B80, 1
00411392
00411392 loc_411392:
00411392
00411392 mov dword_436B88, 1 ; repeatable comment
0041139C push offset sub_4112C0
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

要添加注释,我们使用idc.set_cmt(ea, comment,0),对于可重复的注释,我们使用idc.set_cmt(ea, comment, 1)。ea是地址,注释是我们希望添加的字符串,0表示注释不可重复,1表示注释可重复。下面的代码在每次指令用XOR将一个寄存器或数值清零时都会添加一个注释。

for func in idautils.Functions():
	flags = idc.get_func_attr(func,FUNCATTR_FLAGS)
    if flags&FUNC_LIB or flags&FUNC_THUNK:
        continue
    dism_addr = list(idautils.FuncItems(func))
    for ea in dism_addr:
        if idc.print_insn_mnem(ea) == "xor":
            if idc.print_operand(ea,0) == idc.print_operand(ea,1):
                comment = "%s = 0" % (idc.print_operand(ea,0)) 
                idc.set_cmt(ea,comment,0)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
0040B0F7 xor al, al ; al = 0
0040B0F9 jmp short loc_40B163
  • 1
  • 2

获取地址的注释idc.get_cmt(ea,repeatable) ea是地址,repeatable是bool值表示是否是可重复的注释

Python>print("0x%x %s" % (ea, idc.generate_disasm_line(ea, 0)))
0x40b0f7 xor al, al ; al = 0
Python>idc.get_cmt(ea, False)
al = 0
  • 1
  • 2
  • 3
  • 4

不仅可以对指令添加注释,对函数也可以添加注释

idc.set_func_cmt(ea,cmt,repeatable) idc.get_func_cmt(ea,repeatable) ea可以是函数范围内的任意地址

Python>print("0x%x %s" % (ea, idc.generate_disasm_line(ea, 0)))
0x401040 push ebp
Python>idc.get_func_name(ea)
sub_401040
Python>idc.set_func_cmt(ea, "check out later", 1)
True
----------------------------------------------------------
00401040 ; check out later
00401040 ; Attributes: bp-based frame
00401040
00401040 sub_401040 proc near
00401040 .
00401040 var_4 = dword ptr -4
00401040 arg_0 = dword ptr 8
00401040
00401040 push ebp
00401041 mov ebp, esp
00401043 push ecx
00401044 push 723EB0D5h
----------------------------------------------------------
00401C07 push ecx
00401C08 call sub_401040 ; check out later
00401C0D add esp, 4
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

重命名函数和地址是一项常见的自动化任务,特别是在处理独立位置代码(PIC)、打包器或包装函数时。这在PIC或未打包的代码中很常见,原因是导入表可能不存在于转储中。在包装函数的情况下,完整的函数只是调用一个API。

10005B3E sub_10005B3E proc near
10005B3E
10005B3E dwBytes = dword ptr 8
10005B3E
10005B3E push ebp
10005B3F mov ebp, esp
10005B41 push [ebp+dwBytes] ; dwBytes
10005B44 push 8 ; dwFlags
10005B46 push hHeap ; hHeap
10005B4C call ds:HeapAlloc
10005B52 pop ebp
10005B53 retn
10005B53 sub_10005B3E endp

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

在上面的代码中,这个函数可以被称为 “w_HeapAlloc”。w_是wrapper的缩写。为了 我们可以使用函数idc.set_name(ea, name, SN_CHECK)来重命名一个地址。 地址,name是字符串名称,如 “w_HeapAlloc”。要重命名一个函数,ea需要 是该函数的第一个地址。要重命名我们的HeapAlloc包装器的函数,我们将 使用下面的代码。

Python>print("0x%x %s" % (ea, idc.generate_disasm_line(ea, 0)))
0x10005b3e push ebp
Python>idc.set_name(ea, "w_HeapAlloc", SN_CHECK)
True
---------------------------------------------------------------
10005B3E w_HeapAlloc proc near
10005B3E
10005B3E dwBytes = dword ptr 8
10005B3E
10005B3E push ebp
10005B3F mov ebp, esp
10005B41 push [ebp+dwBytes] ; dwBytes
10005B44 push 8 ; dwFlags
10005B46 push hHeap ; hHeap
10005B4C call ds:HeapAlloc
10005B52 pop ebp
10005B53 retn
10005B53 w_HeapAlloc endp

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

确认重命名成功

Python>idc.get_func_name(ea) 
w_HeapAlloc
  • 1
  • 2

重命名操作符的名称

.text:004047AD lea ecx, [ecx+0]
.text:004047B0 mov eax, dword_41400C
.text:004047B6 mov ecx, [edi+4BCh]
  • 1
  • 2
  • 3

idc.get_operand_value(ea, n) 获取指令的操作符的值或寄存器的类型

Python>print("0x%x %s" % (ea, idc.generate_disasm_line(ea, 0)))
0x4047b0 mov eax, dword_41400C
Python>ea = idc.get_operand_value(ea, 1)
Python>print("0x%x %s" % (ea, idc.generate_disasm_line(ea, 0)))
0x41400c dd 2
Python>idc.set_name(ea, "BETA", SN_CHECK)
True
Python>print("0x%x %s" % (ea, idc.generate_disasm_line(ea, 0)))
0x4047b0 mov eax, BETA[esi]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
import idautils
def rename_wrapper(name, func_addr):
    if idc.set_name(func_addr,name,SN_NOWARN):
        print("Function at 0x%x renamed %s"%(func_addr,idc.get_func_name(func)))
	else:
        print("Rename at 0x%x failed. Function %s is being used"%(func_addr,name))
    return
     
def check_for_wrapper(func):
    flags = idc.get_func_attr(func,FUNCATTR_FLAGS)
    if flags&FUNC_LIB or flags&FUNC_THUNK:
        return
    dism_addr = list(idautils.FuncItems(func))
    func_length = len(dism_addr)
    if func_length>0x20:
        return
    func_call = 0
    instr_cmp = 0
    op = None
    op_addr = None
    op_type = None
    for ea in dism_addr:
        m = idc.print_insn_mnem(ea)
        if m == "call" or m == "jmp":
            temp  = idc.get_operand_value(ea,0)
            if temp in dism_addr:
                continue
            func_call += 1
            if func_call == 2:
                return 
            op_addr = idc.get_operand_value(ea,0)
            op_type = idc.get_operand_type(ea,0)
        elif m == "cmp" or m == "test":
            instr_cmp += 1
            if instr_cmp == 3:
                return
        else:
            continue
    if op_addr == None:
        return 
    name = idc.get_name(op_addr,ida_name.GN_VISIBLE)
    if "[" in name or "$" in name or "?" in name or "@" in name or name == "":
        return
    name = "w_" + name
    if op_type = 7:
        if idc.get_func_attr(op_addr,FUNCATTR_FLAGS) & FUNC_THUNK:
            rename_wrapper(name,func)
            return
        if op_type == 2 or op_type ==6:
            rename_wrapper(name,func)
            return
for func in idautils.Functions():
    check_for_wrapper(func)
    
--------------------output--------------------
Function at 0xa14040 renamed w_HeapFree
Function at 0xa14060 renamed w_HeapAlloc
Function at 0xa14300 renamed w_HeapReAlloc
Rename at 0xa14330 failed. Function w_HeapAlloc is being used.
Rename at 0xa14360 failed. Function w_HeapFree is being used.
Function at 0xa1b040 renamed w_RtlZeroMemory
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61

为了检索标志,我们调用ida_bytes.get_flags(ea)。它需要一个单一的参数,即我们要检索的地址 它需要一个参数,即我们想检索标志的地址。返回的结果是地址标志,然后传递给 idc.hasUserName(flags),以确定该地址是否已被用户重命名。

Python>here()
0x14001ff90
Python>ida_bytes.get_flags(here())
0x51005600
Python>idc.hasUserName(ida_bytes.get_flags(here()))
True
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

着色

给IDA的输出添加一点颜色是加快分析过程的一个简单方法。颜色可以用来在视觉上为指令、块或段添加背景。在浏览大型函数时,很容易错过call指令,从而错过功能。如果我们把所有包含call指令的行都涂上颜色,就会更容易快速识别对子功能的调用。为了改变IDB中显示的颜色,我们使用函数idc.set_color(ea, what, color)。 第一个参数ea是地址。第二个参数是what。它可以是为指令着色的CIC_ITEM,为函数块着色的CIC_FUNC,为段着色的CIC_SEGM。color参数需要一个十六进制颜色代码的整数值。IDA使用BGR(0xBBGGRR)的十六进制颜色代码格式,而不是RGB(0xRRGGBB)。后者的十六进制颜色代码更普遍,因为它被用于HTML、CSS或SVG中。要用十六进制颜色代码0xDFD9F3为一条调用指令着色,我们可以使用以下代码。

for func in idautils.Functions():
    flags = idc.get_func_attr(func, FUNCATTR_FLAGS)
    # skip library & thunk functions
    if flags & FUNC_LIB or flags & FUNC_THUNK:
    	continue
    dism_addr = list(idautils.FuncItems(func))
    for ea in dism_addr:
    	if idc.print_insn_mnem(ea) == "call":
    		idc.set_color(ea, CIC_ITEM ,0xDFD9F3)

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

除了最后一行之外,所有的代码都是以前描述过的。代码在所有函数和所有指令中循环。如果一条指令包含记忆性的call指令,它将改变地址的颜色。最后一行调用函数idc.set_color,将当前地址作为第一个参数。由于我们只对识别一条指令感兴趣,我们将什么参数(第二个)定义为CIC_ITEM。最后一个参数是BGR十六进制编码的颜色代码。如果我们查看一个执行了颜色调用脚本的IBD,下面的0x0401469和0x0401473行的颜色将被改变。

为了检索一个地址的十六进制颜色代码,我们使用函数idc.get_color(ea, what)。第一个参数ea是地址。第二个参数what是我们想获得颜色的项目类型。它使用与前面描述的相同的项目(CIC_ITEM, CIC_FUNC & CIC_SEGM)。 下面的代码得到地址为0x0401469的指令、函数和段的十六进制颜色代码。

Python>"0x%x" % (idc.get_color(0x0401469, CIC_ITEM))
0xdfd9f3
Python>"0x%x" % (idc.get_color(0x0401469, CIC_FUNC))
0xffffffff
Python>"0x%x" % (idc.get_color(0x0401469, CIC_SEGM))
0xffffffff
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

十六进制颜色代码0xffffffff是IDA使用的默认颜色代码。如果你有兴趣 改变IDA的颜色主题,我建议你去看看IDASkins11项目。

获取数据

idc.get_wide_byte(ea)

idc.get_wide_word(ea)

idc.get_wide_dword(ea)

idc.get_qword(ea)

idc.GetFloat(ea)

idc.GetDouble(ea)

idc.get_bytes(ea, size, use_dbg=False)

Python>for byte in idc.get_bytes(ea, 6):
	print("0x%X" % byte),
0x8B 0xD 0xC 0x6D 0xA2 0x0
  • 1
  • 2
  • 3

patching 补丁

idc.patch_byte(ea,value)

idc.patch_word(ea,value)

idc.patch_dword(ea,value)

Python>start = idc.read_selection_start()
Python>end = idc.read_selection_end()
Python>print hex(start)
53
0x1001ed3c
Python>print hex(end)
0x1001ed50
Python>def xor(size, key, buff):
    for index in range(0, size):
        cur_addr = buff + index
        temp = idc.get_wide_byte(cur_addr ) ^ key
        idc.patch_byte(cur_addr, temp)
Python>
Python>xor(end - start, 0x30, start)
Python>idc.get_strlit_contents(start)
WSAEnumNetworkEvents
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

输入输出

当我们不知道文件路径或者不知道用户想把他们的数据保存在哪里时,把文件导入和导出到IDAPython中是很有用的。要导入或保存一个文件的名称,我们使用ida_kernwin.ask_file(forsave, mask, prompt)。forsave的值可以是0,如果我们想打开一个对话框,或1是我们想打开保存对话框。 mask是文件的扩展名或模式。如果我们只想打开.dll文件,我们将使用 "*.dll "的掩码,prompt是窗口的标题。一个输入和输出以及选择数据的好例子是下面的IO_DATA类。

import sys
import idaapi

class IO_DATA():
    def __init__(self):
        self.start = idc.read_selection_start()
        self.end = idc.read_selection_end()
        self.buffer = ''
        self.ogLen = None
        self.status = True
        self.run()
    
    def checkBounds(self):
        if self.end is BADADDR or self.end is BADADDR:
            self.status = False
    
    def getData(self):
        self.ogLen = self.end - self.start
        self.buffer = ''
        try:
            self.buffer = idc.get_bytes(self.start,self.ogLen)
        except:
            self.status = False
        return
    
    def run(self):
        self.checkBounds()
        if self.status == False:
            sys.stdout.write('ERROR:Please select valid data\n')
            return
       	self.getData()
        
        
    def patch(self,temp = None):
        if temp != None:
            self.buffer = temp
            for index,byte in enumerate(self.buffer):
                idc.patch_byte(self.start+index,ord(byte))
                
    def importb(self):
        filename = ida_kernwin.ask_file(0,"*.*",'Import File')
        try:
            self.buffer = open(filename,'rb').read()
        except:
            sys.stdout.write('ERROR: Cannot access file')
    
    def export(self):
        exportFile = ida_kernwin.ask_file(1,"*.*","export buffer")
        f = open(exportFile,'wb')
        f.write(self.buffer)
        f.close()
        
    def stats(self):
        print("start: 0x%x" % self.start)
        print("end: 0x%x" % self.end)
        print("len: 0x%x" % len(self.buffer))
        
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57

有了这个类,数据可以被选择保存到一个缓冲区,然后存储到一个文件。这对IDB中的编码或加密的数据很有用。我们可以使用IO_DATA来选择数据,在Python中对缓冲区进行解码,然后对IDB进行修补。

与其解释每一行代码,不如让读者逐一浏览这些函数,看看它们是如何工作的。下面的要点解释了每个变量和函数的作用。 obj是我们分配给类的任何变量。f是f = IO_DATA()中的obj。

PyQt

本书中记录的与IDAPython的大部分交互是通过命令行进行的。在某些情况下,使用图形用户界面与我们的代码进行交互可能是有用的。在IDAPython的文档中通常被称为Form。IDA的图形用户接口是 写在跨平台的Qt GUI框架中。为了与这个框架交互,我们可以使用 Qt的PyQt绑定,称为PyQt。对PyQt的深入概述超出了本书的范围。 本章所提供的是一个简单的骨架片段,它可以很容易地被修改和建立,用于编写表单。 编写表单。该代码创建了两个小部件,第一个小部件创建了一个表格,第二个 小部件是一个按钮。当按钮被点击时,当前地址和助记符被添加到表格的 行中。如果该行被点击,IDA就会跳到反汇编视图中该行的地址。 把这段代码看作是简单的地址簿标记。下图是将三个地址添加到表格之后的表格 点击 "添加地址 "按钮,然后双击第一行,将三个地址添加到表格中。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NWYRhmp8-1666518039770)(C:\Users\86152\Desktop\学习资料\书籍\固件\IDApython\idapython.assets\image-20221019174915778.png)]

在下面的代码中,并非所有的API都会被涵盖。之所以如此简洁,是因为PyQt的API名称具有描述性。例如,函数setColumnCount可以设置列的数量。如果某个API没有意义,请按名称搜索该API。Qt和PyQt的文档非常完善。一旦我们理解了下面代码的基本原理,就很容易把一些东西黑在一起。在复习下面的代码时,了解PyQt的关键概念是要理解PyQt是一个面向对象的框架。

from idaapi import PluginForm
from PyQt5 import QtCore,QtGui,QtWidgets

class MyPluginFormClass(PluginForm):
	def OnCreate(self,form):
        self.parent = self.FormToPyQtWidget(form)
        self.PopulateForm()
    def PopulateForm(self):
        layout = QtWidgets.QVBoxLayout()
        self.example_row = QtWidgets.QTablewidget()
        column_names = ["Address","Mnemonic"]
        self.example_row.setColumnCount(len(column_names))
        self.example_row.setRowCount(0)
        self.example_row.setHorizontalHeaderLabels(column_name)
        self.example_row.doubleClicked.connect(self.JumpSearch)
        layout.addWidget(self.example_row)
        self.addbtn = QtWidgets.QPushButton("Add Address")
        self.addbtn.clicked.connect(self.AddAddress)
        layout.addWidget(self.addbt)
        self.parent.setLayout(layout)
    def AddAddress(self):
        ea = here()
        index = self.example_row.rowCount()
        self.example_row.setRowCount(index+1)
        h = "0x%x" % ea
        item = QtWidgets.QTableWidgetItem(h)
        item.setFlags(item.flags()^QtCore.Qt.ItemIsEditable)
        self.example_row.setItem(index,0,item)
        self.example_row.setItem(index, 1,QtWidgets.QTableWidgetItem(idc.print_insn_mnem(ea)))
        self.example_row.update()
        
    def JumpSearch(self,item):
        tt = self.example_row.item(item.row(),0)
        ea = int(tt.text(),16)
        idaapi.jumpto(ea)
        
plg = MyPluginFormClass()
plg.Show("Jump Around")
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38

前两行包含导入所需的模块。为了创建一个表单,在idaapi中创建了一个继承于PluginForm的类。在MyPluginFormClass类中有一个名为OnCreate的方法。这个方法在插件表单被创建时被调用。OnClose则相反,当插件被关闭时被调用。self.FormToPyQtWidget(form)函数创建了必要的父实例,用来填充我们的小部件,它被存储在self.parent中。 PopulateForm(self)方法是所有设计和创建部件的地方。这个方法中重要的三个核心步骤是为布局创建一个实例(layout = QtWidgets.QVBoxLayout()),创建(self.example_row = QtWidgets.QTableWidget())并添加小部件(layout.addWidget(self.example_row)),然后设置布局(self.parent.setLayout(layout))。剩下的代码是修改widget或者给widget添加动作。一次这样的动作是在某行被双击时调用方法self.JumpSearch。如果用户双击该行,它会读取第一行,然后调用idaapi.jumpto(ea)来重定向Disassembly视图的地址。当布局被设定后,Form实例中的Show(str)方法被用来显示Form。Show(str)需要一个字符串参数,是表单的标题(例如:“Jump Around”)。

批量生成文件

有时,为一个目录中的所有文件创建IDB或ASM可能是有用的。当分析一组属于同一家族的恶意软件的样本时,这可以帮助节省时间。做批处理文件的生成要比在一个大的集合上手动操作要容易得多。要做批处理分析,我们需要向文本idat.exe传递-B参数。下面的代码可以复制到包含我们想要生成文件的所有文件的目录中。

import os
import subprocess
import glob
paths = glob.glob("*")
ida_path = os.path.join(os.environ['PROGRAMFILES'], "IDA Pro 7.5", "idat.exe")
for file_path in paths:
    if file_path.endswith(".py"):
    	continue
    subprocess.call([ida_path, "-B", file_path])

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

我们使用 glob.glob(" * “) 来获取目录中所有文件的列表。如果我们想只选择某个正则表达式模式或文件类型,可以修改这个参数。如果我们想只获得以 .exe 为扩展名的文件,我们会使用 glob.glob(”*.exe")。os.path.join(os.environ[‘PROGRAMFILES’], “IDA”, “idat.exe”) 用于获得 idat.exe 的路径。某些版本的 IDA 有一个带有版本号的文件夹名称。如果是这种情况,参数 "IDA "需要被修改为文件夹名称。另外,如果我们选择使用非标准的IDA安装位置,整个命令可能都要修改。现在,让我们假设IDA的安装路径是C:\Program Files\IDA。在我们找到路径后,我们循环浏览目录中所有不包含.py扩展名的文件,然后把它们传递给IDA。 对于一个单独的文件,它看起来像C:\Program Files\IDA\idat.exe -B bad_file.exe。 一旦运行,它将为该文件生成一个ASM和IDB。所有的文件都将写在工作目录中。一个输出的例子可以看到如下

C:\injected>dir
0?/**/____ 09:30 AM <DIR> .
0?/**/____ 09:30 AM <DIR> ..
0?/**/____ 10:48 AM 167,936 bad_file.exe
0?/**/____ 09:29 AM 270 batch_analysis.py
0?/**/____ 06:55 PM 104,889 injected.dll
C:\injected>python batch_analysis.py
Thank you for using IDA. Have a nice day!
C:\injected>dir
0?/**/____ 09:30 AM <DIR> .
0?/**/____ 09:30 AM <DIR> ..
0?/**/____ 09:30 AM 506,142 bad_file.asm
0?/**/____ 10:48 AM 167,936 bad_file.exe
0?/**/____ 09:30 AM 1,884,601 bad_file.idb
0?/**/____ 09:29 AM 270 batch_analysis.py
0?/**/____ 09:30 AM 682,602 injected.asm
0?/**/____ 06:55 PM 104,889 injected.dll
0?/**/____ 09:30 AM 1,384,765 injected.idb
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

bad_file.asm, bad_file.idb, injected.asm 和 injected.idb 是生成的文件

执行脚本

import idc
import idaapi
import idautils

idaapi.auto_wait()

count = 0
for func in idautils.Functions():
# Ignore Library Code
	flags = idc.get_func_attr(func, FUNCATTR_FLAGS)
	if flags & FUNC_LIB:
		continue
	for instru in idautils.FuncItems(func):
		count += 1
		
f = open("instru_count.txt", 'w')
print_me = "Instruction Count is %d" % (count)
f.write(print_me)
f.close()

idc.qexit(0)

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

从命令行的角度看,两个最重要的函数是 idaapi.auto_wait() 和 idc.qexit(0)。当IDA打开一个文件时,等待分析的完成是很重要的。这允许IDA填充所有函数、结构或其他基于IDA分析引擎的值。为了等待分析完成,我们调用idaapi.auto_wait()。它将等待/暂停,直到IDA完成其分析。一旦分析完成,它就把控制权返回给脚本。 重要的是,在我们调用任何依赖分析完成的IDAPython函数之前,要在脚本的开头执行这个命令。一旦我们的脚本执行完毕,我们需要调用idc.qexit(0)。这将停止我们脚本的执行,关闭数据库并返回到脚本的调用者。否则,我们的IDB将无法正常关闭

如果我们想执行IDAPython来计算所有的行数,在IDB中我们将执行以下的 命令行。

"C:\Program Files\IDA Pro 7.5\ida.exe" -Scount.py example.idb
  • 1

-S信号,让IDA在IDB打开后运行一个脚本。在工作目录中,我们会看到一个名为instru_count.txt的文件,其中包含所有指令的计数。如果我们想在一个可执行文件上执行我们的脚本,我们需要IDA在自主模式下通过-A来运行。

"C:\Program Files\IDA Pro 7.5\ida.exe" -A -Scount.py example.exe
  • 1

Yara

Yara是一个基于规则的模式匹配软件和库,可用于搜索文件。它是由Victor M. Alvarez编写和维护的。Yara的规则是用基于字符串(“foo”)、字节({66 6f 6f})、文件大小(filesize < 37)或文件的其他条件属性的模式定义的。由于其强大和灵活的规则,Yara被正确地称为 “恶意软件研究人员的模式匹配瑞士刀”。从IDAPython的角度来看,Yara是一个优秀的库,可以添加到你的工具包中,原因有几个。首先,Yara比IDAPython的搜索速度快得多,它的规则可用于自动化分析过程,而且有大量公开可用的Yara签名。我最喜欢的一个自动分析过程的搜索例子是搜索加密函数使用的常数。通过搜索字节模式,我们可以交叉引用匹配,并推断出引用字节的函数与加密算法有关。例如,搜索常数0x67452301可以用来寻找与散列算法MD4、MD5和SHA1有关的函数。

在使用Yara的过程中,第一步是创建规则。Yara规则遵循一个简单的语法,就像C语言一样。一个规则由其name、match pattern(又称Yara文档中的字符串定义)和condition组成。下面的文字是一个简单的Yara规则。它不是一个实用的Yara规则,但它对于演示Yara的规则语法很有用。

/*
Example Yara Rule 
*/ 
rule pe_md5_constant //name
{
	strings:
		$pe_header = "MZ"
		$hex_constant = { 01 23 45 67 } // byte pattern 
 	condition://condition
 		$pe_header at 0 and $hex_constant 
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

前几行是一个多行注释。与C语言和其他语言一样,注释以/* 开始,以*/结束。Yara规则的语法与C语言的结构相似。Yara规则以关键字rule开始,后面是名称(又称规则标识符)。在规则后面是一个开放的大括号{。开头的大括号后面是字符串的定义,它以关键词strings开头,后面是冒号:。字符串定义是用来定义Yara所匹配的规则。每个字符串都有一个标识符,以 字符开头,后面是组成字符串定义名称的字符和数字。字符串定义可以是字符(如 M Z )或十六进制字符串(如 01234567 )。在字符串定义之后是 Y a r a 匹配的条件。条件以关键词 c o n d i t i o n 开始,后面是冒号 : 。在上面的 Y a r a 规则例子中,匹配的条件是:如果字符串定义 字符开头,后面是组成字符串定义名称的字符和数字。字符串定义可以是字符(如MZ)或十六进制字符串(如{ 01 23 45 67 })。在字符串定义之后是Yara匹配的条件。条件以关键词condition开始,后面是冒号:。在上面的Yara规则例子中,匹配的条件是:如果字符串定义 字符开头,后面是组成字符串定义名称的字符和数字。字符串定义可以是字符(如MZ)或十六进制字符串(如01234567)。在字符串定义之后是Yara匹配的条件。条件以关键词condition开始,后面是冒号:。在上面的Yara规则例子中,匹配的条件是:如果字符串定义pe_header位于偏移量0,并且文件包含 h e x c o n s t a n t 中定义的字节模式,那么 Y a r a 就有一个匹配。由于 hex_constant中定义的字节模式,那么Yara就有一个匹配。由于 hexconstant中定义的字节模式,那么Yara就有一个匹配。由于hex_constant没有定义偏移量,所以字节模式只需要出现在文件的任何地方就可以匹配。Yara支持一系列的关键字,可以用来定义宽字符、入口点、大小和其他条件。建议阅读Yara文档中的Writing Yara Rules,以了解所有不同的关键字、它们支持的选项以及文件可以被扫描或匹配的不同方式。

yara安装:pip install yara-python

使用yara的一般步骤:

  1. import yara
  2. rules = yara.compile(source = signature)
  3. data = open(scan_me,“rb”).read()
  4. matches = rules.match(data = self.mem_results)
  5. 输出匹配结果或对其进行操作

当然,这只是对所需步骤的简化描述。Yara包含几个方法和配置,可用于更高级的扫描选项。这些功能的例子有:函数回调、扫描正在运行的进程和大文件的超时。请参阅Yara的文档,了解这些方法和配置的完整列表。在IDA中使用Yara的情况下,同样的步骤也适用于扫描二进制文件。 需要同样的步骤来扫描IDB中的二进制数据。除了需要一个额外的步骤,将Yara匹配文件的偏移量转换为可执行的虚拟地址,因为IDA是这样引用地址的。如果用Yara扫描一个可移植的可执行文件,并且它与文件偏移量0x1000处的模式相匹配,这可以在IDA中表示为虚拟地址0x0401000。下面的代码是一个从IDB读取二进制数据,然后用Yara扫描数据的类。

import yara
import idautils

SEARCH_CASE = 4
SEARCH_REGEX = 8
SEARCH_NOBRK = 16
SEARCH_NOSHOW = 32
SEARCH_UNICODE = 64
SEARCH_IDENT = 128
SEARCH_BRK = 256

class YaraIDASearch:
    def __init__(self):
        self.mem_results = ""
        self.mem_offsets = []
        if not self.mem_results:
            self._get_memory()
            
    def _get_memory(self): //获取文件各个段的数据和长度
        print("Status:Loading memory for Yara.")
        result = b""
        segments_starts = [ea for ea in idautils.Segments()]
        offsets = []
        start_len = 0
        for start in segments_starts:
            end = idc.get_segm_end(start)
            result += idc.get_bytes(start,end-start)
            offsets.append((start,start_len,len(result)))
            start_len = len(result)
        print("Status:Memory has been loaded.")
        self.mem_results = result
        self.mem_offsets = offsets
        
    def _to_virtual_address(self,offset,segments):// fa->va
        va_offset = 0
        for seg in segments:
            if seg[1] <= offset <seg[2]:
                va_offset = seg[0] + (offset - seg[1])
        return va_offset
    
    def _init_sig(self,sig_type,pattern,sflag): //初始化规则
        if SEARCH_REGEX&sflag:
            signature = "/%s/"%pattern
            if SEARCH_CASE & sflag:
                # ida默认大小写不敏感,yara默认敏感
                pass
            else:
                signature += " nocase"
            if SEARCH_UNICODE & sflag:
                signature += " wide"
        elif sig_type == "binary":
            signature = "{ %s }" %s pattern
        elif sig_type == "text" and (SEARCH_REGEX&sflag)==False:
            signature = '"%s"' % pattern
        	if SEARCH_CASE & sflag:
                pass
            else:
                signature += " nocase"
            
            signature += " wide ascii"        
        yara_rule = "rule foo : bar { strings: $a = %s condition: $a }" % signature
        return yara_rule
    
    def _compile_rule(self,signature)://初始化匹配模式
        try:
            rules = yara.compile(source = signature)
        except Exception as e:
            print("ERROR: Cannot compile Yara rule %s" % e)
            return False, None
        return True,rules
    

    def _search(self,signature):
        status,rules=self._compile_rule(signature) //初始化匹配模式
        if not status:
            return False,None
        values=[]
        matches = rules.match(data=self.mem_results) //匹配
        if not matches:
            return False,None
        for rule_match in matches:
            for match in rule_match.strings:
                match_offset = match[0]
                values.append(self._to_virtual_address(match_offset, self.mem_offsets))
                return values
            
            
    def find_binary(self,bin_str,sflag=0):
        yara_sig = self._init_sig("binary",bin_str,sflag)
        offset_matches = self._search(yara_sig)
		return offset_matches    
    
    def fine_text(self, q_str, sflags=0):
        yara_sig = self._init_sig("text", q_str, sflag)
        offset_matches = self._search(yara_sig)
        return offset_matches
    
    def find_sig(self, yara_rule):
        offset_matches = self._search(yara_rule)
        return offset_matches
    
    def reload_scan_memory(self):
        self._get_memory()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103

前面的代码中的所有API都是以前讲过的。函数_to_virtual_address是由Daniel Plohmann创建的(见What’s Next一节),可用于将Yara文件偏移量匹配转换为正确地址内的IDA地址。下面是一个创建YaraIDASearch()实例的例子,该实例扫描一个带有Yara签名的IDB,并返回规则匹配的偏移。应该注意的是,这个规则已经从以前的规则中修改了。IDA并不总是将Portable Executable的MZ头15作为一个段加载。

Python>ys = YaraIDASearch()
Status: Loading memory for Yara.
Status: Memory has been loaded.
Python>example_rule = """rule md5_constant
{
    strings:
    	$hex_constant = { 01 23 45 67 } // byte pattern
    condition:
    	$hex_constant 
}"""
Python>
Python>ys.find_sig(example_rule)
[4199976L]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

第一行创建了一个YaraIDASearch实例并将其分配给ys。Yara规则被保存为一个字符串并分配给变量example_rule。该规则被作为参数传递给 ys.find_sig(yara_rule)方法。搜索方法返回一个Yara规则所匹配的所有偏移量的列表。如果我们想搜索二进制模式,我们可以使用 ys.find_binary(bytes)。ys.find_binary(01 23 45 67)的搜索将返回与自定义Yara规则相同的结果。 YaraIDASearch也支持使用ys.find_text(string)来搜索字符串。

Unicorn Engine

Unicorn引擎是一个轻量级的多平台、多架构的CPU仿真器框架,建立在Qemu的一个修改版本之上。Unicorn是用C语言编写的,但包含许多语言的绑定,包括Python。Unicorn是一个强大的工具,可以帮助逆向工程的进程,因为它基本上允许代码在可配置、可控制和特定的状态下被模拟。后面的形容词specific是Unicorn引擎的强大之处。 执行代码并被告知具体的输出,而不需要真正理解汇编,可以节省时间并有助于分析过程的自动化。例如,使用Unicorn来执行数百个内联字符串解密例程,并使用不同的位移算法和/或XOR密钥,然后将解密的密钥写成字符串作为注释,这只是它的一种用法。

为了很好地理解Unicorn引擎的使用,回顾一些核心概念以及如何使用Unicorn APIs实现它们是很有用的。

初始化unicorn实例

为了初始化Unicorn类,使用了API Uc(UC_ARCH, UC_MODE)。Uc定义了代码应该如何被模拟的具体细节。例如,二进制数据应该作为MIPS-32还是X86-64执行。第一个参数是硬件结构类型。第二个参数是硬件模式类型和/或编码方式。以下是目前支持的架构类型。

  • UC_ARCH_ARM
    • ARM架构 包括Thumb Thumb-2
  • UC_ARCH_ARM64
    • ARM-64 AArch64
  • UC_ARCH_MIPS
  • UC_ARCH_X86
    • 包括x86和x86-64
  • UC_ARCH_PPC
    • PowerPC 架构 现在不支持了
  • UC_ARCH_SPARC
  • UC_ARCH_M68K
  • Endianness
    • UC_MODE_LITTLE_ENDIAN
    • UC_MODE_BIG_ENDIAN
  • ARM
    • UC_MODE_ARM
    • UC_MODE_THUMB
  • MIPS
    • UC_MODE_MIPS32
    • UC_MODE_MIPS64
  • X86/X64
    • UC_MODE_16
    • UC_MODE_32
    • UC_MODE_64
  • SPARC
    • UC_MODE_SPARC32
    • UC_MODE_SPARC64

内存读写

首先要将内存映射

uc.mem_map(address, size, perms=uc.UC_PROT_ALL) 和 uc.mem_map_ptr(address, size, perms,ptr)

  • perms访问权限位
    • UC_PROT_NONE
    • UC_PROT_READ
    • UC_PROT_WRITE
    • UC_PROT_EXEC
    • UC_PROT_ALL

保护内存 uc.mem_protect(address, size, perms=uc.UC_PROT_ALL)

解除映像 uc.mem_unmap(address, size)

写内存 uc.mem_write(address, data)

读内存 uc.mem_read(address, size)

读写寄存器

uc.reg_read(reg_id, opt=None)

uc.reg_write(reg_id, value)

reg_id每个框架不同,对应的id不同

ARM-64 in arm64_const.py

ARM in arm_const.py

M68K in m68k_const.py

MIPS in mips_const.py

SPARC in sparc_const.py

X86 in x86_const.py

要引用这些常量,必须首先将它们导入。对于x86架构常量的导入是通过调用

from unicorn.x86_const import * 
  • 1

开始和结束模拟

uc.emu_start(begin, until, timeout=0, count=0)

为了启动模拟API的Unicorn引擎 uc.emu_start(begin, until, timeout=0, count=0) 被调用。第一个函数start是被仿真的第一个地址。第二个参数until是Unicorn引擎停止仿真的地址(或以上)。参数timeout=用于定义Unicorn引擎执行的毫秒数,直到其超时。 UC_SECOND_SCALE * n可以用来等待n个秒数。最后一个参数count=可以用来定义在Unicorn引擎停止执行前所执行的指令数量。如果count=为零或小于Unicorn Engine的计数,则禁用。要停止对API的仿真,可以使用uc.emu_stop()。

内存和用户自定义的回调hook管理

添加hook uc.hook_add(UC_HOOK_*, callback, user_data, begin, end,…)

前两个参数是强制性的。后面三个是可选的,通常用默认值None、1、0、UC_INS来填充。要删除一个钩子,可以使用API emu.hook_del(hook)。要删除一个钩子,必须将其分配给一个变量。例如,下面的片段是如何删除一个钩子。

i = emu.hook_add(UC_HOOK_CODE, hook_code, None)
emu.hook_del(i)

  • 1
  • 2
  • 3

钩子及其相应的回调允许检测模拟代码。这些回调是我们可以应用逻辑进行分析,修改代码或简单地打印出值的地方。这些回调在调试错误或确保正确初始化时非常有用值。下面的一些例子来自独角兽引擎的示例repo

UC_HOOK_INTR

用来hook所有中断和系统事件 第一个参数 hook_intr是unicorn的实例 第二个参数 intno 中断编号 第三个参数 user_data 传进回调函数的参数

def hook_intr(uc, intno, user_data):
    # only handle Linux syscall
    if intno != 0x80:
        print("got interrupt %x ???" %intno);
        uc.emu_stop()
    return
uc.hook_add(UC_HOOK_INTR, hook_intr)

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
UC_HOOK_INSN

UC_HOOK_INSN在x86指令 IN,OUT SYSCALL的时候执行

def hook_syscall(uc, user_data):
	rax = uc.reg_read(UC_X86_REG_RAX)
	if rax == 0x100:
		uc.reg_write(UC_X86_REG_RAX, 0x200)
		
uc.hook_add(UC_HOOK_INSN, hook_syscall, None,1, 0, UC_X86_INS_SYSCALL)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
UC_HOOK_CODE

可以hook指定的代码,这个hook被执行在所有指定代码执行之前。拥有四个参数

def hook_code(uc, address, size, user_data):
	#uc unicorn的实例
	#address hook的地址
	#size hook的范围
	#user_data 传入的参数
	print("Tracing instruction at 0x%x, instruction size = 0x%x" %(address, size))
uc.hook_add(UC_HOOK_CODE, hook_code)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
UC_HOOK_BLOCK

用来对基本块进行HOOK,参数和UC_HOOK_CODE一样

def hook_block(uc, address, size, user_data):
	print("Tracing basic block at 0x%x, block size = 0x%x" %(address, size)
uc.hook_add(UC_HOOK_BLOCK, hook_block)
  • 1
  • 2
  • 3
UC_HOOK_MEM_*

unicorn有几个专门用于读取,获取,写入和访问内存的钩子。它们都以UC_HOOK_MEM_*开头。他们的回调都具有与下面相同的参数。

def hook_mem_example(uc, access, address, size, value, user_data):
	pass
  • 1
  • 2

uc 是unicorn 的实例,

  • access
    • UC_MEM_READ = 16
    • UC_MEM_WRITE = 17
    • UC_MEM_FETCH = 18
    • UC_MEM_READ_UNMAPPED = 19
    • UC_MEM_WRITE_UNMAPPED = 20
    • UC_MEM_FETCH_UNMAPPED = 21
    • UC_MEM_WRITE_PROT = 22
    • UC_MEM_READ_PROT = 23
    • UC_MEM_FETCH_PROT = 24
    • UC_MEM_READ_AFTER = 25
UC_HOOK_MEM_INVALID

这个hook在非法内存被访问的时候触发

def hook_mem_invalid(uc, access, address, size, value, user_data ):
	eip = uc.reg_read(UC_X86_REG_EIP)
    if access == UC_MEM_WRITE:
        print("Invalid WRITE of 0x%x at 0x%X, data size = %u, data_value = 0x%x" % (address,eip,size,value))
        
    if access == UC_MEM_READ:
        print("invalid READ of 0x%x at 0x%X, data size = %u" % (address, eip, 
size))
    
    if access == UC_MEM_FETCH:
        print("UC_MEM_FETCH of 0x%x at 0x%X, data size = %u" % (address, eip, 
size))
        
    if access == UC_MEM_READ_UNMAPPED:
        print("UC_MEM_READ_UNMAPPED of 0x%x at 0x%X, data size = %u" % (address, 
eip, size))
	
    if access == UC_MEM_WRITE_UNMAPPED:
        print("UC_MEM_WRITE_UNMAPPED of 0x%x at 0x%X, data size = %u" % (address,eip,size))
        
    if access == UC_MEM_FETCH_UNMAPPED:
        print("UC_MEM_FETCH_UNMAPPED of 0x%x at 0x%X, data size = %u" % (address, eip, size))
        
    if access == UC_MEM_WRITE_PROT:
        print("UC_MEM_WRITE_PROT of 0x%x at 0x%X, data size = %u" % (address, 
eip, size))
 	
    if access == UC_MEM_FETCH_PROT:
 		print("UC_MEM_FETCH_PROT of 0x%x at 0x%X, data size = %u" % (address, 
eip, size))
        
 	if access == UC_MEM_FETCH_PROT:
 		print("UC_MEM_FETCH_PROT of 0x%x at 0x%X, data size = %u" % (address, 
eip, size))
        
 	if access == UC_MEM_READ_AFTER:
 		print("UC_MEM_READ_AFTER of 0x%x at 0x%X, data size = %u" % (address, 
eip, size))
        
	return False

uc.hook_add(UC_HOOK_MEM_INVALID, hook_mem_invalid)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
UC_HOOK_MEM_READ_UNMAPPED

读取未映射内存时hook

def hook_mem_read_unmapped(uc, access, address, size, value, user_data):
 	pass
uc.hook_add(UC_HOOK_MEM_READ_UNMAPPED, hook_mem_read_unmapped, None)
  • 1
  • 2
  • 3
UC_HOOK_MEM_WRITE_UNMAPPED

非法内存写事件触发

UC_HOOK_MEM_FETCH_UNMAPPED

非法内存获取事件触发

UC_HOOK_MEM_READ_PROT

内存读保护被触发

UC_HOOK_MEM_WRITE_PROT

内存写保护被触发

UC_HOOK_MEM_FETCH_PROT

内存执行保护被触发

UC_HOOK_MEM_READ

内存读事件触发

UC_HOOK_MEM_WRITE

内存写事件触发

UC_HOOK_MEM_FETCH

内存执行时间触发

UC_HOOK_MEM_READ_AFTER

内存读事件后触发

.text:00401034 push esi
.text:00401035 push edi
.text:00401036 push 0Ah ; Size
.text:00401038 call ds:malloc
.text:0040103E mov esi, eax
.text:00401040 mov edi, offset str_encrypted
.text:00401045 xor eax, eax ; eax = 0
.text:00401047 sub edi, esi
.text:00401049 pop ecx
.text:0040104A
.text:0040104A loop: ; CODE XREF: _main+28↓j
.text:0040104A lea edx, [eax+esi]
.text:0040104D mov cl, [edi+edx]
.text:00401050 xor cl, ds:b_key
.text:00401056 inc eax
.text:00401057 mov [edx], cl
.text:00401059 cmp eax, 9 ; index
.text:0040105C jb short loop
.text:0040105E push esi

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

上面的代码很简单,但它包含了几个细微的差别,在仿真代码时必须加以考虑。第一个问题是在调用malloc的偏移量0x0401038处。独角兽引擎仿真指令就像它是一个CPU处理器一样,但不像它是一个操作系统的仿真器。它不像Window的加载器那样行动。它不会为一个可执行文件初始化内存,以便它能够执行。内存映射、动态链接库的加载或导入表的填充都不是由Unicorn引擎处理的。它可以执行独立于位置的代码,如果它是自成一体的,因此不依赖操作系统填充的内存结构(如进程环境块)。如果这些是成功执行所需要的属性,那么这些属性就需要手动创建和映射,或者通过Hook和Callback手动处理。第二个问题是在偏移量0x0401040处,移动加密字符串的偏移量。字符串的偏移量是在虚拟偏移量0x0402108处。如果可执行文件中的代码被当作没有内存映射的原始数据,那么执行的偏移量将是0x440,但试图读取虚拟地址将返回无效的内存读取,因为内存还没有被映射。当一个可执行文件或IDA的IDB中的可执行数据被执行时,需要映射到正确的地址。最后一个问题是只执行XOR循环而忽略其他代码和异常。下面的代码假设用户从上面的0x401034到0x40105e的汇编中高亮显示。

from unicorn import *
from unicorn.x86_const import *
import idautils
import math

VIRT_MEM = 0x4000

def roundup(x):
	return int(math.ceil(x/1024.0))*1024

def hook_mem_invalid(uc,access,address,size,value,user_data):
    if uc._arch == UC_ARCH_X86:
        eip = uc.reg_read(UC_X86_REG_EIP)
    else:
        eip = uc.reg_read(UC_X86_REG_RIP)
    bb = uc.mem_read(eip,2)
    if bb != b"\xFF\x15":
        return
    if idc.get_name(address) == "malloc":
        uc.mem_map(VIRT_MEM, 8*1024)
    if uc.arch == UC_ARCH_X86:
        uc.reg_write(UC_X86_REG_EAX, VIRT_MEM)
        cur_addr = uc.reg_read(UC_X86_REG_EIP)
        uc.reg_write(UC_X86_REG_EIP, cur_addr + 6)
    else:
        uc.reg_write(UC_X86_REG_RAX, VIRT_MEM)
        cur_addr = uc.reg_read(UC_X86_REG_RIP)
        uc.reg_write(UC_X86_REG_RIP,cur_addr + 6)

def hook_code(uc, addr, size, user_data):    
    print("Tracing instruction at 0x%x, instruction size = 0x%x" % (addr,size))
    
def emulate():
    try:
        segments = []
        for seg in idautils.Segments():
            segments.append((idc.get_segm_start(seg),idc.get_segm_end(seg)))
        
        BASE_ADDRESS = idaapi.get_imagebase()
        
        info = idaapi.get_inf_structure()
        if info.is_64bit(): 
            mu = Uc(UC_ARCH_X86, UC_MODE_64)
        elif info.is_32bit():
            mu = Uc(UC_ARCH_X86, UC_MODE_32)
        
        mu.mem_map(BASE_ADDRESS - 0x1000, 8*1024*1024)
        
        for seg in segments:
            temp_seg = idc.get_bytes(seg[0], seg[1]-seg[0])
            mu.mem_write(seg[0],temp_seg)
        
        stack_size = 1024*1024
        if info.is_64bit():
            stack_base = roundup(seg[1])
            mu.reg_write(UC_X86_REG_RSP, stack_base + stack_size - 0x1000)
        	mu.reg_write(UC_X86_REG_RBP, stack_base + stack_size)
            
        if info.is_32_bit():
            mu.reg_write(UC_X86_REG_ESP, stack_base + stack_size -0x1000)
            mu.reg_write(UC_X86_REG_EBP, stack_base + stack_size)
        
        mu.mem_write(stack_base, b"\x00"*stack_size)
        
        start = idc.read_selection_start()
        end = idc.read_selection_en()
        if start == idc.BADADDR:
            return
        
        mu.hook_add(UC_HOOK_MEM_READ, hook_mem_invalid)
        mu.hook_add(UC_HOOK_CODE, hook_code)
        
        mu.emu_start(start, end)
        decoded = mu.mem_read(VIRT_MEM, 0x0a)
        print(decoded)
        
    except UcError as e:
        print("ERROR: %s" % e)
        return None
    
    return mu

emulate()   
            
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84

从函数emulate开始,它通过调用idautils.Segments()遍历IDB中的所有段,并通过调用idc.get_segm_start(seg)提取段的起始地址,然后通过idc.get_segm_end(seg)获得段的结束地址。 基准地址通过调用idaapi.get_imagebase()来获取。检索到基本地址后,我们调用idaapi.get_inf_structure()来获取idainfo结构的一个实例。 使用存储在info中的idainfo结构,我们调用info.is_64bit()或info.is_32bit()来确定IDB的位。由于这是一个32位的可执行文件,我们调用Uc(UC_ARCH_X86, UC_MODE_32)。这将设置Unicorn实例以32位模式的X86方式执行代码,并将实例存储在变量mu中。通过调用mu.mem_map(BASE_ADDRESS - 0x1000, 8 * 1024 * 1024),在基础地址减去1000后分配8MB的内存。在内存被映射后,就可以对其进行写入。如果试图对没有映射的内存进行写、读或访问,将会发生错误。分段数据被写到它们相应的内存偏移量上。在段被写入后,堆栈内存被分配。通过调用mu.reg_write(UC_X86_REG_ESP, stack_base + stack_size - 0x1000)和mu.reg_write(UC_X86_REG_EBP, stack_base + stack_size)来写入Base Pointer和Stack Pointer寄存器。第一个参数是regid(例如:UC_X86_REG_ESP),第二个值是要写到寄存器的值。堆栈被初始化为空字节(“\0x00”),选择的地址被提取。钩子UC_HOOK_MEM_READ的回调是hook_mem_invalid,钩子UC_HOOK_CODE的回调是hook_code。通过调用mu.emu_start(start, end)来仿真代码,start和end被填充为选定的偏移量。一旦仿真完成,它就会读取XORed字符串并打印出来。

第一个被触发的钩子是UC_HOOK_MEM_READ,当Unicorn试图读取malloc应该被映射到的地址时发生。一旦钩子发生,回调函数hook_mem_invalid被执行。在这个回调函数中,我们通过分配内存,将偏移量写入EAX或RAX,然后返回,编写我们自己的自定义malloc。为了确定是否应该将EAX或RAX写入,我们可以通过比较uc._arch和UC_ARCH_X86来检索存储在Unicorn实例中的架构。另一个选择是将info.is_32bit()的结果作为user_data的一个可选参数传递。通过调用uc.reg_read(reg_id)来读取EIP,参数为UC_X86_REG_EIP。接下来,我们通过调用uc.mem_read(int, size)来读取两个字节,第一个参数是存储在EIP中的偏移量,第二个参数是要读取的数据的大小。这两个字节被比作"\xFF\x15",以确保异常发生在一条调用指令上。如果是调用指令,则通过使用idc.get_name(ea)检索地址的名称,并检查API地址的名称是否为malloc。如果是malloc,则使用uc.mem_map(address, size)对内存进行映射,然后我们将地址写入寄存器EAX uc.reg_write(UC_X86_REG_EAX, VIRT_MEM) 。为了绕过内存异常,我们需要将EIP设置为调用malloc的地址(0x40103E)。为了写到EIP,我们使用uc.reg_write(reg_id, value),值是EIP+6的地址。UC_HOOK_CODE的第二个钩子,用hook_code的回调打印正在被模拟的当前地址和它的大小。下面是在IDA中运行的Unicorn仿真的输出。最后一行包含解密的字符串。

Tracing instruction at 0x401034, instruction size = 0x1
Tracing instruction at 0x401035, instruction size = 0x1
Tracing instruction at 0x401036, instruction size = 0x2
..removed..
Tracing instruction at 0x401059, instruction size = 0x3
Tracing instruction at 0x40105c, instruction size = 0x2
Tracing instruction at 0x40105e, instruction size = 0x1
bytearray(b'test mess\x00')
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

.mem_read(VIRT_MEM, 0x0a)
print(decoded)

except UcError as e:
    print("ERROR: %s" % e)
    return None

return mu
  • 1
  • 2
  • 3
  • 4
  • 5

emulate()


从函数emulate开始,它通过调用idautils.Segments()遍历IDB中的所有段,并通过调用idc.get_segm_start(seg)提取段的起始地址,然后通过idc.get_segm_end(seg)获得段的结束地址。 基准地址通过调用idaapi.get_imagebase()来获取。检索到基本地址后,我们调用idaapi.get_inf_structure()来获取idainfo结构的一个实例。 使用存储在info中的idainfo结构,我们调用info.is_64bit()或info.is_32bit()来确定IDB的位。由于这是一个32位的可执行文件,我们调用Uc(UC_ARCH_X86, UC_MODE_32)。这将设置Unicorn实例以32位模式的X86方式执行代码,并将实例存储在变量mu中。通过调用mu.mem_map(BASE_ADDRESS - 0x1000, 8 * 1024 * 1024),在基础地址减去1000后分配8MB的内存。在内存被映射后,就可以对其进行写入。如果试图对没有映射的内存进行写、读或访问,将会发生错误。分段数据被写到它们相应的内存偏移量上。在段被写入后,堆栈内存被分配。通过调用mu.reg_write(UC_X86_REG_ESP, stack_base + stack_size - 0x1000)和mu.reg_write(UC_X86_REG_EBP, stack_base + stack_size)来写入Base Pointer和Stack Pointer寄存器。第一个参数是regid(例如:UC_X86_REG_ESP),第二个值是要写到寄存器的值。堆栈被初始化为空字节("\0x00"),选择的地址被提取。钩子UC_HOOK_MEM_READ的回调是hook_mem_invalid,钩子UC_HOOK_CODE的回调是hook_code。通过调用mu.emu_start(start, end)来仿真代码,start和end被填充为选定的偏移量。一旦仿真完成,它就会读取XORed字符串并打印出来。

第一个被触发的钩子是UC_HOOK_MEM_READ,当Unicorn试图读取malloc应该被映射到的地址时发生。一旦钩子发生,回调函数hook_mem_invalid被执行。在这个回调函数中,我们通过分配内存,将偏移量写入EAX或RAX,然后返回,编写我们自己的自定义malloc。为了确定是否应该将EAX或RAX写入,我们可以通过比较uc._arch和UC_ARCH_X86来检索存储在Unicorn实例中的架构。另一个选择是将info.is_32bit()的结果作为user_data的一个可选参数传递。通过调用uc.reg_read(reg_id)来读取EIP,参数为UC_X86_REG_EIP。接下来,我们通过调用uc.mem_read(int, size)来读取两个字节,第一个参数是存储在EIP中的偏移量,第二个参数是要读取的数据的大小。这两个字节被比作"\xFF\x15",以确保异常发生在一条调用指令上。如果是调用指令,则通过使用idc.get_name(ea)检索地址的名称,并检查API地址的名称是否为malloc。如果是malloc,则使用uc.mem_map(address, size)对内存进行映射,然后我们将地址写入寄存器EAX uc.reg_write(UC_X86_REG_EAX, VIRT_MEM) 。为了绕过内存异常,我们需要将EIP设置为调用malloc的地址(0x40103E)。为了写到EIP,我们使用uc.reg_write(reg_id, value),值是EIP+6的地址。UC_HOOK_CODE的第二个钩子,用hook_code的回调打印正在被模拟的当前地址和它的大小。下面是在IDA中运行的Unicorn仿真的输出。最后一行包含解密的字符串。

  • 1
  • 2
  • 3
  • 4
  • 5

Tracing instruction at 0x401034, instruction size = 0x1
Tracing instruction at 0x401035, instruction size = 0x1
Tracing instruction at 0x401036, instruction size = 0x2
…removed…
Tracing instruction at 0x401059, instruction size = 0x3
Tracing instruction at 0x40105c, instruction size = 0x2
Tracing instruction at 0x40105e, instruction size = 0x1
bytearray(b’test mess\x00’)


  • 1
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/IT小白/article/detail/550066
推荐阅读
相关标签
  

闽ICP备14008679号