首页
社区
课程
招聘
[翻译]Windows调试内幕系列二之Windows本地(Native)调试内幕
发表于: 2014-6-8 10:58 11422

[翻译]Windows调试内幕系列二之Windows本地(Native)调试内幕

2014-6-8 10:58
11422

Windows Native Debugging Internals



Introduction



In part two of this three part article series, the native interface to Windows debugging is dissected in detail. The reader is expected to have some basic knowledge of C and general NT Kernel architecture and semantics. Also, this is not an introduction on what debugging is or how to write a debugger. It is meant as a reference for experienced debugger writers, or curious security experts.



Windows本地(Native)调试内幕



作者: AlexIonescu


原文链接:


系列的第一部分链接:

http://bbs.pediy.com/showthread.php?t=188300



介绍



这篇文章是这个系列的第二部分,将要详细分析Windows调试的本地接口。读者应该具有C语言和一般的NT内核架构和语法的基本知识。同时,这篇文章不是介绍什么是调试,或者怎样编写一个调试器。它只是为有经验的调试器编写者,或者好奇的安全专家作一个参考。



Native Debugging



Now it's time to look at the native side of things, and how the wrapper layer inside ntdll.dll communicates with the kernel. The advantage of having the DbgUi layer is that it allows better separation between Win32 and the NT Kernel, which has always been a part of NT design. NTDLL and NTOSKRNL are built together, so it's normal for them to have intricate knowledge of each others. They share the same structures, they need to have the same system call IDs, etc. In a perfect world, the NT Kernel should have to know nothing about Win32.



本地调试



现在来看看Native内容的一面,以及位于ntdd.dll里的包装层是怎样和Windows内核通信的。具有DbgUi层的优点是,它允许Win32和NT内核之间更好地分离,这一直都是NT设计的一部分。NTDLL和NTOSKRNL是构建在一起的,所以它们彼此之间有复杂的联系,这是很正常的。它们共享相同的结构,它们需要有相同的系统调用身份,等等。最好的设计应该是,NT内核不应该知道关于Win32的任何细节。



Additionally, it helps anyone that wants to write debugging capabilities inside a native application, or to write a fully-featured native-mode debugger. Without DbgUi, one would have to call the Nt*DebugObject APIs manually, and do some extensive pre/post processing in some cases. DbgUi simplifies all this work to a simple call, and provides a clean interface to do it. If the kernel changes internally, DbgUi will probably stay the same, only its internal code would be modified.



此外,它可以帮助那些想要在本地应用程序中编写调试功能,或者编写一个全功能的本地模式调试器。如果没有DbgUi,他们将不得不手动调用Nt*DebugObject API,并在某些情况下,做一些扩展的预/后(pre/post)处理。DbgUi简化了所有这一切工作,通过一个简单的调用,并提供了一个干净的接口来完成。如果内核内部发生改变,DbgUi可能会保持不变,只有内部的代码将被修改。



We start our exploration with the function responsible for creating and associating a Debug Object with the current Process. Unlike in the Win32 world, there is a clear distinction between creating a Debug Object, and actually attaching to a process.



我们从一个函数开始探索,这个函数负责对当前进程创建并关联一个调试对象。不像在Win32的世界里,这里在创建一个调试对象和实际附加到进程之间有一个明显的区别。



NTSTATUS


NTAPI


DbgUiConnectToDbg(VOID)


{


    OBJECT_ATTRIBUTES ObjectAttributes;



    /* Don't connect twice */


    if (NtCurrentTeb()->DbgSsReserved[1]) return STATUS_SUCCESS;



    /* Setup the Attributes */


    InitializeObjectAttributes(&ObjectAttributes, NULL, 0, NULL, 0);



    /* Create the object */


    return ZwCreateDebugObject(&NtCurrentTeb()->DbgSsReserved[1],


                               DEBUG_OBJECT_ALL_ACCESS,


                               &ObjectAttributes,


                               TRUE);


}



As you can see, this is a trivial implementation, but it shows us two things. Firstly, a thread can only have one debug object associated to it, and secondly, the handle to this object is stored in the TEB's DbgSsReserved array field. Recall that in Win32, the first index, [0], is where the Thread Data was stored. We've now learnt that [1] is where the handle is stored.



正如你所看到的,这是一个简单的实现,但它告诉我们两件事。首先,一个线程只能有一个调试对象与之关联,其次,该对象的句柄存储在当前线程环境块(TEB)的DbgSsReserved数组域中。记得在Win32中,该数组的第0号索引,即DbgSsReserved[0]存储了线程数据结构(DBGSS_THREAD_DATA),我们现在已经知道该数组的第1号索引,即DbgSsReserved[1]存储了该对象的句柄。



Now let's see how attaching and detaching are done:



现在让我们来看看,附加和分离是怎样完成的:



NTSTATUS


NTAPI


DbgUiDebugActiveProcess(IN HANDLE Process)


{


    NTSTATUS Status;



    /* Tell the kernel to start debugging */


    Status = NtDebugActiveProcess(Process, NtCurrentTeb()->DbgSsReserved[1]);


    if (NT_SUCCESS(Status))


    {


        /* Now break-in the process */


        Status = DbgUiIssueRemoteBreakin(Process);


        if (!NT_SUCCESS(Status))


        {


            /* We couldn't break-in, cancel debugging */


            DbgUiStopDebugging(Process);


        }


    }



    /* Return status */


    return Status;


}



NTSTATUS


NTAPI


DbgUiStopDebugging(IN HANDLE Process)


{


    /* Call the kernel to remove the debug object */


    return NtRemoveProcessDebug(Process, NtCurrentTeb()->DbgSsReserved[1]);


}



Again, these are very simple implementations. We can learn, however, that the kernel is not responsible for actually breaking inside the remote process, but that this is done by the native layer. This DbgUiIssueRemoteBreakin API is also used by Win32 when calling DebugBreakProcess, so let's look at it:



再次,这是非常简单的实现。我们能够知道,然而,这个内核不负责实际中断远程进程,而是由本地层(native layer)来完成的。在Win32中,当调用DebugBreakProcess函数时,DbgUiIssueRemoteBreakin API也被使用,让我们来看看它:



NTSTATUS


NTAPI


DbgUiIssueRemoteBreakin(IN HANDLE Process)


{


    HANDLE hThread;


    CLIENT_ID ClientId;


    NTSTATUS Status;



    /* Create the thread that will do the breakin */


    Status = RtlCreateUserThread(Process,


                                 NULL,


                                 FALSE,


                                 0,


                                 0,


                                 PAGE_SIZE,


                                 (PVOID)DbgUiRemoteBreakin,


                                 NULL,


                                 &hThread,


                                 &ClientId);



    /* Close the handle on success */


    if(NT_SUCCESS(Status)) NtClose(hThread);



    /* Return status */


    return Status;


}



All it does is create a remote thread inside the process, and then return to the caller. Does that remote thread do anything magic? Let's see:



它的作用就是在目标进程中创建一个远程线程,然后返回至它的调用者。那么,远程线程做了什么魔法?让我们来看看:



VOID


NTAPI


DbgUiRemoteBreakin(VOID)


{


    /* Make sure a debugger is enabled; if so, breakpoint */


    if (NtCurrentPeb()->BeingDebugged) DbgBreakPoint();



    /* Exit the thread */


    RtlExitUserThread(STATUS_SUCCESS);


}



Nothing special at all; the thread makes sure that the process is really being debugged, and then issues a breakpoint. And, because this API is exported, you can call it locally from your own process to issue a debug break (but note that you will kill your own thread). In our look at the Win32 Debugging implementation, we've noticed that the actual debug handle is never used, and that calls always go through DbgUi. Then the NtSetInformationDebugObject system call was called, a special DbgUi API was called before, to actually get the debug object associated with the thread. This API also has a counterpart, so let's see both in action:



根本没有什么特别之处,这个线程确保目标进程正在被调试,然后产生一个断点。而且,因为DbgBreakPoint这个API已经导出,你可以在你自己的进程中本地调用它,用来产生一个调试断点(注意,这样将结束你自己的线程)。我们从Win32调试实现中看到,注意到,实际的调试句柄从来没有被使用过,并且这种调用总是通过DbgUi函数。之后NtSetInformationDebugObject系统调用被调用,在一个特殊的DbgUi API被调用之前,用来实际获取线程相关联的调试对象。这也有一个对应的API,让我们来看看这两个API的实现:



HANDLE


NTAPI


DbgUiGetThreadDebugObject(VOID)


{


    /* Just return the handle from the TEB */


    return NtCurrentTeb()->DbgSsReserved[1];


}



VOID


NTAPI


DbgUiSetThreadDebugObject(HANDLE DebugObject)


{


    /* Just set the handle in the TEB */


    NtCurrentTeb()->DbgSsReserved[1] = DebugObject;


}



For those familiar with object-oriented programming, this will seem similar to the concept of accessor and mutator methods. Even though Win32 has perfect access to this handle and could simply read it on its own, the NT developers decided to make DbgUi much like a class, and make sure access to the handle goes through these public methods. This design allows the debug handle to be stored anywhere else if necessary, and only these two APIs will require changes, instead of multiple DLLs in Win32.



对于那些熟悉面向对象编程的,这似乎类似于访问器和突变方法的概念(get和set方法)。即使Win32针对这个句柄具有完善的访问,并且它自己可以简单地读取句柄,NT开发者还是决定把DbgUi设计得像一个类,并确保通过这些公有方法来访问这个句柄。这种设计允许调试句柄根据需要可以存储在任何地方,且只有这两个API将需要改变,而不是Win32里多个DLL。



Now for a visit of the wait/continue functions, which under Win32 were simply wrappers:



现在来看看等待和继续(wait/continue)函数,在上层Win32它们是简单包装者:



NTSTATUS


NTAPI


DbgUiContinue(IN PCLIENT_ID ClientId,


              IN NTSTATUS ContinueStatus)


{


    /* Tell the kernel object to continue */


    return ZwDebugContinue(NtCurrentTeb()->DbgSsReserved[1],


                           ClientId,


                           ContinueStatus);


}



NTSTATUS


NTAPI


DbgUiWaitStateChange(OUT PDBGUI_WAIT_STATE_CHANGE DbgUiWaitStateCange,


                     IN PLARGE_INTEGER TimeOut OPTIONAL)


{


    /* Tell the kernel to wait */


    return NtWaitForDebugEvent(NtCurrentTeb()->DbgSsReserved[1],


                               TRUE,


                               TimeOut,


                               DbgUiWaitStateCange);


}



Not surprisingly, these functions are also wrappers in DbgUi. However, this is where things start to get interesting, since if you'll recall, DbgUi uses a completely different structure for debug events, called DBGUI_WAIT_STATE_CHANGE. There is one API that we have left to look at, which does the conversion, so first, let's look at the documentation for this structure:



毫不奇怪,在DbgUi层这些函数同样也是包装者。然而,这就是事情开始变得有趣了,如果你回忆一下,DbgUi为调试事件使用一个完全不同的结构,称为DBGUI_WAIT_STATE_CHANGE。这里有个API我们留到之后再看,它完成转换功能,首先,我们来看看这个结构的文档说明:



//


// User-Mode Debug State Change Structure


//


typedef struct _DBGUI_WAIT_STATE_CHANGE


{


    DBG_STATE NewState;


    CLIENT_ID AppClientId;


    union


    {


        struct


        {


            HANDLE HandleToThread;


            DBGKM_CREATE_THREAD NewThread;


        } CreateThread;


        struct


        {


            HANDLE HandleToProcess;


            HANDLE HandleToThread;


            DBGKM_CREATE_PROCESS NewProcess;


        } CreateProcessInfo;


        DBGKM_EXIT_THREAD ExitThread;


        DBGKM_EXIT_PROCESS ExitProcess;


        DBGKM_EXCEPTION Exception;


        DBGKM_LOAD_DLL LoadDll;


        DBGKM_UNLOAD_DLL UnloadDll;


    } StateInfo;


} DBGUI_WAIT_STATE_CHANGE, *PDBGUI_WAIT_STATE_CHANGE;



The fields should be pretty self-explanatory, so let's look at the DBG_STATE enumeration:



这个结构各个域应该是很好的自我解释,所以我们来看看DBG_STATE枚举:



//


// Debug States


//


typedef enum _DBG_STATE


{


    DbgIdle,


    DbgReplyPending,


    DbgCreateThreadStateChange,


    DbgCreateProcessStateChange,


    DbgExitThreadStateChange,


    DbgExitProcessStateChange,


    DbgExceptionStateChange,


    DbgBreakpointStateChange,


    DbgSingleStepStateChange,


    DbgLoadDllStateChange,


    DbgUnloadDllStateChange


} DBG_STATE, *PDBG_STATE;



If you take a look at the Win32 DEBUG_EVENT structure and associated debug event types, you'll notice some differences which might be useful to you. For starters, Exceptions, Breakpoints and Single Step exceptions are handled differently. In the Win32 world, only two distinctions are made: RIP_EVENT for exceptions, and EXCEPTION_DEBUG_EVENT for a debug event. Although code can later figure out if this was a breakpoint or single step, this information comes directly in the native structure. You will also notice that OUTPUT_DEBUG_STRING event is missing. Here, it's DbgUi that's at a disadvantage, since the information is sent as an Exception, and post-processing is required (which we'll take a look at soon). There are also two more states that Win32 does not support, which is the Idle state and the Reply Pending state. These don't offer much information from the point of view of a debugger, so they are ignored.



如果你对比一下Win32 DEBUG_EVENT结构和相关的调试事件类型,你将会注意到一些差异,这些差异可能对你有用(我把DEBUG_EVENT结构定义加到下面)。首先,异常,断点和单步异常的处理方式不同。在Win32世界里,只有两个区别:RIP_EVENT异常和EXCEPTION_DEBUG_EVENT调试事件。虽然,代码在后面能够搞清楚这是否是一个断点或者单步,这个信息直接来自本地结构。你同时也会注意到OUTPUT_DEBUG_STRING事件消失了。在这里,这是DbgUi处于劣势,因为这信息是作为一个异常发送的,同时后处理是必需的(我们很快就会看到)。还有两个状态是Win32不支持的,那是空闲(Idle)状态和回复未决(Reply Pending)状态。从一个调试器的角度来看,这些没有提供太多的信息,所以它们被忽略了。



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;



Now let's take a look at the actual structures seen in the unions:



现在来看看在共同体中的那些实际结构体定义:



//


// Debug Message Structures


//


typedef struct _DBGKM_EXCEPTION


{


    EXCEPTION_RECORD ExceptionRecord;


    ULONG FirstChance;


} DBGKM_EXCEPTION, *PDBGKM_EXCEPTION;



typedef struct _DBGKM_CREATE_THREAD


{


    ULONG SubSystemKey;


    PVOID StartAddress;


} DBGKM_CREATE_THREAD, *PDBGKM_CREATE_THREAD;



typedef struct _DBGKM_CREATE_PROCESS


{


    ULONG SubSystemKey;


    HANDLE FileHandle;


    PVOID BaseOfImage;


    ULONG DebugInfoFileOffset;


    ULONG DebugInfoSize;


    DBGKM_CREATE_THREAD InitialThread;


} DBGKM_CREATE_PROCESS, *PDBGKM_CREATE_PROCESS;



typedef struct _DBGKM_EXIT_THREAD


{


    NTSTATUS ExitStatus;


} DBGKM_EXIT_THREAD, *PDBGKM_EXIT_THREAD;



typedef struct _DBGKM_EXIT_PROCESS


{


    NTSTATUS ExitStatus;


} DBGKM_EXIT_PROCESS, *PDBGKM_EXIT_PROCESS;



typedef struct _DBGKM_LOAD_DLL


[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

上传的附件:
收藏
免费 3
支持
分享
最新回复 (4)
雪    币: 7059
活跃值: (3537)
能力值: ( LV12,RANK:340 )
在线值:
发帖
回帖
粉丝
2
沙发~
2014-6-8 12:18
0
雪    币: 413
活跃值: (807)
能力值: ( LV9,RANK:170 )
在线值:
发帖
回帖
粉丝
3
谢谢,下载来学习。
2014-6-9 08:13
0
雪    币: 719
活跃值: (777)
能力值: ( LV8,RANK:120 )
在线值:
发帖
回帖
粉丝
4
学习学习
2014-12-28 11:07
0
雪    币: 188
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
5
多谢楼主分享,学习学习。
2014-12-28 11:11
0
游客
登录 | 注册 方可回帖
返回