#!/usr/bin/env python3
"""Recuperation pratique de texte clair sur un fichier reellement chiffre
par NightSpire via le chemin vulnerable (main.EncryptFilev2 / lignee
"Encrypter v2.1", main.EncryptFile / lignee "ImmoRan" : voir writeup.md).
N'implemente PAS le dechiffrement complet du fichier (la cle privee RSA de
l'attaquant reste necessaire pour ca) : exploite uniquement la reutilisation
du keystream AES-256-CTR ENTRE LES CHUNKS D'UN MEME FICHIER (pas entre
fichiers differents, a la difference d'autres familles de ce depot comme
RagnarLocker/Hive).

Principe : le fichier chiffre est decoupe en chunks de CHUNK_SIZE octets
(1 Mio par defaut, taille reelle utilisee par le ransomware) tous chiffres
avec le MEME keystream AES-CTR (le compteur repart de 0 a chaque chunk). Si
on connait/devine le texte clair a un offset donne DANS UN chunk (ex. une
plage de zero-fill dans une image disque VM/ISO), on recupere le keystream
a cet offset-la, valable pour TOUS les chunks du fichier a ce meme offset
relatif.

Usage :
    nightspire_recover_chunk.py FICHIER.enc --known-offset N --known-plaintext-file P [--chunk-size N]
    nightspire_recover_chunk.py FICHIER.enc --known-offset N --known-zero LEN [--chunk-size N]

--known-zero LEN : raccourci pour l'hypothese "ces LEN octets sont a zero
dans le clair" (region non allouee/padding), sans avoir a fabriquer un
fichier de texte clair connu.
"""
import argparse
import sys

CHUNK_SIZE_DEFAULT = 0x100000


def load_chunks(path, chunk_size):
    with open(path, "rb") as f:
        data = f.read()
    return [data[i:i + chunk_size] for i in range(0, len(data), chunk_size)], data


def main():
    ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
    ap.add_argument("target", help="fichier chiffre par NightSpire (chemin vulnerable EncryptFilev2/EncryptFile)")
    ap.add_argument("--chunk-size", type=int, default=CHUNK_SIZE_DEFAULT, help=f"taille de chunk (defaut {hex(CHUNK_SIZE_DEFAULT)})")
    ap.add_argument("--known-offset", type=int, required=True, help="offset DANS UN CHUNK ou le clair est connu/devine")
    group = ap.add_mutually_exclusive_group(required=True)
    group.add_argument("--known-plaintext-file", help="fichier contenant les octets de texte clair connus/devines")
    group.add_argument("--known-zero", type=int, help="nombre d'octets supposes a zero en clair a cet offset")
    ap.add_argument("--known-chunk", type=int, default=0, help="indice du chunk ou le clair est connu (defaut 0)")
    ap.add_argument("-o", "--output-dir", default=None, help="ecrit le clair recupere de chaque chunk dans ce dossier (sinon affiche un apercu)")
    args = ap.parse_args()

    chunks, raw = load_chunks(args.target, args.chunk_size)
    if args.known_chunk >= len(chunks):
        sys.exit(f"chunk {args.known_chunk} inexistant (fichier de {len(chunks)} chunk(s))")

    known_chunk = chunks[args.known_chunk]

    if args.known_zero is not None:
        known_plaintext = b"\x00" * args.known_zero
    else:
        with open(args.known_plaintext_file, "rb") as f:
            known_plaintext = f.read()

    off = args.known_offset
    n = min(len(known_plaintext), len(known_chunk) - off)
    if n <= 0:
        sys.exit("offset connu hors bornes du chunk de reference")
    known_plaintext = known_plaintext[:n]
    known_ciphertext = known_chunk[off:off + n]

    keystream = bytes(c ^ p for c, p in zip(known_ciphertext, known_plaintext))
    print(f"[*] {n} octet(s) de keystream recupere(s) a l'offset {hex(off)} (chunk {args.known_chunk} comme reference)")
    print(f"[*] fichier cible : {len(chunks)} chunk(s) de {hex(args.chunk_size)} octets max")

    for idx, chunk in enumerate(chunks):
        if idx == args.known_chunk:
            continue
        if off >= len(chunk):
            continue
        target_ct = chunk[off:off + n]
        m = min(len(target_ct), len(keystream))
        recovered = bytes(c ^ k for c, k in zip(target_ct[:m], keystream[:m]))
        print(f"\n--- chunk {idx}, offset {hex(off)}, {m} octet(s) recuperes ---")
        if args.output_dir:
            import os
            os.makedirs(args.output_dir, exist_ok=True)
            outp = os.path.join(args.output_dir, f"chunk{idx}_offset{off}.bin")
            with open(outp, "wb") as f:
                f.write(recovered)
            print(f"    ecrit dans {outp}")
        else:
            preview = recovered[:128]
            print("    hex   :", preview.hex())
            print("    ascii :", "".join(chr(b) if 32 <= b < 127 else "." for b in preview))


if __name__ == "__main__":
    main()
