Challenge Protected
Este reto consiste en analizar un volcado de la partición /data de Android proveniente de un dispositivo que ejecuta la aplicación GalleryVault para encontrar un archivo multimedia oculto y la solución consiste en consultar las bases de datos SQLite para ubicar el archivo, utilizar Jadx para analizar estáticamente el APK descompilado y llevar a cabo un ataque de texto plano conocido (KPA) para extraer la clave de cifrado, romper la ofuscación XOR y ensamblar la imagen final que contiene la flag.
Los archivos proporcionados consisten en un volcado del sistema de archivos de Android. Una inspección preliminar del directorio data revela varias carpetas estándar de Android.
$ cd data
$ ls
adb app-ephemeral bootchart fonts local.prop misc_ce per_boot rollback-history system_ce user_de
anr app-lib cache gsi lost+found misc_de preloads rollback-observer system_de vendor
apex app-private dalvik-cache gsi_persistent_data media nfc property server_configurable_flags tombstones vendor_ce
app app-staging data incremental mediadrm ota resource-cache ss system unencrypted vendor_de
app-asec backup drm local misc ota_package rollback system user
Existe una carpeta unencrypted que contiene únicamente claves y referencias.
$ ls unencrypted
key mode per_boot_ref ref
La búsqueda de APKs instalados arroja varias aplicaciones de interés.
$ find -name base.apk 2>/dev/null
./app/~~8Ot3xFnHxIA5G1EBv2XkAA==/com.android.vending-0WnXQTRao1TRfgOYf6N2rw==/base.apk
./app/~~kGGaXfYOgnywjhPpaMVKbQ==/org.thoughtcrime.securesms-fKTywNyaYqPs30rWNVPWtQ==/base.apk
./app/~~xQBJ50iw1oO3bdl3q0vg_Q==/com.google.android.apps.restore-DUZq-5l_c-H-Ehpgh7WTPw==/base.apk
./app/~~EbACqxIPu-027FzlAQAAxw==/com.google.android.contactkeys-hB1aQm1LDeaT148PuUXjzw==/base.apk
./app/~~lDpCJWRbjfx7U3-X05iUCg==/com.google.android.gms-hh6YJN96J7LdG7A22eBKMg==/base.apk
./app/~~a7jcwJHUu9joGQrhYMPkDg==/com.google.ar.core-lhnIYYSmiYFHA2bR0lulxQ==/base.apk
./app/~~YtqSpVnlFnQ-CZBF-Js7HQ==/com.topjohnwu.magisk-S6_izVj57Zo74r4BVN8xbA==/base.apk
./app/~~aSe9Hqb8bP-Dh3DCicv9Qg==/notes.notepad.checklist.calendar.todolist.notebook-Y5KhcAnQ73h6JFGlMYvP1w==/base.apk
./app/~~yGbAQeKpuoLQ_p40HsRbog==/com.google.android.trichromelibrary_677813638-bMi4oXsZdav-Z3Dj_DOmqg==/base.apk
./app/~~2qXqwf1y-IeO0sr8TIHMgw==/com.thinkyeah.galleryvault-lqXfwbOnDTEzGpHYF5CHpA==/base.apk
./app/~~50gTwEyIu6K-QI0D1Snj1Q==/com.google.android.youtube-QnYH4lT8sSigEfVvzUxsVA==/base.apk
./app/~~Bve9RB9NcOrvaXbJeavNWQ==/com.android.chrome-An0s-R6NITvoziJXdBz7uQ==/base.apk
./app/~~txG5PGKmjIicn0p8yjg3MQ==/com.passmark.osfextract-jwbcOR1VUQhPr5C6jxyAjg==/base.apk
./app/~~o_1cVZLkLcKrOUcCdQy5DQ==/com.google.android.apps.youtube.music-CWcfzUMG92jhKEdRyQHBpQ==/base.apk
La presencia de Magisk indica un dispositivo rooteado, mientras que es probable que OSFExtract se haya utilizado para adquirir el volcado del sistema de archivos. Gallery Vault, una aplicación que se usa para ocultar archivos multimedia, destaca como un objetivo principal para la investigación. El archivo de base de datos de la aplicación se puede explorar mediante SQLite3.
$ sqlite3 data/com.thinkyeah.galleryvault/databases/galleryvault.db
SQLite version 3.53.3 2026-06-26 20:14:12
Enter ".help" for usage hints.
Al listar las tablas se revela una tabla file_v1 que contiene metadatos de los archivos ocultos.
sqlite> .tables
android_metadata cloud_transfer_items export_unhidden_history file_v1 recycle_bin_file_view tab_group
break_in_report download_and_encrypt_view file_folder_revision folder_v1 recycle_bin_folder_info tab_group_with_tab_view
browser_history download_task file_system_sync folder_with_cover_file_view recycle_bin_v1 web_url
change_actions encrypt_after_download_v1 file_system_sync_file_add misc_data tab
Al consultar la tabla file_v1 se descubre un archivo PNG oculto llamado flag.png asociado a un UUID específico.
sqlite> select * from file_v1;
╭───┬─────────┬────────┬────────┬────────┬────────┬────────┬──────────┬────────┬────────┬────────┬────────┬────────┬────────┬──────────┬────────┬──────┬────────┬────────┬────────╮
│_id│ uuid │profile_│ name │folder_i│file_typ│mime_typ│original_p│image_or│image_wi│image_he│video_du│encrypt_│added_ti│file_last_│file_siz│source│complete│storage_│file_sor│
│ │ │ id │ │ d │ e │ e │ ath │ientatio│ dth │ ight │ ration │ state │ me_utc │modifie...│ e │ │ _state │ type │t_index │
│ │ │ │ │ │ │ │ │ n │ │ │ │ │ │ │ │ │ │ │ │
╞═══╪═════════╪════════╪════════╪════════╪════════╪════════╪══════════╪════════╪════════╪════════╪════════╪════════╪════════╪══════════╪════════╪══════╪════════╪════════╪════════╡
│ 1│b238f4cd-│ 1│flag.png│ 11│ 0│image/ │/storage/ │ 0│ 564│ 568│ 0│ 0│17423990│1742399018│ 787165│NULL │ 0│ 1│ 0│
│ │f79f-4e19│ │ │ │ │png │emulated/0│ │ │ │ │ │ 78763│ 000│ │ │ │ │ │
│ │-be3b- │ │ │ │ │ │/Pictures/│ │ │ │ │ │ │ │ │ │ │ │ │
│ │068b7add4│ │ │ │ │ │flag.png │ │ │ │ │ │ │ │ │ │ │ │ │
│ │d85 │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │
╰───┴─────────┴────────┴────────┴────────┴────────┴────────┴──────────┴────────┴────────┴────────┴────────┴────────┴────────┴──────────┴────────┴──────┴────────┴────────┴────────╯
La búsqueda de este UUID en el sistema de archivos permite ubicar el archivo físico.
$ find -name "*7add4d85" 2>/dev/null
./media/0/.galleryvault_DoNotDelete_1742398726/files/b2/b238f4cd-f79f-4e19-be3b-068b7add4d85
Al visualizar la imagen directamente, se produce un error.
$ kitty +kitten icat ./media/0/.galleryvault_DoNotDelete_1742398726/files/b2/b238f4cd-f79f-4e19-be3b-068b7add4d85
Esto indica que el archivo está cifrado u ofuscado, requiriendo un análisis más profundo de la aplicación Gallery Vault. La descompilación del APK con JADX permite el análisis estático del código fuente. La inspección de la clase FileTailOperatorV1 revela una rutina de cifrado personalizada.
public final void a(File file, File file2, JSONObject jSONObject, byte[] bArr, boolean z10, long j10, long j11) throws IOException {
...
a aVar = new a(file2, 0L, j10);
...
for (int i7 = 0; i7 < i3; i7++) {
long j13 = ((long) i7) + j12;
bArr2[i7] = (byte) (bArr2[i7] ^ ((byte) (bArr[(int) (j13 % ((long) bArr.length))] ^ ((byte) j13))));
}
...
}
La aplicación lee una porción del archivo original, itera sobre los bytes leídos y aplica un cifrado XOR utilizando una clave, la longitud de la clave y el índice del byte. Además, la clase ThinkSecurity muestra cómo se deriva la clave de cifrado a partir de un PIN.
public static SecretKey e(String str) {
String strSubstring;
...
if (str.length() < 16) {
StringBuilder sb = new StringBuilder();
for (int i3 = 0; i3 < 16 - str.length(); i3++) {
sb.append("0");
}
strSubstring = str + sb.toString();
} else {
strSubstring = str.substring(0, 16);
}
return SecretKeyFactory.getInstance("DES").generateSecret(new DESKeySpec(strSubstring.getBytes()));
} catch (Exception unused) {
return null;
}
}
La aplicación utiliza el PIN del usuario para generar una clave DES, añadiendo un relleno de ceros si el PIN tiene menos de 16 caracteres o truncándolo si excede los 16 caracteres.
Una vez comprendido el mecanismo de cifrado, es posible crear un script personalizado para revertir el proceso. El script necesita procesar la estructura del archivo, descartando la cabecera PNG falsa que sirve de señuelo, y aislando el cuerpo no cifrado hasta la firma >>tyfs>>. La cabecera original de 2803 bytes, cifrada con una clave de 4 bytes y la fórmula XOR EncryptedByte[i] = PlaintextByte[i] ^ Key[i % 4] ^ (i & 0xFF), se encuentra inmediatamente después de esta firma. Dado que se sabe que el archivo original es un PNG, se puede ejecutar un ataque de texto plano conocido (KPA) usando los bytes mágicos de PNG (89 50 4e 47) sobre los primeros 4 bytes de la cabecera cifrada para extraer matemáticamente la clave de 4 bytes.
$ cat exploit.py
#!/usr/bin/env python3
import sys
file_path = './data/media/0/' \
'.galleryvault_DoNotDelete_1742398726/' \
'files/b2/b238f4cd-f79f-4e19-be3b-068b7add4d85'
with open(file_path, 'rb') as f:
data = f.read()
idx = data.find(b'>>tyfs>>')
if idx == -1:
print("Signature not found")
sys.exit(1)
enc_hdr = data[idx + 8 : idx + 8 + 2803]
unenc_body = data[2803 : idx]
png_hdr = bytes.fromhex('89504E47')
key = bytearray()
for i in range(4):
key.append(enc_hdr[i] ^ png_hdr[i] ^ i)
print(f"4 byte key recovered: {key.hex()}")
dec_hdr = bytearray()
for i in range(2803):
dec_hdr.append(enc_hdr[i] ^ key[i % 4] ^ (i & 0xFF))
with open('flag.png', 'wb') as f:
f.write(dec_hdr)
f.write(unenc_body)
print("File saved as flag.png")
La ejecución del script recupera la clave de cifrado y reconstruye la imagen.
$ python3 exploit.py
4 byte key recovered: 2eb8f950
File saved as flag.png
Visualizar la imagen recuperada revela la flag.
$ kitty +kitten icat flag.png
HTB{FLAG}