Под «маской» находится перевод небольшого, но очень полезного текстового файла «%UPX_SOURCE%filter.txt». В указанном пути UPX_SOURCE относится к пути к файлу исходного кода UPX версии 3.91.
В документе описывается очень важный аспект UPX, который называется «фильтрация», и при анализе упакованных файлов UPX очень важно понимать, как он работает. Все, что было описано про UPX, относится и к другим упаковщикам.
Основная цель этого перевода — попытаться помочь тем программистам, которые пишут статические распаковщики для исполняемых файлов. Другими словами, эта информация будет полезна реверс-инженерам. Под статическим распаковщиком я подразумеваю программу, которая принимает упакованный или защищенный исполняемый файл в качестве входных данных и создает выходной файл, как если бы он был создан каким-либо компилятором. Особенность этого типа экстрактора в том, что он работает исключительно на основе знания защиты или структуры упаковки файла, т.е. без использования «дампа дампа», «восстановления импорта» и других видов «читерства».
Понимание процесса фильтрации помогает, например, при проверке упакованных файлов с помощью UPX, RLPack и т. д. В упакованных файлах можно найти некоторые «магические» операции с байтами 0xE8, 0xE9 и другие инструкции перехода. Эта «магия» — «фильтрация». Он направлен на улучшение сжатия исполняемого файла.
Кроме того, знание того, как работает фильтрация, может сэкономить время техника в очень сложных ситуациях. Иногда невозможно получить отфильтрованный фрагмент за разумное время, например, при работе с полиморфами или файлами, использующими виртуализацию кода. Зная, как работает фильтрация, можно решить задачу написания кода, не имея точного исходного фрагмента кода.
Этот документ объясняет концепцию «фильтрации» в системе UPX. По сути, фильтрация — это предварительная обработка данных, которая может улучшить степень сжатия файлов UPX.
В настоящее время фильтры UPX используют метод, основанный на одном очень конкретном алгоритме. Это хорошо согласуется с двоичными файлами в архитектуре i386. В UPX это известно как «наивная» реализация. Есть еще «хитрый» метод, подходящий только для 32-битных бинарников, впервые реализованный в UPX.
Давайте возьмем пример и посмотрим на фрагмент кода (вот где находится 32-битный файл):
Возможно, вы заметили два оператора CALL, вызывающих «FatalError» в приведенном выше фрагменте кода. Вы, вероятно, можете догадаться, что степень сжатия будет лучше, если «движок» компрессора найдет больше последовательностей повторяющихся строк. В нашем случае двигатель имеет следующие два байта последовательности:
Таким образом, он может найти 3-байтовые совпадения.
Теперь давайте применим трюк. Для архитектуры i386 вызовы close кодируются как 0xE8, за которым следует 32-битное относительное смещение адреса перехода. Теперь посмотрим, что произойдет, если значение позиции вызова будет добавлено к значению смещения:
«Движок» компрессора теперь находит 5-байтовое совпадение. Благодаря этому мы сохраняем 2 байта сжатых данных. Неплохо.
Это основная идея «наивного» метода реализации. Вам просто нужно использовать метод «фильтр» перед сжатием и «разблокировать» после распаковки. Просто перейдите в память, найдите 0xE8 байт и обработайте следующие 4 байта, как сказано выше.
Конечно, есть несколько возможностей, где эту схему можно было бы улучшить. Во-первых, не только CALL могут обрабатываться, но и близкие к jmps (0xE9 + 32-битное смещение) работают точно так же.
Второе улучшение будет заключаться в том, что если бы мы ограничили эту фильтрацию только областью, занимаемой фактическим кодом, нет смысла обрабатывать базовые данные.
Другое улучшение было бы, если бы мы изменили порядок байтов в 32-битном смещении. Почему? Вот еще один CALL, который следует во фрагменте выше:
Вы можете заметить, что эти две функции довольно близки друг к другу, но компрессор не может использовать эту информацию (2-байтовые совпадения обычно не используются), если порядок байтов смещения меняется. В таком случае:
Таким образом, «движок» компрессора также ищет такие 3-байтовые совпадения. Это приятное улучшение, теперь «движок» также использует совпадения с близким смещением.
Это хорошо, но что происходит, когда мы находим «фальшивое» ПРИГЛАШЕНИЕ? Другими словами, 0xE8, который является частью другой инструкции? Например этот:
Тогда в этом случае эти большие байты 0x00 будут перезаписаны чуть менее сжимаемыми данными. Это невыгодная «наивная» реализация.
Давайте поумнеем и попробуем обнаруживать и обрабатывать только «важные» СОЕДИНЕНИЯ. UPX использует простой метод для поиска этих вызовов. Мы просто проверяем цель этих вызовов в некоторой степени, как и вызовы (поэтому приведенный выше код является ложным срабатыванием, но в целом помогает). Лучшим способом было бы разобрать код, помощь приветствуется 🙂
Но это только часть работы. Мы не можем просто обработать один ЗВОНОК, а затем добавить другой, процессу фильтрации нужна некоторая информация, чтобы иметь возможность отменить фильтрацию.
UPX использует следующую идею, которая хорошо работает. Во-первых, мы предполагаем, что размер области фильтра меньше 16 МБ. Затем UPX сканирует эту область и записывает байты, следующие за байтами 0xE8. Если нам повезет, мы найдем байты, которые не следуют за другим 0xE8. Эти байты являются нашими кандидатами на использование в качестве токенов.
Помните, мы предполагали, что размер области сканирования меньше 16 МБ? Ну, это означает, что мы обрабатываем настоящий CALL, и результатом также будет смещение меньше 0x00FFFFFF. Поэтому MSB всегда равен 0x00. Какое прекрасное место для хранения нашего чипа. Разумеется, в полученном смещении мы меняем порядок байтов, чтобы этот токен стоял только после байта 0xE8, а не через 4 байта после него.
Это оно! Просто работайте с областью памяти, идентифицирующей «настоящие» ВЫЗОВЫ, и используйте этот метод, чтобы пометить их. Тогда задача фильтрации довольно проста, просто найдите строку 0xE8 + маркера и парад, если вы ее найдете. Это умно, не так ли? 🙂
По правде говоря, в UPX не все так просто. Вы можете использовать необязательный параметр («add_value»), который немного усложняет дело (например, токен может считаться бесполезным, потому что он переполняется при добавлении).
А алгоритм в целом оптимизирован для легкой фильтрации (короткой и быстрой, см. заглушку/макросы.ash), что делает процесс фильтрации менее сложным (fcto_ml.ch, fcto_ml2.ch, filteri.cpp).
Как вы можете видеть в filteri.cpp, существует множество вариаций этих реализаций фильтрации: — нативные/умные, вызовы/переходы/вызовы и переходы, с поворотом смещения/без него — всего около 18 различных фильтров (и 9 других вариантов для 16 -битные программы).
Вы можете выбрать один из них с помощью параметра командной строки «—filter =» или протестировать большинство из них с помощью «—all-filters». Или просто позвольте UPX использовать один из наших исполняемых форматов по умолчанию.
От переводчика:
Рад рассмотреть сообщения об ошибках в моем личном списке сообщений:
* Перевод, правописание или грамматика;
* Технические ошибки в переводе, убедитесь в правильности оригинала!