在 ARM macOS 上,当显式 raise()-ing 信号时,某些 return 地址在堆栈中出现乱码
On ARM macOS when explicitly raise()-ing a signal, some return addresses are garbled on the stack
这是一个用于 ARM macOS 的简单程序,它为 SIGSEGV
安装一个信号处理程序,然后生成一个。在信号处理函数中,用通常的帧指针追逐算法遍历堆栈,然后打印出符号化版本:
#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
发生时工作正常,但是当它被显式引发时,堆栈上的 return 值似乎是乱码,具体来说,上半部分似乎包含垃圾:
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。
地址的高位不是乱码,而是包含寄存器低位的加盐散列。
与您的说法相反,这种情况也会发生在常规段错误中。例如,调用 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 二进制文件一样,如果它将未签名的函数指针传递给库函数或获得已签名的 returned,则会崩溃。所以内核实际上禁用了您的进程可以使用的 4 个键中的 3 个(IA、IB、DA 和 DB)。但其中之一 IB 仅用于堆栈帧,因此即使在 arm64 二进制文件中也启用了一个。
有些 return 地址仍未签名的原因是:
main + 56
和 start + 4
是由您的代码推送的,该代码是 arm64,因此不会对它们进行签名。
flockfile + 28
是崩溃的指令,其地址从未被压入堆栈,而是从线程状态中提取。
所以一切都在正常工作。
编辑:
在尝试使用它来帮助我调试自己之后,我发现 PAC 的地址毕竟很烦人。您在 ptrauth.h
中评论了 ptrauth_strip
,但这实际上不会在 arm64 进程中工作(它被别名为一个什么都不做的宏),__builtin_ptrauth_strip
也不会(编译器会出错) .
当以 arm64 为目标时,编译器甚至不允许您使用原始 xpaci
指令,但硬件级别上没有任何东西阻止指令工作,因此您仍然可以手动注入操作码。
基于此,我编写了一个信号处理程序,可以正确地从 arm64 进程中剥离 PAC 签名:
extern void* xpaci(uint64_t pc);
__asm__
(
"_xpaci:\n"
" .4byte 0xdac143e0\n" // xpaci x0
" ret\n"
);
static void handler(int signum, siginfo_t *siginfo, void *ctx)
{
_STRUCT_MCONTEXT64 *mctx = ((_STRUCT_UCONTEXT*)ctx)->uc_mcontext;
uint64_t pc = mctx->__ss.__pc;
uint64_t fp = mctx->__ss.__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 = mctx->__ss.__pc;
fp = mctx->__ss.__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);
}
这只会在 arm64(e) 上按原样工作,但对于 x86_64,您所要做的就是 ifdef 出 __asm__
并将其替换为 returns pc
不变。
这是一个用于 ARM macOS 的简单程序,它为 SIGSEGV
安装一个信号处理程序,然后生成一个。在信号处理函数中,用通常的帧指针追逐算法遍历堆栈,然后打印出符号化版本:
#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
发生时工作正常,但是当它被显式引发时,堆栈上的 return 值似乎是乱码,具体来说,上半部分似乎包含垃圾:
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。
地址的高位不是乱码,而是包含寄存器低位的加盐散列。
与您的说法相反,这种情况也会发生在常规段错误中。例如,调用 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 二进制文件一样,如果它将未签名的函数指针传递给库函数或获得已签名的 returned,则会崩溃。所以内核实际上禁用了您的进程可以使用的 4 个键中的 3 个(IA、IB、DA 和 DB)。但其中之一 IB 仅用于堆栈帧,因此即使在 arm64 二进制文件中也启用了一个。
有些 return 地址仍未签名的原因是:
main + 56
和start + 4
是由您的代码推送的,该代码是 arm64,因此不会对它们进行签名。flockfile + 28
是崩溃的指令,其地址从未被压入堆栈,而是从线程状态中提取。
所以一切都在正常工作。
编辑:
在尝试使用它来帮助我调试自己之后,我发现 PAC 的地址毕竟很烦人。您在 ptrauth.h
中评论了 ptrauth_strip
,但这实际上不会在 arm64 进程中工作(它被别名为一个什么都不做的宏),__builtin_ptrauth_strip
也不会(编译器会出错) .
当以 arm64 为目标时,编译器甚至不允许您使用原始 xpaci
指令,但硬件级别上没有任何东西阻止指令工作,因此您仍然可以手动注入操作码。
基于此,我编写了一个信号处理程序,可以正确地从 arm64 进程中剥离 PAC 签名:
extern void* xpaci(uint64_t pc);
__asm__
(
"_xpaci:\n"
" .4byte 0xdac143e0\n" // xpaci x0
" ret\n"
);
static void handler(int signum, siginfo_t *siginfo, void *ctx)
{
_STRUCT_MCONTEXT64 *mctx = ((_STRUCT_UCONTEXT*)ctx)->uc_mcontext;
uint64_t pc = mctx->__ss.__pc;
uint64_t fp = mctx->__ss.__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 = mctx->__ss.__pc;
fp = mctx->__ss.__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);
}
这只会在 arm64(e) 上按原样工作,但对于 x86_64,您所要做的就是 ifdef 出 __asm__
并将其替换为 returns pc
不变。