В этой статье речь пойдет о системных программистских трюках, которые помогут тебе сделать свой код более четким, легким и красивым с хакерской точки зрения. Да! Теперь твой код будет вызывать зависть коллег по цеху и повышенное внимание противоположного пола (лето влияет :)), а также в разы повысит твою самооценку.

 

Трюк #1, или El pueblo unido jamas sera vencido!

Хороший лозунг чилийских революционеров, как считаешь? В Windows все (ну или почти все) построено на привилегиях, поскольку любой код, исполняющийся в системе, так или иначе обладает строго определенными возможностями. Я сейчас не имею в виду разделение ядра и юзермодного кода. Речь идет о привязке кода к системе пользовательских привилегий в Windows по типу «Все вокруг п******ы, один я Д’Артаньян». То есть, в винде существует довольно сложный механизм, который только и делает, что проверяет, можешь ли ты выполнить определенный код или нет. Для этого даже предусмотрен механизм получения привилегий — вызовы таких WinAPI-функций как RtlAdjustPrivilege, AdjustTokenPrivileges и прочих. К примеру, просто так вызвать WinAPI ExitWindowsEx() у нас не получится, для этого вызывающий код должен обладать соответствующими привилегиями, что в классическом варианте выглядит вот таким образом (код поскипан):

VOID sutdownSystem()
{
   
if (!OpenProcessToken(GetCurrentProcess(),
TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken))
   
{...}
   
LookupPrivilegeValue(NULL, SE_SHUTDOWN_NAME,
&tkp.Privileges[0].Luid);
   
tkp.PrivilegeCount = 1;
   
tkp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
   
AdjustTokenPrivileges(hToken, FALSE, &tkp, 0,
(PTOKEN_PRIVILEGES)NULL, 0);
   
if (!ExitWindowsEx(...))
   
{...}
}

Идея, заложенная разработчиками Windows в данном (или любом другом похожем) коде, в принципе абсолютно нормальна, понимаема и адекватна: нет привилегий для выполнения операций — попробуй их получить! При этом все делается исключительно из благих намерений — защитить систему от несанкционированных действий (или кривых ручек) пользователя, могущих нанести ущерб безопасности системы. К примеру, начиная с Windows Vista все исполняемые программы по дефолту не имеют администраторских прав. В честной системе для увеличения своих прав нужно их честно запрашивать.

Но мы жить честно не привыкли, нам нужно все, сразу и много, ведь верно? :). В общем, как гласит одна популярная поговорка: «Если нельзя, но очень хочется, тогда можно». Так и поступим, причем самыми простыми средствами, без всяких хитрых изворотов, перехватов, сплайсинга, недокументированных функций и прочих ненужных в нашем случае вещей.

«Как такое возможно?», — спросишь ты. Сейчас увидишь, как выполнить привилегированный код, не получая привилегий. Итак, долой привилегии!

Как ты знаешь, определенная часть важных системных функций, представленных в kernel32.dll и ntdll.dll, являются так называемыми форвардингами. То есть, определенные функции в kernel32. dll и ntdll.dll на самом деле являются «заглушками». Например, создание файла происходит примерно так: kernel32!CreateFileW K ntdll!NtCreateFile ‰„ [вызов INT 0x2e] V ntos!ZwCreateFile — (…).

Наблюдение за системой в различных условиях показало, что при штатном вызове системных Nt*-функций прямое обращение с соответствующими параметрами напрямую к обработчику прерывания INT 0x2e позволит вызывающему обойтись без вызовов kernel32!CreateFileW или ntdll!NtCreateFile. Таким образом, все, что нам нужно — это напрямую дернуть INT 0x2e, передав обработчику этого прерывания нужные параметры. И что самое забавное — такой трюк пройдет без получения необходимых привилегий, достаточно просто вызвать приведенный ниже код из своей программы! Добавлю, что прерывание INT 0x2e появилось начиная с Windows 2000. И хотя с WinXP была введена специальная инструкция SYSENTER, ради совместимости прерывание INT 0x2e было оставлено.
Код очень прост, он всего лишь повторяет то, что делает сама система:

__declspec(naked) NTSTATUS __cdecl NtCallStub(
__in ULONG SdtNumberOfFunc, ...)

{
__asm
{
mov eax, [esp+4]
lea edx, [esp+8]
int 0x2e
ret
}
}
// здесь SdtNumberOfFunc — номер Nt*-функции
// в таблице SSDT

Таким вот нехитрым способом можно вызвать любую NT*-функцию без всякой RtlAdjustPrivilege. Не веришь?

Попробуй сам. Правда, на x64-битных системах указанный код работать не будет, поскольку там для вызова шлюза используется команда SYSENTER (думаю, ты без труда справишься и с этим, написав универсальную обертку под SYSENTER). В качестве полезнейшего побочного эффекта вызова нужных функций через шлюз INT 0x2e хочу отметить следующее: этот способ можно использовать для обхода перехваченных юзермодных функций. Ну, к примеру, решили мы что-то записать в реестр через вызов WinAPI — NtCreateKey(). Эта функция экспортируется ntdll.dll, и «правильные» аверы перехватывают ее (впрочем, как и все другие функции для работы с реестром). Если посмотреть в отладчик, то можно увидеть, что вызов NtCreateKey представляет собой в конечном итоге лишь передачу входных параметров функции шлюзу INT 0x2e с номером самой функции.

Параметры будут переданы в свою очередь уже кернелфункции ZwCreateKey, которая создаст новый ключ в реестре. По логике аверов, если перехватить функцию ntdll.dll!NtCreateKey, то ее вызывающий обязательно попадется в ловушку авера. Но не тут-то было… Если обратиться напрямую к шлюзу, то перехватчик, установленный на вызове NtCreateKey, останется в неведении, что кто-то подлез под него, и со спокойной душой творит свои темные дела. К большой головной боли разработчиков всяких там хипсов, проактивок и аверов, теперь чтобы обойти хук ntdll.dll!NtCreateKey достаточно вызвать вышеуказанную обертку обработчика INT 0x2e.

Такой трюк, во-первых, не требует получения возможных привилегий для своего вызова, а во-вторых на самом деле вызовет системную API NtCreateKey() и тем самым посадит в лужу половину аверов, которые все еще надеются перехватить вызов опасных для системы юзермодных функций. Хотя справедливости ради надо признать, что для отлова таких хитропопых вывертов нужно лишь перехватить INT 0x2e (или SYSENTER). Это делают наиболее продвинутые антивирусы, однако далеко не все.

 

Трюк #2, или свой антивирус на скорую руку

Основываясь на первом трюке, можно в течение пары дней или нескольких часов (при наличии прямых рук, разумеется, и исходников с диска) соорудить некое реалистичное подобие проактивной защиты, которая с очень большой вероятностью сможет определить наличие юзермодных перехватов системных функций. В основе данной «проактивки» будет лежать независимое вычисление и анализ стартовых адресов потоков, а также проверка пролога функций на предмет не установлен ли в первых пяти байтах функции JUMP куда-то-тамдалеко.

if( threadHandle = OpenThread(THREAD_GET_CONTEXT, FALSE,
currThreadEntry.th32ThreadID ) )
{
   
StartAddress = GetThreadStartAddress( threadHandle );
   
if( ( StartAddress < 0x00401000 ||
StartAddress > 0x0040156B ) && StartAddress < 0x70000000 )
   
{
       
// подозрение на перехват
   
}
   
else
   
{
   
NtGetContextThread( threadHandle, &ctx );
   
if( ( ctx.Eip < 0x00401000 || ctx.Eip > 0x0040156B )
&& ctx.Eip < 0x70000000 )
       
// подозрение на перехват
   
}
   
NtClose( threadHandle );
}

Здесь вызов NtGetContextThread будет происходить напрямую через INT 0x2e с передачей номера функции и необходимых параметров. Полный код, реализующий портативную проактивку, ты сможешь найти на диске. В ее основе, как я уже отмечал, лежит возможность вызова необходимых функций непосредственно через INT 0x2e.

 

Трюк #3 или свои malloc/realloc в ядре

Действительно, если malloc можно заменить на ExAllocatePool, то окажется, что аналога такой функции как realloc в ядре нет. Что же делать? Рекомендую реализовать свой аналог malloc/realloc, тем более что они оказываются крайне простыми и являют собой просто оболочку под вызов функции ExAllocatePoolWithTag (функция ExAllocatePool, по утверждению MSDN, является устаревшей, и ее рекомендуется заменить на ExAllocatePoolWithTag).

VOID * malloc(ULONG size)
{
PVOID data = 0;
data = ExAllocatePoolWithTag(PagedPool, size, "Tag");
memset(data, 0x0, size);
return data;
}

И, соответственно, реализация псевдофункции realloc:

VOID * realloc(PVOID memPtr, ULONG size, ULONG oldSize)
{
PVOID newPtr = 0;
newPtr = ExAllocatePoolWithTag(PagedPool, size, "Tag");
if( !newPtr )
return 0;
if ((oldSize) && (memPtr))
{
RtlMoveMemory( newPtr, memPtr, oldSize);
ExFreePool(memPtr);
}
return newPtr;
}

 

Заключение

Не стоит принимать все написанное всерьез, но думаю, что идеи, приведенные в статье, тебе понравятся, и ты сможешь найти им достойное применение. Удачного компилирования и да пребудет с тобой Сила!

 

Links

Блог главного разработчика ReactOS Алекса Ионеску (море интересной и увлекательной инфы о внутренностях Windows): alex-ionescu.com

Теги:

Оставить мнение

Check Also

Эксплоиты в десятку. Обзор самых интересных докладов с мировых ИБ-конференций

В последние годы мы отучились воспринимать Windows как нечто невероятно дырявое. Эта опера…