배경: 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;
}