Pin动态二进制插桩技术详解
字数 2469 2025-08-23 18:31:08
Pin动态二进制插桩技术详解
一、插桩技术概述
插桩技术是将额外的代码注入程序中以收集运行时的信息,主要分为两种类型:
1. 源代码插桩(Source Code Instrumentation, SCI)
- 将额外代码注入到程序源代码中
- 示例:
// 原始程序
void sci() {
int num = 0;
for(int i = 0; i < 100; ++i) {
num += 1;
if(i == 50) {
break;
}
}
printf("%d", num);
}
// 插桩后的程序
char inst[5];
void sci() {
int num = 0;
inst[0] = 1;
for(int i = 0; i < 100; ++i) {
num += 1;
inst[1] = 1;
if(i == 50) {
inst[2] = 1;
break;
}
inst[3] = 1;
}
printf("%d", num);
inst[4] = 1;
}
2. 二进制插桩(Binary Instrumentation, BI)
- 将额外代码注入到二进制可执行文件中
- 分为两种:
- 静态二进制插桩:在程序执行前插入额外的代码和数据,生成一个永久改变的可执行文件
- 动态二进制插桩:在程序运行时实时地插入额外代码和数据,对可执行文件没有任何永久改变
二进制插桩示例
原始汇编代码:
sci:
pushl %ebp
movl %esp, %ebp
pushl %ebx
subl $20, %esp
call __x86.get_pc_thunk.ax
addl $_GLOBAL_OFFSET_TABLE_, %eax
movl $0, -16(%ebp)
movl $0, -12(%ebp)
jmp .L2
插入指令计数代码:
sci:
counter++; pushl %ebp
counter++; movl %esp, %ebp
counter++; pushl %ebx
counter++; subl $20, %esp
counter++; call __x86.get_pc_thunk.ax
counter++; addl $_GLOBAL_OFFSET_TABLE_, %eax
counter++; movl $0, -16(%ebp)
counter++; movl $0, -12(%ebp)
counter++; jmp .L2
插入指令跟踪代码:
sci:
Print(ip); pushl %ebp
Print(ip); movl %esp, %ebp
Print(ip); pushl %ebx
Print(ip); subl $20, %esp
Print(ip); call __x86.get_pc_thunk.ax
Print(ip); addl $_GLOBAL_OFFSET_TABLE_, %eax
Print(ip); movl $0, -16(%ebp)
Print(ip); movl $0, -12(%ebp)
Print(ip); jmp .L2
二、Pin框架简介
Pin是Intel公司研发的动态二进制插桩框架,可以在二进制程序运行过程中插入各种函数以监控程序执行。
1. 版本选择
- 2.x版本:不能在Linux内核4.x及以上版本上运行
- 3.x版本:推荐使用
2. Pin的优点
- 易用性:
- 使用动态插桩,不需要源代码
- 不需要重新编译和链接
- 可扩展性:
- 提供丰富的API
- 可以使用C/C++编写插桩工具(Pintools)
- 多平台支持:
- 支持x86、x86-64、Itanium、Xscale架构
- 支持Windows、Linux、OSX、Android系统
- 鲁棒性:
- 支持插桩现实世界中的应用(数据库、浏览器等)
- 支持插桩多线程应用
- 支持信号量处理
- 高效性:
- 在指令代码层面实现编译优化
三、Pin的基本结构和原理
Pin由以下组件组成:
- Pin:闭源框架,提供API
- Pintool:用户使用API编写的动态链接库形式的插件
Pin内部组成
- 进程级虚拟机
- 代码缓存
- 插桩检测API
Pin虚拟机组成
- JIT(Just-In-Time)编译器:核心部分,负责对二进制文件中的指令进行插桩
- 模拟执行单元
- 代码调度器
工作原理
- Pin拦截可执行代码的第一条指令
- 为后续指令序列生成新的代码(按照用户定义的插桩规则)
- 将控制权交给新生成的指令序列并在虚拟机中运行
- 当程序进入新分支时,Pin重新获得控制权并为新分支生成代码
Pintool中的关键组件
- 插桩代码(Instrumentation code):决定在什么位置插入插桩代码
- 分析代码(Analysis code):在选定位置要执行的代码
回调函数类型
- Instrumentation routines:仅当事件第一次发生时被调用
- Analysis routines:某对象每次被访问时都调用
- Callbacks:特定事件发生时都调用
四、Pin的基本用法
1. 编译Pintool
- 在Pin解压目录下的
source/tools/中创建文件夹MyPintools - 将
mypintool.cpp复制到该目录 - 执行编译:
对于32位架构:
$ make obj-ia32/mypintool.so TARGET=ia32
对于64位架构:
$ make obj-intel64/mypintool.so TARGET=intel64
2. 使用Pintool
启动并插桩一个应用程序:
$ ../../../pin -t obj-intel64/mypintools.so -- application
绑定并插桩一个正在运行的程序:
$ ../../../pin -t obj-intel64/mypintools.so -pid 1234
五、Pintool示例分析
指令计数工具(inscount0.cpp)
#include <iostream>
#include <fstream>
#include "pin.H"
ofstream OutFile;
// 指令计数器
static UINT64 icount = 0;
// 每条指令执行前调用的函数
VOID docount() {
icount++;
}
// Pin遇到新指令时调用的函数
VOID Instruction(INS ins, VOID *v) {
// 在每条指令前插入docount调用
INS_InsertCall(ins, IPOINT_BEFORE, (AFUNPTR)docount, IARG_END);
}
KNOB<string> KnobOutputFile(KNOB_MODE_WRITEONCE, "pintool", "o", "inscount.out", "specify output file name");
// 应用程序退出时调用的函数
VOID Fini(INT32 code, VOID *v) {
OutFile << "Count " << icount << endl;
OutFile.close();
}
// 帮助信息
INT32 Usage() {
cerr << "This tool counts the number of dynamic instructions executed" << endl;
cerr << endl << KNOB_BASE::StringKnobSummary() << endl;
return -1;
}
// 主函数
int main(int argc, char *argv[]) {
// 初始化pin
if (PIN_Init(argc, argv)) return Usage();
OutFile.open(KnobOutputFile.Value().c_str());
// 注册指令插桩函数
INS_AddInstrumentFunction(Instruction, 0);
// 注册结束函数
PIN_AddFiniFunction(Fini, 0);
// 启动程序
PIN_StartProgram();
return 0;
}
执行流程
- 主函数
main中:- 初始化
PIN_Init() - 注册指令粒度回调函数
INS_AddInstrumentFunction(Instruction, 0) - 注册完成函数(用于输出结果)
- 启动Pin执行
- 初始化
- 在每条指令之前(
IPOINT_BEFORE)执行分析函数docount(),递增全局计数器 - 执行完成函数
Fini(),输出计数结果到文件
运行示例
$ pin -ifeellucky -t obj-intel64/inscount0.so -o inscount0.log -- /bin/ls
$ cat inscount0.log
Count 528090
其他自带插件及功能
| 插件名 | 功能 |
|---|---|
| inscount | 统计执行的指令数量,输出到inscount.out文件 |
| itrace | 记录执行指令的eip |
| malloctrace | 记录malloc和free的调用情况 |
| pinatrace | 记录读写内存的位置和值 |
| proccount | 统计Procedure的信息,包括名称、镜像、地址、指令数 |
| w_malloctrace | 记录RtlAllocateHeap的调用情况 |
六、Pintool编写指南
1. main函数的编写
Pintool的入口为main函数,通常需要完成以下功能:
初始化
- 初始化Pin系统环境:
BOOL LEVEL_PINCLIENT::PIN_Init(INT32 argc, CHAR** argv) - 初始化符号表(如果需要调用程序符号信息):
VOID LEVEL_PINCLIENT::PIN_InitSymbols() - 初始化同步变量(多线程程序需要):
使用示例:VOID LEVEL_BASE::InitLock(PIN_LOCK *lock)GetLock(&thread_lock, threadid); // 访问全局变量 ReleaseLock(&thread_lock);
注册回调函数
根据不同的粒度注册不同的回调函数:
-
TRACE粒度
- 表示一个单入口、多出口的指令序列
- 分为若干基本块BBL(Basic Block)
- 常用于记录程序执行序列
- 注册函数:
TRACE_AddInstrumentFunction(TRACE_INSTRUMENT_CALLBACK fun, VOID *val)
-
IMG粒度
- 表示整个被加载进内存的二进制可执行模块
- 注册函数:
IMG_AddInstrumentFunction(IMAGECALLBACK fun, VOID *v) IMG_AddUnloadFunction(IMAGECALLBACK fun, VOID *v)
-
RTN粒度
- 代表由编译器产生的函数/例程/过程
- 需要使用
PIN_InitSymbols()初始化符号表 - 注册函数:
RTN_AddInstrumentFunction(RTN_INSTRUMENT_CALLBACK fun, VOID *val)
-
INS粒度
- 代表一条指令
- 最小的插桩粒度
- 注册函数:
INS_AddInstrumentFunction(INS_INSTRUMENT_CALLBACK fun, VOID *val)
注册结束回调函数
VOID PIN_AddFiniFunction(FINI_CALLBACK fun, VOID *val)
启动Pin虚拟机
VOID PIN_StartProgram()
2. 插桩和分析函数的编写
在main函数中注册插桩回调函数后,Pin虚拟机将在运行过程中对该种粒度的对象进行选择性插桩。
各种粒度的插桩函数:
-
INS粒度
VOID LEVEL_PINCLIENT::INS_InsertCall(INS ins, IPOINT action, AFUNPTR funptr, ...) -
RTN粒度
VOID LEVEL_PINCLIENT::RTN_InsertCall(RTN rtn, IPOINT action, AFUNPTR funptr, ...) -
TRACE粒度
VOID LEVEL_PINCLIENT::TRACE_InsertCall(TRACE trace, IPOINT action, AFUNPTR funptr, ...) -
BBL粒度
VOID LEVEL_PINCLIENT::BBL_InsertCall(BBL bbl, IPOINT action, AFUNPTR funptr, ...)
其中:
funptr为用户自定义的分析函数- 参数列表以
IARG_END标记结束
七、Pin在CTF中的应用
由于程序具有循环、分支等结构,每次运行时执行的指令数量可能不同,可以使用Pin统计执行指令数量进行分析。
示例:密码破解
源代码:
#include <stdio.h>
#include <string.h>
void main() {
char pwd[] = "abc123";
char str[128];
int flag = 1;
scanf("%s", str);
for(int i = 0; i <= strlen(pwd); i++) {
if(pwd[i] != str[i] ||
str[i] == '\0' && pwd[i] != '\0' ||
str[i] != '\0' && pwd[i] == '\0') {
flag = 0;
}
}
if(flag == 0) {
printf("Bad!\n");
} else {
printf("Good!\n");
}
}
使用inscount0分析
-
测试密码长度:
$ echo x | pin -ifeellucky -t obj-intel64/inscount0.so -o inscount.out -- ~/a.out; cat inscount.out Bad! Count 152667 $ echo xx | pin -ifeellucky -t obj-intel64/inscount0.so -o inscount.out -- ~/a.out; cat inscount.out Bad! Count 152688 $ echo xxxxxx | pin -ifeellucky -t obj-intel64/inscount0.so -o inscount.out -- ~/a.out; cat inscount.out Bad! Count 152772 $ echo xxxxxxx | pin -ifeellucky -t obj-intel64/inscount0.so -o inscount.out -- ~/a.out; cat inscount.out Bad! Count 152779- 1-6位密码:差值21
- 7位密码:差值7 → 确定密码长度为6位
-
暴力破解密码:
- 测试第一位:
$ echo axxxxx | pin -ifeellucky -t obj-intel64/inscount0.so -o inscount.out -- ~/a.out; cat inscount.out Bad! Count 152786 $ echo bxxxxx | pin -ifeellucky -t obj-intel64/inscount0.so -o inscount.out -- ~/a.out; cat inscount.out Bad! Count 152772- 第一位为'a'时计数不同 → 确定第一位为'a'
- 同理可确定其他位
- 测试第一位:
八、扩展:Triton框架
Triton是一个二进制执行框架,具有两个重要优点:
- 可以使用Python调用Pin
- 支持符号执行