#!/usr/bin/env python3
"""
unpack_clop.py : deroule les trois etages du crypter/loader de l'echantillon
Clop 389e03b1a1fd1c527d48df74d3c26a0483a5b105f36841193172f1ee80e62c1b jusqu'au
vrai payload PE (voir writeup.md pour le detail du reversing Ghidra qui a
etabli ce pipeline). Fonctionne entierement de facon statique (aucune
execution du sample) :

  1. Stub sur disque : un buffer chiffre (XOR + ROL9 + soustraction d'index)
     est decode et copie dans un VirtualAlloc, puis execute (voir FUN_00401000
     dans le stub).
  2. Ce shellcode decode est lui-meme un reflective PE loader complet :
     resolution dynamique d'API (GetProcAddress/VirtualAlloc/...), lecture
     d'un second blob embarque dans le stub (a l'offset correspondant a la VA
     0x004098b0), de-entrelacement (saute 2 octets sur 3), meme transformation
     XOR/ROL9/soustraction que l'etage 1, puis decompression aPLib
     (voir aplib_depack.py) vers le PE final.
  3. Le resultat est un PE32 valide : le vrai payload ransomware Clop
     (imports CryptEncrypt/RmShutdown/WTSEnumerateSessionsW confirmes,
     hash independamment recoupe avec le resultat d'UnpacMe sur le meme
     sample, voir writeup.md).

Dependance externe : pefile (pip install pefile ; installer dans un
virtualenv si le systeme est en environnement Python "externally managed").

Usage :
    unpack_clop.py SAMPLE.exe [-o sortie.bin]
"""
import argparse
import struct
import sys

import pefile

from aplib_depack import aplib_depack


def rol32(x: int, n: int) -> int:
    x &= 0xFFFFFFFF
    return ((x << n) | (x >> (32 - n))) & 0xFFFFFFFF


def unpack(path: str) -> bytes:
    pe = pefile.PE(path, fast_load=True)
    image_base = pe.OPTIONAL_HEADER.ImageBase
    data = open(path, "rb").read()

    def va_to_offset(va):
        return pe.get_offset_from_rva(va - image_base)

    # --- Etage 1 : decode le shellcode reflectif embarque dans le stub ---
    # FUN_00401000 : VirtualAlloc(NULL, 0x41a0, ...) puis boucle sur 0x564
    # dwords a partir de &DAT_0040831c, cle XOR = DAT_00408318.
    key1_off = va_to_offset(0x00408318)
    blob1_off = va_to_offset(0x0040831C)
    key1 = struct.unpack("<I", data[key1_off:key1_off + 4])[0]
    n1 = 0x564
    blob1 = data[blob1_off: blob1_off + n1 * 4]

    stage2 = bytearray()
    for i in range(n1):
        dw = struct.unpack_from("<I", blob1, i * 4)[0]
        val = rol32(dw ^ key1, 9) ^ key1
        val = (val - i) & 0xFFFFFFFF
        stage2 += struct.pack("<I", val)

    # --- Etage 2 : ce shellcode decode un second blob (contexte passe par le
    # stub : hKernel32, pointeur, taille, cle XOR, taille destination) ---
    raw_size = 0x19ED8               # param_1[2] (taille du blob brut)
    xor_key2 = struct.unpack("<I", data[va_to_offset(0x004098AC):va_to_offset(0x004098AC) + 4])[0]
    dest_size = struct.unpack("<I", data[va_to_offset(0x00423788):va_to_offset(0x00423788) + 4])[0]

    raw = data[va_to_offset(0x004098B0): va_to_offset(0x004098B0) + raw_size]

    # De-entrelacement : saute 2 octets source tous les 3 octets ecrits
    # (boucle identifiee dans le shellcode de l'etage 2, voir writeup.md).
    deinterleaved = bytearray()
    d4 = 0
    d8 = 0
    while d8 < raw_size:
        if d4 % 3 == 0:
            d8 += 2
        if d8 >= raw_size:
            break
        deinterleaved.append(raw[d8])
        d4 += 1
        d8 += 1

    n_decode = (raw_size * 3) // 5
    decoded = bytearray(deinterleaved[:n_decode])
    for i in range(n_decode // 4):
        dw = struct.unpack_from("<I", decoded, i * 4)[0]
        val = rol32(dw ^ xor_key2, 9) ^ xor_key2
        val = (val - i) & 0xFFFFFFFF
        struct.pack_into("<I", decoded, i * 4, val)

    # --- Decompression aPLib vers le PE final ---
    final_pe = aplib_depack(bytes(decoded), dest_size)
    return final_pe


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("sample")
    ap.add_argument("-o", "--output", default="clop_unpacked.bin")
    args = ap.parse_args()

    out = unpack(args.sample)
    open(args.output, "wb").write(out)
    print(f"[*] {len(out)} octets ecrits dans {args.output}")
    print(f"[*] MZ ? {out[:2] == b'MZ'}")

    import hashlib
    print(f"[*] SHA256 : {hashlib.sha256(out).hexdigest()}")


if __name__ == "__main__":
    sys.exit(main())
