背景:メモリ保護キー(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とユーザーが発生した例外は、それぞれ2つのスイッチ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;
}