+ Reply to Thread
Results 1 to 1 of 1

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

  1. #1
    Prosper-H's Avatar

    Default 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, для установки программных брейкпойнтов имеет следующий прототип:

    Code:
    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
    Code:
    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
    Code:
    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
    Code:
    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"
    Code:
    (#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
    Code:
    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
    Code:
    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
    Дорогу осилит идущий. (К. Касперски)

    Двери есть везде. Просто нужно знать, как в них войти. ("Хроники Амбера", персонаж: Корвин)

  2. 4 пользователя(ей) сказали cпасибо:
    Heroin (20-11-2011) Markus (21-11-2011) Rectifier (18-11-2011) ximera (19-11-2011)
+ Reply to Thread

Tags for this Thread

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
All times are GMT. The time now is 01:35
vBulletin® Copyright ©2000 - 2018
www.reverse4you.org