R0 CREW

Антируткиты – атаки на вредоносное ПО

Предисловие

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

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

Основные понятия и термины

Здесь я буду предельно краток – изложу лишь самое нужное.

Под руткит-технологиями в данной статье понимаются методы модификации операционной системы (обычно на низком уровне) с целью изменения её нормального поведения.

Нормальное поведение ОС – реакция ОС на вызовы функций и системных сервисов, при которой возвращающее этой функцией (сервисом) значение является результатом выполнения кода ОС. Здесь приведу банальный пример: допустим, вы хотите открыть файл с атрибутами для чтения. Если это позволяют атрибуты файла, права вашей учётной записи, маркер доступа и некоторые другие вещи, например, привилегии, то NtCreateFile вернёт STATUS_SUCCESS. Другое дело, если в системе установлен руткит, то NtCreateFile может быть перехвачен, а запросы к нему – отфильтрованы. Таким образом, ряд вызовов может завершиться неудачно даже тогда, когда для этого нет никаких оснований, кроме руткита.

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

Самозащита вредоносного кода – активное противодействие удалению и прекращению работы вирусного кода. Именно противодействие, а не сокрытие. Т.е., если вирус скрыл свой процесс от диспетчера задач – это сокрытие, а если не даёт диспетчеру задач его завершить – противодействие.

Немного лирики

По сути, любая программа (не только вирус) может использовать систему самозащиты. Тем не менее, обычному блокноту это ни к чему.

За последние несколько лет уровень подготовки среднего вирусописателя возрос. Сейчас редкий вирус обходится без навесного криптора и драйвера режима ядра. Но, почувствовав собственную силу, многие вирмейкеры забывают о скрытности. Часто можно даже визуально обнаружить зловредное ПО, просто просмотрев список активных процессов. И во многих случаях для этого даже не надо пользоваться продвинутыми антируткитами на подобие Rootkit Unhooker. Достаточно любого приложения третьего кольца, отображающего полные пути к образам процессов. И в глаза сразу бросаются процессы с именами winlogon.exe, располагающиеся где-то во временных папках. Конечно, это никуда не годится. Но бедный пользователь, даже видя такой произвол, не в состоянии улучшить ситуацию штатными средствами.

И действительно, прикрутить к своей программе драйвер, перехватывающий NtOpenProcess, не так уж сложно. Но на этом методы противодействия не заканчиваются. Как же быть? Нам срочно нужно универсальное средство завершения вредоносных процессов. Rootkit Unhooker здесь не подходит, поскольку он не в состоянии справиться со многими угрозами, поэтому нам нужно провести анализ проблемы и разработать что-то своё, и что-то действенное.

Для тренировок и тестов наших методов эффективного завершения процессов нам понадобятся цели. Атаковать вирусы скучно, поскольку в 95% там всё сводится к созданию мониторящего процесса или потока (более подробно – в конце статьи в пункте про многопроцессные связки). Поэтому я предлагаю атаковать тех, кто собаку съел на технологиях противодействия. Я, как это ни парадоксально звучит, предлагаю атаковать антивирусы. Вы, возможно, пожелаете узнать, какой антивирус мы будем атаковать? Я отвечу вам: «ВСЕ». Да, я тестировал на прочность все антивирусы, что попались мне под руку.

Понеслась

В общих чертах, при создании процесса каждый раз происходит одно и то же. ZwOpenFile открывает файл образа, ZwCreateSection создаёт объект «раздел», NtCreateProcessEx (для ХР и старше) создаёт адресное пространство, далее в ядре создаётся EPROCESS, PEB, инициализируется адресное пространство, потом создаётся первичный поток, подгружаются библиотеки и т.д. Сразу оговорюсь, что подробности нас здесь не интересуют, нас интересует конкретно тот момент, где начинается загрузка динамических библиотек в адресное пространство процесса.

Как мы все знаем, ntdll.dll проецируется на адресное пространство процесса независимо от того, существует ли у него таблица импорта, или нет. Более того, эта библиотека, по сути, является переходником в нулевое кольцо. Ведь согласитесь, что любое приложение, выполняющее полезную работу, так или иначе должно обращаться к системным сервисам. Даже если такое приложение имеет собственный драйвер, то этот драйвер нужно установить, передавать ему запросы для формирования IRP пакетов и т.д.
Далее по ходу статьи вы поймёте, зачем я вам тут плёл про ntdll.dll.

Конечно, вредоносное ПО может существовать только лишь как драйвер без программы третьего кольца, но для вирусов это – неприемлемо, а для обычных программ – непрактично. Поэтому, в 99,9% атака на зловред сводится к атаке на его управляющее приложение третьего кольца, после чего систему можно будет перезагрузить, и драйвер либо не запуститься, либо будет функционировать неправильно. В принципе, из этого утверждения существуют и исключения, но, в любом случае, первым этапом нужно «успокоить» приложение третьего кольца.

Ошибки Rootkit Unhooker

Собственно, перед изобретением колеса мне стало интересно, как этот знаменитый антируткит прибивает процессы.

RKU не то, чтобы содержит ошибки, просто алгоритм завершения процессов у него весьма неэффективен. Всё сводится к тому, что парсится PsActiveProcessHead (т.е. парсится двусвязный список, на который указывает PsActiveProcessHead) и выполняется поиск структуры EPROCESS, описывающей завершаемый процесс. От поиска по алгоритму RKU не помогает исправление двусвязного списка, т.к. RKU использует немного усовершенствованный алгоритм, но это сейчас неважно. Далее идёт такой код:

 
xor     esi, esi
lea     eax, [esp+8+Handle]
push    eax
push    esi
push    esi
push    esi
push    esi
push    esi
push    [ebp+arg_0]
call    ds:ObOpenObjectByPointer
test    eax, eax
jl      short loc_12B12
push    esi             ; ExitStatus
push    [esp+0Ch+Handle] ; ProcessHandle
call    ds:ZwTerminateProcess
push    [esp+8+Handle]  ; Handle
call    ds:ZwClose

Имея указатель на объект, RKU получает его дескриптор, а потом использует этот самый дескриптор для завершения процесса.

Что же тут плохого? Во-первых, парсинг PsActiveProcessHead – не идеальная, но достаточно надёжная методика. Здесь проблему может создать лишь разница смещений в различных версиях Windows, но и эта проблема решаема. Во-вторых, ObOpenObjectByPointer может быть перехвачена, причём не тупо сама функция, а код работы с объектами, который находится глубоко в недрах ObOpenObjectByPointer. Но самое плохое – это то, что используется ZwTerminateProcess, которая точно будет перехвачена.

Решив эти проблемы, мы сможем завершать неугодные нам процессы.

Атакующий компонент

Ясное дело, что для атаки нам понадобится драйвер. Но при создании драйвера возникает масса вопросов: как он должен завершать процессы, как загружаться и как управляться?

Загружаться он будет самым стандартным способом – через Service Control Manager. Ведь вредоносное ПО не может совсем блокировать загрузку драйверов, т.к. это нарушит нормальную работу компьютера. Мешать загрузке именно нашего драйвера, в принципе, возможно, но как он определит, что наш драйвер для него опасен, а для системы – бесполезен? В общем случае – никак. Ниже приведен код на Delphi, выполняющий загрузку
и запуск драйвера.

 
function LoadDriver():boolean;stdcall;
const
  DriverFileName = 'Attack.sys';
  FileName = 'Attack';
 
var
 SCMHandle,ServHandle:Cardinal;
 CurrentDir:array[0..MAX_PATH*2] of char;
 TheDriverWay:array[0..MAX_PATH*3] of char;
 TheVector:PAnsiChar;
 
begin
  Result := FALSE;
  TheVector := nil;
  SCMHandle := OpenSCManager(nil,nil,SC_MANAGER_ALL_ACCESS);
  if SCMHandle<>0 then begin
    GetCurrentDirectory(MAX_PATH*2,@CurrentDir);
    lstrcpy(@TheDriverWay,@CurrentDir);
    lstrcat(@TheDriverWay,PAnsiChar(DriverFileName));
    ServHandle := CreateService(SCMHandle,PAnsiChar(FileName),
    PAnsiChar(FileName),SERVICE_ALL_ACCESS,SERVICE_KERNEL_DRIVER,
    SERVICE_DEMAND_START,SERVICE_ERROR_NORMAL,@TheDriverWay,nil,nil,nil,
    nil,nil);
    if ServHandle=0 then begin
      if GetLastError()=ERROR_SERVICE_EXISTS then begin
        ServHandle := OpenService(SCMHandle,PAnsiChar(FileName),SERVICE_ALL_ACCESS);
        if ServHandle=0 then CloseServiceHandle(SCMHandle);        
      end else begin
        CloseServiceHandle(SCMHandle);
      end; 
    end;
    if ServHandle<>0 then begin
      if StartService(ServHandle,0,TheVector) then Result := TRUE
      else if GetLastError()=ERROR_SERVICE_ALREADY_RUNNING then Result := TRUE;           
      CloseServiceHandle(ServHandle);
      CloseServiceHandle(SCMHandle);
    end;
  end;
end;

Допустим, нам удалось загрузить драйвер, что теперь? Теперь давайте немного усовершенствуем алгоритм завершения процессов. Во-первых, поиск по двусвязному списку можно заменить на вызов PsLookupProcesByProcessId. Её, конечно, можно перехватить, но большинство руткитов этого не делают. А не делают они этого потому, что получить указатель на EPROCESS – это ещё не завершить процесс. Это – только половина дела.

Получив указатель на структуру EPROCESS, мы много не выиграли, потому что та же ZwTerminateProcess требует дескриптор потока, а не указатель. Но здесь я предлагаю не получать дескрипторы, провернуть такой трюк: мы переключимся на адресное пространство атакуемого процесса, а в этом адресном пространстве мы можем использовать псевдодескриптор, всегда равный -1h на линейке NT. После того, как процесс завершён, мы можем спокойненько переключиться назад в своё родное АП. Переключение осуществляется через KeStackAttachProcess, хотя, конечно, можно было бы выполнить его и вручную. Здесь:

http://www.wasm.ru/article.php?article=dumping

рассказывается, почему KeStackAttachProcess, в общем случа, достаточно. А вот про CR3 – немного не так, просто изменить его значение недостаточно. Но об этом – в другой раз.

И последнее. Я думал, как же нам завершить процесс? Идея с ZwTerminateProcess не катит, NtTerminateProcess ещё нужно найти, а т.к. она не экспортируется ядром, то задача усложняется. Кроме того, она достаточно сложна в псевдореализации, поскольку сама опирается на ряд неэкспортируемых функций. Но потом я подумал – а почему бы нам просто не нарушить нормальную работу процесса, чтоб он просто был вынужден завершиться?

Все прекрасно знают, что вызовы системных сервисов для любого процесса выполняются через ntdll.dll. Она при любых раскладах проецируется на адресное пространство любого процесса, причём всегда по одному и тому же адресу (с оговоркой лишь на процесс Idle, и то потому, что я этого не проверял). А вот если её не окажется в адресном пространстве, то процесс, по-видимому, не сможет нормально функционировать. Выгрузить образ из адресного пространства вполне возможно с помощью ZwUnmapViewOfSection, что и осуществляется в атакующем драйвере.

Собственно, к статье прилагаются исходники как драйвера, так и управляющей программы. Если что-то не понятно, то вы сами можете всё увидеть.

Об аверах замолвим слово

Эта часть статьи напрямую не относится к теме завершения процессов. Здесь я просто расскажу ленивым вирмейкерам, как мне удалось избежать двухсоткилометрового блэклиста процессов антивирусов. Дело в том, что найти антивирус в системе очень просто – обычно его процесс нельзя открыть через ZwOpenProcess. Простым перечислением процессов нашли Id, если процесс с этим идентификатором не открылся, значит прибиваем его по вышеупомянутой методике. Лишь NOD32 устоял, т.к. его процесс не палился, а нормально открывался через API. Тогда я вбил в код дополнительную проверку на имя его GUI-процесса, и НОД также нас покинул.

Связка из двух (и более) процессов

Излюбленный приём всяких нехороших паразитных программ – двухпроцессное существование. Т.е. существуют в системе два процесса, если какой-то один завершается, то оставшийся процесс вновь запускает своего боевого товарища. Вполне может статься так, что таких процессов будет и не два, а, скажем, 5. Для борьбы с ними существует два способа (может, и больше, но все они сводятся к этим двум): простой и правильный.

Правильный сводится к внедрению в планировщик и вытеснению кода ненужных нам потоков. А простой – к повышению приоритетов потока и процесса, а потом атаке на процессы из потока с повышенным приоритетом.
Это тоже реализовано в моём примере.

Финальная часть

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

Для справки, тест проводился и успешно завершился на таких программных продуктах: Антивирус Касперского 2009, Dr.Web Security Space 5.0., Norton Antivirus (какой-то из новых), Rootkit Unhooker (все версии), Agnitum Outpost и некоторые другие менее известные поделки.

Статья подошла к концу, всем желаю успехов, ну а я пойду - мне ещё кейген реверсить…

Материалы к статье

ARCHANGEL © AHTeam, r0 Crew