0%

ctypes学习笔记三——断点

ctypes学习笔记三 ——断点

捕获调试事件

前面实现了获取CPU寄存器状态的方法,接下来可以继续对进程调试事件进行处理。

首先进入进程,然后等待并获取调试事件,用到的API如下:

1
2
3
4
BOOL WaitForDebugEvent(
LPDEBUG_EVENT lpDebugEvent,
DWORD dwMilliseconds
);

这个函数一般是调试器主循环的组成部分。

调试事件信息定义在一个结构体里面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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值反映了被捕获的事件类型。

dwDebugEventCode值的定义如下:

Value Meaning
CREATE_PROCESS_DEBUG_EVENT
3
报告创建过程调试事件。u.CreateProcessInfo的值指定一个CREATE_PROCESS_DEBUG_INFO 结构。
CREATE_THREAD_DEBUG_EVENT
2
报告创建线程调试事件。u.CreateThread的值指定CREATE_THREAD_DEBUG_INFO 结构。
EXCEPTION_DEBUG_EVENT
1
报告异常调试事件。u.Exception的值指定 EXCEPTION_DEBUG_INFO结构。
EXIT_PROCESS_DEBUG_EVENT
5
报告退出进程调试事件。u.ExitProcess的值指定一个EXIT_PROCESS_DEBUG_INFO 结构。
EXIT_THREAD_DEBUG_EVENT
4
报告退出线程调试事件。u.ExitThread的值指定一个 EXIT_THREAD_DEBUG_INFO结构。
LOAD_DLL_DEBUG_EVENT
6
报告加载动态链接库(DLL)调试事件。u.LoadDll的值 指定 LOAD_DLL_DEBUG_INFO结构。
OUTPUT_DEBUG_STRING_EVENT
8
报告输出调试字符串调试事件。u.DebugString的值 指定OUTPUT_DEBUG_STRING_INFO 结构。
RIP_EVENT
9
报告RIP调试事件(系统调试错误)。u.RipInfo的值 指定RIP_INFO结构。
UNLOAD_DLL_DEBUG_EVENT
7
报告一个卸载DLL调试事件。u.UnloadDll的值指定一个 UNLOAD_DLL_DEBUG_INFO结构。

通过上面的事件代码,我们就可以获取被调试进程的运行过程、当前发生的事件信息,方便后续的调试工作。

1
2
3
4
5
BOOL ContinueDebugEvent(
DWORD dwProcessId,
DWORD dwThreadId,
DWORD dwContinueStatus
);

ContinueDebugEvent()函数使得被调试事件中断的线程继续运行,直到捕获下一个调试事件。

其中,dwContinueStatus参数指定了下一次调试事件发生时的报告选项。

Value Meaning
DBG_CONTINUE
0x00010002L
如果dwThreadId参数指定的线程先前报告了EXCEPTION_DEBUG_EVENT调试事件,则该函数将停止所有异常处理并继续执行该线程,并将异常标记为已处理。对于任何其他调试事件,此标志仅继续执行线程。
DBG_EXCEPTION_NOT_HANDLED
0x80010001L
如果dwThreadId指定的线程先前报告了EXCEPTION_DEBUG_EVENT调试事件,则该函数将继续异常处理。如果这是第一次命中异常事件,则使用结构化异常处理程序的搜索和调度逻辑;否则,该过程终止。对于任何其他调试事件,此标志仅继续执行线程。
DBG_REPLY_LATER
0x40010001L
在Windows 10版本1507或更高版本中受支持,此标志使dwThreadId在目标继续后重播现有的Breaking事件。通过针对dwThreadId调用SuspendThread API ,调试器可以恢复进程中的其他线程,然后返回中断。

下面是一个附加进程后打印调试事件的例子:

1
2
3
4
5
6
7
8
import my_debugger

debugger = my_debugger.debugger()
pid = input("Enter the PID of the process to attach to:")

debugger.attach(int(pid)) # attach the process
debugger.run()
debugger.detach()

调用API的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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),100):
# grab various information with regards to the current exception.
self.h_thread = self.open_thread(debug_event.dwThreadId)
self.context = self.get_thread_context(h_thread=self.h_thread)
self.debug_event = debug_event


print("Event Code: %d Thread ID: %d" % \
(debug_event.dwDebugEventCode,debug_event.dwThreadId))

if debug_event.dwDebugEventCode == EXCEPTION_DEBUG_EVENT:
self.exception = debug_event.u.Exception.ExceptionRecord.ExceptionCode
self.exception_address = debug_event.u.Exception.ExceptionRecord.ExceptionAddress

# call the internal handler for the exception event that just occured.
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()

kernel32.ContinueDebugEvent(debug_event.dwProcessId, debug_event.dwThreadId, continue_status)

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Enter the PID of the process to attach to:18248
Event Code: 3 Thread ID: 20692
Event Code: 6 Thread ID: 20692
Event Code: 2 Thread ID: 18492
Event Code: 2 Thread ID: 22368
Event Code: 2 Thread ID: 21472
Event Code: 6 Thread ID: 20692
Event Code: 6 Thread ID: 20692
Event Code: 6 Thread ID: 20692
Event Code: 6 Thread ID: 20692
Event Code: 6 Thread ID: 20692
Event Code: 6 Thread ID: 20692
Event Code: 6 Thread ID: 20692
Event Code: 6 Thread ID: 20692
Event Code: 6 Thread ID: 20692
Event Code: 2 Thread ID: 13260
Event Code: 1 Thread ID: 13260
[*] Inside the breakpoint handler.
[*] Exception address: 0x77943f80
Event Code: 4 Thread ID: 13260
Event Code: 4 Thread ID: 18492
Event Code: 4 Thread ID: 22368
Event Code: 4 Thread ID: 21472

值得注意的是其中的EXCEPTION_DEBUG_EVENT (0x1)例外事件,它由 windows 设置的断点所引发,允许在 进程启动前观察进程的状态。

最后的几个事件是EXIT_THREAD_DEBUG_EVENT (0x4),它是当detach()函数被调用时,进程结束自身产生。


断点

断点类型 描述
软件断点 是用的最多的断点类型,软件断点的本质是利用一个单字节指令(INT3 中断事件),
用于暂停被调试的程序,然后将控制权转义给调试器的断点处理函数。
软件断点会改变程序运行的内存数据,同时也改变了软件的CRC校验和。
因此当被调试软件会检验CRC校验和的时候,调试行为只能用硬件断点实现。
硬件断点 硬件断点是CPU级别的断点,用到了特定的寄存器:调试寄存器。
调试器被专门用于调试事件,一个CPU一般会有8个调试寄存器。硬件断点一般被用于:
1. 当特定的地址上有指令执行的时候中断
2. 当特定的地址上有数据可以写入的时候
3. 当特定的地址上有数据读或者写但不执行的时候
内存断点 内存断点通过改变内存页面的访问权限,然后通过跟踪被保护页的访问异常,
进一步确定程序对内存的访问行为,进而确认程序对数据的操作。
内存断点可以监控内存中数据的变化。

详细介绍在这里

以下分别对以上三种断点进行处理:


软件断点:

需要用到两个函数:

1
2
3
4
5
6
7
BOOL ReadProcessMemory(
HANDLE hProcess,
LPCVOID lpBaseAddress,
LPVOID lpBuffer,
SIZE_T nSize,
SIZE_T *lpNumberOfBytesRead
);
1
2
3
4
5
6
7
BOOL WriteProcessMemory(
HANDLE hProcess,
LPVOID lpBaseAddress,
LPCVOID lpBuffer,
SIZE_T nSize,
SIZE_T *lpNumberOfBytesWritten
);

这两个函数可以读取或修改被调试进程的内存,参数定义如下:

hProcess具有正在读取的内存的进程的句柄。句柄必须具有对进程的PROCESS_VM_READ访问权限。

lpBaseAddress指向要从中读取的指定进程中的基地址的指针。在进行任何数据传输之前,系统会验证基址和指定大小的内存中的所有数据都可以访问以进行读取访问,如果无法访问,则该功能将失败。

lpBuffer指向缓冲区的指针,该缓冲区从指定进程的地址空间接收内容。

nSize从指定进程中读取的字节数。

lpNumberOfBytesRead指向变量的指针,该变量接收传输到指定缓冲区的字节数。如果lpNumberOfBytesReadNULL,则忽略该参数。

以下以调试printf()函数为例:

首先需要确定一个函数的虚地址,这里用到了GetProcAddress()这个API:

1
2
3
4
FARPROC GetProcAddress(
HMODULE hModule,
LPCSTR lpProcName
);

参数hMoudle为包函数或变量(待调试函数)的模块的句柄。

这个句柄可以通过GetModuleHandle()获取。

1
2
3
HMODULE GetModuleHandleA(
LPCSTR lpModuleName
);

这里有一个坑:

MSDN官方文档中有这么一句话:

检索指定模块的模块句柄。该模块必须已由调用进程加载。

为避免在“备注”部分中描述的竞争条件,请使用 GetModuleHandleEx函数。

返回的句柄不是全局的或可继承的。它不能被其他进程复制或使用。

就是说待获取的模块必须是已经被进程加载的模块(好像是句废话233333),重点是后面,**返回的句柄不能被其余进程复制或使用。**即是说当前调试的句柄只在当前有效。

ok,准备就绪,下面贴软件断点调试需要用到的调用代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# 读取目标地址的数据
def read_process_memory(self,address,length):

data = b""
read_buf = create_string_buffer(length)
count = c_ulong(0)

# kernel32.ReadProcessMemory(self.h_process, address, read_buf, 5, byref(count))
# data = read_buf.raw

# return data
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):
print("[*] Setting breakpoint at: 0x%08x" % address)
# 原先的has_key()方法在python3中被移除了
#首先判断该地址是否在断点列表中
if not (address in self.breakpoints):

# 读取并保存该断点地址的原数据
old_protect = c_ulong(0)
kernel32.VirtualProtectEx(self.h_process, address, 1, PAGE_EXECUTE_READWRITE, byref(old_protect))

original_byte = self.read_process_memory(address, 1)
if original_byte != False:

# write the INT3 opcode
if self.write_process_memory(address, b"\xCC"): # 这里也是bytes

# register the breakpoint in our internal list
self.breakpoints[address] = (original_byte)
return True
else:
return False

# 获取函数在模块中的地址
def func_resolve(self,dll,function):

handle = kernel32.GetModuleHandleA(dll)
address = kernel32.GetProcAddress(handle, function)

kernel32.CloseHandle(handle)
return address

软件断点调试printf()函数的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# my_test.py
import my_debugger

debugger = my_debugger.debugger()
pid = input("Enter the PID of the process to attach to:")
debugger.attach(int(pid))

# 这里还是要使用bytes传参。
printf_address = debugger.func_resolve(b"msvcrt.dll", b"printf")
print("[*] Address of printf: 0x%08x" % printf_address)

debugger.bp_set(printf_address)

debugger.run()
# debugger.detach()
# input("")

运行printf_loop.py,并查找到该进程的PID

1
2
3
4
5
6
7
8
9
10
11
# printf_loop.py
from ctypes import *
import time

msvcrt = cdll.msvcrt
counter = 0

while 1:
msvcrt.printf(b"Loop iteration %d\n" % counter)
time.sleep(2)
counter += 1

软件断点调试的流程如下:

首先通过func_resolve()函数在被附加调试的进程的模块中找到printf()函数的虚拟地址,然后bp_set()函数下断点,run()函数监听调试事件。

下断点的过程如下:

首先读取目标地址的原数据并保存,然后向目标地址写入INT3断点指令,并等待调试事件发生即可。

命中断点之后,将之前保存的原数据写回,并将EIP寄存器值-1,使程序正常执行。

调试效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Enter the PID of the process to attach to:2348
[*] Address of printf: 0x765a4f70
[*] Setting breakpoint at: 0x765a4f70
Event Code: 3 Thread ID: 18548
Event Code: 6 Thread ID: 18548
Event Code: 2 Thread ID: 25680
Event Code: 2 Thread ID: 2636
Event Code: 2 Thread ID: 20208
Event Code: 6 Thread ID: 18548
Event Code: 6 Thread ID: 18548
Event Code: 2 Thread ID: 7376
Event Code: 1 Thread ID: 18548
[*] Exception address: 0x765a4f70
[*] Hit user defined breakpoint.
Event Code: 1 Thread ID: 7376
[*] Exception address: 0x77943f80
[*] Hit the first breakpoint.
Event Code: 4 Thread ID: 7376
Event Code: 4 Thread ID: 20208

硬件断点

硬件断点使用专门的CPU调试寄存器实现,这里务必要记住的是:

当我们使用硬件断点的时候,要跟踪四个可用的调试寄存器哪个是可用的哪个已经被使用了。必须确保我们使用的那个寄存器是空的,否则硬件断点就不能在我们希望的地方触发。

硬件断点流程如下:

首先枚举进程中的所有线程,然后获取其CPU寄存器值,并定义调试寄存器DR0-DR3的值,写入断点地址,然后更新DR7标志位,开启硬件断点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
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 0 not in self.hardware_breakpoints:
available = 0
elif 1 not in self.hardware_breakpoints:
available = 1
elif 2 not in self.hardware_breakpoints:
available = 2
elif 3 not in self.hardware_breakpoints:
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 this threads context with the debug registers
# 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

def exception_handler_single_step(self):
print("[*] Exception address: 0x%08x" % self.exception_address)
# Comment from PyDbg:
# determine if this single step event occured 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 propogating that flag down to us.
if self.context.Dr6 & 0x1 and (0 in self.hardware_breakpoints):
slot = 0
elif self.context.Dr6 & 0x2 and (1 in self.hardware_breakpoints):
slot = 1
elif self.context.Dr6 & 0x4 and (2 in self.hardware_breakpoints):
slot = 2
elif self.context.Dr6 & 0x8 and (3 in self.hardware_breakpoints):
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# my_test.py
import my_debugger
from my_debugger_defines import *

debugger = my_debugger.debugger()
pid = input("Enter the PID of the process to attach to:")
debugger.attach(int(pid))

# 这里还是要使用bytes传参。
printf_address = debugger.func_resolve(b"msvcrt.dll", b"printf")
print("[*] Address of printf: 0x%08x" % printf_address)

debugger.bp_set_hw(printf_address,1,HW_EXECUTE)

debugger.run()
# debugger.detach()
# input("")

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Enter the PID of the process to attach to:6192
[*] Address of printf: 0x765a4f70
Event Code: 3 Thread ID: 24232
Event Code: 6 Thread ID: 24232
Event Code: 2 Thread ID: 20596
Event Code: 2 Thread ID: 3520
Event Code: 2 Thread ID: 24228
Event Code: 6 Thread ID: 24232
Event Code: 6 Thread ID: 24232
Event Code: 6 Thread ID: 24232
Event Code: 6 Thread ID: 24232
Event Code: 6 Thread ID: 24232
Event Code: 6 Thread ID: 24232
Event Code: 6 Thread ID: 24232
Event Code: 2 Thread ID: 22640
Event Code: 1 Thread ID: 24232
[*] Exception address: 0x765a4f70
[*] Hardware breakpoint removed.
Event Code: 1 Thread ID: 22640
[*] Exception address: 0x77943f80
[*] Hit the first breakpoint.
Event Code: 4 Thread ID: 22640
Event Code: 4 Thread ID: 20596
Event Code: 4 Thread ID: 3520
Event Code: 4 Thread ID: 24228

内存断点

内存断点设置流程如下:

首先查询一个内存块以并找到基地址(页面在虚拟内存中的起始地址)。一旦确定了页面大小,接着就设置页面权限,使其成为保护(guard)页。当CPU尝试访问这块内存时,就会抛出一个GUARD_PAGE_EXCEPTION异常。我们用对应的异常处理函数,将页面权限恢复到以前,最后让程序继续执行。

首先需要获取操作系统的内存页默认大小:

1
2
3
void GetSystemInfo(
LPSYSTEM_INFO lpSystemInfo
);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
typedef struct _SYSTEM_INFO {
union {
DWORD dwOemId;
struct {
WORD wProcessorArchitecture;
WORD wReserved;
} DUMMYSTRUCTNAME;
} DUMMYUNIONNAME;
DWORD dwPageSize;
LPVOID lpMinimumApplicationAddress;
LPVOID lpMaximumApplicationAddress;
DWORD_PTR dwActiveProcessorMask;
DWORD dwNumberOfProcessors;
DWORD dwProcessorType;
DWORD dwAllocationGranularity;
WORD wProcessorLevel;
WORD wProcessorRevision;
} SYSTEM_INFO, *LPSYSTEM_INFO;

具体文档在这里

获取默认页面大小之后,接下来使用VirtualQueryEx()函数查询对应页面信息:

1
2
3
4
5
6
SIZE_T VirtualQueryEx(
HANDLE hProcess,
LPCVOID lpAddress,
PMEMORY_BASIC_INFORMATION lpBuffer,
SIZE_T dwLength
);

其中:

1
2
3
4
5
6
7
8
9
typedef struct _MEMORY_BASIC_INFORMATION {
PVOID BaseAddress;
PVOID AllocationBase;
DWORD AllocationProtect;
SIZE_T RegionSize;
DWORD State;
DWORD Protect;
DWORD Type;
} MEMORY_BASIC_INFORMATION, *PMEMORY_BASIC_INFORMATION;

具体文档在这里

其中BaseAddress参数的值就是我们目标页的开始地址,使用VirtualProtectEx()函数对这个地址设置保护权限即可。

1
2
3
4
5
6
7
BOOL VirtualProtectEx(
HANDLE hProcess,
LPVOID lpAddress,
SIZE_T dwSize,
DWORD flNewProtect,
PDWORD lpflOldProtect
);

Debugger实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
def bp_set_mem (self, address, size):

mbi = MEMORY_BASIC_INFORMATION()

# Attempt to discover the base address of the memory page
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

def get_debug_event(self):

debug_event = DEBUG_EVENT()
continue_status = DBG_CONTINUE

if kernel32.WaitForDebugEvent(byref(debug_event),100):
# grab various information with regards to the current exception.
self.h_thread = self.open_thread(debug_event.dwThreadId)
self.context = self.get_thread_context(h_thread=self.h_thread)
self.debug_event = debug_event


print("Event Code: %d Thread ID: %d" % \
(debug_event.dwDebugEventCode,debug_event.dwThreadId))

if debug_event.dwDebugEventCode == EXCEPTION_DEBUG_EVENT:
self.exception = debug_event.u.Exception.ExceptionRecord.ExceptionCode
self.exception_address = debug_event.u.Exception.ExceptionRecord.ExceptionAddress

# call the internal handler for the exception event that just occured.
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()

kernel32.ContinueDebugEvent(debug_event.dwProcessId, debug_event.dwThreadId, continue_status)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# my_test.py
import my_debugger
from my_debugger_defines import *

debugger = my_debugger.debugger()
pid = input("Enter the PID of the process to attach to:")
debugger.attach(int(pid))

# 这里还是要使用bytes传参。
printf_address = debugger.func_resolve(b"msvcrt.dll", b"printf")
print("[*] Address of printf: 0x%08x" % printf_address)


# debugger.bp_set_hw(printf_address,1,HW_EXECUTE)
debugger.bp_set_mem(printf_address, 10)

debugger.run()
# debugger.detach()
# input("")

运行效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Enter the PID of the process to attach to:18932
[*] Address of printf: 0x765a4f70
Event Code: 3 Thread ID: 4304
Event Code: 6 Thread ID: 4304
Event Code: 6 Thread ID: 4304
Event Code: 6 Thread ID: 4304
Event Code: 2 Thread ID: 11536
Event Code: 1 Thread ID: 11536
[*] Exception address: 0x77943f80
[*] Hit the first breakpoint.
Event Code: 4 Thread ID: 11536
Event Code: 1 Thread ID: 4304
Guard Page Access Detected.
Event Code: 2 Thread ID: 25076

可见内存断点触发成功。

这里没有移除对内存的保护,但是程序也可以继续执行,是因为当CPU访问被保护的页面时,系统会抛出GUARD_PAGE_EXCEPTION异常,然后系统会自动将保护移除。

所以我们以后要处理内存断点的时候,只需要捕获GUARD_PAGE_EXCEPTION异常,并进行相应的处理就行了。

至此调试器的部分就基本理清了,可以基于本文中实现的调试功能,进一步组合处理,实现一些自动化分析过程。p52