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

ОГЛАВЛЕНИЕ

Назначение статьи – показать, как вызвать функции другой программы через вашу собственную. Это руководство будет разбито на серию шагов с общим примером, сопровождаемым применением знаний к настоящей программе.

•    Скачать демо - 75.92 Кб
•    Скачать исходный код - 8.36 Кб

Необходимые инструменты

Эта статья использует отладчик (OllyDbg можно бесплатно скачать тут) и инжектор DLL Winject (бесплатно скачивается через интернет и включен в callbin.zip). Нужен компилятор C++ для компиляции кода для DLL. Компиляция тестового приложения и попытка работы над ним, скорее всего, дадут разные результаты из-за настроек компилятора, поэтому лучше всего использовать приложение, включенное в callbin.zip.
Зачем делать это?

Есть много причин для декомпиляции вызовов к внутренним функциям процессов. Например, дополнения или модификации для игр используют прием вызова внутренних функций процесса (игры), чтобы отобразить текст на экране. Возможно, надо расширить функционал горячих клавиш программы или же надо вызвать функцию “win” в игре. Так или иначе, данный прием применяется гораздо шире, чем указано здесь. Это руководство требует хорошего значения ассемблера x86 и некоторого знания Win32 API.

Написание тестового приложения

Сначала пишется приложение, затем оно декомпилируется, чтобы увидеть, как вызывается эта функция. Будет создано простое приложение, выполняющее арифметические действия и выводящее значения с помощью функции. Ниже приведено тестовое приложение:

#undef UNICODE

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

void mySecretFunction(int* param1, const char* param2,
                      DWORD param3, BYTE param4)
{
    printf("----------Function Entry----------\n");
    *param1 += 2008;
    printf("param1: %i\n", *param1);
    printf("param2: %s\n", param2);
    param3 *= *param1;
    printf("param3 (param3 *= *param1): %i\n", param3);
    param4 = 0x90;
    printf("param4: %i\n", param4);
    printf("----------Function Exit----------\n");
}
int main(void)
{
    int anArgument = 123;
   
    for(;;)
    {
    if(GetAsyncKeyState(VK_F11) & 1)
        mySecretFunction(&anArgument,
                         "This is the original text!",
                         123456, 4);
    else if(GetAsyncKeyState(VK_F1) & 1)
        break;
    }

    return 0;
}

Эта функция принимает четыре параметра (1 – по ссылке, 3 – по значению). Большое преимущество написания собственного приложения и его декомпиляции заключается в том, что вы уже знаете, что искать. Пытаясь вызвать функцию по адресу приложения, исходного кода которого у вас нет, вы должны быть готовы проделать большую лишнюю работу, так как это трудная задача. Будет декомпилирована отладочная сборка программы (включенная в callbin.zip), чтобы увидеть, что именно происходит по шагам. Почему отладочная сборка, а не выпускаемая сборка с выключенными оптимизациями? Это слегка облегчает анализ для отладчика, но не играет роли. Если посмотреть на выпускаемую сборку программы, в зависимости от оптимизаций компилятора, mySecretFunction(...) может быть встроена в main, и программа может выглядеть иначе, так как параметры могут храниться в статических местах. Плюс, так как функция может быть встроенной, попытка вызвать ее по адресу может быть тщетным поиском. Анализ начинается с открытия OllyDbg.


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

В окне 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, или аналогично.


Применение к реальному приложению

Далее рассмотрен практический пример, когда не известен исходный код, и приходится работать вслепую. Применяется простая игра Minesweeper(минный тральщик) в качестве реального примера, потому что она распространена на компьютерах, и с помощью указанного приема можно добиться чего-то действительно полезного. Практическим примером было бы большое приложение или игра с большим функционалом, где вызов их функций может дать прекрасные результаты. Но правомерность этого под вопросом, и нарушились бы правила отправки статей на CodeProject. Пора запустить игру Minesweeper и посмотреть, где можно применить этот прием. Окно:

Итак, где можно применить этот прием? Стоит сделать горячую клавишу, выигрывающую игру. Прежде чем начать, надо точно спланировать, что будет сделано. Цели следующие:

•    Найти функцию, которую использует Minesweeper, которая выигрывает игру.
•    Выяснить, какие параметры она принимает и как работает.
•    Написать DLL для инжекции и установить горячую клавишу для вызова этой функции.

Нахождение функции

Как найти эту таинственную функцию? Можно применить несколько способов разной сложности. Подход к этой задаче – посмотреть, как игра ведет себя, когда игрок выигрывает игру. Если звук включен – игра воспроизводит звук, когда игра выигрывается (или часы тикали, или попали в бомбу). С помощью этой информации можно выследить API, воспроизводящий звук, затем, двигаясь назад, можно натолкнуться на функцию, выполняемую, когда игра выигрывается. Итак, откройте игру в OllyDbg и нажмите F9 (Выполнить программу). Сделав это, нажмите Ctrl+A и позвольте OllyDbg еще анализировать код. Ищется API, воспроизводящий звук; поэтому щелкните правой кнопкой мыши по главному окну и перейдите на Искать -> Все межмодульные вызовы. Выведется весьма большой список, больше, чем список в программе-примере. Нажмите вкладку “Адресат”, чтобы отсортировать список в алфавитном порядке по имени API. Есть множество API, но что-то должно броситься в глаза.

PlaySoundW кажется интересным. Согласно MSDN, PlaySound API делает следующее:

Функция PlaySound воспроизводит звук, оговоренный заданным именем файла, ресурсом или системным событием. (Системное событие можно связать со звуком в реестре или в файле WIN.INI.)

BOOL PlaySound(
  LPCTSTR pszSound, 
  HMODULE hmod,    
  DWORD fdwSound   
);

Несомненно, это то, что надо, поэтому на всех трех из них ставится точка останова. Сделав это, надо вернуться к главному окну Minesweeper и щелкнуть по плитке, чтобы начать игру. Сразу после этого попадут на точку останова в:

01003937  |> FF15 68110001  CALL DWORD PTR DS:[<&WINMM.PlaySoundW>]  ;  WINMM.PlaySoundW

Если нажать F9, чтобы продолжить выполнение программы, будет видно, что этот конкретный API вызывается каждый раз, когда таймер увеличивается на 1. Если убрать точку останова и продолжить, будет видно, что этот API больше не вызывается при увеличении таймера. Однако он также не вызывается, когда игра выигрывается или проигрывается. Отсюда следует, что этот конкретный API отвечает за все три, и что, двигаясь назад, можно выяснить, что его вызывает.

Видно, что есть переходы на эту функцию из 0x01003913 и 0x01003925. На них ставятся точки останова, и смотрится, что происходит.

01003903  |. 68 05000400    PUSH 40005                   ;  Вариант 3 оператора выбора 010038FA
01003908  |. FF35 305B0001  PUSH DWORD PTR DS:[1005B30]  ;  WINMINE.01000000
0100390E  |. 68 B2010000    PUSH 1B2
01003913  |. EB 22          JMP SHORT WINMINE.01003937
01003915  |> 68 05000400    PUSH 40005                   ;  Вариант 2 оператора выбора 010038FA
0100391A  |. FF35 305B0001  PUSH DWORD PTR DS:[1005B30]  ;  WINMINE.01000000
01003920     68 B1010000    PUSH 1B1
01003925  |. EB 10          JMP SHORT WINMINE.01003937

Видно, что эти два сегмента помещают три параметра в стек и затем вызывают PlaySoundW. Параметр LPCTSTR pszSound звука тика равен 0x1B0. Здесь имеется 0x1B2 и 0x1B1. За счет логики можно угадать, что эти два соответствуют звуку взрыва и звуку победы. Но который из них? Это придется проверить. Чтобы выяснить, ставится точка останова на каждом из них. Обнаруживается, что если начать новую игру и случайно натолкнуться на бомбу, сработает точка останова в:

0100390E  |. 68 B2010000    PUSH 1B2

Если игра выигрывается –  0x1B1 помещается в стек. Итак, 0x1B2 является звуком взрыва, 0x1B1 является звуком победы, а 0x1B0 является звуком тика. Следовательно, критический кусок кода следующий:

01003915  |> 68 05000400    PUSH 40005                   ;  Вариант 2 оператора выбора 010038FA
0100391A  |. FF35 305B0001  PUSH DWORD PTR DS:[1005B30]  ;  WINMINE.01000000
01003920     68 B1010000    PUSH 1B1
01003925  |. EB 10          JMP SHORT WINMINE.01003937

Двигаясь назад вновь, можно найти, что вызывает это. Если щелкнуть по 0x01003915, в главном окне появится следующее:

 

Замечание: также можно щелкнуть правой кнопкой мыши по команде и перейти на Найти ссылки на -> Выбранная команда (Ctrl+R). Опять двигаясь назад к 0x010038FE и изучая, что находится вблизи него, можно натолкнуться на следующий блок кода, с началом всей этой функции в 0x010038ED:

010038ED  /$ 833D B8560001 >CMP DWORD PTR DS:[10056B8],3
010038F4  |. 75 47          JNZ SHORT WINMINE.0100393D
010038F6  |. 8B4424 04      MOV EAX,DWORD PTR SS:[ESP+4]
010038FA  |. 48             DEC EAX                      ;  Оператор выбора (варианты 1..3)
010038FB  |. 74 2A          JE SHORT WINMINE.01003927
010038FD  |. 48             DEC EAX
010038FE  |. 74 15          JE SHORT WINMINE.01003915
01003900  |. 48             DEC EAX
01003901  |. 75 3A          JNZ SHORT WINMINE.0100393D

Видно, что значение в 10056B8 сравнивается с 3, который является тремя вариантами оператора switch. Значение [ESP+4] перемещается в EAX, и затем видно, чем оно является и как предпринимает соответствующее действие. Еще не все, но уже близко. Надо вернуться к началу функции и посмотреть, что вызывает это. Выберите строку и нажмите Ctrl+R, чтобы увидеть, что вызывает это.

Надо поставить точку останова на всех трех и начать новую игру. Переход будет выполнен на это:

0100382B  |. E8 BD000000    CALL WINMINE.010038ED

Так как еще не начали играть (и даже не выйграл), нам надо убрать точку останова и двигаться дальше. Через пару секунды мы попадем на эту строку:

01003002  |. E8 E6080000    CALL WINMINE.010038ED

Это не то, что нужно. Удалось уменьшить только до одного.

010034CF  |. E8 19040000    CALL WINMINE.010038ED

Испытание показывает, что 0x010034CF вызывается, когда игра выигрывается или проигрывается. Надо провести различие и выяснить, что определяет выигрыш или проигрыш. Стоит посмотреть на следующую функцию целиком:

0100347C  /$ 8325 64510001 >AND DWORD PTR DS:[1005164],0
01003483  |. 56             PUSH ESI
01003484  |. 8B7424 08      MOV ESI,DWORD PTR SS:[ESP+8]
01003488  |. 33C0           XOR EAX,EAX
0100348A  |. 85F6           TEST ESI,ESI
0100348C  |. 0F95C0         SETNE AL
0100348F  |. 40             INC EAX
01003490  |. 40             INC EAX
01003491  |. 50             PUSH EAX
01003492  |. A3 60510001    MOV DWORD PTR DS:[1005160],EAX
01003497  |. E8 77F4FFFF    CALL WINMINE.01002913
0100349C  |. 33C0           XOR EAX,EAX
0100349E  |. 85F6           TEST ESI,ESI
010034A0  |. 0F95C0         SETNE AL
010034A3  |. 8D0485 0A00000>LEA EAX,DWORD PTR DS:[EAX*4+A]
010034AA  |. 50             PUSH EAX
010034AB  |. E8 D0FAFFFF    CALL WINMINE.01002F80
010034B0  |. 85F6           TEST ESI,ESI
010034B2  |. 74 11          JE SHORT WINMINE.010034C5               
010034B4  |. A1 94510001    MOV EAX,DWORD PTR DS:[1005194]
010034B9  |. 85C0           TEST EAX,EAX
010034BB  |. 74 08          JE SHORT WINMINE.010034C5
010034BD  |. F7D8           NEG EAX
010034BF  |. 50             PUSH EAX
010034C0  |. E8 A5FFFFFF    CALL WINMINE.0100346A
010034C5  |> 8BC6           MOV EAX,ESI                             
010034C7  |. F7D8           NEG EAX
010034C9  |. 1BC0           SBB EAX,EAX
010034CB  |. 83C0 03        ADD EAX,3
010034CE  |. 50             PUSH EAX
010034CF  |. E8 19040000    CALL WINMINE.010038ED                   
010034D4  |. 85F6           TEST ESI,ESI
010034D6  |. C705 00500001 >MOV DWORD PTR DS:[1005000],10
010034E0  |. 5E             POP ESI
010034E1  |. 74 2C          JE SHORT WINMINE.0100350F               
010034E3  |. 66:A1 A0560001 MOV AX,WORD PTR DS:[10056A0]
010034E9  |. 66:3D 0300     CMP AX,3
010034ED  |. 74 20          JE SHORT WINMINE.0100350F               
010034EF  |. 8B0D 9C570001  MOV ECX,DWORD PTR DS:[100579C]
010034F5  |. 0FB7C0         MOVZX EAX,AX
010034F8  |. 8D0485 CC56000>LEA EAX,DWORD PTR DS:[EAX*4+10056CC]
010034FF  |. 3B08           CMP ECX,DWORD PTR DS:[EAX]
01003501  |. 7D 0C          JGE SHORT WINMINE.0100350F
01003503  |. 8908           MOV DWORD PTR DS:[EAX],ECX
01003505  |. E8 77E6FFFF    CALL WINMINE.01001B81                   
0100350A  |. E8 9BE6FFFF    CALL WINMINE.01001BAA                  
0100350F  \> C2 0400        RETN 4

Надо поставить точку останова на каждой строке от начала функции до конца оператора возврата. Так как эта функция вызывается при выигрыше и при проигрыше, надо проверить, какие ее части пропускаются, когда возникают эти условия. После установки точки останова на всех этих строках и проигрыша игры выясняется, что пропускаются следующие строки.

010034B4  |. A1 94510001    MOV EAX,DWORD PTR DS:[1005194]
010034B9  |. 85C0           TEST EAX,EAX
010034BB  |. 74 08          JE SHORT WINMINE.010034C5
010034BD  |. F7D8           NEG EAX
010034BF  |. 50             PUSH EAX
010034C0  |. E8 A5FFFFFF    CALL WINMINE.0100346A

Также выясняется, что последним оператором функции, выполняющимся при проигрыше, является:

010034E1  |. 74 2C          JE SHORT WINMINE.0100350F

При выигрыше функция выполняется полностью.

Примечание: Если вы повторяете все действия в отладчике, и игра останавливается на этой строке:

010034ED  |. 74 20          JE SHORT WINMINE.0100350F

то причина в том, что идущие за ней строки проверяют, имеете ли вы право на таблицу рекордов.
Итак, чем является разница, определяющая переход? Доходит до этих строк:

010034B0  |. 85F6           TEST ESI,ESI
010034B2  |. 74 11          JE SHORT WINMINE.010034C5

Здесь проверяется, равен ли ESI 0, и если да –  совершается переход, пропускающий участок, имеющий дело с выигранной игрой. То есть, если ESI равен 0, то игра проиграна. Чтобы узнать, чему равно значение ESI при выигрыше, можно проверить его значение в OllyDbg в той строке.

Видно, что значение ESI при выигрыше равно 1. Где устанавливается ESI? Изучение функции показывает, что около начала есть такая строка:

01003484  |. 8B7424 08      MOV ESI,DWORD PTR SS:[ESP+8]

Первый параметр в этой функции перемещается в ESI. Почему ESP, а не EBP, как в оригинальном примере? Потому что эта функция не устанавливает кадр стека, как другая функция, поэтому к параметрам осуществляется доступ через ESP вместо EBP. Видно, что максимальное значение ESP равно +8 и что эта функция возвращает 4, значит, можно заключить, что она принимает только один параметр. Можно убедиться в этом, проверив ссылки на 0x0100347C и увидев только одну инструкцию PUSH перед вызовом. Достигнуто две из трех исходных целей, осталось только написать DLL, чтобы сделать горячую клавишу, вызывающую эту функцию с 1 в качестве параметра. Так как этот параметр перемещается в 32-битный регистр, можно задать ему тип DWORD в DLL (несмотря на DWORD PTR в дизассемблировании). Если нужно описание всей функции, ниже приведены замечания из пошаговой отладки и быстрого просмотра того, что происходит.

Написание новой DLL

Можно использовать шаблон DLL для оригинальной функции. Надо изменить лишь несколько тривиальных вещей. Новый код показан ниже:

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

DWORD WINAPI MyThread(LPVOID);
DWORD g_threadID;
HMODULE g_hModule;
void __stdcall CallFunction(void);

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)
{
    while(true)
    {
        if(GetAsyncKeyState(VK_F3) & 1) //Установить F3 в качестве горячей клавиши
        {
            CallFunction();
        }
        else if(GetAsyncKeyState(VK_F4) & 1)
            break;
    Sleep(100);
    }
    FreeLibraryAndExitThread(g_hModule, 0);
    return 0;
}

void __stdcall CallFunction(void)
{
    typedef void (__stdcall *pFunctionAddress)(DWORD);
    pFunctionAddress pWinFunction = (pFunctionAddress)(0x0100347C); //Новый адрес
    pWinFunction(1); //Вызов функции с 1, чтобы выиграть игру
}

Надо испытать сделанное. Запустите новый экземпляр Minesweeper и инжектируйте DLL. Щелкните по одной из плиток, чтобы начать игру, затем нажмите горячую клавишу F3. Будет результат, сходный со следующим.

Обратите внимание, что вся доска не открывается при выигрыше. Это оставлено читателю как упражнение.