От переводчика:
Под «капотом» следует перевод небольшого, но крайне полезного текстового файла "%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-битного файла):
В приведенном куске кода Вы могли заметить две инструкции CALL вызывающих «FatalError». Возможно Вы догадались, что степень сжатия будет лучше если «движок» компрессора найдет больше последовательностей повторяющихся строк. В нашем случае движок имеет следующие два байта последовательностей:
Поэтому он может найти 3-байтовых совпадения.
Сейчас применим трюк. Для архитектуры i386, ближние вызовы(«near calls») кодируются как 0xE8, затем следует 32-битное относительное смещение адресу перехода. Теперь посмотрим что же случится, если значение позиции вызова добавить к значению смещения:
Теперь «движок» компрессора находит 5-байтные совпадения. Это позволяет нам сэкономить 2 байта сжимаемых данных. Неплохо.
Это и есть основная идея метода «naive» реализации. Все что Вы должны сделать это использовать метод «filter» перед сжатием и «unfilter» после декомпрессии. Просто перейдите к памяти, найдя байты 0xE8 и обработайте следующие 4 байта как сказано выше.
Конечно, есть несколько возможностей где эта схема может быть улучшена. Во-первых, не только CALL-ы могут быть обработаны, но и near jmp-ы(0xE9 + 32-битное смещение) работает аналогично.
Второе улучшение может быть, если мы ограничим это фильтрование только для области занимаемой действительным кодом, нет смысла обрабатывать основные данные.
Еще одним улучшением будет, если поменять порядок байт 32-битного смещения. Почему? Вот другой CALL который следует в фрагменте выше:
Вы можете заметить, что эти две функции достаточно близки друг к другу, однако компрессор не в состоянии использовать эту информацию (2-байтные совпадения, как правило не используются), если подрядок байт смещений повернуть. В таком случае:
Таким образом, «движок» компрессора ищет и такие 3-байтные совпадения. Это хорошее улучшение, теперь в «движке» также используются совпадения близлежайших смещений.
Это хорошо, но что случится когда мы найдем 'fake' CALL? Другими словами такой 0xE8, который является частью другой инструкции? К примеру такой:
Тогда в таком случае эти замечательные 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 использовать один заданных нами по умолчанию для исполнимых форматов.
От переводчика:
С удовольствием рассмотрю баг-репорты об ошибках у себя в списке личных сообщений:
* Связанные с переводом, орфографией или грамматикой;
* Технического характера допущенные при переводе, убедитесь что в оригинале все верно!
Под «капотом» следует перевод небольшого, но крайне полезного текстового файла "%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 использовать один заданных нами по умолчанию для исполнимых форматов.
От переводчика:
С удовольствием рассмотрю баг-репорты об ошибках у себя в списке личных сообщений:
* Связанные с переводом, орфографией или грамматикой;
* Технического характера допущенные при переводе, убедитесь что в оригинале все верно!