[调试器实现]第四章 多内存断点

内存断点通过修改内存分页的属性,使被调试程序触发内存访问、写入异常而断下。
 
多内存断点的数据关系:
 
    因为我设计的是多内存断点,即在同一个内存分页上可以下多个内存断点,同一个断点也可以跨分页下在几个内存分页上。所以从数据关系上来说断点和内存分页是多对多的关系。因此需要设计三个表:“内存断点信息表”,“内存分页属性表”,以及中间表“内存断点-分页对照表”。在用户下内存断点的时候,首先将断点所跨越的内存分页属性加入到“内存分页属性表”中。然后在中间表“内存断点-分页对照表”中添加内存断点对应的分页信息,一个内存断点对应了几个分页就会添加几条信息。内存断点的信息保存在“断点信息表”中。
三个表的属性字段如下:
名称:  2.JPG
查看次数: 1470
文件大小:  28.0 KB
 
 
内存断点的设置:
 
    内存断点的信息中需要用户输入确定的有:下断点首地址、断点的长度和断点的类型(访问还是写入)。根据用户输入 的信息可以组成一个临时的内存断点结构体,然后到内存断点链表中查找是否已经存在同属性的内存断点,如果已经存在则不需要再设置,否则可以设置这个内存断 点。
 
    设置内存断点,首先根据断点的首地址和长度可以确定断点所跨越的内存分页,用VirtualQueryEx API获取内存分页的属性,然后将内存分页的属性信息添加到“内存分页表”中(需要注意的是,如果“内存分页表”中已经存在同一内存分页的属性记录了,则不需要再添加重复的记录),同时将断点对应分页的信息添加到“内存断点-分页对照表”中,并设置断点所跨过的每一个内存分页的属性为不可访问(PAGE_NOACCESS)。
 
    这一点和OllyDbg的做法不大一样,OllyDbg设置内存访问断点是将断点所跨分页设置为PAGE_NOACCESS属性,而设置内存写入断点是将断点所跨分页属性设置为PAGE_EXECUTE_READ,而我的做法是不管哪种断点都将断点所跨内存页的属性设置为PAGE_NOACCESS,这样做的问题是会产生多余的异常,好处是设置断点,恢复断点时省去类型的判断。而且出于另外一个考虑,OllyDbg是只能设置一个内存断点的,所以它这样设置合情合理,而我设计的是多内存长度任意的断点。假设出现了用户先在某个分页上下了一个内存写入断点,之后用户又在同一个分页上下了内存访问断点,那么如果按照OllyDbg的方式,先将内存页的属性设置为PAGE_EXECUTE_READ,然后处理后一个内存断点时,将内存页的属性设置为PAGE_NOACCESS。而如果相反,出现了用户先在某个分页上下了一个内存访问断点,之后用户又在同一个分页上下了内存写入断点,内存页的属性首先被改为PAGE_NOACCESS,但不能根据第二个断点将内存页的属性改为PAGE_EXECUTE_READ,否则前一个内存访问断点就失效了。与其因设置不同的属性产生这么多种麻烦的情况,不如牺牲一点效率(多了一些异常的情况),对内存访问和写入断点都将断点所跨过的分页属性设置为PAGE_NOACCESS,再通过断点被断下后,异常记录结构体EXCEPTION_RECORD中的访问标志和断点信息中的类型标志来判断是否命中了用户所下的内存断点。
 
    处理完内存页的属性,将内存页原先属性信息、断点-分页对照信息加入对应链表之后,最后需要将断点信息添加到断点链表中。
 
关键代码如下:

 

//根据用户输入创建一个临时内存断点
 stuPointInfo tempPointInfo;
 stuPointInfo* pResultPointInfo = NULL;
 memset(&tempPointInfo, 0, sizeof(stuPointInfo));
 tempPointInfo.lpPointAddr = lpAddr;
 tempPointInfo.ptType = MEM_POINT;
 tempPointInfo.isOnlyOne = FALSE;
 
 if (stricmp("access", pCmd->chParam2) == 0)
 {
     tempPointInfo.ptAccess = ACCESS;
 } 
 else if (stricmp("write", pCmd->chParam2) == 0)
 {
     tempPointInfo.ptAccess = WRITE;
 }
 else
 {
     printf("Void access!\r\n");
     return FALSE;
 }
 
 int nLen = (int)HexStringToHex(pCmd->chParam3, TRUE);
 
 if (nLen == 0 )
 {
     printf("Point length can not set Zero!\r\n");
     return FALSE;
 }
 
 tempPointInfo.dwPointLen = nLen;
 tempPointInfo.nPtNum = m_nOrdPtFlag;
 m_nOrdPtFlag++;
 
 //查找该内存断点在断点链表中是否已经存在
 if (FindPointInList(tempPointInfo, &pResultPointInfo, FALSE))
 {
     if (pResultPointInfo->dwPointLen >= nLen)//存在同样类型且长度大于要设置断点的断点
     {
         printf("The Memory breakpoint is already exist!\r\n");
         return FALSE;
     } 
     else//查找到的断点长度小于要设置的断点长度,则删除掉找到的断点,重新设置
         //此时只需要删除断点-分页表项 和 断点表项
     {
         DeletePointInList(pResultPointInfo->nPtNum, FALSE);
     }
 }
 
 // 根据 tempPointInfo 设置内存断点
 // 添加断点链表项,添加内存断点-分页表中记录,添加分页信息表记录
 // 首先根据 tempPointInfo 中的地址和长度获得所跨越的全部分页
 
 LPVOID lpAddress = (LPVOID)((int)tempPointInfo.lpPointAddr & 0xfffff000);
 DWORD OutAddr = (DWORD)tempPointInfo.lpPointAddr + 
         tempPointInfo.dwPointLen;
 
 MEMORY_BASIC_INFORMATION mbi = {0};
 
 while ( TRUE )
 {
     if ( sizeof(mbi) != VirtualQueryEx(m_hProcess, lpAddress, &mbi, sizeof(mbi)) )
     {
         break;
     }
 
     if ((DWORD)mbi.BaseAddress >= OutAddr)
     {
         break;            
     }
 
     if ( mbi.State == MEM_COMMIT )
     {
         //将内存分页信息添加到分页表中
         AddRecordInPageList(mbi.BaseAddress, 
                             mbi.RegionSize, 
                             mbi.AllocationProtect);
         //将断点-分页信息添加到断点-分页表中
         DWORD dwPageAddr = (DWORD)mbi.BaseAddress;
         while (dwPageAddr < OutAddr)
         {
             stuPointPage *pPointPage = new stuPointPage;
             pPointPage->dwPageAddr = dwPageAddr;
             pPointPage->nPtNum = tempPointInfo.nPtNum;
             g_PointPageList.push_back(pPointPage);
             //设置该内存页为不可访问
             DWORD dwTempProtect;
             VirtualProtectEx(m_hProcess, (LPVOID)dwPageAddr,
                 1, PAGE_NOACCESS, &dwTempProtect);
 
             dwPageAddr += 0x1000;
         }
 
     }
     lpAddress = (LPVOID)((DWORD)mbi.BaseAddress + mbi.RegionSize);
     if ((DWORD)lpAddress >= OutAddr)
     {
         break;
     }
 }
 
 //断点添加到断点信息表中
 stuPointInfo *pPoint = new stuPointInfo;
 memcpy(pPoint, &tempPointInfo, sizeof(stuPointInfo));
 g_ptList.push_back(pPoint);
printf("***Set Memory breakpoint success!***\r\n");

 

 内存断点精确命中的判断思路:
 
    根据产生访问异常时,异常的类型是访问还是写入,以及异常访问的地址这两个信息到“断点-分页对照表”中去查找。如果没有找到,则说明此异常不是用户调试所下的内存断点,调试器不予处理。
 
    如果找到,再根据断点序号,到“断点信息表”中查看断点的详细信息。看断点是否准确命中(下断的内存区域,断点的类型:如果是读异常则只命中访问类型断点;如果是写异常,则访问类型、写入类型断点都算命中)。
 
    如果遍历完“断点-分页对照表”,异常访问地址只是在“断点-分页对照表”中找到,但没有精确命中内存断点,则暂时恢复内存页的原属性,并设置单步,进入单步后再恢复该内存页为不可访问。
 
    如果在“断点-分页表”中找到,且精确命中某个断点,则先暂时恢复页属性,设置单步,并等待用户输入。程序运行进入单步后,再设置内存页属性为不可访问。
 
内存断点的处理:
 
    当被调试程序触发访问异常时,异常事件被调试器接收到,分析此时的异常结构体如下:
 

struct _EXCEPTION_RECORD { 
DWORD ExceptionCode; 
DWORD ExceptionFlags; 
struct _EXCEPTION_RECORD *ExceptionRecord; 
PVOID ExceptionAddress; 
DWORD NumberParameters; 
ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS]; 
}

 我们考察其中最后一个成员ExceptionInformation数组的数据,MSDN上的说明如下:
The first element of the array contains a read-write flag that indicates the type of operation that caused the access violation. If this value is zero, the thread attempted to read the inaccessible data. If this value is 1, the thread attempted to write to an inaccessible address. 
The second array element specifies the virtual address of the inaccessible data.
 
    即:数组的第一个元素ExceptionInformation[0]包含了表示引发访问违规操作类型的读写标志。如果该标志为0,表示线程试图读一个不可访问地址处的数据;如果该标志是1,表示线程试图写数据到一个不可访问的地址。数组的第二个元素ExceptionInformation[1]指定了不可访问的地址。
 
    根据这两个信息,我们就可以利用上面提到的内存断点精确命中的判断思路,来判断是否命中用户所下的内存断点,以及做出对应的处理。
 
整个模块如下:

 

//处理访问异常部分
BOOL CDoException::DoAccessException()
{
BOOL bRet;
DWORD dwAccessAddr; //读写地址
DWORD dwAccessFlag; //读写标志
BOOL isExceptionFromMemPoint = FALSE; //异常是否由内存断点设置引起,默认为否
stuPointInfo* pPointInfo = NULL; //命中的断点
BOOL isHitMemPoint = FALSE; //是否精确命中断点
 
dwAccessFlag = m_DbgInfo.ExceptionRecord.ExceptionInformation[0];
dwAccessAddr = m_DbgInfo.ExceptionRecord.ExceptionInformation[1];
//根据 访问地址 到“断点-分页表”中去查找
//同一个内存分页可能有多个断点
//如果没有在“断点-分页表”中查找到,则说明这个异常不是断点引起的
list<stuPointPage*>::iterator it = g_PointPageList.begin();
int nSize = g_PointPageList.size();
 
//遍历链表中每个节点,将每个匹配的“断点-分页记录”都添加到g_ResetMemBp链表(需要重设的断点的内存分页信息链表)中
for ( int i = 0; i < nSize; i++ )
{
stuPointPage* pPointPage = *it;
//如果在“断点-分页表”中查找到
//再根据断点表中信息判断是否符合用户所下断点信息
if (pPointPage->dwPageAddr == (dwAccessAddr & 0xfffff000))
{
stuResetMemBp *p = new stuResetMemBp;
p->dwAddr = pPointPage->dwPageAddr;
p->nID = pPointPage->nPtNum; 
g_ResetMemBp.push_back(p);
 
//暂时恢复内存页原来的属性
BOOL bDoOnce = FALSE;
if (!bDoOnce)
{
//这些操作只需要执行一次
bDoOnce = TRUE;
isExceptionFromMemPoint = TRUE;
//暂时恢复内存页原来属性的函数
TempResumePageProp(pPointPage->dwPageAddr);
//设置单步,在单步中将断点设回
UpdateContextFromThread();
m_Context.EFlags |= TF;
UpdateContextToThread();
}
 
//先找到断点序号对应的断点
list<stuPointInfo*>::iterator it2 = g_ptList.begin();
for ( int j = 0; j < g_ptList.size(); j++ )
{
pPointInfo = *it2;
if (pPointInfo->nPtNum == pPointPage->nPtNum)
{
break;
}
it2++;
}
 
//再判断是否符合用户所下断点信息(断点类型和断点范围是否均相符)
if (isHitMemPoint == FALSE)
{
if (dwAccessAddr >= (DWORD)pPointInfo->lpPointAddr && 
dwAccessAddr < (DWORD)pPointInfo->lpPointAddr +
pPointInfo->dwPointLen)
{
if ( pPointInfo->ptAccess == ACCESS || 
(pPointInfo->ptAccess == WRITE && dwAccessFlag == 1) )
{
isHitMemPoint = TRUE;
}
}
}
}
it++;
}
 
//如果异常不是由内存断点设置引起,则调试器不处理
if (isExceptionFromMemPoint == FALSE)
{
return FALSE;
}
 
//如果命中内存断点,则暂停,显示相关信息并等待用户输入
if (isHitMemPoint)
{
ShowBreakPointInfo(pPointInfo);
//显示反汇编代码
m_lpDisAsmAddr = m_DbgInfo.ExceptionRecord.ExceptionAddress;
ShowAsmCode();
//显示寄存器值
ShowRegValue(NULL);
 
//等待用户输入
bRet = FALSE;
while (bRet == FALSE)
{
bRet = WaitForUserInput();
}
}
return TRUE;
}

 

 内存断点需要注意的细节:
 
1. 由于内存断点将页面属性改为不可访问了,所有很多命令(如反汇编、查看数据)都需要进行修改。
 
2. 内存断点可能出现多个内存断点下在同一个分页的情况。所以在删除一个内存断点时,如果该断点对应的某个(或某几个)分页也有其他的断点,则不能将该内存分页设置回原属性。
 
本系列文章参考书目、资料如下:
 
1.《加密与解密3》 编著:段钢
2.《调试寄存器(DRx)理论与实践》 作者:Hume/冷雨飘心
3.《数据结构》 作者:严蔚敏

 

posted @ 2015-05-10 10:39  银河彼岸  阅读(761)  评论(0编辑  收藏  举报