Передача управления на функцию обработки сообщений
Двум предыдущим способам "реанимации" приложений присущи серьезные ограничения и недостатки. При тяжелых разрушениях стека, вызванных атаками типа buffer overfull или же просто алгоритмическими ошибками, содержимое важнейших регистров процессора окажется искажено, и мы уже не сможем ни совершить откат (стек утерян), ни выйти из текущей функции (EIP "смотрит" в "космос"). В консольных приложениях в такой ситуации действительно очень мало, что можно сделать… Вот GUI –— другое дело! Концепция событийно ориентированной архитектуры наделяет всякое оконное приложение определенными серверными функциями. Даже если текущий контекст выполнения необратимо утерян, мы можем передать управление на цикл извлечения и диспетчеризации сообщений, заставляя программу продолжить обработку действий пользователя.
Классический цикл обработки сообщений выглядит так как это показано в листинге 3.14.:
Листинг 3.14. Классический цикл обработки сообщений
while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
Листинг 14 классический цикл обработки сообщений
Все, что нам нужно –— это передать управление на цикл while, даже не заботясь о настойке кадра стека, поскольку оптимизированные программы (а таковых большинство) адресуют свои локальные переменные не через EBP, а непосредственно через сам ESP. Конечно, при обращении к переменной msg, функция "угробит" содержимое стека, лежащее ниже его вершины, но это уже не важно.
Правда, при выходе из приложения оно "упадет" окончательно (ведь вместо адреса возврата из функции обработки сообщений, машинная команда RET обнаружит на вершине стека неизвестно что), но это произойдет после сохранения всех данных и потому никакой угрозы не несет. Исключение составляют приложения, "забывающие" закрыть все открытые файлы и перекладывающие эту работу на плечи функции ExitProcess.
Что ж! Можно так подправить адрес возврата, чтобы он указывал на функцию ExitProcess!
Давайте создадим простейшее Windows-приложение и поэкспериментирует с ним. Запустив
Microsoft Visual Studio выберем "New à Project à Win32 Application" и там –— "Typical Hello, World application". Добавим новый пункт меню, а в нем: char *p; *p = 0; и откомпилируем этот проект с отладочной информацией.
"Роняем приложение на пол" и, запустив отладчик, подгоняем мышь к первой строке цикла обработки сообщений и в появившемся контекстном меню находим пункт "Set Next Statement". Нажимаем клавишу <F5> для возобновления работы программы и… она действительно возобновляет свою работу!
А теперь откомпилируем наш проект в чистовом варианте (т. е. без отладочной информации) и попробуем реанимировать приложение в "голом" машинном коде. Пользуясь тем обстоятельством, что Windows –— это действительно многозадачная среда, в которой крушение одного процесса не мешает работе всех остальных, запустим свой любимый дизассемблер (например, IDA Pro) и проанализируем таблицу импорта отлаживаемой программы (вообще-то это может сделать и бесплатно распространяемый утилитой dumpbin, но его отчет не так нагляден).
Целью нашего поиска будут функции TranslateMessage/DispatchMessage и перекрестные ссылки, ведущие к циклу выборки сообщений (листинг 3.15).
Листинг 3.15. Поиск функций TranslateMessage/DispatchMessage в таблице импорта
.idata:004040E0 ; BOOL __stdcall TranslateMessage(const MSG *lpMsg)
.idata:004040E0 extrn TranslateMessage:dword ; DATA XREF: _WinMain@16+71^r
.idata:004040E0 ; _WinMain@16+8D^r
.idata:004040E4 ; LONG __stdcall DispatchMessageA(const MSG *lpMsg)
.idata:004040E4 extrn DispatchMessageA:dword ; DATA XREF: _WinMain@16+94^r
.idata:004040E8
Листинг 15 поиск функций TranslateMessage/DispatchMessage в таблице импорта
С функцией DispatchMessage связана всего лишь одна перекрестная ссылка, со всей очевидностью ведущая к искомому циклу обработки сообщений, дизассемблерный код которого выглядит так как показано в листинге 3.16.:
Листинг 3.16. Дизассемблерный листинг функции обработки сообщений
.text:00401050 mov edi, ds:GetMessageA
.text:00401050 ; первый вызов GetMessageA (это еще не цикл, это только его преддверье)
.text:00401050
.text:00401056 push 0 ; wMsgFilterMax
.text:00401058 push 0 ; wMsgFilterMin
.text:0040105A lea ecx, [esp+2Ch+Msg]
.text:0040105A ; ECX указывает на область памяти, через которую GetMessageA
.text:0040105A ; станет возвращать сообщение. текущее значение ESP может быть
.text:0040105A ; любым, главное, чтобы оно указывало на действительную область
.text:0040105A ; памяти (см. карту памяти, если значение ESP оказалось искажено
.text:0040105A ; настолько, что вывело его в "космос")
.text:0040105A ;
.text:0040105E push 0 ; hWnd
.text:00401060 push ecx ; lpMsg
.text:00401061 mov esi, eax
.text:00401063 call edi ; GetMessageA
.text:00401063 ; вызываем GetMessageA
.text:00401063
.text:00401065 test eax, eax
.text:00401067 jz short loc_4010AD
.text:00401067 ; проверка на наличие необработанных сообщений в очереди
.text:00401067
…
.text:00401077 loc_401077: ; CODE XREF: _WinMain@16+A9vj
.text:00401077 ; начало цикла обработки сообщений
.text:00401077
.text:00401077 mov eax, [esp+2Ch+Msg.hwnd]
.text:0040107B lea edx, [esp+2Ch+Msg]
.text:0040107B ; EDX указывает на область памяти, используемую для передачи сообщений
.text:0040107B
.text:0040107F push edx ; lpMsg
.text:00401080 push esi ; hAccTable
.text:00401081 push eax ; hWnd
.text:00401082 call ebx ; TranslateAcceleratorA
.text:00401082 ; вызываем функцию TranslateAcceleratorA
.text:00401082
.text:00401084 test eax, eax
.text:00401086 jnz short loc_40109A
.text:00401086 ; проверка на наличие в очереди необработанных сообщений
.text:00401086
.text:00401088 lea ecx, [esp+2Ch+Msg]
.text:0040108C push ecx ; lpMsg
.text:0040108D call ebp ; TranslateMessage
.text:0040108D ; вызываем функцию TranslateMessage, если есть что транслировать
.text:0040108D
.text:0040108F lea edx, [esp+2Ch+Msg]
.text:00401093 push edx ; lpMsg
.text:00401094 call ds:DispatchMessageA
.text:00401094 ; диспетчеризуем сообщение
.text:0040109A
.text:0040109A loc_40109A: ; CODE XREF: _WinMain@16+86^j
.text:0040109A push 0 ; wMsgFilterMax
.text:0040109C push 0 ; wMsgFilterMin
.text:0040109E lea eax, [esp+34h+Msg]
.text:004010A2 push 0 ; hWnd
.text:004010A4 push eax ; lpMsg
.text:004010A5 call edi ; GetMessageA
.text:004010A5 ; читаем очередное сообщений из очереди
.text:004010A5
.text:004010A7 test eax, eax
.text:004010A9 jnz short loc_401077
.text:004010A9 ; вращаем цикл обработки сообщений
.text:004010A9
.text:004010AB pop ebp
.text:004010AC pop ebx
.text:004010AD
.text:004010AD loc_4010AD: ; CODE XREF: _WinMain@16+67^j
.text:004010AD mov eax, [esp+24h+Msg.wParam]
.text:004010B1 pop edi
.text:004010B2 pop esi
.text:004010B3 add esp, 1Ch
.text:004010B6 retn 10h
.text:004010B6 _WinMain@16 endp
Листинг 16 дизассемблерный листинг функции обработки сообщений
Мы видим, что цикл обработки сообщений начинается с адреса 401050h и именно на этот адрес следует передать управление, чтобы возобновить работу "упавшей" программы. Пробуем сделать это и… программа работает!
Разумеется, настоящее приложение оживить намного сложнее, поскольку цикл обработки сообщений в нем рассредоточен по большому количеству функций, отождествить которые при беглом дизассемблировании невозможно. Тем не менее, приложения, построенные на основе общедоступных библиотек, (например, MFC (Microsoft Foundation Classes), OWVL (Object Windows Library),) обладают вполне предсказуемой архитектурой и реанимировать их вполне возможно.
Рассмотрим, как устроен цикл обработки сообщений в MFC. Большую часть своего времени исполнения MFC-приложения проводят внутри функции CWinThread::Run(void), которая периодически опрашивает очередь на предмет поступления свежих сообщений и рассылает их соответствующим обработчикам. Если один из обработчиков "споткнулся" и довел систему до критической ошибки, выполнение программы может быть продолжено в функции Run. В этом-то и заключается ее главная прелесть!
Функция не имеет явных аргументов, но принимает скрытый аргумент this, указывающей на экземпляр класса CWinThread или производный от него класс, без которого функция просто не сможет работать.
К счастью, таблицы виртуальных методов класса CWinThread содержат достаточно количество "родимых пятен", чтобы указатель this можно было воссоздать вручную.
Загрузим функцию Run в дизассемблер и отметим все обращения к таблице виртуальных методов, адресуемой посредствамчерез регистра ECX (листинг 3.17).
Листинг 3.17. Дизассемблерный листинг функции Run (фрагмент)
.text:6C29919D n2k_Trasnlate_main: ; CODE XREF: MFC42_5715+1F^j
.text:6C29919D ; MFC42_5715+67vj ...
.text:6C29919D mov eax, [esi]
.text:6C29919F mov ecx, esi
.text:6C2991A1 call dword ptr [eax+64h] ; CWinThread::PumpMessage(void)
.text:6C2991A4 test eax, eax
.text:6C2991A6 jz short loc_6C2991DA
.text:6C2991A8 mov eax, [esi]
.text:6C2991AA lea ebp, [esi+34h]
.text:6C2991AD push ebp
.text:6C2991AE mov ecx, esi
.text:6C2991B0 call dword ptr [eax+6Ch] ; CWinThread::IsIdleMessage(MSG*)
.text:6C2991B3 test eax, eax
.text:6C2991B5 jz short loc_6C2991BE
.text:6C2991B7 push 1
.text:6C2991B9 mov [esp+14h], ebx
.text:6C2991BD pop edi
.text:6C2991BE
.text:6C2991BE loc_6C2991BE: ; CODE XREF: MFC42_5715+51^j
.text:6C2991BE push ebx ; wRemoveMsg
.text:6C2991BF push ebx ; wMsgFilterMax
.text:6C2991C0 push ebx ; wMsgFilterMin
.text:6C2991C1 push ebx ; hWnd
.text:6C2991C2 push ebp ; lpMsg
.text:6C2991C3 call ds:PeekMessageA
.text:6C2991C9 test eax, eax
.text:6C2991CB jnz short n2k_Trasnlate_main
.text:6C2991CD
Листинг 17 дизассемблерный листинг функции Run (фрагмент)
Таким образом, функция Run ожидает получить указатель на двойное слово, указывающее на таблицу виртуальных методов, 0x19 и 0x1B элементы которой представляют собой функции PumpMessage и IsIdleMessage соответственно (или переходники к ним). Адреса импортируемых функций, если только динамическая библиотека не была перемещена, можно узнать в том же дизассемблере; в противном случае, следует отталкиваться от базового адреса модуля, отображаемого отладчиком по команде "Modules". При условии, что эти две функции не были перекрыты программистом, поиск нужной нам виртуальной таблицы не составит никакого труда.
По непонятным причинам библиотека MFC42.DLL не экспортирует символьных имен функций и эту информацию нам приходится добывать самостоятельно. Обработав библиотеку MFC42.LIB утилитой dumpbin, запущенной с ключом "/ARCH", мы определим ординалы [Y92] [n2k93] обеих функций (ординал PumpMessage –— 5307, а IsIdleMessage –— 4079). Остается найти эти значения в экспорте библиотеки MFC42.DLL (dumpbin /EXPORTS mfc42.dll > mfc42.txt), из чего мы узнаем что адрес функции PumpMessage: 6C291194h, а IsIdleMessage –— 6С292583h.
Теперь мы должны найти указатели на функции PumpMessage/IsIdleMessage в памяти, а точнее –— в секции данных, базовый адрес которой содержится в заголовке PE-файла, только помните, что в процессорах x86 наименее значимый байт располагается по меньшему адресу, т. е. все числа записываются в обратном порядке. К сожалению, отладчик Microsoft Visual Studio Debugger не поддерживает операцию поиска в памяти, и нам приходится действовать обходным путем –— копировать содержимое дампа в буфер обмена, вставлять его в текстовой файл и, нажав клавишу <F7> искать адреса уже там.
Долго ли, коротко ли, но интересующие нас указатели обнаруживаются по адресам 403044h/40304Сh (естественно, у вас эти адреса могут быть и другими).
Причем обратите внимание: расстояние между указателями в точности равно расстоянию между указателями на [EAX + 64h] и [EAX + 6Ch], а очередность их размещения в памяти обратна порядку объявления виртуальных методов. Это –— хороший признак и мы, скорее всего, находимся на правильном пути (листинг 3.18).
Листинг 3.18. Адреса функций IsIdleMessage/PumpMessage, найденные в секции данных
00403044 6C2911D4 6C292583 6C291194 ; IsIdleMessage/PumpMessage
00403050 6C2913D0 6C299144 6C297129
0040305C 6C297129 6C297129 6C291A47
Листинг 18 адреса функций IsIdleMessage/PumpMessage, найденные в секции данных
Указатели, указывающие на адреса 403048h/40304Ch, очевидно, и будут кандидатами в члены искомой таблицы виртуальных методов класса CWinThread. Расширив сферу поиска всем адресным пространством отлаживаемого процесса, мы обнаруживаем два следующих переходника (листинг 3.19).
Листинг 3.19. Переходники к функциям IsIdleMessage/PumpMessage, найденные там же
00401A20 jmp dword ptr ds:[403044h] ; IsIdleMessage
00401A26 jmp dword ptr ds:[403048h] ;
00401A2C jmp dword ptr ds:[40304Ch] ; PumpMessage
Листинг 19 переходники к функциям IsIdleMessage/PumpMessage, найденные там же
Ага, уже теплее! Мы нашли не сами виртуальные функции, но переходники к ним. Раскручивая этот запутанный клубок, попробуем отыскать ссылки на 401A26h/401A2Ch, которые передают управление на приведенный ранее код (листинг 3.20).
Листинг 3.20. Виртуальная таблица класса CWinThread
00403490 00401A9E 00401040 004015F0 ß 0x0, 0x1, 0x2 элементы
0040349C 00401390 004015F0 00401A98 ß 0x3, 0x4, 0x5 элементы
004034A8 00401A92 00401A8C 00401A86 ß 0x6, 0x7, 0x8 элементы
004034B4 00401A80 00401A7A 00401A74 ß 0x9, 0xA, 0xB элементы
004034C0 00401010 00401A6E 00401A68 ß 0xC, 0xD, 0xE элементы
004034CC 00401A62 00401A5C 00401A56 ß 0xF, 0x10, 0x11 элементы
004034D8 00401A50 00401A4A 00401A44 ß 0x12, 0x13, 0x14 элементы
004034E4 00401A3E 004010B0 00401A38 ß 0x15, 0x16, 0x17 элементы
004034F0 00401A32 00401A2C 00401A26 ß 0x18, 0x19, 0x1A элементы (PumpMessage)
004034FC 00401A20 00401A1A 00401A14 ß 0x1B, 0x1C, 0x1D элементы (IsIdleMessage)
Листинг 20 виртуальная таблица класса CWinThread
Даже неопытный исследователь программ распознает в этой структуре данных таблицу виртуальных функций. Указатели на переходники к функциям PumpMessage/IsIdleMessage разделяются ровно одним элементом, как того и требуют условия задачи. Предположим, что эта виртуальная таблица, которая нам и нужна. Для проверки этого предположения отсчитаем 0x19 (25) элементов верх от 4034F4h и попытаемся найти указатель, ссылающийся на ее начало. Если повезет и он окажется экземпляром класса CwinThread, тогда программа сможет корректно продолжить свою работу (листинг 3.21).
Листинг 3.21. Экземпляр класса CWinThread, вручную найденный нами в памяти
004050B8 00403490 00000001 00000000
004050C4 00000000 00000000 00000001
Листинг 21 экземпляр класса CWinThread, вручную найденный нами в памяти
Действительно, в памяти обнаруживается нечто похожее. Записываем в регистр ECX значение 4050B8h, находим в памяти функцию Run (как уже говорилось, если только она не была перекрыта, ее адрес –— 6C299164h –— известен). Нажимаем комбинацию клавиш <Ctrl>+<G>, затем вводим "0x6C299164" и в контекстном меню, вызванном правой клавишей мыши, выбираем Set Next Statement. Программа, отделавшись легким "испугом", продолжает свое исполнение, ну а мы на радостях идем пить пиво (кофе, квас, чай –— по вкусу).
Аналогичным путем можно вернуть к жизни и зависшие приложения, потерявшие нить управления и не реагирующие ни на мышь, ни на клавиатуру.