Janken
4 minutes to read
We are given a binary file called janken
and a remote instance to connect to:
$ file janken
janken: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter ./.glibc/ld-linux-x86-64.so.2, BuildID[sha1]=56b54cdae265aa352fe2ebb016f86af831fd58d3, for GNU/Linux 3.2.0, not stripped
It is a Rock-Paper-Scissors game:
$ nc 167.99.86.8 31902
โโโโโโโโโโโโโโโโโโ
โ ใ ใ ใ ๆณ โ
โโโโโโโโโโโโโโโโโโ
1. โ โ โณ าฐ
2. โ โง โ ำ โ
>> 2
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ [*] Rock is called "Guu" (ใใ). โ
โ [*] Scissors is called "Choki" (ใกใใ). โ
โ [*] Paper is called "Paa" (ใฑใ). โ
โ โ
โ 1. Rock > scissors, scissors > paper, paper > rock. โ
โ 2. You have to win [100] times in a row in order to get the prize. ๐ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
1. โ โ โณ าฐ
2. โ โง โ ำ โ
>> 1
[!] Let the game begin! ๐
[*] Round [1]:
Choose:
Rock ๐
Scissors โ๏ธ
Paper ๐
>> rock
[!] Guru's choice: rock
[!] Your choice: rock
[-] You lost the game..
Let’s analyze the binary to see if we can somehow predict the oponent’s moves.
Reverse engineering
If we open it in Ghidra, we will see this main
function in decompiled C code:
void main() {
long in_FS_OFFSET;
long local_18;
long canary;
canary = *(long *) (in_FS_OFFSET + 0x28);
setup();
banner();
local_18 = 0;
while (local_18 != 1) {
fprintf(stdout, &DAT_00102668, &DAT_00102083, &DAT_0010207b, &DAT_00102073, &DAT_0010206b, &DAT_00102008, &DAT_00102073, &DAT_001026ac, &DAT_0010207b, &DAT_0010206b, &DAT_001022b9, &DAT_00102008);
local_18 = read_num();
if (local_18 == 2) {
rules();
} else {
if (local_18 != 1) {
/* WARNING: Subroutine does not return */
exit(0x16);
}
fprintf(stdout, &DAT_001026b8, &DAT_0010207b, &DAT_00102008);
}
}
for (; rounds < 100; rounds = rounds + 1) {
fprintf(stdout,"\n[*] Round [%d]:\n", rounds);
game();
}
if (rounds == 100) {
get_prize();
} else {
fprintf(stdout,"%s\n[-] You lost the game..\n\n", &DAT_00102083);
}
if (canary == *(long *)(in_FS_OFFSET + 0x28)) {
return;
}
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
The relevant function is game
, which is called on each round (99 times, from 1 to 99):
void game() {
int index_choice;
time_t seed;
ushort **ppuVar1;
size_t length;
char *pcVar2;
long in_FS_OFFSET;
ulong i;
char *choices[4];
char *other_choices[4];
char my_choice[32];
long canary;
canary = *(long *) (in_FS_OFFSET + 0x28);
seed = time(NULL);
srand((uint) seed);
index_choice = rand();
choices[0] = "rock";
choices[1] = "scissors";
choices[2] = "paper";
my_choice._0_8_ = 0;
my_choice._8_8_ = 0;
my_choice._16_8_ = 0;
my_choice._24_8_ = 0;
other_choices[0] = "paper";
other_choices[1] = "rock";
other_choices[2] = "scissors";
fwrite(&DAT_00102540, 1, 0x33, stdout);
read(0, my_choice, 0x1f);
fprintf(stdout, "\n[!] Guru\'s choice: %s%s%s\n[!] Your choice: %s%s%s", &DAT_00102083, choices[index_choice % 3], &DAT_00102008, &DAT_0010207b,my_choice, &DAT_00102008);
i = 0;
do {
length = strlen(my_choice);
if (length <= i) {
LAB_001017a2:
pcVar2 = strstr(my_choice,other_choices[index_choice % 3]);
if (pcVar2 == NULL) {
fprintf(stdout,"%s\n[-] You lost the game..\n\n",&DAT_00102083);
/* WARNING: Subroutine does not return */
exit(0x16);
}
fprintf(stdout,"\n%s[+] You won this round! Congrats!\n%s",&DAT_0010207b,&DAT_00102008);
if (canary != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}
ppuVar1 = __ctype_b_loc();
if (((*ppuVar1)[my_choice[i]] & 0x2000) != 0) {
my_choice[i] = '\0';
goto LAB_001017a2;
}
i = i + 1;
} while (true);
}
So, the program usees a time-based seed to initialize a PRNG, then uses rand() % 3
to select the next choice for the game.
Overkill solution
This is vulnerable because we can use the same logic to get the same time-based seed and thus find the same rand() % 3
value on each round of the game:
#include <stdlib.h>
#include <stdio.h>
#include <time.h>
int main() {
srand(time(NULL));
printf("%d\n", rand() % 3);
return 0;
}
When doing the CTF, I compiled the above C code and called the program from Python to get the next choice and find the opposite one to win the game on each round until finding the flag:
#!/usr/bin/env python3
from pwn import context, log, process, remote, sleep, sys
choices = ['rock', 'scissors', 'paper']
host, port = sys.argv[1].split(':')
io = remote(host, port)
io.sendlineafter(b'>> ', b'1')
prog = log.progress('Round')
def get_rand():
with context.local(log_level='CRITICAL'):
return int(process('./prng').recvline().decode())
for r in range(99):
prog.status(str(r))
enemy_choice = choices[get_rand()]
my_choice = choices[(choices.index(enemy_choice) + 2) % 3]
sleep(.1)
io.sendlineafter(b'>> ', my_choice.encode())
prog.success()
io.success(io.recv().decode())
Flag
After a lot of tries, eventually we find the flag:
$ gcc prng.c -o prng
$ python3 solve.py 167.99.86.8:31902
[+] Opening connection to 167.99.86.8 on port 31902: Done
[+] Round: Done
[+]
[!] Guru's choice: paper
[!] Your choice: scissors
[+] You won this round! Congrats!
[+] You are worthy! Here is your prize: HTB{r0ck_p4p3R_5tr5tr_l0g1c_buG}
[*] Closed connection to 167.99.86.8 port 31902
My solution was overkill because the bug was in the use of strstr
. The idea is to send rockpaperscissors
on every round, because strstr
only checks if a substring rock
, paper
or scissors
is found in the input string.