Как проанализировать .exe-файл через VirusTotal API и собрать метаданные

Вы получили подозрительный .exe-файл — возможно, из письма, скачали с малоизвестного сайта или достали из архива старого проекта. Руки не доходят до загрузки на сайт, а проверить нужно. Или у вас десятки файлов и нужна автоматизация. В обоих случаях помогает VirusTotal API. Разберёмся, как с ним работать без лишней теории — только то, что нужно для результата.

Что на самом деле даёт VirusTotal API

VirusTotal — это сервис, который прогоняет файл или URL через несколько десятков антивирусных движков и собирает результаты. Через веб-интерфейс вы просто кидаете файл и смотрите отчёт. Через API вы делаете то же самое программно: отправляете файл или хеш, получаете JSON с результатами и можете дальше их обрабатывать как угодно.

Что именно вы получаете в ответе API:

  • Результаты проверки каждым антивирусным движком (обнаружен / не обнаружен, название угрозы).
  • Метаданные файла: размер, тип, хеши (MD5, SHA-1, SHA-256).
  • Информация о структуре PE-файла (для .exe): секции, импортируемые библиотеки, точки входа, таймстампы.
  • Результаты статического анализа: строки, сигнатуры, поведенческие индикаторы.
  • Данные о сертификатах, если файл подписан.
  • История: когда файл впервые появился в базе, сколько раз загружался, какие мнения у сообщества.

API бесплатный, но с ограничениями. Бесплатный ключ позволяет делать до 500 запросов в минуту и до 30 000 в сутки. Для личных задач и небольших проектов этого хватает с головой. Если нужно больше — есть платные планы с повышенными лимитами и дополнительными возможностями.

Получаем API-ключ и готовим окружение

Сначала регистрируемся на virustotal.com. Можно через Google, GitHub или почту. После входа заходим в профиль — там будет ваш API-ключ. Скопируйте его и храните безопасно: это ваш идентификатор, и по нему считаются все ваши запросы.

Для работы с API подойдёт любой язык, который умеет делать HTTP-запросы. Я покажу примеры на Python — он самый удобный для таких задач. Если вы пишете на другом языке, логика будет идентичной, меняется только синтаксис.

Установите библиотеку requests, если её ещё нет:

pip install requests

Проверка по хешу — самый быстрый способ

Если вы уже знаете хеш файла (SHA-256, SHA-1 или MD5), не нужно загружать сам файл на сервер. Достаточно отправить хеш и посмотреть, есть ли этот файл в базе VirusTotal. Это экономит трафик и время.

import requests
import json

API_KEY = "ваш_ключ_здесь"
FILE_HASH = "a3b9c2d1e4f5..."  # SHA-256 хеш файла

url = f"https://www.virustotal.com/api/v3/files/{FILE_HASH}"

headers = {
    "x-apikey": API_KEY
}

response = requests.get(url, headers=headers)

if response.status_code == 200:
    data = response.json()
    print(json.dumps(data, indent=2))
elif response.status_code == 404:
    print("Файл не найден в базе VirusTotal")
else:
    print(f"Ошибка: {response.status_code}")
    print(response.text)

Если хеш найден, вы получаете полный отчёт. Если нет — придётся загрузить сам файл. Это логично: если файл никто раньше не проверял, его нужно отправить на анализ.

Загрузка файла на анализ

Когда файла нет в базе или вы хотите получить свежий результат, загружаем сам .exe. Для файлов размером до 32 МБ используем простой POST-запрос:

import requests
import os

API_KEY = "ваш_ключ_здесь"
FILE_PATH = "C:/path/to/suspicious_file.exe"

url = "https://www.virustotal.com/api/v3/files"

headers = {
    "x-apikey": API_KEY
}

with open(FILE_PATH, "rb") as f:
    files = {"file": (os.path.basename(FILE_PATH), f)}
    response = requests.post(url, headers=headers, files=files)

if response.status_code == 200:
    data = response.json()
    analysis_id = data["data"]["id"]
    print(f"ID анализа: {analysis_id}")
else:
    print(f"Ошибка загрузки: {response.status_code}")
    print(response.text)

После загрузки вы получаете не результат, а ID анализа. VirusTotal нужно время, чтобы прогнать файл через все движки. Обычно это занимает от 15 секунд до пары минут. Результат запрашивается отдельно по этому ID.

Получаем результат анализа

Загрузили файл — подождите секунд 30–60, потом запрашивайте результат. Делаем GET-запрос с ID анализа:

analysis_id = "u-a3b9c2d1e4f5-1234567890"

url = f"https://www.virustotal.com/api/v3/analyses/{analysis_id}"

headers = {
    "x-apikey": API_KEY
}

response = requests.get(url, headers=headers)
data = response.json()

status = data["data"]["attributes"]["status"]
print(f"Статус: {status}")

if status == "completed":
    stats = data["data"]["attributes"]["stats"]
    print(f"Безопасных: {stats.get('harmless', 0)}")
    print(f"Подозрительных: {stats.get('suspicious', 0)}")
    print(f"Вредоносных: {stats.get('malicious', 0)}")
    print(f"Неопределённых: {stats.get('undetected', 0)}")

Статус может быть queued (в очереди), in-progress (анализируется) или completed (готово). Если статус не готов, подождите ещё немного и повторите запрос. В реальном коде это оборачивают в цикл с задержкой.

Собираем метаданные из отчёта

Когда анализ завершён, из того же ответа можно вытащить метаданные файла. Вот что реально полезно:

attributes = data["data"]["attributes"]

# Основные хеши
md5 = attributes.get("md5")
sha1 = attributes.get("sha1")
sha256 = attributes.get("sha256")
print(f"MD5: {md5}")
print(f"SHA-1: {sha1}")
print(f"SHA-256: {sha256}")

# Размер и тип
size = attributes.get("size")
file_type = attributes.get("type_description")
print(f"Размер: {size} байт")
print(f"Тип: {file_type}")

# Результаты по каждому движку
results = attributes.get("results", {})
for engine, result in results.items():
    category = result.get("category", "unknown")
    if category in ("malicious", "suspicious"):
        print(f"{engine}: {result.get('result')} [{category}]")

Это базовый набор. Но есть и более глубокие метаданные, которые доступны через отдельные эндпоинты.

Подробные метаданные PE-файла

Для .exe-файлов VirusTotal извлекает информацию из PE-заголовка (Portable Executable). Это структура, по которой можно понять, что за файл перед вами, даже не запуская его.

Эти данные доступны через отдельный запрос — не в основном отчёте по анализу, а через эндпоинт файла по его SHA-256:

sha256 = "a3b9c2d1e4f5..."

url = f"https://www.virustotal.com/api/v3/files/{sha256}"
response = requests.get(url, headers=headers)
data = response.json()

attributes = data["data"]["attributes"]

# PE-метаданные
pe_info = attributes.get("pe_info", {})
if pe_info:
    print("=== PE-секции ===")
    for section in pe_info.get("sections", []):
        print(f"  {section['name']}: размер {section['size']}, энтропия {section.get('entropy', 'N/A')}")

    print("\n=== Импортируемые библиотеки ===")
    for entry in pe_info.get("import_list", []):
        print(f"  {entry['library_name']}:")
        for func in entry.get("imported_functions", [])[:5]:
            print(f"    - {func}")
        if len(entry.get("imported_functions", [])) > 5:
            print(f"    ... и ещё {len(entry['imported_functions']) - 5}")

    # Таймстамп компиляции
    compile_time = pe_info.get("compile_time")
    if compile_time:
        from datetime import datetime
        print(f"\nДата компиляции: {datetime.fromtimestamp(compile_time)}")

    # Сертификаты
    signers = attributes.get("signature_info", {})
    if signers:
        print("\n=== Цифровая подпись ===")
        for signer in signers.get("signers", []):
            print(f"  Подписант: {signer.get('name', 'неизвестно')}")
            print(f"  Сертификат действителен: {signer.get('valid from', '?')} — {signer.get('valid to', '?')}")

Энтропия секций — интересный показатель. Если секция с кодом имеет энтропию выше 7.0, это может указывать на упаковку или шифрование — малвари часто так прячутся. Энтропия 7.5–8.0 — это уже серьёзный повод присмотреться внимательнее.

Собираем всё в единый скрипт

Теперь объединим всё в один рабочий скрипт. Он принимает путь к файлу, вычисляет хеш, проверяет по базе, если нет — загружает, ждёт результат и собирает метаданные:

import requests
import hashlib
import time
import json
import os
import sys
from datetime import datetime

API_KEY = "ваш_ключ_здесь"
BASE_URL = "https://www.virustotal.com/api/v3"

HEADERS = {"x-apikey": API_KEY}

def calculate_sha256(file_path):
    sha256 = hashlib.sha256()
    with open(file_path, "rb") as f:
        for chunk in iter(lambda: f.read(8192), b""):
            sha256.update(chunk)
    return sha256.hexdigest()

def get_file_report(file_hash):
    url = f"{BASE_URL}/files/{file_hash}"
    response = requests.get(url, headers=HEADERS)
    if response.status_code == 200:
        return response.json()
    return None

def upload_file(file_path):
    url = f"{BASE_URL}/files"
    with open(file_path, "rb") as f:
        files = {"file": (os.path.basename(file_path), f)}
        response = requests.post(url, headers=HEADERS, files=files)
    if response.status_code == 200:
        return response.json()["data"]["id"]
    return None

def wait_for_analysis(analysis_id, max_wait=120):
    url = f"{BASE_URL}/analyses/{analysis_id}"
    start = time.time()
    while time.time() - start < max_wait:
        response = requests.get(url, headers=HEADERS)
        if response.status_code == 200:
            status = response.json()["data"]["attributes"]["status"]
            if status == "completed":
                return response.json()
            print(f"  Статус: {status}, ждём...")
        time.sleep(15)
    return None

def extract_metadata(report_data):
    attrs = report_data["data"]["attributes"]
    metadata = {
        "hashes": {
            "md5": attrs.get("md5"),
            "sha1": attrs.get("sha1"),
            "sha256": attrs.get("sha256"),
        },
        "size": attrs.get("size"),
        "type": attrs.get("type_description"),
        "detection": {},
        "pe_info": {},
        "signatures": [],
        "first_seen": attrs.get("first_submission_date"),
        "last_seen": attrs.get("last_analysis_date"),
        "reputation": attrs.get("reputation"),
    }

    # Результаты детекции
    results = attrs.get("results", {})
    malicious = []
    suspicious = []
    for engine, result in results.items():
        cat = result.get("category", "undetected")
        if cat == "malicious":
            malicious.append({"engine": engine, "name": result.get("result")})
        elif cat == "suspicious":
            suspicious.append({"engine": engine, "name": result.get("result")})
    metadata["detection"] = {
        "malicious_count": len(malicious),
        "suspicious_count": len(suspicious),
        "malicious_engines": malicious,
        "suspicious_engines": suspicious,
    }

    # PE-информация
    pe = attrs.get("pe_info", {})
    if pe:
        metadata["pe_info"] = {
            "sections": [
                {"name": s["name"], "size": s["size"], "entropy": s.get("entropy")}
                for s in pe.get("sections", [])
            ],
            "imports": [
                {"library": imp["library_name"], "functions": imp.get("imported_functions", [])}
                for imp in pe.get("import_list", [])
            ],
            "compile_time": pe.get("compile_time"),
        }

    # Подписи
    sig_info = attrs.get("signature_info", {})
    if sig_info:
        for signer in sig_info.get("signers", []):
            metadata["signatures"].append({
                "name": signer.get("name"),
                "valid_from": signer.get("valid from"),
                "valid_to": signer.get("valid to"),
            })

    return metadata

def analyze_exe(file_path):
    print(f"Анализ файла: {file_path}")
    file_hash = calculate_sha256(file_path)
    print(f"SHA-256: {file_hash}")

    # Пробуем найти по хешу
    report = get_file_report(file_hash)
    if report:
        print("Файл найден в базе VirusTotal, используем существующий отчёт")
    else:
        print("Файл не найден, загружаем на анализ...")
        analysis_id = upload_file(file_path)
        if not analysis_id:
            print("Ошибка загрузки файла")
            return None
        print(f"Файл загружен, ID анализа: {analysis_id}")
        report = wait_for_analysis(analysis_id)
        if not report:
            print("Таймаут ожидания анализа")
            return None

    metadata = extract_metadata(report)
    return metadata

if __name__ == "__main__":
    if len(sys.argv) < 2:
        print("Использование: python vt_analyzer.py <путь_к_файлу>")
        sys.exit(1)

    result = analyze_exe(sys.argv[1])
    if result:
        print("\n" + "=" * 60)
        print("РЕЗУЛЬТАТ")
        print("=" * 60)
        print(json.dumps(result, indent=2, ensure_ascii=False, default=str))

Этот скрипт уже рабочий. Запускаете его с путём к файлу, и он выдаёт структурированный JSON со всеми метаданными. Можно сохранять в базу, передать в SIEM или просто анализировать глазами.

Какие данные действительно важны, а какие — шум

Когда вы получаете полный отчёт, в нём сотни полей. Не все они одинаково полезны на практике. Вот на что смотреть в первую очередь:

Параметр Что показывает Когда важен
Количество детекций (malicious/total) Сколько антивирусов считают файл вредоносным Всегда — это первичный индикатор
PE-секции с высокой энтропией Упаковка, обфускация, шифрование кода При анализе подозрительных файлов
Импортируемые функции Какие API вызывает программа (сеть, реестр, файловая система) Для понимания поведения без запуска
Цифровая подпись Подписан ли файл и кем Для проверки легитимности
Дата компиляции (compile_time) Когда файл был собран Для хронологии и выявления аномалий
Строки (strings) Текстовые данные внутри файла: URL, пути, команды Для поиска индикаторов компрометации
Reputation (репутация) Социальная оценка от пользователей VT Дополнительный ориентир
Last submission date Когда файл последний раз проверялся Чтобы понять актуальность данных

Сценарии использования

Сценарий 1: Быстрая проверка одного файла. Вы скачали что-то и хотите понять, безопасно ли это. Вычисляете SHA-256, делаете запрос по хешу, смотрите количество детекций. Если 0/70 — скорее всего чисто. Если 15/70 — стоит разобраться, какие именно движки ругаются и на что именно.

Сценарий 2: Массовая проверка. У вас папка с 500 .exe-файлами из старого архива. Пишете скрипт, который проходит по всем файлам, вычисляет хеши, проверяет через API и сохраняет результаты в CSV или базу данных. Здесь важно соблюдать лимиты — не больше 4 запросов в минуту для бесплатного плана.

Сценарий 3: Интеграция в pipeline безопасности. Файлы поступают из почты или скачиваются из интернета, автоматически проверяются через API, результаты логируются. Если обнаружен вредоносный файл — алерт уходит аналитику. Здесь уже нужен платный API для высокой пропускной способности.

Сценарий 4: Расследование инцидента. Вы нашли подозрительный файл на машине и хотите понять, что он делает. Загружаете на VirusTotal, анализируете PE-структуру, строки, импорты. Это заменяет первичный статический анализ без запуска в песочнице.

Частые ошибки при работе с API

Не учитывать лимиты запросов. Бесплатный API — это 4 запроса в минуту. Если отправите больше, получите HTTP 429 (Too Many Requests). Решение: добавляйте time.sleep(15) между запросами или используйте очередь.

Ждать результат сразу после загрузки. После отправки файла вы получаете ID анализа, а не результат. VirusTotal нужно время. Если запросить результат через 2 секунды, получите статус queued. Нормальная практика — ждать 30–60 секунд перед первым запросом результата.

Использовать MD5 как единственный идентификатор. MD5 коллизируются, для серьёзного анализа используйте SHA-256. Он надёжнее и является основным идентификатором в VirusTotal.

Не сохранять промежуточные результаты. Если вы массово проверяете файлы и упадёте на 50-м из 500 — начинать заново обидно. Сохраняйте результаты каждого файла в локальную базу или файл по мере обработки.

Слепо доверять нулевым детекциям. Если файл не обнаружен ни одним движком, это не гарантия безопасности. Новые малвари, кастомные бэкдоры и хорошо упакованные файлы часто проходят мимо антивирусов. Смотрите на другие признаки: подозрительные импорты, высокую энтропию, отсутствие цифровой подписи.

Забывать про приватность файлов. Всё, что вы загружаете на VirusTotal, становится доступно подписчикам платного плана. Не загружайте конфиденциальные файлы — внутренние инструменты, клиентские данные, проприетарное ПО. Для таких случаев есть настройка «Do not share» в корпоративных аккаунтах.

Как лучше организовать хранение и обработку результатов

Если вы делаете больше одной проверки в неделю, заведите простую базу результатов. Минимум — JSON-файл или SQLite:

import sqlite3
import json
from datetime import datetime

def init_db(db_path="vt_results.db"):
    conn = sqlite3.connect(db_path)
    conn.execute("""
        CREATE TABLE IF NOT EXISTS scans (
            sha256 TEXT PRIMARY KEY,
            md5 TEXT,
            file_name TEXT,
            file_size INTEGER,
            malicious_count INTEGER,
            suspicious_count INTEGER,
            first_seen TEXT,
            last_scanned TEXT,
            raw_report TEXT,
            scanned_at TEXT
        )
    """)
    conn.commit()
    return conn

def save_scan(conn, metadata):
    conn.execute("""
        INSERT OR REPLACE INTO scans
        (sha256, md5, file_name, file_size, malicious_count,
         suspicious_count, first_seen, last_scanned, raw_report, scanned_at)
        VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    """, (
        metadata["hashes"]["sha256"],
        metadata["hashes"]["md5"],
        metadata.get("file_name", ""),
        metadata["size"],
        metadata["detection"]["malicious_count"],
        metadata["detection"]["suspicious_count"],
        metadata.get("first_seen", ""),
        metadata.get("last_seen", ""),
        json.dumps(metadata, default=str),
        datetime.utcnow().isoformat()
    ))
    conn.commit()

С такой базой вы можете быстро проверять: был ли уже проанализирован этот файл, когда и с каким результатом. Это экономит запросы к API и время.

Обработка ошибок и отказоустойчивость

API иногда недоступен, иногда возвращает ошибки. Хороший скрипт это обрабатывает:

def safe_request(url, headers, method="get", kwargs):
    max_retries = 3
    for attempt in range(max_retries):
        try:
            if method == "get":
                response = requests.get(url, headers=headers, timeout=30)
            else:
                response = requests.post(url, headers=headers, timeout=60, kwargs)

            if response.status_code == 429:
                print("Лимит запросов, ждём 60 секунд...")
                time.sleep(60)
                continue

            if response.status_code == 200:
                return response.json()

            if response.status_code == 404:
                return None

            print(f"Ошибка {response.status_code}: {response.text[:200]}")
            time.sleep(5)

        except requests.exceptions.Timeout:
            print(f"Таймаут запроса, попытка {attempt + 1}/{max_retries}")
            time.sleep(10)
        except requests.exceptions.ConnectionError:
            print("Проблема с соединением, ждём 30 секунд...")
            time.sleep(30)

    return None

Оборачивайте все обращения к API в подобную функцию. Сеть бывает нестабильной, а VirusTotal иногда перегружен. Лучше подождать и повторить, чем потерять результат.

Что в итоге и с чего начать

Если вам нужно проверить .exe-файл через VirusTotal API:

  1. Получите API-ключ на virustotal.com — это займёт 2 минуты.
  2. Начните с проверки по SHA-256 — это бесплатно по времени и трафику.
  3. Если файла нет в базе — загрузите его и подождите 30–60 секунд перед запросом результата.
  4. Извлекайте из отчёта то, что важно: количество детекций, PE-метаданные, импорты, сертификаты.
  5. Сохраняйте результаты в локальную базу, чтобы не проверять один и тот же файл дважды.
  6. Не забывайте про лимиты: 4 запроса в минуту для бесплатного плана.

Приведённый выше скрипт покрывает 90% реальных задач. Его можно расширить: добавить логирование в файл, интеграцию с Telegram-ботом для алертов, параллельную обработку нескольких файлов с соблюдением лимитов. Но основа — именно такая: хеш → поиск → загрузка → ожидание → извлечение метаданных. Работает надёжно и решает задачу.

Оцените статью
PEFile — Безопасность и технологии простым языком