繁体   English   中英

在 ARM macOS 上,当显式 raise()-ing 信号时,一些返回地址在堆栈中出现乱码

[英]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 + 56start + 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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM