65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 Intel Pin 热补丁 C/C++ 函数

starIconstarIconstarIconstarIconstarIcon

5.00/5 (5投票s)

2019年8月27日

CPOL

5分钟阅读

viewsIcon

8170

本文讨论了使用 Intel Pin 热补丁 C/C++ 函数以消除已知漏洞的想法。

五年前,我在一篇文章中说过,我有一天会带着一种在 实时进程中热补丁函数的方法回归;所以…我想就是今天了。

我们在这里要实现的目标是 从外部替换运行中的 可执行文件中的函数,而不停止/冻结进程(或使其崩溃…)。

在我看来,如果 从头开始实现 热补丁,这是一项相当艰巨的任务,因为

  • 它需要访问不同进程的内存(大多数操作系统都倾向于 进程隔离
  • 存在软件兼容性限制(Windows 二进制文件 vs Linux 二进制文件)
  • 存在架构兼容性限制(32 位 vs 64 位
  • 它涉及到处理机器码,并带来一些问题
  • 它仅具有教学目的 — probably no one would actually use a ‘from-scratch’ method since there are tools that do this better(大概没有人会真正使用“从头开始”的方法,因为有更好的工具可以做到这一点)

考虑到这些,我认为最好使用真正为此任务编写的东西,而不是手动编写。因此,我们将通过 Intel Pin 来实现这一点。我在做一个完全不同的项目时偶然发现了这个工具,但它似乎非常通用。基本上,它被描述为一个 动态二进制插桩工具,然而我们将使用它来促进向另一个进程的内存写入代码的过程。

1. 初始准备

首先 下载 Intel Pin 并将其解压到您的工作区中的某个位置。

免责声明:我将在 Ubuntu x86_64 上进行此教程。您可能会看到一些 bash 命令。

现在,我想象一下,这对于为客户端提供远程服务的端点很有用 — 例如:服务器接收某种输入并期望也返回一些东西。比方说,有人发现某个服务容易受到特定输入的攻击 — 那么第一个提交特制请求的攻击者就可以将其攻陷。我们认为,停止服务、编译、部署和启动新实例不是一个理想的解决方案,因此在准备好新版本之前,就需要进行热补丁。

我将使用以下 示威 C 程序来说明上述模型 — 为了简单起见,我从 stdin 读取输入(而不是 TCP 流/网络)。

#include <stdio.h>

// TODO: hot patch this method
void read_input()
{
    printf("Tell me your name:\n");
    
    char name[11];
    scanf("%s", name); // this looks bad
    
    printf("Hello, %s!\n\n", name);
}

int main()
{
    // not gonna end too soon
    while(1 == 1)
        read_input();
    
    return 0;
}

你们中的一些人可能已经注意到 read_input() 函数写得不太好,因为它使用 scanf("%s", name); 来读取输入,从而使攻击者能够利用 缓冲区溢出来劫持程序的执行。

Scanf() 读取超出了分配缓冲区的限制

我们的目标是通过“替换”有漏洞的读取函数 (read_input()) 为我们知道实际上是安全的函数来修补此漏洞。我在这里使用引号来表达事实,它更像是一个重定向过程 — 原始(有漏洞)函数的代码仍然在进程的内存中,但所有调用都将被转发到新的(已修补的)方法。

希望现在大家都能明白。

2. 项目结构

Intel Pin 的工作方式是执行 工具中指示的操作,针对 二进制文件进程。例如,您可能有一个工具,它会说“每次找到 RET 指令时增加计数器”,您可以将其附加到可执行文件并获取计数器在特定时间的值。

它提供了一个包含 工具示例的目录,位于:pin/source/tools/。为了避免更新 makefile 依赖项,我们将在这里工作,所以请继续创建一个新目录(我将其命名为 Hotpatch)— 这里就是编写代码的地方。

另外,如果您不想自己写一个 makefile,请复制一个到您的新目录。

cp ../SimpleExamples/makefile .

并使用以下内容作为您的 makefile.rules 文件

TEST_TOOL_ROOTS := hotpatch # for hotpatch.cpp
SANITY_SUBSET := $(TEST_TOOL_ROOTS) $(TEST_ROOTS)

最后,创建一个名为 hotpatch.cpp 的文件,其中包含一些示威代码,然后运行 make 命令。如果一切正常,您应该会看到类似这样的结果…

Hot Patch 工具的目录结构

3. 编写热补丁

整个想法围绕着注册一个 回调,该回调在二进制加载图像时被调用(参见 IMG_AddInstrumentFunction())。由于方法定义在运行的程序中,我们对进程加载自身图像时感兴趣。在此回调中,我们查找要 热补丁(替换)的方法 — 在我的示例中,它是 read_input()

您可以使用以下方法列出二进制文件中存在的函数

nm targeted_binary_name

替换函数的过程(RTN_ReplaceSignatureProbed())基于 探针 — 如名称所示,根据 Intel 的说法,探针可确保较低的开销且侵入性较小。在底层,Intel Pin 将用一个指向替换函数的 JMP 指令覆盖原始函数的指令。如果您需要,可以调用原始函数。

话不多说,我最终的代码

#include "pin.H"
#include <iostream>
#include <stdio.h>


char target_routine_name[] = "read_input";

// replacement routine's code (i.e. patched read_input)
void read_input_patched(void *original_routine_ptr, int *return_address)
{
    printf("Tell me your name:\n");
    
    // 5 stars stdin reading method
    char name[12] = {0}, c;
    fgets(name, sizeof(name), stdin);
    name[strcspn(name, "\r\n")] = 0;

    // discard rest of the data from stdin
    while((c = getchar()) != '\n' && c != EOF);

    printf("Hello, %s!\n\n", name);
}

void loaded_image_callback(IMG current_image, void *v)
{
    // look for the routine in the loaded image
    RTN current_routine = RTN_FindByName(current_image, target_routine_name);
    
    // stop if the routine was not found in this image
    if (!RTN_Valid(current_routine))
        return;

    // skip routines which are unsafe for replacement
    if (!RTN_IsSafeForProbedReplacement(current_routine))
    {
        std::cerr << "Skipping unsafe routine " << target_routine_name << 
                     " in image " << IMG_Name(current_image) << std::endl;
        return;
    }

    // replacement routine's prototype: returns void, default calling standard, 
    // name, takes no arugments 
    PROTO replacement_prototype = PROTO_Allocate(PIN_PARG(void), 
           CALLINGSTD_DEFAULT, target_routine_name, PIN_PARG_END());

    // replaces the original routine with a jump to the new one 
    RTN_ReplaceSignatureProbed(current_routine, 
                               AFUNPTR(read_input_patched), 
                               IARG_PROTOTYPE, 
                               replacement_prototype,
                               IARG_ORIG_FUNCPTR,
                               IARG_FUNCARG_ENTRYPOINT_VALUE, 0,
                               IARG_RETURN_IP,
                               IARG_END);

    PROTO_Free(replacement_prototype);

    std::cout << "Successfully replaced " << target_routine_name << 
                 " from image " << IMG_Name(current_image) << std::endl;
}

int main(int argc, char *argv[])
{
    PIN_InitSymbols();

    if (PIN_Init(argc, argv))
    {
        std::cerr << "Failed to initialize PIN." << std::endl; 
        exit(EXIT_FAILURE);
    }

    // registers a callback for the "load image" action
    IMG_AddInstrumentFunction(loaded_image_callback, 0);
    
    // runs the program in probe mode
    PIN_StartProgramProbed();
    
    return EXIT_SUCCESS;
}

在运行 make 后,使用类似以下的命令将 Intel Pin 附加到目标进程的正在运行的实例。

sudo ../../../pin -pid $(pidof targeted_binary_name) -t obj-intel64/hotpatch.so

4. 结果和结论

嗯,好像起作用了

测试热补丁版本以应对缓冲区溢出

总而言之,我非常确定 Intel Pin 比我在这里展示的能够完成更复杂的工作 — 我认为这些只是示例级别的(实际上,这是受示例启发)。对我来说,它没有成为一个更受欢迎的工具似乎相当奇怪 — 而且,我不是 Intel paid to endorse it(Intel 付费让我为其代言)。

然而,我希望本文能够为那些正在寻找 热补丁方法并且像我一样以前从未听说过 Intel Pin 的人提供支持和解决方案/想法。

© . All rights reserved.