R0 CREW

Gray Hat Python: Глава 4 - PyDbg - Windows отладчик на чистом Python (Перевод: Prosper-H)

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

Intro

Если вы зашли так далеко, то должны хорошо представлять, как использовать Python для создания user-mode отладчика под Windows. Ну, а теперь перейдем к изучению того, как использовать мощь PyDbg, отладчика под Windows на Python’e с открытым исходным кодом. PyDbg был зарелизен Педрамом Амини (Pedram Amini) на конференции Recon, прошедшей в 2006 году в Монреале (Канада), как основной компонент PaiMei [1], который является фреймворк для реверсинга. PyDbg был использован в довольно многих инструментах, включая популярный прокси фаззер «Taof» и фаззер драйверов Windows «ioctlizer», создателем которого я и являюсь. Эту главу мы начнем с расширения обработчика брейкпойнтов, а затем перейдем к более продвинутым темам, таким как: обработка сбоев (crashes) приложений и взятие снимков (snapshots) процесса. Некоторые из инструментов, которые мы реализуем в этой главе, могут быть использованы позже, для сопровождения некоторых фаззеров, которые мы собираемся разработать. За дело!

4.1 Расширение брейкойнт-обработчиков

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

Функция PyDbg, для установки программных брейкпойнтов имеет следующий прототип:

bp_set(address, description="",restore=True,handler=None)

Описание параметров:

  • address – адрес, где должен быть установлен программный брейкпойнт.
  • description – необязательный параметр, используется для задания каждому брейкпойнту своего уникального имени.
  • restore – определяет, будет ли брейкпойнт автоматически сброшен после своей обработки.
  • handler – определяет, какую callback-функцию вызвать, когда встречается этот брейкпойнт. Callback-функции, которые устанавливаются на брейкпойнт, принимают только один параметр, являющийся экземпляром класса pydbg(). Вся информация, имеющая отношение к потоку и процессу, будет сохранена в этом классе, когда тот будет передан в callback-функцию.

Давайте, реализуем пользовательскую callback-функцию. В качестве подопытного кролика мы будем использовать тестовый скрипт printf_loop.py. В этом упражнении, мы будем запускать тестовый скрипт и считывать значение счетчика, используемого в цикле printf, из памяти – заменяя его на случайное число между 1 и 100. Правда нужно помнить то, что в действительности мы читаем и изменяем данные в нутрии исследуемого процесса. Это действительно мощно! Создайте новый файл, назвав его printf_random.py и введите следующий код.

Example 1: printf_random.py

from pydbg import *
from pydbg.defines import *

import struct
import random

# This is our user defined callback function
def printf_randomizer(dbg):
    # Read in the value of the counter at ESP + 0x8 as a DWORD
    parameter_addr = dbg.context.Esp + 0x8
    counter = dbg.read_process_memory(parameter_addr,4)

    # When we use read_process_memory, it returns a packed binary
    # string. We must first unpack it before we can use it further.
    counter = struct.unpack("L",counter)[0]
    print "Counter: %d" % int(counter)

    # Generate a random number and pack it into binary format
    # so that it is written correctly back into the process
    random_counter = random.randint(1,100)
    random_counter = struct.pack("L",random_counter)[0]

    # Now swap in our random number and resume the process
    dbg.write_process_memory(parameter_addr,random_counter)

    return DBG_CONTINUE

# Instantiate the pydbg class
dbg = pydbg()

# Now enter the PID of the printf_loop.py process
pid = raw_input("Enter the printf_loop.py PID: ")

# Attach the debugger to that process
dbg.attach(int(pid))

# Set the breakpoint with the printf_randomizer function
# defined as a callback
printf_address = dbg.func_resolve("msvcrt","printf")
dbg.bp_set(printf_address,description="printf_address",handler=printf_randomizer)

# Resume the process
dbg.run()

Теперь запустите оба скрипта printf_loop.py и printf_random.py. Вывод будет похож на тот, что показан в Таблице 4-1.

Таблица 4-1: Демонстрация манипулирования памятью процесса

Вы можете видеть, что отладчик установил брейкпойнт на четвертой итерации цикла printf, так как выведенное отладчиком значение счетчика равно 4-рем. Вы так видно, что скрипт printf_loop.py, успешно дошел до 4-ой итерации, но вместо вывода номера 4 вывел номер 32! Тут ясно видно, как отладчик изменяет настоящее значение счетчика, заменяя его случайное, до того, как оно будет выведено отлаживаемым процессом. Это простой, но все же мощный пример того, как можно легко расширить скриптовый отладчик для выполнения дополнительных действий, при наступлении отладочных событий. Теперь посмотрим на обработку сбоев приложения с использованием PyDbg.

4.2 Обработчики событий “access violation”

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

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

У PyDbg есть прекрасный метод, позволяющий установить обработчик нарушения прав доступа, помимо этого есть и другие вспомогательные функции, позволяющие вывести всю информации относящуюся к сбою. Давайте вначале создадим тестовый скрипт, который будет использовать опасную функцию языка “C” strcpy() для демонстрации переполнения буфера. Следующим напишем небольшой PyDbg скрипт, для присоединения (attach) и обработки нарушения доступа. Давайте начнем с тестового скрипта. Создайте новый файл, назвав его buffer_overflow.py, и введите следующий код:

Example 2: buffer_overflow.py

from ctypes import *

msvcrt = cdll.msvcrt

# Give the debugger time to attach, then hit a button
raw_input("Once the debugger is attached, press any key.")

# Create the 5-byte destination buffer
buffer = c_char_p("AAAAA")

# The overflow string
overflow = "A" * 100

# Run the overflow
msvcrt.strcpy(buffer, overflow)

Теперь, когда buffer_overflow.py создан, создайте еще один новый файл, назвав его access_violation_handler.py и введя следующий код.

Example 2: access_violation_handler.py

from pydbg import *
from pydbg.defines import *

# Utility libraries included with PyDbg
import utils

# This is our access violation handler
def check_accessv(dbg):

    # We skip first-chance exceptions
    if dbg.dbg.u.Exception.dwFirstChance:
        return DBG_EXCEPTION_NOT_HANDLED

    crash_bin = utils.crash_binning.crash_binning()
    crash_bin.record_crash(dbg)
    print crash_bin.crash_synopsis()

    dbg.terminate_process()

    return DBG_EXCEPTION_NOT_HANDLED

pid = raw_input("Enter the Process ID: ")

dbg = pydbg()
dbg.attach(int(pid))
dbg.set_callback(EXCEPTION_ACCESS_VIOLATION,check_accessv)
dbg.run()

Теперь запустите скрипт buffer_overflow.py и запомните его PID; он приостановит свою работу, до того момента, пока вы не продолжите ее. Теперь запустите скрипт access_violation_handler.py, и введите PID тестового скрипта. Как только отладчик присоединиться, нажмите любую клавишу в консоли, где работает тестовый скрипт, после чего вы увидите вывод похожий на Листинг 4-1.

Листинг 4-1: Аварийный вывод с помощью “PyDbg crash binning utility”

(#1): python25.dll:1e071cd8 mov ecx,[eax+0x54] from thread 3376 caused access
violation when attempting to read from 0x41414195

(#2): CONTEXT DUMP
    EIP: 1e071cd8 mov ecx,[eax+0x54]
    EAX: 41414141 (1094795585) -> N/A
    EBX: 00b055d0 ( 11556304) -> @U`" B`Ox,`O )Xb@|V`"L{O+H]$6 (heap)
    ECX: 0021fe90 ( 2227856) -> !$4|7|4|@%,\!$H8|!OGGBG)00S\o (stack)
    EDX: 00a1dc60 ( 10607712) -> V0`w`W (heap)
    EDI: 1e071cd0 ( 503782608) -> N/A
    ESI: 00a84220 ( 11026976) -> AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA (heap)
    EBP: 1e1cf448 ( 505214024) -> enable() -> NoneEnable automa (stack)
    ESP: 0021fe74 ( 2227828) -> 2? BUH` 7|4|@%,\!$H8|!OGGBG) (stack)
    +00: 00000000 ( 0) -> N/A
    +04: 1e063f32 ( 503725874) -> N/A
    +08: 00a84220 ( 11026976) -> AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA (heap)
    +0c: 00000000 ( 0) -> N/A
    +10: 00000000 ( 0) -> N/A
    +14: 00b055c0 ( 11556288) -> @F@U`" B`Ox,`O )Xb@|V`"L{O+H]$ (heap)

(#3): disasm around:
            0x1e071cc9 int3
            0x1e071cca int3
            0x1e071ccb int3
            0x1e071ccc int3
            0x1e071ccd int3
            0x1e071cce int3
            0x1e071ccf int3
            0x1e071cd0 push esi
            0x1e071cd1 mov esi,[esp+0x8]
            0x1e071cd5 mov eax,[esi+0x4]
            0x1e071cd8 mov ecx,[eax+0x54]
            0x1e071cdb test ch,0x40
            0x1e071cde jz 0x1e071cff
            0x1e071ce0 mov eax,[eax+0xa4]
            0x1e071ce6 test eax,eax
            0x1e071ce8 jz 0x1e071cf4
            0x1e071cea push esi
            0x1e071ceb call eax
            0x1e071ced add esp,0x4
            0x1e071cf0 test eax,eax
            0x1e071cf2 jz 0x1e071cff

(#4): SEH unwind:
            0021ffe0 -> python.exe:1d00136c jmp [0x1d002040]
            ffffffff -> kernel32.dll:7c839aa8 push ebp

Вывод разбит на несколько частей полезной информации. Первая часть (#1) говорит вам, какая инструкция вызвала нарушения прав доступа, а так же в каком модуле она находится. Эта информация полезна для написания эксплойта или если вы используете инструмент статического анализа для определения места неисправности. Вторая часть (#2) является дампом всех регистров; особенно интересно то, что мы перезаписали регистр EAX значением 0x41414141 (0x41 это шестнадцатеричное значение прописной буквы А). Так же, мы можем видеть, что регистр ESI указывает на строку символов A, такую же, как указатель стека в ESP+08. Третья часть (#3) содержит дизассемблерные инструкции до и после вызывающей ошибку инструкции. И наконец последняя часть (#4) содержит список обработчиков SEН, которые были зарегистрированы во время неисправности.

Вы видите насколько просто установить обработчик аварийной ситуации с использованием PyDbg. Это невероятно полезная функция, позволяющая автоматизировать обработку аварийных ситуаций и произвести их анализ. Далее мы будем использовать внутренний процесс создания снапшотов (snapshot) PyDbg, который позволяет создавать контрольные точки, в исследуемом процессе, для возврата к сохраненному состоянию, после каких-то манипуляций над ним.

4.3 Снапшоты

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

4.3.1 Создание снапшотов

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

Прежде, чем мы сможем сделать снапшот, нам нужно приостановить все выполняющиеся потоки, что бы они не изменили данные и состояние процесса, во время создания снапшота. Для приостановки всех потоков с помощью PyDbg, используем функцию suspend_all_threads(), а для восстановления, всех приостановленных потоков, используем функцию resume_all_threads(). Как только мы приостановили потоки, просто сделаем вызов функции process_snapshot(). Она автоматически извлекает всю содержащуюся информацию о каждом потоке и всей памяти, в данный конкретный момент. После того как снапшот сделан, мы возобновим все приостановленные потоки. Когда мы захотим восстановить процесс до контрольной точки, нам нужно вновь приостановить все потоки, затем вызвать функцию process_restore(), и возобновить работу потоков. После того, как мы возобновим процесс, мы вернемся в наше исходное состояние, когда был сделан снапшот (контрольная точка). Не плохо, правда?

Для демонстрации этих возможностей используем простой пример, в котором разрешим пользователю создавать и восстанавливаться из снапшотов. Создайте новый файл, назвав его snapshot.py и введите следующий код.

Example 3: snapshot.py

from pydbg import *
from pydbg.defines import *

import threading
import time
import sys

class snapshotter(object):
            def __init__(self,exe_path):
    
                self.exe_path = exe_path
                self.pid = None
                self.dbg = None
                self.running = True

                (#1): # Start the debugger thread, and loop until it sets the PID
                # of our target process
                pydbg_thread = threading.Thread(target=self.start_debugger)
                pydbg_thread.setDaemon(0)
                pydbg_thread.start()

                while self.pid == None:
                    time.sleep(1)

                (#2): # We now have a PID and the target is running; let's get a
                # second thread running to do the snapshots
                monitor_thread = threading.Thread(target=self.monitor_debugger)
                monitor_thread.setDaemon(0)
                monitor_thread.start()

(#3):    def monitor_debugger(self):
                
                while self.running == True:

                    input = raw_input("Enter: 'snap','restore' or 'quit'")
                    input = input.lower().strip()

                    if input == "quit":
                        print "[*] Exiting the snapshotter."
                        self.running = False
                        self.dbg.terminate_process()

                    elif input == "snap":

                        print "[*] Suspending all threads."
                        self.dbg.suspend_all_threads()

                        print "[*] Obtaining snapshot."
                        self.dbg.process_snapshot()

                        print "[*] Resuming operation."
                        self.dbg.resume_all_threads()

                    elif input == "restore":

                        print "[*] Suspending all threads."
                        self.dbg.suspend_all_threads()

                        print "[*] Restoring snapshot."
                        self.dbg.process_restore()

                        print "[*] Resuming operation."
                        self.dbg.resume_all_threads()

(#4):    def start_debugger(self):

                self.dbg = pydbg()
                pid = self.dbg.load(self.exe_path)
                self.pid = self.dbg.pid

                self.dbg.run()

(#5): exe_path = "C:\\WINDOWS\\System32\\calc.exe"
snapshotter(exe_path)

Итак, первый шаг (#1) это запуск приложения под отладочным потоком. При использовании отдельных потоков, мы можем вводить команды, в консоль, без принуждения приложения делать паузу, пока оно ожидает нашего ввода. После того, как отладочный поток вернул действительный PID (#4), мы запускаем новый поток (#2), который использует введенные нами данные. Затем, когда мы отправим ему команду, он определит (#3) делаем ли мы снапшот, восстановление из снапшота или выходим из приложения. В качестве примера, я выбрал Калькулятор (#5), используя который мы сможем наблюдать процесс создания снапшотов в действии. Произведите случайные вычисления в Калькуляторе, потом введите snap в консоль нашего Python скрипта, затем снова произведите какие-нибудь вычисления или очистите результат предыдущих вычислений. Затем введите restore в консоль Python скипта и вы увидите, что появится число, отображаемое в момент снятия снапшота. Используя этот метод, вы можете прогуливаться туда-сюда по определенным частям процесса, которые предоставляют интерес, без перезагрузки самого процесса, для возобновления его точного состояния снова и снова. Теперь давайте объединим некоторые наши новые методы, основанные на применении PyDbg, для создания инструмента фаззинга, который поможет находить уязвимости в программном обеспечении и автоматизировать обработку сбоев.

4.3.2 Собираем все вместе

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

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

Разогрейте пальцы (будет много кода), создайте новый файл, назвав его danger_ track.py и введя следующий код.

Example 4: danger_track.py

from pydbg import *
from pydbg.defines import *

import utils

# This is the maximum number of instructions we will log
# after an access violation
MAX_INSTRUCTIONS = 10

# This is far from an exhaustive list; add more for bonus points
dangerous_functions = {
                        "strcpy" : "msvcrt.dll",
                        "strncpy" : "msvcrt.dll",
                        "sprintf" : "msvcrt.dll",
                        "vsprintf": "msvcrt.dll"
                      }

dangerous_functions_resolved = {}
crash_encountered            = False
instruction_count            = 0

def danger_handler(dbg):

    # We want to print out the contents of the stack; that's about it
    # Generally there are only going to be a few parameters, so we will
    # take everything from ESP to ESP+20, which should give us enough
    # information to determine if we own any of the data
    esp_offset = 0
    print "[*] Hit %s" % dangerous_functions_resolved[dbg.context.Eip]
    print "================================================================="

    while esp_offset <= 20:
        parameter = dbg.smart_dereference(dbg.context.Esp + esp_offset)
        print "[ESP + %d] => %s" % (esp_offset, parameter)
        esp_offset += 4

    print "=================================================================\n"

    dbg.suspend_all_threads()
    dbg.process_snapshot()
    dbg.resume_all_threads()

    return DBG_CONTINUE

def access_violation_handler(dbg):
    global crash_encountered

    # Something bad happened, which means something good happened :)
    # Let's handle the access violation and then restore the process
    # back to the last dangerous function that was called

    if dbg.dbg.u.Exception.dwFirstChance:
        return DBG_EXCEPTION_NOT_HANDLED

    crash_bin = utils.crash_binning.crash_binning()
    crash_bin.record_crash(dbg)
    print crash_bin.crash_synopsis()

    if crash_encountered == False:
        dbg.suspend_all_threads()
        dbg.process_restore()
        crash_encountered = True

        # We flag each thread to single step
        for thread_id in dbg.enumerate_threads():

            print "[*] Setting single step for thread: 0x%08x" % thread_id
            h_thread = dbg.open_thread(thread_id)
            dbg.single_step(True, h_thread)
            dbg.close_handle(h_thread)

        # Now resume execution, which will pass control to our
        # single step handler
        dbg.resume_all_threads()

        return DBG_CONTINUE
    else:
        dbg.terminate_process()

    return DBG_EXCEPTION_NOT_HANDLED

def single_step_handler(dbg):
    global instruction_count
    global crash_encountered

    if crash_encountered:

        if instruction_count == MAX_INSTRUCTIONS:

            dbg.single_step(False)
            return DBG_CONTINUE
        else:

            # Disassemble this instruction
            instruction = dbg.disasm(dbg.context.Eip)
            print "#%d\t0x%08x : %s" % (instruction_count,dbg.context.Eip, instruction)
            instruction_count += 1
            dbg.single_step(True)

    return DBG_CONTINUE


dbg = pydbg()

pid = int(raw_input("Enter the PID you wish to monitor: "))

dbg.attach(pid)

# Track down all of the dangerous functions and set breakpoints
for func in dangerous_functions.keys():

    func_address = dbg.func_resolve( dangerous_functions[func],func )
    print "[*] Resolved breakpoint: %s -> 0x%08x" % ( func, func_address )
    dbg.bp_set( func_address, handler = danger_handler )
    dangerous_functions_resolved[func_address] = func

dbg.set_callback( EXCEPTION_ACCESS_VIOLATION, access_violation_handler )
dbg.set_callback( EXCEPTION_SINGLE_STEP, single_step_handler )
dbg.run()

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

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

4.4 Дополнительные материалы от переводчика

Исходные коды, к примерам данной главы, можно скачать тут.

© Translated by Prosper-H from r0 Crew