R0 CREW

Malware Analysis Tutorial 8: PE Header and Export Table (Перевод: Prosper-H)

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

Цели урока:

  1. Разобраться с PE-заголовком исполняемых файлов.
  2. Разобраться с EXPORT TABLE.
  3. Продолжить практиковать навыки дизассемблирования и реверс инжеринга.

1. Введение

В этом уроке, мы проанализируем первую вредоносную операцию, выполняемую руткитом Max++. Она изменяет структуру таблицы экспорта библиотеки ntdll.dll. Напомним, что в прошлом анализе из Урока 7, Max++ читает информацию из TIB, PEB и проверяет каждый загружаемый модуль, пока не встретит «ntdll.dll» (это достигается за счет использования хэш-функции внутри двух уровневого вложенного цикла).

В этом уроке, мы начнем реверерсить код с адреса 0x40105C.

2. Дополнительная информация о PE-заголовке

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

Большинство двоичных исполняемый файлов Windows следуют следующей структуре: DOS-загловок (64 байта), PE-заголовок, секции (кода и данных). Для полного обозрения и введения в формат исполняемого файла, мы рекомендуем ознакомиться с «Portable Executable File Format - A Reverse Engineering View by Goppit» [1].

DOS-заголовок начинается с магического числа 4D 5A 50 00, а последние 4 байта является местом размещения PE-заголовка в исполняемом бинарном файле. Другие поля для нас не так интересны. PE-головок содержит значительно больше информации и более интересен для нас. На рис. 1 изображена краткая структура PE-заголовка. Мы перечисляем только ту информацию, которая будет интересна нам в этом tutorial’e. За дополнительной информацией обращайтесь к работе Goppit’a [1].

Рис. 1. Структура PE-заголовка

Во время выполнения исполняемого файла, загрузчик Windows в действительности загружает PE-заголовок в адресное пространство процесса. Для каждой из основных частей PE-заголовка, в winnt.h, есть несколько хорошо определенных структур.

Как показано на рис. 1, PE-заголовок состоит из трех частей: (1) 4-байтового магического кода, (2) 20-байтового file header, типом которого является IMAGE_FILE_HEADER, и (3) 224-батового optional header (тип: IMAGE_OPTIONAL_HEADER32). Сам по себе дополнительный заголовок (optional header) состоит из двух частей: первая 96-байтовая часть содержит такую информацию как major operating systems, entry point, etc. Вторая 128-байтовая часть является каталогом данных. Она содержит 16 записей. Каждая из который состоит из 8-байт (адрес, размер).

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

2.1 Debugging Tool Support (Small Lab Experiments)

Современные отладчики предоставляют достаточную поддержку для исследования PE-заголовков. Здесь мы обсудим использование WinDbg и Immunity Debugger.

(1) WinDbg. Предположим, что мы знаем, что структура PE-заголовка ntdll.dll располагается в памяти по адресу 0x7C9000E0. Мы можем отобразить вторую часть заголовка, используя следующую команду:

dt nt!_IMAGE_FILE_HEADER 0x7c9000e4
   +0x000 Machine          : 0x14c
   +0x002 NumberOfSections : 4
   +0x004 TimeDateStamp    : 0x4802a12c
   +0x008 PointerToSymbolTable : 0
   +0x00c NumberOfSymbols  : 0
   +0x010 SizeOfOptionalHeader : 0xe0
   +0x012 Characteristics  : 0x210e

Затем мы можем вычислить начальный адрес дополнительного заголовка (optional header): 0x7C9000E4 + 0x14 (20 байт) = 0x7C9000F8. Атрибуты из дополнительного заголовка отображены ниже. Например, основная версия компоновщика равна 7, а адрес точки входа (entry point) 0x12c28 (относительно базового адреса 0x7c900000).

kd> dt _IMAGE_OPTIONAL_HEADER 0x7c9000F8
nt!_IMAGE_OPTIONAL_HEADER
   +0x000 Magic            : 0x10b
   +0x002 MajorLinkerVersion : 0x7 ''
   +0x003 MinorLinkerVersion : 0xa ''
   +0x004 SizeOfCode       : 0x7a000
   +0x008 SizeOfInitializedData : 0x33a00
   +0x00c SizeOfUninitializedData : 0
   +0x010 AddressOfEntryPoint : 0x12c28
   +0x014 BaseOfCode       : 0x1000
   +0x018 BaseOfData       : 0x76000
   +0x01c ImageBase        : 0x7c900000
   +0x020 SectionAlignment : 0x1000
   +0x024 FileAlignment    : 0x200
   +0x028 MajorOperatingSystemVersion : 5
   +0x02a MinorOperatingSystemVersion : 1
   +0x02c MajorImageVersion : 5
   +0x02e MinorImageVersion : 1
   +0x030 MajorSubsystemVersion : 4
   +0x032 MinorSubsystemVersion : 0xa
   +0x034 Win32VersionValue : 0
   +0x038 SizeOfImage      : 0xaf000
   +0x03c SizeOfHeaders    : 0x400
   +0x040 CheckSum         : 0xb62bc
   +0x044 Subsystem        : 3
   +0x046 DllCharacteristics : 0
   +0x048 SizeOfStackReserve : 0x40000
   +0x04c SizeOfStackCommit : 0x1000
   +0x050 SizeOfHeapReserve : 0x100000
   +0x054 SizeOfHeapCommit : 0x1000
   +0x058 LoaderFlags      : 0
   +0x05c NumberOfRvaAndSizes : 0x10
   +0x060 DataDirectory    : [16] _IMAGE_DATA_DIRECTORY

Как показал Goppit [1], OllyDbg также может отобразить структуру PE-файла. Так как Immunity Debugger основан на OllyDbg, то мы можем достичь того же эффекта. В IMM «View => Memory», можно легко определить начальный адрес для каждого модуля (Рис. 2).

Рис. 2. Получение адреса PE-заголовка

Затем в окне дампа памяти, перейдите на начальный адрес PE-файла ntdll.dll. Нажмите «right click» и выберите «special => PE», после чего мы можем получить всю информацию o PE-заголовке библиотеки ntdll.dll, красиво представленную отладчиком IMM.

3. Таблица экспорта

Напомним, что первая запись IMAGE_DATA_DIRECTORY из дополнительного заголовка, является полем, которое содержит информацию о таблице экспорта. Смотря на рис. 1, можно прийти к выводу, что 4 байта, расположенные по смещению PE + 0x78 (т.е. смещение в 120 байт), являются относительным адресом (относительно базового адреса DLL) таблицы экспорта, а следующий байт (по смещению 0x7C) является размером таблицы экспорта.

Типом данных для таблицы экспорта является структура IMAGE_EXPORT_DIRECTORY. К сожалению, набор символов WinDbg не содержит определение этой структуры, но вы можете легко найти ее в winnt.h при помощи поиска в Google (например, в [2]). Ниже дано определение структуры IMAGE_EXPORT_DIRECTORY взятое из [2]:

typedef struct _IMAGE_EXPORT_DIRECTORY {
  DWORD Characteristics; //offset 0x0
  DWORD TimeDateStamp; //offset 0x4
  WORD MajorVersion;  //offset 0x8
  WORD MinorVersion; //offset 0xa
  DWORD Name; //offset [B][U]0xc[/U][/B]
  DWORD Base; //offset 0x10
  DWORD [B][U]NumberOfFunctions;  //offset 0x14[/U][/B]
  DWORD [B][U]NumberOfNames;  //offset 0x18[/U][/B]
  DWORD [B][U]AddressOfFunctions; //offset 0x1c[/U][/B]
  DWORD [B][U]AddressOfNames; //offset 0x20[/U][/B]
  DWORD [B][U]AddressOfNameOrdinals; //offset 0x24[/U][/B]
 }

Здесь нам необходимо, в ручную, рассчитать смещения для каждого из атрибутов. Это нужно для нашего дальнейшего анализа. В приведенном выше определении структуры, WORD – это компьютерное слово равное 16 битам (2 байтам), а DWORD – это двойное слово равное 4-рем батам. Исходя из этого, можно легко прийти к выводу, что MajorVersion располагается по смещению 0x8, а AddressOfFunctions по смещению 0x1C.

Теперь давайте предположим, что IMAGE_EXPORT_DIRECTORY располагается по адресу 0x7C903400, ниже предоставлен дамп из WinDbg (тут, команда «dd» должна вывести на экран дамп памяти):

kd> dd 7c903400
7c903400  00000000 48025c72 00000000 [B][U]00006786[/U][/B]
7c903410  00000001 00000523 [B][U]00000523[/U][/B] 00003428
7c903420  [B][U]000048b4[/U][/B] 00005d40 00057efb 00057e63
7c903430  00057dc5 00002ad0 00002b30 00002b40
7c903440  00002b20 0001eb58 0001ebb9 0001e3af
7c903450  0002062d 000206ee 0004fe3a 00012d71
7c903460  000211e7 0001eaff 0004fe2f 0004fdaa
7c903470  0001b08a 0004febb 0004fe6d 0004fde6

На основании полученного дампа, можно сделать вывод, что в таблице экспорта находится 0x523 функции и 0x523 имени. Почему? Потому что NumberOfFunctions располагается по смещению 0x14 (таким образом, его адрес равен 0x7c903400+0x14 = 0x7c903414). В качестве другого примера, посмотрите на атрибут «Name», который расположен по смещению 0x1C (т.е. его адрес равен 0x7c90340c), где мы имеем число 0x00006787. Это число – адрес, относительно базового адреса DLL (предположим, он равен 0x7c900000), где мы имеем имя модуля, которое расположено по адресу 0x7c906786. Можно проверить это утверждение, используя команду «db» (отобразить содержимое памяти в виде байтов) в отладчике WinDbg: вы можете убедиться в том, что имя модуля действительно соответствует ntdll.dll.

kd> db 7c906786
7c906786  6e 74 64 6c 6c 2e 64 6c-6c 00 43 73 72 41 6c 6c  [B][U]ntdll.dll[/U][/B].CsrAll
7c906796  6f 63 61 74 65 43 61 70-74 75 72 65 42 75 66 66  ocateCaptureBuff

Прочитав стр. 26 из [1], вы обнаружите, что «AddressOfFunctions», «AddressOfNames» и «AddressOfNameOdinals» являются наиболее важными атрибутами. Есть три массива (показаны ниже), и каждый из выше названных атрибутов соответственно содержит начальный адрес для одного из них:

PVOID Functions[523]; //each element is a function pointer
char * Names[523]; //each element is a char * pointer
short int Ordinal[523]; //each element is an [B][U]16 bit[/U][/B] integer

Например, из наших подсчетов, мы знаем, что массив «Names» начинается с адреса 7C9048B4 (получен из числа 0x48B4 расположенного по смещению 0x20 для атрибута AddressOfNames: и с тем предположением, что базовый адрес равен 0x7C900000). Мы знаем, что каждый элемент массива Names равен 4-рем байтам. Ниже приведен дамп первых 8 элементов:

kd> dd 7c9048b4
7c9048b4  00006790 000067a9 000067c3 000067db
7c9048c4  00006807 0000681f 00006831 00006845

Мы можем проверить первое имя (00006790): это CsrAllocateCaptureBuffer. Обратите внимание, что нулевой байт используется для завершения строки.

kd> db 7c906790
7c906790  43 73 72 41 6c 6c 6f 63-61 74 65 43 61 70 74 75  [B][U]CsrAllocateCaptu[/U][/B]
7c9067a0  72 65 42 75 66 66 65 72-00 43 73 72 41 6c 6c 6f  [B][U]reBuffer[/U][/B].CsrAllo

Также мы можем проверить второе имя (000067a9): Это CsrAllocateMessagePointer.

kd> db 7c9067a9
7c9067a9  43 73 72 41 6c 6c 6f 63-61 74 65 4d 65 73 73 61  [B][U]CsrAllocateMessa[/U][/B]
7c9067b9  67 65 50 6f 69 6e 74 65-72 00 43 73 72 43 61 70  [B][U]gePointer[/U][/B].CsrCap

Теперь, такой вопрос: как с учетом имени функции, мы можем найти ее адрес? Ниже дана формула (заметьте, что индеек массива начинается с 0):

Предположим, что Names[x].equals(FunctionName), тогда «адрес функции» = Functions[Ordinal[x]]

4. Вопрос дня

Ниже показаны первые шестнадцать элементов из Ordinal:

kd> dd 7c905d40
7c905d40  00080007 000a0009 000c000b 000e000d
7c905d50  0010000f 00120011 00140013 00160015

И первые восемь элементов из массива Fucntions:

kd> dd 7c903428
7c903428  00057efb 00057e63 00057dc5 00002ad0
7c903438  00002b30 00002b40 00002b20 0001eb58

Каким будет начальный адрес функции CsrAllocateCaptureBuffer? Ответ: это адрес 7C91EB58. Подумайте, почему? (Обратите особое внимание на порядок байтов целых чисел).

5. Анализ кода

Начнем анализ кода, с адреса 0x40105C. Установите аппаратный брэйкпойнт на 0x40105C (в панели кода, нажмите «right click => Go To => Expression (0x40105c)», затем нажмите «right click => Breakpoints => Hardware, on Execution»). Нажмите F9 для выполнения программы до места срабатывания брэйкпойнта. Первой инструкцией должна быть инструкция PUSH DS:[EAX+8]. Если вы видите кучу BYTE DATA инструкций, то это вызвано расщеплением байта в коде. Выделите все эти BYTE DATA инструкции, нажмите «right click => Analysis => During next analysis, treat selection as => Command», после чего мы должны получить корректное отображение дизассемблерного кода в IMM.

Рис. 4. Обращение к таблице экспорта

Теперь, давайте проанализируем первые несколько инструкций, начиная с адреса 0x40105C (Рис. 4.). Продолжая анализ из Урока 7, мы знаем, что после чтения информации о модуле (один за другим), код выходит из цикла, когда встречает модуль ntdll.dll. В этот момент, EAX содержит адрес из смещения 0x18 из LDR_DATA_TABLE_ENTRY (прим. пер. регистр EAX содержит адрес из смещения 0x10 из LDR_DATA_TABLE_ENTRY). Другими словами, EAX указывает на атрибут «DllBase» (прим. пер. указывает на InInitializationOrderLinks). Таким образом, инструкция по адресу 0x40105C, т.е. PUSH DWORD DS:[EAX+8] должна поместить DllBase в стэк. Выполняя эту команду вы увидите, что на верхушке стэка появился адрес 0x7C900000.

Вскоре поток управления переходит в 0x401077 и 0x401078. Затем значение 0x7C90000 (база DLL) передается регистру ECX, по адресу 0x40107A. Теперь давайте рассмотрим инструкцию 0x40107D.

MOV EAX, DWORD PTR DS:[ECX+3C]

Напомним, что в начале PE-файла лежит DOS-заголовок (который равен 64 байтам), а последние 4 байта DOS-заголовка содержат адрес расположения PE-заголовка (смещение относительно базы DLL) [см. Раздел 2]. Шестнадцатеричное значение 0x3C равняется десятичному значению 60! Таким образом, у нас теперь есть EAX, который содержит смещение PE-заголовка. Посмотрев в панель регистров видно, что EAX = 0xE0. Из чего мы делаем вывод, что PE-заголовок расположен по адресу 0x7C9000E0 (который был получен путем сложения «базы 0x7C900000 + смещения 0xE0»).

Теперь проследите за инструкцией 0x401087:

MOV ESI, DWORD PTR DS:[EAX+78]

Заметьте, что смещение 0x78 равно десятичному значению 120. Смотря на рис. 1, можно вскоре прийти к выводу, что смещение 0x78 это адрес таблицы EXPORT, запись которой содержится в IMAGE_DATA_DIRECTORY, которая в свою очередь содержится в дополнительном заголовке. Таким образом, ESI сейчас содержит адрес таблицы экспорта (смещение относительно базы DLL)! После выполнения инструкции по адресу 0x40108A, значение ESI равно 0x7C903400 (начальный адрес EXPORT TABLE).

5. Задача дня

Мы продемонстрировали вам некоторые базовые методы анализа, применяемые реверс инженерами для исследования логики вредоносных программ. Сегодня ваша задача заключается в том, чтобы продолжить наш анализ и объяснить, что пытается сделать руткит Max++. В частности, можете следовать следующим шагам:

  1. Что делает функция 0x004138A8? Какие у нее входящие параметры?
  2. К каким полям таблицы экспорта получают доступ инструкции между адресами 0x4010AD и 0x4010C4?
  3. Объясните, что означает «EDX*8+C» в инструкции по адресу 0x4010BB.
  4. Объясните логику инструкций, начиная с адреса 0x4010CB по 0x4010F6.
  5. Какая цель цикла начинающегося с адреса 0x401103 по 0x401115?
  6. Что делает функция 0x40165E? Какие у нее входящие параметры?
  7. Объясните код между адресами 0x401117 и 0x40113E.

© Translated by Prosper-H from r0 Crew

Ссылки

[1] Goppit, “Portable Executable Format - a Reverse Enginnering View”, v1(2), Code Breakers Magzine, January 2006.
[2] An online copy of winnt.h