在 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 + 56start + 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不变。