IDA Python 使用总结

sky123

环境配置

切换 python 版本

管理员权限运行 IDA 安装目录下的 idapyswitch.exe ,选择使用的 python 解释器。

安装 IPyIDA 插件

IPyIDA 是一个仅依赖 Python 的解决方案,用于将 IPython 控制台添加到 IDA Pro 中。通过使用 <Shift+>,你可以打开一个内嵌 Qt 控制台的窗口。这样你就可以享受到 IPython 的自动补全、在线帮助、等宽字体输入框、图表等功能。

IPyIDA 提供了一个安装脚本,可以自动从 IDA 控制台安装 IPyIDA 及其依赖项。只需将以下代码复制到 IDA 控制台中即可完成 IPyIDA 的安装:

  • python2:

    1
    import urllib2; exec urllib2.urlopen('https://github.com/eset/ipyida/raw/stable/install_from_ida.py').read()
  • python3:

    1
    import urllib.request; exec(urllib.request.urlopen('https://github.com/eset/ipyida/raw/stable/install_from_ida.py').read())

脚本将执行以下操作:

  1. 如果尚未安装 pip,将安装 pip
  2. 从 PyPi 安装 ipyida 包。
  3. ipyida_plugin_stub.py 复制到用户的插件目录。
  4. 加载 IPyIDA 插件。

另外 IPyIDA 也可以手动安装,用户只需要将 ipyida_plugin_stub.pyipyida 目录复制到 IDA 的插件目录中即可。

手动安装需要用户自己管理依赖项和更新。IPyIDA 需要 ipykernelqtconsole 包,并且如果使用 ipykernel 版本 5 或更高版本,还需要 qasync 包。

1
2
pip install ipykernel qtconsole
pip install qasync

pycharm 自动补全

在 PyCharm 的设置→项目→Python 解释器点击设置选择全部显示...

image-20250819020849977
点击显示解释器路径图标,然后在弹出的解释器路径对话框中添加 IdaPython 的 python 库路径。
image-20250819020905937
旧版本的 IDA 的 python 库有 3 和 2 的区分,分别表示 Python3 和 Python2 对应的 python 库。

添加完 IdaPython 的代码库之后就可以使用 PyCharm 的自动补全功能了。
image-20250819020938082

基本使用

段相关

  • idc.get_segm_name(addr):获取地址 addr 所在段的名字(参数为当前的地址)。
  • idc.get_segm_start(addr):获取地址 addr 所在段的开始地址
  • idc.get_segm_end(addr):获取地址 addr 所在段的结束地址
  • idc.get_first_seg():获取第一个段的地址
  • idc.get_next_seg(addr):获取地址大于 addr 的第一个段的起始地址
  • idautil.Segments():返回一个列表记录所有段的地址

例如遍历所有的段:

1
2
3
4
5
6
7
8
9
import idc
import idaapi
import idautils

for seg in idautils.Segments():
segname = idc.get_segm_name(seg)
segstart = idc.get_segm_start(seg)
segend = idc.get_segm_end(seg)
print("段名:" + segname + " 起始地址:" + hex(segstart) + " 结束地址:" + hex(segend))

地址相关

交叉引用

  • idautils.CodeRefsTo(ea, flow):获取引用 ea 地址处的代码的地址。
    • 参数
      • ea:被引用的代码的地址
      • flow:表示代码顺序执行的是否计算在内(布尔值,0/1 或 False/True),比如如果 flow = True 那么认为当前指令的上一条指令引用了当前指令。
    • 返回值 :代码引用列表(可能是空列表)
  • idautils.CodeRefsFrom(ea, flow)ea 地址处的代码引用了何处的代码。
    • 参数
      • ea:查询的代码的地址
      • flow:表示代码顺序执行的是否计算在内(布尔值,0/1 或 False/True),比如如果 flow = True 那么认为当前指令的下一条指令被当前指令引用。
    • 返回值 :代码引用列表(可能是空列表)
  • idautils.DataRefsTo(ea):获取引用 ea 地址处的内容的地址。
  • idautils.DataRefsFrom(ea)ea 地址处的数据引用了何处的数据。

符号地址

  • idc.get_name_ea_simple(name):获取名称对应的地址,如果获取不到则返回 ida_idaapi.BADADDR

  • 获取符号在 got 表中的地址:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    def get_got_addr(symbol):
    # 获取符号的地址
    symbol_addr = idc.get_name_ea_simple(symbol)

    # 如果该地址不在 'extern' 段中,尝试获取对应的 __imp_ 符号地址
    if idc.get_segm_name(symbol_addr) != 'extern':
    symbol_addr = idc.get_name_ea_simple(f'__imp_{symbol}')

    # 确保符号地址在 'extern' 段中
    assert idc.get_segm_name(symbol_addr) == 'extern'

    # 查找所有引用该符号地址的代码或数据引用
    for ref_addr in (list(idautils.CodeRefsTo(symbol_addr, False)) + list(idautils.DataRefsTo(symbol_addr))):
    # 如果引用地址位于 .got 或 .got.plt 段中,返回该引用地址
    if idc.get_segm_name(ref_addr) in ['.got', '.got.plt']:
    return ref_addr
  • 获取符号在 plt 表中的地址:

    1
    2
    3
    4
    5
    6
    def get_plt_addr(symbol):
    # 获取符号的 GOT 地址
    for ref_addr in (list(idautils.CodeRefsTo(get_got_addr(symbol), False)) + list(idautils.DataRefsTo(get_got_addr(symbol)))):
    # 如果引用地址位于 .plt 或 .plt.got 段中,返回该函数的起始地址
    if idc.get_segm_name(ref_addr) in ['.plt', '.plt.got']:
    return idaapi.get_func(ref_addr).start_ea

数据相关

数据读写

  • ida_bytes.get_wide_byte(addr):以 1 字节为单位获取地址处的值。

  • ida_bytes.patch_byte(addr, value):以 1 字节为单位修改地址处的值。

  • ida_bytes.get_wide_word(addr):以 2 字节为的单位获取地址处的值。

  • ida_bytes.patch_word(addr, value):以 2 字节为的单位修改地址处的值。

  • ida_bytes.get_wide_dword(addr):以 4 字节的单位获取地址处的值。

  • ida_bytes.patch_dword(addr, value):以 4 字节为的单位修改地址处的值。

  • ida_bytes.get_qword(addr):以 8 字节的单位获取地址处的值。

  • ida_bytes.patch_qword(addr, value):以 8 字节为的单位修改地址处的值。

  • idc.get_bytes(addr, len):获取 addr 地址处 len 长度的数据。

  • idc.patch_bytes(addr, data):在 addr 地址处写入 data(bytes 类型数据)。

数据类型

  • idc.get_item_size(addr):获取 addr 地址处的数据大小,例如汇编指令长度。
  • idc.del_items(addr):去除目标地址处数据的属性。
  • idc.set_name(ea, name, flags=0): 为指定地址的对象设置或删除名称,并将名称添加到名称列表中。对象可以是任何类型(如指令、函数、数据字节、字符串、结构体等)。其中参数 name 如果是空字符串则表示删除名称。
  • idc.create_insn(addr):将目标地址处的数据设置为代码。有可能会失败,可以与 ida_name.set_name(addr, '') 配合来避免失败。

数据查找

IdaPython 没有什么好用的数据查找的 API,因此通常搜索数据都是先将数据读出来然后手动搜索。

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
import idaapi
import idautils
import ida_bytes

def find_bytes_in_segment(segment_name, byte_sequence):
# 将目标字节序列转换为字节对象
target_bytes = bytearray(byte_sequence)

# 获取指定段的起始地址和结束地址
for seg in idautils.Segments():
if idaapi.get_segm_name(seg) == segment_name:
start_addr = idaapi.get_segm_start(seg)
end_addr = idaapi.get_segm_end(seg)
break
else:
print(f"Segment {segment_name} not found")
return []

# 读取整个段的字节
segment_bytes = ida_bytes.get_bytes(start_addr, end_addr - start_addr)

# 使用 find() 查找目标字节序列
results = []
pos = segment_bytes.find(target_bytes)
while pos != -1:
results.append(start_addr + pos)
pos = segment_bytes.find(target_bytes, pos + 1)

return results

# 示例:在 .text 段中查找字节序列
byte_sequence = b'\x55\x48\x89\xe5' # 比如这是一段x86_64的指令
matches = find_bytes_in_segment('.text', byte_sequence)

# 输出匹配的地址
for match in matches:
print(f"Found at address: {hex(match)}")

字符串相关

在 IdaPython 脚本中,字符串起到了非常关键的作用。例如通过字符串定位关键代码,或者通过字符串恢复函数符号等。因此这里单独介绍一下 IdaPython 中字符串相关的功能。

字符串提取

字符串获取主要有 idautils.Stringsidc.get_strlit_contents两种方式。

idautils.Strings

idautils.Strings() 是一个非常常用的 API,它会自动识别和遍历整个二进制文件中的所有字符串,得到一个 Strings 对象。

Strings 对象支持下标访问,得到的结果是一个 StringItem 对象,该对象有如下属性:

  • ea:字符串的地址。
  • strtype:字符串类型,有下面几种常见类型:
    • STRTYPE_TERMCHR :字符终止的字符串,字符串以特定字符结束。
    • STRTYPE_C :C 风格字符串(零终止)。
    • STRTYPE_C_16 :16 位字符的 C 风格字符串(零终止)。
    • STRTYPE_C_32 :32 位字符的 C 风格字符串(零终止)。
    • STRTYPE_PASCAL :Pascal 风格的字符串,带一个字节长度前缀。
    • STRTYPE_PASCAL_16 :Pascal 风格的 16 位字符字符串,带一个字节长度前缀。
    • STRTYPE_LEN2 :Pascal 风格的字符串,带两个字节的长度前缀。
    • STRTYPE_LEN2_16 :Pascal 风格的 16 位字符字符串,带两个字节的长度前缀。
    • STRTYPE_LEN4 :Pascal 风格的字符串,带四个字节的长度前缀。
    • STRTYPE_LEN4_16 :Pascal 风格的 16 位字符字符串,带四个字节的长度前缀。
  • length:字符串长度。

另外我们可以通过 str()StringItem 对象对象强制转换为字符串形式。

1
2
3
4
5
6
7
8
9
>>> import idautils
>>> s = idautils.Strings()

>>> str(s[0])
'/lib64/ld-linux-x86-64.so.2'
>>> s[0].ea
0x318
>>> s[0].length
0x1c

idc.get_strlit_contents

idc.get_strlit_contents 主要用于从指定地址提取字符串:

1
def get_strlit_contents(ea, length = -1, strtype = STRTYPE_C):
  • 参数
    • ea:字符串起始地址。
    • length:字符串长度。默认是 -1,此时 IDA 会计算最大字符串长度。
    • strtype:字符串类型,使用 STRTYPE_* 常量。
  • 返回值 :返回字符串内容。如果在指定的地址无法找到有效的字符串,或者字符串内容为空,则返回空字符串。

字符串查找

我们可以通过 idautils.Strings() 构建出程序的字符串表,之后就可以在这个表中查询字符串的地址。

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
import idautils
from collections import defaultdict

# 使用 defaultdict 直接管理字符串缓存
string_cache = defaultdict(list)


def find_string(string):
"""
在缓存的字符串表中查找指定字符串的地址
:param string: 要查找的字符串
:return: 字符串的地址列表
"""
# 如果缓存为空,则初始化缓存
if not string_cache:
for string_info in idautils.Strings():
string_cache[hash(str(string_info))].append(string_info.ea)

# 返回匹配的地址列表,如果没有找到则返回空列表
return string_cache.get(hash(string), [])


# 示例:查找字符串
address_list = find_string("Hello, World!")
if address_list:
print(f"String found at addresses: {address_list}")
else:
print("String not found.")

字符串创建

idc.create_strlit 可以在指定地址创建字符串(字符串类型由 get_inf_attr(INF_STRTYPE) 的值决定):

1
def create_strlit(ea, endea):
  • 参数
    • ea:字符串起始地址。
    • endea:字符串的结束地址(不包括该地址)。如果 endea == BADADDR 则 IDA 会计算最大字符串长度。
  • 返回值 :返回 1 表示成功,0 表示失败。

汇编指令相关

IdaPython 内置了汇编指令相关的 API,但是 IDA 的汇编与主流的汇编库 Keystone 和 Capstone 不通用,进而导致与主流的二进制相关库不兼容。

因此我们通常只是使用 IdaPython 的部分汇编指令功能(主要是获取指令地址和长度),而实际的汇编和反汇编操作主要还是通过 keystone 和 Capstone 实现。

  • idc.GetDisasm(addr)idc.generate_disasm_line(addr,flags):获取地址处的汇编语句,这里 flags 通常为 0 。

  • idc.print_operand(addr,index):获取指定地址 addr 的汇编指令的第 index 个操作数(字符串形式),如果 index 索引超过操作数的个数则返回空字符串。下面简单举几个例子感受一下:

    汇编 inxex = 0 index = 1
    pop rax rax ‘’
    mov [rsp+10h], rax [rsp+10h] rax
    call $+5 $+5 ‘’
    add rax, 68FBh rax 68FBh
    jz short loc_1400100CC loc_1400100CC ‘’
    popfq ‘’ ‘’
    retn ‘’ ‘’
  • idc.get_operand_type(addr, index):获取操作数的类型。

    • o_void (0):无效操作数,表示没有操作数。
    • o_reg (1):寄存器操作数,表示一个寄存器。
    • o_mem (2):内存操作数,表示一个内存地址。
    • o_phrase (3):短语操作数,表示根据寄存器和偏移量计算的内存地址。
    • o_displ (4):带偏移量的内存操作数,表示根据寄存器、偏移量和可选标志寄存器计算的内存地址。
    • o_imm (5):立即数操作数,表示一个立即数值。
    • o_far (6):远跳转操作数,表示一个远跳转地址。
    • o_near (7):相对跳转操作数,表示一个相对于当前指令地址的跳转地址。
  • idc.get_operand_value(addr, index):获取指定索引操作数中的值。

    • 对于寄存器操作数 (o_reg),返回寄存器的编号。
    • 对于内存操作数 (o_mem),返回内存地址的值。
    • 对于立即数操作数 (o_imm),返回立即数的值。
    • 对于相对跳转操作数 (o_near),返回跳转的地址。
    • 对于其他特定于处理器的操作数类型,返回相应的值,具体含义需要参考相关文档。
  • idc.print_insn_mnem(addr):获取指定地址 addr 的汇编指令的操作指令(如 movadd)。

  • idc.next_head(addr):获取当前地址的汇编的下一条汇编的地址。

  • idc.prev_head(addr):获取当前地址的汇编的上一条汇编的地址。

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
import idc
import ida_ida
from keystone import *
from capstone import *

if idc.get_inf_attr(idc.INF_PROCNAME) == 'metapc':
if idc.get_inf_attr(idc.INF_LFLAGS) & ida_ida.LFLG_64BIT:
ks = Ks(KS_ARCH_X86, KS_MODE_64)
cs = Cs(CS_ARCH_X86, CS_MODE_64)
else:
ks = Ks(KS_ARCH_X86, KS_MODE_32)
cs = Cs(CS_ARCH_X86, CS_MODE_32)
elif idc.get_inf_attr(idc.INF_PROCNAME) == 'ARM':
if idc.get_inf_attr(idc.INF_LFLAGS) & ida_ida.LFLG_64BIT:
ks = Ks(KS_ARCH_ARM64, KS_MODE_ARM)
cs = Cs(CS_ARCH_ARM64, CS_MODE_ARM)
else:
ks = Ks(KS_ARCH_ARM, KS_MODE_THUMB)
cs = Cs(CS_ARCH_ARM64, CS_MODE_THUMB)
else:
raise Exception(f"unsport arch")


def get_disasm(addr):
insn = next(cs.disasm(idc.get_bytes(addr, idc.get_item_size(addr)), addr))
print(f"{insn.address:#x}: {insn.mnemonic} {insn.op_str}")
return insn

def asm(asm_code, addr=0):
return bytes(ks.asm(asm_code, addr)[0])

函数相关

  • ida_funcs.FuncItems(start)FuncItems 函数接收一个函数起始地址,并返回一个迭代器,允许你遍历并打印函数内部的每条汇编的地址。

  • idautils.Functions(startaddr,endaddr):获取指定地址之间的所有函数

  • idc.get_func_name(addr):获取指定地址所在函数的函数名

  • get_func_cmt(addr, repeatable):获取函数的注释

    • repeatable:0 是获取常规注释,1 是获取重复注释。
  • idc.set_func_cmt(ea, cmt, repeatable):设置函数注释

  • idc.find_func_end(ea):寻找函数结尾,如果函数存在则返回结尾地址,否则返回 BADADDR

  • ida_funcs.set_func_end(addr, newend):设置函数结尾为 newend

  • ida_funcs.set_func_start(addr, newstart):设置函数开头为 newstart

  • ida_funcs.get_func(addr).start_ea:获取 addr 所在函数的地址

  • idc.get_prev_func(addr):获取 addr 所在函数的前一个函数的地址

  • idc.get_next_func(addr):获取 addr 所在函数的后一个函数的地址

  • ida_funcs.add_func(addr):在 addr 地址创建函数

  • idaapi.FlowChart(func):接受一个函数对象,返回这个函数所有的代码块。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    def get_basic_block(address):
    for block in idaapi.FlowChart(idaapi.get_func(address)):
    if block.start_ea <= address < block.end_ea:
    return block
    return None

    def prev_call(addr):
    addr = idc.prev_head(addr)
    block = get_basic_block(addr)
    while addr >= block.start_ea:
    if arch_ctx.is_call(next(arch_ctx.cs.disasm(ida_bytes.get_bytes(addr, idc.get_item_size(addr)), addr))):
    return addr
    addr = idc.prev_head(addr)
    return block.start_ea

    def next_call(addr):
    addr = idc.next_head(addr)
    block = get_basic_block(addr)
    while addr < block.end_ea:
    if arch_ctx.is_call(next(arch_ctx.cs.disasm(ida_bytes.get_bytes(addr, idc.get_item_size(addr)), addr))):
    return addr
    addr = idc.next_head(addr)
    return block.end_ea

去混淆

基础理论

程序的结构

我们可以认为一个程序的代码结构如下图所示:
在这里插入图片描述
一个程序由多个函数(function)组成,而每个函数由多个分支(branch)组成,对于函数和分支我们做如下定义:

  • 函数:从 CALL 指令跳转到的代码开始,在不通过 CALL 指令跳转的前提下能访问到的所有代码。
  • 分支:通过 JCC 跳转到的代码开始,直到以 RET 结尾或者跳转到已分析过的分支的代码块。

因此去混淆的时候我们可以有如下代码框架,即先 bfs 函数,然后在每个函数内部再 bfs 所有分支。在 bfs 的过程中将已去混淆的代码拼接起来。这样做的好处是同一个函数的代码尽可能放在一起,ida 在反编译的时候容易识别。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func_queue = Queue()
func_queue.put(entry_point)

while not func_queue.empty():
func_address = func_queue.get()

branch_queue = Queue()
branch_queue.put(func_address)
while not branch_queue.empty():
branch_address = branch_queue.get()
... # 去混淆代码
if idc.print_insn_mnem(ea) == 'call': # CALL function
func_queue.put(call_target)
elif idc.print_insn_mnem(ea)[0] == 'j' # JCC branch
branch_queue.put(jcc_target)
... # 重定位代码

代码重定位

代码的位置移动时,原本的 CALL 和 JCC 等跳转指令要想跳转到原来的地方需要进行指令修正,这个可以借助 keystone-engine 和 capstone 来完成。

1
2
def mov_code(ea, new_code_ea):
return asm(disasm(idc.get_bytes(ea, idc.get_item_size(ea)), ea), new_code_ea)

然而在完成去混淆后程序中的绝大多数代码都移动了位置,因此程序中所有的 CALL 和 JCC 等跳转指令跳转的地址需要进行修正,也就是重定位。

对于指令修正我们可以通过并查集来维护。

一个程序的跳转指令可以看做是上图左边的结构。即存在一个跳转指令跳转到另一个跳转指令的情况。通过并查集我们可以将指令 A,B,C,D,E 的真实地址都修正为指令 E 的真实地址

在使用并查集维护重定位的时候需要注意以下几点:

  • 上图中的指令 E 需要确保不存在指令复用的情况。因为有的代码混淆会将程序拆分成指令后放到一个巨大的 switch 中,然后通过在 switch 中查找依次执行指令。这种情况会造成一条指令在不同的分支中都会使用,如果此时我们用并查集维护就会把该指令重定位到其中一个使用该指令的地址,但实际上该指令还会在其他地址出现,这就造成了程序可能会跳转到错误的分支上。对于这种情况我们需要重定位查找 switch 的代码到去混淆的代码上,而不是重定位 switch 中的具体指令,这样就保证一一对应了。
  • 在上图的结构中我们可以发现,只有连接根节点的边是重定位的边,其余的边都是跳转的边。因为在跳转的时候我们不需要关心中间的跳转指令在哪里,而是需要关心最终跳转到的位置的真实地址。因此在并查集合并的时候如果是一条 JMP 指令就需要将该指令的重定位后的实际地址合并到指令的原本地址,然后将指令的原本地址合并到指令的跳转地址,否则将该指令的原本地址合并到指令的重定位后的实际地址。这样在并查集路径压缩之后每一个跳转指令跳转地址都被重定位到非 JMP 指令的实际地址

例题:强网杯2022 find_basic

附件下载链接

观察发现程序由下面的代码块构成:

1
2
3
4
5
6
.text:000048F4 pushf
.text:000048F5 pusha
.text:000048F6 mov cl, 3Fh ; '?'
.text:000048F8 call sub_44FA
.text:000048F8
.text:000048FD pop eax

分析该代码块的执行过程,发现本质是在一个 switch 中查找实际指令。该代码块可由 lea ecx, [esp+4] 指令代替。

首先,我们需要将程序中的代码块提取出来,然后记录几个有用的信息:

  • start_ea:代码块的起始地址
  • end_ea:代码块的结束地址
  • imm:在 switch 中查找指令用的立即数
  • reg:存放立即数用的寄存器
  • call_target:调用的 switch 函数

在提取代码块的有效信息的同时也可以检测该代码块是否有效,因此分析发现程序中会在代码块直接插入一些有实际功能的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Block:
def __init__(self, start_ea, end_ea, imm, reg, call_target):
self.start_ea = start_ea
self.end_ea = end_ea
self.imm = imm
self.reg = reg
self.call_target = call_target

def get_block(start_ea):
global imm, reg, call_target
mnem_list = ['pushf', 'pusha', 'mov', 'call', 'pop']
ea = start_ea
for i in range(5):
mnem = idc.print_insn_mnem(ea)
assert mnem == mnem_list[i]
if mnem == 'mov':
imm = idc.get_operand_value(ea, 1)
reg = idc.print_operand(ea, 0)
elif mnem == 'call':
call_target = idc.get_operand_value(ea, 0)
ea += idc.get_item_size(ea)
return Block(start_ea, ea, imm, reg, call_target)

在提取出代码块之后利用提取到的有效信息可以在 call_target 中查找代码块对应的实际代码。这里有几个特殊情况:

  • 一般情况在 cmp 判断找到对应位置后会依次执行 jnz,popa,popf 三条指令,然后后面紧跟着代码块对应的实际代码。然而想下面这种情况,在执行完 popf 后面紧跟着 pusha 而不是代码块对应的实际代码,简单分析一下发现这种情况代码块对应的实际代码为 retn 。这种情况需要返回 True 表示一个 branch 的结束。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    .text:000045CC popa
    .text:000045CD popf
    .text:000045CE pushf
    .text:000045CF pusha
    .text:000045D0 call dec_index
    .text:000045D0
    .text:000045D5 popa
    .text:000045D6 popf
    .text:000045D7 retn
  • 通常认为代码块对应的实际代码的结束标志为一个 jmp 指令,但是有的地方在 jmp 之后还会执行几条有效指令,因此判断实际代码的结束标志应当是 pushf 。

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
def get_real_code(block, new_code_ea):
ea = block.call_target
while True:
if idc.print_insn_mnem(ea) == 'cmp':
reg = idc.print_operand(ea, 0)
imm = idc.get_operand_value(ea, 1)
if reg == block.reg and imm == block.imm:
ea += idc.get_item_size(ea)
break
ea += idc.get_item_size(ea)

# 在 cmp 判断找到对应位置后会依次执行 jnz,popa,popf 三条指令
assert idc.print_insn_mnem(ea) == 'jnz'
ea += idc.get_item_size(ea)

assert idc.print_insn_mnem(ea) == 'popa'
ea += idc.get_item_size(ea)

assert idc.print_insn_mnem(ea) == 'popf'
ea += idc.get_item_size(ea)

if idc.print_insn_mnem(ea) == 'pushf': # 第一种特殊情况,实际是 ret 指令。
return True, asm('ret')

new_code = b''
while True:
if idc.print_insn_mnem(ea) == 'jmp': # 第二种特殊情况,跳转过去可能还会有几条实际功能指令。
jmp_ea = idc.get_operand_value(ea, 0)
if idc.print_insn_mnem(jmp_ea) == 'pushf':
break
ea = jmp_ea
else:
code = mov_code(ea, new_code_ea)
new_code += code
new_code_ea += len(code)
ea += get_item_size(ea)
return False, new_code

这里涉及到了维护重定位的并查集 RelocDSU ,对应代码如下。在 get 函数中如果遇到了 jmp 指令且操作数是立即数就路径压缩到跳转的地址,直到地址在 .got.plt 或者指令不是 jmp 指令。另外判断是否是已处理代码是根据地址对应的最终地址是否不在 .text 段。

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
class RelocDSU:

def __init__(self):
self.reloc = {}

def get(self, ea):
if ea not in self.reloc:
if idc.print_insn_mnem(ea) == 'jmp' and idc.get_operand_type(ea, 0) != idc.o_reg:
jmp_ea = idc.get_operand_value(ea, 0)

if idc.get_segm_name(jmp_ea) == '.got.plt':
self.reloc[ea] = ea
return self.reloc[ea], False

self.reloc[ea], need_handle = self.get(idc.get_operand_value(ea, 0))
return self.reloc[ea], need_handle
else:
self.reloc[ea] = ea
if self.reloc[ea] != ea: self.reloc[ea] = self.get(self.reloc[ea])[0]
return self.reloc[ea], idc.get_segm_name(self.reloc[ea]) == '.text'

def merge(self, ea, reloc_ea):
self.reloc[self.get(ea)[0]] = self.get(reloc_ea)[0]


reloc = RelocDSU()

接下来就是考虑如何提取出一个 branch 的代码了。前面提到过程序中会在代码块直接插入一些有实际功能的代码,因此需要借助 try:...except:...assert 来处理。除此之外这里还有几个特殊情况:

  • 程序中的 0x900 和 0x435c 处分别有一个获取返回地址 eip 到 ebx 和 eax 的函数,程序借助这两个函数来访问全局变量实现地址无关代码,然而重定位后代码地址改变,因此这里需要将其修正为 mov reg, xxx
  • 需要根据程序中的 jmp 指令来决定下一步需要去混淆的代码位置,这里需要判断 jmp 后面跟的是否是立即数,另外需要判断 jmp 到的代码是否是已经处理过的代码。
  • 并查集合并的时候如果是代码块,需要将代码块的地址合并到代码块对应指令的实际重定位后的地址;如果不是代码块如果是 jmp 指令且操作数是立即数,需要将 jmp 指令和该指令的重定位后的实际地址合并到指令的原本地址,然后将指令的原本地址合并到指令的跳转地址,否则将该指令的地址合并到重定位后的地址。
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
def handle_one_branch(branch_address, new_code_ea):
new_code = b''
ea = branch_address
while True:
try:
block = get_block(ea)
is_ret, real_code = get_real_code(block, new_code_ea)
reloc.merge(ea, new_code_ea)
ea = block.end_ea
new_code_ea += len(real_code)
new_code += real_code
if is_ret: break
except:
get_eip_func = {0x900: 'ebx', 0x435c: 'eax'}
if idc.print_insn_mnem(ea) == 'call' and get_operand_value(ea, 0) in get_eip_func:
reloc.merge(ea, new_code_ea)
real_code = asm('mov %s, 0x%x' % (get_eip_func[get_operand_value(ea, 0)], ea + 5), new_code_ea)
else:
if idc.print_insn_mnem(ea) == 'jmp' and idc.get_operand_type(ea, 0) != idc.o_reg:
reloc.merge(new_code_ea, ea)
else:
reloc.merge(ea, new_code_ea)
real_code = mov_code(ea, new_code_ea)

new_code += real_code
if real_code == asm('ret'): break
new_code_ea += len(real_code)
if idc.print_insn_mnem(ea) == 'jmp' and idc.get_operand_type(ea, 0) != idc.o_reg: # jmp reg is a swtich
jmp_ea = idc.get_operand_value(ea, 0)
if reloc.get(jmp_ea)[1] == False: break # 跳回之前的代码说明是个循环
ea = reloc.get(jmp_ea)[0]
else:
ea += get_item_size(ea)
return new_code

能够处理 branch 后,我们就可以 bfs 依次处理所有的 function 和 branch 了,这里还有几个特殊情况:

  • 0x4148 地址处的函数中有一个 switch ,由于是通过跳转表跳转,去混淆脚本分析不到跳转的分支,因此需要读取跳转表找到跳转的 branch 然后添加到 branch_queue 中。
  • 寻找新的 branch 时需要判断 jcc 的操作数类型是否是立即数。
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
func_queue = Queue()
func_queue.put(entry_point)

while not func_queue.empty():
func_address = func_queue.get()
if reloc.get(func_address)[1] == False: continue
reloc.merge(func_address, new_code_ea)
branch_queue = Queue()
branch_queue.put(func_address)
if func_address == 0x4148: # 特判 0x4148 地址处的函数,读取跳转表。
assert new_code_ea == 0x963d0
for eax in range(0x20):
jmp_target = (ida_bytes.get_dword(jmp_table[0] + eax * 4) + jmp_table[1]) & 0xFFFFFFFF
new_jmp_target, need_handle = reloc.get(jmp_target)
if need_handle: branch_queue.put(jmp_target)

while not branch_queue.empty():
branch_address = branch_queue.get()
new_code = handle_one_branch(branch_address, new_code_ea)
ida_bytes.patch_bytes(new_code_ea, new_code)

# 当前 branch 去完混淆之后需要遍历代码找到 call 和 jmp 指令从而找到其他的 function 和 branch 。
ea = new_code_ea
while ea < new_code_ea + len(new_code):
idc.create_insn(ea)
if idc.print_insn_mnem(ea) == 'call':
call_target, need_handle = reloc.get(get_operand_value(ea, 0))
if need_handle: func_queue.put(call_target)
elif idc.print_insn_mnem(ea)[0] == 'j' and idc.get_operand_type(ea, 0) != idc.o_reg:
jcc_target, need_handle = reloc.get(get_operand_value(ea, 0))
if need_handle == True:
branch_queue.put(jcc_target)
ea += get_item_size(ea)
new_code_ea += len(new_code)

在完成代码去混淆之后需要对代码进行重定位,重定位的时候需要注意 jmp 指令长度的变化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ea = new_code_start
while ea < new_code_ea:
idc.create_insn(ea)
mnem = idc.print_insn_mnem(ea)

if mnem == 'call':
call_target, need_handle = reloc.get(get_operand_value(ea, 0))
assert need_handle == False
ida_bytes.patch_bytes(ea, asm('call 0x%x' % (call_target), ea))
elif mnem[0] == 'j' and idc.get_operand_type(ea, 0) != idc.o_reg:
jcc_target, need_handle = reloc.get(get_operand_value(ea, 0))
assert need_handle == False
ida_bytes.patch_bytes(ea, asm('%s 0x%x' % (mnem, jcc_target), ea).ljust(idc.get_item_size(ea), b'\x90'))
elif mnem == 'pushf':
ida_bytes.patch_bytes(ea, b'\x90' * 9)
ea += 9
continue
ea += get_item_size(ea)

最后去混淆后的 switch 不能被 ida 正常识别出来,具体原因是前面获取返回地址 eip 的函数被 patch 成了 mov reg, xxx 指令,导致其与编译器默认编译出的汇编不同(程序开启了 PIE,直接访问跳转表的地址 ida 不能正确识别),因此需要将这里的代码重新 patch 回去。

同时为了不影响原本程序中的数据,这里我将修复的跳转表放到了其他位置。另外还有两个字符串全局变量也移动到了正确位置。

1
2
3
4
5
6
7
8
9
10
11
12
new_jmp_table = (0xA6000 - 0x2D54, 0xA6000)

# 移动并修复跳转表
for eax in range(0x20):
jmp_target = (ida_bytes.get_dword(jmp_table[0] + eax * 4) + jmp_table[1]) & 0xFFFFFFFF
new_jmp_target, need_handle = reloc.get(jmp_target)
assert need_handle == False
ida_bytes.patch_dword(new_jmp_table[0] + eax * 4, (new_jmp_target - new_jmp_table[1]) & 0xFFFFFFFF)

need_patch_addr = 0x963D7
ida_bytes.patch_bytes(need_patch_addr, asm('call 0x900;add ebx, 0x%x' % (new_jmp_table[1] - (need_patch_addr + 5)), need_patch_addr)) # 修复指令
ida_bytes.patch_bytes(new_jmp_table[1] - 0x2d7a, ida_bytes.get_bytes(jmp_table[1] - 0x2d7a, 0x26)) # 复制字符串到正确位置

最终去混淆脚本如下:

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
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
from queue import *
import ida_bytes
from idc import *
import idc
from keystone import *
from capstone import *

asmer = Ks(KS_ARCH_X86, KS_MODE_32)
disasmer = Cs(CS_ARCH_X86, CS_MODE_32)


def disasm(machine_code, addr=0):
l = ""
for i in disasmer.disasm(machine_code, addr):
l += "{:8s} {};\n".format(i.mnemonic, i.op_str)
return l.strip('\n')


def asm(asm_code, addr=0):
l = b''
for i in asmer.asm(asm_code, addr)[0]:
l += bytes([i])
return l


def print_asm(ea):
print(disasm(idc.get_bytes(ea, idc.get_item_size(ea)), ea))


class RelocDSU:

def __init__(self):
self.reloc = {}

def get(self, ea):
if ea not in self.reloc:
if idc.print_insn_mnem(ea) == 'jmp' and idc.get_operand_type(ea, 0) != idc.o_reg:
jmp_ea = idc.get_operand_value(ea, 0)

if idc.get_segm_name(jmp_ea) == '.got.plt':
self.reloc[ea] = ea
return self.reloc[ea], False

self.reloc[ea], need_handle = self.get(idc.get_operand_value(ea, 0))
return self.reloc[ea], need_handle
else:
self.reloc[ea] = ea
if self.reloc[ea] != ea: self.reloc[ea] = self.get(self.reloc[ea])[0]
return self.reloc[ea], idc.get_segm_name(self.reloc[ea]) == '.text'

def merge(self, ea, reloc_ea):
self.reloc[self.get(ea)[0]] = self.get(reloc_ea)[0]


reloc = RelocDSU()


class Block:
def __init__(self, start_ea, end_ea, imm, reg, call_target):
self.start_ea = start_ea
self.end_ea = end_ea
self.imm = imm
self.reg = reg
self.call_target = call_target


def mov_code(ea, new_code_ea):
return asm(disasm(idc.get_bytes(ea, idc.get_item_size(ea)), ea), new_code_ea)


def get_real_code(block, new_code_ea):
ea = block.call_target
while True:
if idc.print_insn_mnem(ea) == 'cmp':
reg = idc.print_operand(ea, 0)
imm = idc.get_operand_value(ea, 1)
if reg == block.reg and imm == block.imm:
ea += idc.get_item_size(ea)
break
ea += idc.get_item_size(ea)

# 在 cmp 判断找到对应位置后会依次执行 jnz,popa,popf 三条指令
assert idc.print_insn_mnem(ea) == 'jnz'
ea += idc.get_item_size(ea)

assert idc.print_insn_mnem(ea) == 'popa'
ea += idc.get_item_size(ea)

assert idc.print_insn_mnem(ea) == 'popf'
ea += idc.get_item_size(ea)

if idc.print_insn_mnem(ea) == 'pushf': # 第一种特殊情况,实际是 ret 指令。
return True, asm('ret')

new_code = b''
while True:
if idc.print_insn_mnem(ea) == 'jmp': # 第二种特殊情况,跳转过去可能还会有几条实际功能指令。
jmp_ea = idc.get_operand_value(ea, 0)
if idc.print_insn_mnem(jmp_ea) == 'pushf':
break
ea = jmp_ea
else:
code = mov_code(ea, new_code_ea)
new_code += code
new_code_ea += len(code)
ea += get_item_size(ea)
return False, new_code


def get_block(start_ea):
global imm, reg, call_target
mnem_list = ['pushf', 'pusha', 'mov', 'call', 'pop']
ea = start_ea
for i in range(5):
mnem = idc.print_insn_mnem(ea)
assert mnem == mnem_list[i]
if mnem == 'mov':
imm = idc.get_operand_value(ea, 1)
reg = idc.print_operand(ea, 0)
elif mnem == 'call':
call_target = idc.get_operand_value(ea, 0)
ea += idc.get_item_size(ea)
return Block(start_ea, ea, imm, reg, call_target)


def handle_one_branch(branch_address, new_code_ea):
new_code = b''
ea = branch_address
while True:
try:
block = get_block(ea)
is_ret, real_code = get_real_code(block, new_code_ea)
reloc.merge(ea, new_code_ea)
ea = block.end_ea
new_code_ea += len(real_code)
new_code += real_code
if is_ret: break
except:
get_eip_func = {0x900: 'ebx', 0x435c: 'eax'}
if idc.print_insn_mnem(ea) == 'call' and get_operand_value(ea, 0) in get_eip_func:
reloc.merge(ea, new_code_ea)
real_code = asm('mov %s, 0x%x' % (get_eip_func[get_operand_value(ea, 0)], ea + 5), new_code_ea)
else:
if idc.print_insn_mnem(ea) == 'jmp' and idc.get_operand_type(ea, 0) != idc.o_reg:
reloc.merge(new_code_ea, ea)
else:
reloc.merge(ea, new_code_ea)
real_code = mov_code(ea, new_code_ea)

new_code += real_code
if real_code == asm('ret'): break
new_code_ea += len(real_code)
if idc.print_insn_mnem(ea) == 'jmp' and idc.get_operand_type(ea, 0) != idc.o_reg: # jmp reg is a swtich
jmp_ea = idc.get_operand_value(ea, 0)
if reloc.get(jmp_ea)[1] == False: break # 跳回之前的代码说明是个循环
ea = reloc.get(jmp_ea)[0]
else:
ea += get_item_size(ea)
return new_code


def solve():
entry_point = 0x48F4
new_code_start = 0x96150
new_code_ea = new_code_start

jmp_table = (0x892ac, 0x8c000) # [0x8c000 + (eax>>2) - 0x2d54] + 0x8c000

for _ in range(0x10000): idc.del_items(new_code_ea + _)
ida_bytes.patch_bytes(new_code_ea, 0x10000 * b'\x90')

func_queue = Queue()
func_queue.put(entry_point)

while not func_queue.empty():
func_address = func_queue.get()
if reloc.get(func_address)[1] == False: continue
reloc.merge(func_address, new_code_ea)
branch_queue = Queue()
branch_queue.put(func_address)
if func_address == 0x4148: # 特判 0x4148 地址处的函数,读取跳转表。
assert new_code_ea == 0x963d0
for eax in range(0x20):
jmp_target = (ida_bytes.get_dword(jmp_table[0] + eax * 4) + jmp_table[1]) & 0xFFFFFFFF
new_jmp_target, need_handle = reloc.get(jmp_target)
if need_handle: branch_queue.put(jmp_target)

while not branch_queue.empty():
branch_address = branch_queue.get()
new_code = handle_one_branch(branch_address, new_code_ea)
ida_bytes.patch_bytes(new_code_ea, new_code)

# 当前 branch 去完混淆之后需要遍历代码找到 call 和 jmp 指令从而找到其他的 function 和 branch 。
ea = new_code_ea
while ea < new_code_ea + len(new_code):
idc.create_insn(ea)
if idc.print_insn_mnem(ea) == 'call':
call_target, need_handle = reloc.get(get_operand_value(ea, 0))
if need_handle: func_queue.put(call_target)
elif idc.print_insn_mnem(ea)[0] == 'j' and idc.get_operand_type(ea, 0) != idc.o_reg:
jcc_target, need_handle = reloc.get(get_operand_value(ea, 0))
if need_handle == True:
branch_queue.put(jcc_target)
ea += get_item_size(ea)
new_code_ea += len(new_code)

ea = new_code_start
while ea < new_code_ea:
idc.create_insn(ea)
mnem = idc.print_insn_mnem(ea)

if mnem == 'call':
call_target, need_handle = reloc.get(get_operand_value(ea, 0))
assert need_handle == False
ida_bytes.patch_bytes(ea, asm('call 0x%x' % (call_target), ea))
elif mnem[0] == 'j' and idc.get_operand_type(ea, 0) != idc.o_reg:
jcc_target, need_handle = reloc.get(get_operand_value(ea, 0))
assert need_handle == False
ida_bytes.patch_bytes(ea, asm('%s 0x%x' % (mnem, jcc_target), ea).ljust(idc.get_item_size(ea), b'\x90'))
elif mnem == 'pushf':
ida_bytes.patch_bytes(ea, b'\x90' * 9)
ea += 9
continue
ea += get_item_size(ea)

new_jmp_table = (0xA6000 - 0x2D54, 0xA6000)

# 移动并修复跳转表
for eax in range(0x20):
jmp_target = (ida_bytes.get_dword(jmp_table[0] + eax * 4) + jmp_table[1]) & 0xFFFFFFFF
new_jmp_target, need_handle = reloc.get(jmp_target)
assert need_handle == False
ida_bytes.patch_dword(new_jmp_table[0] + eax * 4, (new_jmp_target - new_jmp_table[1]) & 0xFFFFFFFF)

need_patch_addr = 0x963D7
ida_bytes.patch_bytes(need_patch_addr, asm('call 0x900;add ebx, 0x%x' % (new_jmp_table[1] - (need_patch_addr + 5)), need_patch_addr)) # 修复指令
ida_bytes.patch_bytes(new_jmp_table[1] - 0x2d7a, ida_bytes.get_bytes(jmp_table[1] - 0x2d7a, 0x26)) # 复制字符串到正确位置

for _ in range(0x10000): idc.del_items(new_code_ea + _)
idc.jumpto(new_code_start)
ida_funcs.add_func(new_code_start)

print("finish")


solve()

例题:SUSCTF2022 tttree

附件下载链接

首先将 0x1400100740x140017EFA140018C67 起始处的数据转换为汇编。

观察汇编,发现很多代码块之间相互跳转,因此先按照 retn 划分代码块。通过对代码块的观察,发现这些代码块按照 call $+5;pop rax(即 E8 00 00 00 00 58 ) 的出现次数可以分为三种:

  • 出现 0 次:

    在这里插入图片描述 本质上是 `其它操作` + `retn` 。
  • 出现 1 次:

    这种代码块本质为 其它操作 + jmp target ,注意 其它操作 中可能包含 branch 。

  • 出现 2 次:

    这个可以看做 2 个出现 1 次的代码块两个拼在一起,其中前面一个代码块去掉 retn 。执行完前面一个代码块后由于没有 retn ,因此 target1 留在栈中。执行第 2 个代码块跳转到 target2 执行 ,在 target2 代码块返回时会返回到 target1 。因此这种代码块本质上相当于 其它操作 + call target2 且下一个要执行的代码块为 target1

我们定义代码块 Block 几个关键信息:

  • start_addr:代码块的起始地址。
  • asm_list:代码块的有效汇编,由于汇编指令可能包含 [rip + xxx] ,因此需要记录汇编指令的地址以便后续修正。
  • direct_next:执行完此代码块后接下来要执行的代码块地址。
  • branch_list:代码块中的所有条件跳转语句跳到的地址。
  • call_target:代码块调用函数地址。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Block:
def __init__(self, start_ea, asm_list, direct_next, branch_list, call_target):
self.start_ea = start_ea
self.asm_list = asm_list
self.direct_next = direct_next
self.branch_list = branch_list
self.call_target = call_target

def __str__(self):
return 'start_ea: 0x%x\ndirect_next: 0x%x\ncall_target: 0x%x\nbranch_list: %s\nasm_list:\n%s\n' % (
0 if self.start_ea == None else self.start_ea,
0 if self.direct_next == None else self.direct_next,
0 if self.call_target == None else self.call_target,
str([hex(x) for x in self.branch_list]),
str('\n'.join([hex(addr) + ' ' + asm for addr, asm in self.asm_list]))
)

get_block 函数可以获取给定地址处的代码块并提取相关信息。代码块中可能有 push xxx;pop xxx; 这样的无意义指令,可以通过栈模拟来去除。

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
def get_block(start_ea):
ea = start_ea
stack = []
asm_list = []
branch_list = []
call_target = None
direct_next = None

while True:
idc.create_insn(ea)
mnem = idc.print_insn_mnem(ea)

# 处理混淆中跳转的情况
if mnem == 'pushfq':
ea += idc.get_item_size(ea)

assert idc.get_bytes(ea, idc.get_item_size(ea)) == b'\xE8\x00\x00\x00\x00'
ea += idc.get_item_size(ea)
jmp_base = ea

assert idc.print_insn_mnem(ea) == 'pop' and idc.get_operand_type(ea, 0) == o_reg
reg = idc.print_operand(ea, 0)
ea += idc.get_item_size(ea)

assert idc.print_insn_mnem(ea) == 'add' and idc.print_operand(ea, 0) == reg
assert idc.get_operand_type(ea, 1) == o_imm

jmp_target = (jmp_base + idc.get_operand_value(ea, 1)) & 0xFFFFFFFFFFFFFFFF
ea += idc.get_item_size(ea)

assert idc.get_bytes(ea, idc.get_item_size(ea)) == asm('mov [rsp + 0x10], %s' % reg, ea)
ea += idc.get_item_size(ea)

assert idc.print_insn_mnem(ea) == 'popfq'
ea += idc.get_item_size(ea)

assert idc.print_insn_mnem(ea) == 'pop' and idc.print_operand(ea, 0) == reg
assert len(stack) != 0 and stack[-1][0] == 'push' and stack[-1][1] == reg
stack.pop()
asm_list.pop()

assert len(stack) != 0 and stack[-1][0] == 'push' and stack[-1][1] == reg
stack.pop()
asm_list.pop()

ea += idc.get_item_size(ea)

if idc.print_insn_mnem(ea) == 'retn':
if direct_next == None:
direct_next = jmp_target
elif call_target == None:
call_target = jmp_target
asm_list.append((0, 'call 0x%x' % (call_target)))
else:
print("🤔🤔🤔🤔🤔🤔")
assert False
break
else:
assert call_target == None and direct_next == None
direct_next = jmp_target
continue

if mnem == 'push':
stack.append((mnem, idc.print_operand(ea, 0)))
elif mnem == 'pop':
if len(stack) != 0 and stack[-1][0] == 'push' and stack[-1][1] == idc.print_operand(ea, 0):
stack.pop()
asm_list.pop()
ea += idc.get_item_size(ea)
continue
else:
stack.clear()
else:
stack.clear()

asm_list.append((ea, disasm(idc.get_bytes(ea, idc.get_item_size(ea)), ea)))

if mnem == 'retn': break
if mnem[0] == 'j' and mnem != 'jmp' and idc.get_operand_type(ea, 0) != o_reg:
branch_list.append(idc.get_operand_value(ea, 0))

if mnem == 'jmp':
if idc.get_segm_name(idc.get_operand_value(ea, 0)) not in ['.text', '.aaa']:
break
else:
ea = idc.get_operand_value(ea, 0)
else:
ea += idc.get_item_size(ea)

return Block(start_ea, asm_list, direct_next, branch_list, call_target)

能够获取代码块信息之后就可以 bfs 函数以及函数中的所有分支,提取出汇编代码并写入 newcode 段。这里需要注意以下几点:

  • 涉及 rip 的汇编指令不能只是简单把指令中的 rip 替换为对应的具体数值,因为有的指令立即数的长度被限制在 4 字节,直接替换成数值会溢出。一个比较好的解决方法是将 rip 替换为 rip + (指令原本地址 - 指令当前地址) 。这样借助 rip 寄存器扩大访问范围并且代码移动的距离不会超过 0x100000000 因此可以保证正确性。
  • 如果 block.direct_next 对应的代码已经被去混淆了需要加上一条 jmp 指令跳转到已经去混淆的代码。
  • 有的汇编指令 keystone 不支持汇编,比如 bnd ret ,需要特判。
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
while not func_queue.empty():
func_address = func_queue.get()
if reloc.get(func_address)[1] == False: continue
branch_queue = Queue()
branch_queue.put(func_address)

while not branch_queue.empty():
branch_address = branch_queue.get()
ea = branch_address

while True:
block = get_block(ea)
reloc.merge(ea, new_code_ea)

for addr, insn in block.asm_list:
insn = insn.replace('rip', 'rip - 0x%x' % (new_code_ea - addr))
if insn == 'bnd ret ;':
code = b'\xF2\xC3'
else:
code = asm(insn, new_code_ea)
ida_bytes.patch_bytes(new_code_ea, code)
if addr != 0: reloc.merge(addr, new_code_ea)
new_code_ea += len(code)

if block.call_target != None:
call_target, need_handle = reloc.get(block.call_target)
if need_handle: func_queue.put(call_target)

for branch_address in block.branch_list:
jcc_target, need_handle = reloc.get(branch_address)
if need_handle: branch_queue.put(jcc_target)

if block.direct_next == None: break

next_target, need_handle = reloc.get(block.direct_next)
if need_handle == False:
code = asm('jmp 0x%x' % (next_target), new_code_ea)
ida_bytes.patch_bytes(new_code_ea, code)
new_code_ea += len(code)
break
else:
ea = block.direct_next

最后对代码进行重定位,需要注意的是代码块中的有效指令中也可能有 call 指令,这里 call 调用的是一个类似 plt 表的结构,会直接跳转到导入表中的函数地址表指向的函数,需要特判这种情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ea = new_code_start
while ea < new_code_ea:
assert idc.create_insn(ea) != 0
mnem = idc.print_insn_mnem(ea)

if mnem == 'call':
call_target, need_handle = reloc.get(get_operand_value(ea, 0))
if need_handle == True:
if idc.print_insn_mnem(call_target) == 'jmp' and idc.get_segm_name(idc.get_operand_value(call_target, 0)) == '.idata':
ea += get_item_size(ea)
continue
else:
assert False
ida_bytes.patch_bytes(ea, asm('call 0x%x' % (call_target), ea).ljust(idc.get_item_size(ea), b'\x90'))
elif mnem[0] == 'j' and idc.get_operand_type(ea, 0) != idc.o_reg:
jcc_target, need_handle = reloc.get(get_operand_value(ea, 0))
assert need_handle == False
ida_bytes.patch_bytes(ea, asm('%s 0x%x' % (mnem, jcc_target), ea).ljust(idc.get_item_size(ea), b'\x90'))

ea += get_item_size(ea)

最后完整代码:

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
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
from queue import *
from idc import *
import idc
from keystone import *
from capstone import *

asmer = Ks(KS_ARCH_X86, KS_MODE_64)
disasmer = Cs(CS_ARCH_X86, CS_MODE_64)


def disasm(machine_code, addr=0):
l = ""
for i in disasmer.disasm(machine_code, addr):
l += "{:8s} {};\n".format(i.mnemonic, i.op_str)
return l.strip('\n')


def asm(asm_code, addr=0):
l = b''
for i in asmer.asm(asm_code, addr)[0]:
l += bytes([i])
return l


class RelocDSU:

def __init__(self):
self.reloc = {}

def get(self, ea):
if ea not in self.reloc:
if idc.print_insn_mnem(ea) == 'jmp' and idc.get_operand_type(ea, 0) != idc.o_reg:
jmp_ea = idc.get_operand_value(ea, 0)

if idc.get_segm_name(jmp_ea) == '.idata':
self.reloc[ea] = ea
return self.reloc[ea], False

self.reloc[ea], need_handle = self.get(idc.get_operand_value(ea, 0))
return self.reloc[ea], need_handle
else:
self.reloc[ea] = ea
if self.reloc[ea] != ea: self.reloc[ea] = self.get(self.reloc[ea])[0]
return self.reloc[ea], idc.get_segm_name(self.reloc[ea]) in ['.text', '.aaa']

def merge(self, ea, reloc_ea):
# print((hex(ea), hex(reloc_ea)))
self.reloc[self.get(ea)[0]] = self.get(reloc_ea)[0]


reloc = RelocDSU()


class Block:
def __init__(self, start_ea, asm_list, direct_next, branch_list, call_target):
self.start_ea = start_ea
self.asm_list = asm_list
self.direct_next = direct_next
self.branch_list = branch_list
self.call_target = call_target

def __str__(self):
return 'start_ea: 0x%x\ndirect_next: 0x%x\ncall_target: 0x%x\nbranch_list: %s\nasm_list:\n%s\n' % (
0 if self.start_ea == None else self.start_ea,
0 if self.direct_next == None else self.direct_next,
0 if self.call_target == None else self.call_target,
str([hex(x) for x in self.branch_list]),
str('\n'.join([hex(addr) + ' ' + asm for addr, asm in self.asm_list]))
)


def get_block(start_ea):
ea = start_ea
stack = []
asm_list = []
branch_list = []
call_target = None
direct_next = None

while True:
idc.create_insn(ea)
mnem = idc.print_insn_mnem(ea)

# 处理混淆中跳转的情况
if mnem == 'pushfq':
ea += idc.get_item_size(ea)

assert idc.get_bytes(ea, idc.get_item_size(ea)) == b'\xE8\x00\x00\x00\x00'
ea += idc.get_item_size(ea)
jmp_base = ea

assert idc.print_insn_mnem(ea) == 'pop' and idc.get_operand_type(ea, 0) == o_reg
reg = idc.print_operand(ea, 0)
ea += idc.get_item_size(ea)

assert idc.print_insn_mnem(ea) == 'add' and idc.print_operand(ea, 0) == reg
assert idc.get_operand_type(ea, 1) == o_imm

jmp_target = (jmp_base + idc.get_operand_value(ea, 1)) & 0xFFFFFFFFFFFFFFFF
ea += idc.get_item_size(ea)

assert idc.get_bytes(ea, idc.get_item_size(ea)) == asm('mov [rsp + 0x10], %s' % reg, ea)
ea += idc.get_item_size(ea)

assert idc.print_insn_mnem(ea) == 'popfq'
ea += idc.get_item_size(ea)

assert idc.print_insn_mnem(ea) == 'pop' and idc.print_operand(ea, 0) == reg
assert len(stack) != 0 and stack[-1][0] == 'push' and stack[-1][1] == reg
stack.pop()
asm_list.pop()

assert len(stack) != 0 and stack[-1][0] == 'push' and stack[-1][1] == reg
stack.pop()
asm_list.pop()

ea += idc.get_item_size(ea)

if idc.print_insn_mnem(ea) == 'retn':
if direct_next == None:
direct_next = jmp_target
elif call_target == None:
call_target = jmp_target
asm_list.append((0, 'call 0x%x' % (call_target)))
else:
print("🤔🤔🤔🤔🤔🤔")
assert False
break
else:
assert call_target == None and direct_next == None
direct_next = jmp_target
continue

if mnem == 'push':
stack.append((mnem, idc.print_operand(ea, 0)))
elif mnem == 'pop':
if len(stack) != 0 and stack[-1][0] == 'push' and stack[-1][1] == idc.print_operand(ea, 0):
stack.pop()
asm_list.pop()
ea += idc.get_item_size(ea)
continue
else:
stack.clear()
else:
stack.clear()

asm_list.append((ea, disasm(idc.get_bytes(ea, idc.get_item_size(ea)), ea)))

if mnem == 'retn': break
if mnem[0] == 'j' and mnem != 'jmp' and idc.get_operand_type(ea, 0) != o_reg:
branch_list.append(idc.get_operand_value(ea, 0))

if mnem == 'jmp':
if idc.get_segm_name(idc.get_operand_value(ea, 0)) not in ['.text', '.aaa']:
break
else:
ea = idc.get_operand_value(ea, 0)
else:
ea += idc.get_item_size(ea)

return Block(start_ea, asm_list, direct_next, branch_list, call_target)


entry_point = 0x1400133B7
new_code_start = 0x14001D000


def solve():
for i in range(0x10000):
idc.set_name(new_code_start + i, '')
idc.del_items(new_code_start + i)
ida_bytes.patch_bytes(new_code_start, b'\x90' * 0x10000)

func_queue = Queue()
func_queue.put(entry_point)
new_code_ea = new_code_start

while not func_queue.empty():
func_address = func_queue.get()
if reloc.get(func_address)[1] == False: continue
branch_queue = Queue()
branch_queue.put(func_address)

while not branch_queue.empty():
branch_address = branch_queue.get()
ea = branch_address

while True:
block = get_block(ea)
reloc.merge(ea, new_code_ea)

for addr, insn in block.asm_list:
insn = insn.replace('rip', 'rip - 0x%x' % (new_code_ea - addr))
if insn == 'bnd ret ;':
code = b'\xF2\xC3'
else:
code = asm(insn, new_code_ea)
ida_bytes.patch_bytes(new_code_ea, code)
if addr != 0: reloc.merge(addr, new_code_ea)
new_code_ea += len(code)

if block.call_target != None:
call_target, need_handle = reloc.get(block.call_target)
if need_handle: func_queue.put(call_target)

for branch_address in block.branch_list:
jcc_target, need_handle = reloc.get(branch_address)
if need_handle: branch_queue.put(jcc_target)

if block.direct_next == None: break

next_target, need_handle = reloc.get(block.direct_next)
if need_handle == False:
code = asm('jmp 0x%x' % (next_target), new_code_ea)
ida_bytes.patch_bytes(new_code_ea, code)
new_code_ea += len(code)
break
else:
ea = block.direct_next

ea = new_code_start
while ea < new_code_ea:
assert idc.create_insn(ea) != 0
mnem = idc.print_insn_mnem(ea)

if mnem == 'call':
call_target, need_handle = reloc.get(get_operand_value(ea, 0))
if need_handle == True:
if idc.print_insn_mnem(call_target) == 'jmp' and idc.get_segm_name(idc.get_operand_value(call_target, 0)) == '.idata':
ea += get_item_size(ea)
continue
else:
assert False
ida_bytes.patch_bytes(ea, asm('call 0x%x' % (call_target), ea).ljust(idc.get_item_size(ea), b'\x90'))
elif mnem[0] == 'j' and idc.get_operand_type(ea, 0) != idc.o_reg:
jcc_target, need_handle = reloc.get(get_operand_value(ea, 0))
assert need_handle == False
ida_bytes.patch_bytes(ea, asm('%s 0x%x' % (mnem, jcc_target), ea).ljust(idc.get_item_size(ea), b'\x90'))

ea += get_item_size(ea)

for i in range(0x10000): idc.del_items(new_code_start + i)
idc.jumpto(new_code_start)
idc.add_func(new_code_start)

print("finish")


solve()

去花指令

花指令目前没有太好的去除办法,但是同一题目中花指令种类和变化都是有限的,也就是说我们可以将题目中所有花指令的类型总结出来,然后分别编写相应的查找和处理规则。

例题:看雪CTF2019 圆圈舞DancingCircle

附件下载链接

用IDA打开DancingCircle,按G输入 0x401f58 跳转至核心函数,发现有大量花指令。因此需要借助 ida python 脚本正则表达式匹配去除。
分析汇编代码,发现花指令有如下几类:

call 花指令

  • call + pop
    例如 0x00401F9B 处的花指令
    另外还有 push eax + call + pop eax + pop eax 类型的。

  • call + add esp, 4
    例如 0x00401F62 处的花指令

  • call + add [esp], 6 + retn
    例如 0x00401FA3 处的花指令

jx + jnx 花指令

例如 0x00402D67 处的花指令

这类花指令可以做如下检测:

  • 两个跳转指令的第一个字节相差 1 且较小的那个是偶数。
  • 前一个跳转的立即数比后一个多 2 。

fake jmp 花指令

例如 0x00401FB2 这处花指令:

这里有很多跳转,但分析后发现这些跳转都可以忽略。由于这一类花指令比较单一,因此直接匹配特征即可。

stx + jx 花指令

例如 0x0040261F 和 0x004026D7 两处花指令:


此类花指令本质是通过设置标志寄存器的值使得满足后面的条件跳转。由于此类指令较少,直接匹配特征即可。

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
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
import regex as re
from idc import *
import idc
from keystone import *
from capstone import *

asmer = Ks(KS_ARCH_X86, KS_MODE_32)
disasmer = Cs(CS_ARCH_X86, CS_MODE_32)


def disasm(machine_code, addr=0):
l = ""
for i in disasmer.disasm(machine_code, addr):
l += "{:8s} {};\n".format(i.mnemonic, i.op_str)
return l.strip('\n')


def asm(asm_code, addr=0):
l = b''
for i in asmer.asm(asm_code, addr)[0]:
l += bytes([i])
return l


def check_call_to_jmp(call_insn_addr):
call_target = idc.get_operand_value(call_insn_addr, 0)
if call_target not in range(start_ea, end_ea): return None
idc.create_insn(call_target)
if ida_bytes.get_bytes(call_target, idc.get_item_size(call_target)) == asm('add esp, 4', call_target):
return call_target + idc.get_item_size(call_target)
if idc.print_insn_mnem(call_target) == 'pop':
return call_target + idc.get_item_size(call_target)
insn = disasm(ida_bytes.get_bytes(call_target, idc.get_item_size(call_target)), call_target)
if '[esp],' in insn and ('add' in insn or 'sub' in insn) and idc.get_operand_type(call_target, 1) == o_imm:
idc.create_insn(call_target + idc.get_item_size(call_target))
if idc.print_insn_mnem(call_target + idc.get_item_size(call_target)) == 'retn':
return (call_insn_addr + 5 + (1 if idc.print_insn_mnem(call_target) == 'add' else -1) * idc.get_operand_value(call_target, 1)) & 0xFFFFFFFF
return None


def check_jcc_to_jmp(jcc_insn_addr):
code1 = ida_bytes.get_bytes(jcc_insn_addr, idc.get_item_size(jcc_insn_addr))
next_insn_addr = jcc_insn_addr + idc.get_item_size(jcc_insn_addr)
idc.create_insn(next_insn_addr)
code2 = ida_bytes.get_bytes(next_insn_addr, idc.get_item_size(next_insn_addr))
if abs(code1[0] - code2[0]) == 1 and min(code1[0], code2[0]) % 2 == 0 and idc.get_operand_value(jcc_insn_addr, 0) == idc.get_operand_value(next_insn_addr, 0):
return idc.get_operand_value(jcc_insn_addr, 0)
code = ida_bytes.get_bytes(jcc_insn_addr, 12)

print("bbbbb")
pattern_list = [
re.compile(rb"(?s)\x7C\x03\xEB\x03.\x74\xFB"),
re.compile(rb"(?s)\xEB\x07.\xEB\x01.\xEB\x04.\xEB\xF8."),
re.compile(rb"(?s)\xEB\x01.")
]

for pattern in pattern_list:
match = re.match(pattern, code)
if match != None and match.span()[1] != 0:
return jcc_insn_addr + match.span()[1]

return None


st_mnem_map = {'clc': ['jnb'], 'stc': ['jb']}


def check_st_to_jmp(st_insn_addr):
st_mnem = idc.print_insn_mnem(st_insn_addr)
next_insn_addr = st_insn_addr + idc.get_item_size(st_insn_addr)
idc.create_insn(next_insn_addr)
if idc.print_insn_mnem(next_insn_addr) in st_mnem_map[st_mnem]:
return idc.get_operand_value(next_insn_addr, 0)
return None


start_ea = 0x401000
end_ea = 0x4B9CD0

ea = start_ea
while ea < end_ea:
print("aaa: " + hex(ea))
for i in range(ea, ea + 0x10): idc.del_items(i)
if idc.create_insn(ea) == 0:
# idc.patch_byte(ea, 0x90)
ea += 1
continue
mnem = idc.print_insn_mnem(ea)
if mnem == 'call':
jmp_target = check_call_to_jmp(ea)
if jmp_target != None:
assert jmp_target > ea
print("call: " + hex(ea))
print("jmp target: " + hex(jmp_target))
if jmp_target > ea and abs(jmp_target - ea) <= 0x80:
ida_bytes.patch_bytes(ea, b"\x90" * (jmp_target - ea))
ea = jmp_target
else:
code = asm('jmp 0x%x' % (jmp_target), ea)
ida_bytes.patch_bytes(ea, code)
ea += len(code)
continue
elif mnem[0] == 'j':
jmp_target = check_jcc_to_jmp(ea)
if jmp_target != None:
print("jcc: " + hex(ea))
assert jmp_target > ea
if jmp_target > ea and abs(jmp_target - ea) <= 0x80:
ida_bytes.patch_bytes(ea, b"\x90" * (jmp_target - ea))
ea = jmp_target
else:
code = asm('jmp 0x%x' % (jmp_target), ea)
ida_bytes.patch_bytes(ea, code)
ea += len(code)
continue
elif mnem in st_mnem_map:
jmp_target = check_st_to_jmp(ea)
if jmp_target != None:
print("st: " + hex(ea))
assert jmp_target > ea
if jmp_target > ea and abs(jmp_target - ea) <= 0x80:
ida_bytes.patch_bytes(ea, b"\x90" * (jmp_target - ea))
ea = jmp_target
else:
code = asm('jmp 0x%x' % (jmp_target), ea)
ida_bytes.patch_bytes(ea, code)
ea += len(code)
continue

ea += idc.get_item_size(ea)

for _ in range(start_ea, end_ea):
idc.del_items(_)

idc.jumpto(0x004B8DE4)

print("finish")

常用脚本

全局符号错误修复

c535ebdc-9440-4019-8bbe-580daa4842bf

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

data_seg = range(ida_segment.get_segm_by_name(".data").start_ea, ida_segment.get_segm_by_name(".data").end_ea)
rodata_seg = range(ida_segment.get_segm_by_name(".rodata").start_ea, ida_segment.get_segm_by_name(".rodata").end_ea)
text_seg = range(ida_segment.get_segm_by_name(".text").start_ea, ida_segment.get_segm_by_name(".text").end_ea)
got_plt_base = ida_segment.get_segm_by_name(".got.plt").start_ea

start_ea = text_seg.start
end_ea = text_seg.stop

ea = start_ea
while ea < end_ea:
if idc.print_insn_mnem(ea) == 'lea':
sym_addr = ctypes.c_int64(idc.get_operand_value(ea, 1)).value + got_plt_base
if sym_addr in data_seg or sym_addr in rodata_seg or sym_addr in text_seg:
idc.op_plain_offset(ea, 1, got_plt_base)
print("%08x: %s" % (ea, idc.generate_disasm_line(ea, 0)))
ea = idc.next_head(ea)
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
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
# -*- coding: utf-8 -*-
#
# IDA 9.2 IDAPython
# Fix bogus symbolic displacements like:
# .text:... cmp word ptr ds:loc_1EE6E[rbx], 0
# into:
# .text:... cmp word ptr [rbx+126574], 0
#
# Goal: stop Hex-Rays from producing patterns like:
# *(T *)((char *)&loc_xxx + a1)
#
import re
import ctypes

import idc
import ida_bytes
import ida_segment
import ida_name
import ida_ua
import ida_kernwin
import ida_ida

# 只修复这些段里的“伪符号位移”
BAD_REF_SEGS = {".text"}

# 默认排除 jump table / switch table 名字(避免误伤)
EXCLUDE_PREFIXES = (
"jpt_", "jpt.", "switch_", "case_", "jmp_tbl_", "jt_"
)

# 常见自动名形式兜底(即使 flags 不可靠也能识别)
AUTO_NAME_RE = re.compile(r"^(loc|sub|unk|byte|word|dword|qword|off|def|algn)_[0-9A-Fa-f]+$")

# IDA 的最大操作数个数:IDA 9.x 在 ida_ida 里
UA_MAXOP = getattr(ida_ida, "UA_MAXOP", 8)


def is_autoname(ea: int, name: str) -> bool:
"""
优先用 name flags 判断是否自动生成(SN_AUTO)
取不到就用正则兜底匹配 loc_/dword_/unk_...
"""
if not name:
return False

try:
nflags = ida_name.get_name_flags(ea)
if nflags != 0 and (nflags & ida_name.SN_AUTO) != 0:
return True
except Exception:
pass

return AUTO_NAME_RE.match(name) is not None


def should_fix_displ(ea: int, opnum: int) -> bool:
# 只处理 [reg + disp] / [reg + index*scale + disp] 这类 displacement
if idc.get_operand_type(ea, opnum) != ida_ua.o_displ:
return False

op_txt = (idc.print_operand(ea, opnum) or "")
low = op_txt.lower()

# RIP-relative 是 x64 正常全局访问,不动
if "rip" in low:
return False

# displacement 值(对 64bit 做 signed 处理)
disp = ctypes.c_int64(idc.get_operand_value(ea, opnum)).value

# 负位移(常见栈/帧/反向偏移)不处理
if disp < 0:
return False

# 如果这个“位移值”刚好落到 .text 内,且被自动命名了,通常就是误判成符号
segname = idc.get_segm_name(disp)
if not segname or segname not in BAD_REF_SEGS:
return False

name = idc.get_name(disp, idc.GN_VISIBLE) or ""
if not name:
return False

# 排除 jump table 等(可按需删掉)
for pfx in EXCLUDE_PREFIXES:
if name.startswith(pfx):
return False

# 只动自动生成名,降低误伤
if not is_autoname(disp, name):
return False

# 确保操作数字符串里确实用了该名字
if name not in op_txt:
return False

return True


def fix_one(ea: int, opnum: int) -> None:
# 清掉该操作数的所有“表示形式”(offset/enum/stroff/...),回到未定义状态
ida_bytes.clr_op_type(ea, opnum)

# 强制显示为十进制数字(你举例就是 [rbx+126574])
ida_bytes.op_dec(ea, opnum)


def main() -> None:
text = ida_segment.get_segm_by_name(".text")
if not text:
print("[!] .text segment not found.")
return

start_ea, end_ea = text.start_ea, text.end_ea
ea = start_ea
patched = 0

while ea != idc.BADADDR and ea < end_ea:
if ida_bytes.is_code(ida_bytes.get_full_flags(ea)):
# 遍历操作数
for opnum in range(UA_MAXOP):
optype = idc.get_operand_type(ea, opnum)
if optype == ida_ua.o_void:
break

if should_fix_displ(ea, opnum):
before = idc.generate_disasm_line(ea, 0) or ""
fix_one(ea, opnum)
after = idc.generate_disasm_line(ea, 0) or ""

if before != after:
patched += 1
print(f"{ea:08X}: {after}")

ea = idc.next_head(ea, end_ea)

ida_kernwin.refresh_idaview_anyway()
print(f"[+] Done. Patched {patched} operands.")


if __name__ == "__main__":
main()

修复结构体偏移识别

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
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
# -*- coding: utf-8 -*-
#
# IDA 9.2 IDAPython
# Fix bogus symbolic displacements like:
# .text:... cmp word ptr ds:loc_1EE6E[rbx], 0
# into:
# .text:... cmp word ptr [rbx+126574], 0
#
# Goal: stop Hex-Rays from producing patterns like:
# *(T *)((char *)&loc_xxx + a1)
#
import re
import ctypes

import idc
import ida_bytes
import ida_segment
import ida_name
import ida_ua
import ida_kernwin
import ida_ida

# 只修复这些段里的“伪符号位移”
BAD_REF_SEGS = {".text"}

# 默认排除 jump table / switch table 名字(避免误伤)
EXCLUDE_PREFIXES = (
"jpt_", "jpt.", "switch_", "case_", "jmp_tbl_", "jt_"
)

# 常见自动名形式兜底(即使 flags 不可靠也能识别)
AUTO_NAME_RE = re.compile(r"^(loc|sub|unk|byte|word|dword|qword|off|def|algn)_[0-9A-Fa-f]+$")

# IDA 的最大操作数个数:IDA 9.x 在 ida_ida 里
UA_MAXOP = getattr(ida_ida, "UA_MAXOP", 8)


def is_autoname(ea: int, name: str) -> bool:
"""
优先用 name flags 判断是否自动生成(SN_AUTO)
取不到就用正则兜底匹配 loc_/dword_/unk_...
"""
if not name:
return False

try:
nflags = ida_name.get_name_flags(ea)
if nflags != 0 and (nflags & ida_name.SN_AUTO) != 0:
return True
except Exception:
pass

return AUTO_NAME_RE.match(name) is not None


def should_fix_displ(ea: int, opnum: int) -> bool:
# 只处理 [reg + disp] / [reg + index*scale + disp] 这类 displacement
if idc.get_operand_type(ea, opnum) != ida_ua.o_displ:
return False

op_txt = (idc.print_operand(ea, opnum) or "")
low = op_txt.lower()

# RIP-relative 是 x64 正常全局访问,不动
if "rip" in low:
return False

# displacement 值(对 64bit 做 signed 处理)
disp = ctypes.c_int64(idc.get_operand_value(ea, opnum)).value

# 负位移(常见栈/帧/反向偏移)不处理
if disp < 0:
return False

# 如果这个“位移值”刚好落到 .text 内,且被自动命名了,通常就是误判成符号
segname = idc.get_segm_name(disp)
if not segname or segname not in BAD_REF_SEGS:
return False

name = idc.get_name(disp, idc.GN_VISIBLE) or ""
if not name:
return False

# 排除 jump table 等(可按需删掉)
for pfx in EXCLUDE_PREFIXES:
if name.startswith(pfx):
return False

# 只动自动生成名,降低误伤
if not is_autoname(disp, name):
return False

# 确保操作数字符串里确实用了该名字
if name not in op_txt:
return False

return True


def fix_one(ea: int, opnum: int) -> None:
# 清掉该操作数的所有“表示形式”(offset/enum/stroff/...),回到未定义状态
ida_bytes.clr_op_type(ea, opnum)

# 强制显示为十进制数字(你举例就是 [rbx+126574])
ida_bytes.op_dec(ea, opnum)


def main() -> None:
text = ida_segment.get_segm_by_name(".text")
if not text:
print("[!] .text segment not found.")
return

start_ea, end_ea = text.start_ea, text.end_ea
ea = start_ea
patched = 0

while ea != idc.BADADDR and ea < end_ea:
if ida_bytes.is_code(ida_bytes.get_full_flags(ea)):
# 遍历操作数
for opnum in range(UA_MAXOP):
optype = idc.get_operand_type(ea, opnum)
if optype == ida_ua.o_void:
break

if should_fix_displ(ea, opnum):
before = idc.generate_disasm_line(ea, 0) or ""
fix_one(ea, opnum)
after = idc.generate_disasm_line(ea, 0) or ""

if before != after:
patched += 1
print(f"{ea:08X}: {after}")

ea = idc.next_head(ea, end_ea)

ida_kernwin.refresh_idaview_anyway()
print(f"[+] Done. Patched {patched} operands.")


if __name__ == "__main__":
main()

修复函数参数

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
      
import idaapi
import idautils
import idc

for func_ea in idautils.Functions():
tif = idaapi.tinfo_t()
if not idaapi.get_tinfo(tif, func_ea):
continue # 无法获取类型信息,跳过

func_type = idaapi.func_type_data_t()
if not tif.get_func_details(func_type):
continue # 无法获取函数详细信息,跳过

# 统计 double 类型参数的数量
double_param_count = 0
for arg in func_type:
if arg.type.is_double():
double_param_count += 1

if double_param_count <= 4:
continue # 如果 double 参数数量不超过 4 个,跳过

# 创建新的函数类型数据,去除 double 参数
new_func_type = idaapi.func_type_data_t()
new_func_type.cc = func_type.cc
new_func_type.rettype = func_type.rettype

for arg in func_type:
if not arg.type.is_double():
new_func_type.push_back(arg)

# 应用新的函数类型信息
new_tif = idaapi.tinfo_t()
new_tif.create_func(new_func_type)
idaapi.apply_tinfo(func_ea, new_tif, idaapi.TINFO_DEFINITE)
print(f"已更新函数 {idc.get_func_name(func_ea)} 的原型,删除了 {double_param_count} 个 double 参数。")
  • Title: IDA Python 使用总结
  • Author: sky123
  • Created at : 2024-11-11 12:51:12
  • Updated at : 2026-02-02 22:28:21
  • Link: https://skyi23.github.io/2024/11/11/IDA Python 使用总结/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments