Как стать автором
Обновить

Улучшение степени сжатия применяемого в UPX

Время на прочтение5 мин
Количество просмотров11K
Автор оригинала: UPX developers
От переводчика:

Под «капотом» следует перевод небольшого, но крайне полезного текстового файла "%UPX_SOURCE%\doc\filter.txt". В приведенном пути под UPX_SOURCE подразумевается файловый путь до исходных кодов к UPX версии 3.91.

Документ описывает достаточно важный аспект работы UPX называемый «фильтрацией» и при анализе упакованных с помощью UPX файлов крайне важно понимать как это работает. Все что описано про UPX также применимо и к другим упаковщикам.

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

Понимание процесса фильтрации помогает при изучении упакованных файлов к примеру с помощью UPX, RLPack и др. В упакованных файла можно встретить код, где делаются некоторые «магические действиями» с маш. инструкциями переходов байты 0xE8, 0xE9 и др. Этой «магией» как раз и является «фильтрация». Она направлена на улучшение степени сжатия исполняемого файла.

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



Поехали…

Этот документ поясняет концепцию «фильтрация» в UPX. В основном фильтрация это предварительная обработка данных, которая может улучшить коэффициент сжатия файлов с помощью UPX.

В настоящее время фильтры UPX используют метод основаный на одном очень специфичном алгоритме. Он хорошо походит для исполняемых файлов архитектуры i386. В UPX он известен как «naive» реализация. Также существует способ «clever», который подходит только для 32-битных исполняемых файлов и впервые был реализован в UPX.

Давайте-ка возьмем примера и рассмотрим фрагмент кода(это место из 32-битного файла):
00025970: E877410600                     calln     FatalError
00025975: 8B414C                         mov       eax,[ecx+4C]
00025978: 85C0                           test      eax,eax
0002597A: 7419                           je        file:00025995
0002597C: 85F6                           test      esi,esi
0002597E: 7504                           jne       file:00025984
00025980: 89C6                           mov       esi,eax
00025982: EB11                           jmps      file:00025995
00025984: 39C6                           cmp       esi,eax
00025986: 740D                           je        file:00025995
00025988: 83C4F4                         add (d)   esp,F4
0002598B: 68A0A91608                     push      0816A9A0
00025990: E857410600                     calln     FatalError
00025995: FF45F4                         inc       [ebp-0C]

В приведенном куске кода Вы могли заметить две инструкции CALL вызывающих «FatalError». Возможно Вы догадались, что степень сжатия будет лучше если «движок» компрессора найдет больше последовательностей повторяющихся строк. В нашем случае движок имеет следующие два байта последовательностей:
E877 410600 8B   and
E857 410600 FF.

Поэтому он может найти 3-байтовых совпадения.

Сейчас применим трюк. Для архитектуры i386, ближние вызовы(«near calls») кодируются как 0xE8, затем следует 32-битное относительное смещение адресу перехода. Теперь посмотрим что же случится, если значение позиции вызова добавить к значению смещения:
0x64177 + 0x25970 = 0x89AE7
0x64157 + 0x25990 = 0x89AE7

E8 E79A0800 8B
E8 E79A0800 FF


Теперь «движок» компрессора находит 5-байтные совпадения. Это позволяет нам сэкономить 2 байта сжимаемых данных. Неплохо.

Это и есть основная идея метода «naive» реализации. Все что Вы должны сделать это использовать метод «filter» перед сжатием и «unfilter» после декомпрессии. Просто перейдите к памяти, найдя байты 0xE8 и обработайте следующие 4 байта как сказано выше.

Конечно, есть несколько возможностей где эта схема может быть улучшена. Во-первых, не только CALL-ы могут быть обработаны, но и near jmp-ы(0xE9 + 32-битное смещение) работает аналогично.

Второе улучшение может быть, если мы ограничим это фильтрование только для области занимаемой действительным кодом, нет смысла обрабатывать основные данные.

Еще одним улучшением будет, если поменять порядок байт 32-битного смещения. Почему? Вот другой CALL который следует в фрагменте выше:
000261FA: E8C9390600                     calln     ErrorF

0x639C9 + 0x261FA = 0x89BC3

E8 C39B 0800     сравним с

E8 E79A 0800


Вы можете заметить, что эти две функции достаточно близки друг к другу, однако компрессор не в состоянии использовать эту информацию (2-байтные совпадения, как правило не используются), если подрядок байт смещений повернуть. В таком случае:
E8 0008 9AE7

E8 0008 9BC3


Таким образом, «движок» компрессора ищет и такие 3-байтные совпадения. Это хорошее улучшение, теперь в «движке» также используются совпадения близлежайших смещений.

Это хорошо, но что случится когда мы найдем 'fake' CALL? Другими словами такой 0xE8, который является частью другой инструкции? К примеру такой:
0002A3B1: C745 E8 00000000               mov       [ebp-18],00000000

Тогда в таком случае эти замечательные 0x00 байты перезапишутся неcколько менее сжимаемыми данными. Это невыгодная «naive» реализация.

Давайте станем умнее и попытаемся обнаруживать и обрабатывать только «действительные» CALL-ы. В UPX используется простой метод для поиска этих CALL-ов. Мы просто проверяем адресацию(destination) этих CALL-ов внутри некоторой области как и сами CALL-ы(поэтому указанный код выше ложное срабатыване, но это в целом помогает). Лучшим методом будет это дизассемблирование кода, будем рады помощи :)

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

UPX использует следующую идею, которая работает хорошо. Сначала мы предполагаем, что размер области, который будет фильтроваться менее чем 16 МБ. Затем UPX сканирует эту область и сохраняет байты, которые следуют за 0xE8 байтами. Если нам везет, то найдутся байты, которые не следуют за следующим 0xE8. Эти байты наши кандидаты для использования в качестве маркеров.

Помните что мы предполагали размер области сканирования менее чем 16 МБ? Хорошо, это означает что мы обрабатываем реальный CALL, в результате будет смещение тоже меньше чем 0x00FFFFFF. Поэтому MSB всегда 0x00. Какое прекрассное место для хранения нашего маркера. Конечно мы реверснем порядок байт в получаемом смещении, так что этот маркер будет появляться только после 0xE8 байта и не в 4-х байтах после него.

Вот и все! Просто работайте с областью памяти, идентифицируя «реальные» CALL-ы и используйте этот метод для их пометки. После этого задача дефильтрации весьма упрощается, просто ищутся 0xE8 + последовательность маркера и дефильтрует, если нашел его. Это умно, не так ли? :)

Говоря по правде это не так просто в UPX. Может использоваться дополнительный параметр(«add_value»), что делает вещи немного более сложными(к примеру, может быть найден маркер непригодный к использованию, потому что некоторого переполнения во время сложения).

И алгоритм в целом оптимизирован для простоты дефильтрации(коротко и быстро ассемблируемо насколько это возможно, смотри stub/macros.ash), который делает процесс фильтрации менее сложной(fcto_ml.ch, fcto_ml2.ch, filteri.cpp).

Как это может быть видно в filteri.cpp, есть множество вариантов этих фильтрующих реализаций: — native/clever, calls/jumps/calls&jumps, с поворотом смещения/или без — в сумме где-то 18 различных фильтров(и 9 других вариантов для 16-битных программ).

Можете выбрать один из них используя параметр командной строки "--filter=" или испытать большинство из них "--all-filters". Или просто дать UPX использовать один заданных нами по умолчанию для исполнимых форматов.

От переводчика:
С удовольствием рассмотрю баг-репорты об ошибках у себя в списке личных сообщений:
* Связанные с переводом, орфографией или грамматикой;
* Технического характера допущенные при переводе, убедитесь что в оригинале все верно!
Теги:
Хабы:
+23
Комментарии6

Публикации

Изменить настройки темы

Истории

Ближайшие события

Weekend Offer в AliExpress
Дата20 – 21 апреля
Время10:00 – 20:00
Место
Онлайн
Конференция «Я.Железо»
Дата18 мая
Время14:00 – 23:59
Место
МоскваОнлайн