能力值:
( LV3,RANK:30 )
26 楼
继续等待该系列的更新。。。。。。。。。。。。。。。。
能力值:
( LV9,RANK:170 )
27 楼
老孙呀,你的现金数太搞笑了, 250 Kx呀
能力值:
( LV2,RANK:10 )
28 楼
研究精神非常值得学习
能力值:
( LV8,RANK:130 )
29 楼
第四章 多内存断点
内存断点通过修改内存分页的属性,使被调试程序触发内存访问、写入异常而断下。
多内存断点的数据关系:
因为我设计的是多内存断点,即在同一个内存分页上可以下多个内存断点,同一个断点也可以跨分页下在几个内存分页上。所以从数据关系上来说断点和内存分页是多对多的关系。因此需要设计三个表:“内存断点信息表”,“内存分页属性表”,以及中间表“内存断点 -
分页对照表”。在用户下内存断点的时候,首先将断点所跨越的内存分页属性加入到“内存分页属性表”中。然后在中间表“内存断点 -
分页对照表”中添加内存断点对应的分页信息,一个内存断点对应了几个分页就会添加几条信息。内存断点的信息保存在“断点信息表”中。
三个表的属性字段如下:
内存断点的设置:
内存断点的信息中需要用户输入确定的有:下断点首地址、断点的长度和断点的类型(访问还是写入)。根据用户输入的信息可以组成一个临时的内存断点结构体,然后到内存断点链表中查找是否已经存在同属性的内存断点,如果已经存在则不需要再设置,否则可以设置这个内存断点。
设置内存断点,首先根据断点的首地址和长度可以确定断点所跨越的内存分页,用 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
中的访问标志和断点信息中的类型标志来判断是否命中了用户所下的内存断点。
处理完内存页的属性,将内存页原先属性信息、断点 -
分页对照信息加入对应链表之后,最后需要将断点信息添加到断点链表中。
关键代码如下:
//[FONT=宋体]根据用户输入创建一个临时内存断点[/FONT]
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++;
//[FONT=宋体]查找该内存断点在断点链表中是否已经存在[/FONT]
if (FindPointInList(tempPointInfo, &pResultPointInfo, FALSE))
{
if (pResultPointInfo->dwPointLen >= nLen)//[FONT=宋体]存在同样类型且长度大于要设置断点的断点[/FONT]
{
printf("The Memory breakpoint is already exist!\r\n");
return FALSE;
}
else//[FONT=宋体]查找到的断点长度小于要设置的断点长度,则删除掉找到的断点,重新设置[/FONT]
//[FONT=宋体]此时只需要删除断点[/FONT]-[FONT=宋体]分页表项[/FONT] [FONT=宋体]和[/FONT] [FONT=宋体]断点表项[/FONT]
{
DeletePointInList(pResultPointInfo->nPtNum, FALSE);
}
}
// [FONT=宋体]根据[/FONT] tempPointInfo [FONT=宋体]设置内存断点[/FONT]
// [FONT=宋体]添加断点链表项,添加内存断点[/FONT]-[FONT=宋体]分页表中记录,添加分页信息表记录[/FONT]
// [FONT=宋体]首先根据[/FONT] tempPointInfo [FONT=宋体]中的地址和长度获得所跨越的全部分页[/FONT]
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 )
{
//[FONT=宋体]将内存分页信息添加到分页表中[/FONT]
AddRecordInPageList(mbi.BaseAddress,
mbi.RegionSize,
mbi.AllocationProtect);
//[FONT=宋体]将断点[/FONT]-[FONT=宋体]分页信息添加到断点[/FONT]-[FONT=宋体]分页表中[/FONT]
DWORD dwPageAddr = (DWORD)mbi.BaseAddress;
while (dwPageAddr < OutAddr)
{
stuPointPage *pPointPage = new stuPointPage;
pPointPage->dwPageAddr = dwPageAddr;
pPointPage->nPtNum = tempPointInfo.nPtNum;
g_PointPageList.push_back(pPointPage);
//[FONT=宋体]设置该内存页为不可访问[/FONT]
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;
}
}
//[FONT=宋体]断点添加到断点信息表中[/FONT]
stuPointInfo *pPoint = new stuPointInfo;
memcpy(pPoint, &tempPointInfo, sizeof(stuPointInfo));
g_ptList.push_back(pPoint);
printf("***Set Memory breakpoint success!***\r\n");
内存断点精确命中的判断思路:
根据产生访问异常时,异常的类型是访问还是写入,以及异常访问的地址这两个信息到“断点 -
分页对照表”中去查找。如果没有找到,则说明此异常不是用户调试所下的内存断点,调试器不予处理。
如果找到,再根据断点序号,到“断点信息表”中查看断点的详细信息。看断点是否准确命中(下断的内存区域,断点的类型:如果是读异常则只命中访问类型断点;如果是写异常,则访问类型、写入类型断点都算命中)。
如果遍历完“断点 -
分页对照表”,异常访问地址只是在“断点 -
分页对照表”中找到,但没有精确命中内存断点,则暂时恢复内存页的原属性,并设置单步,进入单步后再恢复该内存页为不可访问。
如果在“断点 -
分页表”中找到,且精确命中某个断点,则先暂时恢复页属性,设置单步,并等待用户输入。程序运行进入单步后,再设置内存页属性为不可访问。
内存断点的处理:
当被调试程序触发访问异常时,异常事件被调试器接收到,分析此时的异常结构体如下:
[LEFT]
struct _EXCEPTION_RECORD {
DWORD ExceptionCode ;
DWORD ExceptionFlags ;
struct _EXCEPTION_RECORD *ExceptionRecord ;
PVOID ExceptionAddress ;
DWORD NumberParameters ;
ULONG_PTR ExceptionInformation [EXCEPTION_MAXIMUM_PARAMETERS]; [/LEFT]
}
我们考察其中最后一个成员 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]
指定了不可访问的地址。
根据这两个信息,我们就可以利用上面提到的内存断点精确命中的判断思路,来判断是否命中用户所下的内存断点,以及做出对应的处理。
整个模块如下:
//[FONT=宋体]处理访问异常部分[/FONT]
BOOL CDoException::DoAccessException()
{
BOOL bRet;
DWORD dwAccessAddr; //[FONT=宋体]读写地址[/FONT]
DWORD dwAccessFlag; //[FONT=宋体]读写标志[/FONT]
BOOL isExceptionFromMemPoint = FALSE; //[FONT=宋体]异常是否由内存断点设置引起,默认为否[/FONT]
stuPointInfo* pPointInfo = NULL; //[FONT=宋体]命中的断点[/FONT]
BOOL isHitMemPoint = FALSE; //[FONT=宋体]是否精确命中断点[/FONT]
dwAccessFlag = m_DbgInfo.ExceptionRecord.ExceptionInformation[0];
dwAccessAddr = m_DbgInfo.ExceptionRecord.ExceptionInformation[1];
//[FONT=宋体]根据[/FONT] [FONT=宋体]访问地址[/FONT] [FONT=宋体]到“断点[/FONT]-[FONT=宋体]分页表”中去查找[/FONT]
//[FONT=宋体]同一个内存分页可能有多个断点[/FONT]
//[FONT=宋体]如果没有在“断点[/FONT]-[FONT=宋体]分页表”中查找到,则说明这个异常不是断点引起的[/FONT]
list<stuPointPage*>::iterator it = g_PointPageList.begin();
int nSize = g_PointPageList.size();
//[FONT=宋体]遍历链表中每个节点,将每个匹配的“断点[/FONT]-[FONT=宋体]分页记录”都添加到[/FONT]g_ResetMemBp[FONT=宋体]链表(需要重设的断点的内存分页信息链表)中[/FONT]
for ( int i = 0; i < nSize; i++ )
{
stuPointPage* pPointPage = *it;
//[FONT=宋体]如果在“断点[/FONT]-[FONT=宋体]分页表”中查找到[/FONT]
//[FONT=宋体]再根据断点表中信息判断是否符合用户所下断点信息[/FONT]
if (pPointPage->dwPageAddr == (dwAccessAddr & 0xfffff000))
{
stuResetMemBp *p = new stuResetMemBp;
p->dwAddr = pPointPage->dwPageAddr;
p->nID = pPointPage->nPtNum;
g_ResetMemBp.push_back(p);
//[FONT=宋体]暂时恢复内存页原来的属性[/FONT]
BOOL bDoOnce = FALSE;
if (!bDoOnce)
{
//[FONT=宋体]这些操作只需要执行一次[/FONT]
bDoOnce = TRUE;
isExceptionFromMemPoint = TRUE;
//[FONT=宋体]暂时恢复内存页原来属性的函数[/FONT]
TempResumePageProp(pPointPage->dwPageAddr);
//[FONT=宋体]设置单步,在单步中将断点设回[/FONT]
UpdateContextFromThread();
m_Context.EFlags |= TF;
UpdateContextToThread();
}
//[FONT=宋体]先找到断点序号对应的断点[/FONT]
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++;
}
//[FONT=宋体]再判断是否符合用户所下断点信息(断点类型和断点范围是否均相符)[/FONT]
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++;
}
//[FONT=宋体]如果异常不是由内存断点设置引起,则调试器不处理[/FONT]
if (isExceptionFromMemPoint == FALSE)
{
return FALSE;
}
//[FONT=宋体]如果命中内存断点,则暂停,显示相关信息并等待用户输入[/FONT]
if (isHitMemPoint)
{
ShowBreakPointInfo(pPointInfo);
//[FONT=宋体]显示反汇编代码[/FONT]
m_lpDisAsmAddr = m_DbgInfo.ExceptionRecord.ExceptionAddress;
ShowAsmCode();
//[FONT=宋体]显示寄存器值[/FONT]
ShowRegValue(NULL);
//[FONT=宋体]等待用户输入[/FONT]
bRet = FALSE;
while (bRet == FALSE)
{
bRet = WaitForUserInput();
}
}
return TRUE;
}
内存断点需要注意的细节:
1. 由于内存断点将页面属性改为不可访问了,所有很多命令(如反汇编、查看数据)都需要进行修改。
2. 内存断点可能出现多个内存断点下在同一个分页的情况。所以在删除一个内存断点时,如果该断点对应的某个(或某几个)分页也有其他的断点,则不能将该内存分页设置回原属性。
本系列文章参考书目、资料如下:
1.《加密与解密3》 编著:段钢
2.《调试寄存器(DRx)理论与实践》 作者:Hume/冷雨飘心
3.《数据结构》 作者:严蔚敏
上传的附件:
能力值:
( LV2,RANK:10 )
30 楼
牛B,厉害!!!!!!!!!!
能力值:
( LV2,RANK:10 )
31 楼
大牛,膜拜下
能力值:
( LV2,RANK:10 )
32 楼
感谢分享,支持一个。
能力值:
( LV2,RANK:10 )
33 楼
支持,不忽悠
能力值:
( LV8,RANK:130 )
34 楼
第五章 单步异常的处理
因为在调试器的设计与实现中,很多关键性的操作都是在单步异常处理中完成的,故本章重点论述在单步异常中的处理。首先我们来看看会有哪些情况导致调试器进入单步异常。
进入单步异常的原因:
1. 用户输入了单步进入的命令,调试器需要设置单步,让被调试程序单步执行。
2. 用户所下的 INT3 断点被断下后,调试器会暂时恢复 INT3 断点处的字节为原有的字节,并让被调试线程的 EIP 减一,为了重新设置这个 INT3 断点,调试器自己设置了单步。
3. 用户所下的硬件断点被断下时,会触发单步异常。
4. 用户所下的硬件执行断点被断下后,调试器会暂时取消掉该硬件执行断点,以便被调试进程顺利跑下去,为了重新设置该硬件执行断点,调试器自己设置了单步。
5. 用户所下的内存断点被断下后,调试器会暂时恢复产生访问异常的内存分页为原来的属性,以便被调试进程顺利跑下去,为了重新设置该内存分页的属性(以便内存断点继续起作用),调试器自己设置了单步。
单步异常的处理:
从以上所述各点来看,进入单步的原因有三种,一是用户需要单步步入运行程序;二是调试器需要重新设置之前被临时取消的断点而设置了单步;三是硬件断点被断下时触发的单步。当然也有可能几种原因同时存在。所以我们需要几个 BOOL 变量来表明是否有需要重设的断点。 INT3 断点对应一个 BOOL 变量,硬件执行断点对应一个 BOOL 变量,是否是用户输入的单步步入命令对应一个 BOOL 变量。另外,进入单步后还需要检查线程环境中的 Dr6 的低 4 位是否有值为 1 的位,如果有那么进入单步的原因之一是因为触发了硬件断点,此时需要进一步判断该硬件断点是否是硬件执行断点,如果是硬件执行断点需要做相应的处理(具体处理方法见《调试器实现(第三章)硬件断点》)。
多断点重合的情况:
当用户对代码段的同一个地址(指令首字节)即设置了硬件执行断点,又设置了 INT3 断点,同时还设置了内存访问断点,此时会先触发硬件执行断点,然后会触发内存访问断点,最后会触发 INT3 断点。如果用户不想在同一个地址被多个断点断下多次,可以在相应的异常中做判断,先临时取消掉同一地址处的其他类型的断点,然后设置一个单步,进入单步后再把前面取消的断点再重新设置上。
处理单步异常的模块代码:
//[FONT=宋体]处理单步异常[/FONT]
BOOL CDoException::DoStepException()
{
BOOL bRet;
DWORD dwDr6 = 0; //[FONT=宋体]硬件调试寄存器[/FONT]Dr6[FONT=宋体]的值[/FONT]
DWORD dwDr6Low = 0; //[FONT=宋体]硬件调试寄存器[/FONT]Dr6[FONT=宋体]低[/FONT]4[FONT=宋体]位的值[/FONT]
stuPointInfo tempPointInfo;
stuPointInfo* pResultPointInfo = NULL;
char CodeBuf[24] = {0};
UpdateContextFromThread();
//[FONT=宋体]是否需要重设[/FONT]INT3[FONT=宋体]断点[/FONT]
if (m_isNeedResetPoint == TRUE)
{
m_isNeedResetPoint = FALSE;
char chCC = (char)0xcc;
bRet = WriteProcessMemory(m_hProcess, m_pFindPoint->lpPointAddr,
&chCC, 1, NULL);
if (bRet == FALSE)
{
printf("WriteProcessMemory error!\r\n");
return FALSE;
}
}
//[FONT=宋体]是否需要重设硬件断点[/FONT]
if (m_isNeedResetHardPoint == TRUE)
{
m_Context.Dr7 |= (int)pow(4, m_nNeedResetHardPoint);
UpdateContextToThread();
m_isNeedResetHardPoint = FALSE;
}
dwDr6 = m_Context.Dr6;
dwDr6Low = dwDr6 & 0xf; //[FONT=宋体]取低[/FONT]4[FONT=宋体]位[/FONT]
//[FONT=宋体]如果是由硬件断点触发的单步,需要用户输入才能继续[/FONT]
//[FONT=宋体]另外,如果是硬件执行断点,则需要先暂时取消断点,设置单步,下次再恢复断点[/FONT]
if (dwDr6Low != 0)
{
ShowHardwareBreakpoint(dwDr6Low);
m_nNeedResetHardPoint = log(dwDr6Low)/log(2)+0.5;//[FONT=宋体]加[/FONT]0.5[FONT=宋体]是为了四舍五入[/FONT]
//[FONT=宋体]判断由[/FONT] dwDr6Low [FONT=宋体]指定的[/FONT]DRX[FONT=宋体]寄存器,是否是执行断点[/FONT]
if((m_Context.Dr7 << (14 - (m_nNeedResetHardPoint * 2))) >> 30 == 0)
{
switch (m_nNeedResetHardPoint)
{
case 0:
m_Context.Dr7 &= 0xfffffffe;
break;
case 1:
m_Context.Dr7 &= 0xfffffffb;
break;
case 2:
m_Context.Dr7 &= 0xffffffef;
break;
case 3:
m_Context.Dr7 &= 0xffffffbf;
break;
default:
printf("Error!\r\n");
}
m_Context.EFlags |= TF;
UpdateContextToThread();
m_isNeedResetHardPoint = TRUE;
}
m_isUserInputStep = TRUE; //[FONT=宋体]这个设置只是为了能够等待用户输入[/FONT]
}
if (m_isUserInputStep == FALSE)
{
//[FONT=宋体]重设内存断点[/FONT]
ResetMemBp();
return TRUE;
}
//[FONT=宋体]以下代码在用户输入为[/FONT] "T" [FONT=宋体]命令、或硬件断点触发时执行[/FONT]
//[FONT=宋体]如果此处有[/FONT]INT3[FONT=宋体]断点,则需要先暂时删除[/FONT]INT3[FONT=宋体]断点[/FONT]
//[FONT=宋体]这样做是为了在用户输入“[/FONT]T[FONT=宋体]”命令、或硬件断点触发时忽略掉[/FONT]INT3[FONT=宋体]断点[/FONT]
//[FONT=宋体]以免在一个地方停下两次[/FONT]
memset(&tempPointInfo, 0, sizeof(stuPointInfo));
tempPointInfo.lpPointAddr = m_DbgInfo.ExceptionRecord.ExceptionAddress;
tempPointInfo.ptType = ORD_POINT;
if (FindPointInList(tempPointInfo, &pResultPointInfo, TRUE))
{
//[FONT=宋体]非一次性断点,才需要重设断点[/FONT]
if (pResultPointInfo->isOnlyOne == FALSE)
{
m_Context.EFlags |= TF;
UpdateContextToThread();
m_isNeedResetPoint = TRUE;
}
else//[FONT=宋体]一次性断点,从链表里面删除[/FONT]
{
delete[] m_pFindPoint;
g_ptList.erase(m_itFind);
}
WriteProcessMemory(m_hProcess, m_pFindPoint->lpPointAddr,
&(m_pFindPoint->u.chOldByte), 1, NULL);
if (bRet == FALSE)
{
printf("WriteProcessMemory error!\r\n");
return FALSE;
}
}
m_lpDisAsmAddr = m_DbgInfo.ExceptionRecord.ExceptionAddress;
m_isUserInputStep = FALSE;
//[FONT=宋体]更新[/FONT]m_Context[FONT=宋体]为现在的环境值[/FONT]
UpdateContextFromThread();
//[FONT=宋体]显示汇编代码和寄存器信息[/FONT]
ShowAsmCode();
ShowRegValue(NULL);
//[FONT=宋体]重设内存断点[/FONT]
ResetMemBp();
//[FONT=宋体]等待用户输入[/FONT]
bRet = FALSE;
while (bRet == FALSE)
{
bRet = WaitForUserInput();
}
return TRUE;
}
本系列文章参考书目、资料如下:
1.《加密与解密3》 编著:段钢
2.《调试寄存器(DRx)理论与实践》 作者:Hume/冷雨飘心
3.《数据结构》 作者:严蔚敏
能力值:
( LV2,RANK:10 )
35 楼
期待,是非常相当期待,又多了一个选择。
能力值:
( LV2,RANK:10 )
36 楼
来膜拜强人的
能力值:
( LV3,RANK:20 )
37 楼
顶起来!!!
能力值:
( LV2,RANK:10 )
38 楼
绝对偶像!!
膜拜~~~
能力值:
( LV6,RANK:90 )
39 楼
五期大牛的作品
能力值:
( LV9,RANK:250 )
40 楼
我说呢 找了半天没找到 O(∩_∩)O哈哈~ Up Up ~~
能力值:
( LV2,RANK:10 )
41 楼
发现一个问题,为何查找元素是否存在不使用映射表而使用链表遍历,遍历效率明显不高嘛
能力值:
( LV2,RANK:10 )
42 楼
支持楼主, 顶一下
能力值:
( LV8,RANK:130 )
43 楼
说的很有道理,可惜我不会啊。
能力值:
( LV2,RANK:10 )
44 楼
大牛的网名居然就是我的真名。。。
能力值:
( LV8,RANK:130 )
45 楼
调试器实现(第六章) 功能扩展
前面几章基本上已经将调试器的基本功能及其实现过程讲述的差不多了。本章作为一个结束,将补充一些前面没有提到的细节性问题,并就调试器的功能扩展做一些探讨。
单步步过的实现:
单步步过对于非CALL的指令,其实和单步步入一样,遇到CALL指令的时候我的处理方式是在CALL之后的指令首地址设置一个一次性的INT3断点,这一点和OllyDbg略有差异。OllyDbg的做法是看当前的4个硬件调试寄存器中是否有空闲可用的,如果有就设置一个一次性的硬件执行断点,断点地址为CALL指令后的下一条指令首地址,如果没有可用的硬件调试寄存器,才使用下一次性INT3断点的方式。因为下硬件断点比INT3断点效率高,所以OllyDbg优先使用硬件断点。
显示系统API、DLL导出函数的实现:
大家在使用OD的时候,其API提示功能用的都很爽吧。显示DLL导出函数的方法,可以是先遍历所有DLL的导出函数,将函数名称和函数地址放入一个链表中,反汇编过程中遇到地址值或寄存器值直接查链表匹配API;或者反过来,反汇编过程中遇到地址值或寄存器值到对应DLL的导出表中去查是否有匹配的函数地址。由于代码比较长,且都是不停地查导出表的过程,我就不贴完整的代码了。我把我写的含有寄存器的表达式转化为对应数值的函数贴出来,让大家帮我看看是否还有更好的处理方式,我总觉得我的处理方法比较冗长,应该还有更好的处理方法。
// 有寄存器参与的CALL指令,将寄存器表达式转化为数值
// 参数 pAddr 可能为以下情况的字符串:
// eax
// eax+3
// eax*4
// eax*4+ebx
// eax*8+1000
// eax+ebx+3000
// ebx+eax*2+F10000
int CDoException::ExpressionToInt(char *pAddr)
{
char chNewBuf[30] = {0};
int nRetValue = 0;
//先找有没有 * 号
BOOL isFindMultiplicationSign = FALSE; //是否找到乘号
BOOL isFindPlusSign = FALSE; //是否找到加号
int nLen = strlen(pAddr);
int nMultiplicationPos; //找到的乘号位置下标
for ( nMultiplicationPos = 0; nMultiplicationPos < nLen; nMultiplicationPos++)
{
if (pAddr[nMultiplicationPos] == '*')
{
isFindMultiplicationSign = TRUE;
break;
}
}
if (isFindMultiplicationSign == TRUE)
{
//从乘号向前找,直到遇到加号或找到头
int nTemp = nMultiplicationPos;
while (nTemp > 0 && pAddr[nTemp] != '+')
{
nTemp--;
}
//获得乘法的操作数1,必定是一个寄存器
char chOpNum1[5] = {0};
if (nTemp != 0)
{
memcpy(chOpNum1, &pAddr[nTemp+1], nMultiplicationPos - nTemp -1);
}
else
{
memcpy(chOpNum1, &pAddr[0], nMultiplicationPos);
}
int nOpNum1 = RegStringToInt(chOpNum1);
//从乘号向后找
//获得乘法的操作数2,必定是比例因子2,4,8
if (pAddr[nMultiplicationPos+1] == '2')
{
nRetValue += nOpNum1*2;
}
else if(pAddr[nMultiplicationPos+1] == '4')
{
nRetValue += nOpNum1*4;
}
else if(pAddr[nMultiplicationPos+1] == '8')
{
nRetValue += nOpNum1*8;
}
else
{
printf("invalid scale!\r\n");
return 0;
}
//对 pAddr 字符串进行重组
if (nTemp != 0)
{
memcpy(&pAddr[nTemp], &pAddr[nMultiplicationPos+2], 20);
}
else
{
memcpy(&pAddr[0], &pAddr[nMultiplicationPos+2], 20);
}
nLen = strlen(pAddr);
}
//乘法处理完后,表达式中将只有“+”号,或没有符号,或是空字符串
if (nLen == 0)
{
return nRetValue;
}
//找加号
int nPlusPos; //从前往后找到的加号位置下标
for ( nPlusPos = 0; nPlusPos < nLen; nPlusPos++)
{
if (pAddr[nPlusPos] == '+')
{
isFindPlusSign = TRUE;
break;
}
}
if (isFindPlusSign == TRUE)
{
//加法之前必定是一个寄存器
char chPlusOpNum1[5] = {0};
memcpy(chPlusOpNum1, &pAddr[0], 3);
int nPlusOp1 = RegStringToInt(chPlusOpNum1);
//加法之后可能是一个寄存器或立即数,判断一下是否是Eax等寄存器
if (pAddr[nPlusPos+3] == 'x' || pAddr[nPlusPos+3] == 'X' ||
pAddr[nPlusPos+3] == 'i' || pAddr[nPlusPos+3] == 'I' ||
pAddr[nPlusPos+3] == 'p' || pAddr[nPlusPos+3] == 'P')
{
//是寄存器
char chPlusOpNum2[5] = {0};
memcpy(chPlusOpNum2, &pAddr[nPlusPos+1], 3);
int nPlusOp2 = RegStringToInt(chPlusOpNum2);
nRetValue += nPlusOp1 + nPlusOp2;
//对 pAddr 字符串进行重组
if (nLen == 7)
{
return nRetValue;
}
else
{
memcpy(&pAddr[0], &pAddr[8], 20);
nLen = strlen(pAddr);
}
}
else
{
//是立即数,说明是最后一个操作数
int nPlusOp2 = (int)HexStringToHex(&pAddr[nPlusPos+1], FALSE);
nRetValue += nPlusOp1 + nPlusOp2;
return nRetValue;
}
}
int nLast = (int)HexStringToHex(pAddr, FALSE);
if (nLast == 0)
{
nLast = RegStringToInt(pAddr);
}
nRetValue += nLast;
return nRetValue;
}
脚本功能:
脚本功能其主要目的是能够将用户的操作命令保存成文本,同时也可以从文本中逐行导入命令并执行命令。避免用户的重复操作。其实现也比较简单,就是将用户输入的所有合法命令添加到一个链表中,在用户调试完一个程序后可以将命令链表中的命令导出到文本文件中。导入功能与之相反,当使用导入功能的时候,从脚本文件中逐行读取命令文本,通过查全局的“命令-函数对照表”,调用相应的函数。“命令-函数对照表”为如下所示的结构体:
//全局命令-函数对照表
stuCmdNode g_aryCmd[] = {
ADD_COMMAND("T", CDoException::StepInto)
ADD_COMMAND("P", CDoException::StepOver)
ADD_COMMAND("G", CDoException::Run)
ADD_COMMAND("U", CDoException::ShowMulAsmCode)
ADD_COMMAND("D", CDoException::ShowData)
ADD_COMMAND("R", CDoException::ShowRegValue)
ADD_COMMAND("BP", CDoException::SetOrdPoint)
ADD_COMMAND("BPL", CDoException::ListOrdPoint)
ADD_COMMAND("BPC", CDoException::ClearOrdPoint)
ADD_COMMAND("BH", CDoException::SetHardPoint)
ADD_COMMAND("BHL", CDoException::ListHardPoint)
ADD_COMMAND("BHC", CDoException::ClearHardPoint)
ADD_COMMAND("BM", CDoException::SetMemPoint)
ADD_COMMAND("BML", CDoException::ListMemPoint)
ADD_COMMAND("BMC", CDoException::ClearMemPoint)
ADD_COMMAND("LS", CDoException::LoadScript)
ADD_COMMAND("ES", CDoException::ExportScript)
ADD_COMMAND("SR", CDoException::StepRecord)
ADD_COMMAND("H", CDoException::ShowHelp)
{"", NULL} //最后一个空项
};
其中的ADD_COMMAND为一个宏定义:
#define ADD_COMMAND(str, memberFxn) {str, memberFxn},
简单地说,也就是一个字符串对应一个函数指针,通过命令字符串查对应的函数指针,调用函数。
单步记录指令功能:
单步记录指令就是让程序以单步(步入或步过)的方式运行,将指令地址EIP、对应的二进制指令和一些其他的信息放到一个平衡二叉树(以下简称AVL树)上,单步运行的过程中,不断地比较当前指令的EIP和二进制指令是否已经存在于AVL树上,如果不存在则在AVL树上添加这个新的指令结点。这里之所有要用AVL树是出于对检查重复时效率的要求。当然AVL树记录指令非常占用堆空间,如果堆空间消耗严重,可以将AVL树上的一部分内容放到文件中去。记录指令的意义在于让程序走不同的流程,然后可以比较不同流程的差异。另外也可以使用记录指令的方式跳过无意义的跳转,只让程序记录有意义的指令。
记录指令的过程中,我的做法是遇到CALL一个DLL的导出函数时,就采用单步步过的方式,否则就采用单步步入的方式。实际运用中,对于控制台程序记录很有效,但是对于基于消息的窗口程序,由于窗口回调函数是系统API在调用的,所以需要先在回调函数中设置断点,然后再记录指令才能记录到消息函数的代码。
行文至此,我的调试器也讲的差不多了。最后要感谢我在科锐学习以来,钱老师及其他各位老师的教导,同时也要感谢我的同班同学在学习的过程中给予我的帮助。另外要感谢看雪论坛,我的很多思路和想法都源于在看雪论坛上读到的好文章。
本系列文章参考书目、资料如下:
1.《加密与解密3》 编著:段钢
2.《调试寄存器(DRx)理论与实践》 作者:Hume/冷雨飘心
3.《数据结构》 作者:严蔚敏
能力值:
( LV2,RANK:10 )
46 楼
郁闷死了!我太笨了!怎么学也不会啊!膜拜你们牛
能力值:
( LV2,RANK:10 )
47 楼
想问一下你使用的反汇编引擎是哪个啊,好用么
能力值:
( LV8,RANK:130 )
48 楼
我是用的看雪大礼包里面的OD反汇编引擎。
能力值:
( LV2,RANK:10 )
49 楼
学了快一年了,第一次在论坛说话居然是回复贴哈哈.....这可是好东西,支持一下
能力值:
( LV2,RANK:10 )
50 楼
不能不支持楼主