Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

3204

.pdf
Скачиваний:
9
Добавлен:
15.11.2022
Размер:
3.44 Mб
Скачать

выявление всех функций анализируемой программы, реализующих интересующие нас алгоритмы.

Второй этап заключается в анализе потоков данных внутри программы и выяснении путей преобразования этих данных. На втором этапе анализа программы эффективно работают два метода: метод аппаратной точки останова и метод Step-Trace второго этапа.

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

– мы отслеживаем не обращения к буферу, а только лишь изменения его содержимого.

Метод аппаратной точки останова Данный метод целесообразно использовать в качестве

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

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

41

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

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

Метод Step-Trace второго этапа

Метод Step-Trace может применяться и на втором этапе анализа программы. В этом случае искомая функция х должна удовлетворять всего одному условию – функция х изменяет интересующие нас данные.

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

В остальном применение метода Step-Trace на втором этапе ничем не отличается от его применения на первом этапе.

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

42

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

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

1.4.3. Пример применения динамического метода

Для иллюстрации динамического метода анализа программных реализаций решим практическую задачу – попробуем определить, каким образом стандартная утилита ipconfig, входящая в состав Windows, определяет, какие сетевые интерфейсы компьютера активны в данный момент. Мы будем анализировать утилиту ipconfig из дистрибутива Windows ХР SP2. В качестве отладчика будем использовать встроенный отладчик Microsoft Visual Studio 2005.

Прежде всего для упрощения дальнейшей работы загрузим с сайта Microsoft файлы отладочной информации для Windows ХР SP2, распакуем архив и скопируем файл exe\ipconfig.pdb в директорию windows\system32 рядом с анализируемым файлом. При наличии в директории с анализируемой программой файла с отладочной информацией отладчик выводит в свои окна более осмысленную и удобочитаемую информацию.

Запустим Visual Studio, нажмем [Ctrl-Shift-O] (Open project/solution), и открывшемся диалоговом окне (рис. 1.7) выберем файл ipconfig.exe и нажмем [Open].

43

Рис. 1.7. Открытие ЕХЕ-файла в качестве проекта Microsoft Visual Studio

Нажмем [F10] (Step) или [F11] (Trace), откроем окно состояния процессора (Registers, [Alt-5]) и окно вложенных вызовов функций (Call Stack, [Alt-7]), расположим окна удобным образом. Окно отладчика примет вид, примерно соответствующий приведенному на рис. 1.8.

Начнем проходить программу в режиме Step (клавиша [F10]), визуально отслеживая состояние дел в окне отлаживаемой программы.

При прохождении адреса 010066А5 мы обнаруживаем команду

010066А5 call _main (1005382b.)

При ее прохождении в режиме Step программа ipconfig немедленно выдает в консоль всю информацию и завершается. Это вполне естественное поведение, если вспомнить смысл функции main в консольных программах Windows.

Перезагрузим программу ([Shift-F5], [F10]), дойдем в режиме Step до вызова функции main и войдем внутрь ее командой Trace ([Fll]). Продолжаем трассировку в режиме Step.

44

Рис. 1.8. Примерный вид основного окна отладчика Microsoft Visual Studio

После примерно 100 нажатий клавиши [F10] мы обнаруживаем, что вся информация о всех сетевых интерфейсах выводится в консоль единственным вызовом

(010056ВЕ call _WriteOutput@8 (10043A0h)

Перезагружаем программу, проходим в режиме Step до вызова main, даем команду Trace, доходим в режиме Step до вызова WriteOutput, даем команду Trace и идем дальше в режиме Step. Обнаруживаем вызов

010043EF call dword ptr [imp WriteConsoleW@20 (1001098h)]

выводящий информацию в консоль.

WriteConsole – документированная системная функция библиотеки kernel32.dll. Второй параметр функции представляет собой указатель на буфер с текстом, который надо вывести в консоль. В нашем случае

010043Е2 mov еах,esi

45

010043Е4 push О

010043Е6 lea есх, [ebp + ОСЬ]

010043Е9 push есх

010043ЕА push еах

010043ЕВ push ebx

010043ЕС push dword ptr [ebp + 8] 010043EF call dword ptr

[_imp_WriteConsoleW@20 (1001098h)]

адрес буфера берется из регистра ebx командой

010043ЕВ push ebx

Этот адрес равен 000A4BD8 (забегая вперед, заметим, что точное значение адреса может меняться в зависимости от случайных факторов), т.е. буфер расположен не в стеке (esp = 0007С038, очень далеко от 000A4BD8) и не в области глобальных переменных (она должна идти после секции кода, т.е. после анализируемых команд). Судя по всему, буфер находится в динамически распределяемой памяти (в «куче»).

Попробуем применить метод аппаратной точки останова. Перезагрузим программу, выберем в меню отладчика пункт Debug / New Breakpoint / New Data Breakpoint и

заполним появившееся окно, как показано на рис. 1.9. Поля Byte Count и Language в данном случае (как и в большинстве других случаев) можно оставить заполненными по умолчанию.

Запустим программу на выполнение ([F5]). Программа мгновенно останавливается, в отладчике появляется всплывающее окно.

В окне Call Stack мы видим следующий стек вложенных вызовов функций:

ntdll. dll!_RtlFillMemoryUlong@12() + 0x10 bytes ntdll. dll!_RtlpInsertFreeBlock@12() + 0x2a54c bytes ntdll.dll!_RtlpExtendHeap@8() + Oxad bytes ntdll.dll!_RtlAllocateHeapSlowly@12() + 0xf07a bytes ntdll.dll!_RtlDebugAllocateHeap@12() + Oxaf bytes ntdll.dll !_RtlAllocateRIeapSlowly@12

46

() + 0x2ea6c bytes ntdll.dll!_RtlAllocateHeap@12() + 0xacc4 bytes kernel32.dll!_LocalAlloc@8() + 0x52 bytes iphlpapi.dll! GrabMemoryP4() + 0x10 bytes iphlpapi.dll! GetAdapterOrderMapPO() + 0x4 8 bytes iphlpapi.dll! GetAdapterListP0()

+0x4a bytes iphlpapi.dll! GetAdapterlnfo@0()

+0x25 bytes iphlpapi.dll! GetAdapterAddressesP12( ) + 0x17 bytes

Рис. 1.9. Окно установки аппаратной точки останова в отладчике MicrosoftVisual Studio

iphlpapi.dll! GetAdapterAddressesEx@l6() + 0x26 bytes iphlpapi. dll! GetAdaptersAddresses@20() + 0x5a bytes ipconfig.exe! GetNetworklnformation@8() + 0x3d bytes ipconfig.exe! main() + 0x144 bytes ipconfig.exe! mainCRTStartup () + 0x125 bytes kernel32.dll! BaseProcessStart@4() + 0x23 bytes

Элементы данного списка имеют вид <модуль>!<функция>@<длина кадра стека>() + <смещение> bytes

Модуль – это имя программного модуля (ЪХЕ-файла, библиотеки и т.п.), внутри которого располагается функция.

Функция – имя функции (подчеркивание в начале добавлено компилятором языка С).

47

Длина кадра стека – длина в байтах кадра стека, содержащего параметры функции (обычно это число равно числу параметров функции, умноженному на четыре).

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

Если отладочная информация для некоторого программного модуля, используемого анализируемой программой, не загружена в отладчик, окно Call Stack гораздо менее информативно. В этом случае желательно найти PDBфайл, соответствующий программному модулю, скопировать его в директорию, в которой лежит программный модуль, и перезапустить отладчик. В нашем случае для того чтобы получить список вложенных вызовов в вышеприведенном формате, необходимо скопировать в директорию windows\system32 файлы iphlpapi. pdb, kemel32.pdb и ntdll.pdb,

загруженные с сайта Microsoft.

Список функций, приведенный в окне Call Stack, имеет следующий смысл. Функция, присутствующая в V-й строке списка, вызывается из функции в (V + 1)-й строке списка и, в свою очередь, вызывает функцию в (V - 1)-й строке списка, т. е. в нашем случае функция main программы ipconfig.exe вызывает функцию GetNetworklnformation той же программы, функция

GetNetworklnformation вызывает функцию GetAdaptersAddresses библиотеки iphlpapi.dll и т.д. Если сделать двойной щелчок мышью по любой строке в окне Call Stack, окно дизассемблера перемещается на соответствующий участок кода.

Просматривая содержимое окна Call Stack, мы видим, что точка останова сработала внутри вызова функции LocalAlloc, выделяющей к куче блок динамически распределяемой памяти. В новых версиях Windows функция LocalAlloc всегда заполняет свежевыделенную память бессмысленной информацией, что затрудняет несанкционированное повторное использование удаленной информации, а также облегчает отладку программ. Открыв в

48

отладчике окно дампа памяти ([Ctrl-Alt-М], [1]), мы видим, что в Windows ХР SP2 свежевыделенная память заполняется повторяющимися парами байт FE ЕЕ. Это не имеет никакого отношения к тем данным, порядок обработки которых мы хотим проанализировать.

Запустим программу выполняться дальше ([F5]). Мы видим несколько ложных срабатываний точки останова, а затем, примерно на шестой-седьмой раз, содержимое окна Call Stack приобретает вид

ntdll.dll! memmove () + 0x35 bytes advapi32.dll!

LocalBaseRegQueryValue@24() + 0x185 bytes advapi32.dll! RegQueryValueExW@24() +

0x8c bytes

iphlpapi.dll! GetAdapterOrderMapPO() + 0x66 bytes

iphlpapi.dll! GetAdapterListP0() + 0x4a bytes

iphlpapi.dll! GetAdapterlnfo@0() + 0x25 bytes

iphlpapi.dll! GetAdapterAddressesP12() + 0x17 bytes

iphlpapi.dll! GetAdapterAddressesExP16() + 0x2 6 bytes

iphlpapi.dll! GetAdaptersAddresses@20() + 0x5a bytes

ipconfig.exe! GetNetworklnformation@8() + 0x66 bytes

ipconfig.exe! main() + 0x144 bytes ipconfig.exe! mainCRTStartup() + 0x125

bytes

kernel32.dll! BaseProcessStartP4() + 0x23 bytes

В окне дампа памяти мы видим, что в интересующий нас буфер записываются какие-то имена устройств, это, в принципе, интересно, но это явно не те данные, которые мы

49

отслеживаем.

После 20 ... 30 срабатываний точки останова мы окончательно убеждаемся, что в нашем случае применяемый метод не работает – область памяти, в которой размещается анализируемый буфер, используется программой слишком интенсивно. Попробуем метод Step-Trace.

Перезагружаем программу и проходим ее в режиме Step, глядя в окно дампа памяти, в котором отображается буфер по адресу 000A4BF0. Сначала в этом окне видны только знаки вопроса:

0x000A4BF0 ?? ?? ?? ?? ?? ?? ?? ?? ?? ??

???? ?? ?? ??... Ox000A4BFF ?? ?? ?? ?? ?? ??

???? ?? ?? ?? ?? ?? ?? ??... Ох000А4С0Е ?? ??

???? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ??...

Это означает, что данный участок памяти относится к странице, которая не выделена программе, а находится либо в свободном, либо в зарезервированном состоянии.

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

010054С1 call _GetNetworkInformation@8 (1003E42h)

знаки вопроса в окне дампа памяти сменяются байтами

0x000A4BF0 ее fe ее fe ее fe ее fe ее fe

ееfe ее fe ее оюоюоюоюоюоюоюо 0x000A4BFF fe

ееfe ее fe ее fe ее fe ее fe ее fe ее fe

юоюоюоюоюоюоюою 0х000А4С0Е ее fe ее fe ее fe

ееfe ее fe ее fe ее fe ее оюокююоюоюоюоюо

т. е. внутри вызова функции GetNetworklnformation происходит распределение памяти в «куче». Идем дальше. Дойдя до команды

0100552С jne _main+156h (10054D8h)

мы понимаем, что попали в цикл. Ставим курсор на

50

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]