Идентификация стартовых функций
…чтобы не наделать ошибок в работе, богу понадобился свет. Судя по этому, в предшествовавшие века он сидел в полной темноте. К счастью, он не рисковал обо что-либо стукнуться, ибо вокруг ничего не было.
Лео Таксиль "Забавная Библия"
Если первого встречного программиста спросить "С какой функции начинается выполнение Windows-программы?", вероятнее всего мы услышим в ответ "С WinMain" и это будет ошибкой. На самом же деле, первым управление получает стартовый код, скрыто вставляемый компилятором, – выполнив необходимые инициализационные процедуры, в какой-то момент он вызывает WinMain, а после ее завершения вновь получает управление и выполняет "капитальную" деинициализацию.
В подавляющем большинстве случаев стартовый код не представляет никакого интереса и первой задачей анализирующего становится поиск функции WinMain. Если компилятор входит в число "знакомых" IDA, она опознает WinMain автоматически, в противном же случае это приходится делать руками и головой. Обычно в штатную поставку компилятора входят исходные тексты его библиотек, в том числе и процедуры стартового кода. Например, у Microsoft Visual C++ стартовый код расположен в файлах "CRT\STC\CRT0.C" – версия для статичной компоновки, "CRT\SRC\CRTEXE.C" – версия для динамичной компоновки (т.е. библиотечный код не пристыкуется к файлу, а вызывается из DLL), "CRT\SRC\wincmdln.c" – версия для консольных приложений. У Borland C++ все файлы со start-up кодом хранятся в отдельной одноименной директории, в частности, стартовый код для Windows-приложений содержится в файле "c0w.asm". Разобравшись с исходными текстами, понять дизассемблерный листинг будет намного легче!
А как быть, если для компиляции исследуемой программы использовался неизвестный или недоступный вам компилятор? Прежде, чем приступать к утомительному ручному анализу, давайте вспомним: какой прототип имеет функция WinMain:
int WINAPI WinMain(
HINSTANCE hInstance, // handle to current instance
HINSTANCE hPrevInstance, // handle to previous instance
LPSTR lpCmdLine, // pointer to command line
int nCmdShow // show state of window
);
Во-первых, четыре аргумента (см. "Идентификация аргументов функций") – это достаточно много и в большинстве случаев WinMain
оказывается самой "богатой" на аргументы функцией стартового кода. Во-вторых, последний заносимый в стек аргумент – hInstance – чаще всего вычисляется "на лету" вызовом GetModuleHandleA, - т.е. встретив конструкцию типа "CALL GetModuleHandleA" можно с высокой степенью уверенности утверждать, что следующая функция – и есть WinMain. Наконец, вызов WinMain обычно расположен практически в самом конце кода стартовой функции. За ней бывает не более двух-трех "замыкающих" строй функций таких как "exit" и "XcptFilter".
Рассмотрим следующий фрагмент кода. Сразу бросается в глаза множество инструкций PUSH, заталкивающих в стек аргументы, последний из которых передает результат завершения GetModuleHandleA. Значит, перед нами ни что иное, как вызов WinMain (и IDA подтверждает, что это именно так):
.text:00401804 push eax
.text:00401805 push esi
.text:00401806 push ebx
.text:00401807 push ebx
.text:00401808 call ds:GetModuleHandleA
.text:0040180E push eax
.text:0040180F call _WinMain@16
.text:00401814 mov [ebp+var_68], eax
.text:00401817 push eax
.text:00401818 call ds:exit
Листинг 21 Идентификация функции WinMain по роду и количеству передаваемых ей аргументов
Но не всегда все так просто, - многие разработчики, пользуясь наличием исходных текстов start-up кода, модифицируют его (под час весьма значительно). В результате – выполнение программы может начинаться не с WinMain, а любой другой функции, к тому же теперь стартовый код может содержать критические для понимания алгоритма программы операции (например, расшифровщик основного кода)! Поэтому, всегда
хотя бы мельком следует изучить start-up код – не содержит ли он чего-нибудь необычного?
Аналогичным образом обстоят дела и с динамическими библиотеками – их выполнение начинается вовсе не с функции DllMain (если она, конечно, вообще присутствует в DLL), а с __DllMainCRTStartup (по умолчанию). Впрочем, разработчики под час изменяют умолчания, назначая ключом "/ENTRY" ту стартовую функцию, которая им нужна. Строго говоря, неправильно называть DllMain стартовой функций – она вызывается не только при загрузке DLL, но так же и при выгрузке, и при создании/уничтожении подключившим ее процессором нового потока. Получая уведомления об этих событиях, разработчик может предпринимать некоторые действия (например, подготавливать код к работе в многопоточной среде). Весьма актуален вопрос – имеет ли все это значение для анализа программы? Ведь чаще всего требуется проанализировать не всю динамическую библиотеку целиком, а исследовать работу некоторых экспортируемых ею функций. Если DllMain выполняет какие-то действия, скажем, инициализирует переменные, то остальные функции, на которых распространяется влияние этих переменных, будут содержать на них прямые ссылки, ведущие прямиком к DllMain. Таким образом, не стоит вручную искать DllMain, - она сама себя обнаружит! Хорошо, если бы всегда это было так! Но жизнь сложнее всяких правил. Вдруг в DllMain находится некий деструктивный код или библиотека помимо основной своей деятельности шпионит за потоками, отслеживая их появление? Тогда без непосредственного анализа ее кода не обойтись!
Обнаружить DllMain на порядок труднее, чем WinMain, если ее не найдет IDA – пиши пропало. Во-первых, прототип DllMain достаточно незамысловат и не содержит ничего характерного:
BOOL WINAPI DllMain(
HINSTANCE hinstDLL, // handle to DLL module
DWORD fdwReason, // reason for calling function
LPVOID lpvReserved // reserved
);
А, во-вторых, ее вызов идет из самой гущи довольно внушительной функции __DllMainCRTStartup
и быстро убедиться, что это именно тот CALL, который нам нужен – нет никакой возможности. Впрочем, некоторые зацепки все-таки есть. Так, при неудачной инициализации DllMain возвращает FALSE, и код __DllMainCRTStartup обязательно проверит это значение, в случае чего прыгая аж к концу функции. Подробных ветвлений в теле стартовой функции не так уж много и обычно только одно из них связано с функций, принимающей три аргумента.
.text:1000121C push edi
.text:1000121D push esi
.text:1000121E push ebx
.text:1000121F call _DllMain@12
.text:10001224 cmp esi, 1
.text:10001227 mov [ebp+arg_4], eax
.text:1000122A jnz short loc_0_10001238
.text:1000122C test eax, eax
.text:1000122E jnz short loc_0_10001267
Листинг 22 Идентификация DllMain по коду неудачной инициализации
Прокрутив экран немного вверх, нетрудно убедиться, что регистры EDI, ESI и EBX содержат lpvReserved, fdwReason и hinstDLL соответственно. А значит, перед нами и есть функция DllMain (Для справки, исходный текст __DllMainCRTStartup содержится в файле "dllcrt0.c", который настоятельно рекомендуется изучить).
Наконец, мы добрались и до функции main
консольных Windows-приложений. Как всегда, выполнение программы начинается не с нее, а c функции mainCRTStartup, инициализирующей кучу, систему ввода-вывода, подготавливающую аргументы командной строки и только потом предающей управление main. Функция main
принимает всего два аргумента: "int main(int argc, char **argv)" – этого слишком мало, чтобы выделить ее среди остальных. Однако приходит на помощь тот факт, что ключи командной строки доступны не только через аргументы, но и через глобальные переменные – __argc
и __argv
соответственно. Поэтому, вызов main обычно выглядит так:
.text:00401293 push dword_0_407D14
.text:00401299 push dword_0_407D10
.text:0040129F call _main
.text:0040129F ; Смотрите: оба аргумента функции – указатели на глобальные переменные
.text:0040129F ; (см. "Идентификация глобальных переменных")
.text:0040129F
.text:004012A4 add esp, 0Ch
.text:004012A7 mov [ebp+var_1C], eax
.text:004012AA push eax
.text:004012AA ; Смотрите: возвращаемое функцией знаечние, передается функции exit
.text:004012AA ; как код завершения процесса
.text:004012AA ; Значит, это и main и есть!
.text:004012AA
.text:004012AB call _exit
Листинг 23 Идентификация main
Обратите внимание и на то, что результат завершения main
передается следующей за ней функции (это, как правило, библиотечная функция exit).
Вот мы и разобрались с идентификацией основных типов стартовых функций. Конечно, в жизни бывает не все так просто, как в теории, но в любом случае, описанные выше приемы заметно упростят анализ.
__дописать идентификацию стартовых функций FreePascal, Fortran….