0

0

初探Windows用户态调试机制

蓮花仙者

蓮花仙者

发布时间:2025-09-25 09:35:22

|

834人浏览过

|

来源于php中文网

原创

我们在感叹onlydbg强大与便利的同时,是否考虑过它实现的原理呢?

作为一个技术人员知其然必知其所以然,这才是我们追求的本心。

最近在学习张银奎老师的《软件调试》,获益良多。熟悉Windows调试机制,对我们深入理解操作系统以及游戏保护的原理有着莫大好处。

0X01

初探调试原理

初探Windows用户态调试机制

调试系统的实现思路如图所示:

调试器与被调试程序建立联系,程序像调试器发送调试信息,调试器暂停程序处理完调试信息后再恢复程序运行,如此周而复始。

下面我们看看如何用操作系统提供的API去实现一个简单的调试器。

初探Windows用户态调试机制
代码语言:javascript代码运行次数:0运行复制
//启动要调试的进程或挂接调试器到已运行的进程上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);}
初探Windows用户态调试机制

在调试器开始调试的时候,会启动被调试程序的新进程或者挂接(attach)到一个已运行进程上,此时Win32系统会启动调试接口的服务器端;然后调试器调用WaitForDebugEvent函数等待调试服务器端的调试事件被引发;调试器根据调试事件进行相应的处理;最后调用ContinueDebugEvent函数请求调试服务器继续执行被调试进程,以等待并处理下一个调试事件。

0X02

抽茧剥丝看调试机制

要想深入了解Windows调试机制,对着三个函数的深入分析是必不可少的。

1.DebugActiveProcess

初探Windows用户态调试机制
代码语言:javascript代码运行次数:0运行复制
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;}
初探Windows用户态调试机制

DbgUiConnectToDbg函数内部主要调用ZwCreateDebugObject创建一个调试对象,并将调试对象句柄保存在调试器当前线程的TEB结构的DbgSsReserved[1]中。

其中TEB可以通过FS:[0x18]获得,DbgSsReserved字段在不同操作系统版本中也不相同,在Win732位中处于TEB结构的0xF20中。那么我们可以通过一下汇编得到DbgSsReserved。

初探Windows用户态调试机制
代码语言:javascript代码运行次数:0运行复制
    __asm{        push    eax        mov        eax,FS:[0x18]        lea        eax,[eax+0xF20]        mov        DbgSsReserved,eax        pop        eax    }
初探Windows用户态调试机制

那么到底什么是调试对象呢?

调试任务的顺利进行在于调试器与调试程序两者间的事件交互,一开始的图里已经很好的表示了。既然是两个进程间的交互,那么必定涉及进程间通信的问题,我在Windows进程通信中已经总结的很明白了,进程间通信靠的是所有进程共享高2G内核空间中的内核对象,

比如事件对象,管道对象等。由此可以推断出调试对象就是调试器与被调试程序间通讯的桥梁! 调试对象保存在调试器TEB线程环境变量块的DbgSsReserved[1]中,保存在被调试进程的DebugPort字段中。(这点下文做详细分析)所以判断一个进程是否被调试可

以看这个进程的DebugPort字段。游戏保护其中的一种保护手段就是通过不断抹除DebugPort,从而达到反调试的目的,所以我们发现用OD无法附加游戏,当然我们可以通过端口移位的方法绕过这种保护方法,这里暂且不做讨论。

GetTargProcessHandle函数主要就是运用ZwOpenProcess函数获得了下进程句柄,在此不作分析,我们下面主要看看最后这个DbgUiDebugActiveProcess函数。

初探Windows用户态调试机制
代码语言:javascript代码运行次数:0运行复制
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;}
初探Windows用户态调试机制

我们先来看看DbgUiIssueRemoteBreakin函数

这个函数比较简单的主要作用是创建远程线程下远程断点,如果没有断点进行拦截,那还怎么调试。

到此DebugActiveProcess函数在Ring3下分析的就差不多了,剩下我们可以看见把被调试程序和调试对象作为参数调用系统函数ZwDebugActiveProcess

我结合上面所说的是不是很清晰这个系统调用在内核做了些什么事情呢? 显然在内核把调试对象放到被调试进程的Debugport字段中去了!

MakeSong
MakeSong

AI音乐生成,生成高质量音乐,仅需30秒的时间

下载

但是ZwDebugActiveProcess在内核中所做的事情可不止这么一点哦,这个函数主要做三件事:

(1)取得被调试进程EPROCESS和调试对象的指针。

(2)向调试对象发送杜撰的调试事件。(当调试器附加到一个已经运行的进程时,为了向调试器报告以前发生的但目前仍有意义的调试事件,调试子系统会“捏造”一些调试事件来模拟过去的调试事件,这样的调试消息被称为杜撰的调试消息)。

(3)调用DbgSetprocessDebugObject将调试对象设置到被调试进程的Debug字段,并调用DbgkpmarkprocessPeb设置PEB中的BeingDebugged字段。

我觉得学习新知识就应该从大体入手,千万不能太抠细节,在有了清晰的框架后再逐渐了解细节的实现问题。看到这里肯定有了很多疑问,比如调试事件结构是什么,它又是如何获得的,又是怎么通过调试对象进行传递的?下面我们再来一探究竟。

调试事件的采取

首先我们应该明白什么算调试事件:被调试进程创建了一个进程、创建了一个线程、加载了一个模块......这些都是调试事件,那么调试器又是如何知道的呢?

在操作系统中有一组Dbgk开头的一组函数它们就是采集例程。以创建线程为例,我们看一下调试消息传递过程。

当我们调用CreateThread函数时,函数建立了线程必要的内核对象和数据结构,做了必要的登记后,最终会调用PspUserthreadStartup函数,准备启动该线 程。为了支持调试,PspUserThreadStartup函数总是会调用DbgkCreateThread,以便采集调试事件。DbgkCreateThread函数会检查自己的DebugPort字段是否为空来判断自己是否被调试,如果被调试,则采集调试信息调用DbgkpSendApiMessage函数向DebugPort发送消息。同理可得LoadLibrary会调用系统函数NtMapViewOfSection然后会调用采集函数DbgkMapViewOfMapSection,最后判断自己是否被调试决定是否采集调试事件来调用DbgkpSendApiMessage。

我们看到采集调试事件中最后都是调用DbgkpSendApiMessage,那么这个函数到底做了些什么呢?

我们先来看看这个函数的定义

代码语言:javascript代码运行次数:0运行复制
NTSTATUS DbgkpSendApiMessage(    IN OUT PDBGKM_APIMSG ApiMsg,    IN PVOID Port,    IN BOOLEAN SuspendProcess)

其中ApiMsg用来描述消息的,Port用来指定要发送的端口,大多数时候就是EPROCESS结构的DebugPort字段的值,偶尔是进程中的异常端口,即ExceptionPort字段。

初探Windows用户态调试机制
代码语言:javascript代码运行次数:0运行复制
//消息结构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;
初探Windows用户态调试机制

其中DBGKM_APINUMBER是个枚举常量。

初探Windows用户态调试机制
代码语言:javascript代码运行次数:0运行复制
//枚举类型,指定是哪种事件typedef enum _DBGKM_APINUMBER {     DbgKmExceptionApi,     DbgKmCreateThreadApi,     DbgKmCreateProcessApi,     DbgKmExitThreadApi,     DbgKmExitProcessApi,     DbgKmLoadDllApi,     DbgKmUnloadDllApi,     DbgKmMaxApiNumber } DBGKM_APINUMBER; 
初探Windows用户态调试机制

上面说道DbgkpSendApiMessage把调试消息发送给调试对象,那么调试对象又是如何管理这些调试消息的呢?

初探Windows用户态调试机制
代码语言:javascript代码运行次数:0运行复制
//调试对象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;
初探Windows用户态调试机制

这个就是调试对象的数据结构,里面可以清晰的看见有个LIST_ENTRY的双向链表。

到这里可能已经有点迷糊了,我们需要个图来整理整理。

初探Windows用户态调试机制

这里需要注意的是有个KEVENT的内核事件对象,我们回忆下应用层有个WaitForDebugEvent函数在阻塞着,这个事件就是通知调试器有调试事件到达。

2.WaitForDebugEvent

代码语言:javascript代码运行次数:0运行复制
BOOL WINAPI WaitForDebugEvent(    _Out_  LPDEBUG_EVENT lpDebugEvent,    _In_   DWORD dwMilliseconds    )

WaitForDebugEvent用于等待和接收调试事件,收到调试事件后,调试器便根据事件的类型(事件ID)来分发和处理,并根据情况决定是否要通知用户并进入交互式调试。在处理调试事件的过程中,被调试进程时处于挂起状态的。处理调试事件后,调试器调用ContinueDebugEvent将处理结果回复给调试子系统。到这里细心的似乎已经发现这个调试事件和内核中的调试事件的结构不一样。

初探Windows用户态调试机制
代码语言:javascript代码运行次数:0运行复制
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;
初探Windows用户态调试机制

在内核中调试事件使用DBGKM_APIMSG的结构来描述。在发送调试器时,调试API使用的是DEBUG_EVENT结构。所以之间必定有一个转换过程。简单的说,DBGKM_APIMSG转换成DBGUI_WAIT_STATE_CHANGE然后在转换成DEBUG_EVENT。

我们再来画张图整理一下

初探Windows用户态调试机制

0X03 总结

初探Windows用户态调试机制

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
java基础知识汇总
java基础知识汇总

java基础知识有Java的历史和特点、Java的开发环境、Java的基本数据类型、变量和常量、运算符和表达式、控制语句、数组和字符串等等知识点。想要知道更多关于java基础知识的朋友,请阅读本专题下面的的有关文章,欢迎大家来php中文网学习。

1500

2023.10.24

treenode的用法
treenode的用法

​在计算机编程领域,TreeNode是一种常见的数据结构,通常用于构建树形结构。在不同的编程语言中,TreeNode可能有不同的实现方式和用法,通常用于表示树的节点信息。更多关于treenode相关问题详情请看本专题下面的文章。php中文网欢迎大家前来学习。

538

2023.12.01

C++ 高效算法与数据结构
C++ 高效算法与数据结构

本专题讲解 C++ 中常用算法与数据结构的实现与优化,涵盖排序算法(快速排序、归并排序)、查找算法、图算法、动态规划、贪心算法等,并结合实际案例分析如何选择最优算法来提高程序效率。通过深入理解数据结构(链表、树、堆、哈希表等),帮助开发者提升 在复杂应用中的算法设计与性能优化能力。

17

2025.12.22

深入理解算法:高效算法与数据结构专题
深入理解算法:高效算法与数据结构专题

本专题专注于算法与数据结构的核心概念,适合想深入理解并提升编程能力的开发者。专题内容包括常见数据结构的实现与应用,如数组、链表、栈、队列、哈希表、树、图等;以及高效的排序算法、搜索算法、动态规划等经典算法。通过详细的讲解与复杂度分析,帮助开发者不仅能熟练运用这些基础知识,还能在实际编程中优化性能,提高代码的执行效率。本专题适合准备面试的开发者,也适合希望提高算法思维的编程爱好者。

25

2026.01.06

硬盘接口类型介绍
硬盘接口类型介绍

硬盘接口类型有IDE、SATA、SCSI、Fibre Channel、USB、eSATA、mSATA、PCIe等等。详细介绍:1、IDE接口是一种并行接口,主要用于连接硬盘和光驱等设备,它主要有两种类型:ATA和ATAPI,IDE接口已经逐渐被SATA接口;2、SATA接口是一种串行接口,相较于IDE接口,它具有更高的传输速度、更低的功耗和更小的体积;3、SCSI接口等等。

1099

2023.10.19

PHP接口编写教程
PHP接口编写教程

本专题整合了PHP接口编写教程,阅读专题下面的文章了解更多详细内容。

189

2025.10.17

php8.4实现接口限流的教程
php8.4实现接口限流的教程

PHP8.4本身不内置限流功能,需借助Redis(令牌桶)或Swoole(漏桶)实现;文件锁因I/O瓶颈、无跨机共享、秒级精度等缺陷不适用高并发场景。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

1473

2025.12.29

java接口相关教程
java接口相关教程

本专题整合了java接口相关内容,阅读专题下面的文章了解更多详细内容。

17

2026.01.19

Python 自然语言处理(NLP)基础与实战
Python 自然语言处理(NLP)基础与实战

本专题系统讲解 Python 在自然语言处理(NLP)领域的基础方法与实战应用,涵盖文本预处理(分词、去停用词)、词性标注、命名实体识别、关键词提取、情感分析,以及常用 NLP 库(NLTK、spaCy)的核心用法。通过真实文本案例,帮助学习者掌握 使用 Python 进行文本分析与语言数据处理的完整流程,适用于内容分析、舆情监测与智能文本应用场景。

10

2026.01.27

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Go语言实战之 GraphQL
Go语言实战之 GraphQL

共10课时 | 0.8万人学习

微信小程序开发之API篇
微信小程序开发之API篇

共15课时 | 1.2万人学习

Laravel---API接口
Laravel---API接口

共7课时 | 0.6万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号