понедельник, 17 октября 2011 г.

Игра в прятки на уровне ядра


Процессы в Windows (как и в любой другой ОС) – это наше все. От ядра до калькулятора, операционная система представляет собой лишь набор процессов. Когда ты дважды кликаешь на значок какой-нибудь программки, чтобы запустить ее, в недрах системы приводятся в действие огромные ресурсы, выделяется память, вызываются десятки Native API... И сегодня мы поговорим о том, как эффективно использовать эти ресурсы в решении нетривиальных задач при работе с процессами на уровне ядра Windows.

Процессы в Windows

С некоторой натяжкой процессом в Windows можно назвать набор байт в оперативной памяти. Это если в целом. А в частности – процессом обычно называют экземпляр программы, загруженной в оперативную память и выполняемой Windows. Любой процесс состоит из:
  • структур данных, содержащих всю информацию о процессе, в том числе, список открытых дескрипторов различных системных ресурсов, уникальный идентификатор процесса, различную статистическую информацию и т.д.;
  • адресного пространства – диапазона адресов виртуальной памяти, которым может пользоваться процесс (4 GB, если помнишь);
  • исполняемого кода и данных, которые проецируются в виртуальное адресное пространство процесса.
Мало-мальски опытный кодер знает, что создание Win32 процесса осуществляется вызовом одной из таких функций, как CreateProcess, CreateProcessAsUser (для Win NT/2000) и CreateProcessWithLogonW (начиная с Win2000) и т.д. Но все это касается только юзермода. Если посмотреть на создание процесса на уровне ядра, то волосы начнут шевелиться от того количества ресурсов и системных вызовов, которые ОС задействует при создании нового процесса! За подробностями отсылаю тебя к книге М. Руссиновича и Д.Соломона «Внутреннее устройство Windows», хотя, по правде, с программистской точки зрения она мало что даст. Поэтому, если тебя заинтересует программный процесс создания и запуска процесса из ядра, пиши мне на мыло, обсудим.
Что же может дать честному хакеру доступ к адресному пространству процесса? Да все что угодно! От изменения данных процесса и манипулирования его окружением, вплоть до запуска своего зловредного (или не очень) кода в чужом адресном пространстве.
Получить доступ к адресному пространству чужого процесса можно, все эти способы хорошо документированы. Одно плохо - они довольно эффективно палятся проактивками и специальными утилитами. Что делать? Лезть в нулевое кольцо! Уж там-то нам никакой NOD или Касперский перечить не будет!

Сокрытие процесса

Была раньше такая фича - заказчики малвари, следуя модным тенденциям, требовали от программиста «шоб в процессах не видна была». Это и сейчас довольно распространенный хакерский прием, нацеленный на сокрытие от бдительных глаз процесса трояна (руткита, вируса и пр.). Оговорюсь сразу, что способа, гарантирующего 100% невидимость процесса для различных утилит, антивирей и проактивных защит, не существует. Скрытый процесс можно выявить всегда. Как? Подробности можешь почитать на www.wasm.ru.
Для скрытия процессов в User Mode обычно используется технология внедрения своего кода в чужие процессы и перехвата функции ZwQuerySystemInformation(SystemProcessesAndThreadsInformation , ...) из ntdll.dll. Оно сплайсится и из полученного списка убирается интересующий нас процесс.
Впрочем, не будем углубляться в данную тему, а поговорим о том, как скрыть процесс, находясь в ядре Windows. Сплайсить ничего не будем, поскольку это очень легко выявить. Мы пойдем другим путем. Техника не нова, но достаточно эффективна.
Каждый процесс в ОС Windows представлен структурой EPROCESS. Кроме атрибутов процесса, она ссылается на несколько других структур, связанных с выполняющимся процессом. Например, с каждым процессом связан один или несколько потоков, представленных в системе структурой ETHREAD. Структуры EPROCESS, в свою очередь, связаны в круговой двусвязный список – то есть, прошу прощения за тавтологию, в каждой такой структуре присутствует указатель на LIST_ENTRY, содержащий указатели на предыдущую и последующую структуры. Наша цель - найти текущий EPROCESS, пробежаться по всему списку, найти процесс, который надо скрыть, после чего слинковать предыдущий и последующий процессы. При этом обязательно надо помнить о том, что в Windows от билда к билду меняется «состав» недокументированных структур и, соответственно, смещения на нужные нам поля.
Первое, что нужно сделать, это получить указатель на структуру EPROCESS. Это делается вызовом функции PsGetCurrentProcess():
PEPROCESS ePROC = PsGetCurrentProcess().
Не будем сейчас разбирать, как именно она действует – если интересно, покопайся в отладчике. Вызовом ZwQuerySystemInformation(SystemProcessesAndThreadsInformation, ...) находим нужный нам процесс из общего списка (по PID'у процесса или по его имени). Затем немного поколдуем с двусвязными списками из структуры EPROCESS и... вуаля! запущенный процесс исчезает из менеджера задач.
Вот, в принципе, и все. Драйвер, реализующий сокрытие процесса из менеджера задач, ищи на диске.

Сокрытие загруженной в процесс dll

Часто малварь поставляется конечному юзеру не как отдельный exe-шник (поскольку его легко отловить), а как отдельная библиотека dll, подгруженная в адресное пространство какого-нибудь процесса. А что, совсем недурно: отдельного процесса нет, а искать подгруженную dll в адресном пространстве другого процесса – задача трудоемкая. Этот нехитрый хакерский прием заключается в том, чтобы записать в память чужого процесса свой код через вызов VirtualAllocEx/WriteProcessMemory и затем выполнить его посредством CreateRemoteThread.
Впрочем, справедливости ради скажу, что трюк палится антивирями. Попробуем немного осложнить им жизнь - скроем нашу зловредную dll из списка загруженных в АП процесса.
Как ты помнишь, в ядре Windows для работы с процессами используется целая куча структур. Одна из самых важных - это «Блок переменных окружения процесса» (Process Environment Block, PEB). Блок представляет собой недокументированную и критически важную для нормального функционирования процесса структуру, которая создается и заполняется на стадии создания процесса. К примеру, в ней содержатся такие важные поля, как PEB_LDR_DATA или PROCESS_PARAMETERS. А вот уже в PEB_LDR_DATA содержится список библиотек, загруженных в адресное пространство процесса. Он-то нам и нужен! Получив указатель на PEB, мы заимеем указатель на двусвязный список dll, загруженных в процесс: Peb -> LoaderData -> InLoadOrderModuleList.
Теперь, чтобы спрятать dll от всяческих утилит, просто поступим так же, как и в случае сокрытия процесса из списка задач - слинкуем соседние указатели в InLoadOrderModuleList. Итак, добываем указатель на PEB текущего процесса в Usermode:
PEB* GetPEB()
{
PEB* pPeb;
__asm {
push fs:[0x30]
pop pPeb
}
return pPeb;
}
Также можно вызвать функцию ZwQueryInformationProcess с классом информации ProcessBasicInformation. В этом случае в буфер запишется структура PROCESS_BASIC_INFORMATION, одно из полей которой и есть указатель на PEB.
Замечу, что адреса PEB для всех процессов аналогичны, более того, PEB всегда находится в виртуальном адресном пространстве процесса по адресу 0x7FFDF000.
В ядре получить указатель на PEB можно следующим образом:
PEPROCESS eProcess = PsGetCurrentProcess();
pPeb = (PVOID)(*(PULONG)((PCHAR)eProcess + PebOffset)).
Вот еще одно нехитрое решение. Чтобы защитить процесс от попытки найти там скрытую dll, сделаем перехват функции NtReadVirtualMemory. Затем точно также найдем список загруженных библиотек, исключим нашу dll-ку и возвратим управление вызвавшему коду. Так, путем манипуляций с PEB'ом можно осуществить защиту внедренной dll от чтения. Оба примера, реализованных на С, ищи на диске.
Техника сокрытия dll, которую мы только что рассмотрели, не является чем-то принципиально новым, и ты легко сможешь с ней разобраться. Но это только начало. Следующим этапом будет техника подмены самого PEB, когда путем таких же манипуляций в таргет-процесс осуществляется подгрузка фейковой dll вместо какой-нибудь законопослушной user32.dll. Но об этом в другой раз.

Инжект кода

А теперь – о самом интересном. Нам необходимо исполнить свой не очень добропорядочный код в контексте доброго и хорошего процесса. То бишь, выполнить инжект кода – из ядра и не привлекая внимания бдительных проактивок.
Для новичков, возможно, это будет непросто, но, если у тебя есть опыт программирования в ядре, ничего трудного тут нет. Механизм ОС Windows предусматривает APC Asynchronous Procedure Call – что-то типа «сообщений», которые могут доставляться потоку для выполнения определенных функций, причем поток о них может ничего не знать. Их-то мы и задействуем. Доставляющий код определяет свою callback-функцию для APC, которая будет выполнена в контексте требуемого потока. Например, механизм APC используется в таком важном механизме потоков Windows, как приостановка и восстановление (suspend/resume) потока. Главное преимущество APC с точки зрения малвари - это то, что APC для антивирей и практивок - совершенно нормальная операция, выполняемая ОС, и поэтому соотнести вызов APC со зловредными действиями малвари ей будет крайне трудно. Более подробно (правда, на английском) о вызове APC можно почитать здесь: http://www.cmkrnl.com/arc-userapc.html.
Итак, первое: выделяем кусок памяти в виде MDL, который затем будем использовать для записи нужного нам кода. Затем аттачимся к таргет-процессу, после чего лочим в юзермодной памяти выделенный нам участок памяти. Отключаемся от процесса и вызываем проинициализированный APC, который будет выполнен системой в контексте одного таргет-процесса.
Здесь нужно предусмотреть поток TargetThread, где будет выполняться код. Обычно при решении этой проблемы находится существующий поток в процессе вызовом PKTHREAD thread=KeGetCurrentThread().

Использование APC для инжекта кода

pMdl = IoAllocateMdl(pPayloadBuf, dwBufSize,
FALSE, FALSE, NULL);
MmProbeAndLockPages(pMdl, KernelMode, IoWriteAccess);
KeStackAttachProcess(pTargetProcess, &ApcState);
MappedAddress = MmMapLockedPagesSpecifyCache(pMdl,
UserMode, MmCached, NULL, FALSE, NormalPagePriority);
KeUnstackDetachProcess(&ApcState);
KeInitializeEvent(pEvent, NotificationEvent, FALSE);
KeInitializeApc(pApc, pTargetThread,
OriginalApcEnvironment, &MyKernelRoutine, NULL,
MappedAddress, UserMode, (PVOID)NULL);
KeInsertQueueApc(pApc, pEvent, NULL, 0)
В дополнение к вышесказанному могу сказать, что, приаттачившись к какому-либо процессу, кроме инжекта кода, можно без особых проблем считывать нужные данные через вызов NtReadVitualMemory или же производить запись в адресное пространство процесса соответственности через NtWriteVirtualMemory. К примеру, что-нибудь вроде:
PEPROCESS process = PsGetCurrentProcess();
KeAttachProcess(process);
NtWriteVirtualMemory(process_handle, address,
buffer, numbytes, NULL);
}
KeDetachProcess();
Не забудь проверить, имеет ли секция, куда хочешь произвести запись, аттрибут «writeable», иначе попытка записи в non-writeable участок памяти приведет к BSOD'у. Таковой по умолчанию является секция кода, но проблема решается легко - нужной секции просто присваивается аттрибут для записи.
«Так почему именно ring0, ведь тот же самый инжект кода можно успешно сделать и в юзермоде?», - можешь спросить ты. Открою тайну - кодинг в нулевом кольце не так страшен, как может показаться на первый взгляд и, вмешиваясь в работу ядра своими руками, ты начнешь чувствовать себя «повелителем машин». 

Комментариев нет:

Отправить комментарий