35C3 Modern Windows Userspace Exploitation
At 35C3 I gave a talk named “Modern Windows Userspace Exploitation”, that covered the main exploit mitigations in Windows. The point of the talk was to introduce and evaluate the different mitigations that impact memory safety issues, and examine what kind of primitives an exploit developer would need in order to bypass them (since it’s quite a non-trivial process). Since I feel that the best way in order to do this is by example, I used a CTF challenge as a target, and exploited it on Windows 7, Windows 10 TH1 and Windows 10 RS5. The exploits target the great “Winworld” CTF challenge, from Insomnihack CTF Teaser 2017, written by @awe (thanks for writing this!). For a full explanation of what’s going on in this repo, I recommend watching the talk. The exploits in this repo are based on awe's repo, that had a full exploit for this challenge. There are a couple of key differences between them, though: first, I tried to aim for the simplest exploit for every one of the versions and mitigations I covered in the presentation. The original challenge ran on Windows 10 pre build 16179, compiled with CFG and without ACG, CIG and Child Process Restriction. Second, I used a completely different technique to leak the stack address. While I explained how I gained arbitrary RW and jump primitives in the talk, I didn’t explain this trick, so I will explain it below.
In his exploit, @awe chose to scan the heap memory, hoping to find random stack pointers there. It is a well known technique (for example, see @j00ru’s post). It works great for CTFs but it does reduce the reliably of the exploit: in my experiments it worked once every ~X times . To improve reliability, I used a more deterministic technique, by calling ntdll!RtlCaptureContext. The function looks like this:
I’m not the first one to use this function to leak the stack (here, for instance). It gets as its first argument a pointer to a ContextRecord structure and writes there the current value of all registers. One of them is rsp, so by calling this function and reading the value it wrote, the exploit can retrieve the stack address.
One problem that causes instability of the exploits is that calling ntdll!RtlCaptureContext on a Person object actually corrupts the heap metadata, because sizeof(Person) < sizeof(ContextRecord). When the exploit changes the onEncounter function pointer, it simply sprays more std::strings of commandlines. This “spraying” is dangerous, since commandline is freed immediately after. Put simply, the exploit allocates and frees many chunks, and due to the randomization in the LFH, it writes data on many different chunks in the userblocks. That’s great for setting the uninitialized values in the freed Person instance, but with some low probability, it might hit a corrupted chunk and crash on free(). The solution for that is to leak the address of some other person instance in memory, and use my arbitrary write to corrupt his onEncounter function pointer to points to ucrtbase!gets, and use it as generic reader/writer. And then we can read the stack pointer relatively to our corrupted person (which we have), with our arbitrary read, without spraying any other std::string. Note that the offsets in the exploits depend on the specific builds I used. Other builds may require different offsets relative to ntdll.dll and ucrtbase.dll base.