88 lines
2.8 KiB
Python
88 lines
2.8 KiB
Python
#!/usr/bin/env python3
|
|
"""Compare plaintext vs ransomware-encrypted test files to extract markers and assess scheme.
|
|
|
|
Given a directory of original test files and a directory of their encrypted counterparts, it
|
|
reports added extensions, entropy change, and any common trailing footer/magic (often where a
|
|
wrapped key or IV is stored). Operates on inert test files only.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import math
|
|
import sys
|
|
from collections import Counter
|
|
from pathlib import Path
|
|
|
|
|
|
def entropy(data: bytes) -> float:
|
|
if not data:
|
|
return 0.0
|
|
counts = Counter(data)
|
|
n = len(data)
|
|
return -sum((c / n) * math.log2(c / n) for c in counts.values())
|
|
|
|
|
|
def _common_suffix(blobs: list[bytes], max_len: int = 512) -> bytes:
|
|
if not blobs:
|
|
return b""
|
|
shortest = min(len(b) for b in blobs)
|
|
limit = min(shortest, max_len)
|
|
suffix = bytearray()
|
|
for i in range(1, limit + 1):
|
|
col = {b[-i] for b in blobs}
|
|
if len(col) == 1:
|
|
suffix.insert(0, col.pop())
|
|
else:
|
|
break
|
|
return bytes(suffix)
|
|
|
|
|
|
def compare(plain_dir: str, enc_dir: str) -> dict:
|
|
plain = {p.stem: p for p in Path(plain_dir).iterdir() if p.is_file()}
|
|
enc = [p for p in Path(enc_dir).iterdir() if p.is_file()]
|
|
|
|
added_ext = sorted({"".join(p.suffixes) for p in enc})
|
|
enc_blobs = [p.read_bytes() for p in enc]
|
|
footer = _common_suffix(enc_blobs)
|
|
|
|
per_file = []
|
|
for p in enc:
|
|
base = p.name
|
|
# strip a possible appended extension to match the plaintext stem
|
|
stem = p.stem
|
|
orig = plain.get(stem)
|
|
row = {
|
|
"encrypted": base,
|
|
"encrypted_entropy": round(entropy(p.read_bytes()), 3),
|
|
}
|
|
if orig:
|
|
row["original"] = orig.name
|
|
row["original_entropy"] = round(entropy(orig.read_bytes()), 3)
|
|
row["size_delta"] = p.stat().st_size - orig.stat().st_size
|
|
per_file.append(row)
|
|
|
|
likely_full = all(r.get("encrypted_entropy", 0) > 7.5 for r in per_file)
|
|
return {
|
|
"added_extensions": added_ext,
|
|
"common_footer_len": len(footer),
|
|
"common_footer_hex": footer.hex(),
|
|
"encryption_coverage": "full/high-entropy" if likely_full else "partial/mixed",
|
|
"files": per_file,
|
|
}
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
parser = argparse.ArgumentParser(description="Compare plain vs encrypted test files")
|
|
sub = parser.add_subparsers(dest="cmd", required=True)
|
|
p = sub.add_parser("compare")
|
|
p.add_argument("plain_dir")
|
|
p.add_argument("enc_dir")
|
|
args = parser.parse_args(argv)
|
|
print(json.dumps(compare(args.plain_dir, args.enc_dir), indent=2))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|