IDAPython 学习

IDAPython 背景

IDAPython 创建于 2004 年。这是Gergely ErdelyiEro Carrera的共同努力。他们的目标 是结合强大的python与自动化分析的IDA的类C脚本语言IDCIDAPython 由三个独立模块 组成。第一个是idc,它是封装IDAIDC函数的兼容性模块。第二个模块是idautils,这是IDA里的一个高级实用函数。第三个模块是idaapi,它允许访问更多低级数据,这些数据能 够被类使用通过IDA

基础

1
.text:00012529 mov esi, [esp+4+arg_0]

text是节的名称,地址是00012529。显示的地址是 16 进制格式。mov这个指令被称作助记符。助记符后面的第一个操作是esi和第二个操作是[esp+4+arg_0]

地址

IDAPython文档中地址作为ea被引用,地址可以通过几个不同的函数进行访问:

获取当前的地址

1
2
3
ea = idc.ScreenEA()
--- 或者 ---
ea = here()

获取最大地址

1
MaxEA()

获取最小地址

1
MinEA()

检测地址是否存在

1
2
3
4
5
6
Python> idaapi.BADADDR
4294967295
Python> hex(idaapi.BADADDR)
0xffffffffL
Python> if BADADDR != here(): print("valid address")
valid address

元素访问

获取段名

1
idc.SegName(ea) # ea是地址

获取反汇编字符串

1
idc.GetDisasm(ea)

获取操作数

1
2
idc.GetOpnd(ea,0) # 获取第一个操作数
idc.GetOpnd(ea,1) # 获取第二个操作数

打印一行作用不大。IDAPython的强大来自于遍历所有的指令,交叉引用地址和搜索代 码或数据。后面两部分将在后面更详细地描述。遍历所有段将是一个不错的开始的位置。

1
2
3
4
5
6
7
8
9
10
11
# 获取所有段名与其开始和结束的地址
Python> for seg in idautils.Segments():
... print(idc.SegName(seg), idc.SegStart(seg), idc.SegEnd(seg))
HEADER 65536 66208
.idata 66208 66636
.text 66636 212000
.data 212000 217088
.edata 217088 217184
INIT 217184 219872
.reloc 219872 225696
GAP 225696 229376

idautils.Segments()返回一个遍历类型对象,我们可以循环这个对象通过使用一个for循 环。列表中的每个项都是段的起始地址。如果我们把它作为作为一个参数去调用idc.SegName(ea),地址可以被用来获取名称。开始和结束的段可以通过调用idc.SegStart(ea)idc.SegEnd(ea)获得。地址或ea需要位于段的开始或结束的范围内。如果我们不想遍历所有段,但想找到下一段我们可以使用idc.NextSeg(ea)。地址可以是段范围内的任何我们希望找到的下一段的地址。如果有机会我们想要通过名称获取一个段的开始地址,我们可以使用idc.SegByName(segname)

函数

既然我们知道如何遍历所有段,我们就应该研究如何遍历所有已知函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 遍历所有函数的地址和函数名
Python> for func in idautils.Functions():
... print(hex(func), idc.GetFunctionName(func))
Python>
0x401000 ?DefWindowProcA@CWnd@@MAEJIIJ@Z
0x401006 ?LoadFrame@CFrameWnd@@UAEHIKPAVCWnd@@PAUCCreateContext@@@Z
0x40100c ??2@YAPAXI@Z
0x401020 save_xored
0x401030 sub_401030
....
0x45c7b9 sub_45C7B9
0x45c7c3 sub_45C7C3
0x45c7cd SEH_44A590
0x45c7e0 unknown_libname_14
0x45c7ea SEH_43EE30

idautils.Functions()将返回一个已知函数列表。这个列表将包含起始地址的每一个函数。 idautils.Functions()可传递的参数范围内搜索。如果我们想要搜索可以通过开始地址和结束地址idautils.Funtions(start_addr, end_addr)。 获得一个函数的名称我们使用idc.GetFunctionName(ea)ea可以是函数边界的任何地址。IDAPython含有大量的API集合提供使用的函数。让我们从一个简单的功能开始。这个函数的语义不重要,但我们应该在心里创建一个地址的记录。

1
2
3
4
5
6
.text:0045C7C3 sub_45C7C3 proc near
.text:0045C7C3 mov eax, [ebp-60h]
.text:0045C7C6 push eax ; void *
.text:0045C7C7 call w_delete
.text:0045C7CC retn
.text:0045C7CC sub_45C7C3 endp

获得边界我们可以使用idaapi.get_func(ea)

1
2
3
4
5
6
7
# 获取当前 ea 地址处函数的地址范围
Python> func = idaapi.get_func(ea)
Python> type(func)
<class 'idaapi.func_t'>
Python> print("Start: 0x%x, End: 0x%x" % (func.startEA,
func.endEA))
Start: 0x45c7c3, End: 0x45c7cd

idaapi.get_func(ea)返回一个类的idaapi.func_t。有时它并不总是显而易见的如何使用一 个类的返回通过一个函数调用。一个有用的命令去查询在Python中的类是dir(class)函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
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', 'extend', 'flags', 'fpd', 'frame', 'frregs',
'frsize', 'intersect', 'is_far', 'llabelqty', 'llabels',
'overlaps', 'owner', 'pntqty', 'points', 'referers', 'refqty',
'regargqty', 'regargs', 'regvarqty', 'regvars', 'size', 'startEA',
'tailqty', 'tails', 'this', 'thisown']

从这个输出我们能明白startEAendEA用来访问函数的开始和结束。这些属性只适用于当前函 数。如果我们想要访问其他函数我们可以使用idc.NextFunction(ea)(下一个函数)和idc.PrevFunction(ea)(上一个函数)。ea的值仅需要在分析的函数的边界的地址里。枚举函数的一个警告是,只有当IDA将代码块标识为函数时,它才起作用。在代码块被标记为一个函数之前,它将在函数枚举过程中跳过。未标记为函数的代码将在图例中标记为红色(顶部的颜色栏)。这些可以手动固定或自动。IDAPython有很多不同的方法来访问相同的数据。访问边界内的一种常见的方法是使用 一个函数idc.GetFunctionAttr(ea, FUNCATTR_START)idc.GetFunctionAttr(ea, FUNCATTR_END)

1
2
3
4
5
6
7
8
9
10
11
12
Python> ea = here()
Python> start = idc.GetFunctionAttr(ea, FUNCATTR_START)
Python> end = idc.GetFunctionAttr(ea, FUNCATTR_END)
Python> cur_addr = start
Python> while cur_addr <= end:
... print(hex(cur_addr), idc.GetDisasm(cur_addr))
... cur_addr = idc.NextHead(cur_addr, end)
Python>
0x45c7c3 mov eax, [ebp-60h]
0x45c7c6 push eax ; void *
0x45c7c7 call w_delete
0x45c7cc retn

idc.GetFunctionAttr(ea, attr)是用来获取开始和结束的函数,然后我们打印当前地址和反 汇编通常使用idc.GetDisasm(ea)。我们使用idc.NextHead(eax)来获取下个指令的开始和继续直到我们到达这个函数的末尾。这种方式的一个缺陷是包含在开始和结束的功能边界。如果 有一个跳转地址高于函数的末尾循环也将过早的退出。这些类型的跳转在混淆技术(如代码转换)中非常常见。由于边界是不可靠的最好的实践是调用 idautils.FuncItems(ea)去循环函 数的每个地址。我们将进入更详细的关于这个方法在下面的部分。类似于idc.GetFunctionAttr(ea, attr)另一个有用的函数收集有关于函数的信息是GetFunctionFlags(ea)。它可以用来检索有关函数的信息,如库代码或函数不返回值。一个函数可能有 9 个标志。如果我们想列举所有的功能,我们可以使用以下代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Python> import idautils
Python> for func in idautils.Functions():
... flags = idc.GetFunctionFlags(func)
... 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")

我们使用idautils.Functions()来获取所有已知函数列表的地址同时我们使用idc.GetFunctionFlags(ea)来获取标志。我们检测值通过使用逻辑&在返回值的时候。例如 检测是否函数没有返回值我们将使用接下来的比较if flags & FUNC_NORET。接下来我们重温所有的标志。这些标志是常见的,其他的是罕见的。

FUNC_NORET

这个标志用来标识一个函数没有执行一个返回指令。它的内部表示等于 1。一个不返回值的函数的例子,如下所示:

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

FUNC_FAR

这个标志很少出现,除非逆向软件使用分段内存。它的内部表示为一个整数 2

FUNC_USERFAR

这个标志比较罕见,具有非常小的文件。hexrays描述标志为“用户已指定远性功能”。它的内部值为 32

FUNC_LIB

此标志用于查找库代码。识别库代码非常有用,因为它是在执行分析时通常可以忽略的代码。它的内部表示为整数 4 。下面是一个例子,它的使用具有识别功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Python> for func in idautils.Functions():
... flags = idc.GetFunctionFlags(func)
... if flags & FUNC_LIB:
... print(hex(func), "FUNC_LIB", GetFunctionName(func))
Python>
0x1a711160 FUNC_LIB _strcpy
0x1a711170 FUNC_LIB _strcat
0x1a711260 FUNC_LIB _memcmp
0x1a711320 FUNC_LIB _memcpy
0x1a711662 FUNC_LIB __onexit
...
0x1a711915 FUNC_LIB _exit
0x1a711926 FUNC_LIB __exit
0x1a711937 FUNC_LIB __cexit
0x1a711946 FUNC_LIB __c_exit
0x1a711955 FUNC_LIB _puts
0x1a7119c0 FUNC_LIB _strcmp

FUNC_STATIC

此标志用于标识作为静态函数编译的函数。在C函数中默认是全局的。如果作者定义了 一个函数为静态只能访问内部文件等功能。在有限的方式下,这可以用来帮助理解源代码是如何构造的

FUNC_FRAME

这个标志表明该函数使用帧指针EBP。使用帧指针的函数通常以设置堆栈框架的标准函数序言开始

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

FUNC_BOTTOMBP

类似FUNC_FRAME此标记用于跟踪帧指针。它将确定帧指针等于堆栈指针函数

FUNC_HIDDEN

函数带FUNC_HIDDEN标志意味着他们是隐藏的将需要扩展到视图。如果我们转到一个被标记为隐藏的函数的地址,它会自动扩展

FUNC_THUNK

这标志标识函数是thunk函数。一个简单的功能是跳到另一个函数

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

应该指出的是,一个函数可以有多个标志

1
2
3
4
0x1a716697 FUNC_LIB
0x1a716697 FUNC_FRAME
0x1a716697 FUNC_HIDDEN
0x1a716697 FUNC_BOTTOMBP

指令

既然我们知道函数如何访问它们的指令,如果我们有一个函数的地址,我们能使用idautils.FuncItems(ea)获取列表中所有地址

1
2
3
4
5
6
7
8
9
10
11
12
# 打印该函数段内的所有反汇编
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(hex(line),idc.GetDisasm(line))
0x45c7c3 mov eax, [ebp-60h]
0x45c7c6 push eax ; void *
0x45c7c7 call w_delete
0x45c7cc retn

idautils.FuncItems(ea)实际返回一个迭代器类型但是被强转成一个list。该列表将包含顺序连续的每个指令的起始地址。现在我们已经有了一个很好的知识库来遍历段、函数和指令,让我们展示一个有用的例子。有时当逆向包代码是唯一知道在哪里发生动态调用的。一个动态的调用将调用或跳转到一个操作数是一个寄存器,例如调用eaxjmp edi

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Python>
for func in idautils.Functions(): # 获取每一个函数的地址
... flags = idc.GetFunctionFlags(func) # 获取函数的标志
... if flags & FUNC_LIB or flags & FUNC_THUNK: # 判断是否为库函数或者是thunk函数
... continue
... dism_addr = list(idautils.FuncItems(func)) # 获取函数的每一个反汇编语句地址
... for line in dism_addr:
... m = idc.GetMnem(line) # 获取反汇编助记符
... if m == 'call' or m == 'jmp':
... op = idc.GetOpType(line, 0) # 获取第一个操作数
... if op == o_reg: # 如果第一个操作数是寄存器
... print("0x%x %s" % (line, idc.GetDisasm(line)))
Python>
0x43ebde call eax ; VirtualProtect

我们调用idautils.Functions()去获取所有已知函数列表。每个函数我们检索函数的标志通过调用idc.GetFunctionFlags(ea)。如果这个函数是库代码或者thunk函数,则这个函数会被跳过。 接下来我们调用idautils.FuncItems(ea)来获取所有的地址在函数里。我们使用for循环循环遍历列表。由于我们只对calljmp指令感兴趣我们需要通过调用idc.GetMnem(ea)。然后我 们用一个简单的字符串比较检查法。如果是一个jumpcall我们通过操作的类型调用 idc.GetOpType(ea, n)。这个函数将返回一个整数是内部调用op_t.type。这个值可以用来确定 如果操作数是一个寄存器、内存引用等。然后我们检查op_t.type是一个寄存器。如果是这 样,我们打印行强制返回的idautils.FuncItems(ea)到一个列表是有用的,因为迭代器没有对 象如len()。通过把它作为一个列表,我们可以很容易地获得函数中的行数或指令数

1
2
3
4
5
6
7
Python> ea = here() # 获取当前地址
Python> len(idautils.FuncItems(ea)) # 返回的是迭代器类型,所以没有 len()
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

在前一个示例中,我们使用了一个包含函数中所有地址的列表。我们循环每条指令并访问下条指令。如果我们只有一个地址,想获得下一个指示?移动到下一条指令的地址可以使用idc.NextHead(ea)和获得前一条指令地址我们使用idc.PrevHead(ea)。这些功能将得到下一 个指令的开始而不是下一个地址,得到下一个地址我们使用idc.NextAddr(ea),得到前一个地址我们使用idc.PrevAddr(ea)

1
2
3
4
5
6
7
8
9
10
11
12
13
Python> ea = here()
Python> print(hex(ea), idc.GetDisasm(ea))
0x10004f24 call sub_10004F32
Python> next_instr = idc.NextHead(ea) # 获取下一条指令的地址,而不是当前的地址 + 1
Python> print(hex(next_instr), idc.GetDisasm(next_instr))
0x10004f29 mov [esi], eax
Python> prev_instr = idc.PrevHead(ea) # 获取上一条指令的地址,而不是当前的地址 - 1
Python> print(hex(prev_instr), idc.GetDisasm(prev_instr))
0x10004f1e mov [esi+98h], eax
Python> print(hex(idc.NextAddr(ea)))
0x10004f25
Python> print(hex(idc.PrevAddr(ea)))
0x10004f23

操作数

操作数的类型是常用的,它将有助于复习所有的类型。正如前面所述,我们可以使用idc.GetOpType(ea,n)得到的操作数的类型。ea是地址,n是索引。这里有 8 种不同类型的操 作数类型

o_void

如果一个指令没有任何操作数它将返回 0

1
2
3
4
Python> print(hex(ea), idc.GetDisasm(ea))
0xa09166 retn
Python> print idc.GetOpType(ea,0)
0

o_reg

如果一个操作数是一个普遍的寄存器将返回此类型。这个值在内部表示为 1

1
2
3
4
Python> print(hex(ea), idc.GetDisasm(ea))
0xa09163 pop edi
Python> print(idc.GetOpType(ea,0))
1

o_mem

如果一个操作数是直接内存引用它将返回这个类型。这个值在内部表示为 2。这种类型是有用的在DATA段查找引用

1
2
3
4
Python> print(hex(ea), idc.GetDisasm(ea))
0xa05d86 cmp ds:dword_A152B8, 0
Python> print(idc.GetOpType(ea,0))
2

o_phrase

这个操作数被返回则这个操作数包含一个基本的寄存器或一个索引寄存器。这个值在内部表示为 3

1
2
3
4
Python> print(hex(ea), idc.GetDisasm(ea))
0x1000b8c2 mov [edi+ecx], eax
Python> print(idc.GetOpType(ea,0))
3

o_displ

这个操作数被返回则操作数包含寄存器和一个位移值,这个为位移值是一个整数,例如0x18。这是常见的当一条指令访问值在一个结构中。在内部,它表示为 4 的值

1
2
3
4
Python>print(hex(ea), idc.GetDisasm(ea))
0xa05dc1 mov eax, [edi+18h]
Python>print(idc.GetOpType(ea,1))
4

o_imm

操作数是这样一个为整数的0xc的值的类型(立即数)。它在内部表示为 5

1
2
3
4
Python>print(hex(ea), idc.GetDisasm(ea))
0xa05da1 add esp, 0Ch
Python>print(idc.GetOpType(ea,1))
5

o_far

这个操作数不是很常见当逆向x86x86_64时。它是用来寻找操作数的访问立即数远地址的。它在内部表示为 6

o_near

这个操作数不是很常见当逆向x86x86_64时。它是用来寻找操作数的访问立即数近地址的。它在内部表示为 7

交叉引用

能够找到交叉引用又名外部参考数据或代码是非常重要的。交叉引用是十分重要的,因为它提供在某些数据被使用或一个函数被调用的位置。例如,如果我们想找到WriteFile被调用的地址。使用交叉引用我们需要做的是确定WriteFile的地址在导入表中然后可以找到所有的交叉引用

1
2
3
4
5
6
7
8
Python> wf_addr = idc.LocByName("WriteFile") # 通过函数名定位对应API地址
Python> print(hex(wf_addr), idc.GetDisasm(wf_addr))
0x1000e1b8 extrn WriteFile:dword
Python> for addr in idautils.CodeRefsTo(wf_addr, 0): # API交叉引用
... print(hex(addr), idc.GetDisasm(addr))
0x10004932 call ds:WriteFile
0x10005c38 call ds:WriteFile
0x10007458 call ds:WriteFile

第一行我们得到了API WriteFile的地址通过使用idc.LocByName(str)。这个函数将返回一个API的地址。我们输出WriteFile的地址是一个字符串表示的。然后遍历所有的交叉引用通过调用idautils.CodeRefsTo(ea, flow)。它能通过遍历将返回一个迭代器。ea是我们想要得到的交叉引用的地址。参数流是一个bool类型。它被用来指定是否要遵照正常的代码流。然后显示每一个交叉引用的地址。一个快速的注释关于使用idc.LocByName(str)。所有的重命名函数和APIs在一个IDB中能被访问通过调用idautils.Names()。这个函数返回一个迭代器对 象能够循环遍历输出或者访问名称。每个名称的项是一个(ea, str_name)的元组

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

如果我们想要获得代码在哪里被引用,我们需要使用idautisl.CodeRefsFrom(ea,flow)。例如让我们获取在0x10004932被引用的地址

1
2
3
4
5
6
7
Python> ea = 0x10004932
Python> print(hex(ea), idc.GetDisasm(ea))
0x10004932 call ds:WriteFile
Python> for addr in idautils.CodeRefsFrom(ea, 0): # API交叉引用
... print(hex(addr), idc.GetDisasm(addr))
Python>
0x1000e1b8 extrn WriteFile:dword

如果我们看idautils.CodeRefsTo(ea, flow)这个例子,我们将看见地址0x10004932是一个到WriteFile的地址。idautils.CodeRefsTo(ea, flow)idautils.CodeRefsFrom(ea, flow)是用来搜索交叉引用和代码。使用idautils.CodeRefsTo(ea, flow)的限制是它是 API,是动态导入的,然后手动重命名将不会显示为代码交叉引用。我们手动重命名一个dword "RtlCompareMemory"地址使用idc.MakeName(ea, name)

1
2
3
4
5
6
Python> hex(ea)
0xa26c78
Python> idc.MakeName(ea, "RtlCompareMemory") # 重命名
True
Python> for addr in idautils.CodeRefsTo(ea, 0):
... print(hex(addr), idc.GetDisasm(addr)) # 没有输出

IDA将不标记这些APIs作为交叉引用。接下来我们将描述一个一般方式获得所有交叉引用。如果我们想要搜索交叉引用从数据中我们能使用idautils.DataRefsTo(ea)或者idautils.DataRefsFrom(ea)

1
2
3
4
5
Python> print(hex(ea), idc.GetDisasm(ea))
0x1000e3ec db 'vnc32',0
Python>for addr in idautils.DataRefsTo(ea):
... print(hex(addr),idc.GetDisasm(addr))
0x100038ac push offset aVnc32 ; "vnc32"

idautils.DataRefsTo(ea)获取参数的地址,并返回所有交叉引用数据的地址的迭代器

1
2
3
4
5
Python>print(hex(ea), idc.GetDisasm(ea))
0x100038ac push offset aVnc32 ; "vnc32"
Python>for addr in idautils.DataRefsFrom(ea): # 数据交叉引用
... print hex(addr),idc.GetDisasm(addr)
0x1000e3ec db 'vnc32',0

对于逆向和显示地址我们调用idautils.DataRefsFrom(ea),通过地址作为参数。他返回一 个一个所有地址交叉引用返回给数据的迭代器。使用不同的代码和数据可能有点混乱。如前所述,让我们描述一个更通用的技术。这种方法可以通过调用单个函数获得所有对地址的交叉引用。我们获得到一个地址的所有交叉引用使用idautils.XrefsTo(ea, flags=0),获得从一个地址到所有交叉引用调用idautils.XrefsFrom(ea, flags=0)

1
2
3
4
5
6
7
8
Python> print(hex(ea), idc.GetDisasm(ea))
0x1000eee0 unicode 0, <Path>,0
Python>for xref in idautils.XrefsTo(ea, 1): # 根据后面的参数决定交叉引用对象
... print(xref.type, idautils.XrefTypeName(xref.type), hex(xref.frm), hex(xref.to), xref.iscode)
Python>
1 Data_Offset 0x1000ac0d 0x1000eee0 0
Python>print(hex(xref.frm), idc.GetDisasm(xref.frm))
0x1000ac0d push offset KeyName ; "Path"

第一行显示我的们的地址和一个字符串命名的。我们使用idautils.XrefsTo(ea, 1)获得的所有交叉引用的字符串。我们然后使用xref.type来打印交叉引用类型值。idautils.XrefTypeName(xref.type)被用来打印这个类型的字符串表示。有十二个不同的文件引用类型的值。该值可以在左边看到,它的对应名字可以被看到,如下所示

1
2
3
4
5
6
7
8
9
10
11
12
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'

xref.frm打印在哪里引用的地址,xref.to打印被引用的地址。xref.iscode打印是否交叉引用是在一个代码段中。在前面的例子中,我们使用idautils.XrefsTo(ea, 1)的标志设置值为 1。如果这个标志为 0 任何交叉引用将不被显示。我们有如下反汇编

1
2
3
4
5
6
7
8
.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

我们在1000AB02处进行标记。这个地址从1000AAF6有一个交叉引用,但是它也有第二个交叉引用

1
2
3
4
5
6
7
8
9
10
11
12
Python>print(hex(ea), idc.GetDisasm(ea))
0x1000ab02 mov byte ptr [ebx], 1
Python>for xref in idautils.XrefsTo(ea, 1):
... print(xref.type, idautils.XrefTypeName(xref.type),hex(xref.frm), hex(xref.to), xref.iscode)
Python>
19 Code_Near_Jump 0x1000aaf6 0x1000ab02 1
Python>for xref in idautils.XrefsTo(ea, 0):
... print(xref.type, idautils.XrefTypeName(xref.type),
hex(xref.frm), hex(xref.to), xref.iscode)
Python>
21 Ordinary_Flow 0x1000aaff 0x1000ab02 1
19 Code_Near_Jump 0x1000aaf6 0x1000ab02 1

第二个交叉引用是从1000AAFF1000AB02。交叉引用不会造成分支指令。他们也可以被普通的代码流影响。如果我们设置标志为1 Ordinary_Flow引用类型将不被增加。回到之前的RtlCompareMemory例子。我们能使用idautils.XrefsTo(ea,flow)来获取交叉引用

1
2
3
4
5
6
7
8
9
10
Python> hex(ea)
0xa26c78
Python> idc.MakeName(ea, "RtlCompareMemory")
True
Python> for xref in idautils.XrefsTo(ea, 1):
... print(xref.type, idautils.XrefTypeName(xref.type), hex(xref.frm), hex(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 hex(ea), idc.GetDisasm(ea)
0xa21138 extrn GetProcessHeap:dword
Python> for xref in idautils.XrefsTo(ea, 1):
... print(xref.type, idautils.XrefTypeName(xref.type), hex(xref.frm), hex(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.GetDisasm(0xa143b0))
call ds:GetProcessHea

冗长的来自Data_readCode_near都添加到交叉引用。让所有的地址并将它们添加到一个集合可以使所有有用的地址被留下

1
2
3
4
5
6
7
8
9
10
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

例如剩下的函数GetProcessHeap的例子

1
2
3
4
5
6
Python> print(hex(ea), idc.GetDisasm(ea))
0xa21138 extrn GetProcessHeap:dword
Python> get_to_xrefs(ea)
set([10568624, 10599195])
Python> [hex(x) for x in get_to_xrefs(ea)]
['0xa143b0', '0xa1bb1b']

搜索

我们已经遍历了一些基本的我们所知道的函数或指令。这是有用的,但是有时我们需要 搜索特殊的字节例如0x55 0x8B 0xEC。这些字节搭配是经典的功能语句push ebpmov ebpesp。为了搜索这些字节或者二进制搭配我们可以使用idc.FindBinary(ea, flag, searchstr, radix=16)ea是我们想要搜索的地址从标志是字典或条件中。这里有一些不同类型的标志。 名称和值如下所示

1
2
3
4
5
6
7
8
9
10
11
SEARCH_UP = 0
SEARCH_DOWN = 1
SEARCH_NEXT = 2
SEARCH_CASE = 4
SEARCH_REGEX = 8
SEARCH_NOBRK = 16
SEARCH_NOSHOW = 32
SEARCH_UNICODE = 64 **
SEARCH_IDENT = 128 **
SEARCH_BRK = 256 **
** 较老的IDA版本可能不支持

并不是所有这些标志都值得掌握,而是一些最常用的标志。

SEARCH_UPSEARCH_DOWN被用来选择字典我们希望我们搜索到接下来的。

SEARCH_NEXT被用来获得下一个找到的对象。

SEARCH_CASE被用于指定大小写敏感度。

SEARCH_NOSHOW将不显示搜索过程。

SEARCH_UNICODE被用来处理所有的Unicode字符串搜索。

我们正在寻找的模式是searchstrradix被使用当正在写入进程模块时。本课题是此文章讨论的范围之外。我将推荐阅读《IDA Pro 权威指南》第 19 章。现在radix字段可以留空。快速浏览一下前面提到的函数语句字节模式

1
2
3
4
5
6
7
8
9
10
11
12
Python> pattern = '55 8B EC'
addr = MinEA()
for x in range(0,5):
... addr = idc.FindBinary(addr, SEARCH_DOWN, pattern);
... if addr != idc.BADADDR:
... print(hex(addr), idc.GetDisasm(addr))
Python>
0x401000 push ebp
0x401000 push ebp
0x401000 push ebp
0x401000 push ebp
0x401000 push ebp

在第一行我们发现我们搜索的部分。被搜索的部分能以 16 进制的形式开始带 0x 作为在0x55 0x8B 0xEC或者作为字节 55 8B EC出现在IDA的 16 进制窗口。\x55\x8B\xEC不能被使用,除非我们使用idc.FindText(ea, flag, y, x, searchstr)MinEA()被用来获得第一个地址在可执行的位置。然后我们将idc.FindBinary(ea, flag, searchstr, radix=16)返回的给一个叫做addr的变量。

在搜索时验证搜索有没有找到匹配部分是十分重要的。它通过对比addridc.BADADDR。然后打印地址和反汇编。

注意到地址没有增加吗?这是因为我们没有设定SEARCH_NEXT 标志。当设立该标志时,如果上一个地址包含我们的字节部分搜索将不在通过。如下所示

1
2
3
4
5
6
7
8
9
10
11
12
Python> pattern = '55 8B EC'
addr = MinEA()
for x in range(0,5):
... addr = idc.FindBinary(addr, SEARCH_DOWN|SEARCH_NEXT,pattern);
... if addr != idc.BADADDR:
... print(hex(addr), idc.GetDisasm(addr))
Python>
0x401040 push ebp
0x401070 push ebp
0x4010e0 push ebp
0x401150 push ebp
0x4011b0 push ebp

搜索字节是十分有帮助的,但是有时候我们也许想要搜索字符串例如“chrome.dll”。我们可以转换字符串到 16 进制字节,使用[hex(y) for y in bytearray("chrome.dll")]但是这有一点丑陋。另外,如果字符串是Unicode,我们必须解释这种格式。最简单的方式是使用FindText(ea, flag, y, x, searchstr)。大多数字节应该看起来相似,因为他们是相同的作为idc.FindBinaryea是开始地址,flag是方向和类型用于搜索。y是要搜索的ea中的行数,x是行中的坐标。这些字段通常分配为 0。现在搜索出现的字符串“Accept”。对于这个例子任何字符串从字符串窗口按shift+F12能使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Python> cur_addr = MinEA()
end = MaxEA()
while cur_addr < end:
... cur_addr = idc.FindText(cur_addr, SEARCH_DOWN, 0, 0,"Accept")
if cur_addr == idc.BADADDR:
... break
else:
... print(hex(cur_addr), idc.GetDisasm(cur_addr))
... cur_addr = idc.NextHead(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

我们使用MinEA()获得最小地址和将其赋值给变量名cur_addr。同样的做法对于最大地址调用MaxEA()和赋值返回给变量名end。由于我们不知道字符串出现的次数,我们需要检查搜索是否继续下去,并且小于最大地址。我们然后赋值idc.FindText的返回值给当前地址。因为我们将手动增加地址通过调用idc.NextHead(ea)。我们不需要SEARCH_NEXT这个标志。理由是我们手动增加当前地址到接下来的行中是因为一个字符串能在一行中出现多次。这便使它很难获得下一个字符串的地址。

除了前面描述的模式搜索外,还有两个函数可用于查找其他类型。根据API的名称可以很容易的推断出函数的整体功能。在讨论寻找不同类型之前,我们首先通过地址来识别类型。有一个API的子集,首先是可以用来确定一个地址类型。这个APIs返回一个TrueFalse的布尔值。

idc.isCode(f)

如果IDA标记地址为代码,返回True

idc.isData(f)

如果IDA标记地址为数据,返回True

idc.isTail(f)

如果IDA标记地址为尾部,返回True

idc.isUnknown(f)

如果IDA标记地址为未知,返回True。这个类型被使用当IDA没有标识地址是代码还是数据。

idc.isHead(f)

如果IDA标记地址为头部,返回True。我们首先需要一个内部的标志来表示,然后通过idc.is设置函数。为获得这个内部标志我们使用idc.GetFlags(ea)。现在我们有了一个基本的功能,如何对不同类型的函数使用,让我们举一个简单的例子。

1
2
3
4
Python> print(hex(ea), idc.GetDisasm(ea))
0x10001000 push ebp
Python> idc.isCode(idc.GetFlags(ea))
True

idc.FindCode(ea, flag)

它被用来找到下一个被标记为代码的地址。如果我们想找到一个数据块的结束,这将十分有用。如果ea是一个已经作为代码被标记的地址它将返回下一个地址。这个flag将被用来提前描述idc.FindText

1
2
3
4
5
Python> print(hex(ea), idc.GetDisasm(ea))
0x4140e8 dd offset dword_4140EC
Python> addr = idc.FindCode(ea, SEARCH_DOWN|SEARCH_NEXT)
Python> print(hex(addr), idc.GetDisasm(addr))
0x41410c push ebx

我们能了解ea是地址0x4140e8的一些数据。我们赋值idc.FindCode(ea, SEARCH_DOWN|SEARCH_NEXT)的返回值给addr。然后我们打印addr和它的汇编。通过调用一个函数,我们跳过开始标记为代码 36 字节数据的部分。

idc.FindData(ea, flag)

这的使用方式和idc.findcode完全一样,除了它将返回下一个地址被标记为一个数据块的开始

1
2
3
4
5
6
Python> print(hex(ea), idc.GetDisasm(ea))
0x41410c push ebx
Python> addr = idc.FindData(ea, SEARCH_UP|SEARCH_NEXT)
Python> print(hex(addr), idc.GetDisasm(addr))
0x4140ec dd 49540E0Eh, 746E6564h, 4570614Dh, 7972746Eh, 8, 1,
4010BCh

只有一点比前面的例子稍有不同的是SEARCH_UP|SEARCH_NEXT的方向和搜索的数据

idc.FindUnexplored(ea, flag)

这个函数是用来寻找的字节地址,IDA没有识别码或数据。unknown类型将需要进一步的人工分析或通过脚本观察。

1
2
3
4
5
Python> print(hex(ea), idc.GetDisasm(ea))
0x406a05 jge short loc_406A3A
Python> addr = idc.FindUnexplored(ea, SEARCH_DOWN)
Python> print(hex(addr), idc.GetDisasm(addr))
0x41b004 db 0DFh ; ?

idc.FindExplored(ea, flag)

它是用来寻找一个地址,IDA 确定为代码和数据。

1
2
3
4
0x41b900 db ? ;
Python> addr = idc.FindExplored(ea, SEARCH_UP)
Python> print(hex(addr), idc.GetDisasm(addr))
0x41b5f4 dd ?

这可能看上去不是真实的值,但如果我们要打印的addr的交叉引用,我们会看到它被使用

1
2
3
4
Python> for xref in idautils.XrefsTo(addr, 1):
... print(hex(xref.frm), idc.GetDisasm(xref.frm))
Python>
0x4069c3 mov eax, dword_41B5F4[ecx*4]

idc.FindImmediate(ea, flag, value)

并不是寻找一种我们可能要搜索的一个特定的值。比方说,我们猜测代码调用rand来生成一个随机数,但是我们找不到代码。如果我们知道rand使用值0x343fd作为种子,则我们可以寻找一些数。

1
2
3
4
5
Python> addr = idc.FindImmediate(MinEA(), SEARCH_DOWN, 0x343FD )
Python> addr
[268453092, 0]
Python> print("0x%x %s %x" % (addr[0], idc.GetDisasm(addr[0]), addr[1]))
0x100044e4 imul eax, 343FDh 0

在第一行我们获取最小地址通过MinEA(),接下来,然后搜索值0x343fd。而不是返回一个地址来显示找到的APIs idc.FindImmediate返回一个tuppletupple的第一项将成为地址第二项将成为操作数。类似于idc.GetOpnd第一个操作数开始返回零。当我们打印的地址和反汇编,我们可以看到的值是第二个操作数。如果我们想搜索所有使用立即数我们可以做如下内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Python>addr = MinEA()
while True:
... addr, operand = idc.FindImmediate(addr, SEARCH_DOWN|SEARCH_NEXT, 0x7a)
... if addr != BADADDR:
... print(hex(addr), idc.GetDisasm(addr), "Operand ", operand)
else:
... break
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

大部分的代码应该看起来很熟悉,但因为我们是搜索多个值我们将使用一个while循环和SEARCH_DOWN|SEARCH_NEXT标志。


IDAPython 学习
https://equinox-shame.github.io/2022/07/07/IDAPython 学习/
作者
梓曰
发布于
2022年7月7日
许可协议