Техника защиты компакт-дисков от копирования

Передача управления на функцию обработки сообщений


Двум предыдущим способам "реанимации" приложений присущи серьезные ограничения и недостатки. При тяжелых разрушениях стека, вызванных атаками типа 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. Программа, отделавшись легким "испугом", продолжает свое исполнение, ну а мы на радостях идем пить пиво (кофе, квас, чай –— по вкусу).

Аналогичным путем можно вернуть к жизни и зависшие приложения, потерявшие нить управления и не реагирующие ни на мышь, ни на клавиатуру.


Содержание раздела