Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

вторая_чать_диплома

.pdf
Скачиваний:
8
Добавлен:
19.03.2016
Размер:
1.63 Mб
Скачать

текущими настройками приложения, которые загружаются из .conf файлов.

Работа сканера выполняется в отдельно созданном им потоке, который может быть приостановлен, продолжен, завершен при помощи публичных методов – Suspend(), Resume(), Stop().

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

Таким образом, можно делегировать несколько задач разным объектам, а

получать результаты в одном, общем сервисе обработки событий. Такой подход возможен из-за использования механизмов сигналов и слотов [11],

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

void foundWinApiHook(const IAT_HOOK*); void foundSsdtHook(const SSDT_HK*);

void foundHiddenProccess(const CL0_PROC_INFO*);

void foundHeurObject(QString, const HEUR_FILE_DESCR*); void foundHiddenObject(QString);

void changedStatus(QString); void changedProgress(int); void sendMsg(long);

// ….

Так во время сканирования сканер генерирует ряд сигналов семейства foundXxx(информация_характеризующая_угрозу) при обнаружении руткита,

скрытого объекта, вируса. Текущее состояние можно отслеживать, если следить за событиями – changedStatus, changedProgress, sendMgs. Для того чтобы на эти события можно было бы как-то реагировать, на них нужно подписаться. В терминологии Qt это означает – соединить сигнал со слотом.

Для чего используется функция QObject::connect.

connect(pSecurity /* объект_генератор_сигнала */,

SIGNAL(changedStatus(QString) /*сигнатура_сигнала*/),This

51

/*объект_получатель_сигнала*/,SLOT(HandlerChangedStatus(QString) /*сигнатура_получателя*/) /*Qt::DirectConnection*/);

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

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

используемых процессом модулях, это используется для дальнейшей оптимизации процесса анализа. Иначе бы при нахождении очередной зависимой библиотеки необходимо было бы прерывать процесс анализа для получения информации о зависимом модуле. Основная идея заключается в перечислении ряда последовательных проверок загруженных в память объектов. Поиск угроз на жѐстком диске может быть начат только после завершения данной процедуры. Файловый сканер работает только с исполняемыми файлами формат PE. В процессе принятия решения используются результаты, полученные от эвристического анализатора.

В процессе своей работы, сканер активно взаимодействует со своим драйвером, который расположен в ядре системы. Всѐ это происходит посредствам объекта client0. При проектировании класса данного объекта применялся паттерн Singleton, выбран он был по очень простой причине – объект, который реализует взаимодействие с драйвером, должен существовать не более чем в одном экземпляре. Это обусловлено как архитектурой драйвера, так и тем, что никакое другое приложение не должно иметь возможности доступа к драйверу. В случае получения доступа сторонним приложением это может привести к непредсказуемым последствиям, сбоям системы из-за плохого знания протокола обмена информацией или же в случае если протокол общения известен совершенно

52

точно – можно будет вмешаться в работу всего программного комплекса

(user-mode приложение <–> kernel-mode driver).

Интерфейс для достаточно простой, он предоставляет ряд методов для установки(InstallDriver), загрузки(LoadDriver), выгрузки(UnloadDriver),

удаления(DeleteService), получения информации о состоянии работы драйвера(GetDriverState). Для получения актуальной информации о состоянии системы используются методы:

GetSsdtHooks – возвращает список перехватчиков системных сервисов;

GetCodeInjectors – возвращает список модифицированных участков исполняемого кода в ядре системы;

GetSysenter – возвращает информацию об обработчике прерывания sysener(x86);

GetRunningProcesses – возвращает действительный список всех выполняющихся процессов;

GetKernelModules – возвращет действительный список всех функционирующих драйверов;

И т.д.

Все описанные выше методы получения информации инкапсулируют в себе всего один единственный вызов. Пример реализации такой такого метода представлен ниже.

bool client0::GetSsdtHooks(__out THooksSsdt& hooks){

return GetDataBlock<THooksSsdt, SSDT_HK>(IOCTL_SCAN_SSDT, hooks);

}

Задача сводится к вызову шаблонной функции GetDataBlock, первым аргументом шаблона является тип контейнера-приѐмника информации,

вторым значится тип объекта элемента контейнера. В качестве аргументов функции используется код IOCTL запроса (код запроса ввода/вывода) и

ссылка на контейнер-приѐмник. Код шаблонной функции GetDataBlock

53

можно найти в приложении. Возвращаемое значение данного метода говорит об успешности выполненной операции.

За процессом работы драйвера можно наблюдать в режиме реального времени с использованием утилиты DbgView от Sysinternals. В журнал работы заносятся все поступившие от приложения запросы. Ниже приведѐн пример информации сохраняемой в журнале.

client0 load : \REGISTRY\MACHINE\SYSTEM\ControlSet001\services\client0 client0: kernel was loaded to 83407000

client: sdt offset is 0x169b00

client0: attached to the kernel: 83407000 client0: connecting to the system api interface..

KeServiceDescriptorTable by import: 83570b00 export:83570b00 client0: offset to ssdt.ntoskrnl 0x169b00

client0: ssdt.ntoskrnl addr 0x8348543c (offset 0x7e43c) client0: IOCTL_SCAN_SSDT bodysize 0

client0: LookupSsdtHooks; pEnd 8381a000 srvcnt 401

client0: interceptor was found in ssdt [105]=9d1ac120 \??\G:\projects\rootkit_for_test\rootkit.sys 9d1ab000

client0: error IOCTL_SCAN_SSDT required 268 client0: IOCTL_SCAN_SSDT bodysize 268

client0: LookupSsdtHooks; pEnd 8381a000 srvcnt 401

client0: interceptor was found in ssdt [105]=9d1ac120 \??\G:\projects\rootkit_for_test\rootkit.sys 9d1ab000

client0: IOCTL_SCAN_SSDT wrote to output 268 client0: IOCTL_GET_PROCESSES bodysize 0 client0: GetLoadedProcesses required size 25740 client0: GetLoadedProcesses == failed

client0: IOCTL_GET_PROCESSES bodysize 25740 client0: GetLoadedProcesses required size 25740

54

В таком журнале видна динамика обработки запросов, поступающих от основного приложения.

4.3 Механизмы обнаружения руткитов

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

Самым простым и популярным считается подмена адресов импортируемых функций в таблице импорта [12] приложения. Всѐ сводится к анализу массива структур IMAGE_IMPORT_DESCRIPTOR:

typedef struct _IMAGE_IMPORT_DESCRIPTOR { union { DWORD Characteristics;

DWORD OriginalFirstThunk; };

DWORD TimeDateStamp;

DWORD ForwarderChain;

DWORD Name;

DWORD FirstThunk;

} IMAGE_IMPORT_DESCRIPTOR;

Поле OriginalFirstThunk хранит смещение адреса импортируемой функции, Name это смещение по которому находится ascii строка с названием функции. Процедура обнаружения состоит в сравнении адресов из импорта с адресами оригинальных функции из таблицы экспорта.

Обнаружив несоответствие, восстановить оригинальный адрес не составляет труда. После того как сканер восстанавливает оригинальный адрес,

необходимо определить вредоносный модуль в пределах процесса. Делается это так, есть, к примеру, адрес 0x00403000, для понятия того, кому он принадлежит, обходится список модулей, проверяя принадлежит ли он адресному интервалу памяти модуля, т.е. [ImageBase; ImageBase+ImageSize],

где ImageBase базовый адрес по которому размещѐн модуль в памяти.

55

На данном этапе может возникнуть следующая ситуация – адрес окажется незадействованным, т.е. не будет найден модуль, которому он принадлежит.

Это значит, что внедрѐнный вредоносный модуль был выгружен из памяти,

что не может быть правдой т.к. при переходе по этому адресу (на функцию перехватчик) приложение должно завершиться с ошибкой, но такого ведь не происходит, а значит это скрытие файла руткита из памяти. Для того чтобы определить скрытый модуль нужно для начала объяснить, как происходит такое скрытие. В ОС Windows все процессы имеют в памяти экземпляр недокументированной структуры под названием PEB (Process Environment Block), данная структура имеет достаточно большой размер, всѐ что нужно знать, это то что в ней есть поле LoaderData, указатель на структуру, которая содержит информацию обо всех модулях процесса. Если быть точным, то это три идентичных двусвязных списка, отсортированных по разным критериям. Идея скрытия основана на удалении элемента, описывающего вредоносный модуль из трѐх списков. При таком подходе найти скрытый модуль задача не простая, требующая определѐнных временных затрат. В

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

вызовом системного API VirtualQueryEx предоставляется информации о состоянии всего виртуального адресного пространства процесса со всеми его регионами, флагами, размерами и т.д. Вся эта информация крайне необходима для обнаружения руткита, хоть модуль и скрыт из всех списков,

это не означает, что он удален из памяти. VirtualQueryEx возвращает структуру типа MBI:

typedef struct _MEMORY_BASIC_INFORMATION { PVOID BaseAddress; // начальный адрес региона

PVOID AllocationBase;

DWORD AllocationProtect;

SIZE_T RegionSize; // размер региона

DWORD State;

56

DWORD Protect;

DWORD Type; // тип региона памяти

} MEMORY_BASIC_INFORMATION, *PMEMORY_BASIC_INFORMATION;

State – состояние региона, можно узнать предоставлена ли физическая память, либо регион просто зарезервирован для последующего выделения физической памяти т.д. Имея такую информацию можно без труда найти регион, которому принадлежит адрес функции скрытого из памяти перехватчика. Однако мало найти такой регион, он должен удовлетворять некоторому условию, если быть точным поле Type структуры

MEMORY_BASIC_INFORMATION должно иметь значение MEM_IMAGE.

Это означает, что на данный регион действительно спроецирован исполняемый файл (все исполняемые файлы при проецировании в память имеют подобный атрибут). Вызовом GetMappedFileName можно определить адрес скрываемого модуля на диске. Процедуры для обнаружения файла скрытого модуля можно найти в приложении. Данный метод является достаточно эффективным, доказавшим свою пригодность на практике.

Все это касалось пользовательского режима, однако стоит отметить,

что данный метод перехвата активно применяется в ядре ОС, где некоторые моменты являются даже проще и удобнее. Это потому что в ядре выполняются все те же исполняемые файлы (Portable Executable), имеющие идентичный формат с теми же таблицами импорта, экспорта. ―Удобством‖ перехватов функций в ядре является тот факт, что все модули, драйвера и т.д. разделяют одно адресное пространство. Все перехватчики являются глобальными – работают на уровне всей системы. Для поиска руткитов в ядре используется драйвер, который работает на уровне ядра, сканирует различные области системы по требованию основного приложения. Всѐ общение с драйвером реализовано посредством работы с IRP пакетами, что создает определѐнные хлопоты в процессе разработки и тестирования.

57

Самым популярным на уровне ядра считается перехват системных сервисов в таблице SSDT:

Рисунок 18 - Устройство таблицы системных сервисов

При вызове системной функции из пользовательского режима происходит следующее:

1)Получение индекса сервиса для таблицы системных сервисов;

2)Получение адреса массива с адресами системных сервисов;

3)Используя индекс, полученный на первом этапе, получается адрес

сервиса и последующий вызов.

C использованием системного API руткиты получают указатель на структуру

KeServiceDescriptorTable, в которой хранится информация о всех сервисах и самое интересное и важное поле с указателем на массив с адресами сервисов.

Как и в прошлом случае с перехватом в таблице импорта идея состоит в подмене адреса оригинального сервиса функцией-перехватчиком. Однако отличие всѐ же имеется, хоть и не большое. В данном случае по имени определить нужный сервис не достаточно, нужно иметь индекс сервиса в массиве системных сервисов, который меняется в каждой версии системы.

На примере вызова CreateFile цепочка вызовов следующая: 1) Kernel32.dll.CreateFile;

58

2)Ntdll.dll.NtCreateFile (на данном этапе происходит выборка номера системного сервиса);

3)Прерывание с переходом в режим ядра с кодом(индексом)

вызываемого сервиса;

4)Call_sysService[index].

Индекс (номер, код) сервиса становится известен на втором этапе данной цепочки вызовов. Собственно задача ntdll состоит в том, чтобы загрузить соответствующий индекс в регистр процессора eax, сгенерировать прерывание и перейти в режим ядра.

Рисунок 19 - Дизассемблированный листинг NtCreateFile

В данном случае индекс сервиса NtCreateFile в таблице сервисов – 0x42.

Для восстановления оригинально записи в таблице необходимо сделать следующее:

1)Получить относительное смещение до массива указателей на системные вызовы;

2)Спроецировать в память файл ядра системы (ntoskrnl.exe или ntkrnlpa.exe);

3)Найти массив указателей, используя смещение, полученное ранее;

4)После чего, зная индекс перехваченного сервиса, обратится к массиву по этому индексу, в ячейке будет находиться относительное смещение от начала файла до оригинальной функции;

5)Записать в память по индексу в таблице системных сервисов (system descriptor table) загруженное из файла смещение + базовый адрес по которому загружен файл ядра системы.

59

4.4 Эвристический анализатор

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

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

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

HeuristicClassification, который является основной характеристикой любого исполняемого файла.

enum HeuristicClassification { SysRegistration = 1, Downloader,

Infected,

UsePrivilages,

AvKiller,

ClipboardSpy,

KeyLogger, NoDigitalSign /* … */

};

Набор таких характеристик хранится в полях структуры

HEUR_FILE_DESCR.

typedef struct HEUR_FILE_DESCR_

{

ulong version;

u_short entries[HEUR_CHARACTERISTIC_SIZE]; // ..

}HEUR_FILE_DESCR, *PHEUR_FILE_DESCR;

60