For this pwnable we've got a zip with AppJailLauncher.exe
and thing2.exe
. This
means we get to experience the wonders of ASLR+DEP+Win8.1 🔥 tl;dr ruby solution
- C++ Object Memory Layout (Virtual Function Tables)
- Windows 64bit ABI / Calling Convention
- ASLR, DEP/NX
- ROP
Inputting arbitrary trash tends to crash it. Guessing the input shows the
program provides noticeably different output when given 1
, 2
, 1XXX...
, or 2XXX...
where X is anything.
We noticed the author for this challenge is someone who has written many previous challenges, which all take input in this number-based command format.
An access violation reading from an address we control in rdx
was simple to trigger, and
it seems like a lot of different inputs will trigger it...
So we modify our input to be a de-bruijn sequence of the same length.
We see that rdx
is set to values starting at character 200 in our input.
mov rax, qword ptr ds:[rdx]
mov rsi, rcx
mov rcx, rdx
mov rdi, rdx
call qword ptr ds:[rax+8]
We want rdx
to point to a memory location 8 bytes before a pointer to
code we want called. This should be a pivot gadget that will set rsp
to a place we can store our ropchain. The function pointer is at [rdx]+8
and
rdx
is copied into both rcx
and rdi
, which means we can look for any gadget
that does rsp=rdi|rcx|rdx
. Gadget hunting will come later, because we need to
take care of...
Remember the 2XXX...
command saying Decompressed is ...
? We played with that
for a minute, and noticed it tended to not crash when it was fed numbers...
The astute reader will notice the text after "Decompressed is" is the length of the first number we pass it, and the contents are the decimal ASCII values of other numbers we pass it.
At this point, I went and wasted a ton of time reversing how this was done figuring there was just some string-based infoleak... but it was easier than that!
This is where the message was being printed TO A STRING. It returns without printing back to stdout.
The Decompressed is <data>
string is passed as the format argument to printf.
It's a simple format bug from there on out, that we can then use to leak pointers
off the stack by encoding %p
over and over.
In this, we can note some pointers to various module executable code sections and use them to calculate base addresses of the modules. This is a very naive solution that makes our exploit very version-dependent!
ntdll_base = leaks[48] - 0x15444
k32_base = leaks[42] - 0x13d2
thing2_base = leaks[19] - 0x9200
msvcp_base = leaks[7] - 0x4fd00
msvcr_base = leaks[30] - 0x209eb
It's also noticable that when a previous 1XXX...
command has been run, a pointer
to the heap where it was allocated will be leaked here. This was found entirely coincidentally
by inspecting the stack here in a debugger, as we were trying to see if anything
pointed to our input. It should be noted that this heap memory is free at this point.
"free heap memory of a controlled size" is important, as when we trigger the crash we can allocate this same size -- causing our input to exist at a location we know! 🍃
Let's take a look again at how our buffer needs to be laid out, in C pseudocode.
struct input {
void* ropChainStart; // we pivot RSP to here. this gadget needs to clean the next pointer off the stack
void* ropChainPivot; // first thing that gets called
void* carryOnAsNormal; // rest of the chain
};
So, we went looking for a pivot gadget that does rsp=rdi|rcx|rdx
. In ntdll, we found:
mov rsp, [rcx + 0x98];
mov rcx, [rcx + 0xf8];
jmp rcx;
Which is good enough! Note that the offset to this is at ntdll_base + 0x93ab6
, which means it is
likely to change across win8.1 patch levels. So let's take note of our buffer structure:
[ropChainStart][ropChainPivot][A*136][ptr to set RSP to][A*88][first gadget after pivot][rest of chain]
^ 8 ^ 16 ^ 152 (0x98) ^ 248 (0xf8)
That's pretty much the gist of it. We know the location of our buffer in memory by allocating
a large buffer using the 1
command, running the 2
leak, and then sending our buffer
of the same size. The rest of the chain uses msvcr's: fopen, fread
printf, and flushall to get us back the key.
I noticed that things were slightly janky, and this exploit may have only worked
when I decided to switch to using \x00
as filler for my unimportant buffer data.
I wasted a lot of time reversing some hash table scheme that I believe was the actual
point of the challenge.
Here's the full code: https://gist.github.com/dwendt/f0fcec6f8f48ad53bedb