Обламываем проактивку: элегантный обход проактивной защиты на уровне нулевого кольцаВ очередной статье из нашего цикла, посвященного «нулевому» кодингу, речь пойдет о том, как, обладая базовыми знаниями кодинга в нулевом кольце, серьезно озадачить разработчиков проактивных средств защиты (с файрволами мы уже разобрались, теперь пришла их очередь). Итак, драйвера мы писать уже умеем и, казалось, бы, вот оно — счастье! Но не тут-то было. Начинающие драйверописатели, как правило, дальше DbgPrint’а в ядре не заходят. Со временем, научившись более или менее работать с Native-функциями ядра, вдруг начинаешь хотеть чего-то большего. И вот, нарыв где-то на бескрайних просторах интернета код TDI-фильтра или функций работы с адресным пространством, радуешься, как ребенок. До тех пор, пока не узнаешь, что все твои усилия наперед известны антивирям, проактивкам и файрволам. В настоящее время флагманы борьбы с малварью вкладывают очень значительные средства в разработку передовых программных решений (в первую очередь, ring0-based), которые представляют собой сложные программные комплексы. И поверь мне, их разработчики зря свой хлеб не едят. Да, в ядре позволено все, но чтобы это «все» реализовать, нужно иметь за плечами несколько лет усердного кодинга в нулевом кольце, знать отладку кода, анализ крэшдампов, а также принципы работы проактивных защит. Опыт показывает, что задача выживания в ядре сильно усложняется контролем критических ядерных процессов со стороны всяческих «watchdog’ов» — стражей, выявляющих малейшую активность в ядре ОС. Для подобных проактивных защит, антивирусов и файрволов самый распространенный и зачастую единственный способ отследить телодвижения малвари в ядре — это перехват системных функций. К примеру, чтобы отследить создание и запуск процесса в ядре системы, чаще всего проактивные защиты перехватывают системную функцию ZwCreateProcess либо регистрируют свою callbackфункцию PsSetCreateProcessNotifyRoutine. Точно также загрузка сторонних dll в адресное пространство процесса отслеживается через регистрацию callback-функции PsSetLoadImageNotifyRoutine, а запись в адресное пространство — перехватом CreateRemoteThread или ZwWriteVirtualMemory; вариантов множество. При этом перехват системных функций сводится либо к созданию «заглушки» для системной функции, либо к ее сплайсингу (встречается чаще). Это в том случае, если перехватываемая функция находится в таблице SSDT — KeServiceDescriptorTable, экпортируется ядром или одним из драйверов. Гораздо реже встречается перехват неэкспортируемых функций, адрес которых в ядре находится поиском определенной последовательности байт и вычисляется дизассемблером длин инструкций. А раз выживать в ядре как-то надо, то приходится искать хитропопые способы заставить ядро выполнять то, что тебе нужно, при этом не попадаясь на удочку всяких проактивок. Ниже я расскажу, как можно в ядре беспалевно организовать создание, открытие, чтение и запись файлов, да так, что ни одна из проактивных защит и носом не поведет! О чем речь?Я не буду читать тебе лекции о мегасекретных функциях и особенностях ядра ОС Windows — обойдемся тем, что лежит на поверхности. Речь пойдет об основах коммуникационных взаимодействий в ядре — IRP-пакетах. IRP-пакет — это пакет запроса ввода/вывода (I/O Request Packet). Он является основной формой передачи информации между ядром, драйверами и пользовательскими приложениями. Думаю, здесь особых проблем с восприятием возникнуть не должно. Вместе с тем, путь прохождения IRP-пакета по стеку драйверов невероятно тернист и непонятен для непосвященного взгляда, а разобраться в нем придется, если хочешь овладеть той техникой, которая здесь описывается, хотя бы в самых общих чертах. IRP-пакет обрабатывается драйвером и для его «захвата» (слово некорректно, но вполне подходит по смыслу) используется структура под названием IO_STACK_LOCATION. Эта структура жизненно необходима при работе с IRP-пакетами, и ни один драйвер, создающий или котролирующий IRP-пакеты, не обходится без ее использования. От себя добавлю — научиться правильно обрабатывать IRP-пакеты с выгодой для себя во время их прохождения в стеке драйверов — задача не из легких. Жизненный цикл IPR-пакета начинается с его создания I/O менеджером (корректнее будет сказать, что IRP-пакет все же создается исполнительными модулями системы — ядром, драйверами и пр., I/O менеджер в данном случае — собирательное понятие). Новый IRP-пакет создается вызовом таких системных функций как IoBuildAsynchronousFsdRequest, IoBuildSynchronousFsdRequest, IoBuildDeviceIoControlRequest и IoAllocateIrp. Первые две функции предназначены для работы с файловой системой, однако ничто не помешает нам вызывать их напрямую. IoBuildDeviceIoControlRequest предназначен для создания IRP-пакетов с Major-кодом IRP_ MJ_DEVICE_CONTROL и IRP_MJ_INTERNAL_DEVICE_CONTROL.IoAllocateIrp создает IRP-пакет любого типа. Если привести аналогию, то IRP-пакет сродни SMS-ке, посредством которой ты можешь общаться со своими друзьями и давать им какие-либо указания. Так, стоит послать SMS другу насчет совместного распития пива или девушке для более интересного времяпрепровождения — эта же SMS станет для них направлением к действию. Ситуация с IRP-пакетами в ядре полностью аналогична — к примеру, чтобы создать файл, нужно послать драйверу файловой системы определенный IRP-пакет, получив который, он (т.е. драйвер) файл и создаст, основываясь на тех данных, который IRP-пакет принес с собой. Функция CreateFile и ее «заглушка» в ntdll.dll — NtCreateFile сводятся именно к созданию такого пакета и передаче его драйверу ФС. Чтобы поглубже разобраться со структурой и прохождением различных IRP-пакетов в системе, можно воспользоваться замечательной утилитой IRPTrace (которую я бережно выложил на диск). Она позволяет проконтролировать прохождение IRP-пакетов по всем устройс твам, зарегистрированным в Windows, и является незаменимой, если хочешь быть в теме. Так что же нам помешает, не вызывая таких функций, как ZwCreateFile/ZwReadFile/ZwWriteFile, собственными ручками создать необходимый IRP-пакет и отправить его на исполнение? Ровным счетом ничего! Вероятно, ты подумаешь, что для контроля за созданием и пересылкой IRP-пакетов разработчики проактивных защит могут организовать их перехват. Теоретически это возможно, однако перехват даст непосильную нагрузку на антивирус, который окажется похороненным под лавиной IRP-пакетов, потому что таких в системе ежесекундно создается несколько сотен, если не тысяч, и проконтролировать их все — задача малореальная. Этим мы и воспользуемся — не вызывая системных функций мы ручками создадим нужный нам IRP-пакет, наделим его необходимой функциональностью и заставим систему его переварить! Stealth-создание и чтение файлаДля того чтобы создать файл, не вызывая Native-функции ядра ZwCreateFile (или реже — IoCreateFile), мы будем следовать очень упрощенному алгоритму: 1) Вызовом функции ObCreateObject с параметрами KernelMode и IoFileObjectType создадим основу файла — пустую структуру FILE_OBJECT. Вот примерный скелет кода, реальный код ты можешь найти на диске: Создаем файл через IRP-пакет
Чтение файла выглядит немного проще. При чтении файла нам понадобится указатель на FILE_OBJECT для того файла, который нужно прочесть. Опять-таки создадим пустой IRPпакет, далее — вызовом IoAllocateMdl выделим кусок памяти под те данные, что мы будем читать. Заполним IRP-пакет нужными значениями, не забыв указать флаг пакета, равный IRP_READ_OPERATION. Создадим стек с полем MajorFunction, равным IRP_MJ_READ, и отправим пакет на выполнение. Вот и все! Единственное отличие от чтения файла будет во флагах, которые нужно назначить IRP-пакету — для записи в файл нам нужен флаг IRP_WRITE_OPERATION. Пишем в файл через создание IRP-пакета:
Для полноты открывающейся взору картины могу добавить, что в драйвере новичку может встретиться незнакомая на вид функция — IoSetCompletionRoutine. Она регистрирует особый обработчик — IoCompletion, который будет вызван, когда следующий по уровню драйвер закончит обработку нужного нам IRP-пакета. В нашем случае эта функция нужна, если мы хотим увидеть и проконтролировать выполнение нашего IRP-пакета после прогулки по стеку драйверов. Пишем в файлДо смешного просто выглядит в ядре удаление файла — все, что нам нужно, это вызвать функцию IoSetInformation с переданными ей параметрами структуры FILE_DISPOSITION_INFORMATION. Правда, справедливости ради, надо сказать, что многие «файловые файрволы», как их любят называть, ставят фильтры на устройства файловой системы, и подобный трюк для них — ерунда.
ЗаключениеТаким же нехитрым образом можно вполне успешно совершать множество действий в ядре и обойтись без вызова основных системных функций. Это позволит довольно безопасно существовать, а главное — выживать в ядре операционной системы, не привлекая внимания бдительных стражей — антивирей и проактивных защит. Ты можешь спросить: «а как же запись в адресное пространство процесса, являющегося основой удаленного внедрения кода? Каким образом можно добиться этого, не привлекая внимания антивирей и проактивок?». «Неразрешимых задач нет, — отвечу я. — Из этой ситуации можно выйти, задумавшись о перезаписи или подмене PTE для нужного процесса». Именно так, к примеру, работает руткит ShadowWalker. Не бойся экспериментировать, удачного компилирования и да пребудет с тобой Сила! LinksКачаем последнюю версию WinDbg — незаменимого отладчика и анализатора крэшдампов — http://msdl.microsoft.com/download/symbols/debuggers/dbg_ x86_6.11.1.404.msi. |