INT3断点,简单地说就是将你要断下的指令地址处的第一个字节设置为0xCC,软件执行到0xCC(对应汇编指令INT3)时,会触发异常代码为EXCEPTION_BREAKPOINT的异常。这样我们的调试程序就能够接收到这个异常,然后进行相应的处理。
INT3断点的管理:
在我的程序中,INT3断点的信息结构体如下:
struct stuPointInfo
{
PointType ptType; //断点类型
int nPtNum; //断点序号
LPVOID lpPointAddr; //断点地址
BOOL isOnlyOne; //是否一次性断点(针对INT3断点)
char chOldByte; //原先的字节(针对INT3断点)
};
而每一个INT3断点信息结构体指针又保存到一个链表中。
INT3断点的设置:
设置INT3断点比较简单,只需要根据用户输入的断点地址和断点类型(是否一次性断点),将被调试进程中对应地址处的字节替换为0xCC,同时将原来的字节保存到INT3断点信息结构体中,并将该结构体的指针加入到断点链表中。
一款好的软件,无论大小,必然在程序的逻辑上要求严谨无误。调试器的设计也不例外,如果在被调试的某地址处已经存在一个同样的断点了,那么用户 还要往这个地址上设置相同的断点,则必然会因为重复设置断点导致错误。例如这里的INT3断点,如果不对用户输入的地址进行是否重复的检查,而让用户在同 一个地址先后下了两次INT3断点,则后一次INT3断点会误以为这里本来的字节就是0xCC。所以在设置INT3断点之前应该先看该地址是否已经下过 INT3断点,如果该地址已经存在一个INT3断点,且是非一次性的,则不能再在此地址下INT3断点,如果该地址有一个INT3一次性断点,而用户要继 续下一个INT3非一次性断点,则将原来存在的INT3断点的属性从一次性改为非一次性断点。
以下是设置INT3断点的一些关键代码:
//在断点列表中查找是否已经存在此处的一般断点
stuPointInfo tempPointInfo;
stuPointInfo* pResultPointInfo = NULL;
memset(&tempPointInfo, 0, sizeof(stuPointInfo));
tempPointInfo.lpPointAddr = lpAddr;
tempPointInfo.ptType = ORD_POINT;
//判断所下的INT3断点是否是一次性断点
if (stricmp(pCmd->chParam2, "once") == 0)
{
tempPointInfo.isOnlyOne = TRUE;
}
else
{
tempPointInfo.isOnlyOne = FALSE;
}
//如果查找到在要设置INT3断点的地址处已经存在INT3断点
if (FindPointInList(tempPointInfo, &pResultPointInfo, FALSE))
{
if (tempPointInfo.isOnlyOne == FALSE)//要设置的是非一次性断点
{
if (pResultPointInfo->isOnlyOne == FALSE)//查找到的是非一次性断点
{
printf("This Ordinary BreakPoint is already exist!\r\n");
}
else//查找到的是一次性断点
{
//将查找到的断点属性改为非一次性断点
pResultPointInfo->isOnlyOne = FALSE;
}
}
return FALSE;
}
//要设置INT3断点的位置不存在INT3断点(也就是说,该地址可以设置INT3断点)
char chOld;
char chCC = 0xcc;
DWORD dwOldProtect;
//先读出原先的字节
bRet = ReadProcessMemory(m_hProcess, lpAddr, &chOld, 1, NULL);
if (bRet == FALSE)
{
printf("ReadProcessMemory error! may be is not a valid memory address!\r\n");
return FALSE;
}
//将要设置INT3断点的地址处的字节改为0xCC
bRet = WriteProcessMemory(m_hProcess, lpAddr, &chCC, 1, NULL);
if (bRet == FALSE)
{
printf("WriteProcessMemory error!\r\n");
return FALSE;
}
//将该INT3断点信息结构体添加到断点链表中
stuPointInfo* NewPointInfo = new stuPointInfo;
memset(NewPointInfo, 0, sizeof(stuPointInfo));
NewPointInfo->nPtNum = m_nOrdPtFlag;
m_nOrdPtFlag++;
NewPointInfo->ptType = ORD_POINT;
NewPointInfo->lpPointAddr = lpAddr;
NewPointInfo->u.chOldByte = chOld;
NewPointInfo->isOnlyOne = tempPointInfo.isOnlyOne;
g_ptList.push_back(NewPointInfo);
INT3断点被断下的处理:
INT3断点被断下后,首先从断点链表中找到对应的断点信息结构体。如果没有找到,则说明该INT3断点不是用户下的断点,调试器不做处理,交 给系统去处理(其他类型的断点触发异常也需要做同样的处理)。如果找到对应的断点,根据断点信息将断下地址处的字节还原为原来的字节,并将被调试进程的 EIP减一,因为INT3异常被断下后,被调试进程的EIP已经指向了INT3指令后的下一条指令,所以为了让被调试进程执行本来需要执行的指令,应该让 其EIP减1。
如以下代码:
地址 机器码 汇编代码
01001959 55 push ebp
0100195A 33ED xor ebp,ebp
0100195C 3BCD cmp ecx,ebp
0100195E 896C24 04 mov dword ptr ss:[esp+4],ebp
当用户在0100195A地址处设置INT3断点后,0100195A处的字节将改变为0xCC(原先是0x33)。
此时对应的代码如下:
地址 机器码 汇编代码
01001959 55 push ebp
0100195A CC int3
0100195B ED in eax,dx
0100195C 3BCD cmp ecx,ebp
0100195E 896C24 04 mov dword ptr ss:[esp+4],ebp
当被调试程序执行到0100195A地址处,触发异常,进入异常处理程序后,获取被调试线程的环境(GetThreadContext),可看 出此时EIP指向了0100195B,也就是INT3指令之后,所以我们除了要恢复0xCC为原来的字节之外,还要将被调试线程的EIP减一,让EIP指 向0100195A。否则CPU就会执行0100195B处的指令(0100195B ED in eax,dx),显然这是错误的。
如果查找到的断点信息显示该INT3断点是一个非一次性断点,那么需要设置单步,然后在进入单步后将这一个断点重新设置上(硬件执行断点和内存 断点如果是非一次性的也需要做相同的处理)。因为INT3断点同时只会断下一个,所以可以用一个临时变量保存要重新设置的INT3断点的地址,然后用一个 BOOL变量表示当前是否有需要重新设置的INT3断点。
关于INT3断点的一些细节:
1. 创建调试进程后,为了能够让被调试程序断在OEP(程序入口点),我们可以在被调试程序的OEP处下一个一次性INT3断点。
2. 在创建调试进程的过程中(程序还没有执行到OEP处),会触发一个ntdll.dll中的INT3,遇到这个断点直接跳出不处理。这个断点在使用微 软自己的调试工具WinDbg时会被断下,可以猜测,微软设置这个断点是为了能够在程序到达OEP之前就被断下,方便用户做一些处理(如设置各种断点)。
3. 因为INT3断点修改了被调试程序的代码内容,所以在进行反汇编和显示被调试进程内存数据的时候,需要检查碰到的0xCC字节是否是用户所下的 INT3断点,如果是需要替换为原来的字节,再做相应的反汇编和显示数据工作。这一点olldbg做的很不错,而有一些国产的调试器好像没有注意到这些小 的细节。
本系列文章参考书目、资料如下:
1.《加密与解密3》 编著:段钢
2.《调试寄存器(DRx)理论与实践》 作者:Hume/冷雨飘心
3.《数据结构》 作者:严蔚敏