Skip to main content

RockPaperScissors (100)

¿Crees que puedes vencer al bot invencible de Piedra, Papel o Tijera?

En esta aplicación Android encontrarás un juego aparentemente simple, pero hay algo más escondido en su interior. El bot parece conocer tus movimientos antes de que los hagas... ¿será realmente invencible o hay alguna forma de engañarlo?

Descargar APK

Análisis inicial

Antes de nada, voy a instalar la aplicación mi teléfono o en un emulador para ver cómo funciona. Al abrir la aplicación, se presenta una interfaz sencilla donde puedes jugar al clásico juego de Piedra, Papel o Tijera contra un bot. Eso sí, si te gustas ganar, ya puedes ir olvidándote, porque el bot siempre juega la opción que vence a la tuya.

Captura de pantalla de la aplicación

Para entender cómo funciona el bot, vamos a descompilar el APK usando una herramienta como JADX-GUI. Al inspeccionar el código descompilado, navegamos hasta el paquete principal de la aplicación com.example.rps_vault y encontramos la clase MainActivity.java, que contiene la lógica del juego.

En la función F0 parece que se comparan las elecciones del jugador y del bot para determinar el resultado del juego. Las variables i3 e i4 representan las elecciones del jugador y del bot, respectivamente.

MainActivity.java
    public final String F0(int i3, int i4) {
String[] strArr = {"Piedra", "Papel", "Tijera"};
String str = strArr[i3];
String str2 = strArr[i4];
if (i3 == i4) {
return "Tú: " + str + " | Bot: " + str2 + "\n¡Empate!";
}
if ((i3 == 0 && i4 == 2) || ((i3 == 1 && i4 == 0) || (i3 == 2 && i4 == 1))) {
return "Tú: " + str + " | Bot: " + str2 + "\n¡Ganaste!";
}
return "Tú: " + str + " | Bot: " + str2 + "\n¡Perdiste!";
}
note

Voy a ir renombrando las variables y funciones para que el código sea más legible. Esta es una buena práctica cuando se analiza código descompilado.

La siguiente función G0 genera la elección del bot. Aquí, i3 representa la elección del jugador, y la función a(i3) parece calcular la elección del bot en función de la elección del jugador. Dependiendo del resultado, se actualiza el contador de victorias consecutivas y, si el jugador alcanza 3 victorias seguidas, se llama a la función H0, que muestra la flag del reto.

MainActivity.java
    public final void G0(int i3) {
String compararElecciones = compararElecciones(i3, this.J.a(i3));
this.E.setText(compararElecciones);
if (!compararElecciones.contains("¡Ganaste!")) {
this.I = 0;
I0();
return;
}
this.I++;
I0();
if (this.I >= 3) {
H0();
}
}

La función H0 (renombrada a mostrarFlag) es la encargada de mostrar la flag cuando el jugador ha ganado 3 veces consecutivas. Aquí se llama a un método a() de un objeto K, que probablemente descifra o genera la flag. Se muestra un mensaje de éxito y se deshabilitan los botones del juego para evitar más interacciones.

MainActivity.java
    public final void mostrarFlag() {
try {
String a4 = this.K.a();
Toast.makeText(this, "¡IMPOSIBLE! Aquí está tu flag:", 1).show();
this.descripcionTextView.setText("🎉 FLAG CONSEGUIDA 🎉\n\n" + a4);
this.F.setEnabled(false);
this.G.setEnabled(false);
this.H.setEnabled(false);
} catch (Exception e3) {
this.descripcionTextView.setText("Error al descifrar flag: " + e3.getMessage());
}
}

Analizando la clase CryptoManager

Vamos a centrarnos en la clase CryptoManager, que parece ser la responsable de generar o descifrar la flag.

CryptoManager.java
public class CryptoManager {

public static final byte[] f2100c = {90, -61, 126, -111, 47, -72, 100, -41, 60, -23, 21, -90, 114, -53, 78, -16};
public Context f2101a;
public DatabaseHelper f2102b;

public CryptoManager(Context context) {
this.f2101a = context;
this.f2102b = new DatabaseHelper(context);
}

public String generarFlag() {
byte[] decode = Base64.decode("GbYJxXzdE7APnHjTHr5jhg==", 0);
byte[] decode2 = Base64.decode("S/VIpBrfWKAKh2TBB6h6hg==", 0);
byte[] decode3 = Base64.decode("M6gb40PZCqxL2WL5C/s7ry3zEM5b0FeISYd3wxO/L5I2piHzH8wZ", 0);
byte[] bArr = f2100c;
byte[] b4 = b(decode, bArr);
byte[] b5 = b(decode2, bArr);
byte[] b6 = b(decode3, bArr);
Cipher.getInstance("AES/CBC/PKCS5Padding").init(2, new SecretKeySpec(b4, "AES"), new IvParameterSpec(b5));
return new String(b6, "UTF-8");
}

public final byte[] b(byte[] bArr, byte[] bArr2) {
byte[] bArr3 = new byte[bArr.length];
for (int i3 = 0; i3 < bArr.length; i3++) {
bArr3[i3] = (byte) (bArr[i3] ^ bArr2[i3 % bArr2.length]);
}
return bArr3;
}
}

Tras analizar la función generarFlag, podemos ver que utiliza tres cadenas codificadas en Base64 que son decodificadas y luego XOReadas con una clave secreta fija. El resultado de estas operaciones se utiliza para inicializar un cifrado AES en modo CBC, pero curiosamente, el resultado del cifrado no se utiliza para descifrar la flag, sino que simplemente se convierte a una cadena UTF-8 y se devuelve.

Resolviendo el reto

Para resolver el reto, necesitamos replicar la lógica de la función generarFlag. Tenemos que la función generarFlag() devuelve el resultado de convertir a cadena UTF-8 el resultado de XORear la tercera cadena decodificada en Base64 con la clave secreta fija.

Este podría ser un script en Python que realiza la operación para obtener la flag:

b64decode_xor.py
import base64

def xor(data, key):
return bytes([data[i] ^ key[i % len(key)] for i in range(len(data))])

secret_int = [90, -61, 126, -111, 47, -72, 100, -41, 60, -23, 21, -90, 114, -53, 78, -16]
pre_flag_b64 = "M6gb40PZCqxL2WL5C/s7ry3zEM5b0FeISYd3wxO/L5I2piHzH8wZ"

secret_bytes = bytes(x & 0xFF for x in secret_int)
pre_flag_bytes = base64.b64decode(pre_flag_b64)
flag_bytes = xor(pre_flag_bytes, secret_bytes)
flag = flag_bytes.decode('utf-8')

print(flag)

Al ejecutar este script, obtenemos la flag del reto:

ikerlan{w0w_y0u_w0n_th3_unbeatable_b0t}
note

Le he puesto mucho cariño a este write-up. Durante la resolución del reto, no me tomé las molestias de ir renombrando todas las funciones y variables como he hecho aquí, sino que fui directamente a la función, vi que hacía un XOR y lo implementé en Python. Pero para el write-up he querido hacerlo más didáctico y legible.