import idaapi import idc def get_basic_block(ea)...
Criado em: 20 de fevereiro de 2026
Respondido usando GPT-5.2 Pro por Chat01
Criado em: 20 de fevereiro de 2026
Respondido usando GPT-5.2 Pro por Chat01
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):
textreal_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脚本的共同思路是什么?
下面我按“IDA 里这些概念是什么 → 这段脚本逐行在干什么 → 它隐含的假设/用途 → 写 IDAPython 的关键点、与普通 Python 的区别 → 常见写脚本的通用思路”来讲。
int,但语义上代表“程序里的某个位置”。idaapi.get_func(ea):给你一个 func_t(函数对象),表示“ea 所在的函数”。idaapi.FlowChart(func):基于函数生成控制流图(CFG)视图,里面的节点就是基本块(BasicBlock)。block.start_ea:块起始地址(包含)block.end_ea:块结束地址(不包含,类似 Python 的切片右边界)block.preds():所有“跳到当前块”的前驱块(incoming edges)block.succs():所有“从当前块跳出去”的后继块(outgoing edges)你这段脚本几乎全部在用
preds(),也就是在 CFG 上“反向走”。
get_basic_block(ea)pythondef 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
原理:
ea 属于哪个函数 funcea 的块并返回这个函数就是 “地址 → 所在 basic block” 的映射。
get_block_size(block)pythondef get_block_size(block): return block.end_ea - block.start_ea
这是 “基本块大小 = 地址跨度”。注意这不是“指令条数”,是“字节跨度”。
find_all_real_blocks(fun_ea) 的总体目的先看整体结构:
pythondef 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)来做反向追踪。
脚本里有几处硬编码:
loop_head_addr = 0x140008983:你认为这是“循环头/调度块/汇合块”的地址(dispatcher / loop head)size != 25 and size != 20:过滤掉大小为 20 或 25 字节的块start_ea != 5368715678:跳过一个特定块你写的是十进制 5368715678,它等价于:
0x14000199e所以这行:
pythonif start_ea != 5368715678:
就是 “如果块起始地址不是 0x14000199e 才处理”。
这种“过滤固定大小块 + 固定地址跳过”的模式,非常像在处理**控制流平坦化(control-flow flattening)**或类似混淆:调度块和过渡块往往结构固定、大小相近。
你现在循环的是:loop_head_block 的所有前驱块 block。
pythonfor 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)
我们拆成三层“反向走图”的含义:
block 是 loop head 的前驱block -> loop_head_blockpreds = block.preds() 是 block 的前驱pred -> block -> loop_headlen(preds)==2 时,又取 pred.preds() 的第一个pred_1 -> pred -> block -> loop_head这意味着:脚本想从“跳到 loop head 的那些块(可能是过渡块/落脚块)”往回追一层或两层,去找你认为的“real block(真实块)”。
preds 数量分三种情况?先注意:你用的是 preds()(前驱数),不是 succs()(后继数)。
所以变量名里 “has_branch / no_branch” 从严格定义上并不准确:
但从“混淆/扁平化”的经验来看,你这样分可能是在表达:
len(preds) == 0你把 block.start_ea 认为是“真实块”。
这通常只会发生在:
len(preds) == 1你认为 block 本身可能是“过渡块/壳”,真正的逻辑块是它的唯一前驱 pred:
pythonreal_blocks_has_branch.append(pred.start_ea)
这非常像:
“有个块只是做了状态设置/跳转,然后跳进 loop head;真正的逻辑在它前面那个块。”
len(preds) == 2你又往回追了一层(取每个 pred 的第一个 pred):
pythonpred_1 = preds_1[0] real_blocks_has_no_branch.append(pred_1.start_ea)
这像是在处理:
“进入这个过渡块的路径有两条(比如 if/else 两路汇合到同一个过渡块),但真正的逻辑块在更前面。”
不过这里有两个脚本层面的明显假设/风险:
preds_1[0] 随便取第一个前驱可能不稳定
pred.preds() 可能是 1 个、2 个甚至更多preds_1 是否为空,也没有处理多个前驱“len(preds)==2 就一定要再退一层”的逻辑是经验型的
pythonreal_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)==0 或 len(preds)==2 再退一层)real_blocks_has_no_branch_but_has_pred:其实是 len(preds)==2 那部分的子集(重复记录了一份)然后打印 hex 和 decimal 两种形式。
jmp_table 这段字典可能是干什么的?pythonjmp_table = { 0x1400065df:0x14000661F, 0x140006d53:0x140006D93, 0x140006ad7:0x140006B17, 0x14000685b:0x14000689B, }
它在你给出的代码里没有被使用,但从格式上看非常像:
jmp 指令地址 → 它真正跳到的目标地址”在控制流修复、手工补 CFG 边、或进行 patch/重定位时很常见。
比如后续可能会用它:
idaapi.add_cref(src, dst, fl_JN) 添加控制流引用它做的事情可以概括为:
以
loop_head_addr所在基本块为中心,拿到所有跳入该块的前驱块;再基于这些前驱块的“前驱数量”和块大小等特征,向后回溯一到两层,试图抽取你认为的“真实基本块起始地址集合”。
这基本就是一种基于 CFG 结构特征的反混淆/筛块脚本思路。
func_t、BasicBlock、insn_t 等对象都来自 IDA SDK 的包装FlowChart)preds() / succs()像你这段:
这就是典型的“在某个样本/某类混淆上有效”的规则脚本。
你可以把它理解成:Python 语法没变,但你操作的世界变了。
很多操作不是纯函数,会改 IDB:
ida_name.set_nameidc.set_cmtidc.create_insnida_bytes.patch_byte所以脚本要更谨慎:你是在“改工程”。
get_func 返回 Nonepreds/succs 可能并不完整你会频繁看到:
idaapi.BADADDRNoneea/start_ea/end_ea 这样的地址字段
而且经常需要 hex(ea) 来读输出。(入门最重要的三件套)
idc:偏脚本层、快捷函数(取反汇编文本、操作注释、取操作数等)idaapi:更底层、更接近 SDK(FlowChart、insn 解码、处理器相关)idautils:各种迭代器(遍历函数、遍历指令、遍历 xref)经验上:
遍历用idautils,结构对象用idaapi,文本/快捷操作用idc。
不管你写的是找块、找字符串、找调用链、批量改名,通用流程基本都是:
idaapi.get_func(ea)idaapi.FlowChart(func)idautils.FuncItems(func.start_ea) 或 idautils.Heads(start, end)idautils.XrefsTo(ea) / XrefsFrom(ea)print() 输出idc.set_cmt 写注释ida_name.set_name 批量重命名你现在的脚本硬编码、且 preds_1[0] 有崩溃风险。更通用的写法通常是:
is_stub(block)伪代码思路像这样(你不用照抄,理解思路就行):
pythondef 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 的通用写法思路。
是的,这就是对抗控制流平坦化的脚本,我现在想问,idapython脚本接收的信息源于ida的分析,这个脚本中只是进一步分析,有没有改变ida判断的功能呢,你帮我总结一些idapython脚本经常关注,调用的分析数据,比如这里的block.start_ea。或者说,这个脚本是基于一个函数来分析的,对于一个经过了ida分析的函数,有哪些ida分析的重要数据呢
你理解得很对:IDAPython 脚本“读到的信息”主要来自 IDA 已经做过的 auto-analysis(以及你手工改过的 IDB 状态)。但同时也要明确两点:
你这段脚本属于第 1 类:只读 + 打印结果,它不会改变 IDA 的分析结论。
下面按你问的两个方向展开:
**没有。**原因很简单:它只调用了读接口、做集合处理、最后 print()。
它用到的主要读接口:
idaapi.get_func(ea):读“函数对象”idaapi.FlowChart(func):读“IDA 为该函数建立的 CFG / 基本块”block.start_ea / end_ea:读基本块边界block.preds():读 CFG 边(前驱集合)这些都不会改 IDB。
只要你调用了这些“写接口”,就属于会改变 IDA 状态(从轻到重):
ida_name.set_name(ea, "name")idc.set_cmt(ea, "comment", 0)这类通常不会改变 CFG/函数边界,但会改变你看到的内容与后续脚本读取到的“名字/注释”。
idc.create_insn(ea) / ida_bytes.del_items(ea, ...) 后再 createidc.create_data(ea, ...)这会直接影响:基本块划分、xrefs、函数识别等。
idaapi.add_cref(frm, to, type)idaapi.add_dref(frm, to, type)在对抗控制流平坦化时,这类非常有用:
你补上间接跳转的真实落点引用,FlowChart 才可能“看见”正确的边。
ida_funcs.add_func(start, end) / ida_funcs.del_func(start)这会影响整个函数级分析、CFG、调用图等。
ida_typeinf.apply_tinfo(...)你可以把 IDA 的分析结果想成一套“数据库事实”——脚本就是在读这些事实。常见关注点可以按层次分:
核心问题:某个 ea 上,IDA 认为它是什么?
常见数据:
典型 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。
核心问题:这个地址的指令是什么?操作数是什么?是不是 call/jmp?跳到哪里?
常见分析数据:
典型 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在平坦化里,很多脚本会识别:
cmp/jcc 模式jmp reg / jmp [mem] 这类间接跳转(IDA 经常缺边)核心问题:谁引用了谁?控制流/数据流的“边”在哪里?
常见数据:
典型 API:
idautils.XrefsTo(ea) / idautils.XrefsFrom(ea)idautils.CodeRefsTo(ea, flow) / CodeRefsFrom(ea, flow)idautils.DataRefsTo(ea) / DataRefsFrom(ea)对平坦化很关键的一点:
很多间接跳转没有明确 xref → CFG 边会缺失 → FlowChart 看不全。
所以很多反平坦化脚本会先“补 xref”。
核心问题:IDA 认定这是一个函数,它的范围/属性/组成是什么?
一个 func_t(函数对象)里你经常会关心:
func.start_ea, func.end_eaend_ea 也是右开区间)典型 API:
idaapi.get_func(ea) / ida_funcs.get_func(ea)ida_funcs.get_func_name(ea)idautils.FuncItems(func.start_ea)核心问题:基本块怎么划分?块之间怎么连?入口/出口在哪里?
你在脚本里用到的就是典型 CFG 信息:
idaapi.FlowChart(func):CFGblock.start_ea / block.end_ea:块边界(你脚本的关键数据)block.preds():前驱边集合block.succs():后继边集合(你脚本没用,但反平坦化通常也会用)另外还有一些脚本常用的“块特征”:
end-start)提醒一点:
你脚本里用 “preds 数量” 去叫 “has_branch/no_branch”,从定义上不严谨。一般:
len(block.succs()) 更接近“是否有分支”len(block.preds()) 更接近“是否被多路汇入/是否为汇合点”不过在对抗平坦化时,用 preds 做“回溯层数”的启发式是很常见的。
因为 dispatcher 很像 switch,IDA 会尝试识别 switch 信息(但混淆常让它失败)。
常见数据:
典型 API(不同 IDA 版本接口略有差异,但思想一样):
ida_nalt.get_switch_info(ea) / idaapi.get_switch_info_ex(...)idaapi.calc_switch_cases(...)你最后的 jmp_table 字典就很像用于“补齐间接跳转目标”的数据源:
后续很可能用它去 add_cref 或者创建 code 到目标地址,从而让 CFG 更完整。
核心问题:函数原型、局部变量、表达式树、microcode
常见数据:
典型 API:
ida_hexrays.decompile(func_ea) → cfunc_t反平坦化/去混淆做得“更自动”的,往往会用这层做 state 变量追踪(比纯汇编规则鲁棒)。
如果只给你一份“函数级关键数据清单”,我会按优先级列成这样(也是反平坦化最常用的):
你可以用一个非常直观的标准:
get_* / print_* / decode / FlowChart / XrefsTo 这类“读接口”,最后输出或导出文件set_* / add_* / del_* / create_* / patch_* / apply_* 这类“写接口”,并且通常会导致:
idaapi.auto_wait())你这段脚本明显属于第一类。
如果你想把它进一步升级成“既分析又修复 IDA CFG”的版本,最常见的路线就是:
用你推出来的真实边(比如 jmp_table 或从 dispatcher 计算出来的目标)去 add_cref,再让 FlowChart 重新生成,这样后续脚本可以在“更接近真实的 CFG”上工作。
你要是愿意,把你后续打算怎么用 jmp_table(是补边?还是导出真实图?还是做 patch?)说一下,我可以按你目标给你一份更贴近实战的“从只读分析 → 写回修复 CFG → 迭代”的脚本结构模板。