调试处理流程:
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;
}
// 处理用户输入的函数,完成目的3
void 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; // 跳出循环,返回到框架的第三层中
}
}
} |