Mastodon

Monday, December 27, 2010

Обход детектирования скрытого исполняемого кода

Репост из блога Esage Lab

Универсальные антируткиты, как инструменты для надёжной борьбы со скрытыми объектами, за последние несколько лет стали очень сильно проигрывать в гонке технологий с писателями malware, чьи разработки постепенно уходят от классической модели реализации (драйвер + скрытый файл + скрытый системный сервис). Всем известно, что хороший современный руткит представляет собой уже не отдельный файл, а просто кусок исполняемого кода, получающий управление из зараженного драйвера или загрузочного сектора. В настоящее время присутствует довольно много вредоносных программ, которые нельзя достоверно детектировать (а уже тем более удалить) используя RootkitUnhooker, GMER, RootRepeal или любой другой антируткит подобного класса.

Однако, антируткиты по-прежнему остаются актуальными для детектирования системных аномалий в более широком понимании этого слова: модификация кода в памяти, DKOM, драйвера-фильтры или использование стандартных механизмов нотификации, и так далее. Кроме всего перечисленного важной функцией хороших антируткитов является и детектирование скрытого кода в памяти - кода, который исполняется на процессоре и при этом не принадлежит какому-либо файлу на диске. Данная техника детектирования интересна тем, что способна указать на факт заражения системы абсолютно любым сложным руткитом (из тех, что имеют распространение в настоящий момент) даже в том случае, если другие его скрытые объекты обнаружены не были (зараженные/подмененные файлы, загрузочные сектора, итд.).

Выглядит детектирование скрытого исполняемого кода так:

Rootkit Unhooker

Kernel Detective

RootRepeal

Safe'n'Sec Rootkit Detector

Подобным образом детектируются такие, казалось бы, продвинутые руткиты как TDSS, ZeroAccess а так же практически все известные буткиты, благодаря чему данные механизмы часто используются специалистами как средство для быстрого первичного диагностирования состояния системы.
Существует мнение, что обход детектирования скрытого исполняемого кода не возможен без реализации собственного планировщика потоков или других сложных техник, однако, мы решили показать, что на практике дела обстоят несколько проще, и всерьёз полагаться на такое детектирование не стоит.

Рассмотрим возможные способы реализации поиска скрытого исполняемого кода:
  1. Проверка стартовых адресов потоков, хранящихся в полях StartAddress и Win32StartAddress структуры _ETHREAD. Наиболее примитивный способ.
  2. Перехват ключевых функций ядра (например - ExAllocatePool или IofCallDriver) с проверкой адреса возврата на предмет принадлежности образу какого-либо загруженного драйвера. Способ, который относительно легко обходится путём загрузки в память своей копии ядра. Кроме того, не способен детектировать скрытый код в том случае, если исполняющий его поток большую часть времени проводит в "спящем" состоянии.
  3. Перехват планировщика. Заключается в перехвате какой-либо ключевой функции планировщика потоков, которая вызывается при их переключении (например - SwapContext). В обработчике перехвата текущие адреса потоков, которые отдают или получают квант процессорного времени, проверяются на предмет принадлежности образу какого-либо загруженного драйвера. Данный способ так же не способен детектировать код который исполняется потоком, находящимся в состоянии ожидания большую часть времени.
  4. Анализ указателей. Заключается в поиске активных перехватов кода в ядре (системные вызовы, IRP-обработчики, сплайсинг, нотификаторы) с последующей проверкой адресов обработчиков этих перехватов на предмет принадлежности образу какого-либо загруженного драйвера. Более надёжный способ, но работать он будет только в том случае, если руткит устанавливает какие-либо перехваты заведомо известного типа.
  5. Анализ стеков вызовов. Заключается в перечислении всех активных потоков и получении стека  вызовов для каждого из них. Адрес каждой функции из стека вызовов проверяется на предмет принадлежности образу какого-либо загруженного драйвера. Самый совершенный способ, свободный от перечисленных выше недостатков.
После ознакомления с перечисленными реализациями на ум приходит идея копировать исполняемый код руткита поверх загруженного в память образа какого-либо стандартного системного драйвера (не трогая его файл на диске, разумеется). Для этого должны быть удовлетворены следующие условия:
  1. Не должно нарушаться нормальное функционирование загруженного драйвера.
  2. Участок образа загруженного драйвера (который будет использоваться для хранения кода руткита) должен быть выбран таким образом, что бы антируткит не обнаружил каких-либо модификаций при сверке кода в памяти с тем, что содержится в файле на диске.
Данные условия соблюдаются при внедрении кода руткита в так называемые Discardable-секции PE файла (те, которые имеют установленный флаг IMAGE_SCN_MEM_DISCARDABLE). В случае с драйверами режима ядра Discardable-флаг указывает PE-загрузчику на то, что секция может быть выгружена из памяти после завершения инициализации драйвера. Обычно, Discardable-флаг имеют секции содержащие ресурсы, базовые поправки или инициализационный код драйвера. Такие секции помещаются линковщиком в конец образа:
kd> !dh beep.sys

File Type: EXECUTABLE IMAGE
FILE HEADER VALUES
     14C machine (i386)
       5 number of sections
3B7D82E5 time date stamp Sat Aug 18 00:47:33 2001

   ...
     
SECTION HEADER #3
    INIT name
     284 virtual size
     880 virtual address
     300 size of raw data
     880 file pointer to raw data
       0 file pointer to relocation table
       0 file pointer to line numbers
       0 number of relocations
       0 number of line numbers
E2000020 flags
         Code
         Discardable
         (no align specified)
         Execute Read Write

SECTION HEADER #4
   .rsrc name
     3C8 virtual size
     B80 virtual address
     400 size of raw data
     B80 file pointer to raw data
       0 file pointer to relocation table
       0 file pointer to line numbers
       0 number of relocations
       0 number of line numbers
42000040 flags
         Initialized Data
         Discardable
         (no align specified)
         Read Only

SECTION HEADER #5
  .reloc name
      9A virtual size
     F80 virtual address
     100 size of raw data
     F80 file pointer to raw data
       0 file pointer to relocation table
       0 file pointer to line numbers
       0 number of relocations
       0 number of line numbers
42000040 flags
         Initialized Data
         Discardable
         (no align specified)
         Read Only

Как известно, в общем случае загрузка драйвера режима ядра осуществляется с помощью функции ZwLoadDriver
Рассмотрим её работу:
  1. ZwLoadDriver получает из указанного ключа реестра системной службы путь к файлу её драйвера, после чего вызывает функцию MmLoadSystemImage.
  2. MmLoadSystemImage выполняет чтение исполняемого файла с диска и его проецирование в память, после завершения которого вызывает функцию PsCallImageNotifyRoutines.
  3. PsCallImageNotifyRoutines выполняет вызов нотификаторов, которые были установлены с помощью функции PsSetLoadImageNotifyRoutine.
  4. MmLoadSystemImage вызывает точку входа загруженного образа. Если точка входа вернула статус ошибки - образ немедленно выгружается. В случае успеха вызывается функция MmFreeDriverInitialization.
  5. MmFreeDriverInitialization перечисляет все секции образа и выгружает те их них, которые имеют флаг IMAGE_SCN_MEM_DISCARDABLE.
Для демонстрации техники сокрытия исполняемого кода в Discardable-секциях был разработан PoC, который представляет собой драйвер режима ядра работающий следующим образом:
  1. Драйвер устанавливается в систему как служба, имеющая тип запуска SERVICE_BOOT_START (запускающаяся на ранних этапах загрузки операционной системы).
  2. Во время инициализации драйвер устанавливает нотификатор на загрузку исполняемых образов с помощью функции PsSetLoadImageNotifyRoutine. Функция-нотификатор ожидает загрузки системного драйвера, Discardable-секции которого будут использоваться для хранения скрытого исполняемого кода. В PoC-е, в качестве таких драйверов, были выбраны beep.sys, ndiswan.sys, i8042prt.sys или wanarp.sys.
  3. При загрузке любого из перечисленных драйверов PoC выполняет перехват его точки входа методом модификации кода и отключает нотификатор.
  4. Обработчик перехваченной точки входа вызывает оригинальную, а после того, как она вернула управление - снимает флаг IMAGE_SCN_MEM_DISCARDABLE с секций драйвера-жертвы (для того, что бы предотвратить выгрузку этих секций из памяти) и копирует поверх них свой код. 
  5. После того, как код был успешно внедрен в Discardable-секции - с помощью функции PsCreateSystemThread создается системный поток, который начинает его исполнение.
  6. Созданный поток вызывает функцию ZwUnloadDriver для выгрузки уже ненужного драйвера PoC-а и приступает к исполнению "полезной нагрузки".
В качестве полезной нагрузки PoC всего лишь циклически выводит отладочное сообщение с пятисекундным интервалом, однако, вместо этого мог быть реализован и любой стандартный руткит-функционал.

Отладочные сообщения PoC-а

PoC работоспособен на всех 32-х разрядных версиях Windows начиная с XP, скрытый исполняемый код не детектируется ни одним публично доступным антируткитом (тесты были проведены на RootkitUnhooker, GMER, RootRepeal, Kernel Detective и Safe'n'Sec Rootkit Detector). 
Исходные тексты и исполняемые файлы PoC-а доступны для загрузки в виде архива, а так же в репозитории на GitHub-е.