#!/usr/bin/env python3 """Identify ransomware families from samples, ransom notes, and file extensions. Matches observed indicators (ransom note content, encrypted file extensions, mutex names, string patterns) against a built-in database of known ransomware family signatures. Can also check for available decryptors. """ from __future__ import annotations import argparse import hashlib import json import re import sys from pathlib import Path from typing import Any # Simplified signature database for common ransomware families _FAMILY_SIGNATURES: list[dict[str, Any]] = [ { "family": "LockBit 3.0", "extensions": [".lockbit"], "note_patterns": [r"lockbit", r"restore-my-files"], "mutex_patterns": [r"Global\\.*lockbit"], "decryptor_available": False, }, { "family": "BlackCat/ALPHV", "extensions": [".BlackCat", ".sykffle"], "note_patterns": [r"alphv", r"blackcat", r"RECOVER.*FILES"], "mutex_patterns": [], "decryptor_available": False, }, { "family": "Akira", "extensions": [".akira"], "note_patterns": [r"akira", r"onion"], "mutex_patterns": [], "decryptor_available": False, }, { "family": "Play", "extensions": [".play"], "note_patterns": [r"play", r"ReadMe\.txt"], "mutex_patterns": [], "decryptor_available": False, }, { "family": "WannaCry", "extensions": [".WNCRY", ".WNCRYT"], "note_patterns": [r"wana.*crypt", r"wannacry", r"@WanaDecryptor@"], "mutex_patterns": [r"MsWinZonesCacheCounterMutexA"], "decryptor_available": True, }, { "family": "REvil/Sodinokibi", "extensions": [".sodinokibi"], "note_patterns": [r"revil", r"sodinokibi", r"decode.*files"], "mutex_patterns": [r"Global\\.*REvil"], "decryptor_available": True, }, { "family": "Conti", "extensions": [".CONTI"], "note_patterns": [r"conti", r"readme.*txt"], "mutex_patterns": [], "decryptor_available": False, }, { "family": "Ryuk", "extensions": [".RYK"], "note_patterns": [r"ryuk", r"RyukReadMe"], "mutex_patterns": [r"Global\\.*RYUK"], "decryptor_available": False, }, ] def _sha256(path: Path) -> str: """Compute SHA-256 hash of a file.""" h = hashlib.sha256() with path.open("rb") as fh: for chunk in iter(lambda: fh.read(8192), b""): h.update(chunk) return h.hexdigest() def _extract_strings(path: Path, min_length: int = 4) -> list[str]: """Extract printable ASCII strings from a binary file.""" pattern = re.compile(rb"[\x20-\x7e]{%d,}" % min_length) data = path.read_bytes() return [m.decode("ascii", errors="replace") for m in pattern.findall(data)] def _match_extension(extension: str) -> list[dict[str, Any]]: """Match an encrypted file extension against known families.""" matches: list[dict[str, Any]] = [] for sig in _FAMILY_SIGNATURES: if extension.lower() in [e.lower() for e in sig["extensions"]]: matches.append({ "family": sig["family"], "match_type": "extension", "confidence": "high", "decryptor_available": sig["decryptor_available"], }) return matches def _match_note(note_text: str) -> list[dict[str, Any]]: """Match ransom note text against known family patterns.""" matches: list[dict[str, Any]] = [] for sig in _FAMILY_SIGNATURES: for pattern in sig["note_patterns"]: if re.search(pattern, note_text, re.IGNORECASE): matches.append({ "family": sig["family"], "match_type": "ransom_note", "matched_pattern": pattern, "confidence": "medium", "decryptor_available": sig["decryptor_available"], }) break # one match per family is enough return matches def _match_strings(strings: list[str]) -> list[dict[str, Any]]: """Match extracted strings against mutex and other patterns.""" matches: list[dict[str, Any]] = [] blob = "\n".join(strings) for sig in _FAMILY_SIGNATURES: for pattern in sig.get("mutex_patterns", []): if re.search(pattern, blob, re.IGNORECASE): matches.append({ "family": sig["family"], "match_type": "mutex_string", "matched_pattern": pattern, "confidence": "medium", "decryptor_available": sig["decryptor_available"], }) break return matches def _parse_ransom_note(note_path: Path) -> dict[str, Any]: """Extract intelligence from a ransom note file.""" text = note_path.read_text(encoding="utf-8", errors="replace") btc_addresses = re.findall(r"\b(?:bc1|[13])[a-zA-HJ-NP-Z0-9]{25,62}\b", text) onion_urls = re.findall(r"https?://[a-z2-7]{16,56}\.onion[/\w.-]*", text, re.IGNORECASE) email_addresses = re.findall(r"[\w.+-]+@[\w-]+\.[\w.-]+", text) amounts = re.findall(r"\$[\d,]+(?:\.\d{2})?|\d+\s*(?:BTC|XMR|ETH|Bitcoin|Monero)", text, re.IGNORECASE) return { "note_file": note_path.name, "note_length": len(text), "bitcoin_addresses": btc_addresses, "onion_urls": onion_urls, "email_addresses": email_addresses, "payment_amounts": amounts, "text_preview": text[:500], } def identify( sample_path: Path | None = None, note_path: Path | None = None, extension: str | None = None, check_decryptors: bool = False, parse_note: bool = False, note_only: bool = False, ) -> dict[str, Any]: """Run identification against all available indicators.""" all_matches: list[dict[str, Any]] = [] result: dict[str, Any] = {"matches": [], "note_analysis": None} if extension: ext = extension if extension.startswith(".") else f".{extension}" all_matches.extend(_match_extension(ext)) if note_path and note_path.exists(): note_text = note_path.read_text(encoding="utf-8", errors="replace") all_matches.extend(_match_note(note_text)) if parse_note: result["note_analysis"] = _parse_ransom_note(note_path) if sample_path and sample_path.exists() and not note_only: result["sample_hash"] = _sha256(sample_path) strings = _extract_strings(sample_path) all_matches.extend(_match_strings(strings)) # Deduplicate by family, keeping the highest-confidence match seen: dict[str, dict[str, Any]] = {} for m in all_matches: fam = m["family"] if fam not in seen or m["confidence"] == "high": seen[fam] = m result["matches"] = list(seen.values()) if check_decryptors: result["decryptors"] = [ {"family": m["family"], "available": m["decryptor_available"]} for m in result["matches"] ] return result def analyze(input_path: Path, output_format: str = "json") -> dict[str, Any]: """Analyze a ransomware sample file and return identification results.""" return identify(sample_path=input_path) def main() -> None: parser = argparse.ArgumentParser( description="Identify ransomware families from samples and artifacts." ) parser.add_argument("--input", type=Path, help="Path to input file (sample or note)") parser.add_argument("--sample", type=Path, help="Path to ransomware sample binary") parser.add_argument("--ransom-note", type=Path, help="Path to ransom note text file") parser.add_argument("--encrypted-extension", type=str, help="File extension appended by the ransomware (e.g. .locked)") parser.add_argument("--note-only", action="store_true", help="Only analyse the ransom note (skip sample analysis)") parser.add_argument("--parse-note", action="store_true", help="Extract IOCs from the ransom note") parser.add_argument("--check-decryptors", action="store_true", help="Check if a free decryptor is known for matched families") parser.add_argument("--output", type=Path, help="Path to output file (stdout if omitted)") parser.add_argument("--format", default="json", choices=["json", "text", "csv"], help="Output format (default: json)") args = parser.parse_args() sample = args.sample or args.input result = identify( sample_path=sample, note_path=args.ransom_note, extension=args.encrypted_extension, check_decryptors=args.check_decryptors, parse_note=args.parse_note, note_only=args.note_only, ) rendered = json.dumps(result, indent=2) if args.output: args.output.parent.mkdir(parents=True, exist_ok=True) args.output.write_text(rendered, encoding="utf-8") print(f"[+] Identification results written to {args.output}", file=sys.stderr) else: print(rendered) if __name__ == "__main__": main()