[英]Stack differences between a signal handler being called directly and by raise()?
[英]On ARM macOS when explicitly raise()-ing a signal, some return addresses are garbled on the stack
这是 ARM macOS 的一个简单程序,它为SIGSEGV
安装一个信号处理程序,然后生成一个。 在信号处理程序 function 中,使用通常的帧指针追逐算法遍历堆栈,然后打印出符号化版本:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <execinfo.h>
#include <stdlib.h>
void handler(int signum, siginfo_t* siginfo, void* context)
{
__darwin_ucontext* ucontext = (__darwin_ucontext*) context;
__darwin_mcontext64* machineContext = ucontext->uc_mcontext;
uint64_t programCounter = machineContext->__ss.__pc;
uint64_t framePointer = machineContext->__ss.__fp;
void* bt[100];
int n = 0;
while (framePointer != 0) {
bt[n] = (void*)programCounter;
programCounter = *(uint64_t*)(framePointer + 8);
framePointer = *(uint64_t*)(framePointer);
++n;
}
char** symbols = backtrace_symbols(bt, n);
printf ("Call stack:\n");
for (int i = 0; i < n; ++i) {
printf ("\t %s\n", symbols[i]);
}
free (symbols);
abort ();
}
void Crash ()
{
raise (SIGSEGV);
//*(volatile int*)0 = 0;
}
int main()
{
struct sigaction sigAction;
sigAction.sa_sigaction = handler;
sigAction.sa_flags = SA_SIGINFO;
sigaction (SIGSEGV, &sigAction, nullptr);
Crash ();
}
当“常规” SIGSEGV
发生时,这很好用,但是当它被显式引发时,堆栈上的返回值似乎是乱码,具体来说,上半部分似乎包含垃圾:
Call stack:
0 libsystem_kernel.dylib 0x0000000185510e68 __pthread_kill + 8
1 libsystem_c.dylib 0x116a000185422e14 raise + [...] // Should be 0x0000000185422e14
2 SignalHandlerTest 0x8f6a000104bc3eb8 _Z5Crashv + [...] // Should be 0x0000000104bc3eb8
3 SignalHandlerTest 0x0000000104bc3ef8 main + 56
4 libdyld.dylib 0x0000000185561450 start + 4
无论发出哪个信号,行为都是相同的。 我错过了什么?
正如@Codo 已正确识别的那样,这是PAC 。
地址的高位不是乱码,而是包含寄存器低位的加盐 hash。
与您的说法相反,这种情况也会发生在常规段错误中。 例如,调用fprintf(NULL, "a");
结果是:
Call stack:
0 libsystem_c.dylib 0x000000019139d8a0 flockfile + 28
1 libsystem_c.dylib 0x1d550001913a5870 vfprintf_l + 2113595600120315944
2 libsystem_c.dylib 0x341c80019139efd0 fprintf + 3755016926808506440
3 t 0x5f29000100483e9c Crash + 6857011907648290844
4 t 0x0000000100483edc main + 56
5 libdyld.dylib 0x00000001914b1430 start + 4
这是因为所有系统二进制文件(包括库)都是为 arm64e ABI 编译的,并且将使用 PAC。 现在,您的二进制文件作为常规的旧 arm64 二进制文件运行,如果它将未签名的 function 指针传递给库 function 或返回已签名的指针,则会崩溃。 因此 kernel 实际上禁用了您的进程可以使用的 4 个键中的 3 个(IA、IB、DA 和 DB)。 但其中之一 IB 仅用于堆栈帧,因此即使在 arm64 二进制文件中也启用了一个。
某些返回地址仍未签名的原因是:
main + 56
和start + 4
是由您的代码推送的,该代码是 arm64,因此不会对它们进行签名。flockfile + 28
是崩溃的指令,其地址从未被压入堆栈,而是从线程 state 中提取。所以一切都在按预期进行。
在尝试使用它来帮助我调试自己之后,我发现 PAC 的地址毕竟很烦人。 您在ptrauth.h
ptrauth_strip
但这实际上不会在__builtin_ptrauth_strip
进程中工作(它被别名为一个什么都不做的宏),__builtin_ptrauth_strip 也不会(编译器会出错)。
当以 arm64 为目标时,编译器甚至不允许您使用原始xpaci
指令,但硬件级别上没有任何东西可以阻止指令工作,因此您仍然可以手动注入操作码。
基于此,我编写了一个信号处理程序,可以正确地从 arm64 进程中剥离 PAC 签名:
#include <errno.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <execinfo.h>
#ifdef __arm64__
extern void* xpaci(uint64_t pc);
__asm__
(
"_xpaci:\n"
" mov x1, x30\n"
" mov x30, x0\n"
" .4byte 0xd50320ff\n" // xpaclri
" mov x0, x30\n"
" ret x1\n"
);
#else
static inline void* xpaci(uint64_t pc)
{
return (void*)pc;
}
#endif
static void handler(int signum, siginfo_t *siginfo, void *ctx)
{
_STRUCT_MCONTEXT64 *mctx = ((_STRUCT_UCONTEXT*)ctx)->uc_mcontext;
#ifdef __arm64__
uint64_t orig_pc = mctx->__ss.__pc;
uint64_t orig_fp = mctx->__ss.__fp;
#elif defined(__x86_64__)
uint64_t orig_pc = mctx->__ss.__rip;
uint64_t orig_fp = mctx->__ss.__rbp;
#else
# error "Unknown arch"
#endif
uint64_t pc = orig_pc;
uint64_t fp = orig_fp;
size_t n = 0;
while(1)
{
if(!xpaci(pc))
{
break;
}
++n;
if(!fp)
{
break;
}
pc = ((uint64_t*)fp)[1];
fp = ((uint64_t*)fp)[0];
}
void **bt = malloc(n * sizeof(void*));
if(!bt)
{
fprintf(stderr, "malloc: %s\n", strerror(errno));
exit(-1);
}
pc = orig_pc;
fp = orig_fp;
for(size_t i = 0; i < n; ++i)
{
bt[i] = xpaci(pc);
if(!fp)
{
break;
}
pc = ((uint64_t*)fp)[1];
fp = ((uint64_t*)fp)[0];
}
char **sym = backtrace_symbols(bt, n);
fprintf(stderr, "Caught signal with call stack:\n");
for(size_t i = 0; i < n; ++i)
{
fprintf(stderr, "%s\n", sym[i]);
}
free(sym);
free(bt);
exit(-1);
}
它使用xpaclri
而不是xpaci
,因为前者是 arm64(非 arm64e)硬件上的 NOP,而后者是未定义的。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.