从内部捕获SIGSEGV时,如何知道涉及的无效访问类型?

时间:2017-03-26 18:58:06

标签: c linux segmentation-fault sigsegv sigaction

如您所知,可以捕获任何信号,但使用处理程序杀死和停止/计数 有三种无效的地址访问:

  • 尝试在无效地址执行/跳转
  • 尝试在无效地址读取
  • 尝试在无效地址

我只对拒绝无效的读取访问感兴趣。因此,如果它不是无效的读取访问权限,那么我们的想法是捕获所有分段错误和abort()

到目前为止,我只知道如何将SEGV_MAPERRSEGV_ACCERRsigaction一起使用,这当然无关紧要。

1 个答案:

答案 0 :(得分:1)

事实证明,在Linux on x86-64(又名AMD64)架构中,这实际上是非常可行的。

以下是一个示例程序 crasher.c

#define  _POSIX_C_SOURCE 200809L
#define  _GNU_SOURCE
#include <stdlib.h>
#include <unistd.h>
#include <sys/mman.h>
#include <ucontext.h>
#include <signal.h>
#include <string.h>
#include <stdio.h>
#include <errno.h>

#if !defined(__linux__) || !defined(__x86_64__)
#error This example only works in Linux on x86-64.
#endif

#define  ALTSTACK_SIZE  262144

static const char hex_digit[16] = {
    '0', '1', '2', '3', '4', '5', '6', '7',
    '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'
};

static inline const char *signal_name(const int signum)
{
    switch (signum) {
    case SIGSEGV: return "SIGSEGV";
    case SIGBUS:  return "SIGBUS";
    case SIGILL:  return "SIGILL";
    case SIGFPE:  return "SIGFPE";
    case SIGTRAP: return "SIGTRAP";
    default:      return "(unknown)";
    }
}

static inline ssize_t internal_write(int fd, const void *buf, size_t len)
{
    ssize_t retval;
    asm volatile ( "syscall\n\t"
                 : "=a" (retval)
                 : "a" (1), "D" (fd), "S" (buf), "d" (len)
                 : "rcx", "r11" );
    return retval;
}

static inline int wrerr(const char *p, const char *q)
{
    while (p < q) {
        ssize_t n = internal_write(STDERR_FILENO, p, (size_t)(q - p));
        if (n > 0)
            p += n;
        else
        if (n == 0)
            return EIO;
        else
            return -n;
    }
    return 0;
}

static inline int wrs(const char *p)
{
    if (p) {
        const char *q = p;
        while (*q)
            q++;
        return wrerr(p, q);
    }
    return 0;
}

static inline int wrh(unsigned long h)
{
    static char buffer[4 + 2 * sizeof h];
    char       *p = buffer + sizeof buffer;

    do {
        *(--p) = hex_digit[h & 15];
        h /= 16UL;
    } while (h);

    *(--p) = 'x';
    *(--p) = '0';

    return wrerr(p, buffer + sizeof buffer);
}

static void crash_handler(int signum, siginfo_t *info, void *contextptr)
{
    if (info) {
        ucontext_t *const ctx = (ucontext_t *const)contextptr;
        wrs(signal_name(signum));
        if (ctx->uc_mcontext.gregs[REG_ERR] & 16) {
            const unsigned long sp = ctx->uc_mcontext.gregs[REG_RSP];
            /* Instruction fetch */
            wrs(": Bad jump to ");
            wrh((unsigned long)(info->si_addr));
            if (sp && !(sp & 7)) {
                wrs(" probably by the instruction just before ");
                wrh(*(unsigned long *)sp);
            }
            wrs(".\n");
        } else
        if (ctx->uc_mcontext.gregs[REG_ERR] & 2) {
            /* Write access */
            wrs(": Invalid write attempt to ");
            wrh((unsigned long)(info->si_addr));
            wrs(" by instruction at ");
            wrh(ctx->uc_mcontext.gregs[REG_RIP]);
            wrs(".\n");
        } else {
            /* Read access */
            wrs(": Invalid read attempt from ");
            wrh((unsigned long)(info->si_addr));
            wrs(" by instruction at ");
            wrh(ctx->uc_mcontext.gregs[REG_RIP]);
            wrs(".\n");
        }
    }

    raise(SIGKILL);
}

static int install_crash_handler(void)
{
    stack_t           altstack;
    struct sigaction  act;

    altstack.ss_size = ALTSTACK_SIZE;
    altstack.ss_flags = 0;
    altstack.ss_sp = mmap(NULL, altstack.ss_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_GROWSDOWN, -1, 0);
    if (altstack.ss_sp == MAP_FAILED) {
        const int retval = errno;
        fprintf(stderr, "Cannot map memory for alternate stack: %s.\n", strerror(retval));
        return retval;
    }
    if (sigaltstack(&altstack, NULL)) {
        const int retval = errno;
        fprintf(stderr, "Cannot use alternate signal stack: %s.\n", strerror(retval));
        return retval;
    }

    memset(&act, 0, sizeof act);
    sigemptyset(&act.sa_mask);
    act.sa_flags = SA_SIGINFO | SA_ONSTACK;
    act.sa_sigaction = crash_handler;
    if (sigaction(SIGSEGV, &act, NULL) == -1 ||
        sigaction(SIGBUS,  &act, NULL) == -1 ||
        sigaction(SIGILL,  &act, NULL) == -1 ||
        sigaction(SIGFPE,  &act, NULL) == -1) {
        const int retval = errno;
        fprintf(stderr, "Cannot install crash signal handlers: %s.\n", strerror(retval));
        return retval;
    }

    return 0;
}

int main(int argc, char *argv[])
{
    void         (*jump)(void) = 0;
    unsigned char *addr = (unsigned char *)0;

    if (argc < 2 || argc > 3 || !strcmp(argv[1], "-h") || !strcmp(argv[1], "--help")) {
        fprintf(stderr, "\n");
        fprintf(stderr, "Usage: %s [ -h | --help ]\n", argv[0]);
        fprintf(stderr, "       %s call [ address ]\n", argv[0]);
        fprintf(stderr, "       %s read [ address ]\n", argv[0]);
        fprintf(stderr, "       %s write [ address ]\n", argv[0]);
        fprintf(stderr, "\n");
        return EXIT_SUCCESS;
    }
    if (argc > 2 && argv[2][0] != '\0') {
        char          *end = NULL;
        unsigned long  val;

        errno = 0;
        val = strtoul(argv[2], &end, 0);
        if (errno) {
            fprintf(stderr, "%s: %s.\n", argv[2], strerror(errno));
            return EXIT_FAILURE;
        }
        if (end)
            while (*end == '\t' || *end == '\n' || *end == '\v' ||
                   *end == '\f' || *end == '\r' || *end == ' ')
                end++;
        if (!end || end <= argv[2] || *end) {
            fprintf(stderr, "%s: Not a valid address.\n", argv[2]);
            return EXIT_FAILURE;
        }

        jump = (void *)val;
        addr = (void *)val;
    }

    if (install_crash_handler())
        return EXIT_FAILURE;

    if (argv[1][0] == 'c' || argv[1][0] == 'C') {
        printf("Calling address %p: ", (void *)jump);
        fflush(stdout);
        jump();
        printf("Done.\n");

    } else
    if (argv[1][0] == 'r' || argv[1][0] == 'R') {
        unsigned char  val;

        printf("Reading from address %p: ", (void *)addr);
        fflush(stdout);
        val = *addr;
        printf("0x%02x, done.\n", val);

    } else
    if (argv[1][0] == 'w' || argv[1][1] == 'W') {
        printf("Writing 0xC4 to address %p: ", (void *)addr);
        fflush(stdout);
        *addr = 0xC4;
        printf("Done.\n");
    }

    printf("No crash.\n");
    return EXIT_SUCCESS;
}

使用例如

进行编译
gcc -Wall -O2 crasher.c -o crasher

您可以通过在命令行上指定操作和可选地址来测试对任意地址的调用,读取或写入。不带参数运行以查看用法。

我的机器上运行了一些示例:

./crasher call 0x100
Calling address 0x100: SIGSEGV: Bad jump to 0x100 probably by the instruction just before 0x400c4e.
Killed

./crasher write 0x24
Writing 0xC4 to address 0x24: SIGSEGV: Invalid write attempt to 0x24 by instruction at 0x400bad.
Killed

./crasher read 0x16
Reading from address 0x16: SIGSEGV: Invalid read attempt from 0x16 by instruction at 0x400ca3.
Killed

./crasher write 0x400ca3
Writing 0xC4 to address 0x400ca3: SIGSEGV: Invalid write attempt to 0x400ca3 by instruction at 0x400bad.
Killed

./crasher read 0x400ca3
Reading from address 0x400ca3: 0x41, done.
No crash.

请注意,访问类型是从((ucontext_t *)contextptr)->uc_mcontext.gregs[REG_ERR]寄存器(来自信号处理程序上下文)获得的;它与arch/x86/mm/fault.c in the Linux kernel sources中定义的x86_pf_error_code枚举相匹配。

崩溃处理程序本身非常简单,只需要排除上述“注册”以获取OP寻求的信息。

为了输出崩溃报告,我对write()系统调用进行了开放编码。 (由于某种原因,wrh()函数所需的小缓冲区不能在堆栈中,所以我只是将其设置为静态。)

我没有费心去实现mincore()系统调用来验证例如sp函数中的堆栈地址(crash_handler());可能有必要避免双重错误(SIGSEGV本身发生crash_handler()。)

同样,我也不打算在raise()的末尾开放代码crash_handler(),因为现在在x86-64上它使用tgkill(pid, tid, signum)在C库中实现系统调用,这意味着我还必须对getpid()gettid()系统调用进行开放式编码。我只是懒惰。

最后,上面的代码写得非常粗心,因为我自己只是在与OP,user2284570交换评论之后才发现这个,并且只是想把一些东西放在一起看看这种方法是否真的可靠。 (看起来确实如此,但我只是在一台机器上进行了轻微的测试。)所以,如果你发现代码中有任何错误,拼写错误,思想或其他需要解决的问题,请在评论中告诉我,所以我可以解决它。