Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Fuzzing исследование уязвимостей методом грубой силы.pdf
Скачиваний:
1127
Добавлен:
13.03.2016
Размер:
5.96 Mб
Скачать

340

Глава 20. Фаззинг оперативной памяти: автоматизация

Цикл событий отладки базируется в основном на вызове команды WaitForDebugEvent()1, в котором первым аргументом является указатель структуры DEBUG_EVENT, а вторым – количество миллисекунд, отве$ денных на ожидание события отладки, которое должно произойти в от$ лаживаемой программе. Если событие отладки произойдет, в структу$ ре DEBUG_EVENT появится тип события отладки в атрибуте dwDebug EventCode. Мы изучаем эту переменную, для того чтобы определить, было ли событие отладки запущено вследствие создания или заверше$ ния процесса, создания или завершения потока, загрузки или разгруз$ ки DLL или исключительной ситуации отладки. В случае если произо$ шла исключительная ситуация отладки, мы можем точно определить, в чем заключается ее причина, путем изучения u.Exception.Exception Record.

Атрибут структуры ExceptionCode DEBUG_EVENT. На MSDN2 перечис$ лен целый ряд кодов возможных исключительных ситуаций, но в на$ шем случае представляют интерес в первую очередь следующие:

EXCEPTION_ACCESS_VIOLATION. Нарушение доступа, произо$ шедшее вследствие попытки чтения или записи некорректного ад$ реса памяти.

EXCEPTION_BREAKPOINT. Исключительная ситуация была за$ пущена вследствие обнаружения точки прерывания.

EXCEPTION_SINGLE_STEP. Была запущена ловушка одного ша$ га и выполнена одна команда.

EXCEPTION_STACK_OVERFLOW. Неисправный поток исчерпал размер своего стека. Обычно это свидетельствует о наличии беско$ нечной рекурсии; эта ситуация обычно сводится только к «отказу от обслуживания».

Мы можем использовать различные типы логики – какие только захотим – для различных событий и исключительных ситуаций от$ ладки. После обработки полученного события неисправный поток мо$ жет и дальше вызывать ContinueDebugEvent().

Собрать все воедино

К этому моменту мы уже успели рассмотреть основы схемы располо$ жения ячеек памяти в Windows, составить список обязательных усло$ вий, выбрать язык разработки, изучить основные элементы модуля ctypes и покопаться в фундаментальных характеристиках программ$ ного интерфейса отладчика Windows. Остается несколько чрезвычай$ но важных вопросов:

1http://msdn.microsoft.com/library/default.asp?url=/library/en+us/debug/

base/waitfordebugevent.asp

2http://msdn.microsoft.com/library/default.asp?url=/library/en+us/debug/ base/exception_record_str.asp

Собрать все воедино

341

Каким образом мы реализуем необходимость перехвата объектного процесса в определенных точках?

Каким образом мы будем обрабатывать и восстанавливать момен$ тальные снимки?

Каким образом мы будем размещать и видоизменять область памя$ ти объекта?

Каким образом мы будем выбирать точки для перехвата?

Каким образом мы реализуем необходимость перехвата объектного процесса в определенных точках?

Как уже отмечено ранее в этой главе, перехват процесса может быть реализован в рамках нашего подхода путем использования точек пре$ рывания отладчика. Существует два типа поддерживаемых точек пре$ рывания на нашей платформе: аппаратные и программные. Процессо$ ры 80x86 поддерживают четыре аппаратных точки прерывания. Каж$ дая из них может быть установлена для запуска во время чтения, запи$ си или выполнения любого из одно$, двух$, трех$ или четырехбайтных диапазонов. Для установки аппаратных точек прерывания нам необ$ ходимо изменить контекст объектного процесса, подправив кое$что в регистрах отладки от DR0 до DR3 и в DR7. В первых четырех регист$ рах содержится адрес аппаратной точки прерывания. В регистре DR7 находятся флаги, указывающие на то, какие точки прерывания явля$ ются активными, в каком диапазоне они находятся, а также какой тип доступа (чтение, запись или выполнение) они используют. Аппа$ ратные точки прерывания не влияют на режим работы и не могут из$ менить ваш код. Программные точки прерывания, напротив, должны изменять объектный процесс; они реализуются с однобайтной коман$ дой INT3, представляемой в шестнадцатеричном коде как 0xCC.

При выполнении нашего задания мы можем воспользоваться либо ап$ паратными, либо программными точками прерывания. На тот случай, если в какой$то момент понадобится больше четырех точек перехвата, мы решили использовать программные точки прерывания. Чтобы по$ знакомиться поближе с этим процессом, в вымышленной программе пройдемся по всем этапам установки программной точки прерывания в адресе 0xDEADBEEF. Сначала наш отладчик должен прочитать и со$ хранить исходный байт, хранящийся в объектном адресе, с помощью команды программного интерфейса ReadProcessMemory, о чем уже гово$ рилось ранее и что отражено на рис. 20.3.

Обратите внимание на то, что первая команда по заданному адресу во$ обще$то состоит из двух байтов. Следующим шагом является запись команды INT3 по заданному адресу с помощью программного интер$ фейса WriteProcessMemory, также упоминавшегося ранее (рис. 20.4).

Но что случилось с предыдущими командами? Вставленное значение 0xCC было дизассемблировано как однобайтная команда INT3. Второй

342

 

 

 

Глава 20. Фаззинг оперативной памяти: автоматизация

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

debugger

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

8B

 

OxDEADBEEF

8B

FF

 

mov edi, edi

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

OxDEADBEF1

55

 

 

push ebp

 

 

 

 

 

OxDEADBEF2

 

 

 

mov ebp, esp

 

 

 

 

 

8B

EC

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Рис. 20.3. Сохранение исходного байта по адресу точки перехвата

debugger

8B

OxDEADBEEF

CC

 

 

INT3

 

 

 

 

 

 

 

 

 

OxDEADBEFO

FF

55

8B

call

[ebp 75]

 

OxDEADBEF3

 

 

 

in al, dx

 

EC

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Рис. 20.4. Запись команды INT3

байт команды mov edi, edi (0xFF), совмещенный с байтом, ранее пред$ ставлявшим push ebp (0x55), и первым байтом команды mov ebp, esp (0x8B), был дизассемблирован как команда call [ebp 75]. Оставшийся байт 0xEC дизассемблируется как однобайтная команда in al, dx. Те$ перь, когда выполнение достигло адреса 0xDEADBEEF, команда INT3 запустит событие отладки EXCEPTION_DEBUG_EVENT с кодом исключитель$ ной ситуации EXCEPTION_BREAKPOINT, который наш отладчик поймает во время цикла событий отладки (помните Агнес?). Состоя$ ние процесса на этом этапе отображено на рис. 20.5.

Итак, мы успешно вставили и поймали программную точку прерыва$ ния, но это лишь полдела. Обратите внимание на то, что в исходных ко$

Собрать все воедино

343

debugger

8B

OxDEADBEEF

CC

 

 

INT3

 

 

 

 

 

 

 

 

 

OxDEADBEFO

FF

55

8B

call

[ebp 75]

 

OxDEADBEF3

 

 

 

in al, dx

 

EC

 

 

 

EIP

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Рис. 20.5. Перехват EXCEPTION_BREAKPOINT

debugger

8B

OxDEADBEEF

8B

FF

 

mov edi, edi

 

 

 

 

 

 

 

 

OxDEADBEFO

55

 

 

push ebp

 

 

OxDEADBEF3

 

 

 

mov ebp,

esp

 

8B

EC

 

 

EIP

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Рис. 20.6. Выравнивание EIP

мандах по$прежнему нет действий. Кроме того, указателем команд (EIP, благодаря которому центральный процессор знает, откуда извле$ кать, как декодировать и выполнять следующую команду) является 0xDEADBEF0, а не 0xDEADBEEF. Все это происходит из$за того, что вставленная нами в 0xDEADBEEF однобайтная команда INT3 была ус$ пешно выполнена, что привело к тому, что EIP обновился до 0xDEAD$ BEEF+1. Прежде чем продолжать, исправим значение EIP и восстано$ вим исходный байт в 0xDEADBEEF, как показано на рис. 20.6.

Восстановление байта в 0xDEADBEEF – это задание, с которым мы уже знакомы. Однако изменение значения указателя команд, регист$ ра EIP – это совсем другая история. Мы упоминали ранее в этой главе,

344

Глава 20. Фаззинг оперативной памяти: автоматизация

что контекст потока содержит различные данные регистра, свойствен$ ные данному процессору, например указатель команд (EIP), который интересует нас в данный момент. Мы можем извлечь контекст любого потока с помощью вызова программного интерфейса GetThreadCon text()1, передав ему идентификационный номер текущего потока и ука$ затель структуры CONTEXT. Мы также можем изменить содержимое структуры CONTEXT и затем вызвать программный интерфейс Set ThreadContext()2, опять же передав номер текущего потока с целью из$ менения контекста:

context = CONTEXT() context.ContextFlags = CONTEXT_FULL

kernel32.GetThreadContext(h_thread, byref(context))

context.Eip = 1

kernel32.SetThreadContext(h_thread, byref(context))

На данный момент исходный контекст выполнения восстановлен, и мы готовы продолжить процесс.

Каким образом мы будем обрабатывать и восстанавливать моментальные снимки?

Для того чтобы ответить на этот вопрос, нужно задать себе другой во$ прос: что меняется во время выполнения процесса? Да многое. Создают$ ся и прекращаются новые потоки. Открываются и закрываются номера файлов, сокетов, окон и других элементов. Память распределяется и ос$ вобождается, считывается и записывается. Различные регистры внутри контекстов отдельных потоков чрезвычайно нестабильны и постоянно меняются. Мы можем выполнить поставленную задачу с помощью тех$ нологии виртуальной машины, например VMWare3, которая позволяет нам делать и восстанавливать моментальные снимки целых систем. Од$ нако этот процесс протекает мучительно медленно и предусматривает связь между гостем виртуальной машины и определенным арбитром на хосте виртуальной машины. Вместо этого мы позаимствуем кое$что у ранее рассмотренной технологии4 и «сжульничаем», сосредоточившись только на контекстах потока и изменениях в памяти. Процесс выполне$ ния моментального снимка будет состоять из двух частей.

Сначала мы сохраняем контекст каждого потока внутри нашего объ$ ектного процесса. Мы уже видели, насколько легко получить и уста$

1http://msdn.microsoft.com/library/default.asp?url=/library/en+us/debug/

base/getthreadcontext.asp

2http://msdn.microsoft.com/library/default.asp?url=/library/en+us/debug/

base/setthreadcontext.asp

3

4

http://www.vmware.com

Greg Hoglund, Runtime Decompilation, BlackHat Proceedings

Собрать все воедино

345

новить контексты отдельных потоков. Теперь нужно поместить дан$ ный код в логику, ответственную за перечисление системных потоков, принадлежащих объектному процессу. Для того чтобы это сделать, вос$ пользуемся вспомогательными функциями инструмента.1 Во$первых, получим список всех системных потоков, поместив флаг TH32CS_SNAP THREAD:

thread_entry

=

THREADENTRY32()

contexts

=

[]

snapshot = kernel32.CreateToolhelp32Snapshot( \ TH32CS_SNAPTHREAD, \ 0)

Затем извлекаем первый поток из списка. Но прежде чем это сделать, необходимо выполнить обязательное требование программного интер$ фейса Thread32First():инициализировать переменную dwSize внутри структуры потока. Мы передаем программный интерфейс Thread32 First(), уже сделанный моментальный снимок и указатель в структу$ ру потока:

thread_entry.dwSize = sizeof(thread_entry)

success = kernel32.Thread32First( \ snapshot, \ byref(thread_entry))

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

while success:

if thread_entry.th32OwnerProcessID == pid: context = CONTEXT() context.ContextFlags = CONTEXT_FULL

h_thread = kernel32.OpenThread(

\

THREAD_ALL_ACCESS,

\

None,

\

thread_id)

kernel32.GetThreadContext( \ h_thread, \ byref(context))

contexts.append(context)

kernel32.CloseHandle(h_thread)

1http://msdn2.microsoft.com/en+us/library/ms686832.aspx

346

Глава 20. Фаззинг оперативной памяти: автоматизация

success = kernel32.Thread32Next( \ snapshot, \ byref(thread_entry))

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

На втором этапе мы сохраняем содержимое каждого нестабильного блока памяти. В предыдущей главе говорилось, что каждый процесс 32$битной платформы Windows x86 «видит» свою собственную область памяти размером 4 Гбайт. Обычно нижняя половина этих 4 Гбайт отво$ дится для использования нашим процессом (0x0000000–0x7FFFFFFF). Эта область памяти в дальнейшем разделяется на отдельные страни$ цы, обычно размером 4096 байт. И в конце к каждой из этих страниц применяются права доступа к памяти на уровне самых мелких ячеек. Мы не храним содержимое каждой используемой отдельно страницы памяти, напротив, экономим время и силы, ограничивая наш момен$ тальный снимок страницами памяти, которые представляются нам не$ стабильными. Среди них страницы, отмеченные как:

PAGE_READONLY

PAGE_EXECUTE_READ

PAGE_GUARD

PAGE_NOACCESS

Нам также необходимо исключить страницы, принадлежащие выпол$ няемым изображениям, поскольку они вряд ли будут изменяться. Для прохождения через все доступные страницы памяти необходим про$ стой цикл в рамках механизма программного интерфейса VirtualQue ryEx()1, предоставляющий информацию о страницах внутри диапазона заданного виртуального адреса:

cursor

=

0

 

memory_blocks

=

[]

 

read_buf

=

create_string_buffer(length)

count

=

c_ulong(0)

 

mbi

=

MEMORY_BASIC_INFORMATION()

 

while cursor < 0xFFFFFFFF:

 

save_block = True

 

bytes_read = kernel32.VirtualQueryEx( \

h_process,

\

cursor,

 

\

byref(mbi),

\

sizeof(mbi))

 

1http://msdn2.microsoft.com/en+us/library/aa366907.aspx

Собрать все воедино

347

if bytes_read < sizeof(mbi): break

Если команда VirtualQueryEx()заканчивается неудачей, мы можем предположить, что доступное нам пользовательское пространство бы$ ло исчерпано и цикл чтения был нарушен. Каждый из обнаруженный блоков памяти нашего цикла проверяем на наличие наиболее перспек$ тивных прав доступа к памяти:

if mbi.State != MEM_COMMIT or \ mbi.Type == MEM_IMAGE: save_block = False

if mbi.Protect & PAGE_READONLY: save_block = False

if mbi.Protect & PAGE_EXECUTE_READ: save_block = False

if mbi.Protect & PAGE_GUARD: save_block = False

if mbi.Protect & PAGE_NOACCESS: save_block = False

Если нам встречается блок памяти, который хотелось бы включить в моментальный снимок, то мы прочитываем содержимое этого блока с помощью программного интерфейса ReadProcessMemory()1 и помещаем необработанные данные вместе с информацией памяти в список мо$ ментального снимка. Наконец, мы увеличиваем значение курсора, сканирующего память, и продолжаем:

if save_block: kernel32.ReadProcessMemory( \

h_process, \ mbi.BaseAddress, \ read_buf, \ mbi.RegionSize, \ byref(count))

memory_blocks.append((mbi, read_buf.raw))

cursor += mbi.RegionSize

Вы, наверное, уже заметили, что наш метод не безупречен. Что, если определенная страница в тот момент, когда мы будем производить мо$ ментальный снимок, будет помечена как PAGE_READONLY, а затем будет обновлена и станет перезаписываемой и видоизменяемой? Хоро$ ший вопрос; ответ на него – мы пропускаем эти случаи. Разве мы когда$ нибудь обещали, что наш подход будет идеальным? На самом деле мы, пользуясь случаем, еще раз подчеркнем экспериментальный характер

1http://msdn2.microsoft.com/en+us/library/ms680553.aspx

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