Главная / Блог / Мощь и беспомощность пакера 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».
Хорошо если секция полностью заполнена полезными данными, иначе компилятору придётся забить оставшееся пространство нулями, чтобы сделать её кратной 512. Аналогично, если данных получиться хоть на 1 тушканчик больше (например 513-байт), то компилятор выделит уже 2 блока = 1024 байт, в результате чего 511 во втором останутся безхозными. И это только для одной секции, а ведь их в РЕ-файле как минимум три: кода, данных, и импорта. На скрине ниже можно наблюдать топкое болото выравнивающих нулей в секции-импорта нашего подопытного Hello.exe:
Таким образом, в исполняемых файлах всегда имеется ни кем не используемое свободное пространство 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 в каталоге будет отсутствовать:
Теперь, что делает пакер? Он полностью пересобирает 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 и служит явным признаком того, что данные в текущем контейнере сжаты.
Поскольку структура оригинального файла теперь изменилась, то в РЕ-заголовке пакер меняет и значение точки-входа в программу ОЕР – из скрина ниже видно, что было 0x2000, а стало 0x6150. По этому адресу ждёт своего часа код анпакера (заглушка Stub), который разжимает в первоначальное состояния все секции из UPX1 в одну UPX0, после чего передаёт руль уже оригинальной точке ОЕР. На код стаба возлагается ответственная задача по восстановлению импорта и fixup-элементов:
Процедура распаковки в корень отличается для х32 и х64 исполняемых файлов. Например если это РЕ-32, то на фиктивной точке-входа анпакера, инструкцией PUSHA сразу сохраняется контекст всех регистров процессора, поэтому обнаружив контекстным поиском обратную PUSHA инструкцию POPA можно было без проблем найти конец кода анпакера. Однако в режиме х64 инструкция PUSHA уже не поддерживается, а потому здесь сохраняются только т.н. «Vollatile-Registers» в числе которых RBX,RSI,RDI,RBP, после чего инструкцией CALL управление передаётся специальной процедуре. Вот как это выглядит в отладчике x64Dbg для файлов РЕ64, на точке-входа 0х6150:
Практические приёмы работы с этим отладчиком описаны в материале Скрипты и функции в x64Dbg.
Если для правильной загрузки исходного файла достаточно лишь служебной информации из РЕ-заголовка, то для распаковки требуется ещё и доп.инфа о том, кто и каким методом запаковал оригинал. Для этого пакер 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:
В природе встречается софт класса «UPX-Scramblers», единственная цель которых испортить описанный выше заголовок пакера так, чтобы родной анпакер UPX не смог уже распознать запакованный файл – это делает невозможным авто-распаковку командой с ключом «upx -d». Для этого, скрамблеру всего-то нужно сбросить в нуль или байт с версией, или чаще изменить саму строку с валидной сигнатурой «UPX!» например на «UPX@». Именно поэтому необходимо всегда держать под рукой структуру хидера UPX выше, чтобы при необходимости была возможность восстановить РЕ-файл в исходное состояние.
Сейчас под ручной распаковкой подразумевается именно восстановление лишь UPX-заголовка, и только если это не спасает ситуацию, то прибегают к тяжёлой артилерии в виде снятия дампа памяти секций, с последующей правкой всего импорта. Для этого большинство отладчиков имеют спец.плагины, поэтому обычно проблем не возникает. Целью данной статьи было сделать акцент именно за хидере UPX, и дабы текст не превратился в каламбур, не затрагивать всё остальное.
Все права защищены. © 2016 - 2025