import idaapi import idc def get_basic_block(ea)...

Created on: February 20, 2026

Answered using GPT-5.2 Pro by Chat01

Question

import idaapi
import idc

def get_basic_block(ea):
func = idaapi.get_func(ea)
if not func:
return None
f = idaapi.FlowChart(func) # 获取函数的控制流图
for block in f:
if block.start_ea <= ea < block.end_ea:
return block
return None
def get_block_size(block):
return block.end_ea - block.start_ea

def find_all_real_blocks(fun_ea):

text
real_blocks_has_branch = [] real_blocks_has_no_branch = [] real_blocks_has_no_branch_but_has_pred = [] blocks = idaapi.FlowChart(idaapi.get_func(fun_ea)) loop_head_addr= 0x000000140008983 loop_head_block = get_basic_block(loop_head_addr) blocks = list(loop_head_block.preds()) for block in blocks: size = get_block_size(block) if size != 25 and size != 20: start_ea = block.start_ea if start_ea != 5368715678: preds = list(block.preds()) if len(preds) == 0: real_blocks_has_no_branch.append(start_ea) if len(preds) == 1: pred = preds[0] real_blocks_has_branch.append(pred.start_ea) if len(preds) == 2: for pred in preds: preds_1 = list(pred.preds()) pred_1 = preds_1[0] real_blocks_has_no_branch.append(pred_1.start_ea) real_blocks_has_no_branch_but_has_pred.append(pred_1.start_ea) real_blocks_has_branch = list(set(real_blocks_has_branch)) real_blocks_has_no_branch = list(set(real_blocks_has_no_branch)) real_blocks_has_no_branch_but_has_pred = list(set(real_blocks_has_no_branch_but_has_pred)) real_blocks = real_blocks_has_branch + real_blocks_has_no_branch print("所有有跳转真实块地址真实块地址:", [hex(x) for x in real_blocks_has_branch]) print("所有无跳转真实块地址真实块地址:", [hex(x) for x in real_blocks_has_no_branch]) print("所有真实块地址真实块地址:", [hex(x) for x in real_blocks]) print("所有无跳转但有前驱真实块地址真实块地址:", [hex(x) for x in real_blocks_has_no_branch_but_has_pred]) print('-----------------------------------------------------------------------------------------') print("所有有跳转真实块地址真实块地址:",real_blocks_has_branch) print("所有无跳转真实块地址真实块地址:", real_blocks_has_no_branch) print("所有真实块地址真实块地址:", real_blocks) print("所有无跳转但有前驱真实块地址真实块地址:", real_blocks_has_no_branch_but_has_pred)

find_all_real_blocks(0x1400016D0)

jmp_table = {
0x1400065df:0x14000661F,
0x140006d53:0x140006D93,
0x140006ad7:0x140006B17,
0x14000685b:0x14000689B,
}

分析这个IDA脚本,教我它的原理,我之前并不会写IDApython脚本,但我会用python,讲解完后可以告诉我写IDApython脚本的关键以及它与python的区别吗?写IDApython脚本的共同思路是什么?

Thought

Analyzing script logic

Examining the logic of the IDAPython script, I’m noting the use of idapi.get_func, FlowChart, basic blocks, preds, start_ea, end_ea, and the script's intention to differentiate real blocks from dispatcher blocks.

Assessing block types

I'm thinking through the script's logic, categorizing blocks based on predecessors and sizes. Blocks with one predecessor are often conditional, targeting specific branches.

Distinguishing block roles

I'm mapping out the script's approach to distinguish between "real" and "fake" blocks, filtering out fixed-size stubs and noting that larger "real" blocks with one predecessor are included.

Tracing block influences

OK, let me see. I’m identifying real blocks by examining predecessor addresses, filtering out specific small-sized blocks, and tracing blocks back to deduce their origins and connections.

Classifying control flow

I’m detailing how to identify and classify basic blocks, considering predecessors, and focusing on "real" branch blocks based on their predecessor relationships.

Identifying relevant blocks

I'm mapping out how the script identifies loop head predecessors, filtering out state-setting stubs to focus on larger, real blocks that typically end with an unconditional jump to the loop head.

Examining branch targets

I'm identifying loop head predecessors, filtering out 20/25 byte state-setting stubs, and focusing on larger blocks that set state and jump to the dispatcher, even if they lack a conditional branch.

Analyzing block relationships

I’m digging into how the script evaluates loop head predecessors, filtering out state-setting stubs, and pinpointing real blocks with zero predecessors, typically from indirect jumps within the same function.

Tracking predecessor blocks

Examining loop head predecessors, filtering out stubs, and identifying real blocks with zero predecessors, often due to indirect jumps. This allows pinpointing key code regions without filtering noise.

Identifying conditional blocks

I’m piecing together how the script targets loop head predecessors, excluding 25/20 byte blocks. By tracking predecessors of real blocks, it successfully captures conditional branch blocks for accurate analysis.

Breaking down the script

I’m detailing how the IDAPython script identifies loop head predecessors, filters smaller blocks, and analyzes conditional branches using a graph traversal technique with heuristics. I also discuss potential limitations and issues.

Navigating IDAPython intricacies

I’m piecing together key IDA modules, differentiating IDAPython from regular Python, and outlining the thought process for script enhancement. Points include IDA modules, IDs, database operations, and threading.

Deepening IDAPython insights

I’m exploring key IDA modules, distinctions between IDAPython and Python, threading, and the graph traversal process. Notably, conditional branches and potential limitations are highlighted.

Enhancing script features

I’m noting script updates: automatic loop head identification, refining stub size calculations, robust error handling, and generalizing the script to handle functions, segments, and instructions more effectively.

Tools and concepts

I'm working through IDAPython modules, focusing on ida_funcs, ida_gdl, and idaapi. Additionally, discussing FlowChart's alias with ida_gdl and addressing distinctions in IDAPython usage for completeness.

Thought for 9m 37s

Answer

下面我按“IDA 里这些概念是什么 → 这段脚本逐行在干什么 → 它隐含的假设/用途 → 写 IDAPython 的关键点、与普通 Python 的区别 → 常见写脚本的通用思路”来讲。


1) 先把 IDA 里几个核心概念讲清楚

ea 是什么?

  • ea = effective address(有效地址),就是 IDA 数据库里的一个地址(整数)。
  • 在 Python 里它只是 int,但语义上代表“程序里的某个位置”。

func / basic block / CFG 是什么?

  • idaapi.get_func(ea):给你一个 func_t(函数对象),表示“ea 所在的函数”。
  • idaapi.FlowChart(func):基于函数生成控制流图(CFG)视图,里面的节点就是基本块(BasicBlock)
  • 基本块通常是:一段连续的指令序列,只在入口进入,只在末尾(分支/跳转/返回)离开。

BasicBlock 常用属性/方法

  • block.start_ea:块起始地址(包含)
  • block.end_ea:块结束地址(不包含,类似 Python 的切片右边界)
  • block.preds():所有“跳到当前块”的前驱块(incoming edges)
  • block.succs():所有“从当前块跳出去”的后继块(outgoing edges)

你这段脚本几乎全部在用 preds(),也就是在 CFG 上“反向走”。


2) 逐段分析你的脚本在做什么

2.1 get_basic_block(ea)

python
def get_basic_block(ea): func = idaapi.get_func(ea) if not func: return None f = idaapi.FlowChart(func) for block in f: if block.start_ea <= ea < block.end_ea: return block return None

原理:

  1. 找出 ea 属于哪个函数 func
  2. 为这个函数建 CFG(FlowChart)
  3. 遍历 CFG 中所有基本块,找到那个区间覆盖 ea 的块并返回

这个函数就是 “地址 → 所在 basic block” 的映射。


2.2 get_block_size(block)

python
def get_block_size(block): return block.end_ea - block.start_ea

这是 “基本块大小 = 地址跨度”。注意这不是“指令条数”,是“字节跨度”。


2.3 find_all_real_blocks(fun_ea) 的总体目的

先看整体结构:

python
def find_all_real_blocks(fun_ea): real_blocks_has_branch = [] real_blocks_has_no_branch = [] real_blocks_has_no_branch_but_has_pred = [] blocks = idaapi.FlowChart(idaapi.get_func(fun_ea)) loop_head_addr= 0x000000140008983 loop_head_block = get_basic_block(loop_head_addr) blocks = list(loop_head_block.preds()) for block in blocks: ...

这里有一个非常关键的点:

  • 你一开始写了 blocks = FlowChart(get_func(fun_ea))但下一行就被覆盖了
    • blocks = list(loop_head_block.preds())

所以最后循环的 blocks 并不是“整个函数的所有基本块”,而是:

“loop_head_block 的所有前驱块”(所有跳进 loop_head_block 的块)

也就是说:脚本核心是围绕一个固定地址 loop_head_addr 对应的块(你命名为 loop head)来做反向追踪。


3) 这段脚本的核心逻辑:从 loop head 的前驱里“回溯找真实块”

3.1 先解释一下几个“魔数”

脚本里有几处硬编码:

  • loop_head_addr = 0x140008983:你认为这是“循环头/调度块/汇合块”的地址(dispatcher / loop head)
  • size != 25 and size != 20:过滤掉大小为 20 或 25 字节的块
  • start_ea != 5368715678:跳过一个特定块

你写的是十进制 5368715678,它等价于:

  • 0x14000199e

所以这行:

python
if start_ea != 5368715678:

就是 “如果块起始地址不是 0x14000199e 才处理”。

这种“过滤固定大小块 + 固定地址跳过”的模式,非常像在处理**控制流平坦化(control-flow flattening)**或类似混淆:调度块和过渡块往往结构固定、大小相近。


3.2 主循环逐个分析

你现在循环的是:loop_head_block 的所有前驱块 block

python
for block in blocks: size = get_block_size(block) if size != 25 and size != 20: start_ea = block.start_ea if start_ea != 5368715678: preds = list(block.preds()) if len(preds) == 0: real_blocks_has_no_branch.append(start_ea) if len(preds) == 1: pred = preds[0] real_blocks_has_branch.append(pred.start_ea) if len(preds) == 2: for pred in preds: preds_1 = list(pred.preds()) pred_1 = preds_1[0] real_blocks_has_no_branch.append(pred_1.start_ea) real_blocks_has_no_branch_but_has_pred.append(pred_1.start_ea)

我们拆成三层“反向走图”的含义:

(A) 第一层:block 是 loop head 的前驱

  • 也就是 CFG 里有边:block -> loop_head_block

(B) 第二层:preds = block.preds()block 的前驱

  • 也就是 CFG 里边:pred -> block -> loop_head

(C) 第三层:当 len(preds)==2 时,又取 pred.preds() 的第一个

  • 也就是:pred_1 -> pred -> block -> loop_head

这意味着:脚本想从“跳到 loop head 的那些块(可能是过渡块/落脚块)”往回追一层或两层,去找你认为的“real block(真实块)”。


4) 它为什么要按 preds 数量分三种情况?

先注意:你用的是 preds()(前驱数),不是 succs()(后继数)。

所以变量名里 “has_branch / no_branch” 从严格定义上并不准确:

  • 分支(branch)通常看 succs 数量(一个块跳向几个后继)
  • 你这里是在看有几个块能跳进来(incoming edges)

但从“混淆/扁平化”的经验来看,你这样分可能是在表达:

情况 1:len(preds) == 0

你把 block.start_ea 认为是“真实块”。

这通常只会发生在:

  • 函数入口块(entry)或
  • 因为某些边没被 IDA 建出来 / 你只取了 loop head 的某个局部子图,导致看起来没有前驱

情况 2:len(preds) == 1

你认为 block 本身可能是“过渡块/壳”,真正的逻辑块是它的唯一前驱 pred

python
real_blocks_has_branch.append(pred.start_ea)

这非常像:

“有个块只是做了状态设置/跳转,然后跳进 loop head;真正的逻辑在它前面那个块。”

情况 3:len(preds) == 2

你又往回追了一层(取每个 pred 的第一个 pred):

python
pred_1 = preds_1[0] real_blocks_has_no_branch.append(pred_1.start_ea)

这像是在处理:

“进入这个过渡块的路径有两条(比如 if/else 两路汇合到同一个过渡块),但真正的逻辑块在更前面。”

不过这里有两个脚本层面的明显假设/风险:

  1. preds_1[0] 随便取第一个前驱可能不稳定

    • pred.preds() 可能是 1 个、2 个甚至更多
    • 你没有判断 preds_1 是否为空,也没有处理多个前驱
  2. len(preds)==2 就一定要再退一层”的逻辑是经验型的

    • 很可能只对某一类特定混淆/编译模式有效

5) 输出结果在表达什么?

python
real_blocks_has_branch = list(set(real_blocks_has_branch)) real_blocks_has_no_branch = list(set(real_blocks_has_no_branch)) real_blocks_has_no_branch_but_has_pred = list(set(real_blocks_has_no_branch_but_has_pred)) real_blocks = real_blocks_has_branch + real_blocks_has_no_branch
  • real_blocks_has_branch:你认为“真实块”的地址集合(来自 len(preds)==1 分支:取它的前驱)
  • real_blocks_has_no_branch:另一批你认为“真实块”的地址(来自 len(preds)==0len(preds)==2 再退一层)
  • real_blocks_has_no_branch_but_has_pred:其实是 len(preds)==2 那部分的子集(重复记录了一份)

然后打印 hex 和 decimal 两种形式。


6) jmp_table 这段字典可能是干什么的?

python
jmp_table = { 0x1400065df:0x14000661F, 0x140006d53:0x140006D93, 0x140006ad7:0x140006B17, 0x14000685b:0x14000689B, }

它在你给出的代码里没有被使用,但从格式上看非常像:

  • “某个 jmp 指令地址 → 它真正跳到的目标地址”
  • 或者 “某些 thunk/跳板/间接跳转点 → 真实落点”

在控制流修复、手工补 CFG 边、或进行 patch/重定位时很常见。

比如后续可能会用它:

  • idaapi.add_cref(src, dst, fl_JN) 添加控制流引用
  • 或者做图修复、导出真实边关系

7) 这段脚本的“原理总结”(一句话版)

它做的事情可以概括为:

loop_head_addr 所在基本块为中心,拿到所有跳入该块的前驱块;再基于这些前驱块的“前驱数量”和块大小等特征,向后回溯一到两层,试图抽取你认为的“真实基本块起始地址集合”。

这基本就是一种基于 CFG 结构特征的反混淆/筛块脚本思路。


8) 这脚本有哪些“写 IDAPython 时很典型”的点(也是你入门要抓的关键)

关键 1:你不是在处理“普通数据”,你在处理 IDA 的“数据库对象”

  • func_tBasicBlockinsn_t 等对象都来自 IDA SDK 的包装
  • 它们依赖 IDA 的分析结果(函数边界、基本块划分、交叉引用…)

关键 2:控制流分析的核心就是两件事:图节点和边

  • 节点:basic blocks(FlowChart
  • 边:preds() / succs()
  • 一切“找真实块 / 找入口 / 找分支 / 找 case”本质都是在图上走

关键 3:IDAPython 脚本常常是“经验规则 + 过滤器”

像你这段:

  • 过滤固定大小块(20、25)
  • 跳过固定地址块(0x14000199e)
  • 用前驱数决定回溯深度

这就是典型的“在某个样本/某类混淆上有效”的规则脚本。


9) IDAPython 和你平常写 Python 的区别是什么?

你可以把它理解成:Python 语法没变,但你操作的世界变了。

(1) 运行环境不同

  • 普通 Python:你控制输入数据和程序流程
  • IDAPython:你的“数据源”是 IDA 当前打开的 IDB(反汇编/分析结果)

(2) API 是“反汇编数据库 API”,会有副作用

很多操作不是纯函数,会改 IDB:

  • 改名字 ida_name.set_name
  • 加注释 idc.set_cmt
  • 创建指令、修复代码 idc.create_insn
  • patch 字节 ida_bytes.patch_byte

所以脚本要更谨慎:你是在“改工程”。

(3) 很多信息依赖 IDA 的分析状态

  • 如果函数没识别出来:get_func 返回 None
  • CFG 边可能缺失(间接跳转、异常处理、混淆)
  • 你看到的 preds/succs 可能并不完整

(4) 类型/常量习惯不同

你会频繁看到:

  • idaapi.BADADDR
  • None
  • ea/start_ea/end_ea 这样的地址字段 而且经常需要 hex(ea) 来读输出。

(5) 模块划分不同:idc / idaapi / idautils

(入门最重要的三件套)

  • idc:偏脚本层、快捷函数(取反汇编文本、操作注释、取操作数等)
  • idaapi:更底层、更接近 SDK(FlowChart、insn 解码、处理器相关)
  • idautils:各种迭代器(遍历函数、遍历指令、遍历 xref)

经验上:
遍历用 idautils,结构对象用 idaapi,文本/快捷操作用 idc


10) 写 IDAPython 脚本的共同思路(通用套路)

不管你写的是找块、找字符串、找调用链、批量改名,通用流程基本都是:

Step 1:确定范围(scope)

  • 当前地址?
  • 当前函数?
  • 某个段?
  • 全程序?

Step 2:把范围映射成可遍历对象

  • 函数:idaapi.get_func(ea)
  • CFG:idaapi.FlowChart(func)
  • 指令序列:idautils.FuncItems(func.start_ea)idautils.Heads(start, end)
  • xref:idautils.XrefsTo(ea) / XrefsFrom(ea)

Step 3:提取特征(feature)

  • 块大小、前驱/后继数量
  • 指令 mnemonic/operand
  • 常量、字符串引用
  • 调用目标、跳转目标

Step 4:筛选/聚类/回溯(你的脚本属于这一类)

  • 用 set 去重
  • 用图遍历(BFS/DFS)
  • 反向追踪(preds)或正向走(succs)
  • 用规则过滤“噪音节点”

Step 5:输出或写回 IDB

  • print() 输出
  • idc.set_cmt 写注释
  • ida_name.set_name 批量重命名
  • 导出到文件(json/csv)

11) 给你一个“更稳的写法思路”(把你脚本的想法抽象成模板)

你现在的脚本硬编码、且 preds_1[0] 有崩溃风险。更通用的写法通常是:

  • 把“什么算 stub/过渡块”定义成一个函数 is_stub(block)
  • 然后从 loop head 的 preds 出发,反向 BFS/DFS,一直走到遇到非 stub 的块,就当作 real block 收集

伪代码思路像这样(你不用照抄,理解思路就行):

python
def is_stub(block): size = block.end_ea - block.start_ea return size in {20, 25} or block.start_ea == 0x14000199e def collect_real_blocks(loop_head_ea): head = get_basic_block(loop_head_ea) work = list(head.preds()) seen = set() real = set() while work: b = work.pop() if b.start_ea in seen: continue seen.add(b.start_ea) if is_stub(b): # stub:继续往回追前驱 for p in b.preds(): work.append(p) else: real.add(b.start_ea) return real

这比“根据 preds 数量决定回溯 1 层还是 2 层”更鲁棒,也更符合“图遍历”的通用方式。


如果你愿意的话,你可以把你这个函数里 0x140008983 为什么是 loop head(比如它长什么样、末尾是什么跳转、是不是 switch/dispatcher)描述一下,或者把该块反汇编贴几行,我就能更精确地解释为什么你过滤 20/25 字节块、以及 jmp_table 在你的整体流程里最可能怎么用(例如修 CFG 或导出真实边)。但就算不补充信息,上面这些就是它的核心原理与 IDAPython 的通用写法思路。

Question

是的,这就是对抗控制流平坦化的脚本,我现在想问,idapython脚本接收的信息源于ida的分析,这个脚本中只是进一步分析,有没有改变ida判断的功能呢,你帮我总结一些idapython脚本经常关注,调用的分析数据,比如这里的block.start_ea。或者说,这个脚本是基于一个函数来分析的,对于一个经过了ida分析的函数,有哪些ida分析的重要数据呢

Thought

Identifying key elements

I’m helping identify crucial analysis data for functions in IDA, focusing on boundaries, instructions, references, and stack analysis. Limitations due to obfuscation are also highlighted.

Demonstrating analysis tools

I’m planning to access address, function, block, and decompiler-level data in IDAPython, with examples on modifying FlowChart and adding crefs for unreachable blocks.

Understanding analysis tools

I’m compiling important analysis tools for IDAPython, including accessing function boundaries, xrefs, switch tables, and stack frames. I’m also providing Python code snippets for practical examples.

Reasoned for 6m 36s

Answer

你理解得很对:IDAPython 脚本“读到的信息”主要来自 IDA 已经做过的 auto-analysis(以及你手工改过的 IDB 状态)。但同时也要明确两点:

  1. 脚本可以只做“进一步分析”(只读 IDB)
  2. 脚本也可以“改变 IDA 的判断/结果”(写回 IDB,让 IDA 重新分析或按你的覆盖结果展示)

你这段脚本属于第 1 类:只读 + 打印结果,它不会改变 IDA 的分析结论。

下面按你问的两个方向展开:


1) 你这段脚本有没有“改变 IDA 判断”的功能?

**没有。**原因很简单:它只调用了读接口、做集合处理、最后 print()

它用到的主要读接口:

  • idaapi.get_func(ea):读“函数对象”
  • idaapi.FlowChart(func):读“IDA 为该函数建立的 CFG / 基本块”
  • block.start_ea / end_ea:读基本块边界
  • block.preds():读 CFG 边(前驱集合)

这些都不会改 IDB。

那“改变 IDA 判断/结果”的脚本一般长什么样?

只要你调用了这些“写接口”,就属于会改变 IDA 状态(从轻到重):

A. 改显示但不太改变分析(“表现层”)

  • 改名字:ida_name.set_name(ea, "name")
  • 加注释:idc.set_cmt(ea, "comment", 0)
  • 设颜色、书签等(对分析影响不大)

这类通常不会改变 CFG/函数边界,但会改变你看到的内容与后续脚本读取到的“名字/注释”。

B. 改 IDA 对“这里是代码还是数据”的判断(会连锁影响分析)

  • 强制这里是指令:idc.create_insn(ea) / ida_bytes.del_items(ea, ...) 后再 create
  • 强制这里是数据:idc.create_data(ea, ...)
  • 清理并重建一段区域的分析结果

这会直接影响:基本块划分、xrefs、函数识别等。

C. 改控制流边 / xrefs(对 CFG 特别关键)

  • 增加 code xref(控制流引用):idaapi.add_cref(frm, to, type)
  • 增加 data xref:idaapi.add_dref(frm, to, type)

在对抗控制流平坦化时,这类非常有用:
你补上间接跳转的真实落点引用,FlowChart 才可能“看见”正确的边。

D. 改函数边界/函数本体(影响非常大)

  • 新建/删除函数:ida_funcs.add_func(start, end) / ida_funcs.del_func(start)
  • 调整函数范围、chunks(尾块)
  • 设函数属性(如 noreturn)

这会影响整个函数级分析、CFG、调用图等。

E. 改类型系统(会影响反汇编展示、反编译质量)

  • 应用函数原型/类型:ida_typeinf.apply_tinfo(...)
  • 结构体、枚举、成员类型等

2) IDAPython 脚本“经常关注/调用”的分析数据有哪些?

你可以把 IDA 的分析结果想成一套“数据库事实”——脚本就是在读这些事实。常见关注点可以按层次分:


2.1 地址/单元层(任何脚本都会碰到)

核心问题:某个 ea 上,IDA 认为它是什么?

常见数据:

  • 这里是否是 code/data/unknown(flags)
  • 这里是否是一条指令头(head)
  • 指令长度、字节序列
  • 是否属于某个段(segment)、权限、段名

典型 API(举例):

  • ida_bytes.get_flags(ea) + ida_bytes.is_code/is_data/is_unknown(...)
  • idc.get_bytes(ea, n) / ida_bytes.get_bytes(...)
  • ida_segment.getseg(ea) / idc.get_segm_name(ea)

你现在脚本没直接用 flags,但 FlowChart 的结果依赖 flags:只有被识别为 code 的地方才可能进入 CFG。


2.2 指令层(反混淆脚本很常用)

核心问题:这个地址的指令是什么?操作数是什么?是不是 call/jmp?跳到哪里?

常见分析数据:

  • mnemonic(指令助记符)
  • operand 类型(立即数/寄存器/内存/近跳/远跳…)
  • operand 值(立即数、地址)
  • 反汇编行文本(用于快速规则匹配)
  • SP delta(对栈平衡的估计)

典型 API:

  • idc.print_insn_mnem(ea)
  • idc.get_operand_type(ea, i), idc.get_operand_value(ea, i)
  • idc.generate_disasm_line(ea, 0)(快速拿可读反汇编)
  • 更底层:ida_ua.decode_insn(insn, ea) 得到 insn_t

在平坦化里,很多脚本会识别:

  • dispatcher 的 cmp/jcc 模式
  • state 变量更新(mov/cmov/xor/add 到某个栈变量/全局)
  • jmp reg / jmp [mem] 这类间接跳转(IDA 经常缺边)

2.3 交叉引用层(Xrefs)——IDA 最重要的“分析产物”之一

核心问题:谁引用了谁?控制流/数据流的“边”在哪里?

常见数据:

  • code xref:call/jmp/jcc 形成的控制流引用
  • data xref:指令/数据引用某个地址(字符串、全局变量、表)

典型 API:

  • idautils.XrefsTo(ea) / idautils.XrefsFrom(ea)
  • idautils.CodeRefsTo(ea, flow) / CodeRefsFrom(ea, flow)
  • idautils.DataRefsTo(ea) / DataRefsFrom(ea)

对平坦化很关键的一点
很多间接跳转没有明确 xref → CFG 边会缺失FlowChart 看不全。
所以很多反平坦化脚本会先“补 xref”。


2.4 函数层(你这脚本的基准层)

核心问题:IDA 认定这是一个函数,它的范围/属性/组成是什么?

一个 func_t(函数对象)里你经常会关心:

  1. 函数范围
  • func.start_ea, func.end_ea
    (注意 end_ea 也是右开区间)
  1. 函数块组成(chunks / tails)
  • 很多函数不是连续的,有 tail chunks(尾块)
  • 平坦化/跳转胶水可能造成 chunk 异常
  1. 函数属性 flags
  • 是否 noreturn(不返回)
  • 是否库函数/ thunk
  • 是否有 frame(栈帧)
  1. 栈帧相关估计
  • frame size、argsize 等(不同架构/版本字段略有差异)
  • 这类信息对识别 state 变量位置很有帮助

典型 API:

  • idaapi.get_func(ea) / ida_funcs.get_func(ea)
  • ida_funcs.get_func_name(ea)
  • 遍历函数指令:idautils.FuncItems(func.start_ea)

2.5 基本块 / CFG 层(你这脚本完全属于这一类)

核心问题:基本块怎么划分?块之间怎么连?入口/出口在哪里?

你在脚本里用到的就是典型 CFG 信息:

  • idaapi.FlowChart(func):CFG
  • block.start_ea / block.end_ea:块边界(你脚本的关键数据)
  • block.preds():前驱边集合
  • block.succs():后继边集合(你脚本没用,但反平坦化通常也会用)

另外还有一些脚本常用的“块特征”:

  • 块大小(你用的是 end-start
  • 块末指令(是否 jcc/jmp/call/ret)
  • 块的 out-degree/in-degree(后继/前驱数量)
  • 是否为异常处理相关块(取决于平台/IDA分析)

提醒一点
你脚本里用 “preds 数量” 去叫 “has_branch/no_branch”,从定义上不严谨。一般:

  • len(block.succs()) 更接近“是否有分支”
  • len(block.preds()) 更接近“是否被多路汇入/是否为汇合点”

不过在对抗平坦化时,用 preds 做“回溯层数”的启发式是很常见的。


2.6 Switch / 跳转表层(平坦化经常相关)

因为 dispatcher 很像 switch,IDA 会尝试识别 switch 信息(但混淆常让它失败)。

常见数据:

  • switch_info(case 数、表位置、默认分支)
  • case 的目标地址集合

典型 API(不同 IDA 版本接口略有差异,但思想一样):

  • ida_nalt.get_switch_info(ea) / idaapi.get_switch_info_ex(...)
  • idaapi.calc_switch_cases(...)

你最后的 jmp_table 字典就很像用于“补齐间接跳转目标”的数据源:
后续很可能用它去 add_cref 或者创建 code 到目标地址,从而让 CFG 更完整。


2.7 类型与反编译层(如果你有 Hex-Rays,这层会非常强)

核心问题:函数原型、局部变量、表达式树、microcode

常见数据:

  • 函数原型(参数类型、返回类型、调用约定)
  • 局部变量(lvars)
  • 反编译后的 C tree(ctree)
  • microcode basic blocks(比 FlowChart 更接近语义,有时更利于反混淆)

典型 API:

  • ida_hexrays.decompile(func_ea)cfunc_t
  • 从 cfunc 里读 lvars、body 等

反平坦化/去混淆做得“更自动”的,往往会用这层做 state 变量追踪(比纯汇编规则鲁棒)。


3) 回到你的问题:对一个“IDA 已分析的函数”,最重要的分析数据清单是什么?

如果只给你一份“函数级关键数据清单”,我会按优先级列成这样(也是反平坦化最常用的):

  1. 函数范围与组成
  • start/end
  • chunks/tails(函数是否非连续)
  1. CFG(基本块 + 边)
  • blocks:每个块的 start/end
  • edges:preds/succs
  • 入口块/返回块/异常出口块
  1. 指令语义信息
  • 每条指令的 mnem、operand、立即数/地址
  • 块末指令类型(jmp/jcc/ret/call)
  • 间接跳转点(IDA 常缺边)
  1. Xrefs
  • call graph:谁调用它、它调用谁
  • code refs:跳转边是否齐全
  • data refs:state 变量/常量/表引用
  1. 栈帧与变量信息(有助于找到 state)
  • frame size、sp delta
  • 局部变量位置(尤其 state 变量是否在栈上)
  1. 类型/原型(提高反编译与规则可靠性)
  • 函数原型、参数类型
  • 关键结构体/全局变量类型
  1. switch/jumptable 信息(dispatcher 相关)
  • case 目标集合
  • 默认分支
  • table 地址

4) 一个实用的判断:脚本是在“用 IDA 的判断”,还是在“改 IDA 的判断”?

你可以用一个非常直观的标准:

  • 只读脚本:只调用 get_* / print_* / decode / FlowChart / XrefsTo 这类“读接口”,最后输出或导出文件
  • 改写脚本:出现 set_* / add_* / del_* / create_* / patch_* / apply_* 这类“写接口”,并且通常会导致:
    • IDB 内容变化(名字/注释/类型/边界/字节)
    • auto-analysis 重新跑一部分(很多时候你还会看到脚本里 idaapi.auto_wait()

你这段脚本明显属于第一类。


如果你想把它进一步升级成“既分析又修复 IDA CFG”的版本,最常见的路线就是:
用你推出来的真实边(比如 jmp_table 或从 dispatcher 计算出来的目标)去 add_cref,再让 FlowChart 重新生成,这样后续脚本可以在“更接近真实的 CFG”上工作。

你要是愿意,把你后续打算怎么用 jmp_table(是补边?还是导出真实图?还是做 patch?)说一下,我可以按你目标给你一份更贴近实战的“从只读分析 → 写回修复 CFG → 迭代”的脚本结构模板。

Share this Q&A