четверг, 12 августа 2010 г.

Перевод "%UPX_SOURCE%\upx-3.05-src\doc\filter.txt"

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



В настоящее время фильтры UPX используют основаный на одном очень специфичном алгоритме, который хорошо работает для ix86 исполнимых файлов. Это то, что UPX называет "native" реализацией. Есть также метод "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-байтовые совпадения.

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

E8 E79A0800 8B
E8 E79A0800 FF


Как ты можешь видеть сейчас "движок" компрессора нашел 5-байтные совпадения. Это значит, что мы просто сохраняем 2 байта сжимаемых данных. Неплохо.

Так что это основная идея("native" реализация). Все что мы должны сделать, так это использовать метод "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-батйные совпадения здесь. Это хорошее улучшение, теперь "движок" используются похожесть близлежайших смещений тоже.

Это хорошо, но что случится когда мы найдем фэйковый 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 всегда 0xFF. Какое прекрассное место для хранения нашего маркера. Конечно мы реверснем порядок байт в получаемом смещении, так что этот маркер будет появляться только после 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 использовать один заданных нами по умолчанию для исполнимых форматов.

Комментариев нет: