Фундаментальные основы хакерства. Мастер-класс по анализу исполняемых файлов в IDA Pro

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

С легкой руки Денниса Ритчи повелось начинать освоение нового языка программирования с создания простейшей программы «Hello, World!». Не будем нарушать эту традицию и оценим возможности IDA Pro следующим примером.

Чтобы все было как в статье, компилируй примеры с помощью Visual C++ 2017 вызовом cl.exe first.cpp /EHcs. Флаг /EHcs нужен, чтобы подавить возражение компилятора и вместе с тем включить семантику уничтожения объектов.

#include <iostream>
void main()
{
  std::cout << "Hello, Sailor!n";
}

Компилятор сгенерирует исполняемый файл объемом почти 190 Кбайт, большую часть которого займет служебный, стартовый или библиотечный код. Попытка дизассемблирования с помощью таких средств, как W32Dasm, не увенчается быстрым успехом, поскольку над полученным листингом размером в два с половиной мегабайта (!) можно просидеть не час и не два. Легко представить, сколько времени уйдет на серьезные задачи, требующие изучения десятков и сотен мегабайтов дизассемблированного текста.

Попробуем дизассемблировать эту программу с помощью IDA. Если все настройки оставить по умолчанию, то после завершения анализа экран должен выглядеть следующим образом.

Результат работы IDA Pro

Чтобы открыть текстовое представление, надо из контекстного меню выбрать пункт Text view.

 

Какую версию IDA Pro выбрать?

Последней версией IDA Pro на момент написания статьи была 7.3. Ее цена может быть великовата для покупки в исследовательских целях. Как известно, Ильфак Гильфанов очень строго относится к утечкам и появлению продуктов своей компании в интернете и не допускает подобного.

Однако на сайте компании Hex-Rays в публичный доступ выложена бесплатная версия дизассемблера с функциональными ограничениями. Например, она не получает обновления после достижения майлстоуна целой версии, то есть сейчас для свободной загрузки доступна версия 7.0. Также она поддерживает только архитектуры x86 и x64.

Тем не менее этого вполне достаточно для наших целей. Потому что нам не придется разбираться в коде для процессоров ARM, Motorola, Sparc, MIPS или Zilog. Еще одно ограничение накладывается на использование в коммерческих целях, но и тут наша совесть чиста.

Закончив автоматический анализ файла first.exe, IDA переместит курсор к строке .text:0040628B — точке входа в программу. Не забудь из графического режима отображения листинга переключиться в текстовый. Также обрати внимание на строчку .text:00406290 start endp ; sp-analysis failed, выделенную красным цветом в конце функции start. Так IDA отмечает последнюю строку функции в случае, если у нее в конце return и значение указателя стека на выходе из функции отличается от такового на входе.

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

  • argc — количество аргументов командной строки;
  • argv — массив указателей на строки аргументов;
  • _environ — массив указателей на строки переменных окружения.

Заполняется структура OSVERSIONINFOEX, которая среди прочего включает:

  • dwBuildNumber — билд;
  • dwMajorVersion — старшую версию операционной системы;
  • dwMinorVersion — младшую версию операционной системы;
  • _winver — полную версию операционной системы;
  • wServicePackMajor — старшую версию пакета обновления;
  • wServicePackMinor — младшую версию пакета обновления.

Далее Start инициализирует кучи (heap) и вызывает функцию main, а после возвращения управления завершает процесс с помощью функции Exit. Для получения значений структуры OSVERSIONINFOEXW используется функция GetVersionEx.

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

CRtO.demo.c

#include <stdio.h>
#include <stdlib.h> 
#include <Windows.h>

void main()
{
  OSVERSIONINFOEX osvi;
  ZeroMemory(&osvi, sizeof(OSVERSIONINFOEX));
  osvi.dwOSVersionInfoSize = sizeof(OSVERSIONINFOEX);
  GetVersionEx((OSVERSIONINFO*)&osvi);

  int a;
  printf(">OS Version:ttt%d.%dn
  >Build:ttt%dn
  >Arguments count:t%dn", 
    osvi.dwMajorVersion, osvi.dwMinorVersion, osvi.dwOSVersionInfoSize, __argc);
  for (a = 0; a < __argc; a++)
    printf(">t Argument %02d:tt%sn", a + 1, __argv[a]);
  a = !a - 1;
  while (_environ[++a]);
    printf(">Environment variables count:%dn", a);
  while (a) 
    printf(">tVariable %d:tt%sn", a, _environ[--a]);
}

Прототип функции main как будто указывает, что приложение не принимает никаких аргументов командной строки, но результат работы программы говорит об обратном и на моей машине выглядит так (приводится в сокращенном виде):

OS Version: 6.2
Build: 156
Arguments count: 1
Argument 01: CRt0.demo.exe
Environment variables count: 99

Variable 20: FrameworkVersion=v4.0.30319
Variable 19: FrameworkDIR32=C:\WINDOWS\Microsoft.NET\Framework\
Variable 18: FrameworkDir=C:\WINDOWS\Microsoft.NET\Framework\
Variable 17: Framework40Version=v4.0
Variable 16: ExtensionSdkDir=C:\Program Files (x86)\Microsoft SDKs\Windows Kits\10\ExtensionSDKs
Variable 15: DriverData=C:\Windows\System32\Drivers\DriverData

Очевидно, нет никакой необходимости анализировать стандартный стартовый код приложения, и первая задача исследователя — найти место передачи управления на функцию main. К сожалению, чтобы гарантированно решить эту задачу, потребуется полный анализ содержимого функции Start. У исследователей есть много хитростей, который позволяют не делать этого, но все они базируются на особенностях реализации конкретных компиляторов и не могут считаться универсальными.

Например, Visual C++ всегда, независимо от прототипа функции main, передает ей три аргумента: указатель на массив указателей переменных окружения, указатель на массив указателей аргументов командной строки и количество аргументов командной строки, а все остальные функции стартового кода принимают меньшее количество аргументов.

Рекомендую ознакомиться с исходниками стартовых функций популярных компиляторов. Для Visual C++ 14 в соответствии с архитектурой они находятся в подпапках каталога %Program Files (x86)Microsoft Visual Studio 14.0VCcrtsrc. Их изучение упростит анализ дизассемблерного листинга.

Ниже в качестве иллюстрации приводится фрагмент стартового кода программы first.exe, полученный в результате работы W32Dasm.

//******************** Program Entry Point ********************
:0040628B E802070000 call 00406992
:00406290 E974FEFFFF jmp 00406109

* Referenced by a CALL at Addresses:
|:0040380C , :004038B2 , :0040392E , :00403977 , :00403A8E
|:00404094 , :004040FA , :00404262 , :00404BF4 , :00405937
|:004059AE
|

* Referenced by a (U)nconditional or (C)onditional Jump at Address:
|:004062B6(U)
|
:00406295 8B4DF4 mov ecx, dword ptr [ebp-0C]
:00406298 64890D00000000 mov dword ptr fs:[00000000], ecx
:0040629F 59 pop ecx
:004062A0 5F pop edi
:004062A1 5F pop edi
:004062A2 5E pop esi
:004062A3 5B pop ebx
:004062A4 8BE5 mov esp, ebp
:004062A6 5D pop ebp
:004062A7 51 push ecx
:004062A8 F2 repnz
:004062A9 C3 ret

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

Поэтому способности дизассемблера тесно связаны с его версией и полнотой комплекта поставки — далеко не все версии IDA Pro в состоянии работать с программами, сгенерированными современными компиляторами.

.text:0040628B start proc near
.text:0040628B
.text:0040628B ; FUNCTION CHUNK AT .text:00406109 SIZE 00000127 BYTES
.text:0040628B ; FUNCTION CHUNK AT .text:00406265 SIZE 00000026 BYTES
.text:0040628B
.text:0040628B call sub_406992
.text:00406290 jmp loc_406109
.text:00406290 start endp ; sp-analysis failed
.text:00406290
.text:00406295 ; [00000015 BYTES: COLLAPSED FUNCTION __EH_epilog3. PRESS CTRL-NUMPAD+ TO EXPAND]
.text:004062AA ; [00000011 BYTES: COLLAPSED FUNCTION __EH_epilog3_GS. PRESS CTRL-NUMPAD+ TO EXPAND]
.text:004062BB ; [00000034 BYTES: COLLAPSED FUNCTION __EH_prolog3. PRESS CTRL-NUMPAD+ TO EXPAND]
.text:004062EF ; [00000037 BYTES: COLLAPSED FUNCTION __EH_prolog3_GS. PRESS CTRL-NUMPAD+ TO EXPAND]
.text:00406326 ; [00000037 BYTES: COLLAPSED FUNCTION __EH_prolog3_catch. PRESS CTRL-NUMPAD+ TO EXPAND]
.text:0040635D
.text:0040635D ; =============== S U B R O U T I N E ===============
.text:0040635D
.text:0040635D ; Attributes: thunk
.text:0040635D
.text:0040635D sub_40635D proc near ; CODE XREF: sub_4042FD+19↑p
.text:0040635D jmp sub_406745
.text:0040635D sub_40635D endp
.text:0040635D
.text:00406362

Перечень поддерживаемых компиляторов можно найти в файле %IDA%/SIG/list. В нем есть старинные Microsoft C и Quick C, Visual C++ с первой по восьмую версию и Visual.Net. А вот Visual C++ 14 из Visual Studio 2017 здесь нет. Однако, взглянув в окно IDA, мы видим, что дизассемблер сумел определить многие (но не все) функции.

Заглянем в окно вывода, находящееся внизу. Там, немного прокрутив вывод, мы обнаружим строчку: Using FLIRT signature: SEH for vc7-14, говорящую о том, что используемая версия IDA все же понимает компиляторы Visual C++ от 7 до 14.

Текстовое отображение результата работы IDA

Давай разбираться в получившемся листинге. Первое и в данном случае единственное, что нам надо найти, — это функция main. В начале стартового кода после выполнения процедуры sub_406992 программа совершает прыжок на метку loc_406109:

.text:0040628B start proc near
.text:0040628B call sub_406992
.text:00406290 jmp loc_406109

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

В данном случае, как мы видим по комментарию, IDA отправила нас в начало стартового куска кода. Немного прокрутим листинг вниз, обращая внимание на плавные переходы по меткам.

В итоге доходим до вызова функции: call sub_4010D0. Похоже, это и есть функция main, поскольку здесь дизассемблер смог распознать строковую переменную и дал ей осмысленное имя aHelloSailor, а в комментарии, расположенном справа, для наглядности привел оригинальное содержимое Hello, Sailor!n. Смещение этой строки компилятор закинул на вершину стека, а затем ниже через строчку, по всей видимости, происходит вызов функции вывода на экран:

.text:004010D3 push offset aHelloSailor ; «Hello, Sailor!\n»
.text:004010D8 push offset unk_42DE30
.text:004010DD call sub_401170

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

Если поместить курсор в границы имени aHelloSailor и нажать Enter, IDA автоматически перейдет к строке, в которой выполняется определение переменной:

.rdata:0041E1A0 aHelloSailor db ‘Hello, Sailor!’,0Ah,0 ; DATA XREF: sub_4010D0+3↑o

Выражение DATA XREF: sub_4010D0+3↑o называется перекрестной ссылкой и свидетельствует о том, что в третьей строке процедуры sub_4010D0 произошло обращение к текущему адресу по его смещению (o от слова offset), а стрелка, направленная вверх, указывает на относительное расположение источника перекрестной ссылки.

Если навести курсор на выражение sub_4010D0+3↑o и нажать Enter, то IDA Pro перейдет к следующей строке:

.text:004010D3 push offset aHelloSailor ; «Hello, Sailor!\n»

Нажатие Esc отменяет предыдущее перемещение, возвращая курсор в исходную позицию.

К слову, дизассемблер W32Dasm даже не смог распознать строковую переменную.

 

Что не так с IDA?

Положа руку на сердце — я был слегка разочарован, ибо ожидал, что IDA распознает больше библиотечных процедур. Поэтому я решил натравить «Иду» на такую же программу, но сгенерированную более ранней версией компилятора. Подопытным кроликом выступил Visual C++ 8.0 (VS 2005).

Сравним результаты работы компиляторов. Тот же исходник, компиляция из командной строки (папка first05). Загрузим итоговый экзешник в «Иду». Листинг приводится в сокращенном виде для экономии пространства.

Мало того что стартовый код меньше по объему, так еще было автоматически определено большее количество библиотечных функций, среди которых: GetVersionExA, GetProcessHeap, HeapFree и ряд других. Среди них достаточно просто найти вызов main и перейти на саму функцию.

Тем не менее VC++ 8.0 — ушедшая эпоха, и в пример я ее привел только для ознакомления.

На этом анализ приложения first.cpp можно считать завершенным. Для полноты картины остается переименовать функцию sub_4010D0 в main. Для этого подводи курсор к строке .text:004010D0 (началу функции) и жми N. В появившемся диалоге можешь ввести main. Результат должен выглядеть так:

.text:004010D0 ; int __cdecl main(int argc, const char **argv, const char **envp)
.text:004010D0 main proc near ; CODE XREF: start-8D↓p
.text:004010D0
.text:004010D0 argc = dword ptr 8
.text:004010D0 argv = dword ptr 0Ch
.text:004010D0 envp = dword ptr 10h
.text:004010D0
.text:004010D0 push ebp
.text:004010D1 mov ebp, esp
.text:004010D3 push offset aHelloSailor ; «Hello, Sailor!\n»
.text:004010D8 push offset unk_42DE30
.text:004010DD call sub_401170
.text:004010E2 add esp, 8
.text:004010E5 xor eax, eax
.text:004010E7 pop ebp
.text:004010E8 retn
.text:004010E8 main endp

Обрати внимание: IDA в комментарии подставила прототип функции, а ниже параметры по умолчанию.

Источник: xakep.ru

Ответить

Ваш адрес email не будет опубликован. Обязательные поля помечены *