В прошлый раз меня немного покритиковали за краткость, в этот раз попробую подробнее рассказывать.
Итак, продолжим помогать абстрактному сапёру бороться с минами. В этот раз я покажу, как не взорваться, даже если попали в мину.
Как обычно запускаем winmine.exe (из Windows XP) и отладчик WinDbg. Из отладчика аттачимся к процессу winmine.exe, для этого жмём F6 (или меню File-Attach to a Process…).
Если у вас не настроены отладочные символы, то при подключении к процессу winmine.exe в командном окне появится текст:
Symbol search path is: *** Invalid ***
Т.к. нам нужны отладочные символы, то нужно задать путь к ним. Это можно сделать с помощью команды .sympath (обязательно с точкой вначале, т.к. это мета-команда отладчика)
.sympath SRV*c:symbols*http://msdl.microsoft.com/download/symbols
Дальше нам нужно загрузить отладочные символы к себе в кеш (в данном случае в папку c:symbols). Следующая команда загружает файл с отладочной информацией только для winmine.exe (что позволяет сэкономить трафик):
.reload /f winmine.exe
Проверить, что отладочные символы загружены можно с помощью команды lm:
lm m winmine
Просмотреть отладочные символы можно с помощью команды x:
x winmine!*
В списке функций есть функция GameOver, что красноречиво говорит о её назначении.
0100347c winmine!GameOver = <no type information>
Давайте поставим точку останова на эту функцию и попробуем проиграть или выиграть. Точку останова (break point) можно задать с помощью команды bp:
bp winmine!GameOver
Посмотреть список точек останова можно с помощью команды bl (breakpoint list), а удалить с помощью команды bc (breakpoint clear)
0:001> bl
0 e 0100347c 0001 (0001) 0:**** winmine!GameOver
Дальше жмём F5 или вводим команду g для продолжения выполнения программы и начинаем играть. В первый раз я проиграю, а во второй раз выиграю. И посмотрю изменения в параметрах функции (если они конечно есть). После того как я проиграл, сработала точка останова и в командном окне появляется следующая информация:
Breakpoint 0 hit
eax=00000001 ebx=00000001 ecx=0006fcbc edx=77c49a94 esi=00000007 edi=00000000
eip=0100347c esp=0006fcf4 ebp=0006fd68 iopl=0 nv up ei pl zr na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246
winmine!GameOver:
0100347c 83256451000100 and dword ptr [winmine!fTimer (01005164)],0 ds:0023:01005164=00000001
здесь мы видим состояние регистров и следующую команду:
and winmine!fTimer,0
Уже видя эту команду можно решить, что мы в нужном месте, т.к. этой командой останавливается таймер, а таймер, скорее всего, два раза останавливать не будет. Следовательно, этот код выполняется как при проигрыше, так и при выигрыше и уже здесь решается, что показать на экране. Давайте это проверим, для этого посмотрим параметры переданные через стек. Для просмотра памяти существует несколько команд d*. Нам сейчас нужна команда dd (dump double-word values). dd esp – покажет нам адрес возврата и переданные в функцию параметры:
0:000> dd esp
0006fcf4 010035b0 00000000 00000000 00000200
0006fd04 0006fd68 00000001 010038b6 00000007
010035b0 в данном случае является адресом возврата, а следующее за ним занчение 00000000 – переданным параметром.
Теперь снова запущу программу на выполнение (F5) и попробую выиграть. Снова сработала точка остановки, и я ввожу команду просмотра стека dd esp
0:000> dd esp
0006fcf4 010035b0 00000001 00000000 00000200
0006fd04 0006fd68 00000001 010038b6 00000008
Мы видим, что адрес возврата (010035b0) тот же самый, а вот параметр изменился 00000001 вместо 00000000. Т.е. скорее всего это и есть параметр отвечающий за выигрыш/проигрыш. Можно это проверить, поменяем этот параметр на 0, запустим на выполнение и посмотрим что произойдёт:
0:000> eb esp + 4 0
0:000> dd esp
0006fcf4 010035b0 00000000 00000000 00000200
Проигрыш! Т.е. действительно от этого параметра всё и зависит. Следующие шаги наши будут такими: нужно сравнить найденный параметр с 0 и, если это так, то просто выйти из функции GameOver при этом сохранив целостность стека.
В данном случае мы воспользуемся обычной практикой перехвата функций. Т.е. мы сначала напишем свою функцию, при вызове GameOver передадим управление на неё, а затем она вернёт (или не вернёт) управление в функцию GameOver.
Прежде чем писать свою функцию нужно найти в приложении немного свободной памяти у которой атрибут защиты позволяет выполнять код. Проще всего не искать незадействованную память, а просто выделить память с уже необходимыми нам атрибутами защиты (PAGE_EXECUTE_READWRITE). Для выделения памяти в WinDbg есть команда .dvalloc (и команда .dvfree для освобождения памяти).
Т.к. в память выделяется блоками по 4 КБ, то нет смысла выделять меньше, хотя нам конечно не нужно так много памяти.
Выделяем память и запоминаем адрес:
0:001> .dvalloc 1000
Allocated 1000 bytes starting at 00910000
Если вдруг у вас в данный момент выполняется winmine то его в любой момент можно остановить с помощью клавиш Ctrl+Break (или меню Debug-Break).
Итак, у нас есть 4 КБ памяти начиная с адреса 0x00910000 (у вас этот адрес будет другим).
При проигрыше было бы неплохо показать сапёру о том, что он проиграл, но, благодаря нашим усилиям, выжил. Для этого покажем MessageBox с сообщением о том, что тут мина, а прежде чем показывать сообщение нам нужно в стек закинуть адрес нашей строки с сообщением.
Приступим, сначала разместим сроку в Unicode формате, для этого воспользуемся командой eu
eu 00910000 "Тут мина, будьте аккуратней!"
Проверим:
du 00910000
00910000 "Тут мина, будьте аккуратней!"
После этого, немного отступив, пишем код, т.к. в нашем распоряжении целых 4 килобайта можно смело отступить от строки байт так на 256 (0x100).
Код следующий:
cmp dword ptr [esp+4],0 ; сравниваем параметр с 0
jne 91011a ; если не равен 0 то "прыгаем" на выигрышный код
push 0; начинаем помещать параметры для MessageBox в стек, флаг uType
push 0 ; параметр lpCaption
push 910000 ; параметр lpText, указываем адрес по которому находится наша строка
push 0 ; параметр hWnd, хендл окна, передаём 0
call USER32!MessageBoxW ; вызываем функцию
ret 4 ; очищаем 4 байта из стека и передаём управлении функции вызвавшей GameOver
91011a: метка начала "выигрышного" кода
and dword ptr [winmine!fTimer],0 ; останавливаем таймер
lea eax,[winmine!GameOver+0x7] ; в eax заносим адрес в функции GameOver
jmp eax ; передаём управление по этому адресу
А в начале функции GameOver нам нужно будет написать:
jmp 910100
Есть пара нюансов: т.к. мы перезаписываем код оригинальной функции GameOver, то нам нужно будет повторить его в своей функции, что и делается командой
and dword ptr [winmine!fTimer],0 ; останавливаем таймер
но это ещё не всё, код "and dword ptr [winmine!fTimer],0" занимает 7 байт (83256451000100), в то время как замещающий код "jmp 910100" занимает 5 байт (e97fcc90ff). Поэтому нам нужно затереть два байта оригинальной функции ничего не делающими инструкциями (например, nop, nop, или mov edi, edi), что бы процессор при исполнении эти два байта просто проскочил. А иначе он два байта, оставшиеся от предыдущей команды, попробует исполнить, и тогда всё закончится ошибкой. Т.е. вначале GameOver нужно написать:
jmp 910100
nop
nop
Запись ассемблерных инструкций производится командой a (assemble), дизассемблирование командой u (unassemble).
Приступим (00910000 нужно заменить на ваш адрес):
a 00910000 + 100
cmp dword ptr [esp+4],0
jne 0x00910000 + 0x11a
push 0
push 0
push 0x910000
push 0
call user32!MessageBoxW
ret 4
and dword ptr [winmine!fTimer],0
lea eax, [winmine!GameOver+0x7]
jmp eax
Для выхода из режима ввода ассемблерный инструкций нажмите ещё раз Enter. Теперь меняем функцию GameOver:
a winmine!GameOver
jmp 0x910000 + 0x100
nop
nop
Нажмите Enter ещё раз, для выхода из режима редактирования. Теперь очистите все точки останова с помощью команды bc * и нажмите F5 для продолжения выполнения программы и попробуйте проиграть 🙂
В следующий раз покажу как получить Infinity Zoom в mspaint.