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

ОГЛАВЛЕНИЕ

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

Далее рассмотрен практический пример, когда не известен исходный код, и приходится работать вслепую. Применяется простая игра 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. Будет результат, сходный со следующим.

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