首页
社区
课程
招聘
[翻译]Windows调试内幕系列一之Windows用户模式调试内幕
发表于: 2014-5-27 08:46 19946

[翻译]Windows调试内幕系列一之Windows用户模式调试内幕

2014-5-27 08:46
19946

作者: AlexIonescu
原文链接:http://www.openrce.org/articles/full_view/24

本人第一次翻译外文,可能会有各种缺点,甚至错误,敬请同行专家和广大读者给予斧正。谢谢!

Windows User Mode Debugging Internals

Introduction

The internal mechanisms of what allows user-mode debugging to work have rarely ever been fully explained. Even worse, these mechanisms have radically changed in Windows XP, when much of the support was re-written, as well as made more subsystem portable by including most of the routines in ntdll, as part of the Native API. This three part series will explain this functionality, starting from the Win32 (kernel32) viewpoint all the way down (or up) to the NT Kernel (ntoskrnl) component responsible for this support, called Dbgk, while taking a stop to the NT System Library (ntdll) and its DbgUi component.

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用户模式调试内幕

介绍:
关于用户模式调试的内部机制本来就很少有完整的解释。更糟糕的是,在Windows XP中这些机制根本上又被改变了,这时关于此的支持需要被重写,同时通过包含在大部分位于ntdll中的例程来生成更方便的子系统,作为本地(Native) API的一部分。接下来将分三个部分系列来解释这个功能,从Win32(kerne32l)的视点开始向下(或向上)到达NT内核 (ntoskrnl),把负责这种支持的组件称为Dbgk,同时采取了停止到NT系统库(ntdll)和DbgUi组件。

这里要求读者具有C语言和一般的NT内核架构和语法的基本知识。这篇文章不是介绍什么是调试或者怎么编写一个调试器。它的目的是为有经验的调试器编写者,或者好奇的安全专家作参考。

Win32 Debugging

The Win32 subsystem of NT has allowed the debugging of processes ever since the first release, with later releases adding more features and debugging help libraries, related to symbols and other PE information. However, relatively few things have changed to the outside API user, except for the welcome addition of the ability to stop debugging a process, without killing it, which was added in Windows XP. This release of NT also contained several overhauls to the underlying implementation, which will be discussed in detail. However, one important side-effect of these changes was that LPC (and csrss.exe) were not used anymore, which allowed debugging of this binary to happen (previously, debugging this binary was impossible, since it was the one responsible for handling the kernel-to-user notifications).

Win32调试
NT的 Win32子系统从第一个版本发布开始就允许进程的调试,在之后的版本中又添加了更多的功能和调试帮助库,其中涉及到符号和其他PE信息。然而,对于外部的API用户,功能几乎没有什么改变,除了添加了受欢迎的停止对进程的调试,而不用结束进程,这种功能已经加入到Windows XP系统中。此版本的NT还包含了几处对底层实现的检查,这将要详细讨论。然而,这些变化的一个重要的副作用是,LPC(和csrss.exe)不再被使用,这使得调试此二进制得以发生(以前,调试此二进制是不可能的,因为它是负责处理内核层到应用层的通知)。

The basic Win32 APIs for dealing with debugging a process were simple: DebugActiveProcess, to attach, WaitForDebugEvent, to wait for debug events to come through, so that your debugging can handle them, and ContinueDebugEvent, to resume thread execution. The release of Windows XP added three more useful APIs: DebugActiveProcessStop, which allows you to stop debugging a process (detach), DebugSetProcessKillOnExit, which allows you to continue running a process even after its' been detached, and DebugBreakProcess, which allows you to perform a remote DebugBreak without having to manually create a remote thread. In Windows XP Service Pack 1, one more API was added, CheckRemoteDebuggerPresent. Much like its IsDebuggerPresent counterpart, this API allows you to check for a connected debugger in another process, without having to read the PEB remotely.

用于处理调试进程的基本Win32 API是很简单的:DebugActiveProcess,负责附加(attach),WaitForDebugEvent,等待调试事件的到来,这样你的调试就能处理它们,同时ContinueDebugEvent,负责恢复线程执行。Windows XP的发布版本增加了三个更有用的API:DebugActiveProcessStop,允许你停止调试一个进程(detach),DebugSetProcessKillOnExit,允许你继续运行一个进程,即使这个进程是在被停止调试之后,DebugBreakProcess,允许你完成一个远程DebugBreak,而不用手动创建一个远程线程。在Windows XP Service Pack 1中,又增加了一个API,CheckRemoteDebuggerPresent,它就像是IsDebuggerPresent的对应版本,这个API允许你检查一个进程是否连接着调试器,而不用远程地去读取进程的PED来进行判断。

Because of NT's architecture, these APIs, on recent versions of Windows (2003 will be used as an example, but the information applies to XP as well) do not much do much work themselves. Instead, they do the typical job of calling out the native functions required, and then process the output so that the Win32 caller can have it in a format that is compatible with Win9x and the original Win32 API definition. Let's look at these very simple implementations:

由于Windows NT的架构,在最新的Windows版本中(2003将作为一个例子使用,但是这些信息同样应用于XP上),这些API本身做很少的工作。相反,它们以一种典型的方法实现,即调用所需要的本地函数(native functions),然后处理输出,这样Win32调用者能够形成一种格式,就是能够兼容Win9x和原始的Win32 API定义。让我们来看看这些API很简单的实现:

BOOL
WINAPI
DebugActiveProcess(IN DWORD dwProcessId)
{
    NTSTATUS Status;
    HANDLE Handle;

    /* Connect to the debugger */
    Status = DbgUiConnectToDbg();
    if (!NT_SUCCESS(Status))
    {
        SetLastErrorByStatus(Status);
        return FALSE;
    }

    /* Get the process handle */
    Handle = ProcessIdToHandle(dwProcessId);
    if (!Handle) return FALSE;

    /* Now debug the process */
    Status = DbgUiDebugActiveProcess(Handle);
    NtClose(Handle);

    /* Check if debugging worked */
    if (!NT_SUCCESS(Status))
    {
        /* Fail */
        SetLastErrorByStatus(Status);
        return FALSE;
    }

    /* Success */
    return TRUE;
}

As you can see, the only work that's being done here is to create the initial connection to the user-mode debugging component, which is done through the DbgUi Native API Set, located in ntdll, which we'll see later. Because DbgUi uses handles instead of PIDs, the PID must first be converted with a simple helper function:

正如你所看到的,DebugActiveProcess函数唯一做的工作就是创建一个初始连接,用于连接到用户模式的调试组件,并且它是通过位于ntdll中的DbgUi Native API集合函数来实现的,这些DbgUi函数我们会在之后介绍。由于DbgUi函数使用进程句柄(handle)代替进程PID,所以PID首先需要被转换为句柄,通过下面一个简单的帮助函数:

HANDLE
WINAPI
ProcessIdToHandle(IN DWORD dwProcessId)
{
    NTSTATUS Status;
    OBJECT_ATTRIBUTES ObjectAttributes;
    HANDLE Handle;
    CLIENT_ID ClientId;

    /* If we don't have a PID, look it up */
    if (dwProcessId == -1) dwProcessId = (DWORD)CsrGetProcessId();

    /* Open a handle to the process */
    ClientId.UniqueProcess = (HANDLE)dwProcessId;
    InitializeObjectAttributes(&ObjectAttributes, NULL, 0, NULL, NULL);
    Status = NtOpenProcess(&Handle,
                           PROCESS_ALL_ACCESS,
                           &ObjectAttributes,
                           &ClientId);
    if (!NT_SUCCESS(Status))
    {
        /* Fail */
        SetLastErrorByStatus(Status);
        return 0;
    }

    /* Return the handle */
    return Handle;
}

If you are not familiar with Native API, it is sufficient to say that this code is the simple equivalent of an OpenProcess on the PID, so that a handle can be obtained. Going back to DebugActiveProcess, the final call which does the work is DbgUiDebugActiveProcess, which is again located in the Native API. After the connection is made, we can close the handle that we had obtained from the PID previously. Other APIs function much in the same way. Let's take a look at two of the newer XP ones:

如果你不熟悉Native API,你可以这样理解,这个代码可以简单地等价于用OpenProcess函数打开PID,这样获取进程句柄。回到DebugActiveProcess函数,最后调用DbgUiDebugActiveProcess函数来完成工作,它也是位于Native API中。在完成连接之后,我们可以关闭之前通过PID获取的句柄了。其它的API函数也是通过类似的这种方式来工作。接下来我们来看看两个在XP中新加入的函数:

BOOL
WINAPI
DebugBreakProcess(IN HANDLE Process)
{
    NTSTATUS Status;

    /* Send the breakin request */
    Status = DbgUiIssueRemoteBreakin(Process);
    if(!NT_SUCCESS(Status))
    {
        /* Failure */
        SetLastErrorByStatus(Status);
        return FALSE;
    }

    /* Success */
    return TRUE;
}

BOOL
WINAPI
DebugSetProcessKillOnExit(IN BOOL KillOnExit)
{
    HANDLE Handle;
    NTSTATUS Status;
    ULONG State;

    /* Get the debug object */
    Handle = DbgUiGetThreadDebugObject();
    if (!Handle)
    {
        /* Fail */
        SetLastErrorByStatus(STATUS_INVALID_HANDLE);
        return FALSE;
    }

    /* Now set the kill-on-exit state */
    State = KillOnExit;
    Status = NtSetInformationDebugObject(Handle,
                                       DebugObjectKillProcessOnExitInformation,
                                         &State,
                                         sizeof(State),
                                         NULL);
    if (!NT_SUCCESS(Status))
    {
        /* Fail */
        SetLastError(Status);
        return FALSE;
    }

    /* Success */
    return TRUE;
}

The first hopefully requires no explanation, as it's a simple wrapper, but let's take a look at the second. If you're familiar with the Native API, you'll instantly recognize the familiar NtSetInformationXxx type of API, which is used for setting various settings on the different types of NT Objects, such as files, processes, threads, etc. The interesting to note here, which is new to XP, is that debugging itself is also now done with a Debug Object. The specifics of this object will however be discussed later. For now, let's look at the function.

DebugBreakProcess函数不需要过多的解释,因为它是一个简单的包装,但是让我们来看看第二个函数。如果熟悉Native API,你立即就会识别出像NtSetInformationXxx类型的API,这类API用于对不同类型的NT对象(NT Objects)设置各种属性,比如文件、进程和线程等对象。这里需要注意的是,在XP中新加入一种对象,调试本身对应的调试对象(Debug Object)。这个对象的特别之处我们将在之后讨论。现在,我们先来看看DebugSetProcessKillOnExit函数:

The first API, DbgUiGetThreadDebugObject is another call to DbgUi, which will return a handle to the Debug Object associated with our thread (we'll see where this is stored later). Once we have the handle, we call a Native API which directly communicates with Dbgk (and not DbgUi), which will simply change a flag in the kernel's Debug Object structure. This flag, as we'll see, will be read by the kernel when detaching.

第一个API,DbgUiGetThreadDebugObject也是一个DbgUi函数,它将返回一个调试对象的句柄,此调试对象是与我们线程相关联的(之后我们会看见线程是存放在什么地方)。一旦有了句柄,我们就调用一个Native API NtSetInformationDebugObject,它直接与Dbgk(而不是DbgUi)通信,它将简单地改变一个标志,此标志位于内核层的调试对象结构中。这个标志,正如我们所看到的,在断开连接过程中(Detaching)它将被内核读取用于判断。

A similar function to this one is the CheckRemoteDebuggerPresent, which uses the same type of NT semantics to obtain the information about the process:

一个相似的函数,CheckRemoteDebuggerPresent使用相同类型的NT语法来获取关于进程的信息:

BOOL
WINAPI
CheckRemoteDebuggerPresent(IN HANDLE hProcess,
                           OUT PBOOL pbDebuggerPresent)
{
    HANDLE DebugPort;
    NTSTATUS Status;

    /* Make sure we have an output and process*/
    if (!(pbDebuggerPresent) || !(hProcess))
    {
        /* Fail */
        SetLastError(ERROR_INVALID_PARAMETER);
        return FALSE;
    }

    /* Check if the process has a debug object/port */
    Status = NtQueryInformationProcess(hProcess,
                                       ProcessDebugPort,
                                       (PVOID)&DebugPort,
                                       sizeof(HANDLE),
                                       NULL);
    if (NT_SUCCESS(Status))
    {
        /* Return the current state */
        *pbDebuggerPresent = (DebugPort) ? TRUE : FALSE;
        return TRUE;
    }

    /* Otherwise, fail */
    SetLastErrorByStatus(Status);
    return FALSE;
}

As you can see, another NtQuery/SetInformationXxx API is being used, but this time for the process. Although you probably now that to detect debugging, one can simple check if NtCurrentPeb()->BeingDebugged, there exists another way to do this, and this is by querying the kernel. Since the kernel needs to communicate with user-mode on debugging events, it needs some sort of way of doing this. Before XP, this used to be done through an LPC port, and now, through a Debug Object (which shares the same pointer, however).

正如你所看到的,NtQuery/SetInformationXxx API被使用,但是这次是处理进程对象。虽然你可能知道通过简单地检查进程PEB的BeingDebugged标志(NtCurrentPeb()->BeingDebugged),来检测进程是否处于被调试状态,这里存在另外一种方法,通过查询内核。因为内核在调试事件上需要与用户模式通信,它需要通过某种方式来完成这事。在XP之前,这通过一个LPC端口来完成,而现在,通过一个调试对象来完成(但是内核与用户模式共享相同的指针)。

Since is located in the EPROCESS structure in kernel mode, we do a query, using the DebugPort information class. If EPROCESS->DebugPort is set to something, then this API will return TRUE, which means that the process is being debugged. This trick can also be used for the local process, but it's much faster to simply read the PEB. One can notice that although some applications like to set Peb->BeingDebugged to FALSE to trick anti-debugging programs, there is no way to set DebugPort to NULL, since the Kernel itself would not let you debug (and you also don't have access to kernel structures).

因为在内核模式里调试对象是位于EPROCESS结构中,我们查询调试对象,需要使用调试端口(DebugPort)信息类型作为查询类型。如果EPROCESS->DebugPort被设置为某个有效值,然后这个API将会返回TRUE,这表示这个进程正在被调试。这种计策同样适用于本地进程,但是这比简单地读取PEB更快。这里有一点需要注意,虽然有很多应用程序喜欢把Peb->BeingDebugged标志设置为FALSE,来欺骗反调试程序,这里却不能把EPROCESS->DebugPort置为NULL,因为内核本身将不允许你进行调试(同时你也不能访问内核结构)。

With that in mind, let's see how the gist of the entire Win32 debugging infrastructure, WaitForDebugEvent, is implemented. This needs to be shown before the much-simpler ContinueDebugEvent/DebugActiveProcessStop, because it introduces Win32's high-level internal structure that it uses to wrap around DbgUi.

鉴于上面介绍的,我们来看看整个Win32调试基础设施的要点实现,WaitForDebugEvent函数。在介绍更简单的ContinueDebugEvent/DebugActiveProcessStop函数之前,需要先介绍WaitForDebugEvent函数,因为它介绍了Win32的高层次内部结构,其采用封装DbgUi函数。

BOOL
WINAPI
WaitForDebugEvent(IN LPDEBUG_EVENT lpDebugEvent,
                  IN DWORD dwMilliseconds)
{
    LARGE_INTEGER WaitTime;
    PLARGE_INTEGER Timeout;
    DBGUI_WAIT_STATE_CHANGE WaitStateChange;
    NTSTATUS Status;

    /* Check if this is an infinite wait */
    if (dwMilliseconds == INFINITE)
    {
        /* Under NT, this means no timer argument */
        Timeout = NULL;
    }
    else
    {
        /* Otherwise, convert the time to NT Format */
        WaitTime.QuadPart = UInt32x32To64(-10000, dwMilliseconds);
        Timeout = &WaitTime;
    }

    /* Loop while we keep getting interrupted */
    do
    {
        /* Call the native API */
        Status = DbgUiWaitStateChange(&WaitStateChange, Timeout);
    } while ((Status == STATUS_ALERTED) || (Status == STATUS_USER_APC));

    /* Check if the wait failed */
    if (!(NT_SUCCESS(Status)) || (Status != DBG_UNABLE_TO_PROVIDE_HANDLE))
    {
        /* Set the error code and quit */
        SetLastErrorByStatus(Status);
        return FALSE;
    }

    /* Check if we timed out */
    if (Status == STATUS_TIMEOUT)
    {
        /* Fail with a timeout error */
        SetLastError(ERROR_SEM_TIMEOUT);
        return FALSE;
    }

    /* Convert the structure */
    Status = DbgUiConvertStateChangeStructure(&WaitStateChange, lpDebugEvent);
    if (!NT_SUCCESS(Status))
    {
        /* Set the error code and quit */
        SetLastErrorByStatus(Status);
        return FALSE;
    }

    /* Check what kind of event this was */
    switch (lpDebugEvent->dwDebugEventCode)
    {
        /* New thread was created */
        case CREATE_THREAD_DEBUG_EVENT:

            /* Setup the thread data */
            SaveThreadHandle(lpDebugEvent->dwProcessId,
                             lpDebugEvent->dwThreadId,
                             lpDebugEvent->u.CreateThread.hThread);
            break;

        /* New process was created */
        case CREATE_PROCESS_DEBUG_EVENT:

            /* Setup the process data */
            SaveProcessHandle(lpDebugEvent->dwProcessId,
                              lpDebugEvent->u.CreateProcessInfo.hProcess);

            /* Setup the thread data */
            SaveThreadHandle(lpDebugEvent->dwProcessId,
                             lpDebugEvent->dwThreadId,
                             lpDebugEvent->u.CreateThread.hThread);
            break;

        /* Process was exited */
        case EXIT_PROCESS_DEBUG_EVENT:

            /* Mark the thread data as such */
            MarkProcessHandle(lpDebugEvent->dwProcessId);
            break;

        /* Thread was exited */
        case EXIT_THREAD_DEBUG_EVENT:

            /* Mark the thread data */
            MarkThreadHandle(lpDebugEvent->dwThreadId);
            break;

        /* Nothing to do for anything else */
        default:
            break;
    }

    /* Return success */
    return TRUE;
}

First, let's look at the DbgUi APIs present. The first, DbgUiWaitStateChange is the Native version of WaitForDebugEvent, and it's responsible for doing the actual wait on the Debug Object, and getting the structure associated with this event. However, DbgUi uses its own internal structures (which we'll show later) so that the Kernel can understand it, while Win32 has had much different structures defined in the Win9x ways. Therefore, one needs to convert this to the Win32 representation, and the DbgUiConvertStateChange API is what does this conversion, returning the LPDEBUG_EVENT Win32 structure that is backwards-compatible and documented on MSDN.

首先,让我们来看看出现的DbgUi API。第一,DbgUiWaitStateChange函数是WaitForDebugEvent函数的原始版本(Native version),它负责做实际等待调试对象的工作,同时获取与调试事件相关联的结构。但是,DbgUi函数使用它们自己的内部结构(我们在后面看到),这样内核能够明白它,然而Win32采用Win9x中的方式定义了很大不同的结构。因此,需要转换为Win32表示的结构,DbgUiConvertStateChange API完成这种转换,返回LPDEBUG_EVENT Win32结构,这是向后兼容的并且记录在MSDN中。

What follows after is a switch which is interested in the creation or deletion of a new process or thread. Four APIs are used: SaveProcessHandle and SaveThreadHandle, which save these respective handles (remember that a new process must have an associated thread, so the thread handle is saved as well), and MarkProcessHandle and MarkThreadHandle, which flag these handles as being exited. Let's look as this high-level framework in detail.

接下来是一个开关,它处理一个新进程或者线程的创建或者删除。四个API被使用:SaveProcessHandle和SaveThreadHandle,它们保存代表进程和线程的句柄(记住:一个新进程必须有一个与之关联的线程,所以这个线程句柄同时也要被保存),MarkProcessHandle和MarkThreadHandle,它们标识进程和线程句柄将要退出。让我们来看看这几个高层次框架的实现细节。

VOID
WINAPI
SaveProcessHandle(IN DWORD dwProcessId,
                  IN HANDLE hProcess)
{
    PDBGSS_THREAD_DATA ThreadData;

    /* Allocate a thread structure */
    ThreadData = RtlAllocateHeap(RtlGetProcessHeap(),
                                 0,
                                 sizeof(DBGSS_THREAD_DATA));
    if (!ThreadData) return;

    /* Fill it out */
    ThreadData->ProcessHandle = hProcess;
    ThreadData->ProcessId = dwProcessId;
    ThreadData->ThreadId = 0;
    ThreadData->ThreadHandle = NULL;
    ThreadData->HandleMarked = FALSE;

    /* Link it */
    ThreadData->Next = DbgSsGetThreadData();
    DbgSsSetThreadData(ThreadData);
}

This function allocates a new structure, DBGSS_THREAD_DATA, and simply fills it out with the Process handle and ID that was sent. Finally, it links it with the current DBGSS_THREAD_DATA structure, and set itself as the new current one (thus creating a circular list of DBGSS_THREAD_DATA structures). Let's take a look as this structure:

这个函数分配了一个DBGSS_THREAD_DATA结构缓冲区,并且简单地用传进来的进程句柄和ID填充相关字段。最后,它连接到当前的DBGSS_THREAD_DATA结构上,同时设置自己为当前的DBGSS_THREAD_DATA结构(因此创建了一个DBGSS_THREAD_DATA结构的循环链表)。让我们来看看这个结构:

typedef struct _DBGSS_THREAD_DATA
{
    struct _DBGSS_THREAD_DATA *Next;
    HANDLE ThreadHandle;
    HANDLE ProcessHandle;
    DWORD ProcessId;
    DWORD ThreadId;
    BOOLEAN HandleMarked;
} DBGSS_THREAD_DATA, *PDBGSS_THREAD_DATA;

This generic structure thus allows storing process/thread handles and IDs, as well as the flag which we've talked about in regards to MarkProcess/ThreadHandle. We've also seen some DbgSsSet/GetThreadData functions, which will show us where this circular array of structures is located. Let's look at their implementations:

这类结构从而允许存储进程/线程的句柄和ID,以及标志,我们已经在关于MarkProcess/ThreadHandle函数时谈到过。我们也看到DbgSsSet/GetThreadData函数,它们给我们展示了这个结构的循环数组位于什么地方。让我们来看看它们的实现:

#define DbgSsSetThreadData(d) \
    NtCurrentTeb()->DbgSsReserved[0] = d

#define DbgSsGetThreadData() \
    ((PDBGSS_THREAD_DATA)NtCurrentTeb()->DbgSsReserved[0])

Easy enough, and now we know what the first element of the mysterious DbgSsReserved array in the TEB is. Although you can probably guess the SaveThreadHandle implementation yourself, let's look at it for completeness's sake:

很容易,现在我们已经知道在TEB中神秘的DbgSsReserved数组的第一个元素是什么了。虽然,你可能猜到SaveThreadHandle的实现,为了完整性的目的,我们还是来看看它的实现:

VOID
WINAPI
SaveThreadHandle(IN DWORD dwProcessId,
                 IN DWORD dwThreadId,
                 IN HANDLE hThread)
{
    PDBGSS_THREAD_DATA ThreadData;

    /* Allocate a thread structure */
    ThreadData = RtlAllocateHeap(RtlGetProcessHeap(),
                                 0,
                                 sizeof(DBGSS_THREAD_DATA));
    if (!ThreadData) return;

    /* Fill it out */
    ThreadData->ThreadHandle = hThread;
    ThreadData->ProcessId = dwProcessId;
    ThreadData->ThreadId = dwThreadId;
    ThreadData->ProcessHandle = NULL;
    ThreadData->HandleMarked = FALSE;

    /* Link it */
    ThreadData->Next = DbgSsGetThreadData();
    DbgSsSetThreadData(ThreadData);
}

As expected, nothing new here. The MarkThread/Process functions as just as straight-forward:

正如预期的那样,这里没什么新的东西。MarkThread/ProcessHandle函数和前面一样:

VOID
WINAPI
MarkThreadHandle(IN DWORD dwThreadId)
{
    PDBGSS_THREAD_DATA ThreadData;

    /* Loop all thread data events */
    ThreadData = DbgSsGetThreadData();
    while (ThreadData)
    {
        /* Check if this one matches */
        if (ThreadData->ThreadId == dwThreadId)
        {
            /* Mark the structure and break out */
            ThreadData->HandleMarked = TRUE;
            break;
        }

        /* Move to the next one */
        ThreadData = ThreadData->Next;
    }
}

VOID
WINAPI
MarkProcessHandle(IN DWORD dwProcessId)
{
    PDBGSS_THREAD_DATA ThreadData;

    /* Loop all thread data events */
    ThreadData = DbgSsGetThreadData();
    while (ThreadData)
    {
        /* Check if this one matches */
        if (ThreadData->ProcessId == dwProcessId)
        {
            /* Make sure the thread ID is empty */
            if (!ThreadData->ThreadId)
            {
                /* Mark the structure and break out */
                ThreadData->HandleMarked = TRUE;
                break;
            }
        }

        /* Move to the next one */
        ThreadData = ThreadData->Next;
    }
}

Notice that the only less-than-trivial implementation detail is that the array needs to be parsed in order to find the matching Process and Thread ID. Now that we've taken a look at these structures, let's see the associated ContinueDebugEvent API, which picks up after a WaitForDebugEvent API in order to resume the thread.

请注意,唯一不平常的实现细节是为了找到匹配的进程和线程ID,从而需要解析DBGSS_THREAD_DATA结构数组。我们已经看了一下这些结构,现在让我们来看看相关联的ContinueDebugEvent API,它在WaitForDebugEvent API之后获取状态,为了能够恢复线程。

BOOL
WINAPI
ContinueDebugEvent(IN DWORD dwProcessId,
                   IN DWORD dwThreadId,
                   IN DWORD dwContinueStatus)
{
    CLIENT_ID ClientId;
    NTSTATUS Status;

    /* Set the Client ID */
    ClientId.UniqueProcess = (HANDLE)dwProcessId;
    ClientId.UniqueThread = (HANDLE)dwThreadId;

    /* Continue debugging */
    Status = DbgUiContinue(&ClientId, dwContinueStatus);
    if (!NT_SUCCESS(Status))
    {
        /* Fail */
        SetLastErrorByStatus(Status);
        return FALSE;
    }

    /* Remove the process/thread handles */
    RemoveHandles(dwProcessId, dwThreadId);

    /* Success */
    return TRUE;
}

Again, we're dealing with a DbgUI API, DbgUiContinue, which is going to do all the work for us. Our only job is to call RemoveHandles, which is part of the high-level structures that wrap DbgUi. This functions is slightly more complex then what we've seen, because we're given PID/TIDs, so we need to do some lookups:

这里我们又涉及了一个DbgUI API,DbgUiContinue,对于我们来说,它将处理所有的工作。我们唯一的工作就是调用RemoveHandles函数,它是高层次结构的一部分,包含了DbgUi。正如我们看到的,这个函数稍微有点复杂,因为我们同时传入了进程ID和线程ID,所以我们需要做些查询:

VOID
WINAPI
RemoveHandles(IN DWORD dwProcessId,
              IN DWORD dwThreadId)
{
    PDBGSS_THREAD_DATA ThreadData;

    /* Loop all thread data events */
    ThreadData = DbgSsGetThreadData();
    while (ThreadData)
    {
        /* Check if this one matches */
        if (ThreadData->ProcessId == dwProcessId)
        {
            /* Make sure the thread ID matches too */
            if (ThreadData->ThreadId == dwThreadId)
            {
                /* Check if we have a thread handle */
                if (ThreadData->ThreadHandle)
                {
                    /* Close it */
                    CloseHandle(ThreadData->ThreadHandle);
                }

                /* Check if we have a process handle */
                if (ThreadData->ProcessHandle)
                {
                    /* Close it */
                    CloseHandle(ThreadData->ProcessHandle);
                }

                /* Unlink the thread data */
                DbgSsSetThreadData(ThreadData->Next);

                /* Free it*/
                RtlFreeHeap(RtlGetProcessHeap(), 0, ThreadData);

                /* Move to the next structure */
                ThreadData = DbgSsGetThreadData();
                continue;
            }
        }

        /* Move to the next one */
        ThreadData = ThreadData->Next;
    }
}

Not much explaining is required. As we parse the circular buffer, we try to locate a structure which matches the PID and TID that we were given. Once it's been located, we check if a handle is associated with the thread and the process. If it is, then we can now close the handle.

这里不需要做过多的解释。当我们解析循环缓冲区,我们试图找到和给定的进程ID和线程ID相匹配的结构。一旦这个结构被找到,我们检查与进程和线程相关联的句柄,如果句柄存在,我们关闭这些句柄。

Therefore, the use of this high-level Win32 mechanism is now apparent: it's how we can associate handles to IDs, and close them when cleaning up or continuing. This is because these handles were not opened by Win32, but behind its back by Dbgk. Once the handles are closed, we unlink this structure by changing the TEB pointer to the next structure in the array, and we then free our own Array. We then resume parsing from the next structure on (because more than one such structure could be associated with this PID/TID).

因此,这种高层次Win32机制的使用是明显的:它是我们如何能够关联句柄和ID,当清理或者继续时关闭句柄。这是因为这些句柄不是被Win32打开的,而是其背后的Dbgk。一旦这些句柄都被关闭了,我们通过改变TEB中相关指针指向下一个结构,把该结构与循环数组断开连接, 并且释放我们自己的结构缓冲区。然后我们从下一个结构开始恢复解析(因为不止一个这样的结构能与这个进程ID和线程ID相关联)。

Finally, one last piece of the Win32 puzzle is missing in our analysis, and this is the detach function, which was added in XP. Let's take a look at its trivial implementation:

最后,Win32拼图的最后一块在我们的分析中失踪了,它就是拆卸函数(detach function),在XP系统中加入的。让我们来看看它的简单实现:

BOOL
WINAPI
DebugActiveProcessStop(IN DWORD dwProcessId)
{
    NTSTATUS Status;
    HANDLE Handle;

    /* Get the process handle */
    Handle = ProcessIdToHandle(dwProcessId);
    if (!Handle) return FALSE;

    /* Close all the process handles */
    CloseAllProcessHandles(dwProcessId);

    /* Now stop debgging the process */
    Status = DbgUiStopDebugging(Handle);
    NtClose(Handle);

    /* Check for failure */
    if (!NT_SUCCESS(Status))
    {
        /* Fail */
        SetLastError(ERROR_ACCESS_DENIED);
        return FALSE;
    }

    /* Success */
    return TRUE;
}

It couldn't really get any simpler. Just like for attaching, we first convert the PID to a handle, and then use a DbgUi call (DbgUiStopDebugging) with this process handle in order to detach ourselves from the process. There's one more call being made here, which is CloseAllProcessHandles. This is part of Win32's high-level debugging on top of DbgUi, which we've seen just earlier. This routine is very similar to RemoveHandles, but it only deals with a Process ID, so the implementation is simpler:

这真的不能再简单了。就像附加一样,我们首先把PID转换为句柄,然后通过该进程句柄用一个DbgUi函数(DbgUiStopDebugging)把我们自己从目标进程中拆卸下来。这里还多了一个函数调用,CloseAllProcessHandles。这是在DbgUi上层的Win32高层次调试的一部分,我们之前已经看到过。这个例程和RemoveHandles很相似,但它只涉及到进程ID,所以它的实现更加简单:

VOID
WINAPI
CloseAllProcessHandles(IN DWORD dwProcessId)
{
    PDBGSS_THREAD_DATA ThreadData;

    /* Loop all thread data events */
    ThreadData = DbgSsGetThreadData();
    while (ThreadData)
    {
        /* Check if this one matches */
        if (ThreadData->ProcessId == dwProcessId)
        {
            /* Check if we have a thread handle */
            if (ThreadData->ThreadHandle)
            {
                /* Close it */
                CloseHandle(ThreadData->ThreadHandle);
            }

            /* Check if we have a process handle */
            if (ThreadData->ProcessHandle)
            {
                /* Close it */
                CloseHandle(ThreadData->ProcessHandle);
            }

            /* Unlink the thread data */
            DbgSsSetThreadData(ThreadData->Next);

            /* Free it*/
            RtlFreeHeap(RtlGetProcessHeap(), 0, ThreadData);

            /* Move to the next structure */
            ThreadData = DbgSsGetThreadData();
            continue;
        }

        /* Move to the next one */
        ThreadData = ThreadData->Next;
    }
}

And this completes our analysis of the Win32 APIs! Let's take a look at what we've learnt:
•The actual debugging functionality is present in a module called Dbgk inside the Kernel.
•It's accessible through the DbgUi Native API interface, located inside the NT System Library, ntdll.
•Dbgk implements debugging functionality through an NT Object, called a Debug Object, which also provides an NtSetInformation API in order to modify certain flags.
•The Debug Object associated to a thread can be retrieved with DbgUiGetThreadObject, but we have not yet shown where this is stored.
•Checking if a process is being debugged can be done by using NtQueryInformationProcess and using the DebugPort information class. This cannot be cheated without a rootkit.
•Because Dbgk opens certain handles during Debug Events, Win32 needs a way to associated IDs and handles, and uses a circular array of structures called DBGSS_THREAD_DATA to store this in the TEB's DbgSsReserved[0] member.

这就完成了我们的Win32 API的分析!让我们来看看我们学到了些什么:
1、        实际的调试功能是位于内核里被称为Dbgk的模块;
2、        它是通过访问DbgUi Native API接口,位于NT系统库ntdll里;
3、        Dbgk通过一个NT对象来实现调试功能,被称为调试对象(Debug Object),同时还提供NtSetInformation API来修改某些标志;
4、        调试对象与一个线程相关联,此线程可以通过DbgUiGetThreadObject函数获取,但是我们还没有展示出该线程存储在调试对象的什么地方;
5、        检查一个进程是否正在被调试,可以通过NtQueryInformationProcess函数和DebugPort信息类型来查询。这种方法从本质上讲不能被Rootkit所欺骗,但是你可以Hook NtQueryInformationProcess函数。
6、        因为在调试事件(Debug Events)期间,Dbgk会打开一些句柄,Win32需要一种方式来关联ID和句柄,同时使用DBGSS_THREAD_DATA结构的循环数组来保存,此循环数组为TEB结构的DbgSsReserved[0]成员。


[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)

收藏
免费 3
支持
分享
最新回复 (17)
雪    币: 134
活跃值: (11)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
2
支持一个
2014-5-27 08:59
0
雪    币: 459
活跃值: (398)
能力值: ( LV8,RANK:120 )
在线值:
发帖
回帖
粉丝
3
从Win32(kerne32l)---》从Win32(kerne32)
2014-5-27 09:36
0
雪    币: 63
活跃值: (40)
能力值: ( LV5,RANK:60 )
在线值:
发帖
回帖
粉丝
4
记下了,谢谢!

接下来将分三个部分系列来解释这个功能,从Win32(kerne32)的视点开始向下(或向上)到达NT内核 (ntoskrnl),把负责这种支持的组件称为Dbgk,同时采取了停止到NT系统库(ntdll)和DbgUi组件。
2014-5-27 09:50
0
雪    币: 15
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
5
最近正在学习这方面内容,楼主的文章发的很及时啊,期待楼主的下一篇文章。
2014-5-27 10:23
0
雪    币: 1305
活跃值: (228)
能力值: ( LV5,RANK:75 )
在线值:
发帖
回帖
粉丝
6
支持楼主啊,楼主能整理发一份word的吗?再开一个导航帖
2014-5-27 12:27
0
雪    币: 2664
活跃值: (3401)
能力值: ( LV13,RANK:1760 )
在线值:
发帖
回帖
粉丝
7
翻译是个辛苦活,支持...  以前写调试器的时候看过相关的文章...
2014-5-27 12:35
0
雪    币: 63
活跃值: (40)
能力值: ( LV5,RANK:60 )
在线值:
发帖
回帖
粉丝
8
这个系列还有两篇,打算接下来一周一篇,等翻译完了,再做一篇导航帖。
2014-5-27 14:45
0
雪    币: 124
活跃值: (469)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
9
谢谢楼主,支持楼主下一篇
2014-5-27 19:30
0
雪    币: 100
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
10
这些东西,软件调试里面不是说得很清楚么
2014-5-28 00:07
0
雪    币: 108
活跃值: (15)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
11
mark一下,收藏
2014-5-28 04:46
0
雪    币: 102
活跃值: (19)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
12
最近一直在找这方面的资料看啊。。。谢谢楼主,期待二三篇
2014-5-28 18:59
0
雪    币: 242
活跃值: (448)
能力值: ( LV11,RANK:188 )
在线值:
发帖
回帖
粉丝
13
学习学习了,感谢LZ无私分享
2014-5-29 07:37
0
雪    币: 44
活跃值: (186)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
14
顶LZ,正想看这方面的,LZ就给送出来了
2014-5-29 23:23
0
雪    币: 135
活跃值: (25)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
15
呵呵,楼主发这贴有点多余了,推荐系统的看完<软件调试>这本书,对新手入门很有帮助!!!
2014-6-1 22:23
0
雪    币: 144
活跃值: (38)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
16
不多余,一点都不多余,哪有时间看这么厚的书
2014-6-3 15:49
0
雪    币: 1157
活跃值: (847)
能力值: ( LV8,RANK:150 )
在线值:
发帖
回帖
粉丝
17
翻译过程中,还有个别错误需要修正的,支持
2014-6-4 11:54
0
雪    币: 63
活跃值: (40)
能力值: ( LV5,RANK:60 )
在线值:
发帖
回帖
粉丝
18
错误或者翻译得不完善的地方肯定是有的,欢迎指出,谢谢!
2014-6-4 15:38
0
游客
登录 | 注册 方可回帖
返回
//