Упаковщики: что это и как распознать

Упаковщик сжимает или шифрует исполняемый файл и распаковывает его в памяти при запуске — учимся это распознавать.

Упаковщик (packer) — инструмент, который сжимает или шифрует код и данные исполняемого файла, добавляя маленькую программу-распаковщик (stub), восстанавливающую оригинал в памяти при старте.

Когда аналитик загружает файл и видит крошечный список импортов, нечитаемые секции и почти случайные байты — перед ним, скорее всего, упакованный образ. Статический анализ такого файла бесполезен: настоящий код проявится только во время выполнения, когда stub распакует его в память. Распознавание упаковки — обязательный первый шаг разбора, потому что без него вы анализируете не программу, а её сжатый контейнер.

Зачем это знать защитнику и аналитику

Упаковщики применяются совершенно легально: уменьшение размера дистрибутива, базовая защита от поверхностного реверса, объединение ресурсов. UPX — самый известный пример, его открыто используют в легитимном ПО. Но та же техника крайне популярна у вредоносного ПО: упаковка ломает сигнатурное детектирование (на диске байты другие) и прячет настоящую логику. Поэтому для аналитика факт упаковки сам по себе — не приговор, а сигнал «здесь нужен динамический анализ». Умение распознать упаковщик и его тип экономит часы и направляет разбор по верному пути.

Как устроен упакованный файл

Концептуально упаковщик берёт оригинальный исполняемый файл, сжимает (или шифрует) его секции кода и данных и формирует новый файл. В нём остаётся минимальный «обёрточный» код — stub. При запуске управление получает stub: он распаковывает оригинальные секции в выделенную память, восстанавливает таблицу импортов и передаёт управление на настоящую точку входа исходной программы — её называют OEP (Original Entry Point). До этого момента в памяти лежит лишь распаковщик.

[ упакованный файл на диске ]
  +-----------------------------+
  |  stub (распаковщик)         |  <- сюда указывает Entry Point
  |  сжатые/зашифрованные       |
  |  секции оригинала           |
  +-----------------------------+
            |  запуск
            v
[ память после распаковки ]
  оригинальный код  -> переход на OEP -> работает настоящая программа

Признак 1: высокая энтропия

Энтропия — мера «случайности» байт. У обычного кода и текста она умеренная (повторяющиеся опкоды, ASCII-строки), у сжатых и зашифрованных данных — близка к максимуму (~8 бит на байт), потому что хорошие сжатие и шифрование делают вывод статистически неотличимым от случайного. Резко высокая энтропия секций — классический индикатор упаковки или шифрования. Покажем идею на маленьком примере: сравним энтропию повторяющихся данных и «случайных».

import math
from collections import Counter

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())

plain = bytes([65] * 256)                 # одинаковые байты (как простой код)
packed = bytes(range(256))                # все значения по разу (как сжатые данные)
print("Обычные данные:", round(entropy(plain), 2))
print("Похоже на упаковку:", round(entropy(packed), 2))

Вывод:

Обычные данные: 0.0
Похоже на упаковку: 8.0

В реальности аналитик смотрит энтропию по секциям (инструменты вроде detect-it-easy показывают её графиком). Значение близко к 8 у секции кода — сильный намёк на упаковку. Это эвристика, а не доказательство: легитимно сжатые ресурсы тоже дают высокую энтропию.

Признак 2: мало импортов

Обычная программа импортирует десятки функций из системных библиотек (работа с файлами, окнами, сетью). У упакованного файла таблица импортов часто подозрительно мала — буквально несколько функций, нужных самому stub: выделить память, изменить права страниц, загрузить библиотеку. Полная таблица импортов восстановится только в рантайме после распаковки. Короткий список вида «выделение памяти + загрузка модулей и больше почти ничего» — характерный портрет упаковщика.

Другие признаки

  • Нестандартные имена секций. UPX называет секции UPX0, UPX1; другие упаковщики оставляют свои метки. Имена вроде .text/.data заменены на необычные — повод насторожиться.
  • Секция с нулевым «сырым» размером, но большим виртуальным. Место под распакованный код выделяется в памяти, на диске оно пустое — типичная картина упакованной секции.
  • Сигнатуры упаковщика. Многие упаковщики оставляют узнаваемые строки или байтовые шаблоны в stub; статические сканеры (detect-it-easy, сигнатурные базы) определяют упаковщик и версию по ним.

Как это работает под капотом

Упаковка опирается на способность ОС выполнять код, сгенерированный во время работы программы: stub выделяет память, помещает туда распакованные инструкции, делает страницы исполняемыми и прыгает на них. Именно это поведение — «программа на лету создаёт и запускает новый код» — и есть то, что аналитик ловит динамически. На диске же остаётся высокоэнтропийный контейнер с минимумом импортов. Эти два слоя (статический контейнер и динамическая распаковка) объясняют, почему упакованное ПО нужно анализировать в движении, а не по статике.

Как защититься

С точки зрения обороны и грамотного анализа:

  • Для защитников: упаковка собственного релиза — слабая защита (распознаётся и снимается), но осмысленная как уменьшение размера. Не полагайтесь на неё как на барьер от реверса.
  • Для Blue Team: высокая энтропия + крошечные импорты + нестандартные секции — это сигнал к расследованию, а не вердикт. Многие легитимные программы упакованы; решение принимают по совокупности признаков и поведению при запуске в изолированной среде.
  • Инструменты: статические детекторы упаковщиков (detect-it-easy и аналоги) быстро дают тип и версию; дальше — динамический анализ в безопасной лаборатории (изолированная ВМ, снапшоты).
  • Безопасность лаборатории: подозрительные образцы запускают только в изолированной виртуальной машине без доступа к рабочей сети, на свежем снапшоте.

Анализ упаковки законен на своих файлах, в учебной лаборатории и на CTF. Распространение вредоносного ПО и несанкционированный доступ к чужим системам наказуемы (ст. 272–273 УК РФ).

Итоги

  • Упаковщик сжимает/шифрует код и добавляет stub, который распаковывает оригинал в память при запуске.
  • Главные признаки: высокая энтропия секций (близко к 8) и подозрительно малое число импортов.
  • Дополнительно: нестандартные имена секций (например, UPX0/UPX1), пустые на диске, но большие в памяти секции, сигнатуры упаковщика.
  • Применение бывает и легитимным (UPX в нормальном ПО), и вредоносным; факт упаковки — сигнал к динамическому анализу, не приговор.
  • Настоящий код упакованного файла проявляется только в рантайме, на OEP — анализировать нужно в движении и в изоляции.
Проверьте себя
1. Какая пара признаков сильнее всего намекает на упакованный исполняемый файл?
AБольшой размер файла и много секций
BВысокая энтропия секций и подозрительно малое число импортов
CМного строк и стандартные имена секций
DНаличие отладочной информации
2. Что делает stub (программа-распаковщик) при запуске упакованного файла?
AУдаляет оригинальный код
BРаспаковывает секции в память, восстанавливает импорты и передаёт управление на OEP
CШифрует данные на диске
DСвязывается с сервером обновлений
3. Верно ли, что упаковка файла всегда означает вредоносность?
AДа, упаковка применяется только вирусами
BНет, упаковка бывает и легитимной (например, UPX для уменьшения размера); это лишь сигнал к динамическому анализу
CДа, но только если использован UPX
DНет, упаковка вообще не влияет на анализ