C++ 动态二进制插桩(DBI):`Pin`, `DynamoRIO` 框架的应用

哈喽,各位好!今天咱们来聊聊一个听起来很高级,但其实掌握了也没那么神秘的技术——C++ 动态二进制插桩(DBI)。我们会重点关注两个非常流行的框架:Pin 和 DynamoRIO。

什么是动态二进制插桩(DBI)?

简单来说,DBI 就像一个“间谍”,它可以在程序运行的时候,悄悄地观察甚至修改程序的行为。它不需要程序的源代码,也不需要重新编译。 这就像你在看一场电影,DBI 就像一个影评人,他可以实时地告诉你演员在想什么,剧情下一步会怎么发展,甚至可以修改剧本,让男女主角幸福地生活在一起(当然,这可能破坏了导演的意图)。

为什么要用 DBI?

DBI 的应用场景非常广泛,主要包括:

  • 性能分析: 找出程序的瓶颈,优化代码。
  • 安全研究: 检测恶意代码,发现漏洞。
  • 程序调试: 在运行时动态地调试程序,观察变量的值,函数的调用关系。
  • 动态优化: 根据程序运行时的行为,动态地调整程序的执行路径,提高性能。
  • 代码覆盖率测试: 统计程序执行时覆盖了哪些代码。

Pin 和 DynamoRIO:两个强大的 DBI 框架

Pin 和 DynamoRIO 是两个最流行的 DBI 框架。它们都提供了强大的 API,可以让你轻松地实现各种插桩任务。

特性 Pin DynamoRIO
开发商 Intel VMware (最初由 MIT 开发)
授权协议 免费,但有使用限制 BSD 协议
平台支持 x86, x86-64, IA-64, ARM (部分支持) x86, x86-64, ARM (更广泛的支持)
编程语言 C++ C, C++
API 复杂度 相对简单易用 更加灵活,但也更复杂
文档完整性 官方文档非常详细 文档相对分散,但社区活跃
性能开销 一般来说,Pin 的开销略低于 DynamoRIO 可以通过优化减少开销
工具生态系统 拥有庞大的工具生态系统,很多现成的工具可用 也有一些工具,但相对较少

Pin 入门:你的第一个 Pin 工具

让我们从 Pin 开始。Pin 的 API 设计得非常友好,即使是新手也能很快上手。

1. 安装 Pin

首先,你需要从 Intel 的网站下载 Pin 的 SDK。下载地址是:https://software.intel.com/content/www/us/en/develop/articles/pin-a-dynamic-binary-instrumentation-tool.html

下载后,解压 SDK,设置环境变量 PIN_ROOT 指向 SDK 的根目录。

2. 创建你的第一个 Pin 工具

我们来写一个简单的 Pin 工具,它可以打印出程序中每个函数的地址和名称。

#include <iostream>
#include <fstream>
#include <pin.H>

using namespace std;

ofstream OutFile;

// This function is called before every instruction is executed
VOID Instruction(INS ins, VOID *v)
{
    OutFile << INS_Address(ins) << " " << INS_Disassemble(ins) << endl;
}

// This function is called when the application exits
VOID Fini(INT32 code, VOID *v)
{
    OutFile.close();
}

// This function is called when a new image is loaded
VOID ImageLoad(IMG img, VOID *v)
{
    OutFile << "Image loaded: " << IMG_Name(img) << " at " << IMG_LowAddress(img) << endl;

    for (SEC sec = IMG_SecHead(img); SEC_Valid(sec); sec = SEC_Next(sec))
    {
        OutFile << "tSection: " << SEC_Name(sec) << " at " << SEC_Address(sec) << endl;
    }

    for (RTN rtn = IMG_RtnHead(img); RTN_Valid(rtn); rtn = RTN_Next(rtn))
    {
        RTN_Open(rtn);
        OutFile << "tRoutine: " << RTN_Name(rtn) << " at " << RTN_Address(rtn) << endl;
        RTN_Close(rtn);
    }
}

/* ===================================================================== */
/* Print Help Message                                                    */
/* ===================================================================== */

INT32 Usage()
{
    cerr << "This tool prints the address of every instruction executed" << endl;
    cerr << endl << KNOB_BASE::StringKnobSummary() << endl;
    return -1;
}

/* ===================================================================== */
/* Main                                                                  */
/* ===================================================================== */

int main(int argc, char * argv[])
{
    // Initialize symbol table code, needed for rtn names
    PIN_InitSymbols();

    // Initialize PIN library.  打印用法
    if (PIN_Init(argc, argv))
    {
        return Usage();
    }

    OutFile.open("inscount.out");

    // Register Instruction to be called to instrument instructions
    INS_AddInstrumentFunction(Instruction, 0);

    // Register ImageLoad to be called when each image is loaded.
    IMG_AddInstrumentFunction(ImageLoad, 0);

    // Register Fini to be called when the application exits
    PIN_AddFiniFunction(Fini, 0);

    // Start the program, never returns
    PIN_StartProgram();

    return 0;
}

代码解释:

  • #include <pin.H>: 包含 Pin 的头文件。
  • ofstream OutFile;: 定义一个输出文件流。
  • Instruction(INS ins, VOID *v): 这是一个插桩函数,它会在每个指令执行前被调用。INS ins 代表当前指令的对象。我们使用 INS_Address(ins) 获取指令的地址,INS_Disassemble(ins) 获取指令的反汇编代码,然后将它们输出到文件中。
  • Fini(INT32 code, VOID *v): 这是一个 fini 函数,它会在程序退出时被调用。我们在这里关闭输出文件。
  • ImageLoad(IMG img, VOID *v): 这是一个 image load 函数,它会在每个 image (例如可执行文件或动态链接库) 加载时被调用。IMG img 代表当前 image 的对象。 我们使用 IMG_Name(img) 获取 image 的名称,IMG_LowAddress(img) 获取 image 的加载地址。 遍历 image 中的 section 和 routine (函数),并输出它们的名称和地址。
  • PIN_Init(argc, argv): 初始化 Pin 库。
  • INS_AddInstrumentFunction(Instruction, 0): 注册 Instruction 函数,使其在每个指令执行前被调用。
  • IMG_AddInstrumentFunction(ImageLoad, 0): 注册 ImageLoad 函数,使其在每个 image 加载时被调用。
  • PIN_AddFiniFunction(Fini, 0): 注册 Fini 函数,使其在程序退出时被调用。
  • PIN_StartProgram(): 启动程序。

3. 编译 Pin 工具

在 Pin 的 SDK 目录下,找到 source/tools/ManualExamples 目录,将上面的代码保存为 inscount0.cpp。然后,使用 Pin 提供的 Makefile 编译它:

make obj-intel64/inscount0.so TARGET=intel64

4. 运行 Pin 工具

假设你要分析的程序是 /bin/ls,你可以这样运行 Pin 工具:

pin -t obj-intel64/inscount0.so -- /bin/ls -l

这条命令的意思是:使用 Pin 工具 obj-intel64/inscount0.so 来分析程序 /bin/ls -l

运行结束后,你会看到一个名为 inscount.out 的文件,里面包含了程序中每个指令的地址和名称。

DynamoRIO 入门:更灵活的选择

DynamoRIO 相比 Pin 更加灵活,但也更复杂。它提供了更底层的 API,可以让你更精细地控制插桩过程。

1. 安装 DynamoRIO

从 DynamoRIO 的网站下载 SDK:https://dynamorio.org/

下载后,解压 SDK,设置环境变量 DYNAMORIO_HOME 指向 SDK 的根目录。

2. 创建你的第一个 DynamoRIO 客户端

我们来写一个简单的 DynamoRIO 客户端,它可以打印出程序中每个基本块(basic block)的起始地址。

#include "dr_api.h"
#include <stdio.h>

static void
bb_event(void *drcontext, void *tag, instrlist_t *bb,
         bool for_trace, bool translating)
{
    instr_t *instr = instrlist_first(bb);
    app_pc pc = instr_get_app_pc(instr);

    dr_fprintf(STDERR, "Basic Block at address: %pn", pc);
}

static void
event_exit(void)
{
    dr_fprintf(STDERR, "Exiting DynamoRIOn");
}

DR_EXPORT void
dr_client_main(client_id_t id, int argc, const char *argv[])
{
    dr_fprintf(STDERR, "Hello from DynamoRIO client!n");

    /* register event to be called before every basic block */
    dr_register_bb_event(bb_event);

    /* register event to be called at process exit */
    dr_register_exit_event(event_exit);
}

代码解释:

  • #include "dr_api.h": 包含 DynamoRIO 的头文件。
  • bb_event(void *drcontext, void *tag, instrlist_t *bb, bool for_trace, bool translating): 这是一个基本块事件处理函数,它会在每个基本块执行前被调用。bb 代表当前基本块的指令列表。我们使用 instrlist_first(bb) 获取基本块中的第一条指令,然后使用 instr_get_app_pc(instr) 获取指令的地址,也就是基本块的起始地址。
  • event_exit(void): 这是一个退出事件处理函数,它会在程序退出时被调用。
  • dr_client_main(client_id_t id, int argc, const char *argv[]): 这是 DynamoRIO 客户端的入口函数。id 是客户端的 ID,argcargv 是命令行参数。
  • dr_register_bb_event(bb_event): 注册 bb_event 函数,使其在每个基本块执行前被调用。
  • dr_register_exit_event(event_exit): 注册 event_exit 函数,使其在程序退出时被调用。

3. 编译 DynamoRIO 客户端

在 DynamoRIO 的 SDK 目录下,创建一个名为 my_client 的目录,将上面的代码保存为 my_client.c。然后,创建一个名为 Makefile 的文件,内容如下:

TARGET = my_client
SOURCES = my_client.c

include $(DYNAMORIO_HOME)/makefile.mk

然后,运行 make 命令编译客户端:

make

4. 运行 DynamoRIO 客户端

假设你要分析的程序是 /bin/ls,你可以这样运行 DynamoRIO 客户端:

./bin64/drrun -c my_client/my_client.so /bin/ls -l

这条命令的意思是:使用 DynamoRIO 运行程序 /bin/ls -l,并加载客户端 my_client/my_client.so

运行结束后,你会在终端看到程序中每个基本块的起始地址。

Pin 和 DynamoRIO 的选择

Pin 和 DynamoRIO 都有各自的优点和缺点。选择哪个框架取决于你的具体需求。

  • 如果你需要快速上手,并且需要使用现成的工具,Pin 是一个不错的选择。 Pin 的 API 比较简单易用,而且拥有庞大的工具生态系统。
  • 如果你需要更精细地控制插桩过程,并且需要更高的灵活性,DynamoRIO 是一个更好的选择。 DynamoRIO 提供了更底层的 API,可以让你实现各种复杂的插桩任务。

一些高级技巧

  • 条件插桩: 你可以根据程序的运行状态,动态地决定是否进行插桩。例如,你可以只在某个函数被调用时才进行插桩。
  • 代码缓存: DynamoRIO 允许你修改程序的代码,并将修改后的代码缓存起来,以便下次执行时直接使用。这可以大大提高性能。
  • 多线程插桩: Pin 和 DynamoRIO 都支持多线程插桩。你需要注意线程安全问题,避免出现竞争条件。
  • 使用符号表: Pin 和 DynamoRIO 都支持使用符号表。你可以使用符号表来获取函数和变量的名称,方便你进行分析。

总结

DBI 是一项强大的技术,可以让你深入了解程序的行为。Pin 和 DynamoRIO 是两个最流行的 DBI 框架,它们都提供了强大的 API,可以让你轻松地实现各种插桩任务。希望今天的讲解能够帮助你入门 DBI,并在你的实际工作中发挥作用。 记住,实践是最好的老师!多写代码,多尝试,你就能掌握 DBI 这项强大的技术。

有什么问题,欢迎随时提问! 祝大家学习愉快!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注