#!/usr/bin/env python3
"""
PoC : le generateur d'IV de Trigona (System.Random de Delphi/Borland, LCG 32 bits
lineaire, params publics mult=0x8088405 inc=1 mod 2^32) est invocable a partir
d'un seul IV de 16 octets fuite (present en clair dans l'en-tete/pied de chaque
fichier chiffre). Ce script :
  1. reimplemente Random()/RandSeed exactement comme le binaire (mult/add/scale)
  2. genere une sequence d'IV comme le fait FUN_004e34e4 (16 x Random(256))
  3. retrouve l'etat interne RandSeed a partir d'un seul IV de 16 octets fuite
     (recherche sur les 24 bits bas seulement, le 1er octet de l'IV donnant
     directement les 8 bits hauts de l'etat)
  4. a partir de cet etat retrouve, predit l'IV du fichier suivant chiffre par
     le meme thread (meme flux Random() global, jamais reamorce entre fichiers)
"""
import sys

MOD = 1 << 32
MULT = 0x8088405
INC = 1


def advance(seed):
    return (seed * MULT + INC) & 0xFFFFFFFF


def random_n(seed, n):
    seed = advance(seed)
    return seed, (seed * n) >> 32


def gen_iv(seed, n_bytes=16):
    iv = bytearray()
    for _ in range(n_bytes):
        seed, b = random_n(seed, 256)
        iv.append(b & 0xFF)
    return seed, bytes(iv)


def recover_state_before_iv(iv):
    """
    Retrouve l'etat RandSeed juste avant le tirage de cet IV (c'est a dire
    la valeur de RandSeed telle qu'elle etait avant le premier des 16 appels
    Random(256) qui ont produit cet IV), par recherche sur les 24 bits bas
    du premier etat interne (le 1er octet de l'IV = les 8 bits hauts).
    Complexite : jusqu'a 2^24 essais, rejet quasi immediat (des le 2e octet)
    pour les mauvais candidats.
    """
    b0 = iv[0]
    for low24 in range(1 << 24):
        s0 = (b0 << 24) | low24
        s = s0
        ok = True
        for i in range(1, 16):
            s, b = random_n(s, 256)
            if b != iv[i]:
                ok = False
                break
        if ok:
            # s0 est l'etat APRES le premier advance(); on retrouve l'etat
            # AVANT en inversant la relation lineaire (mult est impair donc
            # inversible mod 2^32)
            mult_inv = pow(MULT, -1, MOD)
            seed_before = (mult_inv * (s0 - INC)) & 0xFFFFFFFF
            return seed_before, s0
    return None, None


def main():
    print("=== Demonstration : seed de depart inconnue (entropie reelle a l'execution) ===")
    real_seed = 0xCAFEBABE  # valeur arbitraire simulant une graine jitter reelle inconnue de l'attaquant
    seed = real_seed

    # fichier 1 chiffre par le thread ransomware
    seed, iv_file1 = gen_iv(seed)
    print(f"fichier_1.docx.trigona -> IV stocke en clair dans le footer : {iv_file1.hex()}")

    # fichier 2 chiffre juste apres, meme thread, meme flux Random() global
    seed_before_file2 = seed
    seed, iv_file2 = gen_iv(seed)
    print(f"fichier_2.xlsx.trigona -> IV reel (que l'attaquant ne voit pas encore) : {iv_file2.hex()}")

    print("\n=== Attaque : a partir du SEUL IV de fichier_1 (public), sans cle, sans RSA ===")
    recovered_seed_before, recovered_state_after = recover_state_before_iv(iv_file1)
    if recovered_seed_before is None:
        print("echec (ne devrait pas arriver)")
        sys.exit(1)
    print(f"etat RandSeed retrouve juste avant fichier_1 : {recovered_seed_before:#010x}")

    # on rejoue exactement gen_iv depuis l'etat retrouve pour reproduire
    # la meme progression que le vrai flux, et predire fichier_2
    s = recovered_seed_before
    s, predicted_iv_file1 = gen_iv(s)
    s, predicted_iv_file2 = gen_iv(s)

    print(f"IV predit pour fichier_1 : {predicted_iv_file1.hex()}  (verification)")
    print(f"IV predit pour fichier_2 : {predicted_iv_file2.hex()}")
    print(f"IV reel     pour fichier_2 : {iv_file2.hex()}")
    assert predicted_iv_file1 == iv_file1
    assert predicted_iv_file2 == iv_file2
    print("\n=> IV du fichier suivant predit EXACTEMENT a partir d'un seul IV fuite,")
    print("   sans connaitre la cle AES ni la graine d'entropie reelle.")

    print("\n=== Borne d'anniversaire sur l'espace d'etat 32 bits ===")
    import math
    n_50pct = int(math.sqrt(2 ** 32 * math.log(2)))
    print(f"~{n_50pct:,} fichiers chiffres par le meme thread suffisent pour une")
    print("probabilite de 50% qu'au moins deux fichiers recoivent un IV identique")
    print("(meme cle, meme IV = reutilisation de keystream OFB, XOR direct des")
    print("deux chiffres = XOR des deux clairs, sans cle ni RSA).")


if __name__ == "__main__":
    main()
