메모리 보호 키: pkey0에 쓰기가 금지되면 예외 처리기가 충돌합니다.

메모리 보호 키: pkey0에 쓰기가 금지되면 예외 처리기가 충돌합니다.

배경: MPK(메모리 보호 키) 및 보호 키 레지스터 PKRU를 사용하는 x86/linux의 메모리 보호 도메인을 기반으로 하는 프로세스 내 격리입니다.

설정: 프로그램은 먼저 새로운 보호 키와 관련 메모리를 할당하는 관리자 코드를 실행하고 사용자 스택 포인터를 해당 메모리로 이동합니다. 이는 해당 메모리에서만 작동할 수 있으므로 사용자 코드로 전환하는 것을 의미합니다. 사용자 코드로 인해 예외가 발생하는 경우 신호 처리기가 실행을 관리자에게 다시 전달하도록 하고 싶습니다. 다음을 수행해야 합니다.

  • (1) 보호키 1과 관련 메모리를 할당하여 사용자 스택 +을 pku=pkey_alloc()생성합니다 . SEGV 및 FPE에 대한 예외 처리기 설치ustack=mmap()pkey_mprotect(ustack, ..., pku)init_handler()
  • (2) rsp를 사용자 스택으로 전환mov ustack, %%rsp
  • (3a) 이제 예외를 발생시키는 사용자 코드를 실행하고 있습니다. 그러면 *(int*)0=0=> =>를 통해 신호 처리기가 트리거됩니다. handler()가 사용자 스택에서 작동하도록 하려면 handler_asm()이 필요합니다.handler_asm()handler()unblock_signal()
  • (4) 신호 처리기가 handler()관리자에게 반환되고 관리자는 rsp원래 스택으로 다시 전환합니다.

관리자/사용자 전환 및 신호 처리를 위한 최소한의 컴파일 가능한 소스 코드는 아래에 표시되어 있습니다. 글머리 기호로 설명된 단계는 기본 또는 로 표시된 기능에서 발생합니다 (). 지금까지 모든 것이 예상대로 작동합니다(Ubuntu 23.04).

사용자 코드는 미리 할당된 리소스에만 액세스하면 된다고 가정합니다(즉, C 라이브러리 함수에 대한 malloc이나 호출이 필요하지 않음). 이제 사용자가 입힐 수 있는 피해를 제한하기 위해 해당 사용자에게 속하지 않은 모든 메모리에 대한 쓰기를 비활성화하고 싶습니다. 특히 pkey 0이 있는 페이지에 대한 쓰기를 비활성화하고 싶습니다(PKRU.WD0=true로 설정, 즉 PKRU=0x55555552). 따라서 3a를 3b 또는 3c로 바꾸십시오.

  • (3b) wrpkru// 사용자가 예외를 발생시키지 않음을 통해 WD0=true로 설정 // 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;
}

관련 정보