Издательский дом ООО "Гейм Лэнд"ЖУРНАЛ ХАКЕР 128, АВГУСТ 2009 г.

Проактивное дилдо для вирусописателей. Низкоуровневая защита от вредоносного кода в домашних условиях

Александр Эккерт (aleksandr-ehkkert@rambler.ru)

В этой статье речь пойдет о том, как, имея под рукой WinDDK и обладая некоторыми навыками программирования, можно за короткий срок соорудить вполне приличную тулзу, защищающую системные файлы и процессы Windows от интимного общения с вирусами.

А началось все с Conficker'a

Да-да, червь, который заразил свыше 10 млн. компьютеров по всему свету, не обошел стороной и мою машину. Рискну вызвать бурю эмоций в свой адрес со стороны воспитанных «продвинутых» пользователей - я не пользуюсь антивирусами, предпочитая обходиться собственными силами и соблюдая правила личной гигиены. В случае заражения я вылавливаю заразу самостоятельно, не прибегая к помощи антивирусного софта. Почему я считаю антивирусы плохой затеей? Ответ на этот философский вопрос ищи в книге многоуважаемого Криса Касперски «Записки исследователя компьютерных вирусов» в главе «Почему антивирусы стали плохой идеей». Я с его выводами абсолютно согласен.

В результате, несмотря на то, что я обычно сижу под админским аккаунтом (правильно, а чего мелочиться?) вирусы на моем рабочем компе – редкий гость. Однако на этот раз мне не повезло. Все началось с того, что ни с того, ни с сего с ошибкой записи памяти начал вылетать svchost.exe. Для стабильно работающей машины – исключительная редкость. Это сразу меня насторожило, побудив прошерстить комп на предмет подозрительных файлов и записей в реестре. Я даже не сомневался, что зараза явилась посредством автозапуска с зараженной флешки (умная мысль отключить автозапуск с USB-носителей, пришла, как всегда, опосля).

В процессе организованного поиска заразы подозрительных ехе’шников я в системе не обнаружил. Это навело меня на мысль о некоей dll, незаметно подгруженной в один из системных процессов. Подумано-сделано, и при осмотре директории system32 искомая dll-ка была локализована. Удаление оказалось невозможно даже в безопасном режиме, что говорило о том, что она успешно грузится в один из основных системных процессов - svchost.exe.

Вооружившись утилитами от Sysinternals, я постарался проанализировать действия заразы. И обнаружил, что dll-ка скрывает себя из списка загруженных в адресное пространство процессов путем патча двусвязного списка в PEB'e (подробно эта техника описана в моей же статье в майском ][), поэтому просмотрщики типа PETools здесь не помогут. Не помогла и утилитка ListDlls от Sysinternals, позволяющая просмотреть все загруженные в процесс библиотеки. И только маленькая тулза HandleViewer показала хендл lepujmlx.dll, загруженный в svchost.exe. Эта библиотека, как потом выяснилось, создавала левые записи в реестре, позволяющие ей выжить в системе.

Как следствие работы этой заразы, я получил перехват нескольких системных функций в ntdll.dll, а также функций для работы с интернетом, что приводило к тотальному облому при попытке посещения сайтов, посвященных компьютерной безопасности. Мелочь, а неприятно. Мегамощная вещь - RKUnhooker - показала появившиеся в системе перехваты.

Промучившись несколько дней с бесплатными утилитами от известных антивирусных брендов, выпущенных специально для удаления Conficker'a (типа KidoKiller От KAV или EConfickerRemover от ESET), я пришел к выводу, что кардинальных решений ни одно из опробованных средств не предлагает, поскольку на тот момент они ограничивались удалением lepujmlx.dll и парочкой других файлов, а также - чисткой реестра.
Все это хозяйство работало только в течение первого часа, затем история повторялась - неизвестный процесс маппил в svchost.exe вышеуказанную lepujmlx.dll, и система вновь возвращалась к зараженному состоянию.

Перезагрузка в безопасном режиме тоже проблемы не решила - dll все равно подгружалась в svchost.exe и удалить ее было невозможно.
Читать маны об очистке системы от Conficker'a, которые можно в изобилии найти на бескрайних просторах интернета не хотелось; устанавливать тяжелую артиллерию в виде антивирусного пакета - тем более. Вот тогда-то мне и пришла в голову мысль написать что-то типа собственной проактивки, защищающей системные файлы и процессы от внедрения постороннего кода.

Сам себе антивирус

Решение напрашивалось само собой: все, что нужно сделать, – запретить загрузку сторонних dll в системные процессы. Легче всего это осуществить путем перехвата системной функции LoadLibrary или, что еще лучше, LdrLoadDll. Эти вещи я реализовал в виде драйвера, однако мне этого показалось мало, и я решил на скорую руку наваять нечто большее, поскольку надежно защитить системные файлы перехватом одной только LdrLoadDll вряд ли возможно. В порыве вдохновения я добавил перехват таких системных вызовов, как NtOpenProcess, NtWriteVirtualMemory и NtReadVirtualMemory и еще парочки других. В результате получилась самопальная проактивная защита системных процессов. Можно добавить ограничение доступа к реестру, но такой цели я перед собой не ставил.

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

Этим мы и займемся. Как ты знаешь, загрузка dll в адресное пространство чужого процесса обычно происходит через вызов CreateRemoteThread примерно так:

hProcess = OpenProcess(...);
LibFileRemote = (PWSTR) VirtualAllocEx(hProcess...);
WriteProcessMemory(hProcess, LibFileRemote, ...);
PTHREAD_START_ROUTINE fnThreadRtn =
(PTHREAD_START_ROUTINE)
GetProcAddress(GetModuleHandle
(TEXT("Kernel32")), "LoadLibraryW");
hThread = CreateRemoteThread(hProcess, NULL, 0,
fnThreadRtn, LibFileRemote, 0, NULL);

LdrLoadDll в свою очередь сводится к неэкспортируемым вызовам LdrpLoadModule и LdrAttachProcess, которые просто проецируют загружаемую библиотеку в адресное пространство целевого процесса.

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

Ставим под контроль загрузку dll

Главную смысловую нагрузку в нашем коде несет перехват системной функции LdrLoadDll, а основной проблемой, стоящей перед нами, будет определение адреса функции LdrLoadDll в таблице экспорта ntdll.dll.

В usermode эта проблема достаточно легко решается - достаточно найти ntdll.dll на диске и перехватить ее через вызовы OpenFile/CreateSection/MapViewOfSection. То же самое, но уже посредством вызова аналогичных ядерных функций, можно сделать в ядре. Таким вот нехитрым образом мы сможем контролировать загрузку библиотек во все процессы в системе (естественно, ведь ntdll.dll по умолчанию подгружается во все процессы):

DWORD GetDllFunctionAddress(char* lpFunctionName,
PUNICODE_STRING pDllName)
{
ZwOpenFile(...);
ZwCreateSection(...);
ZwMapViewOfSection(...);
...
dosheader = (IMAGE_DOS_HEADER *)hMod;
//здесь мы парсим экспортную таблицу
...
for(i = 0; i < pExportTable->NumberOfFunctions; i++)
{
functionName = (char*)( (BYTE*)hMod +
arrayOfFunctionNames[x]);
functionOrdinal = arrayOfFunctionOrdinals[x] +
Base - 1;
functionAddress = (DWORD)( (BYTE*)hMod + arrayOfFunctionAddresses[functionOrdinal]);
if (RtlCompareString(&ntFunctionName,
&ntFunctionNameSearch, TRUE) == 0)
return functionAddress;
}
return 0;
}

Драйвер, реализующий этот перехват LdrLoadDll, ты найдешь на нашем диске. Существует еще один, не слишком красивый и элегантный способ. Он позволяет проконтролировать LdrLoadDll, но уже в конкретном процессе. Так как мы находимся в ядре, то я не нашел ничего лучше, чем сделать следующее. Вызовом KeAttachProcess аттачимся в svchost.exe и находим PEB (Process Environment Block; как это сделать, я также писал в майском ][). А уже через него получаем указатель на LDR_DATA_TABLE_ENTRY, который содержит такое поле, как ModuleBaseAddress.

Идея такова: найдем по имени библиотеки ntdll.dll адрес ее загрузки в svchost'e. Потом, используя этот адрес, пропарсим таблицу экспорта на предмет адреса функции LdrLoadDll. И только затем подменим его на вызов своей функции myLdrLoadDll, которая будет отслеживать и пресекать загрузку зловредных библиотек. Не слишком удобно и элегантно, но вполне работоспособно. Если знаешь способ лучше - напиши, обсудим.
Итак, получаем:

Находим адрес спроецированной в целевой процесс ntdll.dll

ZwQueryInformationProcess (NtCurrentProcess(),
ProcessBasicInformation, &ProcInfo,
sizeof(PROCESS_BASIC_INFORMATION), &Size);
pPeb = ProcInfo.PebBaseAddress;
//хотя можно просто так: pPeb = (PEB*)0x7FFDF000;
PPEB_LDR_DATA Ldr = pPeb->Ldr;
PLIST_ENTRY InitialEntry = Ldr->
InitializationOrder.Flink;
PLDR_DATA_TABLE_ENTRY LdrDataTableEntry =
CONTAINING_RECORD( InitialEntry,
LDR_DATA_TABLE_ENTRY,
InitializationOrder);
PLIST_ENTRY LoadOrderListHead =
LdrDataTableEntry->LoadOrder.Blink;

Далее нам остается только пропарсить полученный список на предмет имени библиотеки и адреса ее загрузки.

Перехват функций для работы с виртуальной памятью

Таких функций несколько - ZwWriteVirtualMemory, ZwReadVirtualMemory, ZwOpenProcess, ZwDuplicateObject, ZwQueryInformationProcess и ZwProtectVirtualMemory. Эти системные функции являются основными при манипуляциях с адресным пространством процесса. На их перехвате и отслеживании их вызова построена работа всякой уважающей себя проактивной защиты. Не обойдем эти системные вызовы и мы. Сейчас я покажу, как подручными средствами организовать контроль их вызовов.

Есть два способа: перехватить в Usermode или в ядре, пофиксив непосредственно таблицу экспорта ntdll.dll, или поступить как настоящие профессионалы - найти KeServiceDescriptorTable (только не спрашивай, что это такое, а то я в тебе разочаруюсь). Сам перехват можно сделать либо подменой адреса вызова, либо спласингом функции.

Перехват KeStackAttachProcess

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

Эта функция не экспортируется из SSDT и чтобы ее перехватить, нужно либо пропарсить таблицу экспорта ядра, либо вызвать такую нехитрую ядерную функцию, как MmGetSystemRoutineAddress. О ней, кстати, очень часто забывают: PVOID func_addres = MmGetSystemRoutineAddress( &ApiNameUnicode ). Функция вернет нам ее адрес. Что с ним делать, думаю, ты уже знаешь. Часто бывает, что самые вкусные и интересные функции ядра Windows просто не экспортируются. В этом случае MmGetSystemRoutineAddress вернет NULL.

Отметая возражения

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

В конце хочу еще раз напомнить, что программирование в ядре сродни собиранию «кубика-рубика» в темноте - из-за затруднений с отладкой драйвера. Поэтому на первых порах тебе не раз придется лицезреть BSOD. Ну а для его анализа и, как правило, отладки драйвера, нужен WinDBG и какой-нибудь ядерный отладчик. Я, к примеру, пользуюсь Immunity debugger'om, что, впрочем, дело вкуса. Удачного компилирования и да пребудет с тобой Сила!

CD

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

WWW

Для совершенствования навыков ковыряния ядра Windows рекомендую к посещению форумы на rsdn.ru и wasm.ru. Много важной и интересной инфы о программировании ядра Win содержится на ntkernel.com.

INFO

Отключить автозапуск всех сменных носителей в Windows можно, добавив в ветку реестра HKEY_CURRENT_USERSoftwareMicrosoftWindowsCurrentVersionPoliciesExplorer ключ "NoDriveTypeAutoRun" со значением 0xff.

Содержание
ttfb: 5.9149265289307 ms