Главная / Блог / Мощь и беспомощность пакера UPX

8 мин.00

Мощь и беспомощность пакера UPX

20 СЕНТЯБРЬ, 2025
Мощь и беспомощность пакера UPX
UPX: алгоритм работы и ручная распаковка

В статье представлен общий алгоритм работы упаковщика UPX, что позволит вручную восстанавливать искажённые малварью запакованные файлы. Для экспериментов нам понадобится обычный EXE-файл для Windows типа «Hello World!», и так-же перечисленный ниже софт. Вот код на ассемблере FASM самого приложения, который позже сожмём пакером «UPX»:

format   pe64 gui
include 'win64ax.inc'
entry    start
;//----------
.data
capt     db   'HackerLab',0
msg      db   'UPX Example',0
 
;//----------
section '.text' code readable executable
start:   sub     rsp,8
invoke  MessageBox,0,msg,capt,0
invoke  ExitProcess,0
 
;//----------
section '.idata' import data readable
library  user32,'user32.dll', kernel32,'kernel32.dll'
include  'api\user32.inc'
include  'api\kernel32.inc'
 
;//----------
section '.hl' data readable
payload  rb  512

Домашняя страница UPX: https://github.com/upx/upx

Пакер UPX с граф.интерфейсом: https://www.pazera-software.com/products/free-upx/

Отличный вьювер РЕ-файлов: https://rammerlabs.alidml.ru/ru/

HEX-редактор «HxD»: https://mh-nexus.de/en/hxd/

Вводная часть

Известно, что конструктивно РЕ-файлы состоят из секций например: кода, данных, импорта, ресурсов, и т.п. Чтобы загрузчику образов ОС было легко ориентироваться в этих секциях, они имеют т.н. выравнивание «Alignment». Шаг выравнивания секций EXE-файла на диске составляет 512-байт, а после загрузки в память он увеличивается в 8 раз до 4 КБайт (одна страница вирт.памяти). Эти два значения указываются в опциональном заголовке файла и известны как «FileAlignment» и «SectionAlignment».

Схема работы пакера UPX и распаковки PE-файлов

Хорошо если секция полностью заполнена полезными данными, иначе компилятору придётся забить оставшееся пространство нулями, чтобы сделать её кратной 512. Аналогично, если данных получиться хоть на 1 тушканчик больше (например 513-байт), то компилятор выделит уже 2 блока = 1024 байт, в результате чего 511 во втором останутся безхозными. И это только для одной секции, а ведь их в РЕ-файле как минимум три: кода, данных, и импорта. На скрине ниже можно наблюдать топкое болото выравнивающих нулей в секции-импорта нашего подопытного Hello.exe:

image

Таким образом, в исполняемых файлах всегда имеется ни кем не используемое свободное пространство Alignment, которое в конечном итоге занимает место на диске. Именно для решения этой проблемы и были придуманы упаковщики двоичных файлов, в числе которых и герой данной статьи UPX, или «Ultimate Packer for eXecutables». Пакеры позволяют рационально использовать пространство устройств хранения данных, а так-же в силу уменьшенного размера, сокращать трафик при передачи данных по сети.

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

ОЕР, и системный загрузчик образов

Вспомним, как система загружает исполняемые файлы exe/dll с диска в память.

За подгрузку образов программ отвечают функции ядра Win с префиксом Ldr_XX(), что подразумевает «Loader». Сначала загрузчик считывает с диска только заголовок РЕ-файла, где находится вся служебная информация для дальнейших действий. Далее выделяются 4 КБайтные страницы вирт.памяти для секций, в них копируются соответствующие данные из файла, и наконец анализируется таблица импорта API-функций из системных библиотек. После того-как всё настроено, лоадер считывает из РЕ-заголовка адрес точки-входа в программу, и передав на неё управление, с чистой совестью отправляется на покой. Этот адрес известен как ОЕР, или «Original Entry-Point».

Подробнее про методы нахождения Original Entry Point см. в нашей статье Знакомство с OEP.

Из всего алгоритма загрузчика, только настройка импорта причиняет головную боль, поскольку является самой сложной и ответственной частью процедуры загрузки образа в память. Импорт поддерживают 2 составляющие файла – это непосредственно секция, где перечисляются все необходимые DLL и импортируемые из них функции, а так-же отдельная таблица IAT или «Import Address Table», которая динамически заполняется уже после загрузки образа в память, текущими адресами API-функций на данном узле. Указатель как на секцию-импорта, так и на IAT, компилятор прописывает в каталог «IMAGE_DATA_DIRECTORY» заголовка. Некоторые компиляторы могут располагать IAT прямо в секции-импорта (сразу после описания функций), как это делает например тот-же FASM – тогда линк на IAT в каталоге будет отсутствовать:

image

Алгоритм работы пакера UPX

Теперь, что делает пакер? Он полностью пересобирает PE-файл меняя всё-что можно, за исключением ресурсов – к ним UPX относится достаточно бережно. Остальное софт переводит в свой внутренний формат и сжимает по алгоритму UCL или LZMA, хотя в опциях можно указать и более прогрессивный NRV2 (Not Really Vanished). Пакер использует библиотеку UCL – Ultra Compression Library. Кому интересно, подноготная сжатия лежит в файле p_w64pe_amd64.cpp исходника, хотя для нас это сейчас не принципиально. Сколько-бы секций ни было в оригинальном файле, на выходе всегда будут только три:

UPX0 – пустая секция для распаковки и восстановления данных.

UPX1 – код, данные, экспорт, и все остальные секции.

UPX2 – секция импорта, и в редком случае ресурсы (хотя последние как-правило остаются несжатыми в открытом виде).

На скринах ниже представлены секции исходного и сжатого РЕ-файла где видно, что секция UPX0 является просто зарезервированной, которая отсутствует на диске (имеется только описание), зато после загрузки образа в память для неё выделяется макс. из всех кол-во байт 5000h=20КБ, о чём свидетельствует значение в столбце «Virtual Size», в то время как «RawSize» определяет размер на диске.

Обратите внимание на второй столбец с «Энтропией» данных – он описывает меру избыточности информации в блоке определённого размера, и чем выше её значение, тем больше находится данных без мусора (типа нулей). Энтропия измеряется по шкале 0-10 и служит явным признаком того, что данные в текущем контейнере сжаты.

image image

Поскольку структура оригинального файла теперь изменилась, то в РЕ-заголовке пакер меняет и значение точки-входа в программу ОЕР – из скрина ниже видно, что было 0x2000, а стало 0x6150. По этому адресу ждёт своего часа код анпакера (заглушка Stub), который разжимает в первоначальное состояния все секции из UPX1 в одну UPX0, после чего передаёт руль уже оригинальной точке ОЕР. На код стаба возлагается ответственная задача по восстановлению импорта и fixup-элементов:

image

Процедура распаковки в корень отличается для х32 и х64 исполняемых файлов. Например если это РЕ-32, то на фиктивной точке-входа анпакера, инструкцией PUSHA сразу сохраняется контекст всех регистров процессора, поэтому обнаружив контекстным поиском обратную PUSHA инструкцию POPA можно было без проблем найти конец кода анпакера. Однако в режиме х64 инструкция PUSHA уже не поддерживается, а потому здесь сохраняются только т.н. «Vollatile-Registers» в числе которых RBX,RSI,RDI,RBP, после чего инструкцией CALL управление передаётся специальной процедуре. Вот как это выглядит в отладчике x64Dbg для файлов РЕ64, на точке-входа 0х6150:

Практические приёмы работы с этим отладчиком описаны в материале Скрипты и функции в x64Dbg.

image

Заголовок UPX

Если для правильной загрузки исходного файла достаточно лишь служебной информации из РЕ-заголовка, то для распаковки требуется ещё и доп.инфа о том, кто и каким методом запаковал оригинал. Для этого пакер UPX имеет свой хидер размером 32-байта, который можно обнаружить по сигнатуре в виде 4-байтной/текстовой строки «UPX!». В нём хранится инфа следующего характера (кстати db=1 байт, dw=2, dd=4 байта):

struct UPX_HEADER
upxMagic        db  'UPX!'  ;// 4-байтная строка
upxVersion      db  0       ;// Код минимальной версии UPX'а (0Dh = 1.3)
upxFormat       db  0       ;// Тип и разрядность файла (PE32 =09h, PE64 =24h)
upxMethod       db  0       ;// Метод сжатия (если NRV или UCL, то =02)
upxLevel        db  0       ;// Степень сжатия (0..10, в дефолте =8)
upxU_adler      dd  0       ;// CRC части EXE в распакованном виде
upxC_adler      dd  0       ;// CRC части EXE в запакованном виде
upxU_len        dd  0       ;// Размер части EXE в распакованном виде
upxC_len        dd  0       ;// Размер части EXE в запакованном виде
upxU_file_size  dd  0       ;// Размер распакованного РЕ-файла.
upxFilter       dw  0       ;// Метод распаковки
upxCRC          db  0       ;// CRC этого заголовка
ends

А так выглядит данный заголовок в упакованном нашем файле «Hello_Upx.exe», и в разделе «Информация» софта «FreeUPX». Обратите внимание на код версии = 0Dh по смещению 0209h. Она указывается с учётом того, что все старшие версии UPX обратно-совместимы с младшими, т.е. версия (в данном случае) 4.00 может без проблем распаковывать все версии до 1.3:

image image

Скрамблеры

В природе встречается софт класса «UPX-Scramblers», единственная цель которых испортить описанный выше заголовок пакера так, чтобы родной анпакер UPX не смог уже распознать запакованный файл – это делает невозможным авто-распаковку командой с ключом «upx -d». Для этого, скрамблеру всего-то нужно сбросить в нуль или байт с версией, или чаще изменить саму строку с валидной сигнатурой «UPX!» например на «UPX@». Именно поэтому необходимо всегда держать под рукой структуру хидера UPX выше, чтобы при необходимости была возможность восстановить РЕ-файл в исходное состояние.

Заключение

Сейчас под ручной распаковкой подразумевается именно восстановление лишь UPX-заголовка, и только если это не спасает ситуацию, то прибегают к тяжёлой артилерии в виде снятия дампа памяти секций, с последующей правкой всего импорта. Для этого большинство отладчиков имеют спец.плагины, поэтому обычно проблем не возникает. Целью данной статьи было сделать акцент именно за хидере UPX, и дабы текст не превратился в каламбур, не затрагивать всё остальное.

Поделиться

0 комментариев

Пожалуйста, войдите, чтобы оставить комментарий.

Загрузка комментариев...