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的优点

  1. 易用性
    • 使用动态插桩,不需要源代码
    • 不需要重新编译和链接
  2. 可扩展性
    • 提供丰富的API
    • 可以使用C/C++编写插桩工具(Pintools)
  3. 多平台支持
    • 支持x86、x86-64、Itanium、Xscale架构
    • 支持Windows、Linux、OSX、Android系统
  4. 鲁棒性
    • 支持插桩现实世界中的应用(数据库、浏览器等)
    • 支持插桩多线程应用
    • 支持信号量处理
  5. 高效性
    • 在指令代码层面实现编译优化

三、Pin的基本结构和原理

Pin由以下组件组成:

  1. Pin:闭源框架,提供API
  2. Pintool:用户使用API编写的动态链接库形式的插件

Pin内部组成

  1. 进程级虚拟机
  2. 代码缓存
  3. 插桩检测API

Pin虚拟机组成

  1. JIT(Just-In-Time)编译器:核心部分,负责对二进制文件中的指令进行插桩
  2. 模拟执行单元
  3. 代码调度器

工作原理

  1. Pin拦截可执行代码的第一条指令
  2. 为后续指令序列生成新的代码(按照用户定义的插桩规则)
  3. 将控制权交给新生成的指令序列并在虚拟机中运行
  4. 当程序进入新分支时,Pin重新获得控制权并为新分支生成代码

Pintool中的关键组件

  1. 插桩代码(Instrumentation code):决定在什么位置插入插桩代码
  2. 分析代码(Analysis code):在选定位置要执行的代码

回调函数类型

  1. Instrumentation routines:仅当事件第一次发生时被调用
  2. Analysis routines:某对象每次被访问时都调用
  3. Callbacks:特定事件发生时都调用

四、Pin的基本用法

1. 编译Pintool

  1. 在Pin解压目录下的source/tools/中创建文件夹MyPintools
  2. mypintool.cpp复制到该目录
  3. 执行编译:

对于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;
}

执行流程

  1. 主函数main中:
    • 初始化PIN_Init()
    • 注册指令粒度回调函数INS_AddInstrumentFunction(Instruction, 0)
    • 注册完成函数(用于输出结果)
    • 启动Pin执行
  2. 在每条指令之前(IPOINT_BEFORE)执行分析函数docount(),递增全局计数器
  3. 执行完成函数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函数,通常需要完成以下功能:

初始化

  1. 初始化Pin系统环境:
    BOOL LEVEL_PINCLIENT::PIN_Init(INT32 argc, CHAR** argv)
    
  2. 初始化符号表(如果需要调用程序符号信息):
    VOID LEVEL_PINCLIENT::PIN_InitSymbols()
    
  3. 初始化同步变量(多线程程序需要):
    VOID LEVEL_BASE::InitLock(PIN_LOCK *lock)
    
    使用示例:
    GetLock(&thread_lock, threadid);
    // 访问全局变量
    ReleaseLock(&thread_lock);
    

注册回调函数

根据不同的粒度注册不同的回调函数:

  1. TRACE粒度

    • 表示一个单入口、多出口的指令序列
    • 分为若干基本块BBL(Basic Block)
    • 常用于记录程序执行序列
    • 注册函数:
      TRACE_AddInstrumentFunction(TRACE_INSTRUMENT_CALLBACK fun, VOID *val)
      
  2. IMG粒度

    • 表示整个被加载进内存的二进制可执行模块
    • 注册函数:
      IMG_AddInstrumentFunction(IMAGECALLBACK fun, VOID *v)
      IMG_AddUnloadFunction(IMAGECALLBACK fun, VOID *v)
      
  3. RTN粒度

    • 代表由编译器产生的函数/例程/过程
    • 需要使用PIN_InitSymbols()初始化符号表
    • 注册函数:
      RTN_AddInstrumentFunction(RTN_INSTRUMENT_CALLBACK fun, VOID *val)
      
  4. INS粒度

    • 代表一条指令
    • 最小的插桩粒度
    • 注册函数:
      INS_AddInstrumentFunction(INS_INSTRUMENT_CALLBACK fun, VOID *val)
      

注册结束回调函数

VOID PIN_AddFiniFunction(FINI_CALLBACK fun, VOID *val)

启动Pin虚拟机

VOID PIN_StartProgram()

2. 插桩和分析函数的编写

在main函数中注册插桩回调函数后,Pin虚拟机将在运行过程中对该种粒度的对象进行选择性插桩。

各种粒度的插桩函数:

  1. INS粒度

    VOID LEVEL_PINCLIENT::INS_InsertCall(INS ins, IPOINT action, AFUNPTR funptr, ...)
    
  2. RTN粒度

    VOID LEVEL_PINCLIENT::RTN_InsertCall(RTN rtn, IPOINT action, AFUNPTR funptr, ...)
    
  3. TRACE粒度

    VOID LEVEL_PINCLIENT::TRACE_InsertCall(TRACE trace, IPOINT action, AFUNPTR funptr, ...)
    
  4. 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分析

  1. 测试密码长度:

    $ 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位
  2. 暴力破解密码:

    • 测试第一位:
      $ 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是一个二进制执行框架,具有两个重要优点:

  1. 可以使用Python调用Pin
  2. 支持符号执行
Pin动态二进制插桩技术详解 一、插桩技术概述 插桩技术是将额外的代码注入程序中以收集运行时的信息,主要分为两种类型: 1. 源代码插桩(Source Code Instrumentation, SCI) 将额外代码注入到程序源代码中 示例: 2. 二进制插桩(Binary Instrumentation, BI) 将额外代码注入到二进制可执行文件中 分为两种: 静态二进制插桩 :在程序执行前插入额外的代码和数据,生成一个永久改变的可执行文件 动态二进制插桩 :在程序运行时实时地插入额外代码和数据,对可执行文件没有任何永久改变 二进制插桩示例 原始汇编代码: 插入指令计数代码: 插入指令跟踪代码: 二、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位架构: 对于64位架构: 2. 使用Pintool 启动并插桩一个应用程序: 绑定并插桩一个正在运行的程序: 五、Pintool示例分析 指令计数工具(inscount0.cpp) 执行流程 主函数 main 中: 初始化 PIN_Init() 注册指令粒度回调函数 INS_AddInstrumentFunction(Instruction, 0) 注册完成函数(用于输出结果) 启动Pin执行 在每条指令之前( IPOINT_BEFORE )执行分析函数 docount() ,递增全局计数器 执行完成函数 Fini() ,输出计数结果到文件 运行示例 其他自带插件及功能 | 插件名 | 功能 | |--------|------| | inscount | 统计执行的指令数量,输出到inscount.out文件 | | itrace | 记录执行指令的eip | | malloctrace | 记录malloc和free的调用情况 | | pinatrace | 记录读写内存的位置和值 | | proccount | 统计Procedure的信息,包括名称、镜像、地址、指令数 | | w_ malloctrace | 记录RtlAllocateHeap的调用情况 | 六、Pintool编写指南 1. main函数的编写 Pintool的入口为main函数,通常需要完成以下功能: 初始化 初始化Pin系统环境: 初始化符号表(如果需要调用程序符号信息): 初始化同步变量(多线程程序需要): 使用示例: 注册回调函数 根据不同的粒度注册不同的回调函数: TRACE粒度 表示一个单入口、多出口的指令序列 分为若干基本块BBL(Basic Block) 常用于记录程序执行序列 注册函数: IMG粒度 表示整个被加载进内存的二进制可执行模块 注册函数: RTN粒度 代表由编译器产生的函数/例程/过程 需要使用 PIN_InitSymbols() 初始化符号表 注册函数: INS粒度 代表一条指令 最小的插桩粒度 注册函数: 注册结束回调函数 启动Pin虚拟机 2. 插桩和分析函数的编写 在main函数中注册插桩回调函数后,Pin虚拟机将在运行过程中对该种粒度的对象进行选择性插桩。 各种粒度的插桩函数: INS粒度 RTN粒度 TRACE粒度 BBL粒度 其中: funptr 为用户自定义的分析函数 参数列表以 IARG_END 标记结束 七、Pin在CTF中的应用 由于程序具有循环、分支等结构,每次运行时执行的指令数量可能不同,可以使用Pin统计执行指令数量进行分析。 示例:密码破解 源代码: 使用inscount0分析 测试密码长度: 1-6位密码:差值21 7位密码:差值7 → 确定密码长度为6位 暴力破解密码: 测试第一位: 第一位为'a'时计数不同 → 确定第一位为'a' 同理可确定其他位 八、扩展:Triton框架 Triton是一个二进制执行框架,具有两个重要优点: 可以使用Python调用Pin 支持符号执行