c - 我们可以读取并错误注入(inject)另一个线程的程序计数器吗?

标签 c linux multithreading assembly

假设我们有一个单线程程序,我们希望在发生预定义中断(如计时器中断)时捕获程序计数器(PC)的值。
您知道,我们只需使用一个特殊的关键字__asm__编写一个特定的程序集代码,并在移位4字节后将该值弹出到堆栈顶部,这似乎很简单。
多线程程序呢?
如何从运行在同一进程中的另一个线程获取所有线程的值?(在多核处理器中,从运行在单独核心上的线程中获取值似乎非常不可思议)。
(在多线程程序中,每个线程都有其堆栈和寄存器)。
我想实现一个破坏者线程。
为了在目标多线程程序中执行错误注入,错误模型为seu(single error upset),这意味着程序计数器寄存器中的任意位被随机修改(bit flip),从而导致违反正确的程序序列。因此,发生控制流误差(CFE)。
由于我们的目标程序是一个多线程程序,我们必须在所有线程的PC机上执行故障注入,这是破坏者的任务。它应该能够获得线程的pc来执行故障注入。
假设我们有这个密码,

main ()
{
foo
}

void foo()
{
__asm__{
pop "%eax"
pop "%ebx" // now ebx holds porgram counter value (for main thread)
// her code injection like  00000111 XOR ebx for example
push ...
push ...
};
}

如果我们的程序是多线程程序。
这是否意味着我们有多个堆栈?
当操作系统执行上下文切换时,这意味着正在运行的线程的堆栈和寄存器移动到内存中的某个位置。这是否意味着如果我们想得到这些线程的程序计数器的值,我们可以在内存中找到它们?在哪里?在运行时有可能吗?

最佳答案

当在标志中使用sigaction()SA_SIGINFO安装信号处理程序时,信号处理程序获得的第二个参数是指向siginfo_t的指针,第三个参数是指向ucontext_t的指针。在linux中,这个结构包含了内核中断线程时的一组寄存器值,包括程序计数器。

#define _POSIX_C_SOURCE 200809L
#define _GNU_SOURCE
#include <signal.h>
#include <ucontext.h>

#if defined(__x86_64__)
#define  PROGCOUNTER(ctx) (((ucontext *)ctx)->uc_mcontext.greg[REG_RIP])
#elif defined(__i386__)
#define  PROGCOUNTER(ctx) (((ucontext *)ctx)->uc_mcontext.greg[REG_EIP])
#else
#error Unsupported architecture.
#endif

void signal_handler(int signum, siginfo_t *info, void *context)
{
    const size_t program_counter = PROGCOUNTER(context);

    /* Do something ... */

}

像往常一样,printf()等。不是异步信号安全的,这意味着在信号处理程序中使用它们是不安全的。如果希望将程序计数器输出为标准错误,则不应使用任何标准I/O打印到stderr,而应构造要手动打印的字符串,并使用循环来write()字符串的内容;例如,
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>

static void wrerr(const char *p)
{
    const int   saved_errno = errno;
    const char *q = p;
    ssize_t     n;

    /* Nothing to print? */
    if (!p || !*p)
        return;

    /* Find end of q. strlen() is not async-signal safe. */
    while (*q) q++;

    /* Write data from p to q. */
    while (p < q) {
        n = write(STDERR_FILENO, p, (size_t)(q - p));
        if (n > 0)
            p += n;
        else
        if (n != -1 || errno != EINTR)
            break;
    }

    errno = saved_errno;
}

注意,您需要在信号处理程序中保持errno的值不变,这样,如果在库函数失败后中断,中断的线程仍然可以看到正确的errno值。(这主要是一个调试问题,而且是“良好的形式”;一些白痴嗤之以鼻,认为“这种情况发生的频率不足以让我担心”。)
您的程序可以检查伪文件(它不是一个真正的文件,而是内核在读取文件时动态生成的文件),以查看程序使用的内存区域,确定程序在传递中断时是在运行C库函数(非常常见)还是在运行其他函数。
如果要中断多线程程序中的特定线程,只需使用/proc/self/maps。否则,该信号将或多或少随机地传递到未阻塞该信号的线程之一。
下面是一个示例程序,当使用pthread_kill()使用gcc-4.8.4编译时,将在x86-64(amd64)和x86中对其进行测试:
#define  _POSIX_C_SOURCE 200809L
#define  _GNU_SOURCE
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <signal.h>
#include <ucontext.h>
#include <time.h>
#include <stdio.h>

#if defined(__x86_64__)
#define PROGRAM_COUNTER(mctx)   ((mctx).gregs[REG_RIP])
#define STACK_POINTER(mctx)     ((mctx).gregs[REG_RSP])
#elif defined(__i386__)
#define PROGRAM_COUNTER(mctx)   ((mctx).gregs[REG_EIP])
#define STACK_POINTER(mctx)     ((mctx).gregs[REG_ESP])
#else
#error Unsupported hardware architecture.
#endif

#define MAX_SIGNALS  64
#define MCTX(ctx)    (((ucontext_t *)ctx)->uc_mcontext)

static void wrerr(const char *p, const char *q)
{
    while (p < q) {
        ssize_t n = write(STDERR_FILENO, p, (size_t)(q - p));
        if (n > 0)
            p += n;
        else
        if (n != -1 || errno != EINTR)
            break;
    }
}

static const char hexc[16] = "0123456789abcdef";

static inline char *prehex(char *before, size_t value)
{
    do {
        *(--before) = hexc[value & 15];
        value /= (size_t)16;
    } while (value);
    *(--before) = 'x';
    *(--before) = '0';
    return before;
}

static volatile sig_atomic_t done = 0;

static void handle_done(int signum)
{
    done = signum;
}

static int install_done(const int signum)
{
    struct sigaction act;

    memset(&act, 0, sizeof act);
    sigemptyset(&act.sa_mask);
    act.sa_handler = handle_done;
    act.sa_flags = 0;
    if (sigaction(signum, &act, NULL) == -1)
        return errno;

    return 0;
}

static size_t jump_target[MAX_SIGNALS] = { 0 };
static size_t jump_stack[MAX_SIGNALS] = { 0 };

static void handle_jump(int signum, siginfo_t *info, void *context)
{
    const int   saved_errno = errno;
    char        buffer[128];
    char       *p = buffer + sizeof buffer;

    *(--p) = '\n';
    p = prehex(p, STACK_POINTER(MCTX(context)));
    *(--p) = ' ';
    *(--p) = 'k';
    *(--p) = 'c';
    *(--p) = 'a';
    *(--p) = 't';
    *(--p) = 's';
    *(--p) = ' ';
    *(--p) = ',';
    p = prehex(p, PROGRAM_COUNTER(MCTX(context)));
    *(--p) = ' ';
    *(--p) = '@';
    wrerr(p, buffer + sizeof buffer);

    if (signum >= 0 && signum < MAX_SIGNALS) {
        if (jump_target[signum])
            PROGRAM_COUNTER(MCTX(context)) = jump_target[signum];
        if (jump_stack[signum])
            STACK_POINTER(MCTX(context)) = jump_stack[signum];
    }

    errno = saved_errno;
}

static int install_jump(const int signum, void *target, size_t stack)
{
    struct sigaction act;

    if (signum < 0 || signum >= MAX_SIGNALS)
        return errno = EINVAL;

    jump_target[signum] = (size_t)target;
    jump_stack[signum] = (size_t)stack;

    memset(&act, 0, sizeof act);
    sigemptyset(&act.sa_mask);
    act.sa_sigaction = handle_jump;
    act.sa_flags = SA_SIGINFO;
    if (sigaction(signum, &act, NULL) == -1)
        return errno;

    return 0;
}

int main(int argc, char *argv[])
{
    const struct timespec sec = { .tv_sec = 1, .tv_nsec = 0L };
    const int pid = (int)getpid();
    ucontext_t ctx;

    printf("Run\n");
    printf("\tkill -KILL %d\n", pid);
    printf("\tkill -TERM %d\n", pid);
    printf("\tkill -HUP  %d\n", pid);
    printf("\tkill -INT  %d\n", pid);
    printf("or press Ctrl+C to stop this process, or\n");
    printf("\tkill -USR1 %d\n", pid);
    printf("\tkill -USR2 %d\n", pid);
    printf("to send the respective signal to this process.\n");
    fflush(stdout);

    if (install_done(SIGTERM) ||
        install_done(SIGHUP)  ||
        install_done(SIGINT) ) {
        printf("Cannot install signal handlers: %s.\n", strerror(errno));
        return EXIT_FAILURE;
    }

    getcontext(&ctx);

    if (install_jump(SIGUSR1, &&usr1_target, STACK_POINTER(MCTX(&ctx))) ||
        install_jump(SIGUSR2, &&usr2_target, STACK_POINTER(MCTX(&ctx))) ) {
        printf("Cannot install signal handlers: %s.\n", strerror(errno));
        return EXIT_FAILURE;
    }

    /* These are expressions that should evaluate to false, but the compiler
     * should not be able to optimize them away. */
    if (argv[0][1] == 'A') {
usr1_target:
        fputs("USR1\n", stdout);
        fflush(stdout);
    }

    if (argv[0][1] == 'B') {
usr2_target:
        fputs("USR2\n", stdout);
        fflush(stdout);
    }

    while (!done) {
        putchar('.');
        fflush(stdout);
        nanosleep(&sec, NULL);
    }

    fputs("\nAll done.\n", stdout);
    fflush(stdout);

    return EXIT_SUCCESS;
}

如果将上面的代码保存为-Wall -O2,则可以使用
gcc -Wall -O2 example.c -o example

然后运行它
./example

按Ctrl +C退出程序。复制命令(用于发送example.cSIGUSR1信号),并从另一个窗口运行它们,您将看到它们修改了当前执行的位置。(这些信号会导致程序计数器/指令指针跳回if子句,否则不应执行该子句。)
有两组信号处理程序。SIGUSR2只需设置handle_done()标志。done将消息输出到标准错误(使用低级I/O),如果指定,则更新程序计数器(指令指针)和堆栈指针。
在创建这样的示例程序时,堆栈指针是一个棘手的部分。如果我们对程序崩溃感到满意,那就很容易了。但是,示例只有在有效时才有用。
当我们任意更改程序计数器/指令指针,并且中断是在函数调用(大多数C库函数…)中传递时,返回地址留在堆栈上。内核可以在任何时候传递中断,所以我们甚至不能假设中断是在函数调用中传递的!因此,为了确保测试程序不会崩溃,我必须将程序计数器/指令指针和堆栈指针成对更新。
当接收到跳转信号时,堆栈指针将重置为使用handle_jump()获得的值。这并不能保证适用于任何跳转位置;这只是我能做的最好的一个小例子。我肯定假设跳转标签在附近,而不是在编译器可能会弄乱堆栈的子范围内,请注意。
同样重要的是要记住,因为我们处理的是留给C编译器的细节,所以我们必须遵循编译器生成的任何二进制代码,而不是相反的方式。对于进程及其线程的可靠操作,ptrace()是一个更好的(而且老实说,更容易)接口。您只需设置一个父进程,并在目标跟踪子进程中显式地允许跟踪。我已经向examples herehere演示了如何在目标进程中启动、停止和单步执行单个线程。最困难的部分是理解总体方案和概念;代码本身比这种信号处理程序上下文操作方式更简单,也更健壮。
对于自引入的寄存器错误(程序计数器/指令指针或任何其他寄存器),假设大多数情况下导致进程崩溃,这种信号处理程序上下文操作就足够了。

关于c - 我们可以读取并错误注入(inject)另一个线程的程序计数器吗?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/38018713/

相关文章:

c - 将 printf 重定向到 fopencookie

linux - 是否有必要关闭一个accept返回文件描述符

c++ - 如何初始化大线程本地对象?

linux - 简单的 POSIX 线程问题

c - 节点作为指针

c - 如何将文本文件最后一行的内容复制到第一行? C语言编程

c - Contiki OS中是否有适合Atmega128平台的双线接口(interface)/I2C读写库?

python - 使用python多处理器将数据导入到mysql RDS - RDS

c - 如何在c linux中销毁线程

java - Android 中的 Activity 缓慢且无响应