Фундаментальные основы хакерства

       

Идентификация стартовых функций


…чтобы не наделать ошибок в  работе, богу понадобился свет. Судя по этому, в предшествовавшие века он сидел в полной  темноте. К счастью, он не рисковал обо что-либо стукнуться, ибо вокруг ничего не было.

Лео Таксиль "Забавная Библия"

Если первого встречного программиста спросить "С какой функции начинается выполнение 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….


Содержание раздела