初探Windows用户态调试机制
我们在感叹Onlydbg强大与便利的同时,是否考虑过它实现的原理呢?
作为一个技术人员知其然必知其所以然,这才是我们追求的本心。
最近在学习张银奎老师的《软件调试》,获益良多。熟悉Windows调试机制,对我们深入理解操作系统以及游戏保护的原理有着莫大好处。
0X01
初探调试原理
调试系统的实现思路如图所示:
调试器与被调试程序建立联系,程序像调试器发送调试信息,调试器暂停程序处理完调试信息后再恢复程序运行,如此周而复始。
下面我们看看如何用操作系统提供的API去实现一个简单的调试器。
//启动要调试的进程或挂接调试器到已运行的进程上
CreateProcess(..., DEBUG_PROCESS, ...) or DebugActiveProcess(dwProcessId) DEBUG_EVENT de;
BOOL bContinue = TRUE;
DWORD dwContinueStatus; while(bContinue)
{
bContinue = WaitForDebugEvent(&de, INFINITE); switch(de.dwDebugEventCode)
{
...
default:
{
dwContinueStatus = DBG_CONTINUE;
break;
}
} ContinueDebugEvent(de.dwProcessId, de.dwThreadId, dwContinueStatus);
}
在调试器开始调试的时候,会启动被调试程序的新进程或者挂接(attach)到一个已运行进程上,此时Win32系统会启动调试接口的服务器端;然后调试器调用WaitForDebugEvent函数等待调试服务器端的调试事件被引发;调试器根据调试事件进行相应的处理;最后调用ContinueDebugEvent函数请求调试服务器继续执行被调试进程,以等待并处理下一个调试事件。
0X02
抽茧剥丝看调试机制
要想深入了解Windows调试机制,对着三个函数的深入分析是必不可少的。
1.DebugActiveProcess
BOOL WINAPI DebugActiveProcessSelf(
_In_ DWORD dwProcessId
)
{
NTSTATUS status;
HANDLE TargProcessHandle; status = DbgUiConnectToDbg(); //DebugObject
if (!NT_SUCCESS(status))
{
BaseSetLastNTError(status);
return false;
} TargProcessHandle = GetTargProcessHandle(dwProcessId);
if (TargProcessHandle == 0)
{
return false;
} //调试目标进程
status = DbgUiDebugActiveProcess(TargProcessHandle); //不管调试是否成功都关闭目标进程句柄
ZwClose(TargProcessHandle); if (!NT_SUCCESS(status))
{
BaseSetLastNTError(status);
return false;
} return true;
}
DbgUiConnectToDbg函数内部主要调用ZwCreateDebugObject创建一个调试对象,并将调试对象句柄保存在调试器当前线程的TEB结构的DbgSsReserved[1]中。
其中TEB可以通过FS:[0x18]获得,DbgSsReserved字段在不同操作系统版本中也不相同,在Win732位中处于TEB结构的0xF20中。那么我们可以通过一下汇编得到DbgSsReserved。
__asm{
push eax
mov eax,FS:[0x18]
lea eax,[eax+0xF20]
mov DbgSsReserved,eax
pop eax
}
那么到底什么是调试对象呢?
调试任务的顺利进行在于调试器与调试程序两者间的事件交互,一开始的图里已经很好的表示了。既然是两个进程间的交互,那么必定涉及进程间通信的问题,我在Windows进程通信中已经总结的很明白了,进程间通信靠的是所有进程共享高2G内核空间中的内核对象,
比如事件对象,管道对象等。由此可以推断出调试对象就是调试器与被调试程序间通讯的桥梁! 调试对象保存在调试器TEB线程环境变量块的DbgSsReserved[1]中,保存在被调试进程的DebugPort字段中。(这点下文做详细分析)所以判断一个进程是否被调试可
以看这个进程的DebugPort字段。游戏保护其中的一种保护手段就是通过不断抹除DebugPort,从而达到反调试的目的,所以我们发现用OD无法附加游戏,当然我们可以通过端口移位的方法绕过这种保护方法,这里暂且不做讨论。
GetTargProcessHandle函数主要就是运用ZwOpenProcess函数获得了下进程句柄,在此不作分析,我们下面主要看看最后这个DbgUiDebugActiveProcess函数。
NTSTATUS DbgUiDebugActiveProcess(HANDLE hTargProcess)
{
NTSTATUS status;
HANDLE hDebugObject; hDebugObject = (GetThreadDbgSsReserved())[1];
status = ZwDebugActiveProcess(hTargProcess,hDebugObject);
if (!NT_SUCCESS(status))
{
return status;
} status = DbgUiIssueRemoteBreakin(hTargProcess); //创建远程线程 设置远程断点
if (!NT_SUCCESS(status))
{
DbgUiStopDebugging(hTargProcess);
}
return status;
}
我们先来看看DbgUiIssueRemoteBreakin函数
这个函数比较简单的主要作用是创建远程线程下远程断点,如果没有断点进行拦截,那还怎么调试。
到此DebugActiveProcess函数在Ring3下分析的就差不多了,剩下我们可以看见把被调试程序和调试对象作为参数调用系统函数ZwDebugActiveProcess
我结合上面所说的是不是很清晰这个系统调用在内核做了些什么事情呢? 显然在内核把调试对象放到被调试进程的Debugport字段中去了!
但是ZwDebugActiveProcess在内核中所做的事情可不止这么一点哦,这个函数主要做三件事:
(1)取得被调试进程EPROCESS和调试对象的指针。
(2)向调试对象发送杜撰的调试事件。(当调试器附加到一个已经运行的进程时,为了向调试器报告以前发生的但目前仍有意义的调试事件,调试子系统会“捏造”一些调试事件来模拟过去的调试事件,这样的调试消息被称为杜撰的调试消息)。
(3)调用DbgSetprocessDebugObject将调试对象设置到被调试进程的Debug字段,并调用DbgkpmarkprocessPeb设置PEB中的BeingDebugged字段。
我觉得学习新知识就应该从大体入手,千万不能太抠细节,在有了清晰的框架后再逐渐了解细节的实现问题。看到这里肯定有了很多疑问,比如调试事件结构是什么,它又是如何获得的,又是怎么通过调试对象进行传递的?下面我们再来一探究竟。
调试事件的采取
首先我们应该明白什么算调试事件:被调试进程创建了一个进程、创建了一个线程、加载了一个模块......这些都是调试事件,那么调试器又是如何知道的呢?
在操作系统中有一组Dbgk开头的一组函数它们就是采集例程。以创建线程为例,我们看一下调试消息传递过程。
当我们调用CreateThread函数时,函数建立了线程必要的内核对象和数据结构,做了必要的登记后,最终会调用PspUserthreadStartup函数,准备启动该线 程。为了支持调试,PspUserThreadStartup函数总是会调用DbgkCreateThread,以便采集调试事件。DbgkCreateThread函数会检查自己的DebugPort字段是否为空来判断自己是否被调试,如果被调试,则采集调试信息调用DbgkpSendApiMessage函数向DebugPort发送消息。同理可得LoadLibrary会调用系统函数NtMapViewOfSection然后会调用采集函数DbgkMapViewOfMapSection,最后判断自己是否被调试决定是否采集调试事件来调用DbgkpSendApiMessage。
我们看到采集调试事件中最后都是调用DbgkpSendApiMessage,那么这个函数到底做了些什么呢?
我们先来看看这个函数的定义
NTSTATUS DbgkpSendApiMessage(
IN OUT PDBGKM_APIMSG ApiMsg,
IN PVOID Port,
IN BOOLEAN SuspendProcess)
其中ApiMsg用来描述消息的,Port用来指定要发送的端口,大多数时候就是EPROCESS结构的DebugPort字段的值,偶尔是进程中的异常端口,即ExceptionPort字段。
//消息结构
typedef struct _DBGKM_APIMSG {
PORT_MESSAGE h; //+0x0
DBGKM_APINUMBER ApiNumber; //+0x18
NTSTATUS ReturnedStatus; //+0x1c
union {
DBGKM_EXCEPTION Exception; //异常
DBGKM_CREATE_THREAD CreateThread; //创建线程
DBGKM_CREATE_PROCESS CreateProcessInfo; //创建进程
DBGKM_EXIT_THREAD ExitThread; //线程退出
DBGKM_EXIT_PROCESS ExitProcess; //进程退出
DBGKM_LOAD_DLL LoadDll; //映射DLL
DBGKM_UNLOAD_DLL UnloadDll; //反映射DLL
} u; //0x20
} DBGKM_APIMSG, *PDBGKM_APIMSG;
其中DBGKM_APINUMBER是个枚举常量。
//枚举类型,指定是哪种事件
typedef enum _DBGKM_APINUMBER {
DbgKmExceptionApi,
DbgKmCreateThreadApi,
DbgKmCreateProcessApi,
DbgKmExitThreadApi,
DbgKmExitProcessApi,
DbgKmLoadDllApi,
DbgKmUnloadDllApi,
DbgKmMaxApiNumber
} DBGKM_APINUMBER;
上面说道DbgkpSendApiMessage把调试消息发送给调试对象,那么调试对象又是如何管理这些调试消息的呢?
//调试对象
typedef struct _DEBUG_OBJECT
{
KEVENT EventsPresent;
FAST_MUTEX Mutex;
LIST_ENTRY EventList;
union
{
ULONG Flags;
struct
{
UCHAR DebuggerInactive:1;
UCHAR KillProcessOnExit:1;
};
};
} DEBUG_OBJECT, *PDEBUG_OBJECT;
这个就是调试对象的数据结构,里面可以清晰的看见有个LIST_ENTRY的双向链表。
到这里可能已经有点迷糊了,我们需要个图来整理整理。
这里需要注意的是有个KEVENT的内核事件对象,我们回忆下应用层有个WaitForDebugEvent函数在阻塞着,这个事件就是通知调试器有调试事件到达。
2.WaitForDebugEvent
BOOL WINAPI WaitForDebugEvent(
_Out_ LPDEBUG_EVENT lpDebugEvent,
_In_ DWORD dwMilliseconds
)
WaitForDebugEvent用于等待和接收调试事件,收到调试事件后,调试器便根据事件的类型(事件ID)来分发和处理,并根据情况决定是否要通知用户并进入交互式调试。在处理调试事件的过程中,被调试进程时处于挂起状态的。处理调试事件后,调试器调用ContinueDebugEvent将处理结果回复给调试子系统。到这里细心的似乎已经发现这个调试事件和内核中的调试事件的结构不一样。
typedef struct _DEBUG_EVENT {
DWORD dwDebugEventCode;
DWORD dwProcessId;
DWORD dwThreadId;
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;
在内核中调试事件使用DBGKM_APIMSG的结构来描述。在发送调试器时,调试API使用的是DEBUG_EVENT结构。所以之间必定有一个转换过程。简单的说,DBGKM_APIMSG转换成DBGUI_WAIT_STATE_CHANGE然后在转换成DEBUG_EVENT。
我们再来画张图整理一下
0X03 总结
初探Windows用户态调试机制的更多相关文章
- systemtap 用户态调试
#include <stdio.h> int main( void) { ; a=fun(,); printf("%d\n",a); } int fun(int a,i ...
- windows下的用户态调试的底层与上层实现
操作系统:windows XP 调试器通过CreateProcess传入带有DEBUG_PROCESS和DEBUG_ONLY_THIS_PROCESS的dwCreationFlags创建被调试进程.这 ...
- Windows用户模式调试内部组件
简介 允许用户模式调试工作的内部机制很少得到充分的解释.更糟糕的是,这些机制在Windows XP中已经发生了根本性的变化,当许多支持被重新编写时,还通过将ntdll中的大多数例程作为本地API的一部 ...
- 总在用户态调试 C# 程序,终还是搭了一个内核态环境
一:背景 一直在用 WinDbg 调试用户态程序,并没有用它调试过 内核态,毕竟不是做驱动开发,也没有在分析 dump 中需要接触用内核态的需求,但未知的事情总觉得很酷,加上最近在看 <深入解析 ...
- windows用户态程序的Dump
熟悉Linux的开发人员都知道,在Linux下开发程序,如果程序崩溃了,可以通过配置Core Dump,来让程序崩溃的瞬间产生一个Dump文件,然后通过dump文件来调试程序为什么崩溃.但是windo ...
- MS12-042 用户态调度机制特权提升漏洞
漏洞编号:MS12-042 披露日期: 2012/6/12 受影响的操作系统:Windows 2000;XP;Server 2003;windows 7;Server 2008; 测试系统:windo ...
- systemtap 用户态调试3
[root@localhost ~]# cat test.c #include <stdio.h> int main( void) { int a=0; a=fun(10,20); pri ...
- systemtap 用户态调试2
[root@localhost ~]# cat user.stpprobe process(@1).function(@2){print_ubacktrace();exit();} session 1 ...
- Windbg 内核态调试用户态程序然后下断点正确触发方法(亲自实现发现有效)
先开启真机内核态kernel调试 !process 0 0 svchost.exe 找到进程cid的地址 然后进入 .process /p fffffa8032be2870 然后 .process ...
随机推荐
- Asp.Net Core中简单使用日志组件log4net
本文将简单介绍在.NET 6中使用log4net的方法,具体见下文范例. 1.首先新建一个ASP.NET Core空项目 2.通过Nuget包管理器安装下面两个包 log4net Microsoft. ...
- [atARC075F]Mirrored
假设$n=\sum_{i=0}^{k}a_{i}10^{i}$(其中$a_{k}>0$),则有$d=f(n)-n=\sum_{i=0}^{k}(10^{k-i}-10^{i})a_{i}$,考虑 ...
- layui某个字段不让页面显示显示
<script src="/layuiadmin/layui/layui.js"></script> <script> layui.config ...
- 洛谷 P4094 [HEOI2016/TJOI2016]字符串(SA+主席树)
题面传送门 一道码农题---- u1s1 感觉这类题目都挺套路的,就挑个有代表性的题写一篇题解罢. 首先注意到答案满足可二分性,故考虑二分答案 \(mid\),转化为判定性问题. 考虑怎样检验 \(m ...
- 洛谷 P4709 - 信息传递(置换+dp)
题面传送门 一道挺有意思的题罢-- 首先看到这种与置换乘法相关的题,首先把这些置换拆成一个个置换环,假设输入的置换有 \(m\) 个置换环,大小分别为 \(s_1,s_2,\cdots,s_m\),显 ...
- JSOI2021 游记
Day 0 - 2021.4.9 写一波最近的事情吧( 3 月 20 号出头 cnblogs 抽风,说 25 号恢复来着,我就囤了一堆博客在本地准备 25 号发,结果到时候就懒得动了.干脆越屯越多,省 ...
- GORM基本使用
GORM 目录 GORM 1. 安装 2. 数据库连接 3. 数据库迁移及表操作 1. 安装 go get -u github.com/jinzhu/gorm 要连接数据库首先要导入驱动程序 // G ...
- kafka的安装及使用
前言花絮 今天听了kafka开发成员之一的饶军老师的讲座,讲述了kafka的前生今世.干货的东西倒是没那么容易整理出来,还得刷一遍视频整理,不过两个比较八卦的问题,倒是很容易记住了. Q:为什么kaf ...
- C#gridview颜色提示
OnRowCreated="gridStatistic_RowCreated private void FillUI() { gridStatistic.DataSource = dtSta ...
- 学习java的第二十五天
一.今日收获 1.java完全学习手册第三章算法的3.2排序,比较了跟c语言排序上的不同 2.观看哔哩哔哩上的教学视频 二.今日问题 1.快速排序法的运行调试多次 2.哔哩哔哩教学视频的一些术语不太理 ...