如您所知,可以捕获任何信号,但可以使用处理程序杀死和停止/计数。
有三种无效地址访问:
- 尝试在无效地址执行/跳转。
- 尝试读取无效地址。
- 尝试在无效地址上写入。
我只对拒绝无效的读取访问感兴趣。因此,我们的想法是捕获所有分段错误并abort()
它是否不是无效的读取访问。
到目前为止,我只知道如何使用SEGV_MAPERR
和SEGV_ACCERR
sigaction
这当然无关
事实证明,在x86-64(又名AMD64)架构上的Linux中,这实际上是非常可行的。
下面是一个示例程序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 ( "syscallnt"
: "=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] != ' ') {
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]
寄存器(从信号处理程序上下文)获取的;它与 Linux 内核源代码中arch/x86/mm/fault.c
中定义的x86_pf_error_code
枚举匹配。
崩溃处理程序本身非常简单,只需要挖掘上述"寄存器"即可获取OP寻求的信息。
为了输出崩溃报告,我打开了write()
系统调用。(出于某种原因,wrh()
函数所需的小缓冲区不能在堆栈上,所以我只是将其设置为静态。
我没有费心实现mincore()
系统调用来验证堆栈地址(sp
在crash_handler()
函数中);可能有必要避免双重错误(SIGSEGV
发生在crash_handler()
本身)。
同样,我没有费心在crash_handler()
结束时打开raise()
代码,因为现在在x86-64上,它是使用tgkill(pid, tid, signum)
系统调用在C库中实现的,这意味着我还必须打开编码getpid()
并gettid()
系统调用。我只是懒惰。
最后,上面的代码写得很粗心,因为我自己是在与 OP user2284570 交换注释后才发现的,只是想把一些东西放在一起看看这种方法是否真的可靠。(似乎确实如此,但我只在一台机器上进行了轻微的测试。因此,如果您发现代码中有任何错误、拼写错误、想法或其他需要修复的内容,请在评论中告诉我,以便我修复它。