суббота, 24 июля 2010 г.

Сплайсер для Windows x64

Disclaimer

Эту заметку я обещал еще ранней весной написать для третьего номера отличного журнала No Bunkum. Но получилось как обычно - сначала я был дико занят, а потом похоже у авторов издания бензина хватило только на два номера. Надеюсь они не сильно обидятся, если я опубликую эти малосвязные жалкие заметки в своем блоге


Intro

"Зачем, зачем же еще один сплайсер !" - я прямо слышу ваши трагичные стенания, когда вы читаете это. В самом деле, несмотря на то что платформа win x64 еще не слишком стара, в интернете уже можно найти некоторое количество вполне работающих сплайсеров - например Mini Hook-Engine. Тем не менее я решил что мне лично нужен свой самобытный, который бы
  1. умел работать как в user mode, так и в kernel mode (кстати сорцы последнего я вам не дам - мало ли чего)
  2. был лишен некоторых дефектов существующих сплайсеров
  3. в силу врожденной вредности и синдрома Not Invented Here
Давайте пристально посмотрим на тот же Mini Hook-Engine - ближе к концу своей статьи его автор показывает отличный пример кода, на котором его сплайсер не работает - функцию MessageBoxW. Как может заметить внимательный читатель
  1. в ней слишком мало места для вставки переходника - вторая же инструкция имеет обращение к глобальной переменной относительно RIP и будучи перенесенной в память по другому адресу будет выполнена неправильно
  2. такие ситуации не обрабатываются - можно было вернуть ошибку "Cannot hook"
  3. это далеко не единственная ситуация, когда сплайсинг невозможен - например могут попасться другие инструкции, работающие с адресацией относительно RIP. На ум сразу приходит семейство jmp/jXX и call
Итого хотелось бы чтобы наш новый сплайсер
  1. умел анализировать код, который сплайсит и говорить вердикт - можно/не можно. Даже в user mode крайне неприятно когда угнанная функция падает при выполнении, а уж в kernel mode это фатально
  2. все же смог выполнить сплайсинг этой несчастной MessageBoxW - мы же должны быть хоть чуть-чуть лучше аналогов, не так ли ? И кстати эта задача вполне достижима - нужно только изыскать внутренние резервы - в военное время синус может принимать значение 3. В данном случае изысканными резервами вполне может служить неиспользуемое пространство между функциями. Дело в том что нам нужно например 8 байт для хранения адреса перехода. А под x64 функции обычно выравнены на 16 (почти 50% вероятность успеха) или на 32 байта (почти 75% вероятность успеха)
Итого вырисовывается следующая архитектура - при сплайсинге нам нужно знать, сплайсим ли мы самое начало функции и таким образом можем поискать позади нее свободные байты, а также разное поведение в зависимости от того, сколько места мы там найдем - т.е. нужно применить динамическое программирование

Код сплайсера лежит целиком в паре файлов - splicer_x64.cpp и написанный на ассемблере ms.nas

ms.nas

Я решил что было бы недурно иметь следующую структуру функций-перехватчиков - в начале идет структура x64_thunk, описывающая сам hook, затем общий для всех перехватчиков код. Структура  x64_thunk (описана в файле cmn_stub.inc) имеет следующие поля:
  • orig_addr - содержит адрес исходной функции - мало ли вам захочется сделать unsplicing например
  • size - число перемещенных байт исходной функции. Как видите при желании имея эти два поля можно сделать и обратную операцию и вобще это не я и ничего не было
  • hook_addr - адрес функции-перехватчика. Описана как typedef void (*x64_hook)(PVOID *stack, struct x64_thunk *this_hook)
  • jmp_addr - адрес перемещенных байт, куда управление попадает после вызова hook_addr. К концу этого набора байт добавлен переходник куда-нть в середину оригинальной функции
  • user_data - просто указатель, может использоваться для чего угодно. Например для передачи данных в x64_hook
А теперь код нашего переходника - с комментариями автора:

dyn_hook:
; грузим в rax адрес нашей структуры x64_thunk
; которая находится непосредственно перед dyn_hook,
; относительно RIP, который во время исполнения
; указывает на адрес следующей инструкции (.next)
    lea rax, qword [rip + hook_s - .next]
.next
; проверим x64_thunk.hook_addr - если равен нулю - не вызывать перехватчик
    mov r11, [rax + hook_addr]
    test r11, r11
    jz .skip
; сохраним регистровые аргументы в shadow stack space
    mov [rsp + 0x8], rcx
    mov [rsp + 0x10], rdx
    mov [rsp + 0x18], r8
    mov [rsp + 0x20], r9
; перед вызовом x64_thunk.hook_addr заполним аргументы 

; поместим первый аргумент в rcx - stack addr
; поместим второй аргумент в rdx - указатель на x64_thunk
    mov rcx, rsp
    lea rdx, qword [rip + hook_s - .next2]
.next2:
; сделаем новый stack shadow space
    sub rsp, 32
; вызов x64_thunk.hook_addr
    call r11
; восстановим stack shadow space
    add rsp, 32
; восстановим оригинальное значение регистров
    mov rcx, [rsp + 0x8]
    mov rdx, [rsp + 0x10]
    mov r8,  [rsp + 0x18]
    mov r9,  [rsp + 0x20]
; снова грузим адрес структуры x64_thunk - ведь rax наверняка был испорчен

; внутри x64_thunk.hook_addr
    lea rax, qword [rip + hook_s - .skip]
.skip:
; и улетаем на x64_thunk.jmp_addr
    jmp [rax + jmp_addr]

Как видите никакой черной магии здесь нет и все довольно просто (если вы знаете x64 calling convention)

Warning ! как вы могли бы заметить, я использую в этом переходнике shadow stack space сплайснутой функции, а также сохраняю только регистры с аргументами - т.е. этот переходник годится только для сплайсинга кода в самом начале функций. Если вы хотите иметь возможность сплайсинга в середину - вам потребуется написать собственные версии переходников, которые бы сохраняли все нужные регистры

splicer_x64.cpp

Содержит все остальное - некоторое количество разнообразных вариаций передачи управления, класс x64_stoled_bytes_storage для хранения тел переходников и перемещенных кусков сплайснутых функций (на самом деле они хранятся сразу же после переходника из ms.nas) и собствено код сплайсера - состоит из двух методов:
  • check(PBYTE addr, int is_start) - проверяет может ли быть успешен сплайсинг некоторой функции по адресу addr. is_start - не является ли этот адрес началом некоторой функции, тогда мы можем поискать в aligned bytes дополнительное место. В случае успеха заносит в переменную класса x64_splicer.m_copy_bytes число перемещаемых байт
  • hook(PBYTE addr, x64_hook func) - делает собственно сам сплайсинг. func - это ваша функция-перехватчик
Для дизассемблирования функций используется библиотека libudis86 - я ее немножко модифицировал чтобы она могла работать и в kernel mode - например все ее таблицы объявлены как const и живут в секции кода драйвера, ну и еще оторвал ей все обращения к CRT

main.cpp

В качестве демонстрационного примера был написан простой примерчик использования данного сплайсера в user mode - просто ставит сплайсом хуки на 4 функции из user32.dll:
  • MessageBoxA
  • MessageBoxW (надо же проверить что yes, we can)
  • GetDesktopWindow
  • GetShellWindow
Из любопытного там можно заметить что при этом используется ровно одна функция-перехватчик - my_hook, которая различает на какой именно функции она вызвалась используя поле x64_thunk.user_data

Source code

Можно невозбранно скачать на sourceforge - проект x64splicer

5 комментариев:

  1. проэкт скомпилился тока в release-mode, в дебуг-моде не линкуется. запустил на win7_x64 в консоли под админом, сначала выплыла консольная мессага "GetDesktopWindow not found", затем системная "splicer.exe has stopped working". закомментил запуск всех хученных функций - появилась мессага "user32.dll not found". чертовщина!!!

    ОтветитьУдалить
  2. > в консоли
    попробуй загрузить в начале примера user32.dll явно через LoadLibrary
    на что линкер в debug-mode ругается ?

    ОтветитьУдалить
  3. привет. к сожалению, на рабочем компе стоит WinXP_32, компилировать не получится, расскажу для чего мне нужен сплайсинг x64. я автор небольшой сетевой утилиты: http://netmontools.com,
    так вот для захвата Ethernet фреймов я использую слегка изменённый и переименованный драйвер ndisprot.sys из WDK. по понятным причинам на Win7_x64 лрайвер не встаёт, т.к. нет цифровой подписи. в то же время в каждой Win7_x64 системе установлен микрософтовский ndisprot.sys. вот если бы его хукнуть, при помощи вашего сплайсера! моих познаний хватило только на пересборку сэмплов из WDK.

    ОтветитьУдалить
  4. ой-вэй как все запущенно

    во первых я настоятельно рекомендую не лезть в kernel-mode, если познаний хватает только на пересборку примеров из wdk

    во вторых - даже чтобы сплайснуть уже готовый драйвер в kernel mode - нужно уже иметь свой код в kernel mode

    в третьих - сплайсинг - в общем случае дико ненадежная технология, и должна применяться только в случаях, когда ничего больше не помогает. В данном случае есть альтернативы - например подписать свой драйвер либо для висты/w7 использовать windows filtering platform из user-mode

    ОтветитьУдалить
  5. спасибо. отрезвляет. в моём случае есть ещё альтернатива - использовать библиотеку WinPCap, они как-то умудрились подписать свой драйвер.

    ОтветитьУдалить