R0 CREW

Malware Analysis Tutorial 17: Infection of System Modules (Part I: Randomly Pick a Driver) (Перевод: Prosper-H)

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

Цели урока:

  1. Разобраться с часто используемыми трюками, которые применяет малварь для инфицирования драйверов.
  2. Понять роль реестра в ОС Windows.
  3. Поупражняться в анализе функций.
  4. Поупражняться в реверс инженеринге сложных системных структур данных.

1. Введение

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

В этом уроке, мы начнем анализ кода с адреса 0x3C2408. Это должно продолжить анализ из Урока 16, где мы показали, что Max++ внедряется в поток другого исполняющегося процесса, и что этот поток удаляет двоичный исполняемый файл Max++Loader2010.exe c диска, как только тот завершит свою работу.

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

(0) Запустите Guest XP в DEBUGGED режиме. Теперь в Host XP, запустите окно командной строки и перейдите в «C:\Program Files\Debugging Tools for Windows (x86)» (туда где установлен WinDbg). Введите «windbg -b -k com:pipe,port=\.\pipe\com_12» (проверьте номер COM-порта, установленный у вашего экземпляра VBox). Когда WinDbg запустится, дважды введите «g» (go), чтобы позволить отлаживаемой операционной системе продолжить свое выполнение.

(1) Теперь на Guest XP запустите IMM и очистите в нем все аппаратные и программные брэйкпойнты (см. «View => Breakpoints» и «View => Hardware Breakpoints»)

(2) Перейдите на адрес 0x4012DC и установите там аппаратный брэйкпойнт (почему не программный BP? Потому что эта область будет перезаписана во время само-распаковки и программный BP будет потерян).

(3) Нажмите F9 несколько раз, чтобы выполнить программу до адреса 0x4012DC. Вы столкнетесь с несколькими брэйкпойнтами прежде чем дойдете до 0x4012DC. Если вы обратите внимание, то заметите, что они в действительности вызваны трюками с INT 2D (объяснено в Уроках 3, 4 и 5). Просто игнорируйте их и продолжайте выполнение (используя F9) до тех пор, пока вы не достигните 0x4012DC.

На рис. 1 показан код, который вы сейчас должны видеть. Как вы можете видеть, это прямо перед вызовом RtlAddVectoredException, где устанавливается аппаратный BP, который предназначен для того, чтобы прервать вызов LdrLoadDll (Урок 11).

Рис. 1. Код по адресу 0x4012DC

(4) Теперь прокрутите на две страницы вниз и установите SOFTWARE BREAKPOINT на 0x401417. Это сразу после вызова LdrLoadDll(«lz32.dll»), где Max++ завершает загрузку lz32.dll. Затем нажмите несколько раз SHIFT+F9 пока не достигните 0x401417 (вы попадете дважды на адрес 0x7C90D500, это где-то внутри функции ntdll.ZwMapViewSection, которая вызывается LdrLoadDll).

Рис. 2. Код по адресу 0x401407

(6) Теперь установим брэйкпойнт на 0x3C2408. Перейдите на 0x3C2408 и установите SOFTWARE BREAKPOINT. Нажмите SHIFT+F9, чтобы программа выполнилась до 0x3C2408 (при установке брэйкпойнта вы можете видеть предупреждение, сообщающее, что его установка происходит вне кодового сегмента. Просто игнорируйте его).

На рис. 3. показан код, который вы должны видеть по адресу 0x3C2408. Первой инструкцией должна быть инструкция TEST EAX, EAX. Значение регистра EAX в данный момент должно быть 0x1, а поток управления продолжит выполнение к адресу 0x3C2410 (PUSH 10000). Отсюда мы и начнем наш анализ.

Рис. 3. Код, начинающийся с 0x3C2408

Раздел 3. Проверка Infection Status

Первое действие Max++ заключается в том, чтобы определить, является ли система уже инфицированной. Он проверяет существование имени виртуального раздела «??\C2CAD972#4079#4fd3#A68D#AD34CC121074». Обратите внимание, что это очень интересное имя, если вы посмотрите в спецификацию имен файлов Microsoft UNC, то заметите, что это не совсем легальное имя. Обычно UNC-имена с расширенной длиной должны начинаться с «\?\», вместо двух знаков вопроса (насколько я знаю). Если система прежде никогда не была инфицирована, то вызов ZwOpenFile по адресу 0x3C242D дожжен провалиться и возвратить код ошибки. Вызов ZwOpenFile будет проходить успешно только тогда, когда Max++ успешно создаст виртуальное устройство и перезапишет драйвер, отвечающий за операции с диском (таким образом, драйвер сможет обрабатывать префикс ??).

Было бы интересно покопаться в вызове ZwOpenFile и посмотреть на его параметры. В прошлом, мы использовали IMM для просмотра параметров. Но когда параметр является сложным типом данных, более удобно использовать WinDbg. Далее мы покажем пример использования WinDbg.

Правильное объявление ZwOpenFile может быть легко найдено в Google. Нас интересуют первые три параметра этой функции: (1) OUT PHANDLE FileHandle, (2) ACCESS_MASK DesiredAccess и (3) POBJECT_ATTRIBUTES ObjectAttributes, согласно [2].

Рис. 4. Содержимое стэка сразу после вызова ZwOpenFile

Если вы посмотрите на содержимое стэка в момент, когда сделан вызов (Рис. 4), то вы заметите, что значение третьего параметра (ObjectAttributes) равно 0x3D3150. Обратите внимание, что типом является POBJECT_ATTRIBUTES, где «P» обозначает указатель. Очевидно, что по адресу 0x3D3150, расположена структура OBJECT_ATTRIBUTES. Вполне возможно, получить правильно объявление структуры OBJECT_ATTRIBUTES и сопоставить смещения для каждого из ее полей. Но более простой способ, для решения этой задачи, заключается в использовании WinDbg.

С этой целью, давайте запустим WinDgb в экземпляре виртуальной машины Guest XP. Нажмите «File => Attach Process => Max++» (примечание: делать это нужно не в Host XP, а в Guest XP), а затем выберите опцию «Noninvasively» (Рис. 6).

Рис. 6. Выполнение WinDbg в Noninvasive режиме

Просто введите «dt _OBJECT_ATTRIBUTES 0x3D3150 -r2» в окне командной строки WinDbg и мы получим содержимое дампа, как показано ниже. Здесь «-r2» отвечает за то, чтобы отобразить 2 layers of details level (recursively). На рис. 7 показан дамп OBJECT_ATTRIBUTES. Вы можете видеть, что файл пытается получить доступ к «??\C2CAD972#4079#4fd3#A68D#AD34CC121074». Вызов вернет код ошибки в EAX, что приведет поток исполнения к адресу 0x3C243B (Рис. 3), а затем к 0x3C2461.

Раздел 4. Поиск модуля для заражения (Функция 0x3C1C2C)

Рассмотрим логику функции 0x3C1C2C (Рис. 7). Первая часть функции должна вызвать функцию 0x3D0BC0. Она часто вызывается из кода Max++. Ее детальный анализ мы оставим для вас.

Задача 1. Проанализируйте функциональность функции 0x003D0BC0 (Какие параметры являются входящими? А какие исходящими?). Подсказка: функция корректирует позицию регистра ESP в зависимости от входящих параметров, а затем помещает адрес следующей инструкции в EAX.

Рис. 7. Первая часть функции 0x3C1C2C

Основная часть функции является циклом, с адреса 0x003C1C5B по 0x003C1D04 (Рис. 8). Во время первой итерации, код вызывает ZwQueryInformation, чтобы получить список системных модулей (все работающие driver processes в системе) и находит системный модуль с именем «ndis.sys». Обратите внимание, что NDIS означает Network Driver Interface Specification, ndis.sys – это драйвер, который управляет сетевыми адаптерами. Уже на основании этого, можно предположить, что же на самом деле делает этот цикл, но вам было бы полезно повторить анализ этого процесса самим, чтобы отточить свои навыки реверс инженеринга.

(1) ZwQuerySystemInformation. Это очень важный системный вызов. Прототип этой функции показан ниже, из MSDN:

NTSTATUS WINAPI ZwQuerySystemInformation(
  __in       SYSTEM_INFORMATION_CLASS SystemInformationClass,
  __inout    PVOID SystemInformation,
  __in       ULONG SystemInformationLength,
  __out_opt  PULONG ReturnLength
);

Первым параметром является число, которое представляет тип запрашиваемой информации. Существует много доступных типов, например, system performance information, time of the day, etc. В нашем случае, в system information class передается тип SystemModuleInformation, который предоставляет информацию о выполняющихся в системе модулях. Обратите внимание, что размер SystemInformation (второй параметр) может вирироваться, в зависимости от типа первого параметра, вы должны передать размер для вашего заранее выделенного буфера в параметре SystemInformationLength. Если ZwQuerySystemInformation будет нуждаться в большем количество пространства, то она сообщит вам, что места не достаточно (с помощью 4-го параметра ReturnLength). В нашем случае, код Max++, вернется обратно для перераспределения пространства (см. вызов функции 0x3D0BC0 по адресу 0x3C1C61, и см. Задача 1)

Рис. 8. Чтение System Module Information

(2) System Module Information. Давайте покопаемся в данных возвращаемых функцией ZwQueryInformation. С помощью простого поиска в Google, мы можем найти определение структур _SYSTEM_MODULE_INFORMATION и _SYSTEM_MODULE. Как показано ниже (код взят из http://source.winehq.org/source/include/winternl.h):

typedef struct _SYSTEM_MODULE_INFORMATION
{
ULONG               ModulesCount;
SYSTEM_MODULE       [B][U]Modules[0];[/U][/B] /* FIXME: should be Modules[0] */
} SYSTEM_MODULE_INFORMATION, *PSYSTEM_MODULE_INFORMATION;
typedef struct _SYSTEM_MODULE
{
PVOID               Reserved1;                      /* 00/00 */
PVOID               Reserved2;                      /* 04/08 */
PVOID               ImageBaseAddress;               /* 08/10 */
ULONG               ImageSize;                      /* 0c/18 */
ULONG               Flags;                          /* 10/1c */
WORD                Id;                             /* 14/20 */
WORD                Rank;                           /* 16/22 */
WORD                Unknown;                        /* 18/24 */
WORD                NameOffset;                     /* 1a/26 */
BYTE                Name[MAXIMUM_FILENAME_LENGTH];  /* 1c/28 */
} SYSTEM_MODULE, *PSYSTEM_MODULE;

Отметим, что Modules[0], второй параметр из структуры _SYSTEM_MODULE_INFORMATION, является реальным массивом (т.е. это не просто указатель на начальный адрес массива). Другими словами, размер _SYSTEM_MODULE_INFORMATION может варироваться, в зависимости от значения ModulesCount. Аналогично, последний атрибут NAME[MAXIMUM-FILENAME_LENGTH] из _SYSTEM_MODULE, является реальным массивом символов (не указателем).

На рис. 9 отображены первые 0x90 байт структуры _SYSTEM_MODULE_INFORMATION.

Задача 2. Исходя из рис. 9, можете ли вы сделать вывод, сколько модулей находится в _SYSTEM_MODULE_INFORMATION? (т.е. сколько модулей, прямо сейчас, загружено в системе?)

Задача 3. Исходя из рис. 9, можете ли вы определить имя первого модуля? Каково значение NameOffset в первом _SYSTEM_MODULE?

Рис. 9. System Module Information

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

Задача 4. Смотря на рис. 8, понаблюдайте за инструкциями по адресам 0x3C1C73 и 0x3C1C78, они помещают параметры для функции ZwQueryInformation. Какой из регистров (EAX, ESI, etc) содержит буфер для хранения системной информации?

Задача 5. Смотря на рис. 8, понаблюдайте за инструкцией по адресу 0x3C1CDF. Этой инструкцией является LEA ESI, DS:[EAX+EDI+1C]. Объясните значение и цель этой инструкции? В особенности, объясните, что содержится в EAX и EDI и что значит смещение 0x1c?

Задача 7. Найдите способ выйти из цикла и определите, каков ImageBaseAddress у «ndis.sys»? (сначала, вы должны правильно установить «условный брэйкпойнт»)

Задача 8. Объясните логику работы инструкций между адресами 0x3C1CF3 (NEG EAX) и 0x3C1CFE (JE 0x3C1E1E), и объясните, почему инструкция JE не срабатывает, когда именем модуля является «NDIS.sys»

Рис. 10. После того, как NDIS.sys был найден

Раздел 5. Test Registry (Функция 0x003C18D4)

Когда модуль NDIS был найден, поток управления продолжается с адреса 0x003C1D04 (Рис. 10). Сначала этот код сравнивает имя «ndis.sys» и «win32ksys» и если они не равны, он вызывает функцию 0x003C18D4 по адресу 0x003C1D27. Мы предоставим анализ этой функции 0x003C18D4, ниже.

Рис. 11. Первая часть функции 0x3C18D4

На рис. 11 показана первая часть функции 0x3C18D4. Вначале, она подсчитывает длину строки из файла ndis.sys и проверяет, не является ли имя файла слишком длинным. Затем она проверяет, является ли первая буква из имени файла ASCII-кодом 2E « . » (если нет, то продолжает работу). После чего, она ищет суффикс файла и проверяет, заканчивается ли она на «.sys». Эти довольно рутинные проверки, предназначены для того, чтобы убедиться что файл (ndis.sys) является нормальным системным драйвером. Позже, анализируемая функция вызовет функцию lz32.003C250C по адресу 0x003c193e. Собственно ее (функцию 0x003c250c) мы сейчас и попытаемся проанализировать.

5.1 Функция 0x003C250C (Создание Object_Attributes)

Вообще, для анализа функции, нам нужно определить три вещи: (1) каковы входящие параметры? (2) каковы исходящие параметры? (3) что собственно делает эта функция?

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

На рис. 12 показано тело функции 0x003C250C. Изучив инструкции, мы знаем, что входящим параметром этой функции является регистр ESI. Тело функции вызывает RtlInitUnicodeString и затем сохраняет некоторые значения в памяти (в ESI+4, ESI+8, etc). Кажется, что функция создает некоторую структуру данных, однако, у нас нет никакого способа точно определить ее тип. В этом случае, мы должны проследить за тем, как используются возвращаемые данные (обратите внимание, что функция возвращает данные в регистре EAX, посмотрите на инструкцию 0x3C2535).

Рис. 12. Тело функции 0x3C250C

На рис. 13 показан код вызывавший функцию 0x3C250C. Обратите внимание, что по адресу 0x003C1943 (прямо после вызова функции 0x3C250C), код помещает EAX (возвращаемое значение из функции 0x3C250C) в стэк, а затем помещает в стэк еще два значения, и под конец вызывает функцию ZwOpenKey. Очевидно, что возвращаемое значение функцией 0x3C250C используется в качестве третьего параметра ZwOpenKey, что сразу же приводит нас к тому выводу, что: функция 0x3C250C создает объект _OBJECT_ATTRIBUTES. На рис. 14 показаны детали созданного объекта. Там вы можете видеть, что основным компонентом является ObjectName, который содержит значение «\registry…\NDIS».

Рис. 13. Как используются данные, возвращаемые функцией 0x3C250C

Рис. 14. Содержимое памяти Object Attributes

5.2 Оставшаяся часть Функции 0x003C18D4 (проверяет существование ключа реестра)

Теперь давайте продолжим анализ оставшейся части кода функции 0x003C18D4 (Рис. 15). Интересно, после вызова ZwOpenKey, код сразу вызывает ZwClose, чтобы немедленно закрыть дескриптор регистра. Тогда какова его цель? Если проверите поток выполнения программы, то вы заметите, что в зависимости от возвращаемого значения ZwOpenKey, функция возвращает «1», когда ключ регистра успешно открыт, и «0», когда этого сделать не удается.

Рис. 15. Оставшаяся часть кода функции 0x3C18D4

Рис. 16. Чтение NDIS.sys

Раздел 6. Поиск подходящих драйверов (начиная с адреса 0x3C12DC)

Как показано на рис. 16, по адресу 0x3C1D27, управление возвращается из функции 0x3C18D4 (проверяет существование ключа реестра для NDIS). Затем последующий код дважды выполняет ZwReadFile, сначала чтобы прочитать 0x40 байт, а затем чтобы прочитать 0xF8 байт. Второе прочтение загружает весь PE-заголовок (0xF8 байт) в 0x12D388.

Рис. 17. Проверка правильного размера

Тут начинается интересная часть, см. Рис. 17. По адресу 0x3C1DDF, код проверяет размер EXPORT TABLE из PE-заголовка и смотрит, НЕ равен ли он нулю. Если он равен нулю, то код продолжит поиск и приступит к проверке другого системного модуля. NDIS.sys не проходит проверку (размер его таблицы экспорта равен 0).

Затем по 0x003C1DF3, он вызывает ZwQueryInformationFile и читает структуру FILE_STANDARD_INFORMATION из модуля (на этот раз, из mup.sys). После чего читает размер файла из модуля и проверяет его со значением 0x4C10. Если размер файла меньше 0x4C10, то он продолжает поиск.

Если до сих пор все было хорошо, то код, по адресу 0x003C1E0E, перезаписывает, в структуре _SYSTEM_MODULE, атрибуты ID и ImageSize. Атрибуту ID присваивается значение 1, а в ImageSize записывается действительный размер системного модуля.

Для каждого найденного системного модуля (c размером EXPORT TABLE > 0 и размером файла > 0x4C10), код увеличивает счетчик по адресу 0x12D5D4 на 1

Задача 9. Объясните, детально, логику кода из рис. 17. Например, для инструкции по адресу 0x3C1DDF, как бы вы узнали, что она проверяет размер EXPORT TABLE?

Раздел 7. Случайны выбор драйвера (начиная с адреса 0x3C1E30)

Давайте рассмотрим следующий раздел вредоносной логики (Рис. 18). Когда код выполняется к адресу 0x3C1E30 (выйдя из большого цикла), в EBP-30 (0x12D5D4), он сохраняет количество системных модулей, которые удовлетворяют следующим критериям: (1) размер файла больше чем 0x4C10 и (2) размер EXPORT TABLE > 0. На нашей системе существует 0x19 таких модулей (у вас может быть другое число), которые удовлетворяют этим условиям.

Следующий раздел (с адреса 0x003C1E30 до 0x3C1E6D) случайным образом подбирает модули для заражения. Сначала, код вызывает GetTickCount, чтобы получить текущее время, затем вызывает RtlRandom, используя текущее время в качестве начального числа. Далее производится операция DIV (по 0x3C1E51) на число 0x19 (количество модулей), после которой остаток от деления сохраняется в EDX.

Рис. 18. Случайны выбор драйвера для заражения

Цикл, из адреса 0x3C1E59 по 0x3C1E6D, используется для получения структуры _SYSTEM_MODULE. Беря в учет то, что в EDX хранится случайный ID, без loss of generality, давайте предположим, что он равен 5. Он пытается получить 5-й модуль, удовлетворяющий всем критериям, просматривая каждый модуль из списка _SYSTEM_MODULES, который был получен ранее, вызовом ZwQuerySystemInformation. Обратите внимание, что по 0x003C1E59, он сравнивает [ESI+14] с BX (равен 1). Здесь, ESI указывает на _SYSTEM_MODULES, а смещение 0x14 на ID. Напомним, что ранее код пометил каждый модуль, удовлетворяющий критериям, установив ID в «1», это сразу объясняет цель сравнения кода. После того как управление выйдет из цикла, ESI будет указывать на выбранный _SYSTEM_MODULE. В этом месте, случайный выбор системного модуля – завершен. Мы объясним процесс инфицирования драйвера в следующем уроке.

© Translated by Prosper-H from r0 Crew