| import argparse |
| import json |
| import os |
| import time |
| import zipfile |
| from io import BytesIO |
| from pathlib import Path |
|
|
| import urllib.request |
| import urllib.parse |
|
|
| BAZAAR_API = "https://mb-api.abuse.ch/api/v1/" |
| ZIP_PASSWORD = b"infected" |
|
|
| DATASET_DIR = Path(__file__).resolve().parent.parent / "datasets" |
| MALICIOUS_DIR = DATASET_DIR / "raw" / "malicious" |
|
|
| PE_TAGS = [ |
| "AgentTesla", "AsyncRAT", "RedLine", "Emotet", "NjRAT", |
| "FormBook", "Remcos", "LokiBot", "GuLoader", "Raccoon", |
| "Cobalt Strike", "Metasploit", "Sliver", "BruteRatel", |
| "Ransomware", "Loader", "Dropper", |
| ] |
|
|
| _API_KEY: str | None = None |
|
|
|
|
| def _post(payload: dict) -> dict: |
| data = urllib.parse.urlencode(payload).encode() |
| req = urllib.request.Request(BAZAAR_API, data=data, method="POST") |
| req.add_header("User-Agent", "VibeCheck-Fetcher/1.0") |
| if _API_KEY: |
| req.add_header("API-KEY", _API_KEY) |
| with urllib.request.urlopen(req, timeout=30) as resp: |
| return json.loads(resp.read()) |
|
|
|
|
| def _download_sample(sha256: str, dest: Path) -> bool: |
| payload = {"query": "get_file", "sha256_hash": sha256} |
| data = urllib.parse.urlencode(payload).encode() |
| req = urllib.request.Request(BAZAAR_API, data=data, method="POST") |
| req.add_header("User-Agent", "VibeCheck-Fetcher/1.0") |
| if _API_KEY: |
| req.add_header("API-KEY", _API_KEY) |
| try: |
| with urllib.request.urlopen(req, timeout=60) as resp: |
| raw = resp.read() |
| if raw[:2] != b"PK": |
| return False |
| with zipfile.ZipFile(BytesIO(raw)) as zf: |
| zf.extractall(path=dest.parent, pwd=ZIP_PASSWORD) |
| extracted = [f for f in dest.parent.iterdir() if f.suffix.lower() in {".exe", ".dll", ".sys", ".bin"} and f.name != dest.name] |
| if extracted: |
| extracted[0].rename(dest) |
| return dest.exists() |
| except Exception as e: |
| print(f" Download failed for {sha256[:16]}...: {e}") |
| return False |
|
|
|
|
| def get_recent_samples(count: int) -> list[dict]: |
| selector = min(count, 100) |
| result = _post({"query": "get_recent", "selector": str(selector)}) |
| if result.get("query_status") != "ok": |
| return [] |
| return result.get("data", []) |
|
|
|
|
| def get_tagged_samples(tag: str, limit: int = 100) -> list[dict]: |
| result = _post({"query": "get_taginfo", "tag": tag, "limit": str(min(limit, 1000))}) |
| if result.get("query_status") not in ("ok", "tag_not_found"): |
| return [] |
| return result.get("data") or [] |
|
|
|
|
| def fetch(count: int, tags: list[str], pe_only: bool, resume: bool, rate_limit: float): |
| MALICIOUS_DIR.mkdir(parents=True, exist_ok=True) |
|
|
| existing = {f.stem for f in MALICIOUS_DIR.iterdir() if f.is_file()} if resume else set() |
| print(f" Resume mode: {len(existing)} samples already present") |
|
|
| candidates: list[dict] = [] |
|
|
| if tags: |
| for tag in tags: |
| print(f" Querying tag: {tag}") |
| samples = get_tagged_samples(tag, limit=count) |
| candidates.extend(samples) |
| print(f" -> {len(samples)} results") |
| time.sleep(0.5) |
| else: |
| print(f" Querying recent samples (up to {count})...") |
| batches = (count + 99) // 100 |
| for _ in range(batches): |
| samples = get_recent_samples(100) |
| candidates.extend(samples) |
| time.sleep(1.0) |
|
|
| seen = set() |
| deduped = [] |
| for s in candidates: |
| h = s.get("sha256_hash", "") |
| if h and h not in seen: |
| seen.add(h) |
| deduped.append(s) |
|
|
| if pe_only: |
| deduped = [s for s in deduped if s.get("file_type", "").lower() in {"exe", "dll", "sys"}] |
|
|
| print(f"\n {len(deduped)} unique PE candidates after filtering") |
|
|
| downloaded = 0 |
| skipped = 0 |
| failed = 0 |
|
|
| for sample in deduped: |
| if downloaded >= count: |
| break |
|
|
| sha256 = sample.get("sha256_hash", "") |
| if not sha256: |
| continue |
|
|
| if sha256 in existing: |
| skipped += 1 |
| continue |
|
|
| file_type = sample.get("file_type", "bin").lower() |
| ext = {"exe": ".exe", "dll": ".dll", "sys": ".sys"}.get(file_type, ".bin") |
| dest = MALICIOUS_DIR / f"{sha256}{ext}" |
|
|
| tags_str = ", ".join(sample.get("tags") or []) |
| print(f" [{downloaded + 1}/{count}] {sha256[:16]}... ({file_type}) [{tags_str}]", end=" ") |
|
|
| ok = _download_sample(sha256, dest) |
| if ok: |
| print("OK") |
| downloaded += 1 |
| existing.add(sha256) |
| else: |
| print("FAILED") |
| failed += 1 |
|
|
| time.sleep(rate_limit) |
|
|
| print(f"\n Done: {downloaded} downloaded, {skipped} skipped (resume), {failed} failed") |
| print(f" Total malicious samples in {MALICIOUS_DIR}: {len(list(MALICIOUS_DIR.iterdir()))}") |
|
|
|
|
| def main(): |
| parser = argparse.ArgumentParser(description="Fetch malware samples from MalwareBazaar") |
| parser.add_argument("--count", type=int, default=1000, |
| help="Number of samples to download (default: 1000)") |
| parser.add_argument("--tags", nargs="+", default=None, |
| help="Filter by MalwareBazaar tags (e.g. --tags Emotet AgentTesla)") |
| parser.add_argument("--pe-only", action="store_true", default=True, |
| help="Only download PE files (exe/dll/sys) (default: True)") |
| parser.add_argument("--no-pe-only", dest="pe_only", action="store_false") |
| parser.add_argument("--resume", action="store_true", default=True, |
| help="Skip already-downloaded samples (default: True)") |
| parser.add_argument("--no-resume", dest="resume", action="store_false") |
| parser.add_argument("--rate-limit", type=float, default=1.0, |
| help="Seconds between downloads (default: 1.0, be polite)") |
| args = parser.parse_args() |
|
|
| tags = args.tags or [] |
| print(f"MalwareBazaar Fetcher") |
| print(f" Target: {args.count} samples") |
| print(f" Tags: {tags or 'recent (no filter)'}") |
| print(f" PE only: {args.pe_only}") |
| print(f" Output: {MALICIOUS_DIR}\n") |
|
|
| fetch( |
| count=args.count, |
| tags=tags, |
| pe_only=args.pe_only, |
| resume=args.resume, |
| rate_limit=args.rate_limit, |
| ) |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|