Ollvm对抗
https://bbs.kanxue.com/thread-272414-1.htm
https://security.tencent.com/index.php/blog/msg/112
https://bbs.kanxue.com/thread-271164-1.htm
https://qmeimei10086.github.io/2025/12/28/长城杯初赛-2025-vvvmmm-最失败的一集/?utm_source=chatgpt.com
ollvm
前置知识
分类&识别
| 分类 | 描述 |
|---|---|
| 指令替换(Instructions Substitution)(Sub) | 将一条运算指令替换为多条等价的运算指令,例如: y = x + 1变为y = x + 1 - 1 |
| 虚假控制流(Bogus Control Flow)(bcf) | 通过加入包含不透明谓词的条件跳转和不可达的基本块,来干扰IDA的控制流分析和F5反汇编 |
| 控制流平坦化(Control Flow Flattening)(Fla) | 主要通过一个主分发器来控制程序基本块的执行流程,将所有基本代码放到控制流最底部,然后删除原理基本块之间跳转关系,添加次分发器来控制分发逻辑,然后过新的复杂分发逻辑还原原来程序块之间的逻辑关系 |
| 字符串加密 | 编写一个pass将其中的字符串信息使用一些加密算法进行加密,然后特定的时间进行还原 |
D-810工具
- 启动快捷键:
edit->D-810或者Ctrl+Shift+D - 使用

| 名称 | 作用 |
|---|---|
default_instruction_only.json |
每条伪代码基本对应一条或少量汇编 |
default_unflattening_ollvm.json |
针对 OLLVM(Obfuscator-LLVM)混淆的反扁平化配置 试图把 while(1) + switch + state 结构:还原成正常的 if / while / for(推荐) |
default_unflattening_switch_case.json |
通用的 switch-case 反扁平化(推荐) |
example_anel.json |
用来演示 如何写 d810 的规则 |
default.json |
默认综合配置(推荐日常使用) |
- 可优先尝试先
default_unflattening_ollvm.json和default_unflattening_switch_case.json的组合
控制流平坦化(FLA)
控制流平坦化原理概要
控制流平坦化(control flow flattening)的基本思想:通过一个主分发器来控制程序基本块的执行流程,例如下图是正常的执行流程:

经过控制流平坦化后的执行流程就如下图 :

- 序言:函数的入口块,主要作用是初始化“状态变量”,将其设置为第一个真实块的“ID”。
- 主 (子) 分发器:负责控制程序跳转到下一个待执行的基本块。通常是一个巨大的while(true)循环。子分发器可能包含另一层的swithch判断。
- 真实块:混淆前的基本块,程序真正执行工作的块。
- 预处理器:跳转到主分发器,这个块的唯一目的就是修改“状态变量”的值,将其设置为下一个应该执行的“真实块”的ID。
- retn 块:函数出口 ,其特征是没有往下执行,且函数最后是以retn结尾
OLLVM反混淆经典思路
对于经典的 ollvm 来说,各个块之间有如下规则
- 找到序言块,这是整个函数的入口
- 序言块的后继是主分发器
- 主分发器的前驱有两个,一个是序言块,一个是预处理器
- 预处理器的前驱是真实块
- 除此之外的其他块是子分发器
去除FLA需要以下三步:
-
找到真实块:除真实块和序言块外,其余的块都是虚假块,需要 NOP 掉他们。
-
得到真实块之间的联系:我们需要让代码运行起来,可以用模拟执行,也可以在真机调试打断点 trace, 核心都是找到真实块之间的调用关系
-
跳转:得到真实块之间的调用关系后,我们只需要在每个真实块的末尾,用跳转汇编指令将每个真实串起来就好了。
获取块关系
获取程序块 -> 下断点(ida动态调试,一直F9即可)-> 获取块之间的顺序
import ida_funcs
import ida_gdl
import ida_kernwin
import ida_name
import ida_bytes
import idautils
import ida_dbg
# =========================
# Config
# =========================
# 方式1:手动指定函数地址
TARGET_FUNC_EA = 0x401C60
# 方式2:自动使用当前光标所在函数
USE_CURRENT_FUNC = False
# 是否先清除该函数 block_start 上已有断点
CLEAR_OLD_BREAKPOINTS = False
# 是否打印块摘要信息
PRINT_BLOCK_INFO = True
# 是否打印命中轨迹
PRINT_TRACE = True
# 是否保存轨迹到内存
SAVE_TRACE = True
# 是否在进程退出时自动打印/保存完整 trace
DUMP_TRACE_ON_EXIT = True
# 调试结束后是否自动清除本脚本添加的断点
CLEAR_BREAKPOINTS_ON_EXIT = True
# 调试结束后是否自动卸载 hook
UNHOOK_ON_EXIT = True
# trace 输出文件;None 表示不写文件
TRACE_OUT_PATH = None
# 例如:
# TRACE_OUT_PATH = r"C:\temp\bb_trace.txt"
# 输出时是否显示 preds/succs 列表
SHOW_PREDS_SUCCS_IN_TRACE = False
# 输出时是否复制最终 trace 到剪贴板
COPY_TRACE_TO_CLIPBOARD_ON_EXIT = False
# =========================
# Helpers
# =========================
def get_target_func():
if USE_CURRENT_FUNC:
ea = ida_kernwin.get_screen_ea()
f = ida_funcs.get_func(ea)
if not f:
raise RuntimeError("Current cursor is not inside a function: 0x%X" % ea)
return f
f = ida_funcs.get_func(TARGET_FUNC_EA)
if not f:
raise RuntimeError("No function at 0x%X" % TARGET_FUNC_EA)
return f
def get_func_name(func):
return ida_name.get_ea_name(func.start_ea) or ("sub_%X" % func.start_ea)
def get_last_insn_ea(start_ea, end_ea):
"""
IDA basic block 的 end_ea 是开区间 [start_ea, end_ea)
返回块内最后一条指令地址
"""
last = None
for ea in idautils.Heads(start_ea, end_ea):
if ida_bytes.is_code(ida_bytes.get_flags(ea)):
last = ea
return last if last is not None else start_ea
def count_instructions(start_ea, end_ea):
cnt = 0
for ea in idautils.Heads(start_ea, end_ea):
if ida_bytes.is_code(ida_bytes.get_flags(ea)):
cnt += 1
return cnt
def guess_block_role(block_size, insn_count, pred_count, succ_count):
"""
只是启发式提示,不是严格结论
"""
if insn_count <= 3 and succ_count == 1:
return "trampoline_like"
if insn_count <= 4 and succ_count >= 2:
return "dispatcher_like"
if insn_count >= 8:
return "real_block_like"
return "uncertain"
def collect_basic_blocks(func):
fc = ida_gdl.FlowChart(func, flags=ida_gdl.FC_PREDS)
block_list = []
for idx, b in enumerate(fc):
preds = [x.start_ea for x in b.preds()]
succs = [x.start_ea for x in b.succs()]
block_start = b.start_ea
block_end_exclusive = b.end_ea
block_end = get_last_insn_ea(block_start, block_end_exclusive)
block_size = block_end_exclusive - block_start
insn_count = count_instructions(block_start, block_end_exclusive)
pred_count = len(preds)
succ_count = len(succs)
role_hint = guess_block_role(block_size, insn_count, pred_count, succ_count)
block_list.append({
"id": idx,
"block_start": block_start,
"block_end": block_end,
"block_end_exclusive": block_end_exclusive,
"block_size": block_size,
"insn_count": insn_count,
"preds": preds,
"succs": succs,
"pred_count": pred_count,
"succ_count": succ_count,
"role_hint": role_hint,
})
block_list.sort(key=lambda x: x["block_start"])
return block_list
def clear_breakpoints(block_list):
for block in block_list:
addr = block["block_start"]
ok = ida_dbg.del_bpt(addr)
if PRINT_BLOCK_INFO:
print("[BPT-DEL] %s at 0x%X" % ("OK" if ok else "FAIL", addr))
def add_breakpoints(block_list):
for block in block_list:
addr = block["block_start"]
ok = ida_dbg.add_bpt(addr)
if PRINT_BLOCK_INFO:
print("[BPT-ADD] %s at 0x%X" % ("OK" if ok else "FAIL", addr))
def format_trace_lines(trace):
lines = []
for item in trace:
line = (
"%d : 0x%X (block_id=%d, size=%d, insn=%d, preds=%d, succs=%d, hint=%s)"
% (
item["hit_index"],
item["ea"],
item["block_id"],
item["block_size"],
item["insn_count"],
item["pred_count"],
item["succ_count"],
item["role_hint"],
)
)
if SHOW_PREDS_SUCCS_IN_TRACE:
preds_text = ", ".join("0x%X" % x for x in item["preds"])
succs_text = ", ".join("0x%X" % x for x in item["succs"])
line += " preds=[%s] succs=[%s]" % (preds_text, succs_text)
lines.append(line)
return lines
def save_trace(trace, out_path):
if not out_path:
return
lines = format_trace_lines(trace)
with open(out_path, "w", encoding="utf-8") as fp:
fp.write("\n".join(lines) + "\n")
print("[TRACE] written to %s" % out_path)
# =========================
# Hook
# =========================
class BlockTraceHook(ida_dbg.DBG_Hooks):
def __init__(self, block_list, func_name, func_start):
super().__init__()
self.block_list = block_list
self.func_name = func_name
self.func_start = func_start
self.hit_index = 0
self.trace = []
# ea -> block info
self.block_map = {b["block_start"]: b for b in block_list}
def dbg_bpt(self, tid, ea):
block = self.block_map.get(ea)
if block is None:
return 0
record = {
"hit_index": self.hit_index,
"ea": ea,
"block_id": block["id"],
"block_size": block["block_size"],
"insn_count": block["insn_count"],
"pred_count": block["pred_count"],
"succ_count": block["succ_count"],
"preds": block["preds"],
"succs": block["succs"],
"role_hint": block["role_hint"],
}
if PRINT_TRACE:
line = format_trace_lines([record])[0]
print(line)
if SAVE_TRACE:
self.trace.append(record)
self.hit_index += 1
return 0
def dbg_process_exit(self, pid, tid, ea, code):
print("[EXIT] process exited with code %d" % code)
if DUMP_TRACE_ON_EXIT and self.trace:
print("[TRACE] total hits: %d" % len(self.trace))
lines = format_trace_lines(self.trace)
for line in lines:
print(line)
if TRACE_OUT_PATH:
save_trace(self.trace, TRACE_OUT_PATH)
if COPY_TRACE_TO_CLIPBOARD_ON_EXIT:
try:
ida_kernwin.set_clipboard_text("\n".join(lines))
print("[TRACE] copied to clipboard")
except Exception as e:
print("[TRACE] clipboard failed:", e)
if CLEAR_BREAKPOINTS_ON_EXIT:
clear_breakpoints(self.block_list)
print("[BPT] breakpoints cleared on exit")
if UNHOOK_ON_EXIT:
try:
self.unhook()
print("[HOOK] uninstalled on exit")
except Exception as e:
print("[HOOK] unhook failed:", e)
return 0
# =========================
# Main
# =========================
my_hook = None
def install_hook(block_list, func_name, func_start):
global my_hook
try:
if my_hook is not None:
my_hook.unhook()
print("[HOOK] removed old hook")
except Exception:
pass
my_hook = BlockTraceHook(block_list, func_name, func_start)
my_hook.hook()
print("[HOOK] installed")
def cleanup():
global my_hook
try:
if my_hook is not None:
clear_breakpoints(my_hook.block_list)
my_hook.unhook()
print("[CLEANUP] done")
except Exception as e:
print("[CLEANUP] failed:", e)
def main():
func = get_target_func()
func_name = get_func_name(func)
block_list = collect_basic_blocks(func)
print("[FUNC] %s @ 0x%X" % (func_name, func.start_ea))
print("[BLOCKS] %d" % len(block_list))
if PRINT_BLOCK_INFO:
for b in block_list:
print(
" id=%d start=0x%X end=0x%X size=%d insn=%d preds=%d succs=%d hint=%s"
% (
b["id"],
b["block_start"],
b["block_end"],
b["block_size"],
b["insn_count"],
b["pred_count"],
b["succ_count"],
b["role_hint"],
)
)
if CLEAR_OLD_BREAKPOINTS:
clear_breakpoints(block_list)
print("[BPT] old breakpoints cleared")
add_breakpoints(block_list)
install_hook(block_list, func_name, func.start_ea)
print("[READY] Breakpoints are set on all block_start addresses.")
print("[READY] Run the program and keep pressing F9 to collect the execution order.")
print("[READY] On process exit, breakpoints will be cleared automatically.")
return block_list
if __name__ == "__main__":
main()
字段含义:
hit_index:第几次命中ea:当前 block 起始地址block_id:块编号size:块地址跨度insn:块内指令数preds:前驱数量succs:后继数量hint:粗略角色判断
D-810
标准 ollvm-fla 用 IDA 插件——D-810。
区分是否标准:
- 非标准 fla 的循环头地址和汇聚块的地址是相等的

标准 fla 的循环头地址和汇聚块的地址是不相等的,其循环头的前驱只有两个基本块, 一个是序言块, 一个是汇聚块。


OLLVM-fla 分析还原(复杂的非标准 fla)
原版
架构:ARM64 OLLVM
from collections import deque # 导入双端队列,用于实现广度优先搜索 (BFS)
import ida_funcs # 导入 IDA Pro 函数相关 API
import idaapi # 导入 IDA Pro 核心 API
import idc # 导入 IDA Pro 经典 API (IDC)
def get_block_by_address(ea):
# 获取地址所在的函数
func = idaapi.get_func(ea)
# 获取函数的流程图(FlowChart),包含了所有基本块
blocks = idaapi.FlowChart(func)
# 遍历函数中的所有基本块
for block in blocks:
# 判断地址 ea 是否在该基本块的范围内
if block.start_ea <= ea < block.end_ea:
# 如果是,则返回这个基本块对象
return block
# 如果没有找到,返回 None
return None
def find_loop_heads(func):
# 查找循环头。在OLLVM-fla中,这通常是主分发器(main dispatcher)
loop_heads = set() # 使用集合来存储循环头地址,自动去重
queue = deque() # 初始化一个队列用于 BFS
# 获取函数入口地址的基本块
block = get_block_by_address(func)
# 将入口块和空路径(表示访问过的节点)加入队列
queue.append((block, []))
# 开始 BFS
while len(queue) > 0:
# 取出当前块和到达该块的路径
cur_block, path = queue.popleft()
# 检查当前块是否已经在路径中
if cur_block.start_ea in path:
# 如果在,说明找到了一条回边(back-edge),当前块是一个循环头
loop_heads.add(cur_block.start_ea)
# 停止在当前路径上继续搜索,避免无限循环
continue
# 将当前块的起始地址添加到路径中
path = path + [cur_block.start_ea]
# 将当前块的所有后继块(succs)及当前路径加入队列,继续搜索
queue.extend((succ, path) for succ in cur_block.succs())
# 将集合转换为列表
all_loop_heads = list(loop_heads)
# 升序排序, 保证函数开始的主循环头在第一个(非标准OLLVM可能有多个循环头)
all_loop_heads.sort()
return all_loop_heads
def find_converge_addr(loop_head_addr):
# 寻找汇聚块(converge block)。真实块执行完毕后会跳转到汇聚块,汇聚块再跳回循环头
converge_addr = None
# 获取循环头基本块的对象
block = get_block_by_address(loop_head_addr)
# 获取循环头的所有前驱块(preds)
preds = block.preds()
pred_list = list(preds)
# 标准OLLVM中,循环头(主分发器)有两个前驱:序言块 和 汇聚块
if len(pred_list) == 2:
for pred in pred_list:
# 获取前驱块的“前驱们”
tmp_list = list(pred.preds())
# 汇聚块的特点是它有多个前驱(来自所有真实块),而序言块通常只有一个
if len(tmp_list) > 1:
# 这个就是汇聚块
converge_addr = pred.start_ea
else: # 非标准ollvm
# 在某些非标准OLLVM变种中,循环头和汇聚块可能是同一个块
converge_addr = loop_head_addr
return converge_addr
def get_basic_block_size(bb):
# 辅助函数:计算一个基本块的大小(结束地址 - 开始地址)
return bb.end_ea - bb.start_ea
def add_block_color(ea):
# 辅助函数:给指定地址所在的基本块在IDA中染色,便于分析
block = get_block_by_address(ea)
curr_addr = block.start_ea
# 遍历块内的每一条指令
while curr_addr < block.end_ea:
# 设置指令颜色为 0xffcc33 (一种黄色)
idc.set_color(curr_addr, idc.CIC_ITEM, 0xffcc33)
# 移动到下一条指令的头部
curr_addr = idc.next_head(curr_addr)
# 清除函数中的颜色渲染
def del_func_color(curr_addr):
# 找到函数的结束地址
end_ea = idc.find_func_end(curr_addr)
# 遍历函数内的所有指令
while curr_addr < end_ea:
# 恢复默认颜色 (0xffffffff)
idc.set_color(curr_addr, idc.CIC_ITEM, 0xffffffff)
curr_addr = idc.next_head(curr_addr)
def find_ret_block_addr(blocks):
# 查找函数的返回块(或逻辑上的返回块)
for block in blocks:
succs = block.succs() # 获取后继块
succs_list = list(succs) # 转为list结构
end_ea = block.end_ea
# 获取块的最后一条指令地址
last_ins_ea = idc.prev_head(end_ea)
# 获取最后一条指令的助记符
mnem = idc.print_insn_mnem(last_ins_ea)
# 如果一个块没有后继
if len(succs_list) == 0:
# 并且最后一条指令是 "RET"
if mnem == "RET":
# 注释说明:不直接用RET块,而是往上找前驱的分支块
# 如果直接去把RET指令所在的块作为返回块的表示 最后可能会出现反混淆代码赋值错误
# 所以这里取RET指令的前驱块并且前驱块的大小不能只有一条指令,一般这个块都是有分支的
ori_ret_block = block # 保存原始的RET块
# 循环向上回溯
while True:
tmp_block = block.preds() # 获取前驱
pred_list = list(tmp_block)
if len(pred_list) == 1: # 如果只有一个前驱
block = pred_list[0]
# 如果块大小为4(可能只是个跳板),则继续向上
if get_basic_block_size(block) == 4:
continue
else:
# 找到了一个非跳板块
break
else:
# 找到了一个有多个前驱的分支块
break
# 特例处理:如果找到的这个“返回块”本身又是一个子分发器(有多个后继)
# 此时应该使用原始的RET块
# 此处while循环是为了解决当上述的ret块作为子分发器时,需要重新更改ret块为带ret指令的块
block2 = block
num = 0
i = 0
while True:
i += 1
succs_block = block2.succs()
for succ in succs_block:
child_succs = succ.succs()
succ_list = list(child_succs)
if len(succ_list) != 0:
block2 = succ
num += 1
if num > 2: # 如果后继太多,判定为子分发器
block = ori_ret_block # 恢复为原始RET块
break
if i > 2: # 限制搜索深度
break
return block.start_ea # 返回最终确定的返回块地址
def find_all_real_block(func_ea):
# 主函数:找到函数(func_ea)中所有的真实块(Real Blocks)
# 获取函数的所有基本块
blocks = idaapi.FlowChart(idaapi.get_func(func_ea))
# 获取所有循环头(非标准ollvm可能有多个主分发器)
loop_heads = find_loop_heads(func_ea)
print(f"循环头数量:{len(loop_heads)}----{[hex(loop_head) for loop_head in loop_heads]}")
all_real_block = [] # 用于存储所有真实块(按循环头分组)
# 遍历每个循环头(主分发器)
for loop_head_addr in loop_heads:
loop_head_block = get_block_by_address(loop_head_addr) # 获取循环头
loop_head_preds = list(loop_head_block.preds()) # 获取循环头的所有前驱块
loop_head_preds_addr = [loop_head_pred.start_ea for loop_head_pred in loop_head_preds] # 把所有前驱块转为地址数组
# 获取汇聚块地址
converge_addr = find_converge_addr(loop_head_addr)
real_blocks = [] # 存储当前这个循环头对应的真实块
# 如果循环头和汇聚块不是同一个块(标准OLLVM)
if loop_head_addr != converge_addr:
loop_head_preds_addr.remove(converge_addr) # 移除汇聚块, 剩下的一个是序言块
real_blocks.extend(loop_head_preds_addr) # 序言块是第一个真实块
# 获取汇聚块,并遍历它的所有前驱(这些就是真实块或其跳板)
converge_block = get_block_by_address(converge_addr)
list_preds = list(converge_block.preds())
for pred_block in list_preds:
end_ea = pred_block.end_ea
last_ins_ea = idc.prev_head(end_ea)
mnem = idc.print_insn_mnem(last_ins_ea) # 获取基本块最后一条指令的操作符
size = get_basic_block_size(pred_block)
# 过滤掉小的(可能是跳板)或无条件跳转的块
if size > 4 and "B." not in mnem:
start_ea = pred_block.start_ea
mnem = idc.print_insn_mnem(start_ea)
# 文章中提到,CSEL块是OLLVM-fla实现分支的关键
if mnem == "CSEL":
# 如果这个块以CSEL开头,它是一个共享块,真正的真实块是它的前驱
csel_preds = pred_block.preds()
for csel_pred in csel_preds:
real_blocks.append(csel_pred.start_ea)
else:
# 否则,这个块本身就是真实块
real_blocks.append(pred_block.start_ea)
real_blocks.sort() # 排序后第一个元素始终是序言块
all_real_block.append(real_blocks)
print("子循环头:", [hex(child_block_ea) for child_block_ea in real_blocks])
# 获取return块
ret_addr = find_ret_block_addr(blocks)
all_real_block.append(ret_addr) # 将返回块也加入列表
print("all_real_block:", all_real_block)
# 将所有分组的真实块合并到一个扁平的列表 all_real_block_list
all_real_block_list = []
for real_blocks in all_real_block:
if isinstance(real_blocks, list): # 如果是列表,用 extend
all_real_block_list.extend(real_blocks)
else: # 如果不是列表(比如ret_addr),用 append
all_real_block_list.append(real_blocks)
# 为所有找到的真实块在IDA中上色
for real_block_ea in all_real_block_list:
# idc.add_bpt(real_block_ea)#断点
add_block_color(real_block_ea) # 渲染颜色
print("\n所有真实块获取完成")
print("===========INT===============")
print(all_real_block_list)
print("===========HEX===============")
print(f"数量:{len(all_real_block_list)}")
print([hex(real_block_ea) for real_block_ea in all_real_block_list], "\n")
# 准备angr分析所需的参数:分离出子序言块(非标准OLLVM)
# 移除ret地址和主序言块相关真实块, 保留子序言块相关的真实块
all_child_prologue_addr = all_real_block.copy()
all_child_prologue_addr.remove(ret_addr)
all_child_prologue_addr.remove(all_child_prologue_addr[0]) # 移除主序言块
print("所有子序言块相关的真实块地址:", all_child_prologue_addr)
# 获取所有子序言块的最后一条指令地址(用于angr hook)
all_child_prologue_last_ins_ea = []
for child_prologue_array in all_child_prologue_addr:
child_prologue_addr = child_prologue_array[0] # 子序言块的起始地址
child_prologue_block = get_block_by_address(child_prologue_addr)
child_prologue_end_ea = child_prologue_block.end_ea
child_prologue_last_ins_ea = idc.prev_head(child_prologue_end_ea) # 最后一条指令地址
all_child_prologue_last_ins_ea.append(child_prologue_last_ins_ea)
print("所有子序言块的最后一条指令的地址:", all_child_prologue_last_ins_ea)
# 返回 真实块列表, 子序言块列表, 子序言块最后指令地址列表
return all_real_block_list, all_child_prologue_addr, all_child_prologue_last_ins_ea
'''========================angr执行============================='''
import logging # 导入日志模块
import time # 导入时间模块
import angr # 导入 angr 框架
from tqdm import tqdm # 导入 tqdm,用于显示进度条
# 过滤angr日志,只显示ERROR日志,里面许多的WARNING输出影响日志分析
logging.getLogger('angr').setLevel(logging.ERROR)
def capstone_decode_csel(insn):
# 辅助函数:解析 CSEL 指令,提取操作数
operands = insn.op_str.replace(' ', '').split(',')
dst_reg = operands[0] # 目标寄存器
condition = operands[3] # 条件码
reg1 = operands[1] # 源寄存器1 (True)
reg2 = operands[2] # 源寄存器2 (False)
return dst_reg, reg1, reg2, condition
def print_reg(state, reg_name):
# 辅助调试函数:打印寄存器值
value = state.regs.get(reg_name)
print(f"地址:{hex(state.addr)},寄存器:{reg_name},value:{value}")
def find_state_succ(proj, base, local_state, flag, real_blocks, real_block_addr, path):
# 辅助函数:用于探索 CSEL 指令的一个分支(真或假)
# 获取当前块的第一条指令(即CSEL指令)
ins = local_state.block().capstone.insns[0]
# 解析 CSEL
dst_reg, reg1, reg2, condition = capstone_decode_csel(ins)
# 获取两个源寄存器的 *符号* 值(在angr中可能是 <BV64 ...>)
val1 = local_state.regs.get(reg1)
val2 = local_state.regs.get(reg2)
# print(f"寄存器值 {reg1}:{val1},{reg2}:{val2}")
# 创建模拟管理器
sm = proj.factory.simgr(local_state)
# 执行 CSEL 指令
sm.step(num_inst=1)
# 获取执行后的状态
tmp_state = sm.active[0]
# 关键:根据 flag(True/False)强制设置CSEL的目标寄存器
if flag:
setattr(tmp_state.regs, dst_reg, val1) # 给寄存器的条件判断结果设为真
else:
setattr(tmp_state.regs, dst_reg, val2) # 给寄存器的条件判断结果设为假
# print(f"开始运行的寄存器:{sm.active[0].regs.get(dst_reg)}")
# 继续模拟执行,直到碰到下一个真实块
while len(sm.active):
# print(sm.active)
for active_state in sm.active:
ins_offset = active_state.addr - base # 计算当前指令的偏移
# if ins_offset == 0x41DC0:
# print_reg("x8")
# 如果当前地址是已知的真实块之一
if ins_offset in real_blocks:
value = path[real_block_addr] # 获取当前分析块的后继列表
if ins_offset not in value: # 如果这个后继块还没被记录
value.append(ins_offset) # 添加到后继列表
return ins_offset # 返回找到的后继块地址
sm.step(num_inst=1) # 继续执行下一条指令
def find_block_succ(proj, base, func_offset, state, real_block_addr, real_blocks, path):
# 主分析函数:找到一个真实块(real_block_addr)的所有后继真实块
msm = proj.factory.simgr(state) # 构造模拟器
# 第一个while的作用: 寻找到传入的真实块地址作为主块, 再复制一份当前state, 准备后继块获取的操作
while len(msm.active):
# print(f"路径{msm.active}")
for active_state in msm.active:
offset = active_state.addr - base
if offset == real_block_addr: # 找到真实块
mstate = active_state.copy() # 复制state, 为后继块的获取做准备
msm2 = proj.factory.simgr(mstate)
msm2.step(num_inst=1) # 防止下个while里获取后继块的时候key和value重复
# 第二个while的作用: 寻找真实块的所有后继块
while len(msm2.active):
# print(msm2.active)
for mactive_state in msm2.active:
ins_offset = mactive_state.addr - base
# 情况一:无分支块(后继是真实块)
if ins_offset in real_blocks:
# 在无条件跳转中,并且有至少两条路径同时执行到真实块时,取非ret块的真实块
msm2_len = len(msm2.active)
if msm2_len > 1: # 如果angr分析出多条路径
tmp_addrs = []
for s in msm2.active:
moffset = s.addr - base
tmp_value = path[real_block_addr]
if moffset in real_blocks and moffset not in tmp_value:
tmp_addrs.append(moffset)
if len(tmp_addrs) > 1: # 如果多条路径指向不同的真实块
print("当前至少有两个路径同时执行到真实块:", [hex(tmp_addr) for tmp_addr in tmp_addrs])
ret_addr = real_blocks[len(real_blocks) - 1] # 获取返回块地址
if ret_addr in tmp_addrs:
tmp_addrs.remove(ret_addr) # 优先排除返回块
ins_offset = tmp_addrs[0] # 选择剩下的那个
print("两个路径同时执行到真实块最后取得:", hex(ins_offset))
value = path[real_block_addr]
if ins_offset not in value: # 记录后继
value.append(ins_offset)
print(f"无条件跳转块关系:{hex(real_block_addr)}-->{hex(ins_offset)}")
return # 找到后继,结束当前块的分析
# 情况二:有分支块(CSEL)
ins = mactive_state.block().capstone.insns[0]
if ins.mnemonic == 'csel':
# 复制状态,分别探索 True 和 False 分支
state_true = mactive_state.copy()
state_true_succ_addr = find_state_succ(proj, base, state_true, True, real_blocks, real_block_addr, path)
state_false = mactive_state.copy()
state_false_succ_addr = find_state_succ(proj, base, state_false, False, real_blocks, real_block_addr, path)
if state_true_succ_addr is None or state_false_succ_addr is None:
print("csel错误指令地址:", hex(ins_offset))
print(f"csel后继有误:{hex(real_block_addr)}-->{hex(state_true_succ_addr) if state_true_succ_addr is not None else state_true_succ_addr},"
f"{hex(state_false_succ_addr) if state_false_succ_addr is not None else state_false_succ_addr}")
return "erro"
print(f"csel分支跳转块关系:{hex(real_block_addr)}-->{hex(state_true_succ_addr)},{hex(state_false_succ_addr)}")
return # 找到两个分支的后继,结束当前块的分析
msm2.step(num_inst=1) # 继续单步执行
return
# 真实块集合中的最后一个基本块如果最后没找到后继,说明是return块,直接返回
msm.step(num_inst=1) # 继续单步执行(在第一个while中,直到找到real_block_addr)
def angr_main(real_blocks, all_child_prologue_addr, all_child_prologue_last_ins_ea, func_offset, file_path):
# angr 分析的主入口
# 加载二进制文件
proj = angr.Project(file_path, auto_load_libs=False)
# 获取基地址
base = proj.loader.min_addr
# 计算函数在内存中的实际地址
func_addr = base + func_offset
# 创建一个空状态,从函数入口开始
init_state = proj.factory.blank_state(addr=func_addr)
# 设置选项:不进入子函数调用
init_state.options.add(angr.options.CALLLESS)
# 初始化用于存储CFG的字典,键为真实块地址,值为空列表(用于存后继)
path = {key: [] for key in real_blocks}
# 找到返回块的地址
ret_addr = real_blocks[len(real_blocks) - 1]
# 获取主序言块(函数入口块)
first_block = proj.factory.block(func_addr)
first_block_insns = first_block.capstone.insns
# 获取主序言块的最后一条指令(用于下hook)
first_block_last_ins = first_block_insns[len(first_block_insns) - 1]
# 遍历所有真实块,为每一个块寻找它的后继
for real_block_addr in tqdm(real_blocks):
if ret_addr == real_block_addr: # 如果是返回块,则跳过(它没有后继)
continue
prologue_block_addr = 0 # 子序言块地址
child_prologue_last_ins_ea = 0 # 子序言块最后一条指令的地址
# 检查当前真实块是否属于某个子序言块组(非标准OLLVM)
if len(all_child_prologue_addr) > 0:
for index, child_prologue_array in enumerate(all_child_prologue_addr):
if real_block_addr in child_prologue_array:
prologue_block_addr = child_prologue_array[0] + base # 子序言块的内存地址
child_prologue_last_ins_ea = all_child_prologue_last_ins_ea[index] # 最后一条指令的地址
# 拷贝初始化state, 确保每个真实块的分析都是从干净的函数入口状态开始
state = init_state.copy()
print("正在寻找:", hex(real_block_addr))
# hook函数:用于劫持PC并跳转到目标真实块
def jump_to_address(state):
# -4 是因为ARM指令长度为4,hook发生在指令执行前,需要指向指令本身
state.regs.pc = base + real_block_addr - 4
# hook函数:用于跳转到子序言块
def jump_to_child_prologue_address(state):
state.regs.pc = prologue_block_addr - 4
# 核心逻辑:设置 hook
if prologue_block_addr == 0: # 标准OLLVM
# 当序言块执行完后(初始化后续条件判断的寄存器),将最后一条指令的pc寄存器指向真实块地址
if real_block_addr != func_offset: # 如果不是序言块本身
# 在主序言块的最后一条指令处下hook,执行完后直接跳转到 real_block_addr
proj.hook(first_block_last_ins.address, jump_to_address, first_block_last_ins.size)
else: # 非标准OLLVM
# hook主序言块,让它跳转到子序言块
proj.hook(first_block_last_ins.address, jump_to_child_prologue_address, first_block_last_ins.size)
# hook子序言块,让它再跳转到 real_block_addr
proj.hook(child_prologue_last_ins_ea, jump_to_address, 4)
# 开始模拟执行,寻找后继
ret = find_block_succ(proj, base, func_offset, state, real_block_addr, real_blocks, path)
if ret == "erro": # 如果出错
return
# 将CFG中的地址转换为十六进制字符串
hex_dict = {
hex(key): [hex(value) for value in values]
for key, values in path.items()
}
print("真实块控制流:\n", hex_dict)
# 返回重建的控制流
return hex_dict
'''重建控制流'''
from collections import deque # 再次导入 deque
import idaapi # 再次导入 idaapi
import idautils # 导入 ida utils
import idc # 再次导入 idc
import keystone # 导入 keystone 汇编引擎
# 初始化Ks arm64架构的so, 模式: 小端序
ks = keystone.Ks(keystone.KS_ARCH_ARM64, keystone.KS_MODE_LITTLE_ENDIAN)
def patch_ins_to_nop(ins):
# 辅助函数:将一条指令 patch 为 NOP
size = idc.get_item_size(ins) # 获取指令大小
for i in range(size):
idc.patch_byte(ins + i, 0x90) # 逐字节替换为 0x90 (NOP)
def get_block_by_address(ea):
# 辅助函数:获取地址所在的基本块(重复定义)
func = idaapi.get_func(ea)
blocks = idaapi.FlowChart(func)
for block in blocks:
if block.start_ea <= ea < block.end_ea:
return block
return None
def patch_branch(patch_list):
# 主函数:根据 angr 生成的CFG (patch_list) 来 patch IDB
# 遍历CFG中所有的真实块
for ea in patch_list:
values = patch_list[ea] # 获取后继块列表
if len(values) == 0: # 如果后继块为0, 基本都是return块, 不需要patch, 直接跳过
continue
# 获取当前真实块的基本块对象
block = get_block_by_address(int(ea, 16))
start_ea = block.start_ea
end_ea = block.end_ea
# 获取块的最后一条指令地址(因为block.end_ea是下一块的起始)
last_ins_ea = idc.prev_head(end_ea)
# 情况一:有2个后继,说明是分支块
if len(values) == 2:
flag = False
# 遍历块内的所有指令,寻找 CSEL
for ins in idautils.Heads(start_ea, end_ea):
if idc.print_insn_mnem(ins) == "CSEL":
# 提取 CSEL 的条件码
condition = idc.print_operand(ins, 3)
# 使用 keystone 将 CSEL 替换为 B.cond(跳转到第一个后继, 即True分支)
encoding, count = ks.asm(f'B.{condition} {values[0]}', ins)
# 使用 keystone 将块的最后一条指令替换为 B(跳转到第二个后继, 即False分支)
encoding2, count2 = ks.asm(f'B {values[1]}', last_ins_ea)
# 执行 patch
for i in range(4):
idc.patch_byte(ins + i, encoding[i])
for i in range(4):
idc.patch_byte(last_ins_ea + i, encoding2[i])
flag = True
# 特例处理:如果在有分支跳转的情况下没有找到CSEL指令
# (说明CSEL在下一个块,当前块是做比较和条件跳转的)
if not flag:
ins = idc.prev_head(last_ins_ea) # 定位到倒数第二条指令
succs_list = list(block.succs())
csel_ea = succs_list[0].start_ea # CSEL 指令在后继块
condition = idc.print_operand(csel_ea, 3) # 获取csel指令的条件判断
# 在当前块的倒数第二条指令 patch B.cond
encoding, count = ks.asm(f'B.{condition} {values[0]}', ins)
# 在当前块的最后一条指令 patch B
encoding2, count2 = ks.asm(f'B {values[1]}', last_ins_ea)
try:
for i in range(4):
idc.patch_byte(ins + i, encoding[i])
for i in range(4):
idc.patch_byte(last_ins_ea + i, encoding2[i])
except:
print("except")
# 情况二:只有1个后继,无分支块
else:
# 将块的最后一条指令替换为 B (无条件跳转) 到唯一的后继
encoding, count = ks.asm(f'B {values[0]}', last_ins_ea)
for i in range(4):
idc.patch_byte(last_ins_ea + i, encoding[i])
print("pach over!!!")
def find_all_useless_block(func_ea, real_blocks):
# 查找所有的“无用块”(即混淆块)
blocks = idaapi.FlowChart(idaapi.get_func(func_ea))
local_real_blocks = real_blocks.copy() # 复制一份真实块列表
useless_blocks = [] # 存储无用块
# 找到返回块
ret_block_addr = local_real_blocks[len(local_real_blocks) - 1]
queue = deque()
ret_block = get_block_by_address(ret_block_addr)
queue.append(ret_block)
# 处理ret块相关的后继块(这些块也不是无用块,比如异常处理)
while len(queue) > 0:
cur_block = queue.popleft()
queue.extend(succ for succ in cur_block.succs())
ret_flag = False
for succ in cur_block.succs():
local_real_blocks.append(succ.start_ea) # 将RET的后继也视为“真实”
end_ea = succ.end_ea
last_ins_ea = idc.prev_head(end_ea)
mnem = idc.print_insn_mnem(last_ins_ea)
if mnem == "RET":
ret_flag = True
if ret_flag: # 找到真正的RET就停止
break
# local_real_blocks.extend(succ.start_ea for succ in cur_block.succs())
# 遍历函数的所有块
for block in blocks:
start_ea = block.start_ea
# 如果一个块不在“真实块”列表中,它就是无用块
if start_ea not in local_real_blocks:
useless_blocks.append(start_ea)
print("所有的无用块:", [hex(b) for b in useless_blocks])
return useless_blocks
def patch_useless_blocks(func_ea, real_blocks):
# 将所有找到的无用块用 NOP 填充
useless_blocks = find_all_useless_block(func_ea, real_blocks)
# print(useless_blocks)
for useless_block_addr in useless_blocks:
block = get_block_by_address(useless_block_addr)
start_ea = block.start_ea
end_ea = block.end_ea
insns = idautils.Heads(start_ea, end_ea) # 获取块内所有指令
for ins in insns:
patch_ins_to_nop(ins) # 替换为NOP
print("无用块nop完成")
def main(func_ea):
# 脚本主入口
file_path = idc.get_input_file_path() # 获取当前so路径
# 步骤一:使用IDA API找到所有真实块
all_real_block_list, all_child_prologue_addr, all_child_prologue_last_ins_ea = find_all_real_block(func_ea)
# 步骤二:使用 angr 执行符号分析,获取真实块之间的控制流
patch_list = angr_main(all_real_block_list, all_child_prologue_addr, all_child_prologue_last_ins_ea, func_ea, file_path)
# 步骤三:使用 keystone 重建控制流(Patch)
patch_branch(patch_list)
# 步骤四(可选,已注释):用NOP填充所有无用的混淆块
# patch_useless_blocks(func_ea,all_real_blocks)
# 步骤五(可选,已注释):刷新IDA的函数控制流图
# ida_funcs.reanalyze_function(ida_funcs.get_func(func_ea))
# print("控制流图已刷新")
# 脚本执行入口,分析地址为 0x41D08 的函数
main(0x41D08)
修改版
from collections import deque
import logging
import os
import ida_funcs
import idaapi
import idautils
import idc
try:
import angr
except ImportError as exc:
raise RuntimeError("angr is required inside IDAPython for this script") from exc
try:
import keystone
except ImportError as exc:
raise RuntimeError("keystone is required inside IDAPython for this script") from exc
try:
from tqdm import tqdm
except ImportError:
def tqdm(iterable, *args, **kwargs):
return iterable
logging.getLogger("angr").setLevel(logging.ERROR)
KS = keystone.Ks(keystone.KS_ARCH_ARM64, keystone.KS_MODE_LITTLE_ENDIAN)
def get_imagebase():
return idaapi.get_imagebase()
def ea_to_off(ea):
return ea - get_imagebase()
def off_to_ea(off):
return get_imagebase() + off
def get_block_by_address(ea):
func = idaapi.get_func(ea)
if func is None:
return None
for block in idaapi.FlowChart(func):
if block.start_ea <= ea < block.end_ea:
return block
return None
def get_basic_block_size(bb):
return bb.end_ea - bb.start_ea
def get_last_insn_ea(block):
return idc.prev_head(block.end_ea, block.start_ea)
def get_mnem(ea):
mnem = idc.print_insn_mnem(ea)
return mnem.lower() if mnem else ""
def is_plain_branch_mnem(mnem):
return mnem in {"b", "br"} or mnem.startswith("b.")
def patch_bytes(ea, data):
for i, byte in enumerate(data):
idc.patch_byte(ea + i, byte)
def patch_ins_to_nop(ins_ea):
encoding, _ = KS.asm("NOP", ins_ea)
patch_bytes(ins_ea, encoding)
def add_block_color(ea):
block = get_block_by_address(ea)
if block is None:
return
curr_ea = block.start_ea
while curr_ea != idc.BADADDR and curr_ea < block.end_ea:
idc.set_color(curr_ea, idc.CIC_ITEM, 0xFFCC33)
curr_ea = idc.next_head(curr_ea, block.end_ea)
def del_func_color(func_ea):
func = idaapi.get_func(func_ea)
if func is None:
return
curr_ea = func.start_ea
while curr_ea != idc.BADADDR and curr_ea < func.end_ea:
idc.set_color(curr_ea, idc.CIC_ITEM, 0xFFFFFFFF)
curr_ea = idc.next_head(curr_ea, func.end_ea)
def find_loop_heads(func_ea):
loop_heads = set()
queue = deque()
block = get_block_by_address(func_ea)
if block is None:
return []
queue.append((block, []))
while queue:
cur_block, path = queue.popleft()
if cur_block.start_ea in path:
loop_heads.add(cur_block.start_ea)
continue
next_path = path + [cur_block.start_ea]
queue.extend((succ, next_path) for succ in cur_block.succs())
all_loop_heads = sorted(loop_heads)
return all_loop_heads
def find_converge_addr(loop_head_addr):
block = get_block_by_address(loop_head_addr)
if block is None:
return None
pred_list = list(block.preds())
if len(pred_list) == 2:
for pred in pred_list:
if len(list(pred.preds())) > 1:
return pred.start_ea
return None
return loop_head_addr
def find_ret_block_addr(blocks):
for block in blocks:
succs_list = list(block.succs())
if succs_list:
continue
last_ins_ea = get_last_insn_ea(block)
if get_mnem(last_ins_ea) != "ret":
continue
ori_ret_block = block
while True:
pred_list = list(block.preds())
if len(pred_list) == 1:
block = pred_list[0]
if get_basic_block_size(block) <= 4:
continue
break
break
block2 = block
sub_dispatcher_score = 0
for _ in range(2):
next_blocks = []
for succ in block2.succs():
succ_succs = list(succ.succs())
if succ_succs:
next_blocks.append(succ)
sub_dispatcher_score += 1
if sub_dispatcher_score > 2:
block = ori_ret_block
break
if not next_blocks:
break
block2 = next_blocks[0]
return block.start_ea
return None
def find_all_real_block(func_ea):
func = idaapi.get_func(func_ea)
if func is None:
raise RuntimeError("target address is not inside a function")
blocks = list(idaapi.FlowChart(func))
loop_heads = find_loop_heads(func_ea)
print(f"loop head count: {len(loop_heads)} -> {[hex(loop_head) for loop_head in loop_heads]}")
grouped_real_blocks = []
for loop_head_addr in loop_heads:
loop_head_block = get_block_by_address(loop_head_addr)
if loop_head_block is None:
continue
loop_head_preds_addr = [pred.start_ea for pred in loop_head_block.preds()]
converge_addr = find_converge_addr(loop_head_addr)
real_blocks = []
if converge_addr is not None and loop_head_addr != converge_addr:
prologue_candidates = [ea for ea in loop_head_preds_addr if ea != converge_addr]
real_blocks.extend(prologue_candidates)
converge_block = get_block_by_address(converge_addr if converge_addr is not None else loop_head_addr)
if converge_block is None:
continue
for pred_block in converge_block.preds():
last_ins_ea = get_last_insn_ea(pred_block)
last_mnem = get_mnem(last_ins_ea)
size = get_basic_block_size(pred_block)
if size <= 4 or is_plain_branch_mnem(last_mnem):
continue
start_ea = pred_block.start_ea
first_mnem = get_mnem(start_ea)
if first_mnem == "csel":
for csel_pred in pred_block.preds():
real_blocks.append(csel_pred.start_ea)
else:
real_blocks.append(start_ea)
real_blocks = sorted(set(real_blocks))
if not real_blocks:
print(f"warning: empty real block group for loop head {hex(loop_head_addr)}")
continue
grouped_real_blocks.append(real_blocks)
print("real block group:", [hex(block_ea) for block_ea in real_blocks])
ret_addr = find_ret_block_addr(blocks)
if ret_addr is None:
raise RuntimeError("failed to find return block")
all_real_block_list = []
for real_blocks in grouped_real_blocks:
all_real_block_list.extend(real_blocks)
all_real_block_list.append(ret_addr)
for real_block_ea in all_real_block_list:
add_block_color(real_block_ea)
print("\nall real blocks:")
print("INT:", all_real_block_list)
print("HEX:", [hex(real_block_ea) for real_block_ea in all_real_block_list])
all_child_prologue_addr = [group for group in grouped_real_blocks[1:] if group] if len(grouped_real_blocks) > 1 else []
print("child prologue groups:", [[hex(ea) for ea in group] for group in all_child_prologue_addr])
all_child_prologue_last_ins_ea = []
for child_prologue_array in all_child_prologue_addr:
if not child_prologue_array:
continue
child_prologue_addr = child_prologue_array[0]
child_prologue_block = get_block_by_address(child_prologue_addr)
if child_prologue_block is None:
continue
all_child_prologue_last_ins_ea.append(get_last_insn_ea(child_prologue_block))
print("child prologue last insns:", [hex(ea) for ea in all_child_prologue_last_ins_ea])
return all_real_block_list, all_child_prologue_addr, all_child_prologue_last_ins_ea
def capstone_decode_csel(insn):
operands = insn.op_str.replace(" ", "").split(",")
if len(operands) != 4:
raise ValueError(f"unexpected CSEL operand format: {insn.op_str}")
dst_reg = operands[0]
reg1 = operands[1]
reg2 = operands[2]
condition = operands[3]
return dst_reg, reg1, reg2, condition
def find_state_succ(proj, base, local_state, choose_true, real_blocks, real_block_addr, path, max_steps=0x4000):
insns = local_state.block().capstone.insns
if not insns:
return None
ins = insns[0]
if ins.mnemonic.lower() != "csel":
return None
dst_reg, reg1, reg2, _ = capstone_decode_csel(ins)
val1 = local_state.regs.get(reg1)
val2 = local_state.regs.get(reg2)
sm = proj.factory.simgr(local_state)
sm.step(num_inst=1)
if not sm.active:
return None
tmp_state = sm.active[0]
if choose_true:
setattr(tmp_state.regs, dst_reg, val1)
else:
setattr(tmp_state.regs, dst_reg, val2)
steps = 0
while sm.active and steps < max_steps:
for active_state in sm.active:
ins_offset = active_state.addr - base
if ins_offset in real_blocks:
value = path[real_block_addr]
if ins_offset not in value:
value.append(ins_offset)
return ins_offset
sm.step(num_inst=1)
steps += 1
return None
def find_block_succ(proj, base, state, real_block_addr, real_blocks, path, max_steps=0x4000):
ret_addr = real_blocks[-1]
msm = proj.factory.simgr(state)
search_steps = 0
while msm.active and search_steps < max_steps:
for active_state in msm.active:
offset = active_state.addr - base
if offset != real_block_addr:
continue
mstate = active_state.copy()
msm2 = proj.factory.simgr(mstate)
msm2.step(num_inst=1)
walk_steps = 0
while msm2.active and walk_steps < max_steps:
hit_real_blocks = []
for mactive_state in msm2.active:
ins_offset = mactive_state.addr - base
if ins_offset in real_blocks:
if ins_offset not in hit_real_blocks:
hit_real_blocks.append(ins_offset)
continue
insns = mactive_state.block().capstone.insns
if not insns:
continue
ins = insns[0]
if ins.mnemonic.lower() == "csel":
state_true = mactive_state.copy()
state_true_succ_addr = find_state_succ(
proj,
base,
state_true,
True,
real_blocks,
real_block_addr,
path,
max_steps=max_steps,
)
state_false = mactive_state.copy()
state_false_succ_addr = find_state_succ(
proj,
base,
state_false,
False,
real_blocks,
real_block_addr,
path,
max_steps=max_steps,
)
if state_true_succ_addr is None or state_false_succ_addr is None:
print("csel error at:", hex(ins_offset))
print(
f"bad csel succ: {hex(real_block_addr)} -> "
f"{hex(state_true_succ_addr) if state_true_succ_addr is not None else state_true_succ_addr}, "
f"{hex(state_false_succ_addr) if state_false_succ_addr is not None else state_false_succ_addr}"
)
return "erro"
print(
f"csel branch: {hex(real_block_addr)} -> "
f"{hex(state_true_succ_addr)}, {hex(state_false_succ_addr)}"
)
return
if hit_real_blocks:
unique_hits = []
for hit in hit_real_blocks:
if hit not in path[real_block_addr] and hit not in unique_hits:
unique_hits.append(hit)
if len(unique_hits) > 1 and ret_addr in unique_hits:
unique_hits.remove(ret_addr)
if unique_hits:
chosen = unique_hits[0]
path[real_block_addr].append(chosen)
print(f"direct branch: {hex(real_block_addr)} -> {hex(chosen)}")
return chosen
msm2.step(num_inst=1)
walk_steps += 1
return None
msm.step(num_inst=1)
search_steps += 1
return None
def make_jump_hook(target_addr, skipped_size):
def _hook(state):
state.regs.pc = target_addr - skipped_size
return _hook
def angr_main(real_block_eas, all_child_prologue_eas, all_child_prologue_last_ins_eas, func_ea, file_path):
if not os.path.exists(file_path):
raise RuntimeError(f"input file not found: {file_path}")
real_blocks = [ea_to_off(ea) for ea in real_block_eas]
child_groups = [[ea_to_off(ea) for ea in group] for group in all_child_prologue_eas]
child_last_offsets = [ea_to_off(ea) for ea in all_child_prologue_last_ins_eas]
func_offset = ea_to_off(func_ea)
proj = angr.Project(file_path, auto_load_libs=False)
base = proj.loader.main_object.mapped_base
func_addr = base + func_offset
init_state = proj.factory.blank_state(addr=func_addr)
init_state.options.add(angr.options.CALLLESS)
path = {key: [] for key in real_blocks}
ret_addr = real_blocks[-1]
first_block = proj.factory.block(func_addr)
first_block_insns = first_block.capstone.insns
if not first_block_insns:
raise RuntimeError("failed to decode function entry block")
first_block_last_ins = first_block_insns[-1]
for real_block_addr in tqdm(real_blocks):
if real_block_addr == ret_addr:
continue
prologue_block_addr = None
child_prologue_last_ins_addr = None
for index, child_prologue_array in enumerate(child_groups):
if real_block_addr in child_prologue_array:
prologue_block_addr = base + child_prologue_array[0]
child_prologue_last_ins_addr = base + child_last_offsets[index]
break
state = init_state.copy()
installed_hooks = []
print("finding succ for:", hex(real_block_addr))
try:
if prologue_block_addr is None:
if real_block_addr != func_offset:
proj.hook(
first_block_last_ins.address,
make_jump_hook(base + real_block_addr, first_block_last_ins.size),
first_block_last_ins.size,
)
installed_hooks.append(first_block_last_ins.address)
else:
proj.hook(
first_block_last_ins.address,
make_jump_hook(prologue_block_addr, first_block_last_ins.size),
first_block_last_ins.size,
)
installed_hooks.append(first_block_last_ins.address)
proj.hook(
child_prologue_last_ins_addr,
make_jump_hook(base + real_block_addr, 4),
4,
)
installed_hooks.append(child_prologue_last_ins_addr)
ret = find_block_succ(proj, base, state, real_block_addr, real_blocks, path)
if ret == "erro":
return None
finally:
for hook_addr in installed_hooks:
if proj.is_hooked(hook_addr):
proj.unhook(hook_addr)
hex_dict = {hex(key): [hex(value) for value in values] for key, values in path.items()}
print("real block cfg:\n", hex_dict)
return path
def patch_branch(patch_list):
for block_off, succ_offs in patch_list.items():
if not succ_offs:
continue
block_ea = off_to_ea(block_off)
block = get_block_by_address(block_ea)
if block is None:
continue
start_ea = block.start_ea
end_ea = block.end_ea
last_ins_ea = get_last_insn_ea(block)
succ_eas = [off_to_ea(off) for off in succ_offs]
if len(succ_eas) == 2:
patched_csel = False
for ins_ea in idautils.Heads(start_ea, end_ea):
if get_mnem(ins_ea) != "csel":
continue
condition = idc.print_operand(ins_ea, 3).lower()
encoding_true, _ = KS.asm(f"B.{condition} 0x{succ_eas[0]:x}", ins_ea)
encoding_false, _ = KS.asm(f"B 0x{succ_eas[1]:x}", last_ins_ea)
patch_bytes(ins_ea, encoding_true)
patch_bytes(last_ins_ea, encoding_false)
patched_csel = True
break
if patched_csel:
continue
succs_list = list(block.succs())
if not succs_list:
continue
csel_ea = succs_list[0].start_ea
if get_mnem(csel_ea) != "csel":
print(f"warning: expected csel successor at {hex(csel_ea)}")
continue
cond_branch_ea = idc.prev_head(last_ins_ea, start_ea)
condition = idc.print_operand(csel_ea, 3).lower()
encoding_true, _ = KS.asm(f"B.{condition} 0x{succ_eas[0]:x}", cond_branch_ea)
encoding_false, _ = KS.asm(f"B 0x{succ_eas[1]:x}", last_ins_ea)
patch_bytes(cond_branch_ea, encoding_true)
patch_bytes(last_ins_ea, encoding_false)
else:
encoding, _ = KS.asm(f"B 0x{succ_eas[0]:x}", last_ins_ea)
patch_bytes(last_ins_ea, encoding)
print("patch over")
def find_all_useless_block(func_ea, real_blocks):
func = idaapi.get_func(func_ea)
if func is None:
return []
blocks = list(idaapi.FlowChart(func))
local_real_blocks = list(real_blocks)
useless_blocks = []
ret_block_addr = local_real_blocks[-1]
ret_block = get_block_by_address(ret_block_addr)
if ret_block is None:
return []
queue = deque([ret_block])
visited = set()
while queue:
cur_block = queue.popleft()
if cur_block.start_ea in visited:
continue
visited.add(cur_block.start_ea)
ret_flag = False
for succ in cur_block.succs():
queue.append(succ)
if succ.start_ea not in local_real_blocks:
local_real_blocks.append(succ.start_ea)
last_ins_ea = get_last_insn_ea(succ)
if get_mnem(last_ins_ea) == "ret":
ret_flag = True
if ret_flag:
break
for block in blocks:
if block.start_ea not in local_real_blocks:
useless_blocks.append(block.start_ea)
print("useless blocks:", [hex(b) for b in useless_blocks])
return useless_blocks
def patch_useless_blocks(func_ea, real_blocks):
useless_blocks = find_all_useless_block(func_ea, real_blocks)
for useless_block_addr in useless_blocks:
block = get_block_by_address(useless_block_addr)
if block is None:
continue
for ins_ea in idautils.Heads(block.start_ea, block.end_ea):
patch_ins_to_nop(ins_ea)
print("useless blocks patched to nop")
def main(func_ea):
file_path = idc.get_input_file_path()
all_real_block_list, all_child_prologue_addr, all_child_prologue_last_ins_ea = find_all_real_block(func_ea)
patch_list = angr_main(
all_real_block_list,
all_child_prologue_addr,
all_child_prologue_last_ins_ea,
func_ea,
file_path,
)
if patch_list is None:
print("angr failed to rebuild cfg")
return None
patch_branch(patch_list)
# Optional cleanup:
# patch_useless_blocks(func_ea, all_real_block_list)
# ida_funcs.reanalyze_function(ida_funcs.get_func(func_ea))
return patch_list
def run_at_current_function():
screen_ea = idaapi.get_screen_ea() if hasattr(idaapi, "get_screen_ea") else idc.here()
func = idaapi.get_func(screen_ea)
if func is None:
raise RuntimeError("place the cursor inside the target function and rerun")
return main(func.start_ea)
if __name__ == "__main__":
run_at_current_function()
# Or use a fixed function start:
# main(0x41D08)
虚假控制流
原理/特征
会在代码中添加两种判断条件,一种是恒为FALSE,另一种是恒为TRUE。在下面这张图中,其中的x和y恒实际上恒为0,我们可以发现上面的就是恒为FALSE,下面的是恒为TRUE。
不会到达并被执行的代码块称为不可达的基本块,如上图中的LABEL_10和它下面的几行代码。
这里的x和y由于IDA不知道其真实值,会导致伪代码复杂,这里的x和y称作不透明谓词。
而那些不可达的基本块,需要nop掉。
利用angr符号执行去除不可达的基本块
利用符号执行去混淆的基本思路是:先找到目标函数的所有基本块,再通过符号执行遍历目标函数所有可达的基本块,剩下的就是不可达的基本块。把不可达的基本块全部nop掉,就能使IDA的F5反汇编正常分析。
示例脚本
python deflat.py -f 目标文件名 -s 起始地址 -e 结束地址
import argparse
import angr
import claripy
# 1. 参数解析 (保持不变)
parser = argparse.ArgumentParser()
parser.add_argument('-f','--file', help='The path of binary file')
parser.add_argument('-s','--start', help='Start address (hex)')
parser.add_argument('-e','--end', help='End address (hex)')
args = parser.parse_args()
if not all([args.file, args.start, args.end]):
parser.print_help()
exit(0)
filename = args.file
start_addr = int(args.start, 16)
end_addr = int(args.end, 16)
# 2. 加载与CFG提取
proj = angr.Project(filename, load_options={'auto_load_libs': False})
cfg = proj.analyses.CFGFast()
target_func = cfg.functions.get(start_addr)
if not target_func:
print(f"Error: Function at {hex(start_addr)} not found.")
exit(1)
# 获取函数内所有的基本块地址
target_blocks = set()
for block in target_func.blocks:
if start_addr <= block.addr <= end_addr:
target_blocks.add(block.addr)
# 3. 符号执行 (修复版)
# 建议: 使用 call_state 或给寄存器赋具体值,防止立刻崩溃
state = proj.factory.blank_state(addr=start_addr)
# 示例:如果函数需要参数,可以手动给rdi, rsi赋具体值
# state.regs.rdi = 1
# state.regs.rsi = 2
simgr = proj.factory.simulation_manager(state)
control_flow = set()
control_flow.add(start_addr)
max_steps = 5000 # 必须加步数限制,防止路径爆炸死循环
step_count = 0
while len(simgr.active) > 0 and step_count < max_steps:
# 正确的 step 方式
simgr.step()
step_count += 1
for active_state in simgr.active:
# 记录当前执行到的地址
control_flow.add(active_state.addr)
# 如果跑飞到函数外面了,就丢弃这条路径
if active_state.addr < start_addr or active_state.addr > end_addr:
simgr.move(active_state, simgr.deadended)
# 4. 找出未被执行的块并进行Patch
base_addr = proj.loader.main_object.mapped_base
patched_addrs = []
# 读取原始文件数据
with open(filename, 'rb') as f:
data = bytearray(f.read())
# 简单的NOP填充函数 (以x86/x64为例)
def write_nops(data, offset, size):
for i in range(size):
if offset + i < len(data):
data[offset + i] = 0x90 # x86 NOP
# 正确的Patch逻辑:不在执行路径中的块,直接NOP
for block_addr in target_blocks:
if block_addr not in control_flow:
# 获取该块的大小以计算需要NOP多少字节
block_size = proj.factory.block(block_addr).size
file_offset = block_addr - base_addr
patched_addrs.append(hex(block_addr))
write_nops(data, file_offset, block_size)
# 5. 保存文件
outpath = filename + '_recovered'
with open(outpath, 'wb') as f:
f.write(data)
print(f'[+] Symbolic execution explored {len(control_flow)} blocks.')
print(f'[+] Patched {len(patched_addrs)} unreachable blocks: {patched_addrs}')
print(f'[+] Saved to: {outpath}')
示例脚本二
python deobfu.py -f 目标程序 -s 函数地址
import angr
# 从angr-management(angr的GUI工具)中导入一个图工具,用于简化CFG(控制流图)
from angrmanagement.utils.graph import to_supergraph
# 导入argparse库,用于解析命令行传入的参数(如-f和-s)
import argparse
# 导入logging库,用于控制日志输出级别(后面用来关闭angr的冗余日志)
import logging
# 导入os库,用于处理文件路径和文件名
import os
# def patch_jmp(block, jmp_addr):
# # (获取基本块中最后一条指令)
# insn = block.capstone.insns[-1]
# # (计算指令在文件中的偏移量)
# offset = insn.address - proj.loader.main_object.mapped_base
# # (将原始的 jx/jnx 指令替换为 NOP)
# binfile[offset : offset + insn.size] = b'\x90' * insn.size
# # (修补一个新的 JMP 指令,使其跳转到真实的后继块)
# binfile[offset : offset + 5] = b'\xE9' + (jmp_addr - (insn.address + 5)).to_bytes(4, 'little', signed=True)
# # (打印修补日志)
# print('Patch [%s\t%s] at %#x' % (insn.mnemonic, insn.op_str, insn.address))
# 定义一个函数,用于将一个基本块的所有指令替换为 NOP(空操作)
def patch_nops(block):
# 计算基本块的内存地址(block.addr)相对于文件基址的偏移量,得到文件偏移
offset = block.addr - proj.loader.main_object.mapped_base
# binfile 是一个全局的 bytearray。这行代码将文件中该基本块对应的所有字节,全部替换为 \x90 (NOP)
binfile[offset : offset + block.size] = b'\x90' * block.size
# 打印日志,告知用户哪个地址的基本块被 NOP 了
print('Patch nop at block %#x' % block.addr)
# 定义一个函数,用于获取指定函数地址的控制流图 (CFG)
def get_cfg(func_addr):
# 运行angr的CFGFast分析,normalize=True表示标准化图,force_complete_scan=False表示不进行强制完全扫描
cfg = proj.analyses.CFGFast(normalize=True, force_complete_scan=False)
# 从完整的CFG中,根据函数地址(func_addr)获取特定函数的CFG(转换图)
function_cfg = cfg.functions.get(func_addr).transition_graph
# 使用 to_supergraph 将CFG图进行简化,合并节点
super_cfg = to_supergraph(function_cfg)
# 返回简化后的CFG
return super_cfg
# 定义核心的去混淆函数
def deobfu_func(func_addr):
# 创建一个空的集合(set),用于存放所有基本块的地址
blocks = set()
# 调用 get_cfg 获取目标函数的CFG
cfg = get_cfg(func_addr)
# 遍历CFG中的所有节点(基本块)
for node in cfg.nodes:
# 将每个基本块的地址添加到集合中
blocks.add(node.addr)
# (用于调试) 打印出此函数中所有基本块的地址(此时包含真实块和虚假块)
print([hex(b) for b in blocks])
# 注释:下面开始进行符号执行
# Symbolic execution
# 创建一个angr的“空白状态”,指定符号执行的起始地址为目标函数地址
state = proj.factory.blank_state(addr=func_addr)
# 创建一个“模拟管理器”(Simulation Manager),用于管理和驱动符号执行
simgr = proj.factory.simgr(state)
# 当模拟管理器中还有“活跃”的(即未探索完的)路径时,循环继续
while len(simgr.active):
# 遍历当前所有的活跃路径
for active in simgr.active:
# !!! 核心逻辑 !!!
# active.addr 是当前符号执行“真实”到达的地址
# .discard() 会从集合中移除这个地址。
# 这样,所有真实能到达的块都会被移除
blocks.discard(active.addr)
# --- 下面是为 call 指令添加 Hook,以加速符号执行 ---
# 获取当前活跃状态地址对应的基本块对象
block = proj.factory.block(active.addr)
# 遍历基本块中的每一条指令
for insn in block.capstone.insns:
# 如果指令是一个 call 指令
if insn.mnemonic == 'call':
# 解析 call 指令的操作数(即目标地址),将其转为整数
next_func_addr = int(insn.op_str, 16)
# 在这个 call 的地址上“挂钩”(hook)
# "ReturnUnconstrained" 是一个angr的“桩”(stub)
# 它告诉angr:不要深入分析这个call,假装它执行了并返回一个“任意值”,然后继续
proj.hook(next_func_addr, angr.SIM_PROCEDURES["stubs"]["ReturnUnconstrained"](), replace=True)
# 打印日志,告知用户哪个 call 被 hook 了
print('Hook [%s\t%s] at %#x' % (insn.mnemonic, insn.op_str, insn.address))
# 让模拟管理器“走一步”,即探索所有活跃路径的下一个基本块
simgr.step()
# 当 while 循环结束(所有真实路径都探索完毕),`blocks` 集合中“剩下”的地址就是虚假块
# 遍历所有这些“剩下”的(虚假的)基本块地址
for block_addr in blocks:
# 调用 patch_nops 函数,将这些虚假块在文件中 NOP 掉
patch_nops(proj.factory.block(block_addr))
# 程序的标准入口点
if __name__ == '__main__':
# 注释:关闭警告日志
# Disable warning
# 将cle(angr的加载器)的日志级别设为ERROR,屏蔽无关信息
logging.getLogger('cle').setLevel(logging.ERROR)
# 将angr主库的日志级别设为ERROR,屏蔽无关信息
logging.getLogger('angr').setLevel(logging.ERROR)
# 创建一个命令行参数解析器
parser = argparse.ArgumentParser()
# 添加 '-f' 或 '--file' 参数,必需(required=True),用于指定要分析的文件
parser.add_argument('-f', '--file', required=True, help='File to deobfuscate')
# 添加 '-s' 或 '--start' 参数,用于指定目标函数地址。
# type=lambda x : int(x, 0) 允许输入十进制或十六进制(0x...)
parser.add_argument('-s', '--start', type=lambda x : int(x, 0), help='Starting address of target function')
# 解析命令行传入的参数
args = parser.parse_args()
# 注释:将二进制文件加载到angr
# Load binary file ${file} into angr
# 创建angr项目。args.file 是文件名。auto_load_libs=False 表示不自动加载libc等依赖库,加快分析
proj = angr.Project(args.file, load_options={"auto_load_libs": False})
# 从解析的参数中获取起始地址
start = args.start
# 如果用户没有提供 -s/--start 参数
if start is None:
# 尝试自动在二进制文件中查找 'main' 函数的符号
main = proj.loader.find_symbol('main')
# 如果 'main' 函数也找不到
if main is None:
# 报告错误并退出,提示用户必须提供 -s 参数
parser.error('Can\'t find the main function, please provide argument -s/--start')
# 如果找到了 main,使用 main 函数的重基址地址作为起始地址
start = main.rebased_addr
# 注释:将二进制文件读入内存
# Load binary file ${file} into memory
# 以二进制只读('rb')模式打开目标文件
with open(args.file, 'rb') as file:
# 读取文件的全部内容,并存入一个“可变的” bytearray 中,命名为 binfile
# (必须是 bytearray 才能在内存中进行修补)
binfile = bytearray(file.read())
# 注释:对目标函数执行去混淆操作
# Do deobfuscation on target function
# 调用核心去混淆函数,传入目标函数地址
deobfu_func(func_addr=start)
# 注释:将被修复的二进制文件写入 ${file}_recovered
# Write the recovered binary file to ${file}_recovered
# 将原始文件名(如 "example/example-1")分割为文件名("example/example-1")和扩展名("")
fname, ext = os.path.splitext(args.file)
# 以二进制写入('wb')模式打开一个新文件(文件名后缀为 _recovered)
with open(fname + '_recovered' + ext, 'wb') as file:
# 将我们修补过的(已 NOP 掉虚假块)binfile 的内容写入新文件
file.write(binfile)
# 打印成功信息
print('Deobfuscation success!')
patch不透明谓词
在汇编中将不透明谓词直接 patch 掉,例如对于该不透明谓词 x_9 , y_10 , 它的 c 表达式为 y_10 >= 10 && ((((_BYTE)x_9 - 1) * (_BYTE)x_9) & 1) != 0。

我们要做的就是让 mov eax, ds:x_9 改成 mov eax, 0 , 这样就可以做到消除 BCF 的目的

所以我们可以用 ida python, 通过找到不透明谓词的所有交叉引用的方式来批量修改。
# 去除虚假控制流 idapython脚本
import ida_xref
import ida_idaapi
from ida_bytes import get_bytes, patch_bytes
# 将 mov 寄存器, 不透明谓词 修改为 mov 寄存器, 0
def do_patch(ea):
if get_bytes(ea, 1) == b"\x8B": # mov eax-edi, dword
reg = (ord(get_bytes(ea + 1, 1)) & 0b00111000) >> 3
patch_bytes(ea, (0xB8 + reg).to_bytes(1,'little') + b'\x00\x00\x00\x00\x90')
else:
print('error')
# 不透明谓词在.bss段的范围
start = 0x00428298
end = 0x00428384
for addr in range(start,end,4):
ref = ida_xref.get_first_dref_to(addr)
print(hex(addr).center(20,'-'))
# 获取所有交叉引用
while(ref != ida_idaapi.BADADDR):
do_patch(ref)
print('patch at ' + hex(ref))
ref = ida_xref.get_next_dref_to(addr, ref)
print('-' * 20)
- 划定
.bss一片可疑地址范围; - 遍历每个 4 字节全局变量;
- 找所有数据交叉引用(谁读了这个变量);
- 被
mov reg, [bss_var]读取,后面紧跟test/cmp/jcc条件跳转; - 直接认定:这就是不透明谓词读取指令,直接 patch 成
mov reg,0废掉分支。
最大风险:
- 上面脚本假设 所有 对
.bss变量的 读取 都是为了混淆。如果一段合法代码读取g_critical_data(mov eax, [g_critical_data]),这个脚本也会(错误地)将其 替换为mov eax, 0,导致 IDA 错误地优化掉 合法的 代码分支。
将全局变量赋值并将 segment 设为只读
核心原理:
- 将
.bss段中的所有变量(包括y_10,x_9等)的值都强制 Patch (修补) 成了2。 - 将
.bss段的权限设为 只读。 - 这就“告诉”了 IDA:“这些变量的值是 常量 2,并且它们永远不会变。”
- 因此,当 IDA 分析
if (y_10 >= 10 ...)这样的不透明谓词时,它会执行常量计算:if (2 >= 10 ...)->if (False ...)。 - 最终,IDA 的“死代码消除”(DCE)功能会启动,自动移除这个
if (False ...)对应的虚假分支。
最大风险:
- 下面脚本假设
.bss段中 所有变量 都是用于混淆的。如果.bss中存在一个合法的全局变量g_critical_data,IDA 也会(错误地)认为它的值 永远是2,并可能因此错误地优化掉依赖该变量的 合法代码。
脚本:
import ida_segment
import ida_bytes
import ida_auto
print("--- Starting BSS segment patch script ---")
# 1. 获取 .bss segment
seg = ida_segment.get_segm_by_name('.bss')
if not seg:
print("Error: .bss segment not found.")
else:
print(f"Found .bss at [0x{seg.start_ea:X} - 0x{seg.end_ea:X}]")
# 2. 遍历并 Patch 字节
print("Patching all dwords in .bss to 2...")
patched_count = 0
for ea in range(seg.start_ea, seg.end_ea, 4):
# 使用 patch_dword 来更新 dword 值为 2
ida_bytes.patch_dword(ea, 2)
patched_count += 1
print(f"Patched {patched_count} dwords.")
# 3. 设置段权限为只读
print("Setting segment permissions to Read-Only (R--)...")
seg.perm = 0b100 # R-- (Read=4, Write=2, Exec=1)
# 4. 将权限更改写回 IDA 数据库
ida_segment.update_segm(seg)
print("Segment permissions updated in the database.")
# 5. 强制 IDA 重新分析 .bss 段的数据
print("Forcing re-analysis of all items in .bss...")
for ea in range(seg.start_ea, seg.end_ea, 4):
# 删除此地址上任何旧的数据定义 (相当于按 U 键)
ida_bytes.del_items(ea, ida_bytes.DELIT_SIMPLE, 4)
# 强制 IDA 重新将它定义为 dword (相当于按 D 键)
ida_bytes.create_dword(ea, 4)
print("--- Script finished successfully! ---")
print(">>> ACTION REQUIRED: Go to your function's pseudocode and press F5 to refresh!")
print(">>> If F5 is not enough, go to assembly, press 'U' on the function, then 'P', then F5.")
IDA插件-D-810

当如果发现一些不透明谓词 d810 没有识别到无法去除的话,我们也可以手动添加规则让 D810 进行匹配来消除 BCF。

浙公网安备 33010602011771号