November 11, 2021

На страже ядра. Обнаруживаем руткиты с помощью нового плагина DRAKVUF

Содержание статьи

  • Техники
  • Inline-перехваты
  • Перехват таблицы системных вызовов
  • Перехваты IDT/GDT
  • MSR LSTAR
  • Перехваты функций DRIVER_OBJECT
  • Техника сокрытия процесса от Task Manager
  • Регистрация системных функций обратного вызова
  • Заключение

Ча­ще все­го при ана­лизе поведе­ния подоз­ритель­ной прог­раммы наб­люда­ют за поль­зователь­ским режимом, а код в ядре оста­ется вне поля зре­ния ана­лити­ка. Отчасти это пра­виль­но, пос­коль­ку боль­ше все­го вре­донос­ной деятель­нос­ти ведет­ся имен­но в поль­зователь­ском прос­транс­тве. Тем не менее вре­донос­ный код в ядре может нанес­ти боль­ше ущер­ба, и его слож­нее обна­ружить. Одна­ко это воз­можно.

С появ­лени­ем KPP (Kernel Patch Protection), или сок­ращен­но PatchGuard, в Windows ста­ло слож­нее модифи­циро­вать ядро без пос­ледс­твий. Если рань­ше, нап­ример, таб­лицу сис­темных вызовов с целью филь­тра­ции сис­темных вызовов перех­ватыва­ли даже такие легитим­ные прог­раммы, как анти­виру­сы, то с появ­лени­ем KPP это ста­ло не так прос­то. Тем не менее для рут­китов PatchGuard не пред­став­ляет осо­бой угро­зы, пос­коль­ку тех­ники его обхо­да мож­но най­ти в откры­том дос­тупе, они раз­вива­ются и акту­аль­ны по сей день. В этой статье будет рас­смат­ривать­ся мно­жес­тво тех­ник модифи­кации ядра, вклю­чая те, что обна­ружи­вает PatchGuard, на при­мере нового пла­гина для DRAKVUF — rootkitmon.

Мы уже нес­коль­ко лет раз­рабаты­ваем песоч­ницу для рис­кори­енти­рован­ной защиты, исполь­зующую фрей­мворк DRAKVUF. DRAKVUF — безаген­тная песоч­ница, осно­ван­ная на биб­лиоте­ке LIBVMI и гипер­визоре XEN. Все воз­можнос­ти DRAKVUF реали­зова­ны в пла­гинах. Каж­дый из двад­цати с лиш­ним пла­гинов выпол­няет опре­делен­ную работу: для обна­руже­ния дос­тупа к фай­лам есть пла­гин filetracer, для трас­сиров­ки сис­темных вызовов — пла­гин syscalls. Rootkitmon — новый пла­гин, поз­воля­ющий отсле­живать вре­донос­ную активность в ядре средс­тва­ми DRAKVUF.

Су­щес­тву­ет мно­жес­тво опре­деле­ний понятия «рут­кит», мы же будем опи­рать­ся на сле­дующее: «компь­ютер­ная прог­рамма, которая исполь­зует недоку­мен­тирован­ные и (или) зап­рещен­ные тех­ники для манипу­ляции ядром опе­раци­онной сис­темы в сво­их целях».

ТЕХНИКИ

В «джен­тель­мен­ский набор» пла­гина вхо­дят воз­можнос­ти детек­тировать сле­дующие типы перех­ватов:

  • мо­дифи­кация кода заг­ружен­ных драй­веров в памяти (inline-перех­ваты);
  • мо­дифи­кация таб­лиц сис­темных вызовов, таб­лиц пре­рыва­ний и таб­лиц дес­крип­торов;
  • мо­дифи­кация регис­тра MSR LSTAR;
  • мо­дифи­кация ука­зате­лей на фун­кции DRIVER_OBJECT, стек DEVICE_OBJECT;
  • сок­рытие про­цес­са из спис­ка EPROCESS;
  • ре­гис­тра­ция раз­личных сис­темных фун­кций обратно­го вызова.

Пос­коль­ку рут­китов нам­ного мень­ше, чем вре­донос­ного ПО в поль­зователь­ском прос­транс­тве, и вре­мя выпол­нения образца всег­да огра­ничен­но, умень­шение наг­рузки пла­гина на работа­ющую сис­тему было при­ори­тет­ной задачей. Для это­го во мно­гих слу­чаях было решено све­рять целос­тность кри­тичес­ких струк­тур в начале ана­лиза и в кон­це, а не выпол­нять непос­редс­твен­ный перех­ват на запись стра­ниц памяти.

Inline-перехваты

Рас­смот­рим самый рас­простра­нен­ный тип перех­ватов и, воз­можно, самый лег­ко обна­ружи­ваемый — inline-перех­ваты. Inline-перех­ваты очень популяр­ны, и даже Microsoft пре­дос­тавля­ет воз­можность перех­ватить API-биб­лиоте­ки, добав­ляя перед фун­кци­ями двух­бай­товый про­лог вро­де mov edi, edi для быс­тро­го редак­тирова­ния фун­кци­ональ­нос­ти уже заг­ружен­ных и работа­ющих ком­понен­тов. Конеч­но, такие перех­ваты воз­можны толь­ко в поль­зователь­ском режиме, а в ядре кара­ются синим экра­ном с кодом ошиб­ки 0x109, если PatchGuard не вык­лючен.

Inline-перех­ваты обыч­но сос­тоят из трех час­тей:

  • под­мена пер­вых нес­коль­ких инс­трук­ций целевой фун­кции для перенап­равле­ния потока выпол­нения на код сво­его при­ложе­ния;
  • об­работ­ка целевой фун­кции: изме­нение парамет­ров, филь­тра­ция, логиро­вание;
  • вы­пол­нение под­менен­ных инс­трук­ций и воз­врат на ори­гиналь­ную фун­кцию.

Рас­смот­рим прос­той при­мер вызова CreateFileW из биб­лиоте­ки kernel32.dll. Прой­дя все биб­лиоте­ки, в ито­ге код ока­жет­ся в ядер­ной фун­кции nt!NtCreateFile. Если бы рут­кит уста­новил перех­ват на эту фун­кцию, он бы мог выг­лядеть сле­дующим обра­зом.

Пос­коль­ку код находит­ся в стра­ницах с пра­вами толь­ко на чте­ние и выпол­нение, для записи в такие стра­ницы необ­ходимо либо выделить новую вир­туаль­ную стра­ницу с пра­вами на запись и спро­еци­ровать ее на физичес­кую стра­ницу, где находит­ся код, либо отклю­чить бит Write Protect в спе­циаль­ном регис­тре управле­ния CR0, что поз­волит выпол­нять запись в стра­ницы в обход их прав для текуще­го ядра.

Об­наруже­ние таких перех­ватов сво­дит­ся к под­сче­ту кон­троль­ной сум­мы сек­ций драй­вера в момент начала ана­лиза и перес­чету, свер­ке кон­троль­ной сум­мы в кон­це. В отли­чие от PatchGuard, который защища­ет толь­ко неболь­шой спи­сок сис­темных драй­веров, мы можем про­верять абсо­лют­но все заг­ружен­ные драй­веры из спис­ка PsLoadedModules.

PsLoadedModules — двус­вязный спи­сок струк­тур _KLDR_DATA_TABLE_ENTRY, опи­сыва­ющих заг­ружен­ный драй­вер: его базовый адрес, раз­мер, имя, харак­терис­тики и про­чее.

Для перечис­ления заг­ружен­ных модулей ядра с помощью DRAKVUF API мы реали­зова­ли метод drakvuf_enumerate_drivers. В пла­гине rootkitmon спи­сок PsLoadedModules про­ходит­ся два раза: в начале ини­циали­зации пла­гина — для под­сче­та кон­троль­ных сумм сек­ций драй­веров — и в кон­це — для срав­нения зна­чений.

В слу­чае рас­хожде­ния в логе появит­ся стро­ка:

"\Device\HarddiskVolume2\Windows\System32\explorer.exe":KiDeliverApc SessionID:0 PID:1968 PPID:476 Reason:"Driver section modification" Driver:"ntoskrnl.exe"

Для отра­бот­ки в кон­це ана­лиза мы добав­ляем перех­ват час­то вызыва­емой ядер­ной фун­кции KiDeliverApc. Пос­коль­ку мы заранее не зна­ем, какой поток вызовет эту фун­кцию и в каком кон­тек­сте про­цес­са этот поток находил­ся, имя про­цес­са, его PID и PPID, которые DRAKVUF авто­мати­чес­ки сох­раня­ет в жур­нале, не име­ют отно­шения к детек­ту, в то вре­мя как поле Reason и все поля, сле­дующие за ним, име­ют. В дан­ном слу­чае поле Reason озна­чает обна­ружен­ную вре­донос­ную тех­нику, а поле Driver — наз­вание модифи­циро­ван­ного драй­вера.

Перехват таблицы системных вызовов

SSDT (System Service Descriptor Table) — мас­сив ука­зате­лей на обра­бот­чики сис­темных вызовов в 32-бит­ных сис­темах или спи­сок сме­щений отно­ситель­но базово­го адре­са таб­лицы на 64-бит­ных ОС. Как говори­лось ранее, до появ­ления PatchGuard эта таб­лица активно исполь­зовалась легитим­ными прог­рамма­ми, а так­же рут­китами для филь­тра­ции и монито­рин­га сис­темных вызовов. Переза­пись одно­го ука­зате­ля в такой таб­лице рав­носиль­на перех­вату всех вызовов опре­делен­ного обра­бот­чика, что нам­ного про­ще inline-перех­ватов, рас­смот­ренных до это­го.

На дан­ный момент в ОС Windows сущес­тву­ет два таких мас­сива под сим­волами nt!KiServiceTable и win32k!W32pServiceTable для сис­темных вызовов к модулю ntoskrnl.exe и гра­фичес­кой под­систе­ме win32k.sys соот­ветс­твен­но. Количес­тво эле­мен­тов в этих мас­сивах сох­ранено в перемен­ных nt!KiServiceLimit и win32k!W32pServiceLimit.

Ука­затель на SSDT находит­ся в спе­циаль­ной струк­туре KSYSTEM_SERVICE_TABLE, или SST, под име­нем ServiceTableBase:

Опи­сание струк­туры KSYSTEM_SERVICE_TABLE

typedef struct _KSYSTEM_SERVICE_TABLE

{

PULONG ServiceTableBase;

PULONG ServiceCounterTableBase;

ULONG NumberOfService;

ULONG ParamTableBase;

} KSYSTEM_SERVICE_TABLE, *PKSYSTEM_SERVICE_TABLE;

В то же вре­мя мас­сив этих струк­тур лежит в таб­лицах nt!KeServiceDescriptorTable и nt!KeServiceDescriptorTableShadow. Если в пер­вой хра­нит­ся толь­ко SST для обра­бот­чиков сис­темных вызовов NT, то во вто­рой добав­ляет­ся SST-таб­лица гра­фичес­кой под­систе­мы win32k.

Мож­но заметить, что пер­вая SST-струк­тура в обе­их таб­лицах оди­нако­ва. При сис­темном вызове ОС Windows исполь­зует ниж­ние 12 бит регис­тра eax как индекс в одной из SSDT-таб­лиц, а биты 12–13 ука­зыва­ют, какую SDT-струк­туру выб­рать: nt!KeServiceDescriptorTable или nt!KeServiceDescriptorTableShadow.

В пер­вой час­ти статьи уже шла речь о том, что на 64-бит­ных сис­темах SSDT содер­жит мас­сив сме­щений, а вир­туаль­ный адрес обра­бот­чика сис­темно­го вызова вычис­ляет­ся по фор­муле

RoutineAddress = ServiceTableBase + (ServiceTableBase[index] >> 4)

Сдвиг на 4 необ­ходим, пос­коль­ку пер­вые 4 бита содер­жат количес­тво парамет­ров, переда­ваемых через стек.

Ес­ли взгля­нуть на при­мер с вызовом CreateFileW, учи­тывая выше­изло­жен­ное, он будет выг­лядеть сле­дующим обра­зом.

Мо­нито­ринг модифи­кации SSDT-таб­лиц зак­люча­ется в выс­тавле­нии ловушек на запись в физичес­кие стра­ницы памяти дан­ных таб­лиц и реали­зован в пла­гине ssdtmon.

Пе­рех­ват nt!KiServiceTable уже был реали­зован в этом пла­гине, и мы лишь добави­ли под­дер­жку таб­лицы win32k!W32pServiceTable. Так­же в пла­гине вре­донос­ной счи­тает­ся запись, совер­шенная на 8 байт ниже начала таб­лицы. Это свя­зано с тем, что с помощью 16-бит­ных XMM-инс­трук­ций мож­но перепи­сать пер­вые 12 байт таб­лицы, записы­вая на 4 бай­та ниже ее начала. Тем самым мож­но обой­ти три­виаль­ную про­вер­ку гра­ниц. Сто­ит заметить, что в то вре­мя, как таб­лица nt!KiServiceTable находит­ся в сек­ции дан­ных модуля ntoskrnl.exe и дос­тупна из все­го ядер­ного прос­транс­тва, win32k!W32pServiceTable при­над­лежит модулю win32k.sys и для дос­тупа к дан­ному драй­веру нуж­но находить­ся в кон­тек­сте какого‑либо гра­фичес­кого про­цес­са, нап­ример explorer.exe.

Что­бы получить физичес­кий адрес стра­ницы таб­лицы win32k!W32pServiceTable, пла­гин находит PID гра­фичес­кого про­цес­са explorer.exe и исполь­зует регистр CR3 это­го про­цес­са для тран­сля­ции вир­туаль­ного адре­са таб­лицы в физичес­кий:

Код получе­ния зна­чений win32k!W32pServiceTable и win32k!W32pServiceLimit

vmi_pid_t gui_pid = 0;

if (!drakvuf_get_process_pid(drakvuf, gui_process, &gui_pid))

{

PRINT_DEBUG("[SSDTMON] Failed to get PID of "explorer.exe"\n");

throw -1;

}

addr_t w32k_base = 0;

vmi_lock_guard vmi(drakvuf);

// Locate Win32k.sys base address

if (!get_driver_base(vmi, this, "win32k.sys", &w32k_base))

{

PRINT_DEBUG("[SSDTMON] Failed to find win32k.sys in PsLoadedModuleList\n");

throw -1;

}

// Read ssdt shadow size

if (VMI_SUCCESS != vmi_read_32_va(vmi, w32k_base + w32psl_rva, gui_pid, &this->w32pservicelimit))

{

PRINT_DEBUG("[SSDTMON] Failed to read W32pServiceLimit\n");

throw -1;

}

if (VMI_SUCCESS != vmi_translate_uv2p(vmi, w32k_base + w32pst_rva, gui_pid, &this->w32pservicetable))

{

PRINT_DEBUG("[SSDTMON] Failed to translate win32k!W32pServiceTable to physical address\n");

throw -1;

}

Пос­ле чего выс­тавля­ются ловуш­ки на эти физичес­кие стра­ницы, и, пос­коль­ку обе таб­лицы находят­ся в сек­циях READ ONLY, любая запись в них может рас­ценивать­ся как вре­донос­ная.

Перехваты IDT/GDT

Про­дол­жая тему перех­вата таб­лиц, отме­тим, что сущес­тву­ет еще два типа, которые могут пред­став­лять инте­рес: таб­лицы IDT и GDT.

IDT (Interrupt Descriptor Table) — таб­лица пре­рыва­ний в архи­тек­туре х86, которая слу­жит для кор­рек­тно­го отве­та на пре­рыва­ния и исклю­чения. В IDT исполь­зуют­ся сле­дующие типы пре­рыва­ний: аппа­рат­ные, прог­рам­мные и пре­рыва­ния, зарезер­вирован­ные про­цес­сором, — они называ­ются исклю­чени­ями (пер­вые 32 шту­ки) и исполь­зуют­ся на слу­чай воз­никно­вения некото­рых недопус­тимых событий, таких как деление на ноль, ошиб­ка трас­сиров­ки, перепол­нение.

Ре­гистр IDTR содер­жит базовый адрес (32 бита в защищен­ном режиме, 64 бита в режиме IA-32e) и 16-бит­ный раз­мер таб­лицы в бай­тах. Инс­трук­ции LIDT и SIDT заг­ружа­ют и сох­раня­ют регистр IDTR соот­ветс­твен­но.

Таб­лица дес­крип­торов пре­рыва­ний исполь­зует­ся для того, что­бы показать про­цес­сору, какую про­цеду­ру обслу­жива­ния пре­рыва­ния (ISR) вызывать для обра­бот­ки исклю­чения. Для этой же цели сущес­тву­ет ассем­блер­ная инс­трук­ция int N, где N — это номер пре­рыва­ния.

Каж­дый эле­мент в таб­лице пре­рыва­ний на 64-бит­ных сис­темах име­ет струк­туру IDTENTRY64.

Ре­аль­ный же адрес обра­бот­чика пре­рыва­ния вычис­ляет­ся по сле­дующей фор­муле:

RoutineAddress = OffsetHigh << 32 + OffsetMiddle << 16 + OffsetLow

В пла­гине мы под­счи­тыва­ем кон­троль­ную сум­му всей IDT-таб­лицы на каж­дом логичес­ком ядре и сох­раня­ем для про­вер­ки в кон­це ана­лиза.

На рисун­ке выше вид­но, что, помимо IDT, мы так­же перечис­ляем и сох­раня­ем GDT-таб­лицу. GDT (Global Descriptor Table) — струк­тура в архи­тек­туре х86, помога­ющая про­цес­сору рас­пре­делить память по сег­ментам. Каж­дый эле­мент таб­лицы, называ­емый сег­мен­тным дес­крип­тором, содер­жит адрес, раз­мер (лимит), фла­ги прав и атри­бутов таких областей.

Ад­рес и раз­мер таб­лицы хра­нит­ся в отдель­ном сис­темном регис­тре GDTR.

В режиме х64 из‑за линей­ной модели памяти каж­дый сег­мент в таб­лице охва­тыва­ет все адресное прос­транс­тво и раз­личие мож­но наб­людать лишь в фла­гах дос­тупа к дан­ным сег­ментам. К сожале­нию, мы не обна­ружи­ли рут­киты, которые как‑либо модифи­циро­вали таб­лицу GDT. Но пос­коль­ку PatchGuard защища­ет ее, мы так­же добави­ли про­вер­ку на добав­ление, уда­ление и модифи­кацию эле­мен­тов таб­лицы.

Ес­ли IDT- или GDT-таб­лицы рас­ходят­ся в начале и в кон­це ана­лиза образца, пла­гин выписы­вает сле­дующие строч­ки:

"\Device\HarddiskVolume2\Windows\System32\explorer.exe":KiDeliverApc SessionID:0 PID:1968 PPID:476 Reason:"IDT modification"

"\Device\HarddiskVolume2\Windows\System32\explorer.exe":KiDeliverApc SessionID:0 PID:1968 PPID:476 Reason:"GDT modification"

MSR LSTAR

Еще одна воз­можная цель рут­китов — переза­пись сис­темно­го регис­тра MSR LSTAR (0xc0000082). Сог­ласно спе­цифи­кации, пос­ле сис­темно­го вызова инс­трук­цией syscall в регистр RIP будет заг­ружено зна­чение MSR LSTAR, которое в Windows ука­зыва­ет на адрес сис­темной фун­кции KiSystemCall64.

Как вид­но, пер­вая инс­трук­ция в ядре пос­ле сис­темно­го вызова — swapgs. Сог­ласно докумен­тации Intel, SWAPGS exchanges the current GS base register value with the value contained in MSR address C0000102H (IA32_KERNEL_GS_BASE).

В то вре­мя как поль­зователь­ский регистр gs всег­да ука­зыва­ет на TEB текуще­го потока, в ядре gs будет ука­зывать на Processor control region текуще­го ядра.

В отли­чие от SSDT-перех­ватов, переза­пись LSTAR поз­воля­ет перех­ватывать и филь­тро­вать все сис­темные вызовы сра­зу. Но для это­го рут­киту нуж­но сде­лать допол­нитель­ную работу, а имен­но: выз­вать инс­трук­цию swapgs, сох­ранить поль­зователь­ский стек, заг­рузить ядер­ный стек. Час­то в перех­вачен­ном обра­бот­чике LSTAR мож­но встре­тить сле­дующий код.

Rootkitmon обна­ружи­вает модифи­кацию MSR LSTAR, све­ряя его зна­чение с адре­сом фун­кции KiSystemCall64 в кон­це ана­лиза. Монито­ринг записи в MSR LSTAR в DRAKVUF на дан­ный момент невоз­можен. Во‑пер­вых, перех­ватывать­ся могут толь­ко записи во все MSR-регис­тры. Во‑вто­рых, перех­ват MSR на запись силь­но наг­ружа­ет сис­тему, пос­коль­ку на каж­дом сис­темном вызове инс­трук­ция swapgs записы­вает в IA32_KERNEL_GS_BASE MSR, как говори­лось выше. В‑треть­их, монито­ринг MSR LSTAR на запись час­то при­водит к лож­ным сра­баты­вани­ям, пос­коль­ку, как мы замети­ли, PatchGuard сам переза­писы­вает этот MSR во вре­мя выпол­нения сво­их про­верок.

ПЕРЕХВАТЫ ФУНКЦИЙ DRIVER_OBJECT

У каж­дого заг­ружен­ного драй­вера в сис­теме есть струк­тура DRIVER_OBJECT. Она опи­сыва­ет сос­тояние драй­вера, который отве­чает за ее ини­циали­зацию во вре­мя сво­ей заг­рузки. В час­тнос­ти, для ком­муника­ции с поль­зователь­ски­ми прог­рамма­ми драй­вер дол­жен ини­циали­зиро­вать мас­сив MajorFunction. Он, в свою оче­редь, содер­жит адре­са обра­бот­чиков для всех зап­росов, на которые драй­вер может отве­чать. Нап­ример, ког­да поль­зователь­ское ПО будет исполь­зовать API ReadFile с опи­сате­лем драй­вера, Windows соз­даст спе­циаль­ный IRP-зап­рос и вызовет обра­бот­чик из мас­сива MajorFunction это­го драй­вера — в дан­ном слу­чае IRP_MJ_READ.

Пе­реза­пись ука­зате­лей на обра­бот­чики, как и в слу­чае с SSDT, при­ведет к перех­вату всех вызовов опре­делен­ного типа зап­роса к драй­веру. Осо­бен­ность этой тех­ники сос­тоит в том, что PatchGuard защища­ет толь­ко неболь­шой спи­сок объ­ектов драй­веров и перех­ват любого драй­вера не из спис­ка не при­ведет к BSOD’у сис­темы.

По­мимо мас­сива MajorFunction, так­же сущес­тву­ет мас­сив быс­трых IO-опе­раций — FastIoDispatch. В отли­чие от MajorFunction, быс­трые IO-опе­рации пред­назна­чены исклю­читель­но для драй­веров фай­ловой сис­темы и сетевых драй­веров. Рас­смот­рим, что такое объ­екты и как мож­но их перечис­лять непос­редс­твен­но из кон­тек­ста гипер­визора.

В Windows каж­дый ресурс (про­цесс, поток, файл и так далее) пред­став­лен в виде объ­екта и управля­ется дис­петче­ром объ­ектов Windows. Нап­ример, в Windows 10 1803 сущес­тву­ет око­ло 64 раз­личных типов объ­ектов.

Каж­дый объ­ект сос­тоит из двух час­тей:

  • OBJECT_HEADER — спе­циаль­ный заголо­вок фик­сирован­ного раз­мера, который находит­ся перед телом объ­екта и которым вла­деет дис­петчер объ­ектов;
  • Body — часть объ­екта, пред­став­ляющая сис­темный ресурс (EPROCESS для про­цес­сов, ETHREAD для потоков, FILE_OBJECT для фай­лов и так далее).

Мно­гие объ­екты содер­жат дан­ные, которые пос­тоян­ны для всех объ­ектов одно­го типа. Для эко­номии памяти дис­петчер объ­ектов хра­нит ста­тич­ные дан­ные одно­го типа объ­ектов в спе­циаль­ной струк­туре OBJECT_TYPE.

Все типы объ­ектов хра­нят­ся в мас­сиве под сим­волом nt!ObTypeIndexTable. В то же вре­мя в заголов­ке объ­екта находит­ся поле TypeIndex, которое ука­зыва­ет на индекс в дан­ном мас­сиве. Для наг­ляднос­ти най­дем струк­туру типа объ­екта EPROCESS.

Объ­екты хра­нят­ся в спе­циаль­ной дирек­тории _OBJECT_DIRECTY. Ука­затель на кор­невую дирек­торию содер­жится в nt!ObpRootDirectoryObject.

Все­го в дирек­тории может быть 37 записей, где каж­дая запись — ука­затель на связ­ный спи­сок _OBJECT_DIRECTORY_ENTRY со сле­дующей струк­турой.

Object — это тело объ­екта. Дирек­тории так­же могут быть вло­жен­ными, для них сущес­тву­ет спе­циаль­ный тип Directory, и, если Object име­ет дан­ный тип, он трак­тует­ся как OBJECT_DIRECTORY.

Со­ответс­твен­но, для перечис­ления всех объ­ектов драй­веров в сис­теме rootkitmon рекур­сивно обхо­дит каж­дую дирек­торию и сох­раня­ет объ­ект, если он отно­сит­ся к типу Driver.

У каж­дого получен­ного объ­екта под­счи­тыва­ется кон­троль­ная сум­ма мас­сивов MajorFunction и FastIoDispatch, а так­же ука­зате­лей DriverStartIo и DriverUnload. Несов­падение кон­троль­ной сум­мы по резуль­татам ана­лиза озна­чает вре­донос­ную модифи­кацию этих струк­тур.

По­мимо DRIVER_OBJECT, мы так­же сох­раня­ем каж­дый DEVICE_OBJECT в сте­ке девай­сов. DEVICE_OBJECT может пред­став­лять логичес­кое, физичес­кое устрой­ство или прос­то некие воз­можнос­ти драй­вера. Сама струк­тура DEVICE_OBJECT всег­да соз­дает­ся драй­вером фун­кци­ей IoCreateDevice. Ког­да сис­тема посыла­ет IRP-зап­рос драй­веру, она нап­равля­ет его какому‑либо девай­су. Мас­сив MajorFunction име­ет ука­зате­ли на обра­бот­чики зап­росов со сле­дующим про­тоти­пом.

Пер­вым аргу­мен­том всег­да будет ука­затель на девайс‑объ­ект, к которо­му был отправ­лен зап­рос. Соот­ветс­твен­но, драй­вер может иметь бес­конеч­но мно­го раз­личных девай­сов и обра­баты­вать каж­дый по‑сво­ему.

Windows поз­воля­ет фор­мировать стек девайс‑объ­ектов. Обыч­но, ког­да на девайс оправля­ется зап­рос, его помога­ют обра­ботать нес­коль­ко драй­веров. Каж­дый из этих драй­веров свя­зан с девайс‑объ­ектом, и такие объ­екты рас­положе­ны в сте­ке. Пос­ледова­тель­ность девайс‑объ­ектов с соот­ветс­тву­ющи­ми драй­верами называ­ется сте­ком девайс‑объ­ектов. Нап­ример, так выг­лядит стек девайс‑объ­ектов жес­тко­го дис­ка.

Вид­но, что зап­рос прой­дет нес­коль­ко «высоко­уров­невых» драй­веров, преж­де чем ока­зать­ся в обра­бот­чике кон­трол­лера жес­тко­го дис­ка stornvme. Такие зап­росы мож­но перех­ватывать, регис­три­руя свой драй­вер в стек‑девай­се объ­ектов с помощью IoAttachDeviceToDeviceStack. Так­же Windows поз­воля­ет регис­три­ровать спе­циаль­ный драй­вер — мини‑филь­тр с помощью FltRegisterFilter, который может вызывать­ся на раз­личных IO-зап­росах. Отличный при­мер работы мини‑филь­тров показан на рисун­ке ниже.

Хо­тя регис­тра­ция мини‑филь­тра не счи­тает­ся вре­донос­ной, перех­ват этой фун­кции может дать отличное пред­став­ление о воз­можнос­тях ана­лизи­руемо­го драй­вера.

Ес­ли стек девай­сов модифи­циро­ван или зарегис­три­рован мини‑филь­тр, DRAKVUF покажет сле­дующие стро­ки:

"\Device\HarddiskVolume2\Windows\System32\explorer.exe":KiDeliverApc SessionID:0 PID:1968 PPID:476 Reason:"Driver stack modification"

"\Device\HarddiskVolume2\Windows\System32\explorer.exe":KiDeliverApc SessionID:0 PID:1968 PPID:476 Reason:"FltRegisterFilter"

ТЕХНИКА СОКРЫТИЯ ПРОЦЕССА ОТ TASK MANAGER

Од­на из клас­сичес­ких тех­ник сок­рытия про­цес­са — уда­ление его из связ­ного спис­ка струк­тур EPROCESS. EPROCESS — ядер­ная струк­тура, опи­сыва­ющая сос­тояние запущен­ного про­цес­са. Дру­гими сло­вами, на каж­дый запущен­ный про­цесс заводит­ся отдель­ная струк­тура EPROCESS. Сис­тема свя­зыва­ет каж­дую струк­туру EPROCESS с дру­гими с помощью двой­ного связ­ного спис­ка LIST_ENTRY. Сам спи­сок сос­тоит из двух эле­мен­тов — ука­зате­ля Flink на сле­дующую струк­туру EPROCESS в цепоч­ке и ука­зате­ля Blink на пре­дыду­щую струк­туру в цепоч­ке. PsProcessActiveHead — ука­затель на связ­ный спи­сок про­цес­сов.

Так, при вызове коман­ды cmd /c tasklist или откры­тии Task Manager Windows прос­матри­вает связ­ный спи­сок про­цес­сов и воз­вра­щает информа­цию по каж­дому из них. Что­бы про­цесс не выс­вечивал­ся в дан­ных спис­ках, дос­таточ­но уда­лить его из цепоч­ки. Для это­го необ­ходимо, что­бы Flink пре­дыду­щего про­цес­са в цепоч­ке ука­зывал на струк­туру сле­дующе­го, а Blink сле­дующе­го про­цес­са ука­зывал на пре­дыду­щий. Так­же во избе­жание BSOD’ов ссыл­ки скры­того про­цес­са дол­жны ука­зывать друг на дру­га, как показа­но на рисун­ке ниже.

Для монито­рин­га сок­рытия про­цес­са из связ­ного спис­ка пла­гин rootkitmon перех­ватыва­ет две сис­темные фун­кции: PspInsertProcess и PspProcessDelete. Как мож­но догадать­ся по наз­ванию, пер­вая фун­кция встав­ляет толь­ко что соз­данный про­цесс в связ­ный спи­сок струк­тур EPROCESS, а вто­рая уда­ляет.

Так­же пла­гин содер­жит внут­ренний спи­сок всех запущен­ных про­цес­сов в сис­теме, который запол­няет­ся при ини­циали­зации пла­гина и попол­няет­ся (или отчи­щает­ся) при соз­дании и завер­шении про­цес­са.

Со­ответс­твен­но, если на завер­шении про­цес­са его ука­зате­ли Flink и Blink зам­кну­ты — про­цесс был скрыт.

Но что, если про­цесс не был завер­шен в кон­це ана­лиза? Для это­го пла­гин перечис­ляет связ­ный спи­сок струк­тур EPROCESS и срав­нива­ет его с име­ющим­ся в момент завер­шения: если про­цесс из ста­рого спис­ка не был най­ден в новом, он был скрыт.

Та­кой под­ход не наг­ружа­ет сис­тему и очень эффекти­вен. Если будет обна­ружен скры­тый про­цесс, в логах DRAKVUF появит­ся такая стро­ка:

"\Device\HarddiskVolume2\Windows\System32\explorer.exe":KiDeliverApc SessionID:0 PID:1968 PPID:476 Reason:"Hidden process" HiddenPid:1624

где HiddenPid — PID скры­того про­цес­са.

РЕГИСТРАЦИЯ СИСТЕМНЫХ ФУНКЦИЙ ОБРАТНОГО ВЫЗОВА

На­пос­ледок раз­берем нес­коль­ко инди­като­ров, которые не явля­ются вре­донос­ными, но могут таковы­ми ока­зать­ся.

Microsoft пре­дос­тавля­ет «офи­циаль­ный» спо­соб перех­вата сис­темных событий с помощью регис­тра­ции фун­кций обратно­го вызова (кол­бэков). Нап­ример, что­бы получать опо­веще­ния о соз­дании или завер­шении про­цес­сов, драй­вер может зарегис­три­ровать кол­бэк с помощью API PsSetCreateProcessNotifyRoutine, для под­писки на соз­дание потоков — PsSetCreateThreadNotifyRoutine.

Толь­ко докумен­тирован­ных кол­бэков боль­ше пят­надца­ти, в DRAKVUF мы перех­ватыва­ем сле­дующие API регис­тра­ции кол­бэков:

  • PspSetCreateProcessNotifyRoutine — под­писка на соз­дание/завер­шение про­цес­са;
  • PspSetCreateThreadNotifyRoutine — под­писка на соз­дание/завер­шение потока;
  • PsSetLoadImageNotifyRoutine — под­писка на заг­рузку модулей;
  • CmpRegisterCallbackInternal — под­писка на работу с реес­тром;
  • ObRegisterCallbacks — под­писка на получе­ние опи­сате­лей про­цес­са, потока и рабоче­го сто­ла;
  • FsRtlRegisterFileSystemFilterCallbacks — под­писка на работу с фай­ловой сис­темой.

Спи­сок не исчерпы­вающий и в будущих ревизи­ях пла­гина может изме­нять­ся. Сама по себе регис­тра­ция кол­бэка не счи­тает­ся вре­донос­ной, но дает хорошее пред­став­ление, что дела­ет ана­лизи­руемый драй­вер в песоч­нице. На текущий момент пла­гин прос­то перех­ватыва­ет фун­кции регис­тра­ции кол­бэков и фик­сиру­ет наз­вания фун­кций регис­тра­ции, если такие были выз­ваны.

ЗАКЛЮЧЕНИЕ

Пла­гин rootkitmon находит­ся на ран­них ста­диях раз­работ­ки, но уже поз­воля­ет обна­ружить такие рут­киты, как Moriya, TDSS, Necurs и мно­гие дру­гие. Rootkitmon не наг­ружа­ет сис­тему и под­держи­вает как Windows 7 x64, так и Windows 10 x64. Даль­ше количес­тво тех­ник обна­руже­ния вре­донос­ной активнос­ти будет толь­ко уве­личи­вать­ся.