加密与解密 调试篇(二) Windows调试器实现(一
加密与解密 调试篇(二) Windos调试器实现(一) 文章目录
- 加密与解密 调试篇(二) Windos调试器实现(一)
- 概述
- 新建一个进程或者附加到一个进程
- 新建一个新的进程
- 附加到一个进程中
- 调试器循环
- 调试器事件
- 调试循环
- 处理调试事件
- CREATE_PROCESS_DEBUG_EVENT
- 三个句柄
- 进程空间相关
- 其他
- 测试代码
- CREATE_THREAD_DEBUG_EVENT
- 测试代码
- EXCEPTION_DEBUG_EVENT
- 测试代码
- EXIT_PROCESS_DEBUG_EVENT
- 测试代码
- EXIT_THREAD_DEBUG_EVENT
- 测试代码
- LOAD_DLL_DEBUG_EVENT
- 测试代码
- UNLOAD_DLL_DEBUG_EVENT
- 测试代码
- OUTPUT_DEBUG_STRING_EVENT
- 测试代码
软件调试对于程序员与逆向工程师来说都是工作中不可缺少的好伙伴。调试器能让我们动态的跟踪程序的运行状态,获取某一瞬间整个程序进程空间中的各类信息及程序的操作运行指令。调试器本身是如何实现的,我之前没有深入跟踪分析过。调试篇(二)的主要内容就是从0开始,逐渐完成一个Windos下调试器的Demo程序的开发过程。目前,参考的内容有软件调试、软件加密技术内幕、Python灰帽子、Zplutor的一个调试器的实现、Writing a basic Windos Debugger – Part 1、WinDebugger等。
在开始套路之前,要明确调试的两个步骤
- 调试器创建或者附加到一个进程中
- 设置调试器的循环来处理调试事件
几个事实
- 调试器是一个程序/进程,我们用这个进程来调试另一个进程
- 被调试程序是一个将要被调试器调试的进程
- 仅能有一个调试器被挂载到被调试进程中,一个调试器可以调试多个进程(用不同线程)
- 只有创建被调试进程的线程才能调试被调试进程,所以创建和后续的操作都需要在同一个线程中
- 当调试器线程结束,被调试进程也会结束,调试器进程可能继续运行
- 当调试器的调试线程忙于处理一个事件时,被调试进程的所有线程都会处于挂起状态
在我们使用调试器进行调试时,常用的方法有两个,一个是针对可执行程序,利用调试器打开一个新的运行实例,然后针对新建进程进行调试;另一个是针对当前已经运行的进程,采用附加(attach)方法进行调试,现在让我们从这两个地方入手,看一下调试器是如何实现这两个功能的。
新建一个新的进程在Windos中,新建进程的API是CreateProcess。根据MSDN文档,该API的原型为
BOOL CreateProcessA( LPCSTR lpApplicationName, LPSTR lpCommandLine, LPSECURITY_ATTRIBUTES lpProcessAttributes, LPSECURITY_ATTRIBUTES lpThreadAttributes, BOOL bInheritHandles, DWORD dCreationFlags, LPVOID lpEnvironment, LPCSTR lpCurrentDirectory, LPSTARTUPINFOA lpStartupInfo, LPPROCESS_INFORMATION lpProcessInformation );
其中,dCreationFlags可以被用来控制新创建进程的优先级以及其属性。根据Windos核心编程,该参数影响新进程创建方式的标志(flag)。多个标志可以使用按位或起来,以便指定多个标志组合。关于调试方面,存在两个参数
- DEBUG_PROCESS,该标识向系统表明父进程电脑维修网希望调试子进程以及子进程将来生成的所有进程。这就表示所有debugee中发生的特定事件都要向父进程(debugger)汇报
- DEBUG_ONLY_THIS_PROCESS,类似于DEBUG_PROCESS,它不会跟踪子进程的子进程信息
VOID CreateProcessDemo() {
STARTUPINFO si = { 0 };
PROCESS_INFORMATION pi = { 0 };
BOOL bRet = false;
si.cb = sizeof(si);
bRet = CreateProcess(
TEXT("C:\Windos\System32\notepad.exe"),
NULL,
NULL,
NULL,
FALSE,
DEBUG_PROCESS | CREATE_NEW_CONSOLE,
NULL,
NULL,
&si,
&pi
);
if (!bRet) {
_tprintf(TEXT("Error create process: %d.n"), GetLastError());
return;
}
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
_tprintf(TEXT("The pid of ne process is %d.n"), pi.dProcessId);
return;
}
以上代码显示的是一个创建新的调试进程的代码片段,其中关键是使用CreateProcessAPI以及使用DEBUG_PROCESS或者DEBUG_ONLY_THIS_PROCESS标识。在这段代码中发现在创建进程时除了调试标识以外,它或操作了一个CREATE_NEW_CONSOLE标识,在Zplutor的一个调试器的实现中,作者标识推荐使用该标识。表示如果被调试进程是一个控制台程序,如果不使用CREATE_NEW_CONSOLE的话,调试器的IO与被调试的IO会显示到一个终端中,显得整个输入输出很混乱。
附加到一个进程中在Windos API中,提供了一个对应的API函数,DebugActiveProcess,其函数原型为
BOOL DebugActiveProcess( DWORD dProcessId );
根据MSDN,该API的功能是“Enables a debugger to attach to an active process and debug it.”。 然后这个功能类似于使用DEBUG_ONLY_THIS_PROCESS来创建目标进程。
在下面的详细描述中提到,如果想要停止调试进程,需要调用DebugActiveProcessS,退出调试器进程也会停止被调试进程,除非使用了DebugSetProcessKillOnExit。
在权限方面,调试器进程需要拥有针对目标进程的合适权限,这个权限必须是PROCESS_ALL_ACCESS。然后如果调试进程拥有SE_DEBUG_NAME权限,那么它就可以调试任何进程。
VOID DebugActiveProcessDemo() {
BOOL bRet = FALSE;
DWORD pid = -1;
_tcscanf_s(TEXT("%d.n"), &pid);
bRet = DebugActiveProcess(pid);
if (!bRet) {
_tprintf(TEXT("Attach to %d error: %d"), pid, GetLastError());
return;
}
_tprintf(TEXT("Attach to %d ok!"), pid);
return;
}
调试器循环
作为一个调试器,监视目标进程的执行,对目标进程发生的每一件调试事件进行处理是它的主要工作。所以,对于一个调试器而言,调试器循环是调试器的核心部位。被调试进程在完成了某些操作或者发生某些异常时,会发送通知给调试器,然后自身挂起,直到调试器命令它继续运行。这点类似于Windos窗口的消息机制。在调试器这个消息处理循环中,WaitForDebugEvent函数发挥了核心功能,因为它会持续获取目标进程的相关环境信息,获取需要处理的调试事件。
BOOL WaitForDebugEvent( LPDEBUG_EVENT lpDebugEvent, DWORD dMilliseconds );
MSDN中描述,WaitForDebugEvent的作用是等待被调试进程产生调试事件。MSDN中提到一个事件,说是以前的操作系统不支持通过OutputDebugStringW来输出Unicode编码的信息,为了使其支持,需要调用WaitForDebugEventEx来实现。我在看其他的调试器源码时,包括python的WinAppDebug以及x64dbg使用的TitanEngine,其中都使用的WaitForDebugEvent。后续的搜索发现WaitForDebugEventEx是Win10新引进的API,然后在细节角度,两者其实都是调用的同一个内部函数,不过在参数上做了些调整。
在上图中,发现有两个调用,都跳转到了同样的kernelbase中的一个地址,区别在于一个将r8d清零,另一个将r8b置位1。
调试器事件,在API中,调试信息的结构体为DEBUG_EVENT,其结构为
typedef struct _DEBUG_EVENT {
DWORD dDebugEventCode;
DWORD dProcessId;
DWORD dThreadId;
union {
EXCEPTION_DEBUG_INFO Exception;
CREATE_THREAD_DEBUG_INFO CreateThread;
CREATE_PROCESS_DEBUG_INFO CreateProcessInfo;
EXIT_THREAD_DEBUG_INFO ExitThread;
EXIT_PROCESS_DEBUG_INFO ExitProcess;
LOAD_DLL_DEBUG_INFO LoadDll;
UNLOAD_DLL_DEBUG_INFO UnloadDll;
OUTPUT_DEBUG_STRING_INFO DebugString;
RIP_INFO RipInfo;
} u;
} DEBUG_EVENT, LPDEBUG_EVENT;
该结构保存记录了一个发生的调试事件,其中dDebugEventCode用来标识发生的调试事件类型,dProcessId记录了发生调试事件的PID,dThreadId则记录了发生调试事件的线程ID信息,的联合体u则是根据dDebugEventCode的不同选择不同的类型进行保存。
调试事件类型总共有9种
上面部分讲到了如何创建或者附加到一个调试进程,调试进程会发出哪些类型的调试事件,这部分将说明调试器是如何监视以及处理事件的。这一部分会展示一个通用的代码结构,该代码结构由循环、WaitForDebugEvent、sitch语句以及ContinueDebugEvent构成。
其中,循环的目的是持续判断调试进程是否产生调试事件,这个是由WaitForDebugEvent来完成的,返回为TRUE时为在等待时间内发生了事件,否则为不存在或者出现了错误。
当获取到调试事件后,根据调试事件类型通过sitch语句来分别跳转到对应的处理例程上。
因为在调试器接受到调试事件并进行处理的过程中调试进程处于挂起装填,在处理完调试事件后,使用ContinueDebugEvent来恢复进程的运行,从挂起状态转换到运行状态中。
VOID MainLoop() {
DEBUG_EVENT debug_event;
BOOL bRunning = FALSE;
hile (bRunning) {
if (!WaitForDebugEvent(&debug_event, INFINITE)) {
_tprintf(TEXT("Error : %d.n"), GetLastError());
break;
}
sitch (debug_event.dDebugEventCode) {
case CREATE_PROCESS_DEBUG_EVENT:
_tprintf(TEXT("Process created!n"));
break;
case CREATE_THREAD_DEBUG_EVENT:
break;
case EXCEPTION_DEBUG_EVENT:
break;
case EXIT_PROCESS_DEBUG_EVENT:
_tprintf(TEXT("Process exit!n"));
bRunning = FALSE;
break;
case EXIT_THREAD_DEBUG_EVENT:
break;
case LOAD_DLL_DEBUG_EVENT:
break;
case OUTPUT_DEBUG_STRING_EVENT:
break;
case UNLOAD_DLL_DEBUG_EVENT:
break;
case RIP_EVENT:
break;
default:
_tprintf(TEXT("Something error ours, process shouldn't go here"));
return;
}
ContinueDebugEvent(debug_event.dProcessId, debug_event.dThreadId, DBG_CONTINUE);
}
}
空调维修
- 我的世界电脑版运行身份怎么弄出来(我的世界
- 空调抽湿是什么意思,设置抽湿的温度有什么意
- 方太燃气灶有一个打不着火 怎么修复与排查方法
- 夏季免费清洗汽车空调的宣传口号
- 清洗完空调后出现漏水现象
- iphone6能玩什么游戏(iphone6游戏)
- 如何设置电脑密码锁屏(如何设置电脑密码锁屏
- win10删除开机密码提示不符合密码策略要求
- 电脑w7显示不是正版(w7不是正版怎么解决)
- 万家乐z8热水器显示e7解决 怎么修复与排查方法
- 1匹空调多少瓦数(1匹空调多少瓦)
- 安卓手机连接电脑用什么软件好(关于安卓手机
- 电脑网页看视频卡是什么原因(爱拍看视频卡)
- 华帝燃气灶点火器一直响然后熄火怎么办:问题
- 电脑壁纸怎么换(关于电脑壁纸怎么换的介绍)
- 冬天空调的出风口应该朝什么方向(冬天空调风