BFS 2019 Exploitation Challenge
- 17/09/2019 - dansContext
The challenge is a single Windows 64-bits binary spawning a service listening on port 54321.
The rules consist in:
- Exploiting the binary to pop a "calc.exe" or "notepad.exe", by defeating
ASLR
remotely; - Ensuring the service continues to work flawlessly.
To be accepted, the exploitation code should be written in Python and work against the latest Windows 10 64-bits version (19H1, aka 1903).
Reverse engineering the service
The binary is quite small, and only consists in a few functions.
In the main function, the following actions are performed:
- If the number of arguments passed to the binary is 0,
WinExec
is called withargv[0]
as argument. This condition should never be true in normal cases, so this call has been put only to help resolving the challenge; - A table (later called
init_table
) of 256QWORD
is initialized with value0x488B01C3C3C3C3
ORed with the table index as the most-significant byte; - Finally, a socket is bound to port
54321
and a connection handler is called for each new connected client.
The table is quite interesting, since value C3
is the encoding for the RET
instruction, and 48 8B 01
encodes a MOV RAX, [RCX]
. Let's keep this information for later.
On the connection handler side:
- Two stack variables are initialized with an address in the
.data
(later calleddata_pointer
) section and value0x3e
(later calledoffset
); - A 0x10 bytes header is then read from the socket. This header has the following structure:
struct header {
dq magic;
dd pktSize;
dd unknown;
};
header.magic
is checked to matchEko2019\0
;header.pktSize
is checked to be lower than512
;header.pktSize
bytes are then read from the socket to a 512-bytes stack buffer.
When looking at what happen to the received data, we immediately know we face a challenge, the behavior being very unusual:
- The received buffer content is displayed on
stdout
usingprintf(" [+] Remote message (%i): '%s'\n")
; - The
printf
return value is stored indata_pointer
; - Entry at index
offset
is retrieved frominit_table
, swapped, then written over a place-holder function usingWriteProcessMemory
; - The placeholder function is called with the stack variable containing
data_pointer
as argument, and its 8-bytes return value is sent back in the socket.
As the default offset
used in the function is 0x3E
, the placeholder function is overwritten with bytes 3E 48 8B 01 C3 C3 C3 C3
. 3E
is a well known x86
segment override prefix: it makes the memory access instruction use the DS
segment. The placeholder function code is thus:
0x0: mov rax, qword ptr ds:[rcx]
0x4: ret
0x5: ret
0x6: ret
0x7: ret
Looking for a vulnerability
When looking at how the size is checked and used, one can notice a bad behavior:
size
is checked to be lower than512
using thejle
instruction, which implies a signed comparison;size
is then casted as an unsigned word using themovzx
instruction.
This is clearly a vulnerability, as we can forge a size
field which will pass the comparison (providing a negative 32-bits integer) and will be greater than 512
after the cast. For example, using value 0xffff0400
will pass the check because it represents the signed number -64512
, but size 0x400
will then be used for the recv()
call.
Exploitation
Defeating ASLR
Just after the fixed-size buffer are the two aforementioned stack variables: offset
and data_pointer
, which mean we can change both the code executed in the placeholder function and its argument! If we stick to the used offset
(3E
), changing only the data_pointer
provides us with an arbitrary read, which is a very strong primitive for a remote exploit. However, we still have to find a suitable memory address to read!
Considering we can chose an arbitrary x86
prefix for the executed mov instruction, we can change offset
to use the GS
override prefix, changing the called function to:
0x0: mov rax, qword ptr gs:[rcx]
0x4: ret
0x5: ret
0x6: ret
0x7: ret
On 64-bits Windows, GS
segment points to the Thread Information Block
, a data structure containing various information about the currently running thread, with interesting values from an exploiter point of view:
- The stack base value at offset
0x8
, giving us a pointer to the stack; - The PEB address at offset
0x60
, from which we can read the processImageBaseAddress
at offset0x10
.
From now, we have successfully defeated ASLR
as we are able to read the whole binary memory space as well as the current thread stack.
Code execution
Instead of changing the byte before the move instruction to a x86
prefix, we can change it to a 1-byte instruction to try to change the execution flow. Considering that we control the function first parameter (passed in rcx
register), we can use the offset
value 51
, changing the function to:
0x0: push rcx
0x1: mov rax, qword ptr [rcx]
0x4: ret
0x5: ret
0x6: ret
0x7: ret
We thus can redirect execution to the address contained in rcx
. We just need to find a suitable gadget to make rsp
point to our stack-buffer, which contains data we control. Such a gadget will allow us to execute a ROP-chain
.
Stack pivot and ROP chain
When we call the placeholder function, the stack-buffer we control is 0x60
bytes above. We can thus find a simple gadget adjusting rsp
to fall in our buffer. Such a gadget can be found at offset 0x1aea
in the binary:
.text:0000000140001AEA add rsp, 88h
.text:0000000140001AF1 retn
For the ROP-chain
, all we need is returning to main
with controlled arguments on the stack to be able to call WinExec
.
The first problem we face is that we need a gadget to set rcx
and rdx
, which are arguments argc
and argv
for function main
. This can easily be circumvented by calling main+0x9
, since the two first instructions copy these registers on the stack:
.text:0000000140001410 main proc near
.text:0000000140001410 mov [rsp+arg_8], rdx
.text:0000000140001415 mov [rsp+arg_0], ecx
We can now provide main
arguments by passing them on the stack.
Another thing we need to care about is the stack alignment. On Windows, it is assumed that the stack pointer is aligned on 0x10
after a function prologue. If the stack is not aligned, some compiler optimizations such as the use of XMM
registers will trigger an access violation.
Finally, we need to craft the fake argv
to have argv[0]
pointing to a string we control. This can be done by scanning the stack to retrieve our buffer ("egg hunting" to find a magic value), and then crafting the fake array in the buffer. All the pieces (ROP-chain
, fake argv
, command to execute) can be put in the stack buffer.
Service continuation
After the WinExec
execution, the main
function will continue to execute until it tries to bind the socket a second time and fail. It will then return, using the next entry in our ROP-chain
.
Now, all we have to do is chaining gadgets incrementing the stack pointer to make it point to the return address of the connection handler. The process will then return to main, just as if the connection handler terminated successfully, and will accept new connections from clients.
Final exploit
The final exploitation code is available on our GitHub, feel free to drop us an e-mail if you have a different exploitation technique!