9.1 概念
在代码任意位置插入自定义代码,实时监控或修改。
静态插桩:直接修改二进制代码,例如修改call,插入监控代码。
动态插桩:运行时插入代码,不修改原代码。
while program_running: next_instruction = fetch() # 获取下条指令 if is_monitor_point(next_instruction): # 判断是否插桩点 inject_code() # 注入监测代码 execute(next_instruction) # 执行原指令
9.2 静态二进制插桩
9.2.1 int3方法
概念:假设我们要在高速公路(程序执行流)上设置检查站。INT3方法就像在某个检查点放置一个临时路障(0xCC指令),当车辆(CPU)经过时会停车(触发断点),此时我们记录车辆信息(数据采集)后移开路障,恢复通行。
示例:
程序执行 – 原始指令
mov eax, ebx ; 机器码 8B C3
add eax, 5 ; 机器码 83 C0 05
程序执行 – int3指令(替换原始指令) – 操作系统触发中断 – 插桩处理器 – 插桩代码 – 原始指令(恢复执行)
步骤1:插入断点
int3 ; 替换mov为0xCC → 新机器码 CC C3
add eax, 5
步骤2:断点处理
当CPU执行到0xCC时:
触发断点异常
执行我们预装的”桩代码”:
printf(“eax当前值:%d\n”, eax); // 记录寄存器值
memcpy(0x1234, original_opcode, 2); // 恢复原mov指令
9.2.2 跳板方法-jmp
概念:这次我们不破坏原有道路,而是在路边新建一个服务站(桩代码段)。当车辆经过特定路口时,指示牌(JMP指令)引导车辆进入服务站完成检查,再返回主路。
原始代码段(地址0x400500):
cmp edx, 10 ; 机器码 83 FA 0A
jle label ; 机器码 7E 02
步骤1:插入跳板
jmp 0x500000 ; 新地址 → 机器码 E9 FB FA AF 00
nop ; 90(填充字节)
步骤2:在新地址(0x500000)写入桩代码
// 桩代码区
pushfq ; 保存标志寄存器
push rax
printf(“edx值:%d\n”, edx); // 记录数据
pop rax
popfq
// 原始指令
cmp edx, 10 ; 还原被覆盖的指令
jle label ; 原地址+5 → 0x400505
9.3 动态二进制插桩
9.3.1 DBI 系统的体系结构
包含:DBI引擎(大脑)、DBI工具(用户编写的自定义逻辑)、代码缓存(高速执行区)
工作流程:初始化(DBI引擎启动,加载DBI工具) – 插桩与编译(JIT编译器加工代码) – 在代码缓存中执行 – 控制流转移
9.3.2 Pin 介绍
Intel开发的一个DBI引擎(大脑)
两种模式:
JIT模式(运行时插桩和编译)
探针模式(预先一次性插桩所有代码,直接运行)
函数角色:
插桩例程:告诉pin在哪里以及插入什么
分析例程:实际的监控和分析逻辑
9.4 使用 Pin 进行分析
9.4.1 Profiler工具的数据结构和创建代码
目标:性能分析工具(Profiler)是如何用pin构建的
费曼方式的概念:搭建一个监控系统的框架——定义要监控什么(数据结构),设置监控选项(Knob),然后告诉监控系统什么时候开始工作(各种注册函数)
9.4.2 解析函数符号
目标:让Profiler能识别函数的名字,而不只是冷冰冰的地址。
原理:程序加载 – pin调用parse_funcsyms函数 – 该函数遍历所有节,和节中每个函数 – 把函数地址和名字存储到funcnames映射表里
9.4.3 插桩基本块
目标:统计程序执行了多少条指令
步骤:关注程序的基本块,忽略库代码 – 对每个块用instrument_bb来插桩 – 使用BBL_InsertCall插入回调函数,传递基本块中的指令数
9.4.4 检测控制流指令
目标:统计分析程序的控制流转移。使用INS_IsBranchOrCall判断是否是控制流指令。
三种场景:路遇红灯举例
跳转边:遇到了岔路口 – 绿灯 – 右转 (岔路口要转)
直行边:遇到了岔路口 – 红灯 – 选择等待 – 代表要继续执行 (岔路口选择直行,控制流不转弯)
函数调用:回家 – 突然想喝水 – 拐进便利店买水 – 买完水 – 回到原来路线回家
9.4.5 指令、控制转移及系统调用计数
目标:写出一个用来分析的函数,这个函数要被运行成千上万次,要求快速简洁
四个核心函数:
count_bb_insns:统计指令数(只是简单地把基本块的指令数加到总计数)
count_cflow:统计控制流转移,记录源地址和目标地址
count_call:统计函数调用
log_syscall:统计系统调用(通过PIN_GetSyscallNumber获取系统调用号)
9.4.6 测试Profiler
9.5 用 Pin 自动对二进制文件脱壳
9.5.1 可执行文件加壳器简介
构建自动脱壳器
9.5.2 脱壳器的配置代码及其使用的数据结构
原理:监视程序,当检测到“跳转到之前被写入的内存区域”时,就认为找到了OEP,然后转储内存
关键数据结构:
mem_access_t:记录内存字节的访问信息
w:是否被写入过
x:是否被执行过
val:存储的字节值
mem_cluster_t:合并相邻内存区域
base/size:内存块的起始地址和大小
w/x:访问权限
全局变量:
shadow_mem:影子内存,记录所有内存写入
clusters:存储发现的内存块
saved_addr:临时保存内存地址
main函数流程:初始化Pin – 注册内存和控制流插桩函数 – 启动被加壳程序
9.5.3 对内存写入插桩
目标:记录程序对所有内存的写入操作
挑战:Pin在内存写入之前知道地址,但是写入之后才知道具体写入值
解决:
写入前(IPOINT_BEFORE):调用queue_memwrite保存写入地址到saved_addr
写入后(IPOINT_AFTER或跳转边):调用log_memwrite记录实际写入的字节值
9.5.4 插桩控制流指令
目标:检测跳转到OEP的时刻
策略:只监控间接跳转指令
关键判断:如果跳转目标是曾被写入且未被处理过的内存,就可能是OEP!
实现:
使用INS_IsIndirectBranchOrCall检测间接控制流指令
插入check_indirect_ctransfer回调函数
这个函数会检查目标地址是否在之前被写入过的内存区域
9.5.5 跟踪内存写入
具体实现内存记录:
queue_memwrite函数:极其简单,只是保存地址:saved_addr = addr
log_memwrite函数:
从saved_addr开始,遍历所有被写入的字节
在shadow_mem中标记这些地址为”已写入”
使用PIN_SafeCopy安全地复制字节值
9.5.6 检测原始入口点并转储脱壳二进制文件
if (目标地址曾被写入过 AND 该地址不在已处理区域中):
# 很可能找到了OEP!
创建内存块包含目标地址周围的连续区域
将该区域标记为”可执行”
将内存块添加到已发现列表
转储该内存区域到文件
参考报告
DynamoRio Github地址:https://github.com/DynamoRIO/dynamorio
DynamoRio函数说明文档:https://dynamorio.org/files.html
利用 DynamoRIO 追踪和操控反分析技术:https://0xreverse.com/tracing-and-manipulating-anti-analysis-techniques-with-dynamorio
使用 DynamoRio 进行动态二进制插桩 (DBI):https://blog.talosintelligence.com/dynamic-binary-instrumentation-dbi-with-dynamorio/
利用动态二进制插桩框架Frida进行恶意软件分析:https://blogs.blackberry.com/en/2021/04/malware-analysis-with-dynamic-binary-instrumentation-frameworks
