初探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,那么这个函数到底做了些什么呢?

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

				
时间: 2024-10-19 13:05:14

初探Windows用户态调试机制的相关文章

[并发并行]_[线程同步]_[Windows用户态下的原子访问的互锁函数]

场景: 1. 多线程编程时,有时候需要统计某个变量或对象的创建个数或者是根据某个变量值来判断是否需要继续执行下去,这时候互锁函数是比较高效的方案之一了. 说明: 1.原子访问:(互锁函数族) -- 原子访问,指线程在访问资源时能够确保所有其他线程都不在同一时间内访问相同的资源. -- 调用一个互锁函数通常只需要执行几个CPU周期(通常小于50),并且不需要从用户模式转换为内核模式(通常这需要1000个CPU周期). -- 互锁函数是CPU硬件支持的,如果是x86处理器,互锁函数会向总线发出一个硬

linux c 用户态调试追踪函数调用堆栈以及定位段错误[转载]

一般察看函数运行时堆栈的方法是使用GDB(bt命令)之类的外部调试器,但是,有些时候为了分析程序的BUG,(主要针对长时间运行程序的分析),在程序出错时打印出函数的调用堆栈是非常有用的. 在glibc头文件"execinfo.h"中声明了三个函数用于获取当前线程的函数调用堆栈. int backtrace(void **buffer,int size) 该函数用于获取当前线程的调用堆栈,获取的信息将会被存放在buffer中,它是一个指针列表.参数 size 用来指定buffer中可以保

Windows.用户态程序高效排错.2007.电子工业出版社(熊力)__笔记(杂)

ZC: 下面记录的页码编号(P.??)都是指 书的 页码,而非 PDF的页码 (书P.21 <--> Pdf.P.41) 1.P.32:SRV*D:\Symbols_Web*http://msdl.microsoft.com/download/symbols;C:\Symbols ZC: 各个 应该是使用 分号(";")隔开 ZC: VC6编译的Release,默认设置下,未发现 pdb文件 ZC: 应该把本地的目录写在前面:D:\XiongLi(被调试程序对应的symbo

systemtap 用户态调试2

[[email protected] ~]# cat user.stpprobe process(@1).function(@2){print_ubacktrace();exit();} session 1 执行 stap user.stp "./a.out" "fun" session 2 执行 [[email protected] ~]# ./a.out session 1中打印显示结果 0x4004f9 : fun+0xb/0x12 [/root/a.out]

Windows.用户态程序高效排错.2007.电子工业出版社(熊力)__代码保存

1.2.1.3 #include <stdio.h> #include <stdlib.h> char* getcharBuffer() { return "6969,3p3p"; } void changeto4p(char* buffer) { while (*buffer) { if (*buffer == '3') *buffer = '4'; buffer ++; } } void main() { printf("%s\n", &

聊聊Linux用户态驱动设计

序言 设备驱动可以运行在内核态,也可以运行在用户态,用户态驱动的利弊网上有很多的讨论,而且有些还上升到政治性上,这里不再多做讨论.不管用户态驱动还是内核态驱动,他们都有各自的缺点.内核态驱动的问题是:系统调用开销大:学习曲线陡峭:接口稳定性差:调试困难:bug致命:编程语言选择受限:而用户态驱动面临的挑战是:如何中断处理:如何DMA:如何管理设备的依赖关系:无法使用内核服务等.对此,<User-Space Device Drivers in Linux: A First Look> 一文有较详

打印更多的用户态段错误信息

    在调试上层程序时,经常会遇到的错误是段错误,当出现段错误时,系统往往只会给出一个 segmention error,而在没有更多的信息(默认不产生core dump),在这种情况下,可以通过修改内核启动参数来使能调试模式,让用户态出现段错误时,打印出更多的提示信息,有助于定位错误.      分析流程:      先从在内核态的段错误出发,当产生内核态的段错误时,通常会打印出如下字段:       Unable to handle kernel paging request at vir

从socket can中断到netlink用户态内核态通信

1. Linux中的进程间的通信机制源自于Unix平台上的进程通信机制.Unix的两大分支AT&T Unix和BSD Unix在进程通信实现机制上的各有所不同,前者形成了运行在单个计算机上的System V IPC,后者则实现了基于socket的进程间通信机制.同时linux也遵循IEEE制定的posix IPC标准,在三者的基础上实现以下几种主要的IPC机制:管道(Pipe)和命名管道(Name Pipe),信号(Signal),消息队列(Message queue),共享内存(Shared

Windows内核分析——内核调试机制的实现(NtCreateDebugObject、DbgkpPostFakeProcessCreateMessages、DbgkpPostFakeThreadMessages分析)

本文主要分析内核中与调试相关的几个内核函数. 首先是NtCreateDebugObject函数,用于创建一个内核调试对象,分析程序可知,其实只是一层对ObCreateObject的封装,并初始化一些结构成员而已. 我后面会写一些与window对象管理方面的笔记,会分析到对象的创建过程. //来自WRK1.2NTSTATUS NtCreateDebugObject ( OUT PHANDLE DebugObjectHandle, IN ACCESS_MASK DesiredAccess, IN P