Analyse et contournement d'un mécanisme de vérification de flag Android

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.bin et puzzle.bin.
  • Désobfuscation des constantes : Des clés situées dans la section .rodata subissent 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.bin par 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:>

Étiquettes: reverse engineering Android Security JNI cryptography TEA Cipher

Publié le 19 juillet à 20h23