# PinBreaker: una app Android pensada para enseñar por qué ocultar secretos en cliente no funciona
Table of Contents
Máquina creada por: Oscar
Plataforma: The Hackers Labs
Sistema operativo: Android
Dificultad: principiante
Sobre este CTF
Este laboratorio lo diseñé para enseñar una idea muy concreta: cuando un secreto crítico queda embebido en el cliente, la validación deja de ser un control real y pasa a ser simplemente una barrera superficial. En este caso, la aplicación pide un PIN y sugiere que, una vez encontrado, se calcule su SHA256 para obtener la flag. La parte importante no es “romper” la app, sino entender qué decisión de diseño la hace débil desde el principio.
La cadena aquí es deliberadamente corta y directa. No buscaba construir una APK llena de trucos, ofuscación agresiva o rutas artificiales, sino obligar a mirar el binario con criterio: descompilar, localizar el punto de entrada lógico y revisar cómo se valida la entrada del usuario. El aprendizaje que quería forzar es muy reutilizable fuera de un CTF: si una aplicación cliente contiene el valor esperado o la lógica completa de validación, cualquiera con acceso al paquete puede reconstruirla.
También quise que el laboratorio sirviera como recordatorio de algo básico pero todavía frecuente en entornos reales: no se deben confiar secretos ni decisiones de autorización al lado cliente. En un reto pequeño esto se ve en forma de PIN hardcodeado; en escenarios reales aparece como claves embebidas, validaciones offline mal planteadas, feature flags sensibles o lógica de negocio que debería residir en servidor.
Información técnica
| Campo | Valor |
|---|---|
| Nombre | PinBreaker |
| IP objetivo | No aplica |
| Servicios | Aplicación Android (.apk) |
| Cadena principal | Descompilación de la APK → revisión de MainActivity → extracción del PIN hardcodeado → cálculo de SHA256 |
| Dificultad | Principiante |
Descubrimiento del artefacto
El punto de partida es simplemente la APK proporcionada en el laboratorio:
oscar@oscar-System-Product-Name:~/CTF$ ls -latotal 4915drwxrwxr-x 2 oscar oscar 3 mar 1 16:42 .drwxr-x--- 37 oscar oscar 57 mar 1 16:41 ..-rw-rw-r-- 1 oscar oscar 5777184 mar 1 16:41 PinBreaker.apkAquí no hay superficie de red ni un servicio que enumerar. La fase de reconocimiento consiste en identificar correctamente el tipo de objetivo y elegir el enfoque adecuado. Ese detalle forma parte del diseño del reto: quería que el análisis arrancase desde ingeniería inversa básica, no desde automatismos de enumeración típicos de máquinas Linux o Windows.
Lo que representa esta fase en términos de aprendizaje es sencillo: antes de atacar nada, hay que entender qué se tiene delante. En este caso, el valor pedagógico está en reconocer que la fuente de verdad probablemente esté dentro del paquete y que, por tanto, revisar la APK es el camino natural.
Descompilación y revisión inicial
El siguiente paso fue descompilar la aplicación con jadx:
oscar@oscar-System-Product-Name:~/CTF$ jadx -d PinBreaker_jadx PinBreaker.apkINFO - loading ...INFO - processing ...ERROR - finished with errors, count: 32Aunque la salida muestra errores, en este caso no impiden el análisis útil del binario. jadx genera igualmente la estructura de trabajo:
oscar@oscar-System-Product-Name:~/CTF/PinBreaker_jadx$ lsresources sourcesEste detalle también me interesaba como parte del laboratorio. En escenarios reales, las herramientas de reversing no siempre producen una reconstrucción perfecta. Aun así, eso no significa que el análisis haya fracasado. Muchas veces basta con recuperar las clases relevantes y seguir el flujo principal. El reto está construido para que el alumno no se bloquee por mensajes ruidosos de herramienta y se centre en lo importante: si el código sensible se ha recuperado, el problema ya es visible.
Localización de la lógica principal
Una vez generada la estructura, el siguiente movimiento fue localizar la actividad principal:
oscar@oscar-System-Product-Name:~/CTF/PinBreaker_jadx$ find -name "MainActivity*"./sources/com/pinbreaker/ctf/MainActivity.javaLa ruta encontrada fue:
./sources/com/pinbreaker/ctf/MainActivity.javaDiseñé esta fase para que el recorrido fuese legible y útil para quien está empezando a analizar aplicaciones móviles. No hacía falta perseguir múltiples clases, servicios secundarios ni mecanismos de carga dinámica. La intención era que el alumno asociase rápidamente la interfaz con la lógica que procesa el PIN y entendiese cómo una comprobación aparentemente inocente puede revelar el secreto completo.
Este tipo de revisión es relevante fuera del CTF porque muchas implementaciones móviles siguen concentrando decisiones sensibles en clases muy predecibles: actividades principales, controladores de login, validadores locales o utilidades de configuración. La simplicidad de acceso a esa lógica es, precisamente, el problema.
Extracción del PIN embebido
Al revisar MainActivity.java, aparece la validación del PIN:
package com.pinbreaker.ctf;
import android.os.Bundle;import android.view.View;import android.widget.Button;import android.widget.EditText;import android.widget.TextView;import androidx.appcompat.app.AppCompatActivity;import androidx.constraintlayout.widget.ConstraintLayout;import kotlin.Metadata;import kotlin.jvm.internal.Intrinsics;
public final class MainActivity extends AppCompatActivity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Button unlockButton = (Button) findViewById(R.id.unlockButton); final EditText pinInput = (EditText) findViewById(R.id.pinInput); final TextView result = (TextView) findViewById(R.id.result); unlockButton.setOnClickListener(new View.OnClickListener() { @Override public final void onClick(View view) { MainActivity.onCreate$lambda$0(pinInput, this, result, view); } }); }
private static final void onCreate$lambda$0(EditText $pinInput, MainActivity this$0, TextView $result, View it) { String pin = $pinInput.getText().toString(); if (this$0.checkPin(pin)) { $result.setText("✅ PIN correcto. Calcula su hash SHA256 para obtener la flag."); } else { $result.setText("❌ PIN incorrecto"); } }
private final boolean checkPin(String pin) { return Intrinsics.areEqual(pin, "8524947156"); }}La clave está en esta línea:
return Intrinsics.areEqual(pin, "8524947156");El PIN correcto es, por tanto:
8524947156Aquí está el núcleo del laboratorio. Quería mostrar de la forma más limpia posible el error de diseño: la aplicación contiene el valor esperado y realiza la comparación localmente. No hay una comprobación remota, no hay separación entre interfaz y decisión de seguridad, y no hay nada que realmente proteja el secreto una vez el atacante dispone del APK.
Ese patrón es pequeño, pero representa un fallo muy real. En producción no siempre adopta la forma de un PIN. A veces es una API key incrustada, una contraseña de servicio, un token de prueba olvidado, una comprobación de licencia local o una validación de privilegios que debería vivir en backend. El problema de fondo es el mismo: confiar en que el cliente va a ocultar algo que, por definición, se entrega al usuario.
Obtención de la flag
Una vez recuperado el PIN, la propia aplicación y el enunciado indican que la flag se obtiene calculando su hash SHA256. El cálculo se hizo así:
oscar@oscar-System-Product-Name:~/CTF/PinBreaker_jadx$ echo "8524947156" | sha256sum2a4be6606b9490b9955c7aac8e856c8e3098f9b15e98a8985ce5c192049c96efCon las notas disponibles, la flag resultante es:
2a4be6606b9490b9955c7aac8e856c8e3098f9b15e98a8985ce5c192049c96efHay, no obstante, un matiz técnico importante. El comando mostrado usa echo sin -n, así que introduce un salto de línea al final antes de pasar la cadena a sha256sum. Eso significa que el hash calculado corresponde a 8524947156\n, no necesariamente al texto desnudo 8524947156. No lo corrijo porque no hay que inventar ni alterar el comportamiento reflejado en las notas, pero sí conviene dejar constancia de ello: en un laboratorio así, ese tipo de detalle puede ser intencionado o simplemente una consecuencia operativa del comando usado.
También hay aprendizaje aquí. Incluso cuando el secreto ya se ha recuperado, sigue siendo necesario respetar exactamente la transformación pedida. En seguridad ofensiva y en análisis de aplicaciones, los detalles de formato importan.
Qué quería enseñar al diseñar PinBreaker
La idea de fondo en este CTF era enseñar una cadena mínima pero muy clara: entregar la lógica de validación y el secreto al cliente equivale a renunciar a protegerlos. No hace falta explotar memoria, interceptar tráfico ni abusar de componentes complejos. Basta con inspeccionar el paquete y leer con atención.
Más que un walkthrough, este laboratorio está pensado como una demostración de criterio: revisar dónde vive la validación, preguntarse quién controla realmente el secreto y entender que la ofuscación ligera o la simple compilación no son medidas de protección.
| Fase | Qué enseña | Error representado |
|---|---|---|
| Identificación de la APK | Elegir el enfoque según el tipo de objetivo | Tratar un binario cliente como si fuera una caja negra |
Descompilación con jadx | Obtener visibilidad suficiente aunque haya errores parciales | Confiar en que compilar u ofuscar mínimamente oculta la lógica |
Revisión de MainActivity | Seguir el flujo principal de una validación local | Colocar decisiones sensibles en el lado cliente |
| Extracción del PIN | Recuperar secretos embebidos desde código descompilado | Hardcoding de credenciales o valores de acceso |
| Cálculo del SHA256 | Respetar exactamente la transformación pedida | Ignorar detalles de formato al derivar artefactos de seguridad |
La cadena tiene sentido fuera del entorno CTF porque el patrón sigue apareciendo en software real. Cada vez que una app móvil, de escritorio o incluso una SPA contiene secretos o validaciones críticas que deberían residir en servidor, el problema es esencialmente el mismo que aquí, solo con más capas alrededor.
Recursos y referencias
jadx, para descompilación y revisión estática de aplicaciones Android.sha256sum, para derivar la flag a partir del PIN recuperado.- Buenas prácticas de seguridad en aplicaciones móviles: no almacenar secretos sensibles ni lógica de autorización en cliente.