Низкоуровневое программирование для Дzenствующих

Вид материалаДокументы

Содержание


Тонкости PE-формата
OEP и иже с ним
Подобный материал:
1   ...   30   31   32   33   34   35   36   37   ...   42

Тонкости PE-формата


Вы решили потратить деньги на приобретение пакера. ОК, выбор ваш. Положим, вы не уверены, что способны написать хорошую защиту, просто нет времени или еще что-то. Тогда проверьте пакер! Хороший криптор должен не только перезагружать Windows если кто-то подошел к монитору, он должен и корректно обрабатывать многие тонкости и сложности PE-формата.

Одним из достаточно простых, но достаточно забавных тестов на качество написанного пакера может быть следующий, почти гениальный, клочок кода:

__declspec(thread ) int i = 1;

__declspec(thread ) int m = 0;


void main(void)

{

/*для нас совершенно неважно, какой именно код тут используется,

он написан просто так, чтобы что-то написать;

что действительно важно, так это объявления переменных.

Статические объявления заставляют компилятор

создать секцию .tls в результирующем файле*/

printf(“%ld\n”, i*m);

}

Основной смысл такого упражнения – в создании секции .tls в результирующем PE-файле (заметьте, мы говорим о статическом tls – с tls можно работать и динамически – подробнее – Джеффери Рихтер). .tls-секция (если она существует) обрабатывается лоадером при загрузке – вызываются callback-функции и т.п. Все это достаточно подробно описано Питреком в

ссылка скрыта

и облегчать жизнь писателям пакеров у нас желания нет. Однако факт остается фактом – многие коммерческие упаковщики не учитывают инициализацию tls-цепочек лоадером, в результате чего запакованный файл падает. Среди таковых и Aspack 2.12, который такой файл даже обработать не может! А люди еще за это и деньги платят...

Более того, можно сделать еще веселее! Положим, мы имеем дело с dll (tls-цепочки используются, в основном, именно в dll) – как прикажете обрабатывать секцию tls, которая подвержена перемещению, т.е. появляются fixup-элементы? В случае exe-файла аналогичного результата можно добиться опцией MS-линкера /FIXED:NO. Так это вообще фантастика! UPX гарантировано обрабатывает такие вещи, а вот некоторые остальные, не будем показывать пальцами...

Вот такими и является большинство упаковщиков. С миру по нитке накопировано антиотладочных приемов (зачастую, беззастенчиво спертых из кода вирусов или толковых статей кракеров), все это достаточно неумело отлажено, кое-как обрабатывается PE-файл, при этом совершенно не учитываются многие нюансы и тонкости формата и за это просят деньги, утверждая, что пакер работает превосходно и надежно спасает от недалеких кракеров.

OEP и иже с ним


Для этой главы неплохо было бы выбрать самый простой из всех возможных упаковщик, на котором и проиллюстрировать некоторые закономерности работы. Такой, к счастью, есть. Называется PE Deminisher и доступен для закачки с ссылка скрыта.

Итак, пакуем наш старый добрый calc.exe и что мы имеем:

Name VirtSize RVA PhysSize Offset Flag
.text 000124EE 00001000 0000782B 00000600 E0000020
.data 000010C0 00014000 000003E1 00008000 C0000040
.rsrc 00002B98 00016000 00002C00 00008400 40000040
.teraphy 00001000 00019000 0000041A 0000B000 C0000040

Очевидно, три секции являются нормальными, четвертая принадлежит упаковщику. Так же очевидно, что, в этом случае, остальные секции (за исключением .rsrc) являются сжатыми по какому-либо алгоритму (в данном случае это apLib, но нам это не важно). Что находится в четвертой секции и почему она расположена именно четвертой? Вот слегка обрезанный отчет HIEW о данном файле:

Name RVA Size
Import 00019391 00000089 ;импорт перенаправлен
Resource 00016000 00002B98 ;ресурсы оставлены
Debug 00001210 0000001C ;старый трюк – см. первую часть
Import Table 00001000 0000020C
;недоработка данного упаковщика – IID

;перенаправлена в секцию .teraphy, IAT оставлена, но не валидна

Использование директории отладки является старым добрым антиотладочным приемом, мы это уже описывали и повторяться неинтерестно. Ресуры тоже уже мало кого удивляют. Любопытнее выглядит изменение RVA директории импорта в секцию пакера. Если глянуть на заголовок (Optional Header), то можно видеть, что и точка входа переориентирована в новую секцию, и количество секций, соответственно, увеличено. Разумеется, изменилось поле SizeOfImage, иначе файл валидным не будет. Кому интерестно видеть все мелочи – воспользуйтесь функцией Compare из Pe Tools. Нам же интерестнее ответить на вопрос: так почему же секция идет четвертой. Если чуточку подумать над проблемкой, то ответ ясен – так легче. Положим, можно поставить и третьей, если не лень пересчитывать ресурсы как директорию и как секцию. А вот первой – ни-ни. Так как слишком уж это дело будет хлопотное... Но если кому-то не лень, что тогда? Смотрите – теоретически невозможно поставить секцию пакера первой – это потребует коррекции ссылок в секции кода и коррекции ссылок между секциями кода и данных и т.п., да и не только. Откуда получить такую информацию? Если файл содержит IMAGE_DIRECTORY_ENTRY_BASERELOC, то тогда, используя информацию оттуда, такое дело возможно, однако методика для общего случая работать не будет.

Что в этом плане можем извлечь мы. Да очень простой, старый и почти безотказно работающий трюк. Только давайте сначала четко определимся с терминами. Итак: OEPoriginal entry point – это не та самая точка входа которая записана в заголовке PE файла (OptionalHeader.AddressOfEntryPoint). OEP - это VA, куда упаковщик передаст управление после полной распаковки файла. Т.е. это - оригинальная точка входа которая была в заголовке PE файла до упаковки. А точка входа в запакованном файле называется EP (Entry Point). Так вот, нетрудно заметить, что прыжок после распаковки всегда будет происходить из области больших адресов в область меньших адресов. На этом механизме и были построены многие OEP-трейсеры – revirgin и icedump в их числе. Очевидно, что если кто-нибудь (гм, например, мы) не поленится написать драйвер, который даже будет не сколько трассировать приложение, сколько просто смотреть за EIP, когда тот будет выходить за пределы секции (секций) упаковщика. Положим, протектор сможет делать ложные прыжки, положив оные в конструкцию try/catch (см. ниже) – но отчет утилиты покажет это все человеку, а уж человек элементарно разберется – какой прыжок ложный, а какой нет. С другой стороны, пакер вполне может применять засечки количество тактов процессора – rdtsc и, чуть что не так, начинать орать. Словом, тут есть над чем подумать...

Работать с dll примерно так же просто. Достаточно давно разработана методика Break & Enter. Смысл ее состоит во влеплении опкода СС (о самом опкоде см. ниже) прямо в EP программы. Известно, что Soft-Ice Symbol Loader часто просто проскакивает мимо EP. Поэтому LordPE и PE Tools лепят СС-байт прямо в EP, предварительно запоминая оригинальный. Все, что остается пользователю – ввести bpint 3 и восстановить старый байт после всплытия Soft-Ice. Скоро будет написан плагин под PE Tools в виде лоадера dll, т.к. dll, с нашей с вами точки зрения, мало чем отличается от exe.

Что до директории импорта – тоже достаточно просто понять, что оригинальная директория импорта остается нетронутой лоадером (он ее просто не видит). Вместо этого пакер сам, после расшифровки содержимого файла, находит эту директорию и в цикле, перед передачей управления на OEP, с помощью GetProcAddress, наполняет ее валидными для данной системы адресами и производит перерасчет RVA на VA (см практический пример с Aspack). Обязательно следует заметить, что и тут прогресс ушел далеко вперед. Современные крипторы уже не используют GetProcAddress. Уж слишком легко нам поставить на нее брейкпоинт и разобраться в логике пакера (см. практические примеры).

bpx на функции API

Команда bpx Soft-Ice использует зарезервированный Intel опкод CC. Интеловские талмуды говорят нам следующее: «The INT 3 instruction generates a special one byte opcode (CC) that is intended for calling the debug exception handler. (This one byte form is valuable because it can be used to replace the first byte of any instruction with a breakpoint, including other one byte instructions, without over-writing other code).». Не путайте также опкод CD 03 с CC – они используются в разных случаях – «Note that the “normal” 2-byte opcode for INT 3 (CD03) does not have these special features. Intel and Microsoft assemblers will not generate the CD03 opcode from any mnemonic, but this opcode can be created by direct numeric code definition or by self-modifying code.». Поставить точку останова на API и получить результат – это прекрасно для нас с вами, но не слишком хорошо для авторов упаковщиков. Вот пример немного наивного кода для детекции bpx в самом начале LoadLibraryA:

mov eax,[KernelBase]
push offset LoadLibraryA
push eax
call GetProcAddress
cmp byte ptr [eax],0cch
je Found_Hook

Код прост, но хорошо отражает суть. Его можно выразить и чуть иначе, например, так:

mov edi, offset на собственную IAT – сканируем первый байт всех адресов
mov al, 0CCh
repnz scasb ;код скорее, схематичен,

;чем реален, но в сети есть настоящие примеры

а можно и еще парой десятков вариаций. Для его обхода опытные люди используют трюк с bpx API-name + x, где х – число, которое приходится на начало другой инструкции. Важно заметить, что + х может быть не абы каким числом. Это обязательно должно быть поле операнда, иначе исключения #DB не произойдет, произойдет другое исключение :) Скажем, часто предлагается ставить нечто вроде bpx GetProcAddress + 3. В этом случае Soft-Ice превратит символьное имя GetProcAddress в VA (гм, а может и не в VA, бог его знает, что там внутри Soft-Ice происходит – ведь большинство не задумывается ПОЧЕМУ команда bpx на какую-нибудь API-функцию типа MessageBox срабатывает в любом адресном пространстве, т.е. контекстно-неспецифична – под Windows для первых двух гигабайт это попахивает черной магией!), добавит к этому адресу 3 и воткнет туда СС. Будьте готовы – авторам упаковщиков элементарно проверить каждую вызываемую функцию на наличие CC, а тогда ... Тогда можно еще использовать bpm или уходить на уровень NativeAPI. К примеру, для GetProcAddress цепочка выглядит так:

GetProcAddress -> LdrGetProcedureAddress -> LdrpGetProcedureAddress

Последняя основательно прокомментирована в статье о DLL-лоадере, рассмотренной нами в первой части.

Вместо этого используется более изощренная технология – прямое сканирование директории экспорта целевых dll. Более подробно об этом можно почитать в статье «Win32 Assembly Components» написанной LSD Team. Статью можно скачать с ссылка скрыта или с сайта команды –
t/projects.phpl#windowsassembly. Что мы можем сказать по этому поводу? М-м-м... Опять таки, если время сканирования экспорта опирается на rdtsc – тут тяжелее. В общем случае – почему бы, наконец, не раскачать Sten’a на написание нормальной bpr-команды, которую, к сожалению, убрали из Soft-Ice... Присутствие такой bpr-команды позволит ставить точки останова на большие диапазоны памяти – например, на диапазон директории экспорта, и отслеживать обращающиеся к этому диапазону инструкции. Команда должна быть контекстно-специфичной.

Теперь еще один вопрос. Когда надо дампить программу? Почему в подавляющем большинстве статеек по пакерам сказано, что нужно вводить образ в бесконечный цикл? Давайте проясним ситуацию.

Как ни странно, ответ на вопрос – когда нужно дампить программу, часто заключается в самой запакованной программе. Все зависит от того, насколько разумно она проектировалась программистом. Как известно, язык С позволяет создавать статические и глобальные переменные. Оставим спор по поводу необходимости их применения в стороне, а сами зададим вопрос: «а чем от этого плохо нам?». Рассмотрим следующий пример:

#include "windows.h"

static void* h_heap = 0; /*вот в этом вся и соль*/

void main(void)

{

if (!h_heap)

h_heap=HeapCreate(0,0x1000,0);

HeapAlloc(h_heap, HEAP_ZERO_MEMORY, 0x500);


MessageBox(0, "I said NOW!", "Dump me NOW!", MB_OK);


HeapDestroy(h_heap);

}

Программа запускается и работает абсолютно нормально, но стоит сдампить ее как раз на MessageBox – последствия не заставят себя ждать. Причина в том, что статические и глобальные переменные инициализируются КОМПИЛЯТОРОМ! Следовательно, при нормальном развитии событий переменная имеет свой нолик еще в секции PE-файла и проверка проходит нормально. В сдампленной программе переменная уже заполнена функцией HeapCreate и, следовательно, при следующем запуске будет нам с вами радость.

Так что, в общем случае, рекомендация должна звучать примерно так: секции данных лучше дампить сразу после раскриптовки/распаковки, ибо протектор может их подпортить еще до ОЕР. И это подводит нас к очень важному выводу: не надо полагаться на ImpRec, OEP-finder’ы, TRW с его makepe или что-либо еще – с каждым пакером пока приходится работать индивидуально, т.к. общее решение проблемы пока не разработано.