Издательский дом ООО "Гейм Лэнд"ЖУРНАЛ ХАКЕР #134, ФЕВРАЛЬ 2010 г.

Усложняем анализ кода с помощью векторной обработки исключений

r0064, r0064@mail.ru


Наверное, тебе уже доводилось читать об обработке исключений в x64. А если нет, то поясню, что механизм структурной обработки исключений изменили до неузнаваемости. Если быть точнее, его сделали статическим. Никакие манипуляции с сегментным регистром fs уже не помогут. Но решения есть. Пусть они пока не описаны, зато проверены теорией и практикой. Позволь довести их до тебя в этой небольшой статье.

Не SEH единым

Могущественный и вездесущий в 32-разрядной системе регистр [fs] остался фактически не у дел: часть его функций забрал на себя сегментный регистр gs (например, указатель на TEB в ring-3 ныне лежит по адресу gs:30h, на peb - gs:60h). С каждой процедурой отныне связан свой обработчик, эта связь закрепляется в EXCEPTION_DIRECTORY PE-файла. Ну да, адрес обработчика теперь в стеке не хранится, fs использовать не можем, то есть SEH теряет свою привлекательность для применения в защите. То ли дело раньше - регистрация SEH «на лету», какие были времена...

Но SEH ли единым, как говорится. Один мощный механизм почему-то остался не упомянутым в контексте 64-битных ОС Windows. Векторная обработка исключений (она же VEH). О ней ты знаешь по опыту работы с 32-разрядными Windows (один только Крис не раз писал об этой вопиющей теме). Если ты не в курсе, или немного выбился из него, прочитай статью Мэта Питрека, посвященную 32-битному VEH - wasm.ru/article.php?article=veh.

Теория метода

Подобно SEH, VEH позволит нам получать управление при возникновении эксепшена в программе: достаточно зарегистрировать свой обработчик (он же хендлер) функцией из ntdll RtlAddVectoredExceptionHandler.

RtlAddVectoredExceptionHandler(ULONG FirstHandler,
PVECTORED_EXCEPTION_HANDLER VectoredHandler )

Большое преимущество векторных обработчиков в том, что они выполняются до SEH. Кто не верит, может посмотреть функцию RtlDispatchException в ntdll — что вызывается первым? Внутренняя функция RtlpCallVectoredHandlers, а потом уже идет работа со структурными обработчиками.

А теперь о самом интересном! Как нам, собственно, подменить существующий обработчик? Скажем, в случае, если мы по какой-то причине не хотим ставить новый вызовом RtlAddVectoredExceptionHandler. Ведь чертовски палевно вызывать функцию каждый раз, когда хочешь зарегистрировать обработчик исключений.

Неэкспортируемая переменная в ntdll RtlpCalloutEntryList хранит указатель на список зарегистрированных обработчиков (в win xp, в vista список по-другому называется). Каждый элемент списка представлен некой структурой. Для удобства назовем ее (по аналогии со статьей Питрека) - VECTORED_EXCEPTION_NODE64. Ниже приводится частичный листинг RtlAddVectoredExceptionHandler в windows XP x64:

public RtlAddVectoredExceptionHandler
RtlAddVectoredExceptionHandler proc near
lea r8, RtlpCalloutEntryList
jmp short RtlpAddVectoredHandler
RtlAddVectoredExceptionHandler endp
...
lea r8d, [rdx+20h]
mov rcx, [rcx+30h]
call RtlAllocateHeap ; выделяем память для узла в куче
test rax, rax
mov rdi, rax ; сохраняем указатель на выделенный блок памяти в rdi
...

Как видно из дизассемблерного листинга, по смещению 18h (запомни его!) в структуре, содержащей инфу об обработчике, хранится его шифрованный адрес.
Вид VECTORED_EXCEPTION_NODE64, полученный по имеющимся данным:

struct _VECTORED_EXCEPTION_NODE64
{
ULONG64 m_pNextNode;
ULONG64 m_pPreviousNode;
ULONG64 unknwn;
PVOID64 m_pfnVectoredHandler;
}

Сама структура, в целом, немногим отличается от той, что дана в статье Питрека (о 32-битном VEH). Обратимся снова к анализу RtlpAddVectoredHandler. На самом деле вызов NtQueryInformationProcess и последующий ксор адреса обработчика здесь не что иное, как функция RtlEncodePointer, вставленная в код оптимизирующим компилятором.

А теперь о замене адресов обработчиков «на лету». Это можно сделать потому, что RtlAddVectoredExceptionHandler возвращает указатель на выделенную структуру VECTORED_EXCEPTION_NODE64. А значит, чтобы подменить хендлер динамически, нужно:

  1. зашифровать свой новый обработчик с помощью функции RtlEncodePointer (адреса ведь не хранятся в открытом виде);
  2. записать шифрованный RtlEncodePointer-указатель по смещению 18h в VECTORED_EXCEPTION_NODE64. Смещение, надо сказать, не поменялось вплоть до Висты, так что юзаем с чистой совестью.

Coding

Попробуем применить прелести VEH на практике. Писать будем в shellcode-стиле, как и полагается. Для начала, опишу алгоритм. Потребуется:

  1. Поиск базы загрузки ntdll.dll.
  2. Нахождение функций RtlAddVectoredExceptionHandler, RtlEncodePointer.
  3. Регистрация своего обработчика, сохранение указателя на область памяти, где содержится обработчик.
  4. Подмена адреса обработчика.

Базу загрузки ntdll ищем через PEB:

....
mov rcx,gs:[60h] ; указатель на Peb в x64 располагается по новому адресу
mov rcx,[rcx+18h] ; PEB_LDR_DATA
mov rcx,[rcx+10h] ; PEB_LDR_DATA.InLoadOrderModuleList
mov rcx,[rcx]
mov rbx,[rcx+30h] ; ntdll.dll base
....

А поиск необходимых функций выполняем через разбор таблицы экспорта ntdll. Здесь его не привожу, полная реализация кодеса прилагается к статье. Функция у нас не одна, а значит, лучше свернуть имена в хеш простым алгоритмом:

....
hash_str:
push rdx
push rsi
sub rax,rax
mov rsi,rdx
sub rdx,rdx
nxt:
cld
lodsb
cmp al,ah
je dn
add dx, ax
rol rdx,14
jmp nxt
dn:
mov rax,rdx
pop rsi
pop rdx
ret

...

В качестве параметра передаем в rdx указатель на строку с именем функции и, в конце концов, регистрируем свой хендлер (hndlr01). Тут же заменяем «вручную» его адрес на другой (hndlr02). Затем мы вызываем исключение привилегированной командой rdmsr (чтение машинно-зависимого регистра), и после этого будет вызван не hndlr01 (первоначальный обработчик), а hndlr02. Код, демонстрирующий сказанное:

...
mov rbx,rsp
sub rsp,4*8
mov rdx, offset hndlr01
mov rcx, 1 ; first_handler
call qword ptr [rbx] ; RtlAddVectoredExceptionHandler
add rsp,4*8 ; восстанавливаем стек

push rax ; сохраняем указатель на наш VECTORED_EXCEPTION_NODE64
sub rsp,4*8
mov rcx,offset hndlr02
call qword ptr [rbx+8] ; RtlEncodePointer
add rsp,4*8
pop rdi ; rdi -> PVECTORED_EXCEPTION_NODE64
; сохраняем адрес обработчика hndlr02
mov [rdi+18h],rax
; вызываем исключение
; cpl = 3 => exception
rdmsr
...

Понимаешь, в чем суть? Достаточно единожды вызвать RtlAddVectoredExceptionHandler и установить хендлер, а дальше можно менять динамически адрес обработчика! От нас требуется только хранить указатель на VECTORED_EXCEPTION_NODE64.

То есть, фактически потеря fs скомпенсирована (ну, почти). Этим же способом мы можем добавить в список новый обработчик, не вызывая RtlAddVectoredExceptionHandler.

Отслеживаем выполнение VEH-обработчиков

Представим на минуту, что нам пришлось исследовать код, хитро манипулирующий с VEH, как описано выше. Не скажу, что динамическая смена обработчиков сильно затруднит отладку (но не статический анализ!), так как отловить вызываемый хендлер можно через функцию RtlDispatchException. Правда, она неэкспортируемая, но есть экспортируемая функция RtlRaiseException, из которой вызывается RtlDispatchException.

.text:0000000077EE6770 ; int __fastcall RtlRaiseException(struct _EXCEPTION_RECORD *ExceptionRecord, int, int, __int64, __int64, __int64)
....
.text:0000000077EE67ED call RtlVirtualUnwind
.text:0000000077EE67F2 mov r11, [rsp+538h+ContextRecord._Rip]
.text:0000000077EE67FA mov [rbx+10h], r11
.text:0000000077EE67FE mov rax, gs:30h
.text:0000000077EE6807 mov rcx, [rax+60h]
.text:0000000077EE680B cmp byte ptr [rcx+2], 0
.text:0000000077EE680F jnz loc_77F528CB
.text:0000000077EE6815 lea rdx, [rsp+538h+ContextRecord]
.text:0000000077EE681A mov rcx, rbx
.text:0000000077EE681D call RtlDispatchException
....

После того, как нашли RtlDispatchException - переходим по первому call в ней - это и есть вызов RtlpCallVectoredHandlers:

...
.text:0000000077EE5F00 RtlDispatchException proc near ; CODE XREF: RtlRaiseException+AD

.text:0000000077EE5F00 mov [rsp+arg_8], rdx
.text:0000000077EE5F05 mov rax, rsp
.text:0000000077EE5F08 sub rsp, 6A8h
.text:0000000077EE5F0F mov [rax+18h], rbx
.text:0000000077EE5F13 mov [rax+20h], rbp
.text:0000000077EE5F17 mov [rax-8], rsi
.text:0000000077EE5F1B mov [rax-10h], rdi
.text:0000000077EE5F1F mov [rax-18h], r12
.text:0000000077EE5F23 mov [rax-20h], r13
.text:0000000077EE5F27 mov [rax-28h], r14
.text:0000000077EE5F2B lea r8, RtlpCalloutEntryList
.text:0000000077EE5F32 mov rbx, rdx
.text:0000000077EE5F35 mov [rax-30h], r15
.text:0000000077EE5F39 mov rsi, rcx
.text:0000000077EE5F3C mov [rsp+6A8h+var_668], 0
.text:0000000077EE5F41 call RtlpCallVectoredHandlers
...

Дальше — ключевые для постановки бряка фрагменты RtlpCallVectoredHandlers. WinXP:

...
.text:0000000077F251F5 loc_77F251F5: ; CODE XREF: RtlpCallVectoredHandlers+3F2BE
.text:0000000077F251F5 mov rbx, [rsi+18h]
.text:0000000077F251F9 mov r9d, 4
.text:0000000077F251FF lea r8, [rsp+88h+var_58]
.text:0000000077F25204 lea edx, [r9+20h]
.text:0000000077F25208 mov rcx, 0FFFFFFFFFFFFFFFFh
.text:0000000077F25212 mov [rsp+88h+var_68], r12
.text:0000000077F25217 call NtQueryInformationProcess
.text:0000000077F2521C mov r11d, [rsp+88h+var_58]
.text:0000000077F25221 lea rcx, [rsp+88h+var_48]
.text:0000000077F25226 xor r11, rbx ; <- расшифровка адреса обработчика
.text:0000000077F25229 call r11 ; <- вызов очередного обработчика в списке
.text:0000000077F2522C lock btr cs:dword_77FA58C8, 0
...

Vista:

...
.text:0000000078E9393F loc_78E9393F: ; CODE XREF: RtlpCallVectoredHandlers-16030
.text:0000000078E9393F add dword ptr [r12+10h], 1
.text:0000000078E93945 lea rbx, [r12+10h]
.text:0000000078E9394A mov rcx, rdi
.text:0000000078E9394D mov [rsp+68h+arg_18], r12
.text:0000000078E93955 call RtlReleaseSRWLockExclusive
.text:0000000078E9395A mov rcx, [r12+18h]
.text:0000000078E9395F call RtlDecodePointer ; <- расшифровка адреса обработчика
.text:0000000078E93964 lea rcx, [rsp+68h+var_48]
.text:0000000078E93969 call rax ; <- вызов очередного обработчика в списке
...

Внимательно посмотрев на листинги, ты, наверняка, понял, что необходимо найти цикл вызова обработчиков - и дело в шляпе. Найти его труда не составляет - в нем обязательно находится вызов функции NtQueryInformationProcess/RtlDecodePointer. После того, как отыскали цикл - ставим бряк на инструкцию call-регистр. Звучит это пугающе, но при наличии Иды все делается за считанные минуты. Очень желательны здесь отладочные символы, если же сноровки достаточно, то можно и без них обойтись.

Кстати, на Vista появилась возможность и вовсе «отключить» выполнение VEH-обработчиков через PEB. Как? В начале RtlpCallVectoredHandlers встречается такой код:

...
.text:0000000078EA99E2 mov rax, gs:30h ; TEB
.text:0000000078EA99EB mov r15, [rax+60h] ; PEB
.text:0000000078EA99EF mov eax, [r15+50h] ; ProcessUsingVEH
.text:0000000078EA99F3 bt eax, r8d ; проверка второго бита
.text:0000000078EA99F7 jb call_vectored_handlers
...

То есть можно наставить кучу обработчиков, а потом сбросить флаг в PEB и ни один из них не будет выполнен!

Сохрани памятку

Вот как на 64-битных версиях Windows, в условиях тотального контроля над SEH, у нас в запасе остался VEH. Пригодиться он может много где. Ну, например, в различных пакерах, протекторах или в разных интересных программах. Вкупе с обфускацией описанная в статье техника может затруднить (пускай и незначительно) анализ кода. В последующих статьях я косвенно буду касаться изложенных здесь приемов, поэтому храни этот материал как своего рода памятку.

Содержание