调试处理流程:
TF置1 -> 执行代码 -> CPU产生中断 -> IDT函数被调用 -> 操纵系统进行异常分发 ->
调试器子系统发送调试事件 -> 调试器得到EXCEPTION_DEBUG_EVENT异常事件 -> 调试器显示反汇编信息
一、实现单步断点
单步断点是靠CPU中的标志寄存器的TF标志位实现的,将线程环境中的TF标志位设置为1即可。(TF:调试标志位。当TF=1时,处理器每次只执行一条指令,即单步执行)
若被调试历程有多个线程,仅设置此中一个,极大概率会出现TF断点无效的情况
解决方法:设置所有线程的环境块,或获取当前产生异常的线程的环境块。
[C] 纯文本查看 复制代码 //获取线程环境块 CONTEXT ct = { 0 }; ct.ContextFlags = CONTEXT_CONTROL; GetThreadContext(hThread, &ct); //将TF标志位置置1 PREG_EFLAGS pEflags = (PREG_EFLAGS)&ct.EFlags; pEflags->TF = 1; //设置线程环境块 SetThreadContext(hThread, &ct);
二、实现软件断点
软件断点实质上就是利用int3指令实现的,当CPU执行int3指令时,就会产生一个陷阱类异常,int3指令对应的机器码为0xCC,设置软件断点就是将0xCC写入到必要设置断点的位置,当CPU执行到软件断点后,就会产生陷阱类异常,调试器就可以或许接收到异事件。
在下软件断点之前,应先读取一个字节将其保存起来,再写入软件断点(即写入0xCC),中断之后,将原来的一个字节数据写回内存中去,再EIP减1,得到异常真正产生的地点。
[C] 纯文本查看 复制代码 //读取历程内存,保存一个字节的数据 DWORD dwSize = 0; if (!ReadProcessMemory(hProcess, pAddress, oldByte, 1, &dwSize)) { return FALSE; } //写入一个字节,\xcc就是int3指令的机器码 BYTE cc = '\xcc'; if (!WriteProcessMemory(hProcess, pAddress, &cc, 1, &dwSize)) { return FALSE; } return TRUE;
移除软件断点
[C] 纯文本查看 复制代码 DWORD dwSize = 0; return WriteProcessMemory(hProcess, pAddress, &oldByte, 1, &dwSize);
三、实现硬件断点
实现硬件断点,必要设置调试寄存器,将断点的地点设置到DR0~DR3中,将断点长度设置到DR7的LEN0~LEN3中,将断点类型设置到DR7的RW0~RW3中,将是否启用断点设置到DR7的L0~L3中。
[C] 纯文本查看 复制代码typedef struct _DBG_REG7 //调试寄存器DR7的位段信息结构体{ // 局部断点(L0~3)与全局断点(G0~3)的标记位 unsigned L0 : 1; // 对Dr0保存的地点启用 局部断点 unsigned G0 : 1; // 对Dr0保存的地点启用 全局断点 unsigned L1 : 1; // 对Dr1保存的地点启用 局部断点 unsigned G1 : 1; // 对Dr1保存的地点启用 全局断点 unsigned L2 : 1; // 对Dr2保存的地点启用 局部断点 unsigned G2 : 1; // 对Dr2保存的地点启用 全局断点 unsigned L3 : 1; // 对Dr3保存的地点启用 局部断点 unsigned G3 : 1; // 对Dr3保存的地点启用 全局断点 // LE,GE【已经弃用】用于低落CPU频率,以方便准确检测断点异常 unsigned LE : 1; // 保留字段 unsigned GE : 1; // 保留字段 unsigned Reserve1 : 3; // 保护调试寄存器标志位,如果此位为1,则有指令修改条是寄存器时会触发异常 unsigned GD : 1; // 保留字段 unsigned Reserve2 : 2; // 保存Dr0~Dr3地点所指向位置的断点类型(RW0~3)与断点长度(LEN0~3),状态形貌如下: unsigned RW0 : 2; // 设定Dr0指向地点的断点类型 unsigned LEN0 : 2; // 设定Dr0指向地点的断点长度 unsigned RW1 : 2; // 设定Dr1指向地点的断点类型 unsigned LEN1 : 2; // 设定Dr1指向地点的断点长度 unsigned RW2 : 2; // 设定Dr2指向地点的断点类型 unsigned LEN2 : 2; // 设定Dr2指向地点的断点长度 unsigned RW3 : 2; // 设定Dr3指向地点的断点类型 unsigned LEN3 : 2; // 设定Dr3指向地点的断点长度}DBG_REG7, * PDBG_REG7;
1.设置硬件执行断点
[C] 纯文本查看 复制代码 CONTEXT ct = { CONTEXT_DEBUG_REGISTERS }; //获取线程环境块 GetThreadContext(hThread, &ct); DBG_REG7* pDr7 = (DBG_REG7*)&ct.Dr7; if (pDr7->L0 == 0) //DR0没有被利用 { ct.Dr0 = uAddress; pDr7->RW0 = 0; pDr7->LEN0 = 0; //长度域设置为0 pDr7->L0 = 0; //启用第0个断点 } else if (pDr7->L1 == 0) //DR1没有被利用 { ct.Dr1 = uAddress; pDr7->RW1 = 0; pDr7->LEN1 = 0; //长度域设置为0 pDr7->L1 = 0; //开启第1个断点 } else if (pDr7->L2 == 0) //DR2没有被利用 { ct.Dr2 = uAddress; pDr7->RW2 = 0; pDr7->LEN2 = 0; //长度域设置为0 pDr7->L2 = 0; //开启第2个断点 } else if (pDr7->L3 == 0) //DR3没有被利用 { ct.Dr3 = uAddress; pDr7->RW3 = 0; pDr7->LEN3 = 0; //长度域设置为0 pDr7->L3 = 0; //开启第3个断点 } else { return FALSE; } SetThreadContext(hThread, &ct); return TRUE;
2.设置硬件读写断点
[C] 纯文本查看 复制代码 //获取线程环境块 CONTEXT ct = { 0 }; ct.ContextFlags = CONTEXT_DEBUG_REGISTERS; GetThreadContext(hThread, &ct); //对地点和长度进行对齐处理(向上取整) if (dwLen == 1) //2字节的对齐粒度 { uAddress = uAddress - uAddress % 2; } else if (dwLen == 3) //4字节的对齐粒度 { uAddress = uAddress - uAddress % 4; } else if (dwLen > 3) { return FALSE; } //判定哪些寄存器没有被利用 DBG_REG7* pDr7 = (DBG_REG7*)&ct.Dr7; if (pDr7->L0 == 0) //DR0没有被利用 { ct.Dr0 = uAddress; pDr7->RW0 = type; pDr7->LEN0 = dwLen; } else if (pDr7->L1 == 0) //DR1没有被利用 { ct.Dr1 = uAddress; pDr7->RW1 = type; pDr7->LEN1 = dwLen; } else if (pDr7->L2 == 0) //DR2没有被利用 { ct.Dr2 = uAddress; pDr7->RW2 = type; pDr7->LEN2 = dwLen; } else if (pDr7->L3 == 0) //DR3没有被利用 { ct.Dr3 = uAddress; pDr7->RW3 = type; pDr7->LEN3 = dwLen; } else { return FALSE; } SetThreadContext(hThread, &ct); return TRUE;
四、实现内存访问断点
内存访问断点就是利用内存访问异常,当程序访问一个没有任何访问权限的内存分页时,就是产生内存访问异常,如果想要下执行断点,就可以将这个地点的所在的内存分页设置为没有任何访问权限,读写断点同理。
[C] 纯文本查看 复制代码 VirtualProtectEx(process, LPVOID((DWORD)address & 0xfffff000), 0x1000, dwNewProtect, &dwOldProtect);
产生内存访问异常时,表示异常信息的EXCEPTION_RECORD结构体将内存访问异常的详细信息保存再ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS]数组中,当产生的是内存访问异常时,数组的第0个元素保存的是内存访问异常的详细异常方式,保存0时表示读取时异常,保存1时表示写入时异常,保存8时表示执行时异常,第二个元素保存的是发生异常的线性虚拟地点。
五、组合断点
1.API断点
API断点即得到函数名对应的地点,然后对该地点下一个软件断点
获取函数名对应地点的方法:
①遍历所有模块导出表,匹配函数名
②利用调试符处理器,通过符号名得到地点
这里利用通过符号名,获取对应地点的方法。
[C] 纯文本查看 复制代码 char buffer[sizeof(SYMBOL_INFO) + MAX_SYM_NAME * sizeof(TCHAR)]; PSYMBOL_INFO pSymbol = (PSYMBOL_INFO)buffer; pSymbol->SizeOfStruct = sizeof(SYMBOL_INFO); pSymbol->MaxNameLen = MAX_SYM_NAME; //根据名字查询符号信息,输出到pSymbol中 if (!SymFormName(hProcess,pszName,pSymbol) { return 0; } //返回函数地点 return (SIZE_T)pSymbol->Address;
2.单步步过断点
单步步过断点 = TF断点 + 软件断点
①如果当前要执行的指令不是CALL或者REP,则利用TF断点
②如果是CALL或者REP,则在当前指令的下一条指令下一个软件断点,运行调试历程后就能使其在当前指令的下一条指令处断下,以达到单步步过的目的。
附:调试器三层架构
1.1 建立调试循环的目的有以下3点
1.1.1 为了可以或许持续的接收到目标历程的调试事件.
1.1.2 为了可以或许在恰当的时间输出反汇编信息,线程环境块等信息
1.1.3 为了可以或许接受用户的控制
1.2 搭建调试循环的框架
1.2.1 框架第一层(完成目的1)
接收调试事件,并将调试事件交给一个函数处理,用这个函数的返回值来作为ContinueDebugEvent的第三个参数
1.2.2 框架的第二层
框架的第二层将调试事件分为两部分,历程创建和退出,线程创建和退出,DLL加载和卸载,调试字符串输出,内部错误作为一部分, 异常事件独立作为一部分
1.2.3 框架的第三层(完成目的2和3)
框架的第三次处理的是异常事件,由于异常可以细分为多种类型的,不同类型的异常的规复手段不一样,因此必要进行分类处理
别的,将信息输出给用户,接收用户的输入也是在第三层中
[C] 纯文本查看 复制代码// 框架的第一层void StartDebug(const char* pszFile /*目标历程的路径*/){ if (pszFile == nullptr) return; STARTUPINFOA stcStartupInfo = { sizeof(STARTUPINFOA) }; PROCESS_INFORMATION stcProcInfo = { 0 }; // 历程信息 /* 创建调试历程程 */ BOOL bRet = FALSE; bRet = CreateProcessA( pszFile, // 可执行模块路径 NULL, // 下令行 NULL, // 安全形貌符 NULL, // 线程属性是否可继续 FALSE, // 否从调用历程处继续了句柄 DEBUG_ONLY_THIS_PROCESS | CREATE_NEW_CONSOLE, // 以调试的方式启动 NULL, // 新历程的环境块 NULL, // 新历程的当前工作路径(当前目录) &stcStartupInfo, // 指定历程的主窗口特性 &stcProcInfo // 接收新历程的辨认信息 ); /*建立调试循环*/ DEBUG_EVENT dbgEvent = { 0 }; DWORD dwRet = DBG_CONTINUE; while (1) { /*框架的第一层*/ WaitForDebugEvent(&dbgEvent, -1);// 等待调试事件 dwRet = DispatchEvent(&dbgEvent); // 分发调试事件,进入框架的第二层 ContinueDebugEvent( dbgEvent.dwProcessId, dbgEvent.dwThreadId, dwRet);// 复兴调试事件的处理效果,如果不复兴,目标历程将会一直处于暂停状态. }}// 框架的第二层DWORD DispatchEvent(DEBUG_EVENT* pDbgEvent){ // 框架的第二层 // 第二层框架将调试事件分为两部分来处理 DWORD dwRet = 0; switch (pDbgEvent->dwDebugEventCode) { // 第一部分是异常调试事件 case EXCEPTION_DEBUG_EVENT: dwRet = DispatchException(&pDbgEvent->u.Exception); //进入到第三层分发异常 return dwRet; // 返回到框架的第一层 // 第二部分是其他调试事件 default: return DBG_CONTINUE; }}// 框架的第三层DWORD DispatchException(EXCEPTION_DEBUG_INFO* pExcDbgInfo){ // 框架的第三层 // 第三层是专门负责修复异常的. // 如果是调试器自身设置的异常,那么可以修复,返回DBG_CONTINUE // 如果不是调试器自身设置的异常,那么不能修复,返回DBG_EXCEPTION_NOT_HANDLED switch (pExcDbgInfo->ExceptionRecord.ExceptionCode) { case EXCEPTION_BREAKPOINT: // 软件断点 { // 修复断点 } break; case EXCEPTION_SINGLE_STEP: // 硬件断点和TF断点 { // 修复断点 } break; case EXCEPTION_ACCESS_VIOLATION:// 内存访问断点 { // 修复断点 } break; default: return DBG_EXCEPTION_NOT_HANDLED; } UserInput(); //和用户进行交互 // 返回到框架的第二层中 return DBG_CONTINUE;}// 处理用户输入的函数,完成目的3void UserInput(){ // 输出信息,完成目的2 printf("断点在地点 % 08X上触发\n", pDbgEvent->u.Exception.ExceptionRecord.ExceptionAddress); // 输出反汇编代码 // 输出寄存器信息 // 接收用户输入,完成目的3 char buff[100]; while (1) { printf("请输入下令: "); gets_s(buff, 100); if (buff[0] == 't') // 单步步入 { } else if (strcmp(buff, "bp") == 0)// 设置断点 { } else if (buff[0] == 'g') { break; // 跳出循环,返回到框架的第三层中 } }}
来源:http://www.12558.net
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |