L'analyse commence par l'examen du fichier APK fourni pour le challenge. L'utilisation de l'outil JADX permet d'inspecter le code source décompilé et d'identifier rapidement le point d'entrée de la validation du flag.
Analyse de la couche Java
Dans la classe principale, une méthode de vérification impose des contraintes strictes sur le format de la chaîne de caractères saisie par l'utilisateur. Le code peut être résumé ainsi :
if (inputFlag.length() == 36 && inputFlag.startsWith("ISCC{") && inputFlag.endsWith("}")) {
// Transition vers la logique native
boolean isValid = NativeBridge.verifyFlag(getAssets(), inputFlag);
if (isValid) {
updateStatus("Succès", Color.GREEN);
}
} else {
updateStatus("Format invalide", Color.RED);
}
Le cœur du processus de validation est déporté dans une bibliothèque native via JNI (Java Native Interface). L'appel à NativeBridge.verifyFlag nous oriente vers l'analyse d'un fichier .so (Shared Object) contenu dans l'APK.
Exploration de la bibliothèque native
L'ouverture du fichier libnativecrypto.so avec un désassembleur tel qu'IDA Pro révèle plusieurs étapes de traitement cryptographique complexes :
- Extraction des ressources : Le binaire charge quatre fichiers depuis les assets de l'aplication :
cipher1.bin,cipher2.bin,cipher3.binetpuzzle.bin. - Désobfuscation des constantes : Des clés situées dans la section
.rodatasubissent une transformation initiale suivant l'algorithme(valeur - 0x13) ^ 0x5A. - Étage 1 (TEA-like) : Les 8 premiers octets du flag (Partie 1) sont chiffrés par une variante de l'algorithme TEA sur 16 rounds, puis comparés au contenu de
cipher1.bin. - Étage 2 (Matrices GF) : Les 12 octets suivants (Partie 2) sont validés via une multiplication matricielle 4x4 dans le corps de Galois
GF(2^8). - Étage 3 (LCG & Keystream) : Un générateur congruentiel linéaire (LCG) de 64 bits utilise les octets de la Partie 2 comme graine pour produire un flux de clés (keystream). Ce flux est utilisé pour déchiffrer
cipher3.binpar XOR. - Contraintes d'intégrité : Une fonction finale vérifie la cohérence entre les différentes parties du flag via des sommes de contrôle et des opérations XOR cumulées.
Script de résolution (Exploit)
Le script suivant automatise l'extraction des composants, inverse les opérations matricielles et déchiffre les différentes sections pour reconstituer le flag original.
import struct
import zipfile
from pathlib import Path
# Configuration des constantes algorithmiques
DELTA_VAL = 0x9E3779B9
MULT_LCG = 0x5851F42D4C957F2D
INC_LCG = 0x14057B7EF767814F
CHARS_VALIDES = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz{}_-!@#$%^&*()[]<>?.,:;+/\\"
S_BOX_REFS = bytes.fromhex(
"637c777bf26b6fc53001672bfed7ab76ca82c97dfa5947f0add4a2af9ca472c0"
"b7fd9326363ff7cc34a5e5f171d8311504c723c31896059a071280e2eb27b275"
"09832c1a1b6e5aa0523bd6b329e32f8453d100ed20fcb15b6acbbe394a4c58cf"
"d0efaafb434d338545f9027f503c9fa851a3408f929d38f5bcb6da2110fff3d2"
"cd0c13ec5f974417c4a77e3d645d197360814fdc222a908846eeb814de5e0bdb"
"e0323a0a4906245cc2d3ac629195e479e7c8376d8dd54ea96c56f4ea657aae08"
"ba78252e1ca6b4c6e8dd741f4bbd8b8a703eb5664803f60e613557b986c11d9e"
"e1f8981169d98e949b1e87e9ce5528df8ca1890dbfe6426841992d0fb054bb16"
)
def gf_mul(a, b):
p = 0
for _ in range(8):
if b & 1:
p ^= a
hi_bit = a & 0x80
a = (a << 1) & 0xFF
if hi_bit:
a ^= 0x1B
b >>= 1
return p
def solve_matrix_inv(matrix_bytes):
# Transformation de la matrice pour inversion dans GF(2^8)
# Implémentation simplifiée pour la démonstration
size = 4
matrix = [list(matrix_bytes[i*size:(i+1)*size]) for i in range(size)]
iden = [[1 if i == j else 0 for j in range(size)] for i in range(size)]
for i in range(size):
for j in range(i, size):
if matrix[j][i] != 0:
matrix[i], matrix[j] = matrix[j], matrix[i]
iden[i], iden[j] = iden[j], iden[i]
break
inv = next(x for x in range(1, 256) if gf_mul(matrix[i][i], x) == 1)
matrix[i] = [gf_mul(x, inv) for x in matrix[i]]
iden[i] = [gf_mul(x, inv) for x in iden[i]]
for k in range(size):
if k != i:
factor = matrix[k][i]
matrix[k] = [matrix[k][m] ^ gf_mul(matrix[i][m], factor) for m in range(size)]
iden[k] = [iden[k][m] ^ gf_mul(iden[i][m], factor) for m in range(size)]
return iden
def decrypt_tea(data, key):
v0, v1 = struct.unpack(">2I", data)
k = struct.unpack(">4I", key)
sum_val = (DELTA_VAL * 16) & 0xFFFFFFFF
for _ in range(16):
v1 = (v1 - ((((v0 << 4) & 0xFFFFFFFF) + k[2]) ^ ((v0 + sum_val) & 0xFFFFFFFF) ^ ((v0 >> 5) + k[3]))) & 0xFFFFFFFF
v0 = (v0 - ((((v1 << 4) & 0xFFFFFFFF) + k[0]) ^ ((v1 + sum_val) & 0xFFFFFFFF) ^ ((v1 >> 5) + k[1]))) & 0xFFFFFFFF
sum_val = (sum_val - DELTA_VAL) & 0xFFFFFFFF
return struct.pack(">2I", v0, v1)
def extract_flag_data(apk_file):
with zipfile.ZipFile(apk_file, 'r') as z:
c1 = z.read("assets/cipher1.bin")
c2 = z.read("assets/cipher2.bin")
c3 = z.read("assets/cipher3.bin")
so_data = z.read("lib/arm64-v8a/libnativecrypto.so")
return c1, c2, c3, so_data
def find_keys(so_blob):
pos = so_blob.find(CHARS_VALIDES)
raw_keys = so_blob[pos-48:pos]
decoded = bytes(((b - 0x13) & 0xFF) ^ 0x5A for b in raw_keys)
return decoded[:16], decoded[16:32], decoded[32:48]
def run_solver():
# Chargement et extraction
c1, c2, c3, lib_data = extract_flag_data("challenge.apk")
k1, k2, k3 = find_keys(lib_data)
# Reconstruction de l'état interne
mix = bytes(a ^ b for a, b in zip(k1, k3))
state_a = mix[3:] + mix[:3]
# Récupération Partie 1
rolling = bytes(S_BOX_REFS[state_a[i] ^ k3[i]] for i in range(8)) # simplification
p1 = bytes(a ^ b for a, b in zip(decrypt_tea(c1, state_a), rolling[:8]))
# Récupération Partie 2 via inversion matricielle
# Note: L'état matriciel B est dérivé de k2 et state_a dans le binaire
# ... (logique de dérivation omise pour la brièveté) ...
print(f"Flag partiel récupéré: ISCC{{{p1.decode()}...}}")
if __name__ == "__main__":
# run_solver()
# Le script complet permet de retrouver : ISCC{o<y: pass=""></y:>