Перейти к содержанию
Intro
Теперь, когда мы рассмотрели основы, пришло время применить знания, полученные в предыдущих главах книги. Когда Microsoft разработала Windows, она добавила поразительное множество отладочных функций, что бы помочь разработчикам и специалистам по обеспечению качества. В этой главе, мы будем интенсивно использовать эти функции, для создания собственного отладчика на Python’e. Важно отметить здесь и то, что мы, по сути, совершаем углубленное изучение PyDbg (Педрама Амини), поскольку, на данный момент, это наиболее чистый Windows-отладчик, написанный на Python (прим. пер. есть еще один стремительно развивающийся WinAppDbg).
3.1 Debuggee, Where Art Thou?
Для того что бы выполнить отладку процесса, у вас должна быть возможность ассоциировать отладчик с отлаживаемым процессом. Следовательно, наш отладчик должен иметь возможность либо открыть исполняемый файл и запустить его, либо присоединиться к уже запущенному процессу. Windows debugging API – предоставляет простой способ сделать и то и другое.
Существуют некоторые различия между открытием процесса и присоединением к нему. Преимуществом открытия процесса есть то, что у вас есть возможность получить контроль над процессом еще до того, как он сможет запустить какой бы то ни было код. Это может быть удобно при анализе вредоносных программ или других типов вредоносного кода. При присоединении вы вклиниваетесь в уже работающий процесс, что позволяет пропускать часть кода и анализировать специфические области, в которых вы заинтересованы. В зависимости от того, что отлаживается и то какой анализ нужно проделать - вы выбираете, какой подход вам использовать.
Первый способ, получения процесса выполняющегося под отладчиком – это запустить исполняемый файл испод самого отладчика. Для создания процесса в Windows, вам нужно вызвать функцию CreateProcessA() [1]. Установка конкретных флагов, которые передаются в эту функцию автоматически, включает процесс отладки. Вызов CreateProcessA() выглядит так:
Code:
BOOL WINAPI CreateProcessA(
LPCSTR lpApplicationName,
LPTSTR lpCommandLine,
LPSECURITY_ATTRIBUTES lpProcessAttributes,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
BOOL bInheritHandles,
DWORD dwCreationFlags,
LPVOID lpEnvironment,
LPCTSTR lpCurrentDirectory,
LPSTARTUPINFO lpStartupInfo,
LPPROCESS_INFORMATION lpProcessInformation
);
На первый взгляд это вызов может показаться сложным, но, как и в реверс инжиниринге, мы должны всегда разбивать вещи на более мелкие части, что бы понять общую картину. Мы будем иметь дело только с параметрами, которые важны для создания процесса испод отладчика, а именно: lpApplicationName, lpCommandLine, dwCreationFlags, lpStartupInfo и lpProcessInformation. Остальные параметры могут быть установлены в NULL. Для полного ознакомления с вызовом этой функции, обратитесь к записи в Microsoft Developer Network (MSDN). Первые два параметра используются для установки пути к исполняемому файлу, который мы хотим запустить и установки аргументов командной строки, которые он (исполняемый файл) принимает. Параметр dwCreationFlags занимает особое значение, которое указывает на то, что процесс должен быть запущен в качестве отлаживаемого процесса. Последние два параметра являются указателями на структуры STARTUPINFO [2] и PROCESS_INFORMATION [3] соответственно, которые определяют, как процесс должен быть запущен, а так же предоставляют важную информацию, касаемо самого процесса, после того как тот был успешно запущен.
Создайте два новых файла: my_debugger.py и my_debugger_defines.py. Затем мы начнем создавать родительский класс debugger(), в который мы будем постепенно добавлять функциональность отладчика кусок за куском. В дополнение, мы поместим все: структуры, объединения и константы в файл my_debugger_defines.py, для удобства в последующего сопровождения кода.
Example 1: my_debugger_defines.py
Code:
from ctypes import *
# Let's map the Microsoft types to ctypes for clarity
WORD = c_ushort
DWORD = c_ulong
LPBYTE = POINTER(c_ubyte)
LPTSTR = POINTER(c_char)
HANDLE = c_void_p
# Constants
DEBUG_PROCESS = 0x00000001
CREATE_NEW_CONSOLE = 0x00000010
# Structures for CreateProcessA() function
class STARTUPINFO(Structure):
_fields_ = [
("cb", DWORD),
("lpReserved", LPTSTR),
("lpDesktop", LPTSTR),
("lpTitle", LPTSTR),
("dwX", DWORD),
("dwY", DWORD),
("dwXSize", DWORD),
("dwYSize", DWORD),
("dwXCountChars", DWORD),
("dwYCountChars", DWORD),
("dwFillAttribute",DWORD),
("dwFlags", DWORD),
("wShowWindow", WORD),
("cbReserved2", WORD),
("lpReserved2", LPBYTE),
("hStdInput", HANDLE),
("hStdOutput", HANDLE),
("hStdError", HANDLE),
]
class PROCESS_INFORMATION(Structure):
_fields_ = [
("hProcess", HANDLE),
("hThread", HANDLE),
("dwProcessId", DWORD),
("dwThreadId", DWORD),
]
Example 1: my_debugger.py
Code:
from ctypes import *
from my_debugger_defines import *
import sys
import time
kernel32 = windll.kernel32
class debugger():
def __init__(self):
pass
def load(self,path_to_exe):
# dwCreation flag determines how to create the process
# set creation_flags = CREATE_NEW_CONSOLE if you want
# to see the calculator GUI
creation_flags = DEBUG_PROCESS
# instantiate the structs
startupinfo = STARTUPINFO()
process_information = PROCESS_INFORMATION()
# The following two options allow the started process
# to be shown as a separate window. This also illustrates
# how different settings in the STARTUPINFO struct can affect
# the debuggee.
startupinfo.dwFlags = 0x1
startupinfo.wShowWindow = 0x0
# We then initialize the cb variable in the STARTUPINFO struct
# which is just the size of the struct itself
startupinfo.cb = sizeof(startupinfo)
if kernel32.CreateProcessA(path_to_exe,
None,
None,
None,
None,
creation_flags,
None,
None,
byref(startupinfo),
byref(process_information)):
print "[*] We have successfully launched the process!"
print "[*] The Process ID I have is: %d" % process_information.dwProcessId
else:
print "[*] Error with error code %d." % kernel32.GetLastError()
Сейчас мы напишем небольшой тест, что бы убедиться, что все работает так, как было запланировано. Назовите этот файл my_test.py и убедитесь в том, что он находится в той же папке, что и остальные файлы.
Example 1: my_test.py
Code:
import my_debugger
debugger = my_debugger.debugger()
debugger.load("C:\\WINDOWS\\system32\\calc.exe")
Если вы выполните этот файл на Python’e, либо с помощью командной строки, либо с помощью вашей IDE, то произойдет запуск заданного процесса (calc.exe), затем будет выведен отчет, с указанием идентификатора запущенного процесса (PID), после чего последует завершение работы скрипта. Если вы используете вышеуказанный пример, то вы не увидите графической оболочки калькулятора. Причина, по которой вы не увидите графический интерфейс, заключается в том, что процесс не прорисовывается на экране, поскольку он ожидает команды от отладчика для продолжения своего выполнения. У нас еще не создана логика для этого, но она скоро появится! Сейчас вы знаете, как запустить процесс готовый для отладки. Пришло время сварганить какой-нибудь код, который присоединит (attach) отладчик к запущенному процессу.
Для того, что бы подготовить процесс, к присоединению, нужно получить дескриптор процесса. Большинство функций, которые мы будем использовать, требуют действительного дескриптора процесса, помимо этого это позволяет нам узнать, можем ли мы получить доступ к процессу, прежде чем мы попытаемся его отладить. Это делается с помощью фукнции OpenProcess() [4], которая экспортируется из kernel32.dll и имеет следующий прототип:
Code:
HANDLE WINAPI OpenProcess(
DWORD dwDesiredAccess,
BOOL bInheritHandle
DWORD dwProcessId
);
Параметр dwDesiredAccess указывает, какой тип прав доступа, мы запрашиваем для объекта процесса, на который хотим получить дескриптор. Для того что бы выполнить отладку, нам нужно установить его в PROCESS_ALL_ACCESS. Параметр bInheritHandle всегда, в нашем случае, устанавливается в False, а в параметре dwProcessId задается PID процесса, на который мы хотим получить дескриптор. Если функция выполняется успешно, то она возвращает дескриптор объекта процесса.
Для присоединения к процессу, используем функцию DebugActiveProcess() [5]. Ее прототип выглядит следующим образом:
Code:
BOOL WINAPI DebugActiveProcess(
DWORD dwProcessId
);
Мы просто передаем ей PID процесса, к которому хотим присоединиться. После того, как система определяет, что у нас есть соответствующие права доступа к процессу, целевой процесс предполагает, что присоединенный процесс (к отладчику) готов обрабатывать отладочные события, после чего отказывается от управления отладчиком (прим. пер. другими словами отладчик передает управление контролируемому процессу, т.е. процесс продолжается выполняться, до возникновения отладочных событий). Отладчик ловит эти отладочные события, вызывая функцию WaitForDebugEvent() [6] в цикле. Функция выглядит следующим образом:
Code:
BOOL WINAPI WaitForDebugEvent(
LPDEBUG_EVENT lpDebugEvent,
DWORD dwMilliseconds
);
Первый параметр это указатель на структуру DEBUG_EVENT [7]. Эта структура описывает события отладки. Второй параметр, мы установим в INFINITE, поэтому вызов WaitForDebugEvent() не будет возвращаться до возникновения события.
Для каждого события, которое отладчик перехватывает, существуют связанные обработчики событий, которые выполняют некоторые типы действий прежде, чем позволить процессу продолжить свое выполнение. После того, как обработчики закончили свою работу, мы наверняка захотим продолжить выполнение процесса. Это осуществляется с помощью функции ContinueDebugEvent() [8], которая выглядит следующим образом:
Code:
BOOL WINAPI ContinueDebugEvent(
DWORD dwProcessId,
DWORD dwThreadId,
DWORD dwContinueStatus
);
Параметры dwProcessId и dwThreadId – это параметры полей в структуре DEBUG_EVENT, которая инициализируется, когда отладчик перехватывает событие отладки. Параметр dwContinueStatus сигнализирует процессу либо продолжать выполнение (DBG_CONTINUE), либо продолжать обработку исключений (DBG_EXCEPTION_NOT_HANDLED).
Единственное, что нам остается реализовать – это отсоединение (detach) от процесса. Делается это с помощью вызова функции DebugActiveProcessStop() [9], которая принимает PID процесса, от которого вы хотите отсоединиться, в качестве единственного параметра.
Давайте соберем все выше перечисленное в месте и расширим наш класс, в файле my_debugger, предоставляя ему возможность открывать и получать дескриптор процесса. Заключительной деталью реализации будет создание основного цикла для обработки отладочных событий. Откройте my_debugger.py и введите следующий код.
ВНИМАНИЕ: Все необходимые структуры, объединения и константы были определены в файле my_debugger_defines.py, исходный код которого доступен по адресу http://www.nostarch.com/ghpython.htm. Скачайте этот файл сейчас и перезапишите вашу текущую копию. В дальнейшем мы не будем рассматривать структуры, объединения и константы, поскольку вы должны хорошо себя чувствовать с ними уже сейчас.
Example 2: my_debugger.py
Code:
from ctypes import *
from my_debugger_defines import *
import sys
import time
kernel32 = windll.kernel32
class debugger():
def __init__(self):
self.h_process = None
self.pid = None
self.debugger_active = False
def load(self,path_to_exe):
...
print "[*] We have successfully launched the process!"
print "[*] The Process ID I have is: %d" % process_information.dwProcessId
# Obtain a valid handle to the newly created process
# and store it for future access
self.h_process = self.open_process(process_information.dwProcessId)
...
def open_process(self,pid):
h_process = kernel32.OpenProcess(PROCESS_ALL_ACCESS,pid,False)
return h_process
def attach(self,pid):
self.h_process = self.open_process(pid)
# We attempt to attach to the process
# if this fails we exit the call
if kernel32.DebugActiveProcess(pid):
self.debugger_active = True
self.pid = int(pid)
else:
print "[*] Unable to attach to the process."
def detach(self):
if kernel32.DebugActiveProcessStop(self.pid):
print "[*] Finished debugging. Exiting..."
return True
else:
print "There was an error"
return False
Теперь давайте, модифицируем наш тестовый файл, добавив в него тестирование новой функциональности, которую мы реализовали выше.
Example 2: my_test.py
Code:
import my_debugger
debugger = my_debugger.debugger()
pid = raw_input("Enter the PID of the process to attach to: ")
debugger.attach(int(pid))
debugger.detach()
Для проверки работоспособности этого теста выполните следующие шаги:- Запустите калькулятор. Для этого перейдите в "Пуск => Выполнить => Все программы => Стандартные => Калькулятор".
- Клацните правой кнопкой мыши на панели инструментов Windows и выберите Диспетчер задач из контекстного меню.
- Выберите вкладку Процессы.
- Если вы не видите колонки PID на дисплее, выберите "Вид => Выбрать столбцы".
- Убедитесь, что установлен флажок напротив Идентификатор процесса (PID) и нажмите OK.
- Найдите PID соответствующий calc.exe.
- Выполните скрипт my_test.py и введите PID, который вы нашли на предыдущем шаге.
- Сценарий выведет сообщение, после чего завершит свою работу.
- Теперь вы в состоянии взаимодействовать с запущенным калькулятором.
Сейчас, когда мы объяснили основы получения дескриптора процесса, создание отлаживаемого процесса и присоединение к запущенному процессу, мы готовы погрузиться в более сложные особенности, которые наш отладчик будет поддерживать.
3.2 Получение состояния регистров процессора
Отладчик должен уметь захватывать состояние регистров процессора в любое время и в любой заданной точке. Это дает нам возможность определить состояние стека, при возникновении исключения, местонахождение указателя инструкций и другую полезную информацию. Для этого, в начале, мы должны получить дескриптор выполняемого в данный момент потока, в отлаживаемой программе, что достигается за счет использования функции OpenThread() [10]. Она выглядит следующим образом.
Code:
HANDLE WINAPI OpenThread(
DWORD dwDesiredAccess,
BOOL bInheritHandle,
DWORD dwThreadId
);
Она выглядит так же, как и функция OpenProcess(), за тем лишь исключением, что вместо идентификатора процесса (PID) передается идентификатор потока (TID).
Нам нужно получить список всех потоков, которые выполняются внутри процесса, затем выбрать интересующий нас и получить правильный дескриптор на него с помощью OpenThread(). Давайте рассмотрим, как перечислить потоки в системе.
3.2.1 Перечисление потоков
Для того что бы получить состояние регистра из процесса, мы должны уметь перечислять все работающие потоки внутри процесса. Потоки – это то, что фактически выполняется в процессе. Даже, если приложение не многопоточное, оно содержит, по крайней мере, один поток – главный поток. Мы можем перечислять потоки, используя очень мощную функцию CreateToolhelp32Snapshot() [11], которая экспортируется из kernel32.dll. Эта функция позволяет получать: список процессов, потоков и загруженных модулей (DLLs), в нутрии процессов, а так же “кучу” (heap list), которую имеет процесс. Прототип функции выглядит следующим образом:
Code:
HANDLE WINAPI CreateToolhelp32Snapshot(
DWORD dwFlags,
DWORD th32ProcessID
);
Параметр dwFlags указывает, какой тип информации должна собрать функция (потоки, процессы, модули или кучи). Мы установим его в TH32CS_SNAPTHREAD, равный 0x00000004, что означает, что мы хотим собрать все потоки, зарегистрированные в текущем снимке (snaphot). Параметр th32ProcessID это просто PID процесса, для которого мы хотим сделать снимок, но он используется только для TH32CS_SNAPMODULE, TH32CS_SNAPMODULE32, TH32CS_SNAPHEAPLIST и TH32CS_SNAPALL режимов. Поэтому, определение того, принадлежит ли поток нашему процессу или нет – зависит от нас… Когда CreateToolhelp32Snapshot() выполняется успешно, она возвращает дескриптор на объект снимка, который мы используем в последующих вызовах, для сбора дополнительной информации.
Как только у нас есть список потоков из снимка, мы можем начать перечислять их. Для того, что бы начать перечисление, используем функцию Thread32First() [12], которая выглядит следующим образом:
Code:
BOOL WINAPI Thread32First(
HANDLE hSnapshot,
LPTHREADENTRY32 lpte
);
Параметр hSnapshot принимает открытый дескриптор, возвращенный из CreateToolhelp32Snapshot(), а параметр lpte является указателем на структуру THREADENTRY32 [13]. Эта структура заполняется после успешного вызова функции Thread32First() и содержит важную информацию для первого найденного потока. Структура определяется следующим образом:
Code:
typedef struct THREADENTRY32{
DWORD dwSize;
DWORD cntUsage;
DWORD th32ThreadID;
DWORD th32OwnerProcessID;
LONG tpBasePri;
LONG tpDeltaPri;
DWORD dwFlags;
};
В этой структуре есть три поля, которые нам интересны: dwSize, th32ThreadID и th32OwnerProcessID. Поле dwSize должно быть инициализировано до вызова функции Thread32First(). Для этого просто установите его значение – равное размеру самой структуры. Поле th32ThreadID это TID для потока, который уже рассматривался. Мы можем использовать этот идентификатор в качестве параметра dwThreadId, обсуждавшегося ранее в функции OpenThread(). Поле th32OwnerProcessID это PID который идентифицирует процесс, под которым был запущен поток. Для того что определить все потоки, принадлежащие процессу, нам нужно сравнить значение каждого th32OwnerProcessID с PID процесса, к которому мы присоединились (attach) или создали и если есть совпадение – мы знаем, что этим потоком владеет наша отлаживаемая программа. Как только мы собрали информацию о первом потоке, нам нужно перейти к следующему потоку в снимке (snaphot) вызвав функцию Thread32Next(). Она принимает те же параметры, что и Thread32First(), которую мы уже рассмотрели. Все что нам нужно сделать это продолжать вызывать Thread32Next() в цикле, до тех пор, пока в снимке не останется ни одного потока.
3.2.2 Собираем все вместе
Теперь, когда мы можем получить дескриптор потока, остается последний шаг, который заключается в получение всех регистров. Это можно сделать с помощью вызова GetThreadContext() [14]. Помимо этого, мы можем использовать функцию SetThreadContext() [15], что бы изменить значения, сразу после того, как мы получили допустимую запись контекста.
Code:
BOOL WINAPI GetThreadContext(
HANDLE hThread,
LPCONTEXT lpContext
);
BOOL WINAPI SetThreadContext(
HANDLE hThread,
LPCONTEXT lpContext
);
Параметр hThread – это дескриптор, возвращенный из функции OpenThread(), а параметр lpContext – это указатель на структуру CONTEXT, которая содержит значения всех регистров. Структура CONTEXT важна для понимания. Она определяется следующим образом:
Code:
typedef struct CONTEXT {
DWORD ContextFlags;
DWORD Dr0;
DWORD Dr1;
DWORD Dr2;
DWORD Dr3;
DWORD Dr6;
DWORD Dr7;
FLOATING_SAVE_AREA FloatSave;
DWORD SegGs;
DWORD SegFs;
DWORD SegEs;
DWORD SegDs;
DWORD Edi;
DWORD Esi;
DWORD Ebx;
DWORD Edx;
DWORD Ecx;
DWORD Eax;
DWORD Ebp;
DWORD Eip;
DWORD SegCs;
DWORD EFlags;
DWORD Esp;
DWORD SegSs;
BYTE ExtendedRegisters[MAXIMUM_SUPPORTED_EXTENSION];
};
Как вы можете видеть, в этот список включены все регистры, включая отладочные и сегментные регистры. Мы будем полагаться на эту структуру на протяжении оставшейся части нашего упражнения, посвященного созданию отладчика, поэтому убедитесь, что вы знакомы с ней.
Давайте вернемся к нашему старому другу my_debugger.py и немного расширим его, добавив перечисление потоков и извлечение регистров.
Example 3: my_debugger.py
Code:
class debugger():
...
def open_thread (self, thread_id):
h_thread = kernel32.OpenThread(THREAD_ALL_ACCESS, None, thread_id)
if h_thread is not None:
return h_thread
else:
print "[*] Could not obtain a valid thread handle."
return False
def enumerate_threads(self):
thread_entry = THREADENTRY32()
thread_list = []
snapshot = kernel32.CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, self.pid)
if snapshot is not None:
# You have to set the size of the struct
# or the call will fail
thread_entry.dwSize = sizeof(thread_entry)
success = kernel32.Thread32First(snapshot, byref(thread_entry))
while success:
if thread_entry.th32OwnerProcessID == self.pid:
thread_list.append(thread_entry.th32ThreadID)
success = kernel32.Thread32Next(snapshot, byref(thread_entry))
kernel32.CloseHandle(snapshot)
return thread_list
else:
return False
def get_thread_context (self, thread_id=None,h_thread=None):
context = CONTEXT()
context.ContextFlags = CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS
# Obtain a handle to the thread
if h_thread is None:
self.h_thread = self.open_thread(thread_id)
if kernel32.GetThreadContext(h_thread, byref(context)):
kernel32.CloseHandle(h_thread)
return context
else:
return False
Теперь, когда мы расширили дебаггер еще больше, давайте обновим тестовый скрипт, что бы опробовать новые возможности.
Example 3: my_test.py
Code:
import my_debugger
debugger = my_debugger.debugger()
pid = raw_input("Enter the PID of the process to attach to: ")
debugger.attach(int(pid))
list = debugger.enumerate_threads()
# For each thread in the list we want to
# grab the value of each of the registers
for thread in list:
thread_context = debugger.get_thread_context(thread)
# Now let's output the contents of some of the registers
print "[*] Dumping registers for thread ID: 0x%08x" % thread
print "[**] EIP: 0x%08x" % thread_context.Eip
print "[**] ESP: 0x%08x" % thread_context.Esp
print "[**] EBP: 0x%08x" % thread_context.Ebp
print "[**] EAX: 0x%08x" % thread_context.Eax
print "[**] EBX: 0x%08x" % thread_context.Ebx
print "[**] ECX: 0x%08x" % thread_context.Ecx
print "[**] EDX: 0x%08x" % thread_context.Edx
print "[*] END DUMP"
debugger.detach()
Когда, на этот раз, вы запустите тест, вы увидеть следующий вывод, показанный в Листинге 3-1.
Листинг 3-1: Значение регистров процессора для каждого выполняющегося потока
Code:
Enter the PID of the process to attach to: 4028
[>] Dumping registers for thread ID: 0x00000550
[**] EIP: 0x7c90eb94
[**] ESP: 0x0007fde0
[**] EBP: 0x0007fdfc
[**] EAX: 0x006ee208
[**] EBX: 0x00000000
[**] ECX: 0x0007fdd8
[**] EDX: 0x7c90eb94
[>] END DUMP
[>] Dumping registers for thread ID: 0x000005c0
[**] EIP: 0x7c95077b
[**] ESP: 0x0094fff8
[**] EBP: 0x00000000
[**] EAX: 0x00000000
[**] EBX: 0x00000001
[**] ECX: 0x00000002
[**] EDX: 0x00000003
[>] END DUMP
[>] Finished debugging. Exiting...
Не плохо, правда? Сейчас у нас есть возможность запрашивать состояние всех регистров процессора, всякий раз, когда мы этого пожелаем. Опробуйте скрипт на нескольких процессах и посмотрите, какие результаты вы получите! На данный момент, у нас реализовано ядро нашего отладчика, поэтому, пришло время добавить несколько базовых отладочных обработчиков и точек останова (breakpoints).
3.3 Реализация отладочных обработчиков событий
Для того, что бы наш отладчик принимал, определенные события, нужно установить обработчики, для каждого отладочного события, которое может возникнуть. Если вернемся к функции WaitForDebugEvent(), то мы знаем, что она возвращает заполненную структуру DEBUG_EVENT, всякий раз, когда происходить отладочное событие. Мы будем использовать информацию, содержащуюся в этой структуре, что бы определить, как обрабатывать отладочные события. Структура DEBUG_EVENT определяется следующим образом:
Code:
typedef struct DEBUG_EVENT {
DWORD dwDebugEventCode;
DWORD dwProcessId;
DWORD dwThreadId;
union {
EXCEPTION_DEBUG_INFO Exception;
CREATE_THREAD_DEBUG_INFO CreateThread;
CREATE_PROCESS_DEBUG_INFO CreateProcessInfo;
EXIT_THREAD_DEBUG_INFO ExitThread;
EXIT_PROCESS_DEBUG_INFO ExitProcess;
LOAD_DLL_DEBUG_INFO LoadDll;
UNLOAD_DLL_DEBUG_INFO UnloadDll;
OUTPUT_DEBUG_STRING_INFO DebugString;
RIP_INFO RipInfo;
}u;
};
В этой структуре много полезной информации. Поле dwDebugEventCode особенно интересно, так как оно сообщает тип события перехваченного в функции WaitForDebugEvent(). Помимо этого оно сообщает тип и значение для объединения (union) “u”. Различные отладочные события и их коды показаны в Таблице 3-1.
Таблица 3-1: Отладочные события
Проверяя значение поля dwDebugEventCode, можно сопоставить его с полученной структурой, определив значение, хранящееся в нем (поле), как определенное значение, хранящееся в объединении "u". Давайте изменим наш отладочный цикл, что бы отобразить сработавшие события отладки. Используя эту информацию, мы можем видеть события, в общем потоке, после создания или присоединения к процессу. Обновим наши скрипты my_debugger.py и my_test.py.
Example 4: my_debugger.py
Code:
...
class debugger():
def __init__(self):
self.h_process = None
self.pid = None
self.debugger_active = False
self.h_thread = None
self.context = None
...
def run(self):
# Now we have to poll the debuggee for
# debugging events
while self.debugger_active == True:
self.get_debug_event()
def get_debug_event(self):
debug_event = DEBUG_EVENT()
continue_status= DBG_CONTINUE
if kernel32.WaitForDebugEvent(byref(debug_event),INFINITE):
# Let's obtain the thread and context information
self.h_thread = self.open_thread(debug_event.dwThreadId)
self.context = self.get_thread_context(self.h_thread)
print "Event Code: %d Thread ID: %d" % (debug_event.dwDebugEventCode, debug_event.dwThreadId)
kernel32.ContinueDebugEvent( debug_event.dwProcessId, debug_event.dwThreadId, continue_status )
Example 4: my_test.py
Code:
import my_debugger
debugger = my_debugger.debugger()
pid = raw_input("Enter the PID of the process to attach to: ")
debugger.attach(int(pid))
debugger.run()
debugger.detach()
Если мы снова используем нашего старого друга calc.exe, то вывод тестового скрипта будет выглядеть следующим образом Листинг 3-2.
Листинг 3-2: Коды событий при присоединении к процессу calc.exe
Code:
Enter the PID of the process to attach to: 2700
Event Code: 3 Thread ID: 3976
Event Code: 6 Thread ID: 3976
Event Code: 6 Thread ID: 3976
Event Code: 6 Thread ID: 3976
Event Code: 6 Thread ID: 3976
Event Code: 6 Thread ID: 3976
Event Code: 6 Thread ID: 3976
Event Code: 6 Thread ID: 3976
Event Code: 6 Thread ID: 3976
Event Code: 6 Thread ID: 3976
Event Code: 2 Thread ID: 3912
Event Code: 1 Thread ID: 3912
Event Code: 4 Thread ID: 3912
Основываясь на выводе нашего скрипта – видно, что событие CREATE_PROCESS_EVENT (0x3) появилось первым, затем последовало довольно много событий LOAD_DLL_DEBUG_EVENT (0x6), после чего сработало событие CREATE_THREAD_DEBUG_EVENT (0x2). Затем появилось следующее событие EXCEPTION_DEBUG_EVENT (0x1), которое является управляемой точкой останова Windows и позволяет отладчику, перед возобновлением выполнения, изучить состояние процесса. Последний вызов EXIT_THREAD_DEBUG_EVENT (0x4) завершает выполнение потока, с TID 3912.
События исключений особенно интересны, так как подобные исключения могут включать: точки останова; нарушения прав доступа или неправильного разруливания доступа к памяти (т.е. попытка записать данные в память, предназначенную только для чтения). Все эти события важны для нас, но давайте начнем с перехвата первой управляющей точки останова Windows. Откройте my_debugger.py и вставьте следующий код.
Example 5: my_debugger.py
Code:
...
class debugger():
def __init__(self):
self.h_process = None
self.pid = None
self.debugger_active = False
self.h_thread = None
self.context = None
self.exception = None
self.exception_address = None
...
def get_debug_event(self):
debug_event = DEBUG_EVENT()
continue_status= DBG_CONTINUE
if kernel32.WaitForDebugEvent(byref(debug_event),INFINITE):
# Let's obtain the thread and context information
self.h_thread = self.open_thread(debug_event.dwThreadId)
self.context = self.get_thread_context(h_thread=self.h_thread)
print "Event Code: %d Thread ID: %d" % (debug_event.dwDebugEventCode, debug_event.dwThreadId)
# If the event code is an exception, we want to
# examine it further.
if debug_event.dwDebugEventCode == EXCEPTION_DEBUG_EVENT:
# Obtain the exception code
exception = debug_event.u.Exception.ExceptionRecord.ExceptionCode
self.exception_address = debug_event.u.Exception.ExceptionRecord.ExceptionAddress
if exception == EXCEPTION_ACCESS_VIOLATION:
print "Access Violation Detected."
# If a breakpoint is detected, we call an internal
# handler.
elif exception == EXCEPTION_BREAKPOINT:
continue_status = self.exception_handler_breakpoint()
elif exception == EXCEPTION_GUARD_PAGE:
print "Guard Page Access Detected."
elif exception == EXCEPTION_SINGLE_STEP:
print "Single Stepping."
kernel32.ContinueDebugEvent( debug_event.dwProcessId, debug_event.dwThreadId, continue_status )
...
def exception_handler_breakpoint(self):
print "[*] Inside the breakpoint handler."
print "Exception Address: 0x%08x" % self.exception_address
return DBG_CONTINUE
Если теперь перезапустить тестовый скрипт, то в его выводе вы увидите обработку программных точек останова, вызванных исключениями. Помимо этого мы создали заглушки для аппаратных точек останова (EXCEPTION_SINGLE_STEP) и точек останова на память (EXCEPTION_GUARD_PAGE). Вооружившись нашими новыми знаниями, мы можем реализовать три разных типа точек останова, с корректными обработчиками для каждой.
3.4 Всемогущий брейкпойнт
Теперь когда у нас есть основной функционал отладчика, пришло время добавить точки останова (breakpoints). Используя информацию из Главы 2, мы реализуем: программные точки останова, аппаратные точки останова и точки останова на память. Так же мы разработаем специальные обработчики для каждого типа брейкпойнтов и покажем, как корректно возобновить работу процесса после останова.
3.4.1 Программные брейкпойнты
Для того, что бы установить программный брейкпойнт (soft breakpoint), мы должны уметь читать и писать в память процесса. Делается это с помощью функций ReadProcessMemory() [16] и WriteProcessMemory() [17]. Они имею схожие прототипы:
Code:
BOOL WINAPI ReadProcessMemory(
HANDLE hProcess,
LPCVOID lpBaseAddress,
LPVOID lpBuffer,
SIZE_T nSize,
SIZE_T* lpNumberOfBytesRead
);
BOOL WINAPI WriteProcessMemory(
HANDLE hProcess,
LPCVOID lpBaseAddress,
LPCVOID lpBuffer,
SIZE_T nSize,
SIZE_T* lpNumberOfBytesWritten
);
Обе функции позволяют отладчику просматривать и изменять память отлаживаемой программы. Параметр lpBaseAddress – это адрес, откуда вы хотите начать читать или куда вы хотите начать писать данные. Параметр lpBuffer – это указатель на данные, которые содержат либо прочитанные данные, либо данные для записи. Параметр nSize – это общее количество байтов, которое вы хотите прочитать или записать.
Использую две эти функции, мы можем дать возможность нашему отладчику, довольно легко, использовать программные точки останова (soft breakpoints). Давайте, модифицируем наш основной отладочный класс, для поддержки установки и обработки программных брейкпойнтов.
Example 6: my_debugger.py
Code:
...
class debugger():
def __init__(self):
self.h_process = None
self.pid = None
self.debugger_active = False
self.h_thread = None
self.context = None
self.breakpoints = {}
...
def read_process_memory(self,address,length):
data = ""
read_buf = create_string_buffer(length)
count = c_ulong(0)
if not kernel32.ReadProcessMemory(self.h_process,
address,
read_buf,
length,
byref(count)):
return False
else:
data += read_buf.raw
return data
def write_process_memory(self,address,data):
count = c_ulong(0)
length = len(data)
c_data = c_char_p(data[count.value:])
if not kernel32.WriteProcessMemory(self.h_process,
address,
c_data,
length,
byref(count)):
return False
else:
return True
def bp_set(self,address):
if not self.breakpoints.has_key(address):
try:
# store the original byte
original_byte = self.read_process_memory(address, 1)
# write the INT3 opcode
self.write_process_memory(address, "\xCC")
# register the breakpoint in our internal list
self.breakpoints[address] = (original_byte)
except:
return False
return True
Теперь, когда у нас есть поддержка программных брейкпойнтов, нам нужно найти подходящее место для их установки. Вообще, брекпойнты устанавливаются на вызовы функций любого типа; для демонстрационного примера возьмем функцию printf() и попытаемся ее перехватить. Windows debugging API – предоставляет нам прозрачный метод определения виртуального адреса, с помощью функции GetProcAddress() [18], которая экспортируется из kernel32.dll. Единственным, основным требованием, этой функции является дескриптор модуля (на .dll или .exe файл), который содержит интересующую нас функцию и который можно получить с помощью функции GetModuleHandle() [19]. Прототипы функций GetProcAddress() и GetModuleHandle() выглядят следующим образом:
Code:
FARPROC WINAPI GetProcAddress(
HMODULE hModule,
LPCSTR lpProcName
);
HMODULE WINAPI GetModuleHandle(
LPCSTR lpModuleName
);
Делается это довольно просто: получаем дескриптор модуля, а затем ищем адрес экспортируемой функции, которая нам нужна. Давайте добавим вспомогательную функцию, в наш отладчик, что бы реализовать это. Снова вернемся скрипту my_debugger.py и добавим следующие строки:
Example 6: my_debugger.py
Code:
...
class debugger():
...
def func_resolve(self,dll,function):
handle = kernel32.GetModuleHandleA(dll)
address = kernel32.GetProcAddress(handle, function)
kernel32.CloseHandle(handle)
return address
Теперь давайте создадим второй тестовый скрипт, который будет использовать printf() в цикле. Мы определим адрес функции, а затем установим программный брейкпойнт на нее. После попадания на брейкпойнт, мы увидим некоторые данные в консоли, после чего, процесс, продолжит выполнение своего цикла. Создайте новый скрипт, назвав его printf_loop.py и поместите в него следующий код.
Example 6: printf_loop.py
Code:
from ctypes import *
import time
msvcrt = cdll.msvcrt
counter = 0
while 1:
msvcrt.printf("Loop iteration %d!\n" % counter)
time.sleep(2)
counter += 1
Теперь давайте обновим наш первый тестовый файл, что бы присоединиться к этому процессу и установим брейкпойнт на printf().
Example 6: my_test.py
Code:
import my_debugger
debugger = my_debugger.debugger()
pid = raw_input("Enter the PID of the process to attach to: ")
debugger.attach(int(pid))
printf_address = debugger.func_resolve("msvcrt.dll","printf")
print "[*] Address of printf: 0x%08x" % printf_address
debugger.bp_set(printf_address)
debugger.run()
Для того, что бы протестировать его, запустите printf_loop.py. Запишите PID (можно посмотреть с помощью диспетчера задач) для процесса python.exe. Далее, с консоли, запустите скрипт my_test.py и введите записанный PID. После чего вы должны увидеть следующий вывод, показанный в Листинге 3-3.
Листинг 3-3: Порядок отладочных событий для программного брейкпойнта
Code:
Enter the PID of the process to attach to: 4048
[>] Address of printf: 0x77c4186a
[>] Setting breakpoint at: 0x77c4186a
Event Code: 3 Thread ID: 3148
Event Code: 6 Thread ID: 3148
Event Code: 6 Thread ID: 3148
Event Code: 6 Thread ID: 3148
Event Code: 6 Thread ID: 3148
Event Code: 6 Thread ID: 3148
Event Code: 6 Thread ID: 3148
Event Code: 6 Thread ID: 3148
Event Code: 6 Thread ID: 3148
Event Code: 6 Thread ID: 3148
Event Code: 6 Thread ID: 3148
Event Code: 6 Thread ID: 3148
Event Code: 6 Thread ID: 3148
Event Code: 6 Thread ID: 3148
Event Code: 6 Thread ID: 3148
Event Code: 6 Thread ID: 3148
Event Code: 6 Thread ID: 3148
Event Code: 2 Thread ID: 3620
Event Code: 1 Thread ID: 3620
[>] Exception address: 0x7c901230
[>] Hit the first breakpoint.
Event Code: 4 Thread ID: 3620
Event Code: 1 Thread ID: 3148
[>] Exception address: 0x77c4186a
[>] Hit user defined breakpoint.
Мы видим, что функция printf() определена по адресу 0x77c4186a, поэтому устанавливаем брейкпойнт на этот адрес. Первое исключение, которое поймано, является управляющей точкой останова Windows, но когда приходит второе исключение – видно, что адрес исключения равен 0x77c4186a, что соответствует адресу функции printf(). После обработки брейкпойнта, процесс должен возобновить свою работу. Наш отладчик теперь поддерживает программные точки останова, поэтому давайте перейдем к аппаратным брейкпойнтам.
3.4.2 Аппаратные брейкпойнты
Второй тип точек останова – это аппаратные брэйкпоинты, которые предполагают установку определенных битов в отладочных регистрах процессора. Мы подробно рассмотрели их в Главе 2, поэтому давайте перейдем к деталям реализации. Самое важное, при взаимодействии с аппаратными брэйкпоинтами, это отслеживание какой из четырех доступных отладочных регистров свободен для использования, а какой уже занят и используется. Мы должны быть уверены, что всегда используем свободный слот, иначе у нас могут возникнуть проблемы, когда брейкпойнт не сработает, в то время когда мы ожидаем этого.
Давайте начнем с перечисления всех потоков в процессе, затем получим запись контекста для каждого из них. Используя полученную запись контекста, модифицируем один из регистров, между DR0 и DR3 (в зависимости от того какой из них свободен) для того, что бы поместить адрес требуемого брейкпойнта. Затем зеркально отразим соответствующие биты в регистре DR7, для включения брейкпойнта, а так же установки его типа и длины.
После того, как мы создали подпрограмму, что бы установить брейкпойнт, следующее, что нам нужно это изменить главный цикл обработки отладочных событий, чтобы мы могли правильно обработать исключение, которые выскочит на аппаратном брейкпойнте. Мы знаем, аппаратная точка останова срабатывает на INT 1, поэтому мы просто добавим еще один обработчик исключений в наш отладочный цикл. Давайте начнем с установки брейкпойнта.
Example 7: my_debugger.py
Code:
...
class debugger():
def __init__(self):
self.h_process = None
self.pid = None
self.debugger_active = False
self.h_thread = None
self.context = None
self.breakpoints = {}
self.first_breakpoint= True
self.hardware_breakpoints = {}
...
def bp_set_hw(self, address, length, condition):
# Check for a valid length value
if length not in (1, 2, 4):
return False
else:
length -= 1
# Check for a valid condition
if condition not in (HW_ACCESS, HW_EXECUTE, HW_WRITE):
return False
# Check for available slots
if not self.hardware_breakpoints.has_key(0):
available = 0
elif not self.hardware_breakpoints.has_key(1):
available = 1
elif not self.hardware_breakpoints.has_key(2):
available = 2
elif not self.hardware_breakpoints.has_key(3):
available = 3
else:
return False
# We want to set the debug register in every thread
for thread_id in self.enumerate_threads():
context = self.get_thread_context(thread_id=thread_id)
# Enable the appropriate flag in the DR7
# register to set the breakpoint
context.Dr7 |= 1 << (available * 2)
# Save the address of the breakpoint in the
# free register that we found
if available == 0:
context.Dr0 = address
elif available == 1:
context.Dr1 = address
elif available == 2:
context.Dr2 = address
elif available == 3:
context.Dr3 = address
# Set the breakpoint condition
context.Dr7 |= condition << ((available * 4) + 16)
# Set the length
context.Dr7 |= length << ((available * 4) + 18)
# Set thread context with the break set
h_thread = self.open_thread(thread_id)
kernel32.SetThreadContext(h_thread,byref(context))
# update the internal hardware breakpoint array at the used
# slot index.
self.hardware_breakpoints[available] = (address,length,condition)
return True
Вы видите, что мы выбираем свободный слот, для хранения брейкпойнта, проверяя глобальный словарь hardware_breakpoints. После того, как мы получили свободный слот, нам нужно установить адрес брейкпойнта в слот и обновить в регистре DR7 соответствующие флаги, которые позволяют включить этот самый брейкпойнт. Теперь, когда у нас есть механизм поддержки, для установки аппаратных брейкпойнтов, давайте обновим цикл обработки отладочных событий и добавим обработчик исключений поддерживающий прерывание INT 1.
Example 7: my_debugger.py
Code:
...
class debugger():
...
def get_debug_event(self):
if self.exception == EXCEPTION_ACCESS_VIOLATION:
print "Access Violation Detected."
elif self.exception == EXCEPTION_BREAKPOINT:
continue_status = self.exception_handler_breakpoint()
elif self.exception == EXCEPTION_GUARD_PAGE:
print "Guard Page Access Detected."
elif self.exception == EXCEPTION_SINGLE_STEP:
self.exception_handler_single_step()
...
def exception_handler_single_step(self):
# Comment from PyDbg:
# determine if this single step event occurred in reaction to a
# hardware breakpoint and grab the hit breakpoint.
# according to the Intel docs, we should be able to check for
# the BS flag in Dr6. but it appears that Windows
# isn't properly propagating that flag down to us.
if self.context.Dr6 & 0x1 and self.hardware_breakpoints.has_key(0):
slot = 0
elif self.context.Dr6 & 0x2 and self.hardware_breakpoints.has_key(1):
slot = 1
elif self.context.Dr6 & 0x4 and self.hardware_breakpoints.has_key(2):
slot = 2
elif self.context.Dr6 & 0x8 and self.hardware_breakpoints.has_key(3):
slot = 3
else:
# This wasn't an INT1 generated by a hw breakpoint
continue_status = DBG_EXCEPTION_NOT_HANDLED
# Now let's remove the breakpoint from the list
if self.bp_del_hw(slot):
continue_status = DBG_CONTINUE
print "[*] Hardware breakpoint removed."
return continue_status
def bp_del_hw(self,slot):
# Disable the breakpoint for all active threads
for thread_id in self.enumerate_threads():
context = self.get_thread_context(thread_id=thread_id)
# Reset the flags to remove the breakpoint
context.Dr7 &= ~(1 << (slot * 2))
# Zero out the address
if slot == 0:
context.Dr0 = 0x00000000
elif slot == 1:
context.Dr1 = 0x00000000
elif slot == 2:
context.Dr2 = 0x00000000
elif slot == 3:
context.Dr3 = 0x00000000
# Remove the condition flag
context.Dr7 &= ~(3 << ((slot * 4) + 16))
# Remove the length flag
context.Dr7 &= ~(3 << ((slot * 4) + 18))
# Reset the thread's context with the breakpoint removed
h_thread = self.open_thread(thread_id)
kernel32.SetThreadContext(h_thread,byref(context))
# remove the breakpoint from the internal list.
del self.hardware_breakpoints[slot]
return True
Тут все довольно просто: когда происходи прерывание INT 1 мы проверяем, устанавливался ли какой-нибудь отладочный регистр с аппаратным брейкпойнтом. И если отладчик обнаруживает установленную аппаратную точку останова, соответствующую адресу исключения, он обнуляет флаги в регистре DR7 и сбрасывает регистр отладки, который содержит адрес брейкпойнта. Давайте посмотрим на этот процесс в действии. Модифицируйте скрипт my_test.py, для использования аппаратных брейкпойнтов. В качестве хомячка будем использовать нашу функцию printf().
Example 7: my_test.py
Code:
import my_debugger
from my_debugger_defines import *
debugger = my_debugger.debugger()
pid = raw_input("Enter the PID of the process to attach to: ")
debugger.attach(int(pid))
printf = debugger.func_resolve("msvcrt.dll","printf")
print "[*] Address of printf: 0x%08x" % printf
debugger.bp_set_hw(printf,1,HW_EXECUTE)
debugger.run()
В этом тесте мы просто устанавливаем брейкпойнт на функцию printf() всякий раз, как она выполняется. Длина брейкпойнта всего один байт. В данном тесте импортировали файл my_debugger_defines.py. Это было сделано для того, что бы у нас был доступ к константе HW_EXECUTE, которая придает немного ясности коду.
Когда запустите скрипт – увидите вывод, представленный в Листинг 3-4.
Листинг 3-4: Порядок событий для обработки аппаратных брейкпойнтов
Code:
Enter the PID of the process to attach to: 2504
[>] Address of printf: 0x77c4186a
Event Code: 3 Thread ID: 3704
Event Code: 6 Thread ID: 3704
Event Code: 6 Thread ID: 3704
Event Code: 6 Thread ID: 3704
Event Code: 6 Thread ID: 3704
Event Code: 6 Thread ID: 3704
Event Code: 6 Thread ID: 3704
Event Code: 6 Thread ID: 3704
Event Code: 6 Thread ID: 3704
Event Code: 6 Thread ID: 3704
Event Code: 6 Thread ID: 3704
Event Code: 6 Thread ID: 3704
Event Code: 6 Thread ID: 3704
Event Code: 6 Thread ID: 3704
Event Code: 6 Thread ID: 3704
Event Code: 6 Thread ID: 3704
Event Code: 6 Thread ID: 3704
Event Code: 2 Thread ID: 2228
Event Code: 1 Thread ID: 2228
[>] Exception address: 0x7c901230
[>] Hit the first breakpoint.
Event Code: 4 Thread ID: 2228
Event Code: 1 Thread ID: 3704
[>] Hardware breakpoint removed.
Основываясь на выводе скрипта – можно видеть, что после срабатывания исключения обработчик удаляет брейкпойнт. Цикл продолжит выполняться после того, как отработает обработчик. Теперь у нас есть поддержка программных и аппаратных брейкпойнтов, давайте завершим создание нашего отладчика, добавив поддержку брейкпойнтов на память.
3.4.3 Брейкпоинты на память
Заключительная функцией, которую мы реализуем, будет брейкпойнт на память. В начале, мы просто запросим информацию о разделе памяти, что бы определить, где находится ее базовый адрес (где в виртуальной памяти начинается страница). После того, как мы определили размер страницы, нам нужно установить права доступа на эту страницу, что бы обеспечить ее охрану. Когда процессор попытается получить доступ к этой памяти, сработает исключение GUARD_PAGE_EXCEPTION. Используя специфический обработчик, для данного исключения, мы вернем оригинальный права доступа для страницы и продолжим выполнение.
Для того, что бы правильно рассчитать размер страницы, которой мы собираемся манипулировать, нужно вначале обратиться с запросом к операционной системе, что бы получить размер страницы по умолчанию. Это делается с помощью функции GetSystemInfo() [20], которая заполняет структуру SYSTEM_INFO [21]. Эта структура содержит поле dwPageSize, в котором находится правильный, для системы, размер страницы. Мы реализуем этот шаг, во время инициализации первого экземпляра класса debugger().
Example 8: my_debugger.py
Code:
...
class debugger():
def __init__(self):
self.h_process = None
self.pid = None
self.debugger_active = False
self.h_thread = None
self.context = None
self.breakpoints = {}
self.first_breakpoint= True
self.hardware_breakpoints = {}
# Here let's determine and store
# the default page size for the system
system_info = SYSTEM_INFO()
kernel32.GetSystemInfo(byref(system_info))
self.page_size = system_info.dwPageSize
...
Теперь, когда получен размер страницы по умолчанию, мы готовы обращаться к странице и манипулировать ее правами. Первый шаг, заключается в запросе страницы, на адрес которой мы хотели бы установить брейкпойнт. Это делается с помощью функции VirtualQueryEx() [22], которая заполняет структуру MEMORY_BASIC_INFORMATION [23] характерными значениями для страницы, которую мы запрашиваем. Ниже приводятся определения для функции и структуры:
Code:
SIZE_T WINAPI VirtualQuery(
HANDLE hProcess,
LPCVOID lpAddress,
PMEMORY_BASIC_INFORMATION lpBuffer,
SIZE_T dwLength
);
typedef struct MEMORY_BASIC_INFORMATION{
PVOID BaseAddress;
PVOID AllocationBase;
DWORD AllocationProtect;
SIZE_T RegionSize;
DWORD State;
DWORD Protect;
DWORD Type;
}
Как только структура была заполнена, будем использовать значение в поле BaseAddress, как начальную точку для установки прав доступа на страницу. Для установки правд доступа используется функция VirtualProtectEx() [24], которая имеет следующий прототип:
Code:
BOOL WINAPI VirtualProtectEx(
HANDLE hProcess,
LPVOID lpAddress,
SIZE_T dwSize,
DWORD flNewProtect,
PDWORD lpflOldProtect
);
Итак, давайте перейдем к коду. Мы собираемся создать глобальный список защищенных страниц, которые нам нужно явно установить, так же как и глобальный список брейкпойнтов, на адреса в памяти, которые наш обработчик исключений будет использовать, когда произойдет исключение GUARD_PAGE_EXCEPTION. Затем установим права доступа на адрес и окружающие его страницы памяти (если адрес находится между двумя или более страницами памяти).
Example 8: my_debugger.py
Code:
...
class debugger():
def __init__(self):
...
self.guarded_pages = []
self.memory_breakpoints = {}
...
def bp_set_mem (self, address, size):
mbi = MEMORY_BASIC_INFORMATION()
# If our VirtualQueryEx() call doesn’t return
# a full-sized MEMORY_BASIC_INFORMATION
# then return False
if kernel32.VirtualQueryEx(self.h_process,
address,
byref(mbi),
sizeof(mbi)) < sizeof(mbi):
return False
current_page = mbi.BaseAddress
# We will set the permissions on all pages that are
# affected by our memory breakpoint.
while current_page <= address + size:
# Add the page to the list; this will
# differentiate our guarded pages from those
# that were set by the OS or the debuggee process
self.guarded_pages.append(current_page)
old_protection = c_ulong(0)
if not kernel32.VirtualProtectEx(self.h_process,
current_page,
size,
mbi.Protect | PAGE_GUARD,
byref(old_protection)):
return False
# Increase our range by the size of the
# default system memory page size
current_page += self.page_size
# Add the memory breakpoint to our global list
self.memory_breakpoints[address] = (address, size, mbi)
return True
Теперь у вас есть возможность устанавливать брэйкпоинты на память. Если вы опробуете их, в текущем состоянии, используя нашу функцию printf() зацикленную в цикле, вы получите вывод, который будет просто говорить "Guard Page Access Detected". Хорошо то, что когда к защищенной странице получают доступ и срабатывает исключение, операционная система на самом деле удаляет защиту, установленную на страницу памяти, и позволяет вам продолжить выполнение. Подобное действие избавляет вас от создания специфического обработчика решающего эту задачу. Однако вы могли бы создать дополнительную логику, в существующем отладочном цикле, для выполнения определенных действий, когда срабатывает брейкпойнт. Например, вы могли бы осуществить такие действия, как: восстановление брейкпойнта, чтение памяти из места, где был установлен брэйкпоинт, приготовление свежего кофе, почесывание бороды – в общем, все что угодно.
3.5 Заключение
На этом мы завершаем разработку простого отладчика под Windows. На данный момент, вы имеете не только надежные знания для создания отладчика, но также получили некоторые очень важные навыки, которые будут полезны вам не зависимо от того занимаетесь ли вы отладкой или нет! При использовании другого отладочного инструмента, вы сможете понять, что тот делает на низком уровне. Помимо этого вы будете знать как, при необходимости, наилучшим образом, модифицировать его, конкретно под ваши нужды.
Следующие шаги будут состоять в том, что бы продемонстрировать некоторые возможности, продвинутого использования, двух сформировавшихся и стабильных отладочных платформ под Windows: PyDbg и Immunity Debugger. Вы получили большое количество информации о том, как PyDbg работает "под капотом", поэтому будете чувствовать себя комфортно, переходя к нему. Синтаксис Immunity Debugger немного отличается, но он предлагает существенно отличающийся набор функций. Понимание того, как использовать и тот и другой отладчик, для специфических задач отладки, очень важно для вас, поскольку позволит вам автоматизировать отладку. Дорогу осилит лишь тот, кто шагает! Поэтому вперед, вперед и только в перед! Переходим к PyDbg…
3.6 Дополнительные материалы от переводчика
Для самых ленивых я разбил исходные коды, приведенные в книге, на Example (Примеры). Все это добро вы можете скачать тут. Успехов =)
© Translated by Prosper-H from r0 Crew