R0 CREW

Malware Analysis Tutorial 25: Deferred Procedure Call (DPC) and TCP Connection (Перевод: Prosper-H)

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

Цели урока:

  1. Использовать WinDbg для отладки на уровне ядра
  2. Применить аппаратные брэйкпойнты на данные, чтобы проследить за их содержимым и проанализировать код, который к ним обращается
  3. Разобраться с TDI Network Service
  4. Разобраться с ключевыми структурами ввода/вывода, такими как _IRP и _IO_STACK_LOCATION
  5. Разобраться в работе процедур отложенного вызова (deferred procedure call)
  6. Разобраться в использовании системного таймера для задержки действий.
  7. Разобраться в использовании очереди WorkItem и worker routines

1. Введение

Сегодня мы закончим исследование последней части кода связанного raspppoe.sys и перейдем к рассмотрению действий руткита связанных с использованием процедур отложенного вызова (Deferred Procedure Call, DPC) и установления TCP-соединения с удаленным сервером. Мы покажем, как использовать WinDbg для того, чтобы найти зашифрованный конфигурационный файл, который содержит список вредоносных серверов (IP и домены), с которыми Max++ пытается связаться. В этом уроке, в сочетании с Уроком 24, мы так же покажем трюки, основанные на многопоточности (multi-thread tricks), которые выполняет Max++, чтобы осложнить анализ. Сетевые действия (создание TCP-соединений, отправка и обработка данных) осуществляется с помощью нескольких потоков. Это создает проблемы в отладке.

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

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

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

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

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

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

3. Код начиная с адреса _+38A1

Рис. 1. Последняя часть rasppoe.sys

Как показано на Рис. 1 (первая выделенная область), первым вызовом является IoAllocateWorkItem(PDevice_Object). Он производит структуру под названием IO_WorkItem, которая описывает объект рабочего потока. Используя WinDbg, мы можем легко определить device_object, который передается вызову.

kd> dt _DEVICE_OBJECT ffa50bb8 -r1
nt!_DEVICE_OBJECT
   ...
      +0x010 DriverSize       : 0x8e00
      +0x014 DriverSection    : 0x8131d310 Void
      +0x018 DriverExtension  : 0x81131888 _DRIVER_EXTENSION
      +0x01c DriverName       : _UNICODE_STRING [B]"\Driver\Disk"[/B]

Затем созданный IoWorkItem сохраняется в глобальной переменной.

Задача 1. Определить, с помощью брэйкпойнтов на данные, когда используется IoWorkItem.

На следующем шаге производится вызов KeInitializeTimer, и объект таймера (timer object) также сохраняется в глобальной переменной.

Задача 2. Определите, как и когда используется таймер.

Как показано на Рис. 1, последующим действием является вызов KeInitializeDPC. Это важный вызов. Согласно MSDN [1], прототип KeInitializeDPC имеет вид представленный ниже. Здесь DPC означает отложенный вызов процедуры (deferred procedure call). Она часто используется для обслуживания запросов ввода/вывода (для того, чтобы низкоприоритетные вызовы были выполнены позже). Функция создает и хранит структуру _DPC в своем первом параметре и регистрирует функцию в качестве второго параметра.

VOID KeInitializeDpc(
  __out     PRKDPC Dpc,
  __in      PKDEFERRED_ROUTINE [B]DeferredRoutine[/B],
  __in_opt  PVOID DeferredContext
);

Используя WinDbg, мы можем вскоре заключить, что отложенная процедура размещена по адресу « _+3016 »

kd> p
_+0x38c2:
faf4f8c2 6a00            push    0
kd> p
_+0x38c4:
faf4f8c4 6816f0f4fa      [B]push    offset _+0x3016 (faf4f016)[/B]
kd> p
_+0x38c9:
faf4f8c9 be7821f5fa      mov     esi,offset _+0x6178 (faf52178)
kd> p
_+0x38ce:
faf4f8ce 56              push    esi
kd> p
_+0x38cf:
faf4f8cf ff152001f5fa    call    dword ptr [_+0x4120 (faf50120)]

Мы можем установить брэйкпойнт на адрес « _+3016 » (bp _+3016) и посмотреть что произойдет.

Далее, Max++ вызывает другую интересную функцию KeSetTimerEx, чтобы связать все предыдущие вызовы вместе. Согласно [2], прототип функции KeSetTimerEx имеет следующий вид:

BOOLEAN KeSetTimerEx(
  __inout   PKTIMER [B]Timer[/B],
  __in      LARGE_INTEGER [B]DueTime[/B],
  __in      LONG Period,
  __in_opt  PKDPC [B]Dpc[/B]
);

KeSetTimerEx использует ранее созданный таймер, чтобы установить «alarm», таки образом, чтобы через некоторое время сработала DPC-процедура, которая расположена по адресу «_+3016 ».

Задача 3. Докажите, что через 15 секунд alarm вызовет процедуру «_+3016 ».

4. Процедура IO Worker, которая подключается к серверу

На Рис. 2. показано тело функции « _+3016 ». Её основная зада состоит в вызове IoQueueWorkItem.

Рис. 2. Тело функции _+3016

Ниже приводится прототип функции IoQueueWorkItem взятый из [3]. Она помещает work item в очередь и связывает его с функцией, которая будет вызвана для его обработки, когда тот будет вынут из очереди.

VOID IoQueueWorkItem(
  __in      PIO_WORKITEM IoWorkItem,
  __in      PIO_WORKITEM_ROUTINE [B][U]WorkerRoutine[/U][/B],
  __in      WORK_QUEUE_TYPE QueueType,
  __in_opt  PVOID Context
);

Используя WinDbg для дампа содержимого стэка, мы можем легко определить, что WorkerRoutine находится по адресу « _+1a0c » (мы назвали ее DPCWorkItemFunc_connects_to_server). Теперь переедем к анализу функции « _1a0c ». Согласно MSDN [4], WorkerRoutine должна иметь следующий прототип:

VOID WorkItem(
  __in      PDEVICE_OBJECT DeviceObject,
  __in_opt  PVOID Context
)

Теперь, когда нам известен прототип процедуры WorkerRoutine, приступим к ее непосредственному анализу. Стоя на первой инструкции, по адресу « _+1a0c », сдампим содержимое стэка. Получим следующий результат:

kd> dd esp
fafb3d64  [B][U]80564610[/U] 8114edf0 00000000[/B] 805622fc
fafb3d74  fafb3dac 804e426b 81227cb0 00000000
fafb3d84  812e9da8 00000000 00000000 00000000
fafb3d94  00000001 80562334 00000000 812e9da8
fafb3da4  00000000 805645fd fafb3ddc 8057aeff
fafb3db4  81227cb0 00000000 00000000 00000000
fafb3dc4  fafb3db8 00000000 ffffffff 804e2490
fafb3dd4  804f8ab0 00000000 00000000 804f88ea

kd> dt _DEVICE_OBJECT 8114edf0  -r1
nt!_DEVICE_OBJECT
   ...
       +0x01c DriverName       : _UNICODE_STRING "\Driver\Disk"
   ...

Очевидно, что 80564610 это адрес возврата, 8114edf0 это device object (см. прототип процедуры WorkItem, выше) и 0x0000 это context, На Рис. 3 показано тело функции « _+1a0c ». Оно состоит всего лишь из 4 инструкций. По адресу 10001A0E, она помещает смещение объекта атрибутов в стэк, а затем вызывает функцию (readConfig_ConnectToServer) расположенную по адресу « _+18d6 »

Рис. 3. Процедура Worker Item

Сначала давайте сдампим содержимое OBJ_ATTR_CONFIG_FILE и переедем к адресу « _+18d6 ». Содержимое дампа показано ниже: из него совершенно ясно, что внутри находится имя скрытого драйвера диска (disk driver)!

kd> dt _OBJECT_ATTRIBUTES faf4a050
nt!_OBJECT_ATTRIBUTES
   +0x000 Length           : 0x18
   +0x004 RootDirectory    : (null) 
   +0x008 ObjectName       : 0xfaf482e8 _UNICODE_STRING [B][U]"\??\C2CAD972#4079#4fd3#A68D#AD34CC121074\{49B474EB-92D0-464f-B1DD-1F37AABF9D95}"[/U][/B]
   +0x00c Attributes       : 0x40
   +0x010 SecurityDescriptor : (null) 
   +0x014 SecurityQualityOfService : (null)

5. Считать конфигурационный файл из скрытого диска и соединиться с сервером

Рис. 5. Первая часть _+18d6

Теперь перейдем к функции по адресу « _+18d6 ». На Рис. 5 показана первая часть ее кода. Поток исполнения программы довольно прост. Сначала Max++ открывает файл «??\C2CAD …». Затем читает его содержимое, после чего устанавливает encoding table и использует её для дешифровки содержимого файла. Ниже показан дамп содержимого файла, из которого мы можем извлечь некоторую интересную информацию: список доменов и IP-адресов для связи!

kd> db fafb3b3c
fafb3b3c  3c 69 70 3e 38 35 2e 31-37 2e 32 33 39 2e 32 31  [B]<ip>85.17.239.21[/B]
fafb3b4c  32 3c 2f 69 70 3e 3c 68-6f 73 74 3e 69 6e 74 65  [B]2</ip><host>inte[/B]
fafb3b5c  6e 73 65 64 69 76 65 2e-63 6f 6d 3c 2f 68 6f 73  [B]nsedive.com</hos[/B]
fafb3b6c  74 3e 4e 80 98 3b fb fa-12 cc 51 80 08 00 00 00  [B]t>[/B]N..;....Q.....
fafb3b7c  00 00 00 00 98 3b fb fa-2e cc 51 80 01 20 f1 df  .....;....Q.. ..
fafb3b8c  02 02 00 00 5d 34 4e 80-4a 61 00 00 f6 32 4e 80  ....]4N.Ja...2N.
fafb3b9c  fd 32 4e 80 12 c6 05 fb-30 00 00 00 38 3c fb fa  .2N.....0...8<..
fafb3bac  08 56 6f 80 00 0d db ba-08 40 00 00 00 00 00 00  .Vo......@......

Задача 1. Самостоятельно повторите и найдите содержимое конфигурационного файла Max++

На Рис. 6. показана оставшаяся часть кода функции « _+18d6 ». Логики весьма очевидна. По адресу 0x10001983 она вызывает собственную функцию руткита getTagContent (находится по адресу « _+1486 »), чтобы найти в конфигурационном файле тэг , которая вскоре находит строку 85.17.239.212. Затем она вызывает системную функцию RtlIpv4StringToAddressExA для создания 32-битного представления IP-адреса.

Задача 2. Проанализируйте функцию «_+1486 » (getTagContent). Каковы её входящие и исходящие параметры?

Рис. 6. Оставшаяся часть функции _+18d6

После чего по адресу 0x100019F8 (см. последнюю выделенную область, Рис. 6), она вызывает собственную функцию руткита openConnectTo85.17.xx (находится по адресу « _+1832 »). Ниже приводится содержимое стэка перед вызовом этой функции. Проследите за его первым параметром: d4ef1155. Можете ли вы угадать его предназначение?

kd> dd esp
fafb3a28  [B][U]d4ef1155[/U][/B] 00005000 00000001 ffb4b850
fafb3a38  812e9da8 652e7ecd a4362152 3c135905
fafb3a48  ee8d12d7 d1b04962 0150df55 0fd8ad5f

Не трудно видеть, что d4ef1155 это целочисленное представление IP-адреса 85.17.239.212 (обратите внимание на последовательность байтов и интерпретацию HEX-значений, например, 0x55 является десятичным числом 85).

6. Установка TCP-соединения

Функция « _+1832 » устанавливает TCP-соединение с адресом 85.17.239.212/5000, однако, ничего не отправляет туда. Операция TDI_SEND в действительности производится системным потоком, обсужденным в Уроке 24. Почему Max++ не пытается выполнить эту операцию в одном потоке? Это создает проблемы для анализа.

На Рис. 7 представлено тело функции « _+1832 ». Мы опускаем большую часть анализа и оставляем более подробное исследование для вас.

Рис. 7. Установка TCP-соединения

Как показано на Рис. 7, Max++ следует общей процедуре установления TCP-соединения. Сначала он связывает (bind) адрес, а затем запрашивает соединение.

Задача 4. Полностью проанализируйте все детали функции « _+1832 »

© Translated by Prosper-H from r0 Crew

Ссылки

[1] MSDN, “KeInitializeDPC routine”
[2] MSDN, “KeSetTimerEx routine”
[3] MSDN, “IoQueueWorkItem”
[4] MSDN, “Work Item Routine”