Содержание статьи
Хочешь стать Черным Властелином своего компьютера? Хочешь знать все тайны разработчиков малвари и антивирусов? Читай статью, медитируй, постигай азы дзена, и да снизойдет на тебя прозрение (всем спокойно, познание Дао и прободение Шамбалы в результате прочтения этой статьи редакцией не гарантируется — прим. ред)!
RTFM
Что такое хук (hook — крючок, англ.)? Это механизм, позволяющий отследить некое событие в операционной системе. Было оно разработано дяденьками из Microsoft с самыми благими намерениями — позволить программисту более эффективно решать свои задачи путем установки контроля над событиями клавиатуры, мыши и многого другого. Реализовался он при помощи всем известных функций: SetWindowsHook(Ex), UnhookWindowsHook(Ex) и CallNextHook(Ex).
Хук служит для перехвата неких событий до того, как они дойдут до приложения. Эта функция может реагировать на события и, в некоторых случаях, изменять или отменять их. Функции, получающие уведомления о событиях, называются «фильтрующими функ циями» и различаются по типам перехватываемых ими событий. Пример — фильтру ющая функция для перехвата всех событий мыши или клавиатуры. Чтобы Windows смогла вызывать функцию-фильтр, эта функция должна быть установлена, то есть, прикреплена к хуку (например, к клавиатурному). Прикрепление одной или нескольких фильтрующих функций к какомунибудь хуку называется установкой хука. Если к одному хуку прикреплено несколько фильтрующих функций, Windows реализует очередь функций, причем функция, прикрепленная последней, оказывается в начале очереди, а самая первая функция — в ее конце.
Со временем благородное понятие хука извратилось, причиной чего стали действия вирусописателей и малварщиков. Первые вирусы были, как бы это сказать… наивными, наверное. Они представляли собой отдельный exe-файл, напрямую вызывающий нужные функции системы. Шло время и антивирусы, которые появились чуть позже и вдруг стали коммерческими, довольно быстро научились ловить вирусы по сигнатурам путем простого сканирования оперативной памяти или дискового пространства.
И вот тут-то, в пылу извечной борьбы между писателями вирусов и их «ловцами» встал один-единственный вопрос, который стоит на повестке дня до сих пор и будет стоять в ближайшем необозримом будущем — это вопрос выживания в операционной системе. Причем он также актуален и для антивирусов, ведь для хорошего системного программиста, пишущего вирусы/ руткиты, вынести процесс антивируса из системы — не слишком сложная задача.
Поэтому можно смело утверждать, что одна из задач антивирусов — это умение сохранить свой процесс в целостности и сохранности от злонамеренных действий вируса. В общем, на сегодняшний день под хуками следует понимать установку контроля над основными системными функциями операционной системы, от которых зависит жизнеспособность любой программы — речь идет, как правило, о функциях работы с процессами, потоками, сетью и интернетом и т.д.
«А как же SetWindowsHook?» — спросишь ты меня. «Прошлый век», — отвечу я. Использовать их давно уже не кошерно.
Что имеем?
Проще всего установить хук в системе путем создания так называемой прокси-функции. Иначе говоря, тебе надо определиться, какую функцию ты перехватываешь, и найти адрес ее вызова. Для этого обычно используется функция GetProcAddress примерно вот так: GetProcAddress(GetModuleHandle("ntdll.dll"), "CsrNewThread").
Однако просвещенные знают, что она практически всегда перехватывается аверами, и для нахождения адреса функции используют парсинг таблицы импорта той или иной библиотеки, обычно ntdll.dll, kernel32.dll (kernelbase.dll в Windows7) или advapi32.dll.
Далее тебе нужно создать свою прокси-функцию, точь-в-точь повторяющую вызываемую примерно вот таким образом:
int MyNewFunction(void *param1,
int param2, bool param3)
{
return OriginalFunction(param1,
param2, param3);
}
После этого следует перезаписать адрес вызова OriginalFunction на свой — то есть, на MyNewFunction.
Теперь, если кто-либо захочет вызвать для исполнения OriginalFunction, сначала будет вызвана твоя прокси-функция MyNewFunction, которая уже потом передаст управление на оригинальный адрес. Вот таким вот нехитрым образом действуют, наверное, 8 хуков из 10. Этот способ удобен лишь своей простотой, но при этом представляет собой ужасное палево для аверов. Как? Поразмысли сам — все, что аверу нужно, это сравнить прежний, «законный», адрес функции с тем, что есть на самом деле. Если они отличаются — бьем тревогу. Кстати, встает и следующий вопрос: откуда взять этот самый адрес оригинальной функции? Тут особо гадать не надо — его считывают с нужного файла на диске. Этот подход основывается на том предположении, что вирус не будет патчить таблицу экспорта файла, лежащего на диске, ограничившись патчем виртуальной памяти.
Итак, едем дальше. Как я уже говорил, использование хука в виде прокси-функции хоть и удобная вещь, но, во-первых, палевная, а во-вторых, подходит лишь для начинающих. То есть не для тебя :). Самый распространенный вид хука — это сплайсинг. Уверен, ты не раз слышал это слово. В нашем случае это запись на начало функции пятибайтовой последовательности, которая представляет собой команду jmp по адресу обработчика перехвата. Здесь первый байт — опкод jmp, оставшиеся четыре байта — адрес твоей функции.
Если необходимо вызывать перехватываемую функцию, то перед заменой необходимо сохранить ее начальные байты и перед вызовом восстанавливать их. Недостаток данного метода состоит в следующем: если после восстановления начала функции произошло переключение контекста на другой поток приложения, то он сможет вызвать функцию, минуя перехватчик. Этот недостаток можно устранить, останавливая все побочные потоки приложения перед вызовом, и запуская после вызова. Ну и конечно, сплайсинг, как и прокси-функции, тоже легко выявляется методом сканирования памяти, так как сразу будет видно, что вызов функции идет куда-то в другое место.
Вообще, забегая вперед, должен донести до широкой общественности, что почти все методы перехвата вызова функций так или иначе детектятся сканированием памяти. За исключением двух методов, но об этом читай ниже.
IAT, EAT и другие звери
Возникает вопрос: а на что и, самое главное, где можно ставить свои хуки? Первое, что приходит на ум — конечно же, поставить перехват на Import Address Table (IAT). Когда приложение использует функцию из библиотеки, приложение должно импортировать адрес функции. Каждая DLL, используемая приложением, описана в структуре, называемой IMAGE_IMPORT_DESCRIPTOR. Эта структура содержит имя DLL, чьи функции импортированы приложе нием, и два указателя на два массива структур IMAGE_IMPORT_BY_ NAME. Структура IMAGE_IMPORT_BY_NAME содержит имена импортированных функций, используемых приложением.
Когда операционная система загружает приложение в память, читается структура IMAGE_IMPORT_DESCRIPTOR и каждая требуемая DLL загружается в память приложения.
Как только DLL отображена (mapped), операционная система располагает каждую импортированную функцию в памяти и записывает поверх одного из массивов IMAGE_IMPORT_BY_ NAME с исполнительным адресом функции.
Как только hook-функция появляется в адресном пространстве приложения, твой вирус сможет прочесть формат PE целевого приложения в памяти и заменить целевой адрес функции в IAT адресом hook-функции. Тогда, когда перехватываемая функция будет вызвана, твоя hook-функция будет выполнена вместо первоначальной функции. Чуть более редкий вариант, встречающий в природе, реализованный по приниципу «Если гора не идет к Магомеду...» — перехват Export Address Table (EAT), когда патчится, наоборот, таблица экспорта Dll, которая экспортирует целевую функцию.
STELTH-хуки: поймай меня, если сможешь
Как я уже писал выше, главный недостаток вышеуказанных методов перехвата — это вынужденная модификация памяти, что неизбежно ведет к ее детекту со стороны аверов. Есть ли выход? Как ни странно, есть. Даже два. Первый из них — это зарегистрировать свой обработчик исключений, затем добиться, чтобы он получил управление. Это можно сделать, например, потерев какой-либо участок памяти. Второй способ представляет собой несколько видоизмененный первый. То есть, ты, как и раньше, регистрируешь обработчик исключений, но для их генерирования ты используешь прием, известный среди дебаггеров. Как ты знаешь, дебагрегистры процессора используются для отладки приложений и доступны, как правило, из кернелмода. Однако их можно устанавливать и из юзермодных приложений путем использования функций GetThreadContext/ SetThreadContext. Используются дебаг-регистры для установки точек останова (Breakpoints) на доступе к участку памяти или исполнении.
Всего имеется восемь регистров, их назначение следующее:
- DR0 - DR3 — Каждый из этих регистров содержит линейный адрес одной из четырех контрольных точек. Если подкачка страниц разрешена, то их значения транслируются в физические адреса по общему алгоритму;
- DR4 - DR5 — Регистры зарезервированы и в процессоре i486 не используются;
- DR6 — Отладочный регистр состояния. Он сообщает об условиях, выявленных во время генерирования отладочного исключения (номер 1). Биты регистра устанавливаются аппаратно, а сбрасываются программно;
- DR7 — Регистр задает вид доступа к памяти, связанный с каждой контрольной точкой.
Итак, все, что тебе нужно сделать — это установить хардварный бряк (hardware breakpoint, он же int 1) на начало функции, чтобы процессор сгенерировал так называемое «одношаговое исключение» (single step exception) и затем, путем установки своего обработчика исключения: AddVectoredExceptionHandler(0, (PVECTORED_EXCEPTION_ HANDLER)DebugHookHandler), перехватить этот самый EXCEPTION_SINGLE_STEP.
При его генерации твой обработчик получит управление желанной функцией. Несомненное достоинство такого метода в том, что он абсолютно невыявляем путем сканирования памяти, поскольку ее модификация здесь не происходит.
int SetDebugBreak(FARPROC address)
{
int status = -1;
HANDLE thSnap = CreateToolhelp32Snapshot(
TH32CS_SNAPTHREAD, NULL);
THREADENTRY32 te;
te.dwSize = sizeof(THREADENTRY32);
Thread32First(thSnap, &te);
do
{
if(te.th32OwnerProcessID != GetCurrentProcessId())
continue;
HANDLE hThread = OpenThread(
THREAD_ALL_ACCESS, FALSE, te.th32ThreadID);
CONTEXT ctx;
ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS;
GetThreadContext(hThread, &ctx);
if(!ctx.Dr 0)
{
ctx.Dr0 = MakePtr( ULONG, address, 0);
ctx.Dr7 |= 0x00000001;
status = 0;
}
else if(!ctx.Dr1)
{
ctx.Dr1 = MakePtr( ULONG, address, 0);
ctx.Dr7 |= 0x00000004;
status = 1;
}
else if(!ctx.Dr2)
{
ctx.Dr2 = MakePtr( ULONG, address, 0);
ctx.Dr7 |= 0x00000010;
status = 2;
}
else if(!ctx.Dr3)
{
ctx.Dr3 = MakePtr( ULONG, address, 0);
ctx.Dr7 |= 0x00000040;
status = 3;
}
else
status = -1;
ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS;
SetThreadContext(hThread, &ctx);
CloseHandle(hThread);
}
while(Thread32Next(thSnap, &te));
return status;
}
Заключение
Как ты видишь, даже в самых сложных ситуациях можно найти возможность исполнить свой код. Уверен, что при этом твой код нацелен исключительно на решение задач по защите твоей системы. Удачи тебе и удачного компилирования!
Links
http://vx.netlux.org — своеобразный музей вирусов, вирусных движков и прочей интересной ерунды. Must visit, одним словом.