R0 CREW

Gray Hat Python: Глава 6 - Hooking (Перевод: Prosper-H)

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

Intro

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

На платформе Windows существует множество методов позволяющих осуществить перехваты. Мы сфокусируемся на двух основных, которые я называю «программные» и «жесткие» перехваты. Программный перехват (soft hook) – это когда вы присоединяетесь к исследуемому процессу и устанавливаете обработчики прерываний (breakpoint handlers), чтобы перехватить поток исполнения процесса. Это может походить на уже знакомую теорию потому, что по сути вы написали перехватчик в 4-ой главе «4.1 Расширение брэйкпоинт-обработчиков». Жесткий перехват (hard hook) – это когда вы напрямую вставляете переходник, на обработчик перехвата, в код, где хотите осуществить сам перехват. Программные перехваты полезно использовать для не интенсивно или редко используемых вызовов функций. Однако, для того чтобы перехватить вызов часто используемой процедуры и оказывать наименьшее воздействие на процесс, нужно использовать жесткие перехваты. Главными кандидатами на использование жестких перехватов являются процедуры управления кучей или процедуры, интенсивно использующие файловые операции ввода/вывода (file I/O operations).

В этой главе мы воспользуемся рассмотренными ранее инструментами для того, чтобы применить обе методики перехвата. Начнем с использования PyDbg, который будет задействован для установки нескольких программных перехватов (soft hooking), которые позволят перехватить (sniff) зашифрованный сетевой трафик. Далее задействовав Immunity Debugger, перейдем к жестким перехватам (hard hooking), чтобы сделать высокоэффективный инструментарий для работы с процедурами управления кучи.

6.1 Программные перехваты с помощью PyDbg

Первый пример, который мы рассмотрим, включает в себя перехват зашифрованного трафика на прикладном уровне (прим. пер. см. модель OSI). Обычно, чтобы понять, как клиент или сервер взаимодействует с сетью, мы будем использовать анализатор трафика Wireshark [1]. К сожалению, Wireshark ограничен тем, что может видеть только зашифрованные данные, что, конечно же, скрывает истинную природу изучаемого протокола. Используя технику программных перехватов, мы можем перехватить данные, до того как они будут зашифрованы и до того, как они будут получены и расшифрованы.

Программой, на которой мы будем испытывать программные перехваты, будет популярный веб-браузер с открытым исходным кодом Mozilla Firefox [2]. В этом упражнении мы сделаем вид, что Firefox – это браузер с закрытым кодом (иначе было бы не так весело, не правда ли?) и наша работа состоит в том, чтобы перехватить данные из процесса firefox.exe до того, как они будут зашифрованы и отправлены на сервер. Наиболее распространенной формой шифрования, которую использует Firefox, является протокол шифрования SSL (Secure Sockets Layer); таким образом, мы определились с основной целью в нашем упражнении.

Чтобы обнаружить вызов или вызовы отвечающие за передачу незашифрованных данных, вы можете использовать технику для регистрации межмодульных вызовов (logging intermodular calls) , которая описана в следующей теме http://forum.immunityinc.com/index.php?topic=35.0. Нет «конкретного» места, где именно нужно ставить перехват; все зависит только от ваших предпочтений. Поэтому, чтобы мы не разошлись в разные стороны, договоримся, что перехват установлен на функцию PR_Write, которая экспортируется из nspr4.dll. Когда эта функция будет вызвана, в указателе на массив символов ASCII, который размещен по адресу [ESP + 8], будут содержаться данные до их непосредственного шифрования. Смещение +8 от регистра ESP говорит нам о том, что это второй параметр, передаваемый в интересующую нас функцию PR_Write. Именно здесь мы и будем перехватывать ASCII данные, регистрировать их и продолжать выполнение процесса.

Сначала давайте убедимся, что мы действительном можем видеть данные которые нас интересуют. Откройте Firefox и перейдите на один из моих любимых сайтов: https://www.openrce.org/. Как только вы приняли SSL-сертификат сайта и его страница загрузилась, присоедините Immunity Debugger к процессу firefox.exe и установите http://ru.und3rgr0und.org/wiki/Breakpointбрэйкпоинт на nspr4.PR_Write. В правом верхнем углу веб-сайта OpenRCE есть форма входа; введите имя пользователя «test», пароль «test» и нажмите кнопку «Login». Брэйкпоинт, который вы установили, должен сразу же сработать; продолжайте жать кнопку F9 и вы будете видеть его постоянное срабатывание. В конце концов, вы увидите указатель на строку в стеке, которая разыменовываеться во что-то похожее:

[ESP + 8] => ASCII "username=test&password=test&remember_me=on"

Славно! Мы можем отчетливо видеть имя пользователя и пароль, но если бы вы посмотрели на эту транзакцию на сетевом уровне (network level), то все данные были бы непонятными из-за сильного SSL-шифрования. Этот метод будет работать не только с сайтом OpenRCE; например, чтобы дать выход вашей паранойи, зайдите на более критичный к утечке данных сайт, и посмотрите, как можно легко видеть незашифрованную информацию, передаваемую на сервер. Теперь давайте автоматизируем этот процесс так, чтобы мы могли перехватывать соответствующую информацию, не управляя отладчиком вручную.

Чтобы определить программный перехват с помощью PyDbg, необходимо вначале определить hook container, который будет содержать все ваши объекты перехватов (hook objects). Для инициализации контейнера используйте следующую команду:

hooks = utils.hook_container()

Чтобы определить перехват и добавить его в контейнер, используйте метод add() из класса hook_container. Прототип функции имеет следующий вид:

add( pydbg, address, num_arguments, func_entry_hook, func_exit_hook )

Первый параметр – pydbg объект. Параметр address – это адрес, на который вы хотите установить перехват. В параметре num_arguments задается количество параметров, которые получает перехватываемая функция. Параметры func_entry_hook и func_exit_hook – это функции обратного вызова (callback), которые определяют код, который будет вызван сразу после срабатывания перехвата и перед его завершением. Стартовые перехваты (entry hooks, прим. пер. т.е. перехваты, установленные на начало функции) полезны тем, что позволяют видеть параметры передаваемые в функцию, в то время как конечные перехваты (exit hooks, прим. пер. т.е. перехваты, установленные в конце функции) позволяют перехватывать возвращаемые значения.

Функция entry hook callback должна иметь следующий прототип:

def entry_hook( dbg, args ):
    
    # Hook code here

    return DBG_CONTINUE

Параметр dbg – это pydbg объект, который используется для установки перехвата. Параметр args – это список zero-based параметров, которые были перехвачены во время срабатывания перехвата.

Прототип функции exit hook callback немного отличается от предыдущей тем, что имеет дополнительный параметр ret, который является возвращаемым значением перехваченной функции (т.е. значением из регистра EAX).

def exit_hook( dbg, args, ret ):
    
    # Hook code here

    return DBG_CONTINUE

Чтобы продемонстрировать, как использовать entry hook callback для перехвата нешифрованного трафика, откройте новый файл Python, назовите его firefox_hook.py и введите следующий код.

firefox_hook.py

from pydbg import *
from pydbg.defines import *

import utils
import sys

dbg = pydbg()
found_firefox = False

# Let's set a global pattern that we can make the hook
# search for
pattern = "password"

# This is our entry hook callback function
# the argument we are interested in is args[1]
def ssl_sniff( dbg, args ):

    # Now we read out the memory pointed to by the second argument
    # it is stored as an ASCII string, so we'll loop on a read until
    # we reach a NULL byte
    buffer = ""
    offset = 0

    while 1:
        byte = dbg.read_process_memory( args[1] + offset, 1 )

        if byte != "\x00":
            buffer += byte
            offset += 1
            continue
        else:
            break

    if pattern in buffer:
        print "Pre-Encrypted: %s" % buffer

    return DBG_CONTINUE

# Quick and dirty process enumeration to find firefox.exe
for (pid, name) in dbg.enumerate_processes():

    if name.lower() == "firefox.exe":

        found_firefox = True
        hooks = utils.hook_container()

        dbg.attach(pid)
        print "[*] Attaching to firefox.exe with PID: %d" % pid

        # Resolve the function address
            hook_address = dbg.func_resolve_debuggee("nspr4.dll","PR_Write")

        if hook_address:
            # Add the hook to the container. We aren't interested
            # in using an exit callback, so we set it to None.
            hooks.add( dbg, hook_address, 2, ssl_sniff, None )
            print "[*] nspr4.PR_Write hooked at: 0x%08x" % hook_address
            break
        else:
            print "[*] Error: Couldn't resolve hook address."
            sys.exit(-1)

if found_firefox:
    print "[*] Hooks set, continuing process."
    dbg.run()
else:
    print "[*] Error: Couldn't find the firefox.exe process."
    sys.exit(-1)

Этот код довольно прост. Он устанавливает перехват на PR_Write, и когда тот срабатывает, мы пытаемся прочитать ASCII-строку, на которую указывает второй параметр. Если читаемая строка соответствует нашему шаблону, то мы выводим ее в консоль. Запустите новый экземпляр Firefox, после чего запустите firefox_hook.py из командной строки. Повторите ваши предыдущие шаги, попытавшись залогиниться на https://www.openrce.org/ и вы должны будете увидеть вывод, похожий на Листинг 6-1.

Листинг 6-1: Круто! Мы можем четко видеть имя и пароль, до их шифрования.

[*] Attaching to firefox.exe with PID: 1344
[*] nspr4.PR_Write hooked at: 0x601a2760
[*] Hooks set, continuing process.
Pre-Encrypted: username=test&password=test&remember_me=on
Pre-Encrypted: username=test&password=test&remember_me=on
Pre-Encrypted: username=jms&password=yeahright!&remember_me=on

Мы только что показали, насколько программные перехваты легки и в тоже время мощны в использовании. Эта техника может быть применена ко всем видам отладочных сценариев. Этот сценарий хорошо подходит для перехвата мало используемых функций. Если бы мы применили его для функции, которая используется более интенсивно, то очень быстро мы бы заметили, что выполнение процесса замедляется и он начинает вести себя странным образом или даже завершается (crash). Это происходит потому, что инструкция INT3 вызывает обработчик, который затем вызывает наш собственный перехватчик (hook code), – и помимо этого контролирует возвращаемое значение. Это требует много работы, особенно если это должно происходить тысячи раз в секунду! Давайте посмотрим, как мы можем обойти это ограничение, применив жесткий перехват (hard hook). Вперед!

6.2 Жесткие перехваты с помощью Immunity Debugger

Теперь мы добрались до интересного материала – технике жесткого перехвата. Это более продвинутый метод. Он оказывает гораздо меньшее воздействие на процесс, чем предыдущий, потому что наш перехватчик (hook code) будет написан исключительно in x86 assembly. В случае программного перехватчика (soft hook), происходило много событий (и еще больше инструкций), которые выполнялись в момент между срабатыванием прерывания, выполнения перехватчика (hook code) и восстановлением выполнения процесса. Используя hard hook, вы в действительности просто расширяете конкретный кусок кода, чтобы выполнить наш перехватчик и восстановить нормальную работу процесса. Хорошо то, что когда вы используете hard hook, отлаживаемый процесс никогда не останавливается, в отличии от soft hook.

Immunity Debugger снижает сложность установки hard hook предоставляя простой объект FastLogHook. Объект FastLogHook автоматически устанавливает заглушку (stub), которая регистрирует (logs) значения, которые вас интересуют, и перезаписывает оригинальную инструкцию, которую вы хотите перехватить, прыжком на заглушку (jump to the stub). При создании быстрых регистрирующих перехватчиков (fast log hooks), нужно вначале определить указатель на сам перехватчик, а затем определить указатель на данные, которые нужно зарегистрировать. Определение скелета установки перехватчика имеет следующий вид:

imm = immlib.Debugger()
fast = immlib.FastLogHook( imm )

fast.logFunction( address, num_arguments )
fast.logRegister( register )
fast.logDirectMemory( address )
fast.logBaseDisplacement( register, offset )

Метод logFunction() необходим для установки перехвата, так как он передает основной адрес того, куда будут перезаписаны оригинальные инструкции из места, где будет размещен перехватывающий код (hook code). Его параметрами являются адрес перехватчика и количество перехватываемых аргументов. Если вы устанавливаете перехватчик на начало функции и хотите перехватить ее параметры, тогда вам, скорее всего, нужно установить количество аргументов. Если вы устанавливаете перехватчик на конец функции, тогда вам, с большой вероятностью, следует установить num_arguments в нуль. Методами, которые непосредственно занимаются регистрацией (logging), являются logRegister(), logBaseDisplacement() и logDirectMemory(). Три эти функции имеют следующие прототипы:

logRegister( register )
logBaseDisplacement( register, offset )
logDirectMemory( address )

Метод logRegister(), во время срабатывания перехвата, отслеживает значение определенного регистра. Это полезно для отслеживания возвращаемого значения, после вызова функции, которое хранится в регистре EAX. Метод logBaseDisplacement() принимает два параметра регистр и смещение; он предназначен для разыменовывания параметров в стеке или для получения данных, когда известно смещение от регистра. Последний вызов logDirectMemory() используется, чтобы зарегистрировать известное смещение памяти во время перехвата (which is used to log a known memory offset at hook time).

Когда срабатывает перехват и вызываются функции регистрации, они хранят собранную информацию в выделенной области памяти, которую создает объект FastLogHook. Чтобы получить результат от вашего перехватчика, нужно запросить эту страницу используя функцию-обертку getAllLog(), которая анализирует память и возвращает список Python в следующей форме:

[( hook_address, ( arg1, arg2, argN )), ... ]

Таким образом, каждый раз, когда срабатывает перехватчик, его адрес сохраняется в hook_address, а вся информация, которую вы запросили, содержится в форме кортежа, во второй записи. В заключение, важно отметить, что существует дополнительная разновидность FastLogHook, метод STDCALLFastLogHook, который устанавливается для соглашения о вызовах STDCALL. Для cdecl соглашения используется обычный метод FastLogHook. Хотя используются они одинаково.

Прекрасным примером использования мощного hard hook является PyCommand hippie, автором которой был один из ведущих мировых экспертов по переполнению кучи, Nicolas Waisman из Immunity, Inc. По словам самого Nico:

[INDENT]Выход hippie стал ответным действием на необходимость высокоэффективной регистрации перехватов (hook), которые действительно могли бы обрабатывать большое количество вызовов, которые требуют функции Win32 API. Возьмем, к примеру, Notepad; если вы откроете file dialog, то на это потребуется около 4.500 вызовов RtlAllocateHeap или RtlFreeHeap. Если же вы возьмете Internet Explorer, который, более интенсивно использует кучу (heap), то вы увидите увеличение числа вызовов функций, связанных с кучей, раз в 10 и больше.[/INDENT]

Как сказал Nico, мы можем использовать hippie в качестве примера того, как инструментировать процедуры кучи (to instrument heap routines), что очень важно для понимания, при написании ориентированных на кучу эксплойтов (heap-based exploits). Для краткости, мы рассмотрим только базовые части hippie и в процессе создадим упрощенную версию, которую назовем hippie_easy.py.

Прежде, чем мы начнем, важно разобраться с прототипами функций RtlAllocateHeap и RtlFreeHeap, чтобы наши точки перехвата (hook points) имели смысл.

BOOLEAN RtlFreeHeap(
        IN PVOID HeapHandle,
        IN ULONG Flags,
        IN PVOID HeapBase
);
PVOID RtlAllocateHeap(
        IN PVOID HeapHandle,
        IN ULONG Flags,
        IN SIZE_T Size
);

Итак, в RtlFreeHeap мы перехватим все три аргумента, а в RtlAllocateHeap мы перехватим три аргумента плюс указатель, который возвращается после вызова функции. Возвращаемый указатель указывает на новый блок кучи, который только что был создан. Теперь, когда мы разобрались с hook points, откройте новый файл Python, назовите hippie_easy.py его и введите следующий код.

hippie_easy.py

import immlib
import immutils

# This is Nico's function that looks for the correct
# basic block that has our desired ret instruction
# this is used to find the proper hook point for RtlAllocateHeap
(#1): def getRet(imm, allocaddr, max_opcodes = 300):
        addr = allocaddr
        for a in range(0, max_opcodes):
                op = imm.disasmForward( addr )

                if op.isRet():
                        if op.getImmConst() == 0xC:
                                op = imm.disasmBackward( addr, 3 )
                                return op.getAddress()
                addr = op.getAddress()
        
        return 0x0

# A simple wrapper to just print out the hook
# results in a friendly manner, it simply checks the hook
# address against the stored addresses for RtlAllocateHeap, RtlFreeHeap
def showresult(imm, a, rtlallocate):
        if a[0] == rtlallocate:
                imm.Log( "RtlAllocateHeap(0x%08x, 0x%08x, 0x%08x) <- 0x%08x %s" % (a[1][0], a[1][1], a[1][2], a[1][3], extra), address = a[1][3] )

                return "done"
        else:
                imm.Log( "RtlFreeHeap(0x%08x, 0x%08x, 0x%08x)" % (a[1][0], a[1][1], a[1][2]) )

def main(args):

        imm = immlib.Debugger()
        Name = "hippie"
        fast = imm.getKnowledge( Name )

        (#2): if fast:
                # We have previously set hooks, so we must want
                # to print the results
                hook_list = fast.getAllLog()

                rtlallocate, rtlfree = imm.getKnowledge("FuncNames")
                for a in hook_list:
                        ret = showresult( imm, a, rtlallocate )

                return "Logged: %d hook hits." % len(hook_list)

        # We want to stop the debugger before monkeying around
        imm.Pause()
        rtlfree = imm.getAddress("ntdll.RtlFreeHeap")
        rtlallocate = imm.getAddress("ntdll.RtlAllocateHeap")

        module = imm.getModule("ntdll.dll")

        if not module.isAnalysed():
                imm.analyseCode( module.getCodebase() )

        # We search for the correct function exit point
        rtlallocate = getRet( imm, rtlallocate, 1000 )
        imm.Log("RtlAllocateHeap hook: 0x%08x" % rtlallocate)

        # Store the hook points
        imm.addKnowledge( "FuncNames", ( rtlallocate, rtlfree ) )

        # Now we start building the hook
        fast = immlib.STDCALLFastLogHook( imm )

        # We are trapping RtlAllocateHeap at the end of the function
        imm.Log("Logging on Alloc 0x%08x" % rtlallocate)
        (#3): fast.logFunction( rtlallocate )
        fast.logBaseDisplacement( "EBP", 8 )
        fast.logBaseDisplacement( "EBP", 0xC )
        fast.logBaseDisplacement( "EBP", 0x10 )
        fast.logRegister( "EAX" )

        # We are trapping RtlFreeHeap at the head of the function
        imm.Log("Logging on RtlFreeHeap 0x%08x" % rtlfree)
        fast.logFunction( rtlfree, 3 )

        # Set the hook
        fast.Hook()

        # Store the hook object so we can retrieve results later
        imm.addKnowledge(Name, fast, force_add = 1)

        return "Hooks set, press F9 to continue the process."

Прежде чем запускать этот скрипт, давайте посмотрим на код. Первая функция, которую вы видите (#1) является одним из кусков кода, который написал Nico, для того, чтобы найти правильное место для перехвата RtlAllocateHeap. Для иллюстрации, дизассемблируйте RtlAllocateHeap и в нескольких последних инструкциях вы увидите эти:

0x7C9106D7  F605 F002FE7F    TEST BYTE PTR DS:[7FFE02F0],2
0x7C9106DE  0F85 1FB20200    JNZ ntdll.7C93B903
0x7C9106E4  8BC6             MOV EAX,ESI
0x7C9106E6  E8 17E7FFFF      CALL ntdll.7C90EE02
0x7C9106EB  C2 0C00          RETN 0C

Таким образом код Python, начинает дизассемблировать с головы функции пока не найдет инструкцию RET по адресу 0x7C9106EB, после чего проверяет константу 0x0C, чтобы убедиться, что он оказался в правильном месте. Затем он дизассемблирует последние три инструкции, которые располагаются по адресу 0x7C9106D7. Этот небольшой танец, мы совершаем, чтобы убедиться, что у нас есть достаточно места, чтобы записать нашу 5-байтовую инструкцию JMP. Если бы мы попытались установить 5-байтовый JMP прямо на 3-байтовый RET, то мы бы перезаписали 2-ва дополнительных байта, который повредили бы выравнивание кода, и процесс бы немедленно завершился (crash). Привыкните к написанию этих маленьких служебным функций, которые помогут вам избежать этих типичных препятствий. Двоичные файлы сложные твари, и они не терпят ошибок.

Следующий кусок кода (#2) является простой проверкой относительно того, установлены ли перехватчики, и если да, то мы просто получаем необходимые объекты из базы знаний (knowledge base) и распечатываем результаты наших перехватов. Скрипт разработан таким образом, чтобы вы, запустив его одни раз, установили перехватчики, а затем запускали его снова и снова для мониторинга результатов. Если вы хотите создать кастомный (custom) запрос для какого-либо из объектов, хранящихся в базе знаний, то вы можете получить к ним доступ из Питоновской оболочки отладчика.

Последний кусок (#3) это конструкция перехватчиков и точек мониторинга. Для вызова RtlAllocateHeap мы перехватываем три аргумента из стека и возвращаемое значение функции. Для RtlFreeHeap мы перехватываем три аргумента из стека, в момент, когда она получает управление. В менее чем в 100 строках кода мы использовали очень мощную технику перехвата – причем, без использования компилятора или каких-либо дополнительных инструментов. Очень крутой stuff.

Давайте, используем notepad.exe и посмотрим насколько был прав Nico, говоря о 4.500 вызовах, которые происходят во время открытия file dialog. Запустите «C:\WINDOWS\System32\notepad.exe» под отладчиком Immunity Debugger и запустите PyCommand !hippie_easy в командной строке (если вы забыли как это сделать, перечитайте Главу 5). Возобновите процесс и затем выберите «File => Open» в Notepad.

Пришло время проверить полученные результаты. Запустите повторно PyCommand и вы должны увидеть вывод в окне Log отладчика Immunity Debugger (ALT-L), который похож на Листинг 6-2.

Листинг 6-2: Вывод команды !hippie_easy (PyCommand)

RtlFreeHeap(0x000a0000, 0x00000000, 0x000ca0b0)
RtlFreeHeap(0x000a0000, 0x00000000, 0x000ca058)
RtlFreeHeap(0x000a0000, 0x00000000, 0x000ca020)
RtlFreeHeap(0x001a0000, 0x00000000, 0x001a3ae8)
RtlFreeHeap(0x00030000, 0x00000000, 0x00037798)
RtlFreeHeap(0x000a0000, 0x00000000, 0x000c9fe8)

Прекрасно! У нас есть результаты, и если вы посмотрите на строку состояния (status bar) в Immunity Debugger, то она сообщит количество срабатываний. У меня вышло 4.675, так что Nico был прав. Вы можете перезапустить скрипт в любое время, когда захотите увидеть изменения. Самое замечательное здесь то, что мы прошлись по тысячам вызовов без какого-либо ухудшения производительности.

Hooking - это то, что вы несомненно будете использовать бесчисленное количество, во время реверсинга приложений. Мы не только продемонстрировали, как применить некоторые методы перехвата, но и автоматизировали их. Теперь, когда вы знаете как эффективно мониторить за данными приложений с помощью перехватчиков, пришло время изучить то, как можно манипулировать процессами. Выполнять манипуляции мы будем с помощью внедрения кода и внедрения DLL-библиотек. Ну что приступим?

© Translated by Prosper-H from r0 Crew

Ссылки:

[1] See:
http://www.wireshark.org/

[2] For the Firefox download, go to:
http://www.mozilla.com/en-US/