内核用户模式调试支持(Dbgk)

简介

将详细分析Windows调试的内核模式接口。希望读者对C和通用NT内核体系结构和语义有一些基本的了解。此外,这并不是介绍什么是调试或如何编写调试器。它可以作为经验丰富的调试器编写人员或好奇的安全专家的参考。

内核用户模式调试支持

最后一块拼图以内核模式存在,并提供了我们到目前为止看到的事件和结构,以便调试可以工作。Dbgk不依赖KD,它是一个完全不同的组件,因为Windows XP提供了自己的对象和系统调用来管理它。以前的Windows版本没有这样的对象,而是依赖于静态数据结构,该结构将在内存中进行分析,然后用于通过Windows的本地过程调用(LPC)机制发送的各种通知。

这些系统调用的可用性和调试对象的存在的一个好处是内核模式驱动程序也可以参与用户模式调试。虽然这可能不是这个新设计的目标之一,但它是一些人应该感兴趣的特性。虽然没有导出实际的Nt*调用,但是它们仍然可以被知道其系统调用ID的驱动程序访问。即使这个数字在每个OS版本之间发生变化,在驱动程序中保留一个表还是相对容易的。通过将TDI接口添加到这样的驱动程序中,可以开发一个高速远程调试器驱动程序,它将完全没有用户模式组件,从而允许远程调试机器上的每个进程。

我们要做的第一件事是查看实现用户模式调试的实际对象,DEBUG_OBJECT:

//
// Debug Object
//
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;

如您所见,对象本身是用户模式使用WaitForDebugEvent的实际事件、实际调试事件列表、锁和与此调试会话相关的某些标志(例如调试器是否已连接,以及在断开连接时是否应终止进程)的轻量级包装器。因此,我们更感兴趣的结构是调试事件结构:

//
// Debug Event
//
typedef struct _DEBUG_EVENT
{
    LIST_ENTRY EventList;
    KEVENT ContinueEvent;
    CLIENT_ID ClientId;
    PEPROCESS Process;
    PETHREAD Thread;
    NTSTATUS Status;
    ULONG Flags;
    PETHREAD BackoutThread;
    DBGKM_MSG ApiMsg;
} DEBUG_EVENT, *PDEBUG_EVENT;

这个结构包含与调试事件相关的所有数据。由于许多事件可以在调用者在用户模式下执行WaitForDebugEvent之前排队,因此调试事件必须与调试对象链接在一起,这就是事件列表的用途。
其他一些成员持有发出通知的事件的PID和TID,以及在内核模式下指向相应进程和线程对象的指针。此结构中的事件用于在对调试器消息的响应可用时在内部通知内核。此响应通常以来自Win32的continuedbugevent调用的形式出现,该调用将发出事件信号。
调试事件中包含的最终结构是正在发送的实际API消息,其中包含用户模式将看到的数据,并且是DBGUI_WAIT_STATE_CHANGE结构的内核模式表示。没错,内核还有另一种表示调试事件的方式,它也需要稍后进行转换,以便本机调试接口能够理解它。
从下面的结构中可以看出,好的一面是大多数字段都保持不变,内核内部仍然使用DBGKM结构,DbgUi结构中已经显示了DBGKM结构。但是,内核不使用DBG_STATE常量,而是使用另一种称为API消息编号的常量,如下所示:

//
// Debug Message API Number
//
typedef enum _DBGKM_APINUMBER
{
    DbgKmExceptionApi = 0,
    DbgKmCreateThreadApi = 1,
    DbgKmCreateProcessApi = 2,
    DbgKmExitThreadApi = 3,
    DbgKmExitProcessApi = 4,
    DbgKmLoadDllApi = 5,
    DbgKmUnloadDllApi = 6,
    DbgKmErrorReportApi = 7,
    DbgKmMaxApiNumber = 8,
} DBGKM_APINUMBER;

这些API编号是自解释的,仍然保留以与旧的LPC机制兼容。内核将把它们转换为DbgUi所期望的实际调试状态。现在让我们看看调试消息结构本身,它与以前版本的windows中使用的LPC机制上的相同消息相匹配:

//
// LPC Debug Message
//
typedef struct _DBGKM_MSG
{
    PORT_MESSAGE h;
    DBGKM_APINUMBER ApiNumber;
    ULONG ReturnedStatus;
    union
    {
        DBGKM_EXCEPTION Exception;
        DBGKM_CREATE_THREAD CreateThread;
        DBGKM_CREATE_PROCESS CreateProcess;
        DBGKM_EXIT_THREAD ExitThread;
        DBGKM_EXIT_PROCESS ExitProcess;
        DBGKM_LOAD_DLL LoadDll;
        DBGKM_UNLOAD_DLL UnloadDll;
    };
} DBGKM_MSG, *PDBGKM_MSG;

当然,出于我们的目的,我们可以忽略结构中的PORT_MESSAGE部分,因为我们将不会关注DbgSs(处理LPC消息的组件和在Windows XP之前包装DbgUi的层)。
现在我们知道了结构的含义,我们可以开始研究一些Native API函数(系统调用),这些函数将调试对象包装成另一个对象,如事件或信号量。
第一个系统调用是必需的,我们在第2部分的DbgUi中看到的是NtCreateDebugObject,它将返回一个调试对象的句柄,可以在稍后附加和等待。实现相当简单:

NTSTATUS
NTAPI
NtCreateDebugObject(OUT PHANDLE DebugHandle,
                    IN ACCESS_MASK DesiredAccess,
                    IN POBJECT_ATTRIBUTES ObjectAttributes,
                    IN BOOLEAN KillProcessOnExit)
{
    KPROCESSOR_MODE PreviousMode = ExGetPreviousMode();
    PDEBUG_OBJECT DebugObject;
    HANDLE hDebug;
    NTSTATUS Status = STATUS_SUCCESS;
    PAGED_CODE();

    /* Check if we were called from user mode*/
    if (PreviousMode != KernelMode)
    {
        /* Enter SEH for probing */
        _SEH_TRY
        {
            /* Probe the handle */
            ProbeForWriteHandle(DebugHandle);
        }
        _SEH_HANDLE
        {
            /* Get exception error */
            Status = _SEH_GetExceptionCode();
        } _SEH_END;
        if (!NT_SUCCESS(Status)) return Status;
    }

    /* Create the Object */
    Status = ObCreateObject(PreviousMode,
                            DbgkDebugObjectType,
                            ObjectAttributes,
                            PreviousMode,
                            NULL,
                            sizeof(DEBUG_OBJECT),
                            0,
                            0,
                            (PVOID*)&DebugObject);
    if (NT_SUCCESS(Status))
    {
        /* Initialize the Debug Object's Fast Mutex */
        ExInitializeFastMutex(&DebugObject->Mutex);

        /* Initialize the State Event List */
        InitializeListHead(&DebugObject->EventList);

        /* Initialize the Debug Object's Wait Event */
        KeInitializeEvent(&DebugObject->EventsPresent,
                          NotificationEvent,
                          FALSE);

        /* Set the Flags */
        DebugObject->KillProcessOnExit = KillProcessOnExit;

        /* Insert it */
        Status = ObInsertObject((PVOID)DebugObject,
                                 NULL,
                                 DesiredAccess,
                                 0,
                                 NULL,
                                 &hDebug);
        if (NT_SUCCESS(Status))
        {
            _SEH_TRY
            {
                *DebugHandle = hDebug;
            }
            _SEH_HANDLE
            {
                Status = _SEH_GetExceptionCode();
            } _SEH_END;
        }
    }

    /* Return Status */
    DBGKTRACE(DBGK_OBJECT_DEBUG, "Handle: %p DebugObject: %p\n",
              hDebug, DebugObject);
    return Status;

直接从用户模式使用这个API的一个有趣之处是,它允许命名调试对象,以便可以将其插入到对象目录名称空间中。不幸的是,没有退出NtOpendoGoBbDebug调用,因此该名称不能用于查找,但这可以在内部存储,或者可以将对象插入到树中,例如可以稍后枚举的\Debug GObjObjts。
再加上DbgUi层可以被跳过,这意味着调试对象句柄不需要存储在TEB中,这使调试器编写器能够编写一个调试器,该调试器同时调试多个进程,在调试对象之间无缝切换,并通过使用WaitForMultipleObjects,它可以从多个进程接收调试事件。
处理来自所有这些进程的消息和句柄可能有点困难,但是使用类似于kernel32如何在TEB中存储每个线程数据的模型,可以对其进行建模,以另外存储每个进程的数据。最终的结果将是一个强大的调试器一个其他人还不支持的创新。
现在让我们看看进程的实际附件,它是由NtDebugActiveProcess完成的:

NTSTATUS
NTAPI
NtDebugActiveProcess(IN HANDLE ProcessHandle,
                     IN HANDLE DebugHandle)
{
    PEPROCESS Process;
    PDEBUG_OBJECT DebugObject;
    KPROCESSOR_MODE PreviousMode = KeGetPreviousMode();
    PETHREAD LastThread;
    NTSTATUS Status;
    PAGED_CODE();
    DBGKTRACE(DBGK_PROCESS_DEBUG, "Process: %p Handle: %p\n",
              ProcessHandle, DebugHandle);

    /* Reference the process */
    Status = ObReferenceObjectByHandle(ProcessHandle,
                                       PROCESS_SUSPEND_RESUME,
                                       PsProcessType,
                                       PreviousMode,
                                       (PVOID*)&Process,
                                       NULL);
    if (!NT_SUCCESS(Status)) return Status;

    /* Don't allow debugging the initial system process */
    if (Process == PsInitialSystemProcess) return STATUS_ACCESS_DENIED;

    /* Reference the debug object */
    Status = ObReferenceObjectByHandle(DebugHandle,
                                       DEBUG_OBJECT_ADD_REMOVE_PROCESS,
                                       DbgkDebugObjectType,
                                       PreviousMode,
                                       (PVOID*)&DebugObject,
                                       NULL);
    if (!NT_SUCCESS(Status))
    {
        /* Dereference the process and exit */
        ObDereferenceObject(Process);
        return Status;
    }

    /* Acquire process rundown protection */
    if (!ExAcquireRundownProtection(&Process->RundownProtect))
    {
        /* Dereference the process and debug object and exit */
        ObDereferenceObject(Process);
        ObDereferenceObject(DebugObject);
        return STATUS_PROCESS_IS_TERMINATING;
    }

    /* Send fake create messages for debuggers to have a consistent state */
    Status = DbgkpPostFakeProcessCreateMessages(Process,
                                                DebugObject,
                                                &LastThread);
    Status = DbgkpSetProcessDebugObject(Process,
                                        DebugObject,
                                        Status,
                                        LastThread);

    /* Release rundown protection */
    ExReleaseRundownProtection(&Process->RundownProtect);

    /* Dereference the process and debug object and return status */
    ObDereferenceObject(Process);
    ObDereferenceObject(DebugObject);
    return Status;
}

这个API也很简单,并且依赖于更大的内部例程来执行大部分工作。首先,附加的一个问题是进程可能已经创建了多个新线程,以及加载了各种dll。系统无法预测哪个进程将被调试,因此它不会在任何地方将这些调试事件排队,相反,Dbgk模块必须扫描每个线程和模块,并向调试器发送适当的“ake”事件消息。例如,当附加到进程时,这将生成大量的DLL加载事件,以便调试器能够知道发生了什么。
不需要进入Dbgkp调用的内部,DLL加载消息的发送方式是通过PEB->Ldr循环PEB中包含的加载程序列表。有一个硬编码最大的500个DLL,因此列表不会无限期地循环。但是,使用名为MmGetFileNameForAddress的内部API,它将查找DLL基址的VAD,并使用它获取与其关联的SECTION_OBJECT,而不是使用相应LDR_DATA_TABLE_ENTRY结构中包含的DLL名称。内存管理器可以从这个SECTION_OBJECT中找到FILEOBJECT,然后使用ObQueryNameString查询DLL的全名,该全名可用于打开用户模式将接收的句柄。
请注意,虽然加载DLL结构的NamePointer参数很容易被填充,但它并没有被填充。为了循环新创建的线程,使用了助手PsGetNextProcessThread API,它将循环每个线程。对于第一个线程,这将生成一个Create Process debug事件,而每个后续线程将生成Create thread消息。对于进程,事件数据是从SectionBaseAddress指针中检索的,该指针具有基本图像指针。对于线程,返回的唯一数据是已保存在ETHREAD中的起始地址。
最后,DbgkpSetProcessDebugObject完成了将对象与进程关联的复杂工作。首先,存在一种可能性,即在DbgpPostFakeThreadMessages中解析初始列表之后,甚至创建新的线程。因此,这个例程将实际获取调试端口互斥量并再次调用DbgpPostFakeThreadMessages,以捕获任何丢失的线程。从逻辑上讲,这可能会导致同一消息被发送两次,但ETHREAD标志之一起作用:CreateThreadReported。DbgpPostFakeThreadMessages将在发送消息之前检查此标志,从而避免重复(EPROCESS也是如此)。
DbgkpSetProcessDebugObject的第二部分将解析已经与该对象关联的任何调试事件。这意味着我们刚发的那些假消息。这将意味着获取每个线程的运行保护,以及检查各种争用条件或可能已被拾取的未完全插入的线程。最后,修改PEB以启用BeingDebugged标志。例程完成后,调试对象将与目标完全关联。
现在让我们看看实现了类似的例程NtRemoveProcessDebug,它允许从活动调试的进程分离。

NTSTATUS
NTAPI
NtRemoveProcessDebug(IN HANDLE ProcessHandle,
                     IN HANDLE DebugHandle)
{
    PEPROCESS Process;
    PDEBUG_OBJECT DebugObject;
    KPROCESSOR_MODE PreviousMode = KeGetPreviousMode();
    NTSTATUS Status;
    PAGED_CODE();
    DBGKTRACE(DBGK_PROCESS_DEBUG, "Process: %p Handle: %p\n",
              ProcessHandle, DebugHandle);

    /* Reference the process */
    Status = ObReferenceObjectByHandle(ProcessHandle,
                                       PROCESS_SUSPEND_RESUME,
                                       PsProcessType,
                                       PreviousMode,
                                       (PVOID*)&Process,
                                       NULL);
    if (!NT_SUCCESS(Status)) return Status;

    /* Reference the debug object */
    Status = ObReferenceObjectByHandle(DebugHandle,
                                       DEBUG_OBJECT_ADD_REMOVE_PROCESS,
                                       DbgkDebugObjectType,
                                       PreviousMode,
                                       (PVOID*)&DebugObject,
                                       NULL);
    if (!NT_SUCCESS(Status))
    {
        /* Dereference the process and exit */
        ObDereferenceObject(Process);
        return Status;
    }

    /* Remove the debug object */
    Status = DbgkClearProcessDebugObject(Process, DebugObject);

    /* Dereference the process and debug object and return status */
    ObDereferenceObject(Process);
    ObDereferenceObject(DebugObject);
    return Status;
}

与大多数富NT对象一样,Dbgk提供了调试对象的接口,并允许查询或修改其某些设置。目前,只实现了set例程,并且它支持一个标志,即分离是否会终止进程。这是通过NTSETATIONDebug的系统调用来支持相应的Win32 API(Debug GestPosikIyOnEnter):

NTSTATUS
NTAPI
NtSetInformationDebugObject(IN HANDLE DebugHandle,
                            IN DEBUGOBJECTINFOCLASS DebugObjectInformationClass,
                            IN PVOID DebugInformation,
                            IN ULONG DebugInformationLength,
                            OUT PULONG ReturnLength OPTIONAL)
{
    PDEBUG_OBJECT DebugObject;
    KPROCESSOR_MODE PreviousMode = ExGetPreviousMode();
    NTSTATUS Status = STATUS_SUCCESS;
    PDEBUG_OBJECT_KILL_PROCESS_ON_EXIT_INFORMATION DebugInfo = DebugInformation;
    PAGED_CODE();

    /* Check buffers and parameters */
    Status = DefaultSetInfoBufferCheck(DebugObjectInformationClass,
                                       DbgkpDebugObjectInfoClass,
                                       sizeof(DbgkpDebugObjectInfoClass) /
                                       sizeof(DbgkpDebugObjectInfoClass[0]),
                                       DebugInformation,
                                       DebugInformationLength,
                                       PreviousMode);
    /* Check if the caller wanted the return length */
    if (ReturnLength)
    {
        /* Enter SEH for probe */
        _SEH_TRY
        {
            /* Return required length to user-mode */
            ProbeForWriteUlong(ReturnLength);
            *ReturnLength = sizeof(*DebugInfo);
        }
        _SEH_EXCEPT(_SEH_ExSystemExceptionFilter)
        {
            /* Get SEH Exception code */
            Status = _SEH_GetExceptionCode();
        }
        _SEH_END;
    }
    if (!NT_SUCCESS(Status)) return Status;

    /* Open the Object */
    Status = ObReferenceObjectByHandle(DebugHandle,
                                       DEBUG_OBJECT_WAIT_STATE_CHANGE,
                                       DbgkDebugObjectType,
                                       PreviousMode,
                                       (PVOID*)&DebugObject,
                                       NULL);
    if (NT_SUCCESS(Status))
    {
        /* Acquire the object */
        ExAcquireFastMutex(&DebugObject->Mutex);

        /* Set the proper flag */
        if (DebugInfo->KillProcessOnExit)
        {
            /* Enable killing the process */
            DebugObject->KillProcessOnExit = TRUE;
        }
        else
        {
            /* Disable */
            DebugObject->KillProcessOnExit = FALSE;
        }

        /* Release the mutex */
        ExReleaseFastMutex(&DebugObject->Mutex);

        /* Release the Object */
        ObDereferenceObject(DebugObject);
    }

    /* Return Status */
    return Status;
}

最后,debug对象提供的最后一个功能是wait-and-continue调用,它实现了双向通道,通过该通道,调试器可以接收事件、修改目标状态或自己的内部数据,然后恢复执行。由于涉及到固有的同步问题,这些调用更为复杂。首先,让我们探索一下NtWaitForDebugEvent:

NTSTATUS
NTAPI
NtWaitForDebugEvent(IN HANDLE DebugHandle,
                    IN BOOLEAN Alertable,
                    IN PLARGE_INTEGER Timeout OPTIONAL,
                    OUT PDBGUI_WAIT_STATE_CHANGE StateChange)
{
    KPROCESSOR_MODE PreviousMode = ExGetPreviousMode();
    LARGE_INTEGER SafeTimeOut;
    PEPROCESS Process;
    LARGE_INTEGER StartTime;
    PETHREAD Thread;
    BOOLEAN GotEvent;
    LARGE_INTEGER NewTime;
    PDEBUG_OBJECT DebugObject;
    DBGUI_WAIT_STATE_CHANGE WaitStateChange;
    NTSTATUS Status = STATUS_SUCCESS;
    PDEBUG_EVENT DebugEvent, DebugEvent2;
    PLIST_ENTRY ListHead, NextEntry, NextEntry2;
    PAGED_CODE();
    DBGKTRACE(DBGK_OBJECT_DEBUG, "Handle: %p\n", DebugHandle);

    /* Clear the initial wait state change structure */
    RtlZeroMemory(&WaitStateChange, sizeof(WaitStateChange));

    /* Protect probe in SEH */
    _SEH_TRY
    {
        /* Check if we came with a timeout */
        if (Timeout)
        {
            /* Check if the call was from user mode */
            if (PreviousMode != KernelMode)
            {
                /* Probe it */
                ProbeForReadLargeInteger(Timeout);
            }

            /* Make a local copy */
            SafeTimeOut = *Timeout;
            Timeout = &SafeTimeOut;

            /* Query the current time */
            KeQuerySystemTime(&StartTime);
        }

        /* Check if the call was from user mode */
        if (PreviousMode != KernelMode)
        {
            /* Probe the state change structure */
            ProbeForWrite(StateChange, sizeof(*StateChange), sizeof(ULONG));
        }
    }
    _SEH_HANDLE
    {
        /* Get the exception code */
        Status = _SEH_GetExceptionCode();
    }
    _SEH_END;
    if (!NT_SUCCESS(Status)) return Status;

    /* Get the debug object */
    Status = ObReferenceObjectByHandle(DebugHandle,
                                       DEBUG_OBJECT_WAIT_STATE_CHANGE,
                                       DbgkDebugObjectType,
                                       PreviousMode,
                                       (PVOID*)&DebugObject,
                                       NULL);
    if (!NT_SUCCESS(Status)) return Status;

    /* Clear process and thread */
    Process = NULL;
    Thread = NULL;

    /* Start wait loop */
    while (TRUE)
    {
        /* Wait on the debug object given to us */
        Status = KeWaitForSingleObject(DebugObject,
                                       Executive,
                                       PreviousMode,
                                       Alertable,
                                       Timeout);
        if (!NT_SUCCESS(Status) ||
            (Status == STATUS_TIMEOUT) ||
            (Status == STATUS_ALERTED) ||
            (Status == STATUS_USER_APC))
        {
            /* Break out the wait */
            break;
        }

        /* Lock the object */
        GotEvent = FALSE;
        ExAcquireFastMutex(&DebugObject->Mutex);

        /* Check if a debugger is connected */
        if (DebugObject->DebuggerInactive)
        {
            /* Not connected */
            Status = STATUS_DEBUGGER_INACTIVE;
        }
        else
        {
            /* Loop the events */
            ListHead = &DebugObject->EventList;
            NextEntry =  ListHead->Flink;
            while (ListHead != NextEntry)
            {
                /* Get the debug event */
                DebugEvent = CONTAINING_RECORD(NextEntry,
                                               DEBUG_EVENT,
                                               EventList);
                DBGKTRACE(DBGK_PROCESS_DEBUG, "DebugEvent: %p Flags: %lx\n",
                          DebugEvent, DebugEvent->Flags);

                /* Check flags */
                if (!(DebugEvent->Flags &
                     (DEBUG_EVENT_FLAGS_USED | DEBUG_EVENT_FLAGS_INACTIVE)))
                {
                    /* We got an event */
                    GotEvent = TRUE;

                    /* Loop the list internally */
                    NextEntry2 = DebugObject->EventList.Flink;
                    while (NextEntry2 != NextEntry)
                    {
                        /* Get the debug event */
                        DebugEvent2 = CONTAINING_RECORD(NextEntry2,
                                                        DEBUG_EVENT,
                                                        EventList);

                        /* Try to match process IDs */
                        if (DebugEvent2->ClientId.UniqueProcess ==
                            DebugEvent->ClientId.UniqueProcess)
                        {
                            /* Found it, break out */
                            DebugEvent->Flags |= DEBUG_EVENT_FLAGS_USED;
                            DebugEvent->BackoutThread = NULL;
                            GotEvent = FALSE;
                            break;
                        }

                        /* Move to the next entry */
                        NextEntry2 = NextEntry2->Flink;
                    }

                    /* Check if we still have a valid event */
                    if (GotEvent) break;
                }

                /* Move to the next entry */
                NextEntry = NextEntry->Flink;
            }

            /* Check if we have an event */
            if (GotEvent)
            {
                /* Save and reference the process and thread */
                Process = DebugEvent->Process;
                Thread = DebugEvent->Thread;
                ObReferenceObject(Process);
                ObReferenceObject(Thread);

                /* Convert to user-mode structure */
                DbgkpConvertKernelToUserStateChange(&WaitStateChange,
                                                    DebugEvent);

                /* Set flag */
                DebugEvent->Flags |= DEBUG_EVENT_FLAGS_INACTIVE;
            }
            else
            {
                /* Unsignal the event */
                KeClearEvent(&DebugObject->EventsPresent);
            }

            /* Set success */
            Status = STATUS_SUCCESS;
        }

        /* Release the mutex */
        ExReleaseFastMutex(&DebugObject->Mutex);
        if (!NT_SUCCESS(Status)) break;

        /* Check if we got an event */
        if (!GotEvent)
        {
            /* Check if we can wait again */
            if (SafeTimeOut.QuadPart < 0)
            {
                /* Query the new time */
                KeQuerySystemTime(&NewTime);

                /* Substract times */
                SafeTimeOut.QuadPart += (NewTime.QuadPart - StartTime.QuadPart);
                StartTime = NewTime;

                /* Check if we've timed out */
                if (SafeTimeOut.QuadPart > 0)
                {
                    /* We have, break out of the loop */
                    Status = STATUS_TIMEOUT;
                    break;
                }
            }
        }
        else
        {
            /* Open the handles and dereference the objects */
            DbgkpOpenHandles(&WaitStateChange, Process, Thread);
            ObDereferenceObject(Process);
            ObDereferenceObject(Thread);
            break;
        }
    }

    /* We're done, dereference the object */
    ObDereferenceObject(DebugObject);

    /* Protect write with SEH */
    _SEH_TRY
    {
        /* Return our wait state change structure */
        RtlCopyMemory(StateChange,
                      &WaitStateChange,
                      sizeof(DBGUI_WAIT_STATE_CHANGE));
    }
    _SEH_EXCEPT(_SEH_ExSystemExceptionFilter)
    {
        /* Get SEH Exception code */
        Status = _SEH_GetExceptionCode();
    }
    _SEH_END;

    /* Return status */
    return Status;
}

发生的第一件事是对调试对象进行等待,或者更具体地说,对EventsPresent事件进行等待。当这个等待得到满足时,对象被锁定,API首先确保它在获取锁之前没有变为非活动状态。确认后,将分析当前调试事件,并且系统调用将确保调试事件尚未使用(处理)并且未处于非活动状态。如果这些标志签出,则再次分析列表,以确保同一进程没有任何其他事件。如果找到任何事件,则此事件标记为不活动,并且不发送任何内容。如果多个事件用于同一个进程,则这似乎会阻止它们被发送。获得调试事件后,将引用进程和线程,并将结构转换为DbgUi等待状态更改结构,并将事件标记为已使用。释放调试对象锁后,在找到调试事件时,在成功的情况下调用DbgkOpenHandles,这将打开用户模式在DbgUi结构中期望的正确句柄,之后将删除对进程和线程的额外引用。等待完成后,DbgUi结构被复制回调用方。最后一个API NtDebugContinue允许从调试事件继续,其实现用于删除发送的调试事件并唤醒目标。它的实现方式如下:

NTSTATUS
NTAPI
NtDebugContinue(IN HANDLE DebugHandle,
                IN PCLIENT_ID AppClientId,
                IN NTSTATUS ContinueStatus)
{
    KPROCESSOR_MODE PreviousMode = ExGetPreviousMode();
    PDEBUG_OBJECT DebugObject;
    NTSTATUS Status = STATUS_SUCCESS;
    PDEBUG_EVENT DebugEvent = NULL, DebugEventToWake = NULL;
    PLIST_ENTRY ListHead, NextEntry;
    BOOLEAN NeedsWake = FALSE;
    CLIENT_ID ClientId;
    PAGED_CODE();
    DBGKTRACE(DBGK_OBJECT_DEBUG, "Handle: %p Status: %p\n",
              DebugHandle, ContinueStatus);

    /* Check if we were called from user mode*/
    if (PreviousMode != KernelMode)
    {
        /* Enter SEH for probing */
        _SEH_TRY
        {
            /* Probe the handle */
            ProbeForRead(AppClientId, sizeof(CLIENT_ID), sizeof(ULONG));
            ClientId = *AppClientId;
            AppClientId = &ClientId;
        }
        _SEH_HANDLE
        {
            /* Get exception error */
            Status = _SEH_GetExceptionCode();
        } _SEH_END;
        if (!NT_SUCCESS(Status)) return Status;
    }

    /* Make sure that the status is valid */
    if ((ContinueStatus != DBG_CONTINUE) &&
        (ContinueStatus != DBG_EXCEPTION_HANDLED) &&
        (ContinueStatus != DBG_EXCEPTION_NOT_HANDLED) &&
        (ContinueStatus != DBG_TERMINATE_THREAD) &&
        (ContinueStatus != DBG_TERMINATE_PROCESS))
    {
        /* Invalid status */
        Status = STATUS_INVALID_PARAMETER;
    }
    else
    {
        /* Get the debug object */
        Status = ObReferenceObjectByHandle(DebugHandle,
                                           DEBUG_OBJECT_WAIT_STATE_CHANGE,
                                           DbgkDebugObjectType,
                                           PreviousMode,
                                           (PVOID*)&DebugObject,
                                           NULL);
        if (NT_SUCCESS(Status))
        {
            /* Acquire the mutex */
            ExAcquireFastMutex(&DebugObject->Mutex);

            /* Loop the state list */
            ListHead = &DebugObject->EventList;
            NextEntry = ListHead->Flink;
            while (ListHead != NextEntry)
            {
                /* Get the current debug event */
                DebugEvent = CONTAINING_RECORD(NextEntry,
                                               DEBUG_EVENT,
                                               EventList);

                /* Compare process ID */
                if (DebugEvent->ClientId.UniqueProcess ==
                    AppClientId->UniqueProcess)
                {
                    /* Check if we already found a match */
                    if (NeedsWake)
                    {
                        /* Wake it up and break out */
                        DebugEvent->Flags &= ~DEBUG_EVENT_FLAGS_USED;
                        KeSetEvent(&DebugEvent->ContinueEvent,
                                   IO_NO_INCREMENT,
                                   FALSE);
                        break;
                    }

                    /* Compare thread ID and flag */
                    if ((DebugEvent->ClientId.UniqueThread ==
                        AppClientId->UniqueThread) &&
                        (DebugEvent->Flags & DEBUG_EVENT_FLAGS_INACTIVE))
                    {
                        /* Remove the event from the list */
                        RemoveEntryList(NextEntry);

                        /* Remember who to wake */
                        NeedsWake = TRUE;
                        DebugEventToWake = DebugEvent;
                    }
                }

                /* Go to the next entry */
                NextEntry = NextEntry->Flink;
            }

            /* Release the mutex */
            ExReleaseFastMutex(&DebugObject->Mutex);

            /* Dereference the object */
            ObDereferenceObject(DebugObject);

            /* Check if need a wait */
            if (NeedsWake)
            {
                /* Set the continue status */
                DebugEvent->ApiMsg.ReturnedStatus = Status;
                DebugEvent->Status = STATUS_SUCCESS;

                /* Wake the target */
                DbgkpWakeTarget(DebugEvent);
            }
            else
            {
                /* Fail */
                Status = STATUS_INVALID_PARAMETER;
            }
        }
    }

    /* Return status */
    return Status;
}

首先,它负责将使用的延续状态验证为有限数量的已识别状态代码。然后,循环每个调试事件。如果进程和线程id匹配,那么调试事件将从列表中删除,并且例程将记住找到了事件。如果找到同一进程的任何其他事件,则取消活动标志。回想一下,这个标志是由wait例程添加的,用于禁止同一进程同时发送多个事件。一旦所有的调试事件都被解析,那么目标就会被唤醒,这基本上意味着如果消息是一个假的线程创建消息,则恢复线程,释放它的运行保护,然后释放调试事件或通知等待它的人(取决于它是如何创建的)。由于这一节主要讨论内核模式,让我们看看我们从这一节中学到了什么:

  • Dbgk是内核中处理调试功能的所有支持代码的组件。
  • 该实现通过一个名为DEBUG_Object的NT对象公开,并提供各种系统调用来访问它。
  • 可以为用户模式应用程序编写内核模式调试器。
  • 只要跳过DbgUi层并手动使用系统调用重新实现,就可以编写一个可以同时调试多个应用程序的调试器。
  • 内核使用自己版本的wait state change结构,封装在DEBUG_EVENT结构中。
  • 内核仍然支持基于LPC的DbgSs调试。
  • 内核打开事件结构中存在的所有句柄,用户模式负责关闭它们。
  • 编写内核时,一次只发送同一进程的一个事件。
  • 内核需要解析PEB加载程序数据以获得加载的dll列表,并且有500个循环迭代的硬编码限制。

posted on 2019-11-14 00:33  活着的虫子  阅读(1557)  评论(0编辑  收藏  举报

导航