软件调试学习笔记(七)—— 单步步入&单步步过

  • 单步步入
    • 设置单步异常
    • 处理单步异常
    • 实验1:单步异常的设置与处理
  • 单步步过
    • 实现思路
    • 实验2:实现单步步过

单步步入

描述

  1. 单步步入的实现依赖于单步异常
  2. 当我们需要观察每一行代码(包括函数内部的代码)执行之后寄存器与内存的变化,通常会采用单步步入。
  3. 当使用单步步入时,可采用在下一行代码的首字节设置INT 3断点的方式实现。
  4. CPU为我们提供了一种更为方便的方法,即使用陷阱标志位(TF位)

设置单步异常

将TF位置1

处理单步异常

单步产生的异常与硬件断点产生的异常一致,都是STATUS_SINGLE_STEP(单步异常)

实验1:单步异常的设置与处理

1)编译并运行以下代码:

#include <stdio.h>
#include <windows.h>
#include <tlhelp32.h>#define DEBUGGEE "C:\\helloworld.exe"//被调试进程ID,进程句柄,OEP
DWORD dwDebuggeePID = 0;//被调试线程句柄
HANDLE hDebuggeeThread = NULL;
HANDLE hDebuggeeProcess = NULL;//系统断点
BOOL bIsSystemInt3 = TRUE;//被INT 3覆盖的数据
CHAR OriginalCode = 0;//线程上下文
CONTEXT Context;typedef HANDLE (__stdcall *FnOpenThread) (DWORD, BOOL, DWORD);VOID InitDebuggeeInfo(DWORD dwPID, HANDLE hProcess)
{dwDebuggeePID = dwPID;hDebuggeeProcess = hProcess;
}DWORD GetProcessId(LPTSTR lpProcessName)
{HANDLE hProcessSnap = NULL;PROCESSENTRY32 pe32 = {0};hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);if(hProcessSnap == (HANDLE)-1){return 0;}pe32.dwSize = sizeof(PROCESSENTRY32);if(Process32First(hProcessSnap, &pe32)){do {if(!strcmp(lpProcessName, pe32.szExeFile))return (int)pe32.th32ProcessID;} while (Process32Next(hProcessSnap, &pe32));}else{CloseHandle(hProcessSnap);}return 0;
}BOOL WaitForUserCommand()
{BOOL bRet = FALSE;CHAR command;printf("COMMAND>");command = getchar();switch(command){case 't':bRet = TRUE;//1. 获取线程上下文Context.ContextFlags = CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS;GetThreadContext(hDebuggeeThread, &Context);//2. 设置陷阱标志位Context.EFlags |= 0x100;//3. 设置线程上下文SetThreadContext(hDebuggeeThread, &Context);break;case 'p':bRet = TRUE;break;case 'g':bRet = TRUE;break;}getchar();return bRet;
}VOID SetHardBreakPoint(PVOID pAddress)
{//1. 获取线程上下文Context.ContextFlags = CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS;GetThreadContext(hDebuggeeThread, &Context);//2. 设置断点位置Context.Dr0 = (DWORD)pAddress;Context.Dr7 |= 1;//3. 设置断点长度和类型Context.Dr7 &= 0xfff0ffff;   //执行断点(16、17位 置0) 1字节(18、19位 置0)//5. 设置线程上下文SetThreadContext(hDebuggeeThread, &Context);
}BOOL Int3ExceptionProc(EXCEPTION_DEBUG_INFO *pExceptionInfo)
{BOOL bRet = FALSE;//1. 将INT 3修复为原来的数据(如果是系统断点,不用修复)if(bIsSystemInt3){bIsSystemInt3 = FALSE;return TRUE;}else{WriteProcessMemory(hDebuggeeProcess, pExceptionInfo->ExceptionRecord.ExceptionAddress, &OriginalCode, 1, NULL);}//2. 显示断点位置printf("Int 3断点:0x%p \r\n", pExceptionInfo->ExceptionRecord.ExceptionAddress);//3. 获取线程上下文Context.ContextFlags = CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS;GetThreadContext(hDebuggeeThread, &Context);//4. 修正EIPContext.Eip--;SetThreadContext(hDebuggeeThread, &Context);//5. 显示反汇编代码、寄存器等/*硬件断点需要设置在被调试进程的的线程上下文中。因此当被调试程序触发调试器设置的INT 3断点时,此时设置硬件断点较为合理。*///SetHardBreakPoint((PVOID)((DWORD)pExceptionInfo->ExceptionRecord.ExceptionAddress+1));//6. 等待用户命令while(bRet == FALSE){bRet = WaitForUserCommand();}return bRet;
}BOOL AccessExceptionProc(EXCEPTION_DEBUG_INFO *pExceptionInfo)
{BOOL bRet = TRUE;return bRet;
}BOOL SingleStepExceptionProc(EXCEPTION_DEBUG_INFO *pExceptionInfo)
{BOOL bRet = FALSE;//1. 获取线程上下文Context.ContextFlags = CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS;GetThreadContext(hDebuggeeThread, &Context);//2. 判断是否是硬件断点导致的异常if(Context.Dr6 & 0xF) //B0~B3不为空 硬件断点{//2.1 显示断点信息printf("硬件断点:%x 0x%p \n", Context.Dr7&0x00030000, Context.Dr0);//2.2 将断点去除Context.Dr0 = 0;Context.Dr7 &= 0xfffffffe;}else    //单步异常{//2.1 显示断点信息printf("单步异常:0x%p \n", Context.Eip);//2.2 将断点去除Context.Dr7 &= 0xfffffeff;}//3.设置线程上下文SetThreadContext(hDebuggeeThread, &Context);//4. 显示寄存器信息/反汇编代码//...略//5. 等待用户命令while(bRet == FALSE){bRet = WaitForUserCommand();}return bRet;
}BOOL ExceptionHandler(DEBUG_EVENT *pDebugEvent)
{ BOOL bRet = TRUE;EXCEPTION_DEBUG_INFO *pExceptionInfo = NULL;pExceptionInfo = &pDebugEvent->u.Exception;//得到线程句柄,后面要用FnOpenThread MyOpenThread = (FnOpenThread)GetProcAddress(LoadLibrary("kernel32.dll"), "OpenThread");hDebuggeeThread = MyOpenThread(THREAD_ALL_ACCESS, FALSE, pDebugEvent->dwThreadId);switch(pExceptionInfo->ExceptionRecord.ExceptionCode){//INT 3异常case EXCEPTION_BREAKPOINT:bRet = Int3ExceptionProc(pExceptionInfo);break;//访问异常case EXCEPTION_ACCESS_VIOLATION:bRet = AccessExceptionProc(pExceptionInfo);break;//单步执行case EXCEPTION_SINGLE_STEP:bRet = SingleStepExceptionProc(pExceptionInfo);break;}return bRet;
}VOID SetInt3BreakPoint(LPVOID addr)
{CHAR int3 = 0xCC;//1. 备份ReadProcessMemory(hDebuggeeProcess, addr, &OriginalCode, 1, NULL);//2. 修改WriteProcessMemory(hDebuggeeProcess, addr, &int3, 1, NULL);
}int main(int argc, char* argv[])
{BOOL nIsContinue = TRUE;DEBUG_EVENT debugEvent = {0};BOOL bRet = TRUE;DWORD dwContinue = DBG_CONTINUE;//1.创建调试进程STARTUPINFO startupInfo = {0};PROCESS_INFORMATION pInfo = {0};GetStartupInfo(&startupInfo);bRet = CreateProcess(DEBUGGEE, NULL, NULL, NULL, TRUE, DEBUG_PROCESS || DEBUG_ONLY_THIS_PROCESS, NULL, NULL, &startupInfo, &pInfo);if(!bRet){printf("CreateProcess error: %d \n", GetLastError());return 0;}hDebuggeeProcess = pInfo.hProcess;//2.调试循环while(nIsContinue){bRet = WaitForDebugEvent(&debugEvent, INFINITE);if(!bRet){printf("WaitForDebugEvent error: %d \n", GetLastError());return 0;}switch(debugEvent.dwDebugEventCode){//1.异常case EXCEPTION_DEBUG_EVENT:bRet = ExceptionHandler(&debugEvent);if(!bRet)dwContinue = DBG_EXCEPTION_NOT_HANDLED;break;//2.case CREATE_THREAD_DEBUG_EVENT:break;//3.创建进程case CREATE_PROCESS_DEBUG_EVENT://设置INT 3断点SetInt3BreakPoint((PCHAR)debugEvent.u.CreateProcessInfo.lpStartAddress);break;//4.case EXIT_THREAD_DEBUG_EVENT:break;//5.case EXIT_PROCESS_DEBUG_EVENT:break;//6.case LOAD_DLL_DEBUG_EVENT:break;//7.case UNLOAD_DLL_DEBUG_EVENT:break;//8.case OUTPUT_DEBUG_STRING_EVENT:break;}bRet = ContinueDebugEvent(debugEvent.dwProcessId, debugEvent.dwThreadId, DBG_CONTINUE);}return 0;
}

运行结果:

2)多次输入t并回车,单步执行

3)输入g并回车,程序继续运行

单步步过

  1. 当遇到CALL指令时,若无需进入函数内部进行调试,可以使用单步步过
  2. 与单步步入不同的是,单步步过的实现依赖于软件断点硬件断点

实现思路

  1. 判断当前指令是否为CALL指令
  2. 不是CALL指令,设置TF为1触发单步异常
  3. CALL指令,判断OPCODEE8还是FF15
  4. 若OPCODE是E8,在当前地址之后的第5个字节设置软件断点(E8指令占5个字节)
  5. 若OPCODE是FF15,在当前地址之后的第6个字节设置软件断点(FF15指令占6个字节)

实验2:实现单步步过

1)编译并运行以下代码:

#include <stdio.h>
#include <windows.h>
#include <tlhelp32.h>#define DEBUGGEE "C:\\helloworld.exe"//被调试进程ID,进程句柄,OEP
DWORD dwDebuggeePID = 0;//被调试线程句柄
HANDLE hDebuggeeThread = NULL;
HANDLE hDebuggeeProcess = NULL;//系统断点
BOOL bIsSystemInt3 = TRUE;//被INT 3覆盖的数据
CHAR cOriginalCode = 0;//线程上下文
CONTEXT Context;typedef HANDLE (__stdcall *FnOpenThread) (DWORD, BOOL, DWORD);VOID InitDebuggeeInfo(DWORD dwPID, HANDLE hProcess)
{dwDebuggeePID = dwPID;hDebuggeeProcess = hProcess;
}DWORD GetProcessId(LPTSTR lpProcessName)
{HANDLE hProcessSnap = NULL;PROCESSENTRY32 pe32 = {0};hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);if(hProcessSnap == (HANDLE)-1){return 0;}pe32.dwSize = sizeof(PROCESSENTRY32);if(Process32First(hProcessSnap, &pe32)){do {if(!strcmp(lpProcessName, pe32.szExeFile))return (int)pe32.th32ProcessID;} while (Process32Next(hProcessSnap, &pe32));}else{CloseHandle(hProcessSnap);}return 0;
}VOID SetInt3BreakPoint(LPVOID addr)
{CHAR int3 = 0xCC;//1. 备份ReadProcessMemory(hDebuggeeProcess, addr, &cOriginalCode, 1, NULL);//2. 修改WriteProcessMemory(hDebuggeeProcess, addr, &int3, 1, NULL);
}BOOL WaitForUserCommand()
{BOOL bRet = FALSE;CHAR command;WORD wBuffer;printf("COMMAND>");command = getchar();switch(command){case 't':bRet = TRUE;//1. 获取线程上下文Context.ContextFlags = CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS;GetThreadContext(hDebuggeeThread, &Context);//2. 设置陷阱标志位Context.EFlags |= 0x100;//3. 设置线程上下文SetThreadContext(hDebuggeeThread, &Context);break;case 'p':bRet = TRUE;bRet = TRUE;//1. 获取线程上下文Context.ContextFlags = CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS;GetThreadContext(hDebuggeeThread, &Context);//2. 读取当前EIP指向的机器码ReadProcessMemory(hDebuggeeProcess, (LPVOID)Context.Eip, &wBuffer, 2, NULL);if((wBuffer & 0xFF) == 0xE8){//3. 在当前地址之后的第5个字节设置软件断点(E8指令占5个字节)SetInt3BreakPoint((LPVOID)(Context.Eip+5));}else if(wBuffer == 0x15FF){//3. 在当前地址之后的第6个字节设置软件断点(FF15指令占6个字节)SetInt3BreakPoint((LPVOID)(Context.Eip+6));}else{//3. 不是CALL指令,设置陷阱标志位触发单步异常即可Context.EFlags |= 0x100;//4. 设置线程上下文SetThreadContext(hDebuggeeThread, &Context);}break;case 'g':bRet = TRUE;break;}getchar();return bRet;
}VOID SetHardBreakPoint(PVOID pAddress)
{//1. 获取线程上下文Context.ContextFlags = CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS;GetThreadContext(hDebuggeeThread, &Context);//2. 设置断点位置Context.Dr0 = (DWORD)pAddress;Context.Dr7 |= 1;//3. 设置断点长度和类型Context.Dr7 &= 0xfff0ffff;   //执行断点(16、17位 置0) 1字节(18、19位 置0)//5. 设置线程上下文SetThreadContext(hDebuggeeThread, &Context);
}BOOL Int3ExceptionProc(EXCEPTION_DEBUG_INFO *pExceptionInfo)
{BOOL bRet = FALSE;//1. 将INT 3修复为原来的数据(如果是系统断点,不用修复)if(bIsSystemInt3){bIsSystemInt3 = FALSE;return TRUE;}else{WriteProcessMemory(hDebuggeeProcess, pExceptionInfo->ExceptionRecord.ExceptionAddress, &cOriginalCode, 1, NULL);}//2. 显示断点位置printf("Int 3断点:0x%p \r\n", pExceptionInfo->ExceptionRecord.ExceptionAddress);//3. 获取线程上下文Context.ContextFlags = CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS;GetThreadContext(hDebuggeeThread, &Context);//4. 修正EIPContext.Eip--;SetThreadContext(hDebuggeeThread, &Context);//5. 显示反汇编代码、寄存器等/*硬件断点需要设置在被调试进程的的线程上下文中。因此当被调试程序触发调试器设置的INT 3断点时,此时设置硬件断点较为合理。*///SetHardBreakPoint((PVOID)((DWORD)pExceptionInfo->ExceptionRecord.ExceptionAddress+1));//6. 等待用户命令while(bRet == FALSE){bRet = WaitForUserCommand();}return bRet;
}BOOL AccessExceptionProc(EXCEPTION_DEBUG_INFO *pExceptionInfo)
{BOOL bRet = TRUE;return bRet;
}BOOL SingleStepExceptionProc(EXCEPTION_DEBUG_INFO *pExceptionInfo)
{BOOL bRet = FALSE;//1. 获取线程上下文Context.ContextFlags = CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS;GetThreadContext(hDebuggeeThread, &Context);//2. 判断是否是硬件断点导致的异常if(Context.Dr6 & 0xF) //B0~B3不为空 硬件断点{//2.1 显示断点信息printf("硬件断点:%x 0x%p \n", Context.Dr7&0x00030000, Context.Dr0);//2.2 将断点去除Context.Dr0 = 0;Context.Dr7 &= 0xfffffffe;}else    //单步异常{//2.1 显示断点信息printf("单步异常:0x%p \n", Context.Eip);//2.2 将断点去除Context.Dr7 &= 0xfffffeff;}//3.设置线程上下文SetThreadContext(hDebuggeeThread, &Context);//4. 显示寄存器信息/反汇编代码//...略//5. 等待用户命令while(bRet == FALSE){bRet = WaitForUserCommand();}return bRet;
}BOOL ExceptionHandler(DEBUG_EVENT *pDebugEvent)
{ BOOL bRet = TRUE;EXCEPTION_DEBUG_INFO *pExceptionInfo = NULL;pExceptionInfo = &pDebugEvent->u.Exception;//得到线程句柄,后面要用FnOpenThread MyOpenThread = (FnOpenThread)GetProcAddress(LoadLibrary("kernel32.dll"), "OpenThread");hDebuggeeThread = MyOpenThread(THREAD_ALL_ACCESS, FALSE, pDebugEvent->dwThreadId);switch(pExceptionInfo->ExceptionRecord.ExceptionCode){//INT 3异常case EXCEPTION_BREAKPOINT:bRet = Int3ExceptionProc(pExceptionInfo);break;//访问异常case EXCEPTION_ACCESS_VIOLATION:bRet = AccessExceptionProc(pExceptionInfo);break;//单步执行case EXCEPTION_SINGLE_STEP:bRet = SingleStepExceptionProc(pExceptionInfo);break;}return bRet;
}int main(int argc, char* argv[])
{BOOL nIsContinue = TRUE;DEBUG_EVENT debugEvent = {0};BOOL bRet = TRUE;DWORD dwContinue = DBG_CONTINUE;//1.创建调试进程STARTUPINFO startupInfo = {0};PROCESS_INFORMATION pInfo = {0};GetStartupInfo(&startupInfo);bRet = CreateProcess(DEBUGGEE, NULL, NULL, NULL, TRUE, DEBUG_PROCESS || DEBUG_ONLY_THIS_PROCESS, NULL, NULL, &startupInfo, &pInfo);if(!bRet){printf("CreateProcess error: %d \n", GetLastError());return 0;}hDebuggeeProcess = pInfo.hProcess;//2.调试循环while(nIsContinue){bRet = WaitForDebugEvent(&debugEvent, INFINITE);if(!bRet){printf("WaitForDebugEvent error: %d \n", GetLastError());return 0;}switch(debugEvent.dwDebugEventCode){//1.异常case EXCEPTION_DEBUG_EVENT:bRet = ExceptionHandler(&debugEvent);if(!bRet)dwContinue = DBG_EXCEPTION_NOT_HANDLED;break;//2.case CREATE_THREAD_DEBUG_EVENT:break;//3.创建进程case CREATE_PROCESS_DEBUG_EVENT://设置INT 3断点SetInt3BreakPoint((PCHAR)debugEvent.u.CreateProcessInfo.lpStartAddress);break;//4.case EXIT_THREAD_DEBUG_EVENT:break;//5.case EXIT_PROCESS_DEBUG_EVENT:break;//6.case LOAD_DLL_DEBUG_EVENT:break;//7.case UNLOAD_DLL_DEBUG_EVENT:break;//8.case OUTPUT_DEBUG_STRING_EVENT:break;}bRet = ContinueDebugEvent(debugEvent.dwProcessId, debugEvent.dwThreadId, DBG_CONTINUE);}return 0;
}

运行结果:

2)多次输入p并回车单步执行,若执行CALL则会触发INT 3异常

3)对照OD查看是否相符

软件调试学习笔记(七)—— 单步步入单步步过相关推荐

  1. 软件调试学习笔记(六)—— 硬件断点

    软件调试学习笔记(六)-- 硬件断点 硬件断点 设置硬件断点 触发硬件断点 处理硬件断点 实验:硬件断点的设置与处理 硬件断点 描述: 与软件断点与内存断点不同,硬件断点不依赖被调试程序,而是依赖于C ...

  2. 软件调试学习笔记(五)—— 软件断点内存断点

    软件调试学习笔记(五)-- 软件断点&内存断点 调试的本质 软件断点 软件断点的执行流程 分析INT 3执行流程 实验:处理软件断点 内存断点 内存断点的执行流程 实验:处理内存断点 调试的本 ...

  3. 软件调试学习笔记(四)—— 异常的处理流程

    软件调试学习笔记(四)-- 异常的处理流程 要点回顾 异常的处理流程 实验1:理解调试器与异常的关系 未处理异常:最后一道防线 实验2:理解UnhandledExceptionFilter执行流程 实 ...

  4. 软件调试学习笔记(三)—— 调试事件的处理

    软件调试学习笔记(三)-- 调试事件的处理 要点回顾 调试事件的处理 实验一:实现简单调试器(创建进程) 实验二:分析异常来源 实验三:实现简单调试器(附加进程) 实验四:分析NtDebugActiv ...

  5. 软件调试学习笔记(二)—— 调试事件的采集

    软件调试学习笔记(二)-- 调试事件的采集 要点回顾 调试事件的种类 调试事件采集函数 例:分析PspUserThreadStartup 例:分析PspExitThread 总结 要点回顾 调试器与被 ...

  6. 软件调试学习笔记(一)—— 调试对象

    软件调试学习笔记(一)-- 调试对象 准备工作 调试器与被调试程序 DebugActiveProcess 连接调试器 分析kernel32!DebugActiveProcess 分析ntdll!Dbg ...

  7. Windows软件调试学习笔记(1)

    --WINDBG中的表达式 WINDBG接受两种表达式,C++表达式和MASM表达式. 1. MASM表达式中的数值: MASM表达式中的数值可以基于16,10,8,2四种进制.用n命令可以设置WIN ...

  8. 软件设计模式学习笔记(七)

    软件设计模式学习笔记(七) 结构型模式 1. 组合模式 1.1 概述 ​ 对于这个图片肯定会非常熟悉,上图我们可以看做是一个文件系统,对于这样的结构我们称之为树形结构.在树形结构中可以通过调用某个方法 ...

  9. 逆向脱壳破解分析基础学习笔记七 堆栈图(重点)

    本文为本人 大神论坛 逆向破解脱壳学习笔记之一,为本人对以往所学的回顾和总结,可能会有谬误之处,欢迎大家指出. 陆续将不断有笔记放出,希望能对想要入门的萌新有所帮助,一起进步 堆栈图 首先给定一段反汇 ...

最新文章

  1. POJ3268 Silver Cow Party(最短路径)
  2. mysql 日均pv100w_日均百万PV架构第四弹(分布式监控)_MySQL
  3. Gradle sync failed: Minimum supported Gradle version is 3.3.Current version is 3.2
  4. 吃糖果(信息学奥赛一本通-T1193)
  5. 新到的电脑BIOS中无法识别U盘
  6. upper_bound和lower_bound的用法
  7. P1533 可怜的狗狗
  8. sql转化为int类型
  9. [NLP]自然语言的处理步骤
  10. 阶段5 3.微服务项目【学成在线】_day01 搭建环境 CMS服务端开发_21-页面查询服务端开发-Dao-分页查询测试...
  11. TeamViewer开机自启动实现在远程使用时重启远程计算机
  12. 基于python 爬虫网络舆情分析系统_基于Python的网络爬虫系统
  13. 文件创建失败 无法继续下载_iOS更新失败了怎么办?教你如何排除故障解决问题...
  14. 戴尔电脑硬件自检教程
  15. vue 挑战最强大脑 记忆力5分 观察力5分 推理力4分
  16. 杭电2818——带权并差集
  17. win10修改ip或dns弹出“出现了一个意外”对话框解决办法
  18. 【自定义View】洋葱数学同款阴影布局-ShadowLayout
  19. 网络安全工程师(渗透运维)难学吗?
  20. 电脑录屏没有声音该怎么办

热门文章

  1. Interview:算法岗位面试—10.23下午—上海某科技公司算法岗位(偏机器学习算法,上市)技术面试之比赛积累、项目经验、个人未来发展
  2. 20 道 Spring Boot 面试题
  3. BootstrapTable-加载数据
  4. [CF587F]Duff is Mad[AC自动机+根号分治+分块]
  5. 树莓派debian配置lamp[解决Apache不显示php网页]
  6. forEach 与 map 的区别
  7. Unity3d简单的socket通信
  8. ES6入门之对象扩展
  9. 【设计模式】单例模式-生成器模式-原型模式
  10. CodeBlocks 更改 gui 程序为 命令行