Исследование алгоритма работы упаковщика ASPack v1.08.03Delphi , Программа и Интерфейс , Исследование программИсследование алгоритма работы упаковщика ASPack v1.08.03
Сегодня мы "придем" за ASPack'ом. Автора зовут Солодовников Алексей - и я уже вижу, как в меня полетят камни праведного гнева: "Разве мы не должны защищать отечественных программистов?" Конечно должны! Однако, меня интересовала не сама программа, а алгоритм ее работы. Кроме того отказ от исследований отечественных программных продуктов по морально-этическим соображениям отнюдь не означает их хорошую защищенность, а даже наоборот, может стать предпосылкой к игнорированию этого аспекта нашими программистами! Введение Итак, что мы имеем. Программа неким мистическим образом "ускользает" из-под SoftICE. Даже сейчас, проанализировав её код, я не смогу дать ответ на вопрос "Почему?". В самом коде я не нашёл ничего "необычного". Остаётся предположить, что программа обманывает не сам SoftICE, а его символьный загрузчик (loader32.exe) - и делает она это, вероятнее всего, вследствие хорошо поправленной структуры PE-файла. В SoftICE же мы видим примерно следующее:
Что эти каракули означают - я и сам не ведаю (особенно что такое KPEB), но сильно напоминает отслеживание загрузки необходимых программе системных библиотек. Возможно, символьный загрузчик ожидает, что после загрузки должно произойти ещё что-то, после чего он уже с чувством выполненного долга сообщит SoftICE, что тому пора действовать - но этого "что-то" не происходит. Потому что системные библиотеки загружаются не как при обычном запуске программы (т.е. операционной системой), а программа сама загружает их после распаковывания в памяти. Но возможно, что я и не прав - у меня было мало времени на выяснение этого. Также не помощник нам и ProcDump (может быть вследствие неверного использования или недопонимания, но этот инструмент бывает мне полезен примерно в одном случае из 7-8). Несмотря на то, что у него гордо прописан метод декомпрессии ASPack, программа "убегает" и от него. Правда, он честно снимает копию участка (dump) памяти с уже запущенной программы, но пользоваться им потом нельзя - ни один дизассемблер не может с уверенностью распознать, программа ли это вообще. Ещё одна особенность - дизассемблеры ведут себя на ASPackе не лучшим образом. Скажем, IDA Pro в режиме автоанализа долго обращается к жесткому диску и выдаёт листинг, весьма отдалённо похожий на программный код, WinDasm просто зависает, у QView и HView также не могут ничего сделать. Короче, на сей раз мы имеем кое-что посложнее, чем программы типа "ставим контрольную точку на strcmp() - это и будет наш серийный номер". Однако, как говорил знаменитый Old Red Cracker (ORC+): " если программу можно запустить - её можно сломать"! Используемые программы Данная статья предполагает знание читателем ассемблера, языка C, Windows 32 API и общее представление о формате PE файлов, а также умение пользоваться отладчиком SoftICE и дизассемблером IDA Pro. Вам понадобятся следующие программы:
Исследование Советую начинать всегда с чтения прилагающейся документации. Что мы можем почерпнуть из файлов readme.txt и history.txt? Очень много, а именно:
Загрузим программу в IDA Pro, но будем держать всё под контролем, а именно - выберем пункт "Manual Load" в диалоговом окне "Load File of New Format". IDA будет спрашивать у нас подтверждение на загрузку каждого сегмента программы. Мы пропустим совершенно бесполезные в данном случае CODE, DATA, BSS, .idata, .tls, .rdata, .reloc, .rsrc, а загрузим только последние два сегмента .adata и .udata. Точка входа расположена по адресу 465000h:
Замечательный пример определения адреса, по которому выполняется код. Инструкция CALL $+5 вызывает в виде функции код, следующий непосредственно за ней, но при этом помещает в стек адрес возврата, т.е. 465006h. Инструкция POP EBP извлекает его из стека - и вот мы имеем адрес, по которому расположен код. Далее вычитается некоторое смещение - в EBP на протяжении работы всей программы будет находиться смещение на данные и код (поскольку загрузчик должен работать на множестве упакованных программ, он обычно пишется с применением так называемой "относительной" адресации, т.е. когда код может быть расположен по любому адресу.
Происходит проверка dword по адресу 4656A8h на равенство 0 - если не 0, то переход к запуску распакованной программы по адресу 465544h (я назвал его run_programm). По адресу 4654B7h записывается ранее вычисленное значение 444A0Ah + ebp - [4656ADh] = 400000h
У упакованной программы имеется сегмент импорта, но содержит ровно столько импортируемых функций, сколько необходимо для работы декомпрессора:
Лаконичность поражает воображение. Все необходимые функции для работы декомпрессора загружаются динамически. Для начала извлекается описатель (handle) библиотеки "kernel32.dll" (посредством вызова функции GetModuleHandleA()) и сохраняется в переменной по адресу 4656C9h, далее с помощью функции GetProcAddress() извлекаются адреса функций VirtualAlloc() и VirtualFree(), и сохраняются по адресам 4656B5h и 4656B9h соответственно.
Извлекается ранее вычисленное значение 400000h из [4654B7h], и помещается по новому адресу 4656A8h. Я назвал последний base - оно используется далее как стартовый адрес для декомпрессированного кода.
Вызывается функция VirtualAlloc() (помните, что параметры передаются в обратном порядке) с аргументами (0, 049Ah, 1000h, 4). Она выделяет несколько страниц памяти в виртуальном адресном пространстве процесса. Первый аргумент - адрес, обычно 0. Второй - размер области памяти. Третий - флаг, 1000h = MEM_COMMIT, выделить физическую память для запрашиваемых страниц. Последний аргумент - атрибуты защиты для выделенной памяти, 4 = PAGE_READWRITE (я надеюсь, не нужно объяснять). Указатель на выделенную память запоминается по адресу 4656B1h.
А вот это и есть обещанная защита декомпрессора - процедура декомпрессора сама сжата 2)! В EBX помещается её адрес (4650CBh), в EAX расположен адрес только что выделенного участка памяти. Сама процедура находится по адресу 465565h. Приводить её текст и комментировать его у меня нет желания - профессионалы и так разберутся, а начинающие всё равно ничего не поймут. Достаточно сказать, что это обычный (правда, очень вылизанный, что свидетельствует о его почтенном возрасте) алгоритм декомпрессии LZ, о чём можно догадаться, например, по такому коду:
Далее распакованный декомпрессор копируется из буфера по адресу 4656B1h (помните, что movsd перемещает по 4 байта, но длина распакованного кода может быть не кратна 4, поэтому мы должны позаботиться об остатке). Итак, для дальнейших исследований мы должны распаковать декомпрессор. Я написал небольшую программу на C (точнее, две трети на ассемблере), которая декомпрессирует этот кусок кода и сохраняет его в файле unpacked. Исходный текст программы прилагается (файл as1.c). Два момента заслуживают внимания:
Теперь мы должны как-то загрузить распакованный код обратно в IDA Pro. Для этого воспользуемся одной из уникальных возможностей этого инструмента - встроенным языком программирования IDC (документацию на него можно найти в файле помощи самой IDA Pro). Сценарий выглядит примерно так (файл unpack.idc):
(1178 = 049Ah). Я поместил этот script во внешний файл, загрузил его посредством команды Load File -> IDC File ... (можно просто нажать F2). Далее (нажав Shift+F2) наберём команду "unpack_one();". Теперь мы можем продолжить. Вы можете убедиться, что сейчас мы имеем осмысленный ассемблерный листинг.
По адресу 4656B1h записан указатель на ранее выделенный буфер памяти. Здесь вызывается функция VirtualFree() с аргументами (адрес_буфера, 0, 8000h). Интуитивно понятно, что происходит освобождение ранее выделенной памяти. Далее происходит переход на адрес 465233h. Он выглядит несколько странным (через стек), но мы должны помнить, что здесь не должна использоваться прямая адресация - потому что этот загрузчик универсален и код должен работать по любому (заранее неизвестному) адресу (также можно было использовать инструкцию jmp eax).
Малопонятное место. Проверяется dword по адресу 4650DBh, если он не 0 (в нашем случае 0), происходит копирование dword из [4650DBh], запись его в 4650DFh, а прежнее содержимое 4650DFh копируется в [4650DBh]. Далее (код я опустил - ничего интересного) происходит повторное определение адресов функций VirtualAlloc() и VirtualFree()
Происходит здесь следующее: в ESI загружается адрес начала таблицы со смещениями и размерами компрессированных блоков кода (названа мною pack_table). Далее в EAX помещается размер области памяти, выделяется виртуальная память посредством вызова VirtualAlloc() (см. пояснения выше), происходит определение адреса сжатого блока - в таблице хранится смещение относительно адреса загрузки программы (который хранится по адресу 4656A8h - base). Затем происходит декомпрессия. Функция unpack() возвращает длину декомпрессированного блока. Если эта длина не совпадает с указанной в таблице pack_table - происходит переход на адрес 465421h с сообщением "Decompress error". Там расположен код, который загружает все необходимые для своей работы функции из системных библиотек, выдаёт MessageBox с переданным в EBX сообщением, и осуществляет выход из программы (я назвал этот адрес say_BAD).
В этой части кода происходит расшифровка распакованного кода. Проверяется переменная по адресу 4656ACh на равенство с 0, и если там не 0 - переход на loc_465316. Иначе - значение 4656ACh увеличивается на 1, гарантируя, что последующий код исполнится только один раз. Так как начальное значение этой переменной 0, то этот код исполняется только в первом цикле. В ECX помещается длина распакованного кода - 6, в ESI - адрес буфера в памяти с самим распакованным кодом. Далее следует цикл: пока длина (ECX) больше 0: в EAX грузится байт по адресу в ESI (при этом ESI увеличивается на 1), и если он равен E8h или E9h - из dword по адресу в ESI вычитается EBX. Далее счётчики соответствующим образом увеличиваются для следующей итерации.
Распакованный код копируется обратно на своё законное место в памяти (base + смещение в таблице pack_table) (инструкции 465316h - 465330h). Затем восстанавливается в ESI текущий указатель в таблице pack_table и освобождается ранее выделенный буфер в памяти. Указатель в таблице pack_table перемещается на следующую структуру - до тех пор, пока смещение в этой таблице не примет значение 0. Далее снова происходит малопонятные манипуляции с переменными по адресам 4650DBh и 4650DFh
Происходит сравнение переменной base и 4650D7h (base2?), и если они равны (в нашем случае они равны), переход на 4653EEh. Я не смотрел, что происходит, если они не равны - у меня было мало времени.
Здесь вычисляется адрес таблицы импорта. В переменной 4650E7h содержится смещение на таблицу импорта относительно base.
Ндаа... Без SoftICE сложно сказать, что происходит. Чтобы таки посмотреть программу под отладчиком, я применил следующий трюк: найдём смещение в шестнадцатеричном редакторе на начало декомпрессора (см. выше, как именно), и изменим один байт на CC (инструкция Int 3). Загрузим SoftICE, скажем ему i3here on, чтобы он перехватывал третье прерывание. Теперь запускаем исследуемую программу - и она прерывается в том месте, где мы поменяли команду. Ставим нужные контрольные точки и приступаем к работе. Только не забудьте восстановить исправленный байт в нашей программе и запустить её снова. Итак, этот участок кода эмулирует работу загрузчика операционной системы - а именно, он грузит все необходимые программе функции из системных библиотек. Сначала идёт попытка получить описатель уже загруженной библиотеки вызовом функции GetModuleHandleA(), если же файл ещё не был загружен - LoadLibaryA(). Если библиотека не может быть загружена - на выход с соответствующим сообщением. Иначе описатель загруженной библиотеки помещается в переменную 46576Ah (я назвал её implib_handle), и обнуляется счётчик порядкового номера импортируемых функций - переменная 46576Eh (import_counter). Тут же располагается процедура защиты от копирования участков памяти - в dword имени библиотеки записывается 0. Далее следует цикл по всем именам функций (причём, как и в обычной таблице импорта, можно загрузить функцию как по имени, так и по номеру - в последнем случае адрес имеет установленный старший бит).
Здесь происходит запуск полностью распакованной программы. По адресу 4650EBh находится смещение точки входа относительно base. Если оно не 0 - происходит переход по вычисленному адресу. Результаты исследования
Далее я сделал копию участка памяти в файл с работающей программы - и вот оно работает! Правда, проблемы с ресурсами, но это уже исправляется (дизассемблер, правда, таблицу импорта так и не увидел, но программа, по крайней мере, стала запускаться).
В первую же свободную (помните, что, поскольку признаком окончания таблицы repack_table считается нулевая величина в поле offset, то первые два dword со значениями 0 в конце таблицы нужно считать её продолжением) ячейку таблицы repack_table я поместил две строки "dump.dll" (адрес 46511Bh - имя библиотеки) и "fnDump" (адрес 465124h - имя экспортируемой из библиотеки функции). Функция эта имеет такой прототип:
Первый параметр - базовый адрес (base, хранится, как мы помним, по адресу 4656A8h), второй - адрес первого элемента таблицы repack_table (её структура приведена над описанием функции).
Я надеюсь, всё понятно из комментариев. Я использовал для обработки ошибок оригинальный код декомпрессора (инкапсуляция на уровне ассемблера) по адресу say_BAD (см. описание выше). Последний участок, передающий управление оригинальной точке входа, скопирован полностью. Это не относительный код, он специфичен для данной конкретной программы, но Вы можете использовать его, поменяв адреса в инструкциях загрузки адресов строк. Можно переписать его, чтобы он также был относительным, но в таком случае нам придётся задействовать память за нашими строками (с адреса 46512Ch) - как мы помним, следующий нужный код начинается с адреса 4650CBh, а последняя инструкция в ранее добавленном коде располагается по адресу 4650C9h - едва поместилось.
В самой же функции Вы вольны делать что угодно! Например, модифицировать память, сохранить в файле содержимое сегментов и т.д. И всё это не создавая VxD и не задействуя нулевого кольца процессора! Приложение Список созданных мною в процессе исследования файлов:
1) Если Вы не знаете, что делает функция GetModuleHandleA() (или любая другая), советую найти хорошую документацию по Win32 API (скажем, с Visual C++ поставляется достаточно хорошая), или подписаться на MSDN. Я не вижу ничего предосудительного в том, чтобы изучать Windows API (равно как и любую программистскую технологию или приёмы защиты программ от любой фирмы, включая Microsoft) - Вы должны уважать своих врагов, внимательно изучать их, и брать от них самое лучшее. Иначе Вы никогда не сможете победить. Возвращаясь же к нашей теме: все функции Win API возвращают результат в регистре EAX, параметры передаются им в обратном порядке, и они сами чистят за собой стек (так называемое соглашение о вызовах функций stdcall). 2) В общем-то нет ничего уникального в том, что ASPack сжат ASPackом. В виде аналогии такой рекурсии можно вспомнить, что компилятор GCC собирает сам себя, для сборки Perlа используется усечённая версия Perlа - miniperl. Это, правда, не означает, что все ассемблеры написаны на ассемблере (хотя это возможно), и уж тем более, что Visual Basic написан на Visual Basic. Статья Исследование алгоритма работы упаковщика ASPack v1.08.03 раздела Программа и Интерфейс Исследование программ может быть полезна для разработчиков на Delphi и FreePascal. Комментарии и вопросыМатериалы статей собраны из открытых источников, владелец сайта не претендует на авторство. Там где авторство установить не удалось, материал подаётся без имени автора. В случае если Вы считаете, что Ваши права нарушены, пожалуйста, свяжитесь с владельцем сайта. :: Главная :: Исследование программ ::
|
||||||||||||||||||||||||||||||
©KANSoftWare (разработка программного обеспечения, создание программ, создание интерактивных сайтов), 2007 |