Сначала я объясню некоторые моменты по самому изложению моей работы. Во-первых, те, кто не знают асм, могут не пугаться – я все четко прокомментировал. Выбрал я именно его из-за того, что он позволяет получить полное представление о работе системы в целом и моей проге-примере в частности. Ну и, конечно, из-за моих личных предпочтений :). Все статью я разбил на 4 части:

1) Вступление
2) Теория или как это работает?
3) Реализация
4) Практика или полет фантазии

Для кого это? Для всех, кто интересуется системным программированием. Предполагается, что читатель знает слова «хук», «синхронизация», «MMF», «Event» 😉
Весь исходный код на masm’e от Хатча (ссылка в конце статьи).

Вступление.

О чем собственно речь… Я наткнулся на
интересную особенность в «идеологии» винды. Дело в том, что SetWindowsHookEx() все же позволяет ставить локальный хук без использования каких либо библиотек, только это извращение… Но зато какие возможности открываются! Что – не представляешь какие? У тебя плохая фантазия :). Подумаем, что у нас получается в целом. Можно выполнять код в чужом процессе. Более того, для этого не нужен хэндл этого самого процесса. Более того, не нужна дополнительная библиотека для этого, как предлагал Рихтер в своем способе с CreateRemoteThread()’ом и LoadLibrary()’ем! Кстати, как пример, я сделал CRT, который не требует, чтобы код находился в чужом процессе
(это делается автоматом). Единственный недостаток – не работает в 9х, но ты увидишь, что это не смертельно – реализовать эту байду в 9х проще некуда… Да, особо сообразительные, наверное, подумали, что пока наш код в чужом процессе наш главный модуль обязан находится в памяти и светится в списке процессов. Черта с два! Я это обошел. Как бонус к статье имеются модули на ассемблере
(я использую masm 8.0), позволяющие довольно легко программировать в жесткой и агрессивной среде
(о ней позже) + модуль, в котором реализован CreateRemoteThread().

Теория.

Не все так просто, как может показаться… Скажу, что я довольно много парился для того, чтобы привести все в цивильный и пригодный для использования вид. Начнем с самого главного, а именно с того, что происходит при установке ЛОКАЛЬНОГО хука
(довольно упрощенный вариант):

1) Мы ставим хук, к примеру, WH_GETMESSAGE.
2) Допустим, в окно жертвы приходит сообщение, тогда.
3) Система проверяет – если библиотека (параметр hMod) еще не загружена в чужой процесс, то она его отображает (посредством обращения к драйверу режима ядра, вроде… SoftIce уходит куда-то далеко…
).
4) Lock count модуля в чужом процессе увеличивается на
1 (lock count – это счетчик блокировок, если он равен 0, система выгружает модуль из памяти).
5) Где-то тут, наверное, происходят релоки…
6) Lock count увеличивается на 1.
7) Управление передается MsgProc’у, на адрес,
который высчитывается как NewModBase+(lpfn-LastModBase).
8) Lock count уменьшается на 1.
9) Мы делаем UnhookWindowsHookEx (или просто завершаем главную прогу) и lock count уменьшается на 1 => модуль выгружается.

Менее подробно, но по делу – у Рихтера (ссылка внизу).
Да, наверное не помешает описание SetWindowsHookEx, дабы была перед глазами(выдрал из SDK):

HHOOK SetWindowsHookEx(
int idHook, // type of hook to install
HOOKPROC lpfn, // address of hook procedure
HINSTANCE hMod, // handle of application instance
DWORD dwThreadId // identity of thread to install hook for 
);

А почему бы нам ни сделать все это, но вместо hMod вставить базовый адрес нашего главного модуля? Ну,
тут проблем много. Во-первых, когда происходит отображение в чужое адресное пространство меняется базовый адрес, а по дефолту НЕ_БИБЛИОТЕКИ не могут менять базового адреса. Решение – параметр линковщику /FIXED:NO(збазззззибо Four-F 😉 ), что заставит его включить инфу о релоках в pe-заголовок. Таблица релоков
(находится в секции .reloc) – это табличка, состоящая из смещений к операндам инструкций процессора, оперирующих с жестко привязанными к базовому адресу данными
(собственно операндами). Такими инструкциями, к примеру, являются mov REG, offset DATA – тут DATA вбивается компилятором в виде виртуального адреса, а не смещения.
Естественно виртуальный адрес зависит от базы, а если она измениться, то загрузчику придется пройтись по таблице релоков и по каждому смещение подправить адрес в соответствии с базовым адресом.

Во-вторых, в NT системах (пробовал в ХР(build 2600 без sp) и 2000) даже если мы включим инфу о релоках, они все равно не произойдут :(. Я не знаю фишка это или баг, но факт есть факт – релоки не происходят. Тем не менее они нужны загрузчику… Мой поиск решения этой проблемы/выявления ее возникновения ни к чему не привел, чтобы не загромождать статью приводить его я не буду. Попробуй ты, может тебе повезет больше… Еще больше я удивился, когда узнал, что в 9х релоки происходят 8)… Так что решение этой проблемы в 9х – тривиально. Почему это работает??? А черт его знает… У меня есть пару догадок на это тему, но они уж больно недостоверные и в большинстве своем построены на догадках… Будем считать, что это из-за того, что SWH() в NT и в 9х имеют различную реализацию. Но мы говорим про NT. Так как релоков нема, нам придется очень сильно извращаться, чтобы прога работала
(поэтому я и выбрал ассемблер). Я покажу несколько приемов, которые позволяют делать базонезависимый код почти так же легко, как и базозависимый
(об этом позже).

Следующая проблема – это то, что, как ты наверное заметил из схемы, наш модуль выгружается сразу после Unhook’a или завершения проги… Я долго думал, как это обойти, чтобы главный модуль смог сделать ExitProcess, а наш «засланец» 🙂 мог продолжать спокойно работать. Все сводилось к увеличению счетчика модуля-«засланца»/увеличению счетчика
объекта хука. Перепробовав довольно много различных вариантов
(ооооочень много…), меня осенила совершенно очевидная мысль – сделать LoadLibrary в MsgProc’e на модуль-«засланец». Таким образом, так как этот модуль уже загружен, LoadLibrary просто увеличивает Lock Count данного модуля, что нам собственно и нужно. В чем подвох? В том, что этот код может не успеть выполниться пока не произойдет Unhook
(если делать его сразу после SWH). Это обойти легко – благо в Windows’е ОЧЕНЬ широкие возможности синхронизации… Итак, сразу после SWH делаем WaitForSingleObject. В качестве первого параметра хэндл, предварительно созданного Event’a. В MsgProc’e, как только сделаем все нужные действия, делаем SetEvent.

Что у нас с правами??? Ну, в 9х, я думаю все понятно – мочи кого хочешь :). В NT вроде можно ставить хук только на те процессы, которые запустил тот же пользователь, что и тебя… По крайней мере, на SYSTEM ты хук не поставишь… Забудь о winlogon'e :). Но, например, explorer тебе доступен со всеми потрохами 😉 !

Реализация.

Итак, сначала разберем моменты связанные с базонезависимым кодом, далее рассмотрим код CreateRemoteThread’a и MsgProc’a.

Что у нас есть к моменту загрузки нашего модуля в удаленный процесс? Все смещения сбиты. Все импортируемые функции не вызываются. О секции .data можно забыть… Значит надо искать GetProcAddress и эмулировать data’у прямо в секции кода, для чего нужно указать линковщику параметр /SECTION:.text,ERW
(так как загрузчик при установке хука просто копирует
(PAGE_EXECUTE_WRITECOPY) страницы, устанавливая им те же атрибуты, то это пройдет… Кстати в 9х скорее всего не работает код под
NT как раз из-за того, что он копирует страницы с другими атрибутами без W…).

БАЗОНЕЗАВИСИМОСТЬ

Часть 1. Макросы dodata и dobigdata

Начнем с эмуляции data’ы. Я написал 2 макроса dodata и dobigdata, которые ты можешь найти в фаиле API_Emul.asm. Оба эти макроса предполагают установку атрибутов страниц кода с флагом PAGE_READWRITE. Один резервирует область памяти в секции кода для данных меньше 127 байт, а другой для данных больше 127 байт, при этом второй автоматом резервирует эти 127 байт сразу после ваших данных. Оба возвращают указатель на данные в регистре ecx. Вот сами макросы:

dobigdata MACRO some_data
call GetCurrPosition
add ecx, 8h
jmp @F
&some_data 
;
Заставляем компилятор генерить jmp FAR
db 127 dup(0)
@@: 
ENDM

; Если данных меньше 127 байт...
dodata MACRO some_data 
call GetCurrPosition 
add ecx, 5h
jmp @F
&some_data
@@: 
ENDM 

Несколько пояснений. Что такое GetCurrPosition? Просто 

GetCurrPosition proc 
mov ecx, [esp]
ret
GetCurrPosition endp

Как ты, наверное, знаешь в стэке при входе в процедуру содержится адрес возврата. Гораздо более изящный, но менее наглядный метод – это 

call $+5
pop ecx

Но я пожертвую красоту наглядности :). В то же время, если бы я просто сделал mov ecx, $, то ничего не вышло бы, потому что релоки не происходят.

Ты должен знать, что если мы ставим хук в 9х, то там страницы кода копируются без W атрибута… Просто PAGE_READ :(. Именно поэтому сам макрос ты выполнить сможешь, а вот записать по адресу в есх не сможешь… Но там ведь релоки происходят, поэтому эти макросы там и не нужны… В то же время теряется мультиплатформенность… Но с этим ничего не поделаешь… Найдешь выход - напиши мне 🙂 

Пример использования? Смотри дальше… Я его буду активно использовать в MsgProc’e.

Часть 2. Нахождения адресов внутренних процедур.

Небольшое замечанице: Например, вот этот банальный код работать не будет
(я думаю ты понял почему):

push offset SOME_PROC
pop ecx

В eсx будет неправильный адрес… Как же это обойти… Ведь без этого не обойтись! Тут я позволил себе импровизацию и вот, что придумал:

jmp to_get_SOME_PROC_ADDR
to_get_SOME_PROC_ADDR2:
add ecx, 2 ; <====== Здесь адрес SOME_PROC
<…>
to_get_ SOME_PROC_ADDR:
call GetCurrPosition 
jmp to_get_SOME_PROC_ADDR2
SOME_PROC proc
<…>
SOME_PROC endp

Зачем мы прибавляем 2 байта? Потому что инструкция jmp вместе с операндом занимает 2 байта. Если между to_get_SOME_PROC_ADDR2 и to_get_SOME_PROC_ADDR «расстояние» больше 127 байт, то вместо add ecx, 2 пишем 5. Это потому, что компилятор генерит либо short либо far jmp в зависимости от «расстояния». Естественно, что far занимает больше, чем short. Как определить, когда far, когда short? Естественно, не стоит высчитывать сколько занимает каждая инструкция. Гораздо легче определить это экспериментально, то бишь в отладчике. Как ты заметил на этом основаны и предыдущие макросы.

Часть 3. GetProcAddress()

Что делать, когда неизвестны адреса функций, находящихся в другом модуле? Найти их! :). Но нам ведь неизвестен адрес и самого GetProcAddress. Как его найти? Анализируем таблицу экспорта kernel32.dll. Тема нахождения адресов функций в таблице экспорта очень заезжена. Я реализовал свой GetProcAddressByET так наглядно, как это вообще возможно. Если ты не знаком с этой проблемой, я дам тебе ссылку в конце статьи на отличное описание этого алгоритма+статью о таблице экспорта. Гораздо более интересная проблема – это нахождение базового адреса kernel32.dll. И действительно, мы же не знаем его и GetModuleHandle() нам не доступен. Выход я нашел в отличной статье U_dev’a
(ссылка в конце), где он предлагает изящное решение. Вкратце – в регистре fs находится указатель на массив обработчиков исключений, последний из них ВСЕГДА
(для ехешников) находится в kernel’e! Таким образом узнаем адрес обработчика и перебираем вниз от этого адреса байты кода в поисках сигнатуры mz, найдя ее пробуем проверить содержание по смещению, указывающему на pe заголовке, если это pe, то мы в начале модуля, значит мы знаем его базовый адрес. Естественно, все это небезопасно, поэтому используем обработчик исключений. Как его установить – смотри в коде. Тоже довольно тривиальная задача…

Итак, код я здесь приводить не буду, ибо его слишком много. Но я разработал также несколько полезных функций, связанных с упрощением жизни программисту. Например, вот функция EGetProcAddress:

EGetProcAddress proc USES ecx libname: PCHAR, funcname: PCHAR
local GPA: DWORD
; Находим GetProcAddress 
call GetFunc_GetProcAddress
mov GPA, eax
; Находим GetModuleHandle
dodata <db "GetModuleHandleA",0>
push ecx
call GetKernelBase
push eax 
call GPA 
; Вызываем GetModuleHandle 
push libname
call eax 
cmp eax, NULL
jz cant_find_mod_exit
push funcname
push eax 
; Возвращаем тоже, что и GPA
call GPA 
ret
cant_find_mod_exit: 
mov eax, -1
ret 
EGetProcAddress endp 

Как ты видишь, она основана на GetFunc_GetProcAddress:

GetFunc_GetProcAddress proc USES ecx
local KernBase :DWORD 
call GetKernelBase
mov KernBase, eax 
dodata <db "GetProcAddress",0> 
invoke GetProcAddressByET, KernBase, ecx 
ret
GetFunc_GetProcAddress endp 

Тут все очевидно… Все исходники в API_Emul.asm. Пример пользования будет в MsgProc’e.

РЕАЛИЗАЦИЯ CRT

В принципе, CRT я использую только в качестве примера… Можно было бы сделать кое-что гораздо более интересное… Об этом в разделе ПРАКТИКА… Ну ладно, перейдем к самому CRT. 

Итак, что мы должны сделать? План действий.

1) Находим окно жертву
2) Находим по этому окну поток, к которому оно принадлежит
3) Создаем событие (для синхронизации), нужные MMF
(понадобятся для передачи параметров и возврата ответа из хука)
4) Заполняем параметры, нужные CreateThread’у в спец структуру и копируем их в MMF, который мы создали раньше…
5) Устанавливаем локальную ловушку. В качестве параметров указываем: hMod=GetModuleHandle(NULL) dwThreadId=
НАЙДЕННЫЙ_В_ ПРЕДЫДУЩИХ_ШАГАХ_ ХЭНДЛ; idHook значения не имеет, для примера возьмем WH_GETMESSAGE
6) Постим мессадж, который ждем в хуке
7) Ждем события
8) <В ЭТО ВРЕМЯ В ЛОВУШКЕ>: 

- Увеличиваем счетчик нашего модуля (LoadLibrary на самого себя
(GetModuleFileName))
- Принимаем из MMF параметры и обрабатываем их в соответствии с новой базой
(например, параметр lpStartAddress передается в соответствии со старой базой=>надо его подкорректировать)
- Вызываем CreateThread с переданными параметрами
- В тот же MMF записываем результат
- Устанавливаем событие

9) Когда событие произошло освобождаем все ресурсы и возвращаем результат

Вот в принципе и все… Тут есть несколько тонкостей, но описывать их не имеет смысла… Смотри исходный код ниже. Неплохо бы так же иметь перед глазами прототип CreateThread’a
(выдрал из SDK):

HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes, // pointer to thread security attributes 
DWORD dwStackSize, // initial thread stack size, in bytes 
LPTHREAD_START_ROUTINE lpStartAddress, // pointer to thread function 
LPVOID lpParameter, // argument for new thread 
DWORD dwCreationFlags, // creation flags 
LPDWORD lpThreadId // pointer to returned thread identifier 
);

lpParameter и lpThreadAttributes мне сильно влом было реализовывать, ибо это указатели… Да они в общем-то и не сильно нужны… В любом случае, вместо них передаются NULL’и.

(Продолжение следует)

 

Ссылки
1) Где купить Рихтера? Например http://www.books.ru/shop/books/8283
или http://www.books-shop.com/book89.html
или  http://www.mistral.ru/content/38168.shtml
(А ты че думал??? Электронный вариант? Черта с два… Не для этого Рихтер работал, чтобы ты на халяву его талмуд скатал)
2) masm32 by Hutch http://www.movsd.com/masmdl.htm
3) Описание таблицы экспорта http://www.wasm.ru/article.php?article=1002007
4) Эмуляция GetProcAddress’a http://www.wasm.ru/article.php?article=searchapi
5) Нахождение базы kernel’a http://sbvc.host.sk/articles/9.html

Greetzzzzz

Спасибо Four-F’у, который додумался, что все это работает только из-за релоков. Если бы не он, я бы, наверное, до сих пор делал бы /DEBUG :).

Спасибо ZeroIce’у, который часто указывал мне на баги и ошибки в моих примерах.

Исходники

  • Подпишись на наc в Telegram!

    Только важные новости и лучшие статьи

    Подписаться

  • Подписаться
    Уведомить о
    0 комментариев
    Межтекстовые Отзывы
    Посмотреть все комментарии