内存保护键:如果 pkey0 被禁止写入,异常处理程序将崩溃

内存保护键:如果 pkey0 被禁止写入,异常处理程序将崩溃

背景:x86/linux 中基于内存保护域的进程内隔离,使用内存保护密钥 (MPK) 和保护密钥寄存器 PKRU。

设置:程序首先执行分配新保护密钥和关联内存的管理员代码,并将用户堆栈指针移动到该内存。这表示切换到用户代码,因为它只能在该内存中操作。如果用户代码导致异常,我希望信号处理程序将执行传递回管理员——我必须这样做:

  • (1)分配保护键1pku=pkey_alloc()以及相关内存来创建用户堆栈ustack=mmap()+ pkey_mprotect(ustack, ..., pku)。安装init_handler()SEGV 和 FPE 的异常处理程序
  • (2)将rsp切换到用户栈mov ustack, %%rsp
  • (3a)我们现在正在执行用户代码,这会导致异常,例如。这将通过=> =>*(int*)0=0触发信号处理程序。注意,handler_asm() 是必需的,以允许 handler() 在用户堆栈上操作handler_asm()handler()unblock_signal()
  • (4) 信号处理程序handler()返回给管理员,管理员切换rsp回原来的堆栈

下面展示了管理员/用户切换和信号处理的最小可编译源代码,作为要点描述的步骤要么发生在 main 中,要么发生在由 指示的函数中()。到目前为止,一切都按预期进行(Ubuntu 23.04)

假设用户代码只需要访问预先分配的资源(即不需要 malloc 或调用任何 C 库函数)。现在,为了限制用户可能造成的损害,我想禁用不属于该用户的所有内存的写入,特别是禁用 pkey 0 的页面上的写入(设置 PKRU.WD0=true 即 PKRU=0x55555552)。因此,将 3a 替换为 3b 或 3c

  • (3b) set WD0=true via wrpkru// 用户不会导致异常 // set WD0=false =>工作正常
  • (3c) WD0=true // 用户导致 FPE 或 SEGV // WD0=false =>与信号处理崩溃相关的系统调用,请参阅下面/或代码中的详细信息

我怎样才能避免这次崩溃,我犯了一个错误吗?或者,如果 WD0=true,通常不可能成功处理异常——在某种程度上违背了 MPK 的目的?

感谢您的任何帮助!!


最小实现:写禁用 pkey0 和用户引发的异常分别由两个开关 PROTECT_WD0 和 INVOKE_SEGV 控制。管理员/用户之间的切换发生在 main() 中

PROTECT_WD0=0 用户代码的 PKRU 为 0x55555550

  • INVOKE_SEGV=0:用户导致FPE
  • INVOKE_SEGV=1:用户引发SEGV

PROTECT_WD0=1 用户代码的 PKRU 为 0x55555552

  • INVOKE_SEGV=0:用户导致 FPE => 在进入信号处理程序时崩溃handler_asm()
  • INVOKE_SEGV=1:用户导致 SEGV => 崩溃后unblock_signal() => sigprocmask() => __GI___sigprocmask => __GI___pthread_sigmask => syscall 14

这里,“崩溃”是指当内核切换到我的信号处理程序时,它会在指定位置导致另一个SEGV

#include <stdexcept>
#include <signal.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <sys/mman.h>

// Save as "main.cpp" and compile via
// gcc -O0 -fexceptions -fnon-call-exceptions -g main.cpp
// gdb ./a.out
// after exception continue to handler via "signal SIGSEGV" or "signal SIGFPE"

// PROTECT_WD0=1 will write disable pkey 0
#define PROTECT_WD0 1
// INVOKE_SEGV=1: user code causes SEGV, =0: user code causes FPE
#define INVOKE_SEGV 1

uint8_t* ustack, *ostack;
void *ripret;

/////////////////////////////////////////////////////////////////////////////////////
// init_handler     installs signal handler "handler_asm"
// handler_asm      resets pkru before calling handler
// handler          modify RIP to return to after the error
// unblock_signal   ensure signal can be resent
// 
// modified from https://github.com/Plaristote/segvcatch
// except handler_asm, modified from https://github.com/IAIK/Donky
struct kernel_sigaction {
    void (*k_sa_sigaction)(int,siginfo_t *,void *);
    unsigned long k_sa_flags;
    void (*k_sa_restorer) (void);
    sigset_t k_sa_mask;
};

#  define RESTORE(name, syscall) RESTORE2 (name, syscall)
#  define RESTORE2(name, syscall)                     \
asm (                                                 \
   ".text\n"                                          \
   ".byte 0  # Yes, this really is necessary\n"       \
   ".align 16\n"                                      \
   "__" #name ":\n"                                   \
   "    movq $" #syscall ", %rax\n"                   \
   "    syscall\n"                                    \
   );

/* The return code for realtime-signals.  */
RESTORE (restore_rt, __NR_rt_sigreturn)
void restore_rt (void) asm ("__restore_rt")
  __attribute__ ((visibility ("hidden")));

static void unblock_signal(int signum __attribute__((__unused__))) {
    sigset_t sigs;
    sigemptyset(&sigs);
    sigaddset(&sigs, signum);
    // SIGSEGV crashes at sigprocmask => __GI___sigprocmask => __GI___pthread_sigmask => syscall 14
    sigprocmask(SIG_UNBLOCK, &sigs, NULL);
}

// Exception handler
void handler(int s, siginfo_t *, void *_p  __attribute__ ((__unused__))) {
  ucontext_t *_uc = (ucontext_t *)_p;                                             
  gregset_t &_gregs = _uc->uc_mcontext.gregs;                                   
  unblock_signal(s);
 _gregs[REG_RIP] = (greg_t)ripret;                               
}

// kernel resets pkru to 0x55555554: give full access before handling
void __attribute__((naked)) handler_asm(int, siginfo_t*, void *) {
    // SIGFPE crashes here
  __asm__ volatile(
    "mov %%rdx, %%r14\n"               //   save ucontext
    "xorl %%eax, %%eax; xorl %%ecx, %%ecx; xorl %%edx, %%edx; wrpkru;" // full access
    "mov %%r14, %%rdx\n"               //   restore ucontext
    "jmp %P0\n" :: "i"(handler));
} 

// install signal handlers
void init_handler(int signal) {
    struct kernel_sigaction act;                                        
    act.k_sa_sigaction = handler_asm;                     
    sigemptyset (&act.k_sa_mask);                                       
    act.k_sa_flags = SA_SIGINFO|0x4000000;                          
    act.k_sa_restorer = restore_rt;                                   
    syscall (SYS_rt_sigaction, signal, &act, NULL, _NSIG / 8);  
}

/////////////////////////////////////////////////////////////////////////////////////
int main(int argc, char *argv[]) {
    // allocate pkey (assumed:1) and associated stack ustack
    int pku=pkey_alloc(0,0);
    ustack = (uint8_t*)mmap(NULL, 0x10000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS ,-1, 0);
    pkey_mprotect(ustack, 0x10000, PROT_READ | PROT_WRITE, pku); 
    ustack += 0xFFF0;

    // initialize handlers for SEGV and FPE
    // ripret is the address for the return from signal handler
    init_handler(SIGSEGV);
    init_handler(SIGFPE);
    ripret = &&ret;

    // ADMINISTRATOR: switch to user stack and write-disable pkey 0
    asm("mov %%rsp, %0; mov %1, %%rsp" : "=g" (ostack) : "g" (ustack));
    #if PROTECT_WD0
    asm("xorl %%ecx, %%ecx; rdpkru; xorl $2, %%eax; wrpkru" :::); 
    #endif

    // USER: causes SEGV or FPE
    #if INVOKE_SEGV
    *(int*) 0 = 0;
    #else
    ustack[0] = 0;
    ustack[0] = 10/ustack[0];
    #endif
ret:  
    // ADMINISTRATOR: write-enable pkey 0 and switch back to original stack
    #if PROTECT_WD0
    asm("xorl %%ecx, %%ecx; rdpkru; xorl $2, %%eax; wrpkru" :::);
    #endif
    asm("mov %0, %%rsp" : : "g" (ostack));

    printf("done\n");
    return 0;
}

相关内容