Janken
4 minutos de lectura
Se nos proporciona un archivo binario llamado janken
y una instancia remota a la que conectarnos:
$ 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
Se trata de un juego de Piedra-Papel-Tijera:
$ 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..
Analicemos el binario para ver si de alguna manera podemos predecir los movimientos del oponente.
Ingeniería inversa
Si lo abrimos en Ghidra, veremos esta función main
en el código en C descompilado:
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();
}
La función relevante es game
, que se llama en cada ronda (99 veces, de 1 a 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);
}
Entonces, el programa usa una semilla basada en tiempo para inicializar un PRNG, luego usa rand() % 3
para seleccionar la siguiente opción del juego.
Solución excesiva
Esto es vulnerable porque podemos usar la misma lógica para obtener la misma semilla basada en el tiempo y, por lo tanto, encontrar el mismo valor rand() % 3
en cada ronda del juego:
#include <stdlib.h>
#include <stdio.h>
#include <time.h>
int main() {
srand(time(NULL));
printf("%d\n", rand() % 3);
return 0;
}
Al hacer el CTF, compilé el código C anterior y llamé al programa desde Python para obtener la siguiente opción y encontrar la opuesta para ganar el juego en cada ronda hasta encontrar la 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
Después de muchos intentos, eventualmente encontramos la 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
Mi solución fue exagerada porque el error estaba en el uso de strstr
. La idea es enviar rockpaperscissors
en cada ronda, porque strstr
solo verifica si se encuentra una subcadena rock
, paper
o scissors
en la cadena de entrada.