Mastodon

Wednesday, March 23, 2011

Анализ покрытия кода при поиске уязвимостей

Репост из блога Esage Lab

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

Для программ, которые написаны на компилируемых в машинный код языках, существуют следующие классы инструментов, позволяющих анализировать покрытие кода:
  1. Профилировщики, использующие промежуточное представление кода (пример: LLVM, Vallgrind). Основным их недостатком является то, что такие профилировщики пригодны только для анализа программ с доступными исходными текстами.
  2. Инструменты, использующие возможности для отладки, предоставляемые операционной системой (например: системный вызов ptrace() в *nix или Debug API в Windows). В качестве примера программ подобного рода можно привести ProcessStalker, фреймворк PaiMei а так же множество сомнительного вида скриптов, как для отладчиков, так и выполненные в виде отдельных инструментов (blocks.py, Tracer.py и другие). Данные инструменты обладают огромным количеством недостатков, что делает их совершенно непригодными для сколь-либо серьёзного использования. Среди основных: низкая производительность, сложности при анализе исполнения всех модулей в контексте процесса а так же, во многих случаях, потребность предварительного анализа интересующих модулей с помощью внешней программы (IDA Pro).
  3. Инструменты,  на прямую использующие возможности для отладки, заложенные в самой аппаратной архитектуре (пример: CFSearch, IKWYD). Подход как таковой не имеет явно выраженных недостатков, и целесообразность применения на практике той или иной программы определяется исключительно недостатками их реализации.
  4. Полные эмуляторы, представляющие собой модифицированные виртуальные машины исполняющие весь код операционной системы, в среде которой запущенно тестируемое приложение (пример: TEMU). Основным недостатком данного класса программ является крайне низкая производительность, ведь эмуляции подлежит вся операционная система, а не только тестируемое приложение.
  5. JiT-рекомпиляторы, осуществляющие анализ хода исполнения программы путём модификации её инструкций в процессе исполнения (пример: PIN Toolkit, DynamoRIO). По моему мнению, данные инструменты свободны от большей части перечисленных выше минусов, и поэтому лучше всего подходят для анализа покрытия кода при фаззинге. Из недостатков можно отметить разве что отсутствие готовых инструментов подобного рода, пригодных для анализа покрытия кода компонентов режима ядра.
В данной заметке я расскажу об относительно малоизвестном но весьма мощном инструментальном средстве под названием PIN Toolkit. PIN является наиболее функциональным и стабильным представителем программ, использующих динамическую рекомпиляцию кода для анализа его исполнения.

Основы работы с PIN


PIN представляет собой спонсируемую компанией Intel бесплатную разработку, которая поставляется с частично открытыми исходными текстами в виде SDK для Linux, Windows и Mac OS X. PIN работает на архитектурах IA-32, IA-64 (Itanium) и Intel64 (x86_64). Структурно его можно разделить на основное приложение (pin.exe), внедряемое в контекст анализируемого процесса ядро (pinvm.dll) и разрабатываемый пользователем инструментальный модуль (он так же внедряется в контекст анализируемого процесса). Инструментальный модуль представляет собой DLL-библиотеку, которая использует API, предоставляемый ядром PIN. Суть работы с API сводится к регистрации обработчиков, которые будут вызываться ядром в ходе исполнения кода исследуемого приложения и осуществлять либо логирование связанной с ходом исполнения информации, либо менять логику исполнения кода приложения любым образом, который разработчик инструментального модуля сочтет нужным.

Работать с анализируемым кодом назначаемые обработчики могут на следующих уровнях функциональности и гранулярности:
  • Исполняемые модули. Обработчик, который вызывается при загрузке какого-либо исполняемого модуля в контекст исследуемого процесса регистрируется при помощи функции IMG_AddInstrumentFunction(). Для работы с загружаемым модулем из обработчика используются другие IMG-функции, а  для работы с отдельными секциями - функции SEC-семейства.
  • Функции. Обработчик, который вызывается для каждой функции исследуемой программы регистрируется при помощи функции RTN_AddInstrumentCall(). Для работы с инструкциями исследуемой функции внутри этого обработчика следует использовать RTN_InsHead()/RTN_InsTail() а так же семейство INS-функций для исследования самих инструкций.
  • Базовые блоки (basic blocks). Под базовым блоком подразумевается линейный участок кода находящийся между двумя инструкциями, которые являются или точкой входа вектора исполнения или непосредственно меняют значение EIP (CALL, JMP/Jxx, RET, и так далее). Обработчик, который вызывается для каждого базового блока исследуемой программы, регистрируется при помощи функции TRACE_AddInstrumentFunction(). Назначать обработчики для отдельных базовых блоков можно с помощью BBL_InsertCall(). Для работы с отдельными инструкциями, составляющими базовый блок, в теле обработчика можно использовать функции BBL_InsHead()/BBL_InsNext().
  • Собственно, сами инструкции. Обработчик, который вызывается для каждой инструкции исследуемой программы регистрируется при помощи функции INS_AddInstrumentFunction(). Назначать обработчики для отдельных инструкций можно с помощью INS_InsertCall(). Для анализа инструкций PIN использует библиотеку под названием XED. Его API так же доступно для использования в инструментальных модулях.
Так как ядро PIN перехватывает исполнение целевого процесса начиная с точки входа системного загрузчика - в инструментальных модулях гарантированно безопасное использование только функций самого PIN-а и стандартной С/С++ библиотеки. Попытка использования, к примеру, Win32 API чаще всего приводит к неработоспособности инструментального модуля.

Множество примеров разработки инструментальных модулей с подробными комментариями доступны в официальном руководстве.

Как уже было сказано, PIN работает по принципу динамической рекомпиляции кода исследуемой программы: в процессе его исполнения на место нужной инструкции записывается переход (JMP) в pinvm.dll, которая вызывает зарегистрированные инструментальным модулем обработчики и модифицирует инструкцию (или базовый блок) следующую за текущей. Таким образом достигается контроль над исполнением всего кода. Высокая скорость работы исследуемого кода при таких, казалось бы, сложных манипуляциях с ним обеспечивается за счёт отсутствия накладных расходов на взаимодействие между процессами и переключение потока между режимом пользователя и ядра, которые "съедают" основную часть процессорного времени при использовании более традиционных способов трассировки. Что интересно, под PIN нормально работают и сложные многопоточные приложения и даже самомодифицирующийся код, вроде следующего:
 1 #include <Windows.h>
 2 
 3  #pragma comment(linker,"/ENTRY:f_main")
 4 #pragma comment(linker,"/SECTION:.text,EWR")
 5 
 6 VOID Func_1(VOID)
 7 {
 8     MessageBoxA(0, __FUNCTION__"() called", "Message", 0);
 9 }
10 
11 VOID Func_2(VOID)
12 {
13     MessageBoxA(0, __FUNCTION__"() called", "Message", 0);
14 }
15 
16 __declspec(naked) VOID Func(VOID)
17 {
18     __asm
19     {
20         push    0x90909090
21         ret
22     }
23 }
24 
25 DWORD WINAPI f_main(VOID)
26 {
27     DWORD Address = 0;
28     char szMessage[0x50];
29 
30     __asm
31     {
32         call    _get_addr
33 
34 _get_addr:
35 
36         pop     eax
37         mov     Address, eax
38     }
39 
40     wsprintf(szMessage, "Address is 0x%.8x", Address);
41     MessageBoxA(0, szMessage, "Message", 0);
42 
43     *(PDWORD)((PUCHAR)&Func + 1) = (DWORD)&Func_1;
44 
45     Func();
46 
47     *(PDWORD)((PUCHAR)&Func + 1) = (DWORD)&Func_2;
48 
49     Func();
50 
51     return 0;
52 }

Анализ покрытия кода с PIN


Для анализа покрытия кода с использованием PIN нами был разработан относительно несложный инструментальный модуль а так же некоторые другие утилиты, которые были объединены в набор под названием Code Coverage Analysis Tools. В него входят:
  • Coverager.dll - Собственно, инструментальный модуль для PIN.
  • coverage_test.exe - Тестовое приложение, которое позволяет строить карты покрытия кода для Internet Explorer-а используя PIN и Coverager.dll.
  • coverage_parse.py - Скрипт для работы с логами, которые генерирует Coverager.dll.
  • symlib.pyd - Используемая в coverage_parse.py небольшая Python библиотека для работы с PDB символами.
Используются эти утилиты следующим образом:
  1. Для начала необходимо загрузить актуальную версию PIN и распаковать архив в произвольную директорию.
  2. Скопировать Coverager.dll в директорию с файлами PIN.
  3. Отредактировать сценарий execute_pin.bat, что бы переменная среды PINPATH содержала актуальный путь к директории PIN.
Для построения карты покрытия кода какого-нибудь приложения следует запустить execute_pib.bat из консоли, передав ему командную строку для запуска целевого приложения в качестве аргумента. Пример:
> execute_pin.bat calc.exe
После завершения работы целевого приложения в текущей директории будут созданы следующие текстовые файлы:
  • CoverageData.log - Общая информация об исследуемом процессе (размер карты покрытия, количество потоков и исполняемых модулей, и так далее).
  • CoverageData.log.modules - Список полных путей к файлам исполняемых модулей, которые были загружены в контекст исследуемого процесса.
  • CoverageData.log.routines - Информация о функциях, которые получали управление в ходе исполнения исследуемого процесса.
  • CoverageData.log.blocks - Информация об отдельных базовых блоках инструкций, которые получали управление в ходе исполнения исследуемого процесса.
Файлы CoverageData.log.routines и CoverageData.log.blocks содержат информацию о функциях/блоках, принадлежащих всем исполняемым модулям, включая файл самого процесса а так же стандартные системные (ntdll, kernel32, user32, и так далее) и любые другие динамически загружаемые библиотеки.

Для представления информации из перечисленных выше логов в удобном для работы виде служит скрипт coverage_parse.py, который принимает следующие параметры командной строки:
> python coverage_parse.py <log_file_path> <--dump-blocks|--dump-routines> [other_options]
В качестве <log_file_path> указывается путь к файлу CoverageData.log, а один из обязательных ключей --dump-blocks или --dump-routines указывает на то, с информацией какого уровня гранулярности следует работать (базовые блоки или функции).

В качестве опциональных параметров указываются следующие ключи:
  • --outfile <file_path> - Сохранять информацию в указанный файл, вместо вывода в консоль.
  • --order-by-names - Сортировать вывод по имени (адресу) символа, который соответствует функции или базовому блоку (данный режим используется по умолчанию).
  • --order-by-calls - Сортировать вывод по количеству вызовов функции или базового блока.
  • --modules <modules> - Получать информацию только для указанных модулей (по именам).  Для передачи списка из нескольких модулей следует разделять из имена запятой (например: --modules "iexplore.exe, ieframe.dll"). Если параметр --modules не указан - в вывод скрипта будет включена информация обо всех исполняемых модулях исследуемого процесса.
  • --skip-symbols - По умолчанию скрипт автоматически загружает PDB-символы для нужных исполняемых модулей и использует их для преобразования адресов всех функций и базовых блоков к виду module!symbol+offset. Параметр --no-symbols позволяет отключить загрузку PDB символов, в таком случае, все адреса в выводе скрипта будут представлены в виде module+ofsset.
Пример использования скрипта для получения информации о наиболее часто вызываемых функциях модуля calc.exe:
> python coverage_parse.py CoverageData.log --dump-routines --modules "calc" --order-by-calls

SYMLIB: DLL_PROCESS_ATTACH
SYMLIB: Symbols path is ".\Symbols;SRV*.\Symbols*http://msdl.microsoft.com/download/symbols"
SYMLIB: Module loaded from "C:\Windows\system32\calc.exe"
SYMLIB: 2774 symbols loaded for "C:\Windows\system32\calc.exe"
Filtering by module name "calc"
[+] Ordering list by number of calls
[+] Parsing routines list, please wait...

#
#   Calls count -- Function Name
#
          11560 -- calc.exe!UIntAdd
           7863 -- calc.exe!ATL::CAtlArray<ATL::CAtlRegExp::INSTRUCTION,ATL::CElementTraits<ATL::CAtlRegExp::INSTRUCTION> >::operator[]
           6559 -- calc.exe!_destroynum
           5780 -- calc.exe!ULongLongToUInt
           5780 -- calc.exe!_createnum
           5102 -- calc.exe!ATL::CAtlRegExp::MatchToken
           3788 -- calc.exe!ATL::CAtlArray<ATL::CAtlRegExp::INSTRUCTION,ATL::CElementTraits<ATL::CAtlRegExp::INSTRUCTION> >::SetCount
           3416 -- calc.exe!ATL::CAtlArray<ATL::CAtlRegExp::INSTRUCTION,ATL::CElementTraits<ATL::CAtlRegExp::INSTRUCTION> >::CallConstructors
           3404 -- calc.exe!ATL::CAtlRegExp::AddInstruction
           3196 -- calc.exe!addnum
           2902 -- calc.exe!lessnum
           2636 -- calc.exe!memcpy
           2470 -- calc.exe!_addnum
           2453 -- calc.exe!__security_check_cookie
           1743 -- calc.exe!CArgument::getPosition
           1084 -- calc.exe!CCalculatorMode::HandleBackgroundForChildWindows
           1084 -- calc.exe!CProgrammerMode::ProgDlgProc
            918 -- calc.exe!operator delete[]
            895 -- calc.exe!CEditBoxInput::HandleDlgProcMessage
            704 -- calc.exe!ATL::CAtlArray<FORMULA_PART *,ATL::CElementTraits >::GetAt

            ...
            пропущено ещё несколько сотен функций
            ...

              1 -- calc.exe!Gdiplus::Graphics::Graphics
              1 -- calc.exe!GdipGetImageGraphicsContext
              1 -- calc.exe!Gdiplus::Graphics::SetInterpolationMode
              1 -- calc.exe!GdipSetInterpolationMode
              1 -- calc.exe!Gdiplus::Graphics::SetSmoothingMode
              1 -- calc.exe!GdipSetSmoothingMode
              1 -- calc.exe!CProgrammerMode::ShowBitStrip
              1 -- calc.exe!CProgrammerMode::NormalizeCurrentBit
              1 -- calc.exe!CIO_vUpdateDecimalSymbol

[+] Processed modules list:

#
# Routines count -- Module Name
#
            422 -- gdiplus.dll
             58 -- winmm.dll
            107 -- apphelp.dll
            221 -- gdi32.dll
            370 -- kernel32.dll
            196 -- msvcrt.dll
             57 -- oleaut32.dll
             30 -- dwmapi.dll
            692 -- ntdll.dll
             60 -- shlwapi.dll
              5 -- sechost.dll
             92 -- imm32.dll
            705 -- calc.exe
             28 -- cryptbase.dll
             37 -- advapi32.dll
            577 -- ole32.dll
             33 -- lpk.dll
            825 -- msctf.dll
             10 -- version.dll
            309 -- usp10.dll
            278 -- windowscodecs.dll
            483 -- uxtheme.dll
            138 -- oleacc.dll
             95 -- clbcatq.dll
            251 -- comctl32.dll
            258 -- kernelbase.dll
            117 -- shell32.dll
             25 -- rpcrt4.dll
            465 -- user32.dll

[+] DONE

SYMLIB: DLL_PROCESS_DETACH

PIN так же корректно анализирует исполняемый код, который не принадлежит какому-либо загруженному модулю, и находится в произвольной executable-странице памяти. Для того, что бы получить информацию о таком коде из логов, для coverage_parser.py необходимо указать "?" в качестве имени модуля:
> python coverage_parse.py CoverageData.log --dump-routines --modules "?"

SYMLIB: DLL_PROCESS_ATTACH
SYMLIB: Symbols path is ".\Symbols;SRV*.\Symbols*http://msdl.microsoft.com/download/symbols"
Filtering by module name "?"
[+] Parsing routines list, please wait...

#
#   Calls count -- Function Name
#
              5 -- ?0x541be250
              5 -- ?0x55009060

              ...

Как видно из примера - в выводе скрипта такой код будет представлен в форме ?<address>.

Тестирование


В последнее время наблюдается следующая тенденция: многие исследователи, разрабатывающие различные системы динамического анализа кода, в качестве демонстрации возможностей этих систем ссылаются на тесты, для которых в качестве объекта используются маленькие приложения вроде calc и notepad или всевозможные вариации hackme.exe/vulnerable.exe из десятка строчек. Чаще всего подобные явления говорят или о невозможности решать с помощью демонстрируемого инструмента задачи из реальной жизни, или о том, что разработчик данного инструмента слишком увлечен ним самим, и воспринимает его в отрыве от практических задач.

Так как подобные тенденции мы не разделяем - в качестве демонстрации возможностей PIN и инструментального модуля Coverager.dll будут приведены результаты их тестирования на построении карты покрытия кода процесса браузера Internet Explorer 8, работающего в среде операционной системы Windows 7 SP1.

Для проведения тестирования была написана программа coverage_test.exe, ключевая часть исходных текстов которой выглядит следующим образом:
  1 int _tmain(int argc, _TCHAR* argv[])
  2 {
  3     char *lpszCmdLine = GetCommandLine();
  4     char *lpszCmd = strstr(lpszCmdLine, "@ ");
  5     if (lpszCmd == NULL)
  6     {
  7         /*
  8             Командная строка для запуска внешнего приложения не указана,
  9             coverage_test был запущен из командной строки.
 10         */
 11         char szSelfPath[MAX_PATH], szExecPath[MAX_PATH];
 12         GetModuleFileName(GetModuleHandle(NULL), szSelfPath, MAX_PATH);
 13         DWORD dwTestIterations = 1;
 14 
 15         SetConsoleCtrlHandler(CtrlHandler, TRUE);
 16 
 17         /*
 18             Регистрируем исполняемый файл текущего процесса как отладчик для IEXPLORE.EXE,
 19             при попытке его запуска система запустит приложение а путь к исполняемому файлу
 20             IEXPLORE.EXE будет передан ему в качестве аргумента командной строки.
 21         */
 22         sprintf_s(szExecPath, "\"%s\" @", szSelfPath);
 23         SetImageExecutionDebuggerOption("IEXPLORE.EXE", szExecPath);
 24 
 25         for (int i = 1; i < argc; i++)
 26         {
 27             if (!strcmp(argv[i], "--iterations") && argc > i + 1)
 28             {
 29                 // количество итераций для теста
 30                  dwTestIterations = atoi(argv[i + 1]);
 31                 printf("Iterations count: %d\n", dwTestIterations);
 32             }
 33             else if (!strcmp(argv[i], "--instrumentation-path") && argc > i + 1)
 34             {
 35                 /*
 36                     Путь к файлу инструментальной программы (в нашем случае это pin.exe),
 37                     сохраняем его в параметре реестра и использует при запуске IEXPLORE.EXE
 38                 */
 39                 SetOptionalApp(argv[i + 1]);
 40                 printf("Instrumentation tool path: \"%s\"\n", argv[i + 1]);
 41             }
 42         }
 43 
 44         // инициализация COM
 45         HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);
 46         if (FAILED(hr))
 47         {
 48             printf("CoInitializeEx() ERROR 0x%.8x\n", hr);
 49             goto end;
 50         }
 51 
 52         DWORD dwTime = GetTickCount();
 53 
 54         // редактирование настроек IE: активируем single process mode 
 55         DisableIeMultiprocessMode();
 56 
 57         /*
 58             Вызов функции, которая открывает в IE тестовый адрес указанное число раз.
 59             Для взаимодействия с процессом браузера используется интерфейс IWebBrowser2.
 60         */
 61         IeOpenUrl(TEST_URL, dwTestIterations);
 62 
 63         // замер времени исполнения
 64         dwTime = GetTickCount() - dwTime;
 65         printf("Execution time: %d ms\n", dwTime);
 66 
 67         EnableIeMultiprocessMode();
 68         SetOptionalApp(NULL);
 69     }
 70     else
 71     {
 72         /*
 73             Текущий процесс был запущен системой в результате попытки
 74             запуска IEXPLORE.EXE.
 75         */
 76         DWORD dwExitCode = 0;
 77         char szOptional[MAX_PATH], szCmdLine[MAX_PATH];
 78         strcpy_s(szCmdLine, MAX_PATH, lpszCmd);
 79 
 80         lpszCmd += 2;
 81 
 82         // удаляем опцию "Debugger" для IEXPLORE.EXE
 83         SetImageExecutionDebuggerOption("IEXPLORE.EXE", NULL);
 84 
 85         // получаем путь к инструментальной программе
 86         if (GetOptionalApp(szOptional, MAX_PATH))
 87         {
 88             sprintf_s(szCmdLine, "\"%s\" %s", szOptional, lpszCmd);
 89         }
 90 
 91         // запуск IE c использованием PIN
 92         printf("CMDLINE: %s\n", szCmdLine);
 93         ExecCmd(&dwExitCode, szCmdLine);
 94     }
 95 
 96 end:
 97     printf("Press any key to quit...\n");
 98     _getch();
 99 
100     return 0;
101 }

Программа coverage_test.exe входит в состав Code Coverage Analysis Tools, её запуск следует производить с помощью сценария coverage_test_with_pin.bat, который инициирует запуск процесса браузера Internet Explorer под контролем PIN и проводит с ним то количество тестовых итераций, которое было указанно в качестве аргумента командной строки к coverage_test_with_pin.bat. Каждая тестовая итерация представляет собой открытие адреса google.com с помощью IWebBrowser2::Navigate() и ожидание завершения полной загрузки страницы.

С данной программой было проведено пять серий тестов с количеством итераций 1, 5, 10, 20 и 30. Ниже представлена гистограмма зависимости времени исполнения процесса браузера (в миллисекундах) от количества итераций:



Как видно, исполнение процесса под контролем PIN обуславливает примерно десятикратное снижение производительности в тестах. Однако, спад производительности становится менее значительным при увеличении числа итераций. Причина подобного, вероятно, кроется в самом принципе динамической рекомпиляции: высокие накладные расходы требуются только тогда, когда какой-либо фрагмент кода исполняется в первый раз, а при его повторное исполнение требует гораздо меньше процессорного времени.

Схожие разработки


PIN является не единственным фреймворком подобного подобного плана, существует так же разработка под названием DynamoRIO, которая похожа на PIN во всех отношениях. В качестве главных преимуществ DynamoRIO можно выделить более высокую производительность а так же полностью открытый под BSD лицензией исходный код. Однако, актуальная версия DynamoRIO имеет ряд проблем со стабильностью - мне так и не удалось добиться нормальной её работы на Windows 7, из-за чего выбор и пал на PIN.

C данными по сравнительной производительности обеих фреймворков можно ознакомится в заметке на GoVirtual: Performance Comparison of DynamoRIO and Pin.

Выводы


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

Code Coverage Analysis Tools доступны для загрузки на странице проекта в GitHub.