Введение
В современном мире программного обеспечения давно сложилась ситуация, когда практически любое приложение, динамическая библиотека, скринсейвер или даже драйвер оказывается сжатым (или упакованным), что накладывает определенные ограничения на использование. Например, если простой пользователь программы захочет ее русифицировать или дизассемблировать, чтобы разобраться в каких-либо отдельных принципах ее функционирования, ему понадобиться в большинстве случаев ее распаковать. Здесь я не говорю о "тяжелых" навесных протекторах (ASProtect, Armadillo, EXECryptor), так как их принцип распаковки требует не только специфичных навыков в области обратной инженерии, но и отдельного подхода.
Я буду говорить об обычных простых упаковщиках (пакерах) типа UPX, PECompact, FSG и многих других, которые успели появиться во многом количестве. Хотя принцип работы и метод сжатия в них используются, зачастую, абсолютно разные, но общий алгоритм функционирования достаточно прост и схож у большинства из них.
Дабы не создавать отдельного распаковщика для каждого пакера, почему бы не создать один общий распаковщик, который смог бы справиться со всем многообразием существующих упаковщиков? Тут существует ряд глобальных в масштабе данной темы проблем, которые можно выделить:
1. Упакованная программа может быть обработана обфускатором/скрамблером/спуфером и прочими программами, изменяющими основные характеристики, критичные для универсального распаковщика.
2. Существует подмножество пакеров, которые видоизменены авторами в пользу защиты от распаковки, используют методы антиотладки и других приемов. Можно сказать "недоростки" протекторов.
Забегая вперед, следует сказать, что мною создана утилита Quick Unpack, являющаяся универсальным распаковщиком простых пакеров, способная одним нажатием кнопки за пару секунд распаковать популярные пакеры и даже часть их неизвестных "братьев". Основной особенностью ее реализации является ее движок работы с процессом, выполненный в виде драйвера, позволяющей на любом месте работы программы легко остановить процесс и считать его контекст устройства (важно состояние регистров, например). Такой движок называется трейсер (tracer), т.е. трассирующим программу, и имеющим над ней полный контроль. Весь движок написан талантливым программистом syd'ом, который является автором автоматического распаковщика Stripper, предназначенным для распаковки ASProtect'a. Я лишь на основе его движка систематизировал сведения о множестве пакеров и превратил идею в более универсальную. Следует отметить, что это всё относится только к NT-платформе (Win2000/ХР/2003).
Универсальный принцип действия распаковки, используемый в Quick Unpack, следующий:
1. Поиск оригинальной точки входа (ОЕР)
2. Запуск программы и остановка на точке входа (ЕР)
3. Установка брейкпоинта на ОЕР
4. Запуск программы на исполнение
5. Срабатывание брейкпоинта, происходит остановка процесса на ОЕР
6. Снятие дампа
7. Восстановление таблицы импорта
8. Восстановление таблицы настроек адресов (FixUp Table или Relocations). Принципиально только для DLL.
9. Восстановление локальной области данных цепочек (TLS), если она повреждена
10. Корректировка ресурсов (перемещение, склеивание)
11. Структурирование и модификация дампа с учетом восстановленных данных
После проделанных манипуляций распакованный ехе/dll должен в 80% случаев работать. Если подумать, то
опытный человек в обратной инженерии может резонно заметить, что с помощью существующих инструментов можно всё это сделать "в ручную". Действительно, однако, куда проще нажать на кнопку "Unpack" и за секунды получить готовенькую программу, со снятым упаковщиком.
Следует остановиться на каждом пункте, чтобы понять, что каждое действие из себя представляет и для чего оно нужно в целом.
Описание базовых принципов работы автораспаковщика
1. Поиск оригинальной точки входа (ОЕР)
Поиск ОЕР представляет из себя один из важнейших этапов распаковки. Именно от этого DWORD значения зависит работоспособность остальных действий. Почему он важен? Если представить, что ЕР найден не верный, то бряк будет установлен (если вообще сможет быть поставлен) либо на область памяти, не принадлежащей программе, либо адрес, где-то внутри программы, но не являющийся "отправной точкой ехе/dll". В таком случае бряк установится, программа просто запустится и всё и он никогда не сработает, распаковщик никогда не узнает, распакована ли программа, т.к. сигналом того, что всё готово к снятию дампа является остановка трейсера на ОЕР.
В последней версии Quick Unpack используется система плагинов для поиска ОЕР для каждого отдельного пакера, если универсальный поисковик ОЕР (generic finder) не дал результата или он неверный. В основном при поиске ОЕР используются 2 метода: 1) поиск по сигнатурам (маскам) 2) поиск по конкретному смещению.
Поиск по маскам весьма популярен, его используют, где только можно: в антивирусах, в анализаторах, в различных детекторах. Его принцип прост. Тока входа, например, у Delphi выглядит обычно так:
55 | PUSH EBP |
8BEC | MOV EBP,ESP |
83C4 F0 | ADD ESP,-0x10 |
B8 20F24400 | MOV EAX, 0044F220 |
Маска составляется таким образом, чтобы постоянные байты были в маске, а переменные при поиске по маске просто пропускать. Тогда для примера маска будет 558BEC83C4xxB8xxxxxxxx. Где x-переменный байт.
Поиск по маске ОЕР выглядит так: программа проецируется в память через CreateFileMapping и MapViewOfFile; создается цикл поиска по маске; адрес, по которому совпала маска, будет являться ОЕР.
В другом случае бывает проще сделать поиск ОЕР по смещению. Главный минус метода: если пакер чуть изменят (вернее стаб пакера, т.е. тот блок кода, который внедряется в упаковываемую программу и приводит в памяти программу в оригинальный вид) - смещение изменится. Например, так было с FSG. Стаб FSG пакера:
0040700E | > | BE A4014000 | MOV ESI, 004001A4 (точка входа) |
00407013 | AD | LODS DWORD PTR DS:[ESI] | |
004070AD | FE0E | DEC BYTE PTR DS:[ESI] | |
… | |||
004070AF | -0F84 4B9FFFFF | JE 00401000 (выход на ОЕР) |
Если внимательно посмотреть и сделать пару тестов, то будет видно, что прыжок 4070AF ведет в конечном итоге на ОЕР, а часть опкода 4B9FFFFF - это смещение до ОЕР считая от адреса 4070AF
(4070AF+FFFF9F4B+6 (длина опкода прыжка) = 401000 = ОЕР). Сделать этот расчет легко, даже не запуская программу. Искомое смещение рассчитываем по формуле ОЕР-ЕР (для примера,
4070AF-40700E =A1). Проще говоря, из программы считывается число являющееся смещением на ОЕР: EP+0xA1+2.
Уже можно догадаться, что для каждого нового пакера (пусть даже новой версии имеющегося пакера) методология поиска ОЕР будет различна. Поиск ОЕР является одной из важнейших составляющих ручной распаковки, т.к. ее невозможно подогнать под единый стандарт. Единственный видимый теоретический выход - создание "баз данных", в которых хранятся алгоритмы для всех пакеров, а это дело трудоёмкое. Принцип работы generic-метода поиска ОЕР прост (GenOEP.dll модуль из PEiD относится к таким), но очень ненадежен. Он запускает программу, определяет, когда она полностью запустится, затем ищет в ней маску забитых в базу поисковика разных компиляторов. А так как компиляторов существует много и ЕР может быть очень разным, то его правильный результат работы можно охарактеризовать как 40 случаев из 100. Для определения того что программа именно упакована пакером конкретным, используется тоже своя "база данных" масок для пакеров.
2. Запуск программы и остановка на точке входа (ЕР)
На этом этапе всё достаточно просто. Программа запускается через CreateProcess, но без флага DEBUG_PROCESS (этот флаг не нужен, ведь используется драйвер для тотального контроля), что дает еще плюс в том, что наш распаковщик не обнаруживается через IsDebuggerPresent. Остановка на точке входа выполняется через установку бряка на ЕР примерно так:
AddBreak(brEP, Image_Base+Entry_point_RVA); // добавляет адрес в очередь бряков
EnableBreak(brEP); // активируется бряк
Continue(); // программа запускается (цикл ожидания какого-либо события)
3. Установка брейкпоинта на ОЕР
Для работы с брейкпоинтами (бряками), как уже было сказано, был написан драйвер-трейсер, через который распаковщик получал контекст устройства (CONTEXT). Есть 2 основных метода установки бряков - через dr-регистры и через прерывание int 3. Второй способ хуже тем, что на место адреса бряка записывается опкод int3 - байт 0xCC, тем самым, изменяя программу, чего допустить никак нельзя. Брейкпоинты ставились через dr-регистры процессора, тем самым не изменяя начало ОЕР.
После остановки на ЕР просто устанавливается бряк на определенный ранее ОЕР (или указанный вручную).
4. Запуск программы на исполнение
Здесь программа просто запускается и теоретически ожидается срабатывание бряка на ОЕР. Но здесь не всё так просто. Например, может быть такая ситуация, когда адрес ОЕР может исполняться сколь угодно раз (пока на место ОЕР не запишется оригинальный код). Решить эту проблему можно, если перед всеми операциями запустить программу и посчитать, сколько раз сработает бряк на ОЕР и уже после этого снова перезапускать ее и ставить бряк на последнее срабатывание бряка (режим force
unpacking). Может быть, случай, когда ЕР=ОЕР. Тогда используется тот же режим force unpacking, когда считается количество сработанных бряков на ОЕР и первый бряк на ЕР пропускается.
В стабе пакера может так же, как в любой программе, срабатывать обрабатываемое исключение (SEH). Эти исключения ловятся драйвером и движком распаковщика правильно обрабатываются.
5. Срабатывание брейкпоинта, происходит остановка процесса на ОЕР
Теоретически предполагается, что программа к этому моменту распакована. Будем считать, что это так.
6. Снятие дампа
С этого момента начинаются манипуляции, приводящие распакованную в памяти программу в работоспособный вид. Дамп призван сбросить распакованный образ программы на жесткий диск. В движке Quick Unpack построена хорошая подсистема для работы с РЕ файлом. Вместо сохранения дампа на диск, создается копия дампа в динамической памяти распаковщика (выполняется эмуляция дампа), что избавляет от многократных изменений файла на жестком диске. После снятия копии можно приступать изменять поля РЕ-заголовка. Основные параметры: Size Of Image, Size Of Headers и выравнивание секций. Используется простая схема для автоматического определения правильных значений полей и в этом вопросе проблем обычно не бывает.
7. Восстановление таблицы импорта
Восстановление импорта является самой интересной частью распаковщика. Цель: собрать всю информацию об импортируемых программой функциях, затем все данные систематизировать и создать таблицу импорта, совместимую с загрузчиком Windows. Syd реализовал очень интересный движок для восстановления импорта. Принцип его работы следующий.
Перед запуском программы из ставится бряк на LoadLibrary. При его срабатывании, загружаемая через LoadLibrary библиотека (не сама API!), которая передается первым параметром, заносится во внутреннюю базу данных распаковщика.
Затем из этой собранной базы данных загруженных DLL вытаскиваются все экспортируемые функции и вычисляются их адреса. Создаются множество DWORD-массивов адресов API для каждой DLL. Затем на этапе восстановления импорта производится поиск каждого адреса из массивов по секциям кода и данных. Если адрес совпал - он заносится в список импортированных функций программы.
Все выглядит вполне работоспособно и является интересным решением. Однако, есть и свои подводные камни. Часто адрес совпадает с неким "левым" адресом из DLL и определяется как совершенно посторонняя запись. В Quick Unpack сделан автоматический определитель неверных записей. Основан он на поиске "выделяющихся из общей массы" API. Все эти действия свели возможность попадания "левых" API к минимуму, но совсем избавиться от них пока не удается. Однако, работы над этим ведутся.
8. Восстановление таблицы настроек адресов (FixUp Table или
Relocations)
Таблица релоков нужна только для DLL, т.к. ехе загружаются по статическому Imagebase, который указан в заголовке. DLL же может загрузится по любому, который может "выделить" ОС. Идея, которую использует Quick Unpack для восстановления не нова. Определяется Imagebase из РЕ-заголовка, затем, когда DLL загрузится
(для загрузки DLL Quick Unpack создает ехе-болванку, который только загружает ее в свою память), LoadLibrary ехе-болванки вернет адрес Imagebase, который используется в дальнейшем.
Происходит сначала поиск по секциям кода и данных на поиск первого Imagebase, после остановки на ОЕР происходит снова поиск второго Imagebase. Где найдено различие - это и будет место, которое нужно занести в таблицу релоков (технически создание совместимой с РЕ-форматом таблицы написал syd).
9. Восстановление локальной области данных цепочек
(TLS)
Для наших целей TLS является простой областью из 24 байт. Ее стоит только переклеить в новое место, которое будет доступно. В целом операция простая и затруднений в реализации не вызывает.
10. Корректировка ресурсов (перемещение, склеивание)
Большинство софта, с помощью которого русифицируют программы, требуют целостность ресурсов. Но многие пакеры расчленяют ресурсы друг от друга, не говоря уже об упаковке их. Часто иконка или VersionInfo оказывается отдельно совсем от общей массы элементов ресурсов. Решить ситуацию в глобальном варианте можно и работы в этом направлении ведутся. Но и в этом вопросе есть много загвоздок. Многие пакеры сильно меняют расположение ресурсов так, что чтобы склеить всё обратно придется передвигать другие части РЕ-файла, а в других частях могут быть некие абсолютные адреса, указывающие на расположение данных, что скажется на отрицательной работоспособности программы. Таким образом, общего решения пока добиться не удается.
11. Структурирование и модификация дампа с учетом восстановленных данных
Окончательный этап - сбор всей восстановленной информации и вклеивание в конец программы. Этот процесс автоматизирован полностью и требует лишь правильной работы движка с РЕ-файлами.
Заключение
По результатам эмпирических исследований, при умелом использовании Quick Unpack, можно добиться автоматической либо полуавтоматической распаковки 70-80% программ, упакованных популярными пакерами. Это весьма хороший показатель, если учесть, что достойных автоматических распаковщиков не существует.
FEUERRADER [AHTeam]
www.AHTeam.org
Последний Quick Unpack можно отсюда: