Декомпиляция и вызов функции по адресу - Объяснение принципа работы

ОГЛАВЛЕНИЕ

Объяснение принципа работы

В окне OllyDbg нажмите F3 и найдите TutExample.exe. После его открытия появится следующее окно сообщения:

Можно просто нажать кнопку OK и продолжить; это предупреждение появляется из-за особенностей отладочной сборки (собрана в VS2008 IDE, для справки). Щелчок правой кнопкой мыши по главному окну и выбор Искать -> Все межмодульные вызовы выдаст новое окно:

Это окно показывает вызовы всех функций, которые делает исполняемая программа. Видно, что она вызывает printf из MSVCR90D.dll, GetAsyncKeyState из USER32.dll, и т.д. С помощью этого можно определить, где находятся main() и mySecretFunction(…). Следует начать с main() и увидеть, как вызывается mySecretFunction(…). Дважды щелкните по одной из функций GetAsyncKeyState –  и OllyDbg покажет адрес, где они вызываются. Щелкните на верхней строке –  и OllyDbg покажет следующее:

Так как есть исходный код, можно просто поставить точку останова (выделить строку и нажать F2) на GetAsyncKeyState, так как известно, что она контролирует, вызывается mySecretFunction(…) или нет. Так как не нужно заходить в функцию и смотреть, как USER32.dll реализует ее, ставится точка останова на первой функции GetAsyncKeyState, и продолжается установка точек останова на каждой строке включительно до второй функции GetAsyncKeyState. После завершения получится следующее:

Начинается анализ – нажмите F9 (Выполнить программу). OllyDbg должен сразу дойти до первой точки останова и подсветить строку. Давайте нажимать F2 построчно и смотреть, что программа делает без вмешательства. Она доходит до этой строки:

0041152A   74 1A            JE SHORT TutExamp.00411546

и совершает переход. Обратите внимание, что код клавиши для VK_F11 равен 0x7A, и он помещается в стек непосредственно перед вызовом GetAsyncKeyState. Теперь можно изучить, что эта функция делает, или чего не делает. Надо посмотреть, что делают пропущенные строки:

0041152C   6A 04            PUSH 4
0041152E   68 40E20100      PUSH 1E240
00411533   68 EC574100      PUSH TutExamp.004157EC  ; ASCII "Это оригинальный текст!"
00411538   8D45 F8          LEA EAX,DWORD PTR SS:[EBP-8]
0041153B   50               PUSH EAX
0041153C   E8 D8FAFFFF      CALL TutExamp.00411019
00411541   83C4 10          ADD ESP,10
00411544   EB 19            JMP SHORT TutExamp.0041155F

Видно, что три значения помещаются в стек, затем адрес одного загружается в регистр EAX, который затем помещается в стек. Далее вызывается некоторая функция, стек исправляется после возврата управления ею, и продолжается цикл. Это кажется знакомым и на самом деле является разобранной формой:

mySecretFunction(&anArgument, "This is the original text!", 123456, 4);

Четыре параметра помещаются в стек в обратном порядке, так как стек работает как LIFO (последним пришёл, первым вышел), и затем вызывается функция по адресу 0x00411019. Выделите строку вызова и нажмите Enter, чтобы дойти до функции. Отладчик дойдет до следующей строки:

00411019   E9 A2030000      JMP TutExamp.004113C0

это очередной переход на 0x004113C0. Оказывается, что функция  расположена не по адресу 0x00411019, а расположена по адресу 0x004113C0. Снова нажмите Enter на этой строке и перейдете к функции по этому адресу. Перейдя к 0x004113C0, вы увидите знакомые вещи, такие как вход функции и выходные инструкции printf. 0x004113C0 – адрес, который надо вызвать в программе, чтобы вызвать эту функцию с пользовательскими аргументами (прежде чем приступить к этому, надо хотя бы изучить, что происходит в функции, и увидеть, как она выглядит разобранная). Их будет легко отследить, так как они отделены вызовами printf. Также, смотря на исходный код, можно его прямо сопоставить с разобранным кодом.

Расшифровка параметров

Примечание: Те, кто знает ассемблер, могут пропустить эту часть, так как функции и объяснения весьма простые.

Прежде чем изучать это, стоит посмотреть на начало функции, где видно:

00413690   55               PUSH EBP
00413691   8BEC             MOV EBP,ESP
00413693   81EC C0000000    SUB ESP,0C0

Это обычно называется вводной частью и устанавливает кадр стека для переданных аргументов, локальных переменных, сохраненных регистров и так далее. В конце этой функции видна группа извлеченных из стека регистров:

0041376E   81C4 C0000000    ADD ESP,0C0
00413774   3BEC             CMP EBP,ESP
00413776   E8 CAD9FFFF      CALL TutExamp.00411145
0041377B   8BE5             MOV ESP,EBP
0041377D   5D               POP EBP

Это называется завершающим кодом и восстанавливает состояние стека, так что программа может нормально продолжить выполняться после завершения этой функции. Ко всем параметрам при этом будут обращаться как к чему-то, добавленному в EBP (указатель базы). Переменные, переданные функции в качестве параметров, являются [EBP+0x04*n], где n может быть 1, 2, 3, и т.д. [EBP+0x04] является местом, где адрес возврата находится в стеке, делая [EBP+0x08] первым параметром, [EBP+0x0C] вторым параметром, и так далее, всякий раз добавляя 0x04, чтобы получить следующий параметр. Аргументы являются [EBP+0x04*n], локальные переменные - [EBP-0x04*n]. Пора вернуться к изучению первой части, разыменовывающей param1 и прибавляющей 2008 (0x7D8) к нему. Важные части были объяснены, и в основном все понятно без объяснений, при условии знания мнемоник языка ассемблера. В тексте эти вещи подробно не анализируются, так как комментарии на картинке ясно описывают, что происходит. Начинается с param1, и далее идет до param4. Происходящее объясняется путем комментариев рядом с важными строками в дизассемблировании. Чтобы следовать за точками останова в отладчике, поставьте точку останова на этой строке и начните анализировать:

004113F5   8B45 08          MOV EAX,DWORD PTR SS:[EBP+8]

Почему пропускаются следующие строки:

004113EB   83C4 04          ADD ESP,4                             
004113EE   3BF4             CMP ESI,ESP                            
004113F0   E8 50FDFFFF      CALL TutExamp.00411145

Потому что эти три строки не нужны в анализе, они лишь производят очистку после вызова printf. Начнем со следующего:

*param1 += 2008;
printf("param1: %i\n", *param1);

printf("param2: %s\n", param2);

Ставится точка останова в 0x00411424:

param3 *= *param1;
printf("param3 (param3 *= *param1): %i\n", param3);

Ставится точка останова в 0x0041143D:

param4 = 0x90;
printf("param4: %i\n", param4);

Ставится точка останова в 0x00411464:

Продвижение

Теперь, когда все было изучено, и есть хорошее понимание, как все выглядит и работает, пора идти дальше. Надо как-то вызвать функции по конкретному адресу памяти в процессе. Нельзя вызвать ее из своего процесса, так как каждый процесс находится в своем собственном виртуальном пространстве. Допустим, что код скомпилирован, исполняемый файл запускается как secret.exe. Также допустим, что ничего неизвестно о виртуальной памяти, и было написано приложение с функцией, указывающей на 0x004113C0, при предположении, что это mySecretFunction(..). При попытке вызвать эту функцию не будут получены нужные результаты (из-за того что процесс находится в другом адресном пространстве). Следовательно, надо инжектировать DLL. Путем этого можно получить доступ к пространству памяти процесса secret.exe. Код этой DLL выглядел бы так.

Написание DLL

#include <span class="code-keyword"><windows.h></span>

DWORD WINAPI MyThread(LPVOID);
DWORD g_threadID;
HMODULE g_hModule;
void __stdcall CallFunction(int&, const char*, DWORD, BYTE);

INT APIENTRY DllMain(HMODULE hDLL, DWORD Reason, LPVOID Reserved)
{
    switch(Reason)
    {
    case DLL_PROCESS_ATTACH:
        g_hModule = hDLL;
        DisableThreadLibraryCalls(hDLL);
        CreateThread(NULL, NULL, &MyThread, NULL, NULL, &g_threadID);
    break;
    case DLL_THREAD_ATTACH:
    case DLL_PROCESS_DETACH:
    case DLL_THREAD_DETACH:
        break;
    }
    return TRUE;
}

DWORD WINAPI MyThread(LPVOID)
{
    int myInt = 1;
    while(true)
    {
        if(GetAsyncKeyState(VK_F2) & 1)
        {
            CallFunction(myInt, "My custom text";, 1, 1);
        }
        else if(GetAsyncKeyState(VK_F3) &1)
            break;
    Sleep(100);
    }
    FreeLibraryAndExitThread(g_hModule, 0);
    return 0;
}

void __stdcall CallFunction(int& param1, const char* param2, DWORD param3, BYTE param4)
{
    typedef void (__stdcall *pFunctionAddress)(int&, const char*, DWORD, BYTE);
    pFunctionAddress pMySecretFunction = (pFunctionAddress)(0x004113C0);
    pMySecretFunction(param1, param2, param3, param4);
}

Код весьма простой. Создается поток внутри процесса, в который была инжектирована эта DLL, и устанавливается цикл, ждущий нажатия клавиши. После нажатия F2 должна вызываться функция, в свою очередь вызывающая внутреннюю функцию процесса. Однако передаются собственные параметры вместо тех, которые жестко закодированы в оригинальный файл. Функция CallFunction(…) имеет точное такое же объявление, как и mySecretFunction(…), и в ней устанавливается указатель функции. Было выбрано соглашение о вызовах __stdcall, потому что его чаще всего использует компилятор C++. При необходимости можно было бы еще детальней изучить дизассемблированный исполняемый файл и посмотреть, как очищается стек, чтобы установить, используется ли другое соглашение о вызовах __cdecl. В исполняемом файле создается указатель функции на адрес 0x004113C0, являющийся адресом, ранее найденным для mySecretFunction(…). После его нахождения он вызывается с пользовательскими параметрами. Надо протестировать и увидеть. Запуск скомпилированного кода из начала руководства дает следующее:

Это выглядит нормально и является полностью ожидаемым поведением программы. Надо инжектировать DLL и посмотреть, что произойдет при нажатии F2.

Ясно видно, что что-то отличается. Но ожидалось ли это? Надо посмотреть на параметры и выяснить, какими они должны быть согласно коду mySecretFunction(…). Функции было передано 1, “My custom text”, 1, 1. 1 += 2008 = 2009 (правильно). “My custom text” было выведено (правильно). 1 *= 2009 = 2009 (правильно). Param4 установлен равным 0x90 (144 десятичное, правильно). Похоже, что все сработало прекрасно. Далее можно нажать F11, чтобы получить оригинальную функцию, или можно нажать F2 и получить пользовательскую функцию, но ясно видно, что  что-то отличается, и что можно решить вызвать эту функцию со своими собственными параметрами.

Передача правильных параметров

Крайне важно быть осторожным с типами данных. Следует всегда выполнять по шагам каждый кусок и смотреть, что он выводит. Если           как-то принять param2 за int, вместо const char*, получатся неожиданные результаты или даже аварийное завершение программы из-за проблем с памятью. Просмотр адреса относительно EBP покажет, что param2 хранит символы, тогда как param1, param3 и param4 хранят числа некоторого типа. Следует изучить несколько случаев, когда не обязательно объявляются такие же типы параметров, как и в оригинальной функции, и посмотреть, что произойдет. Необходимо изучить несколько вариантов, а именно:
•    Неиспользование const char* в качестве param2.
•    Использование разных числовых типов данных для param1/3/4.

Вначале первый случай. Ниже показано, какие строки были найдены и на что они были заменены при проходе сверху вниз кода DLL.

void __stdcall CallFunction(int&, int, DWORD, BYTE);
CallFunction(myInt, 12345, 1, 1);
void __stdcall CallFunction(int& param1, int param2, DWORD param3, BYTE param4)
typedef void (__stdcall *pFunctionAddress)(int&, int, DWORD, BYTE);

После внесения этих изменений, инжекции новой DLL в процесс, можно нажать F11 и увидеть знакомое сообщение. Однако при нажатии F2 программа аварийно завершается прямо в момент вывода param2. Нужно проявлять внимание и работать с правильными типами данных, когда речь идет о двух разных вещах, таких как const char* и числовой тип. Что произойдет, если изменить один из параметров? Завершится ли программа аварийно, если изменить третий параметр с DWORD на BYTE? Краткий ответ – нет, этого не случится (попробуйте сами). Однако из параметров получится неожиданный вывод, что вроде делает вызов функции бесполезным, если он не дает ожидаемого. Вопрос об отсутствии аварийного завершения программы не универсален; это скорее исключение, чем правило. Программы могут или не могут аварийно завершаться в зависимости от того, что делается с параметрами. Данная программа состоит из сложения, умножения и вывода; другие программы могут не так благосклонно принимать BYTE вместо DWORD, или аналогично.