A little over a month ago, LegitBS held the qualifier for this year’s DEF CON CTF. As the competition was nearing a close, the organizers released an atypical pwnable challenge, a Windows binary. There are only a handful of CTFs that tend to release Windows exploitation challenges and there is minimal support in regards to tooling. While I wasn’t able to solve the challenge before the end of the competition, I decided to use it as motivation for adding minimal Windows support to the pwntools project to construct my exploit.
When starting a new challenge, one of the first things I try to do is model the challenge on a test system to aid in dynamic analysis. In order to sandbox the binary, the organizers have employed TrailOfBits’ AppJailLauncher. This utility takes advantage of a feature introduced in Windows 8 that allows you to run a binary in a container to isolate it from the rest of the operating system. After downloading the challenge files, I ran kernel32.dll through a simple script I wrote to map the version number with the Windows release it represents.
With the exact OS version in hand, I spun up a Windows 2012 R2 AWS instance to do my testing on. Unfortunately, when I ran the AppJailLauncher container app, the following error message popped up.
Rather than spend much time trying to troubleshoot why the this error was happening, ( or smartly downloading the source and recompiling ) I wrote a python script that redirected stdin and stdout to a socket so I could use pwntools to interact with the application. With a rough environment setup for testing, I moved on to static analysis with IDA Pro.
After some initial review, it appears the challenge is menu based with only 5 options. Unfortunately however, all of the challenge logic has been shoved into one giant function of over 15000 lines of decompiled code. Most of the bloat appears to be from the expansion of an inline function used for memory allocation into a custom heap. There is also extensive use of while loops to further obfuscate things. A snapshot of the available menus is listed below.
Since the target is running on a modern Windows OS, we will likely need a memory leak to identify where the binary is running in memory. Looking at the output from menu items 4 and 5, my guess is that these two functions will be responsible for extracting that information. Tracing back the “Active User” value to the code we can see that this is just the address of a global variable xor-ed with 0xFFFFFF. We can use this as our leak to identify where the application is being loaded in memory. Also, the initial value of “Money Earned” appears to be the address of VirtualAlloc in kernel32.dll and can be used for our ROP chain once we find the memory corruption bug.
Using my test environment and my IO wrapper script, I began some dumb fuzzing against the application to try and cause some crashes. I installed Windbg and cloned Skylined’s BugId project to use as my test harness for classifying any crashes I produced. After a fair amount of time, this process proved to be quite inefficient as I had to manually synchronize several of the components. To overcome these issues, I attempted to install pwntools on my test server (Windows 2008 R2) so that hopefully I could script the creation of a payload, the launching of the process, the attaching of the debugger, and the construction of an exploit. After minimal pain getting the library installed, it become apparent that pwntools is only supported on Linux based OSes. Somewhere around this point, the competition ended and I found myself with a new project.
PWNTOOLS FOR WINDOWS
For those of you that aren’t CTF regulars, pwntools is an amazing python library that greatly simplifies exploit development and the general tasks surrounding it. Things like process & socket creation, debugging, ROP chain construction, ELF parsing & symbol resolution, and much much more. Since most CTF challenges are Linux based, up until this point this library has been focused on these OSes. However, since it is written in python, there’s no reason we can’t add Windows support too 🙂 The remainder of this post is going to be structured more like a exploit development tutorial using each of the support features I added for Windows in my proof of concept. Keep in mind that each of these features are already present on the Linux side of the house and I merely added it for Windows.
Picking up where we left off during the competition, we have a memory leak for both the application and kernel32. We have yet to find any memory corruption vulnerabilities. One of the things I like to do with pwntools when I’m at this stage is to automatically attach a debugger from inside my python fuzzing/proof of concept code. This makes it much easier to track down the source of the crash if one happens. Pwntools currently only supports GDB, so I decided to add the same functionality for WinDbg. This includes the ability to pass a script to the debugger for things like setting break-points.
bin_path = 'C:\\Divided\\ConsoleApplication2.exe' #Windbg args args =  args.append('bp ConsoleApplication2+0xf0b1') # print plane info args.append('bp ConsoleApplication2+0xFB86') # func ptr callable args.append('g') #Start the process with windbg attached r = windbg.debug([bin_path], args ) #Attach windbg to already running process #r = process([bin_path]) #windbg.attach( r, args )
After spending a fair amount of time exercising the different combinations of menu options, I decided to think a little bit about the hints surrounding the challenge. The application represents some airline booking software and with the name, Divided, I assume it’s likely something in regards to the drama surrounding United Airlines and overbooking. With that in mind, I continue my fuzzing but only after I’ve sold a large number of tickets and almost instantly I get a crash.
Using a combination of WinDbg and IDA Pro, I tracked the bug down that is causing the crash. It appears to be the result of an out-of-bounds write in the buffer used for tracking the ticket sales and plane creation. The structure of the buffer looks like the following.
#main_app_struct struc ; (sizeof=0x202C, align=0x4, mappedto_36)
#00000000 customer_count dd ?
#00000004 gap4 dd ?
#00000008 ticket_arr db 4096 dup(?) ; ticket sale array
#00001008 num_of_planes dq ?
#00001010 plane_array db 4096 dup(?)
#00002010 money_earned? dq ?
#00002018 overwritable_func_ptr dq ? ; called when 0 menu is called
#00002020 gap2020 db 8 dup(?)
#00002028 func_ptr_arg dd ?
#0000202C main_app_struct ends
After 0x200 tickets have been sold, elements following the ticket array are overwritten by heap addresses representing new ticket sale structures. Additional research revealed that one of the fields being overwritten represents the function pointer being called if the hidden menu option ‘0’ is called. This means getting control of execution should be trivial.
Testing our overwrite and function pointer call, we see we are able to redirect execution. However, since we merely overwrote a function pointer, we still have to pivot the stack so we can retain control of execution. In an attempt to mirror how I usually do things with pwntools, I added support for loading a PE, loading the ROP gadgets, and searching for an appropriate stack pivot.
bin_path = 'C:\\Divided\\ConsoleApplication2.exe' #Get gadgets from the application pe = PE(bin_path) rop = ROP(pe, load_all=True) #Get stack pivot from main binary gadget_iter = rop.search_iter( regs=['rsp', 'rax'], ops=['xchg'] ) gadget_list = list(gadget_iter) gadget = gadget_list log.info('Gadget Address: ' + hex(gadget.address) )
Searching through the available stack pivot gadgets, we find a conveniently placed stack pivot that was intentionally introduced by the challenge writer as there aren’t actually any viable ones in any of the loaded modules. It’s actually the value that is added to the “money_earned” field from the previous memory leak screenshot.
Once we pivot the stack, we see that we landed in the name field of a ticket sale structure that we control. From here we simply have to construct a ROP chain to open the flag file, read it, and write it to stdout. Just to round out more of the Windows support, I added symbol resolution and some finer control over ROP gadget filtering.
#Add file open call lopen_off = rop.resolve('_lopen') buf += p64(lopen_off + k32_base) #Skip over garbage buf += p64(rdx_rop) buf += 'C' * 8 #Garbage that gets overwritten rax_gadg = None gadget_iter = rop.search_iter( dst_regs=['ecx'], src_regs=['eax', 'r9d'], ops=['mov'] ) gadget_list = list(gadget_iter) for gadget in gadget_list: if gadget.move == 0x2c: rax_gadg = gadget break