简介
允许用户模式调试工作的内部机制很少得到充分的解释。更糟糕的是,这些机制在Windows XP中已经发生了根本性的变化,当许多支持被重新编写时,还通过将ntdll中的大多数例程作为本地API的一部分,使更多的子系统变得可移植。希望读者对C和通用NT内核体系结构和语义有一些基本的了解。此外,这并不是介绍什么是调试或如何编写调试器。它可以作为经验丰富的调试器编写人员或好奇的安全专家的参考。
Win32 Debugging
NT的Win32子系统从第一个版本开始就允许对进程进行调试,随后的版本添加了更多与符号和其他PE信息相关的特性和调试帮助库。但是,对于外部API用户来说,除了在Windows XP中添加的停止调试进程而不杀死进程的功能之外,其他的变化相对较少。NT的这个版本还包含了对底层实现的几次彻底检查,我们将对此进行详细讨论。但是,这些更改的一个重要副作用是不再使用LPC(和csrss.exe),这允许调试这个二进制文件(以前,调试这个二进制文件是不可能的,因为它负责处理内核到用户的通知)。
处理调试进程的基本win32api很简单:DebugActiveProcess,to attach,WaitForDebugEvent,等待调试事件的出现,以便调试可以处理它们,ContinueDebugEvent,恢复线程执行。WindowsXP的发布增加了三个更有用的API:Debug活动进程,它允许您停止调试一个进程(Debug),Debug GestPosikIon退出,这允许您即使在它被拆分和Debug处理之后继续运行一个进程,它允许您执行远程DebugBreak,而无需手动创建远程线程。在Windows XP Service Pack 1中,又添加了一个API,CheckRemoteDebuggerPresent。与IsDebuggerPresent类似,此API允许您在另一个进程中检查连接的调试器,而无需远程读取PEB。
由于NT的体系结构,这些api在最新版本的Windows上(2003将被用作一个例子,但是这些信息也适用于XP)本身做不了多少工作。相反,它们执行的典型工作是调用所需的Native函数,然后处理输出,以便Win32调用方能够以与Win9x和原始Win32 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;
}
如您所见,这里所做的唯一工作是创建到用户模式调试组件的初始连接,这是通过位于ntdll中的DbgUi Native API集完成的,我们稍后将看到。由于DbgUi使用句柄而不是PIDs,因此必须首先使用简单的helper函数转换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 == -) dwProcessId = (DWORD)CsrGetProcessId(); /* Open a handle to the process */
ClientId.UniqueProcess = (HANDLE)dwProcessId;
InitializeObjectAttributes(&ObjectAttributes, NULL, , NULL, NULL);
Status = NtOpenProcess(&Handle,
PROCESS_ALL_ACCESS,
&ObjectAttributes,
&ClientId);
if (!NT_SUCCESS(Status))
{
/* Fail */
SetLastErrorByStatus(Status);
return ;
} /* Return the handle */
return Handle;
}
如果您不熟悉Native API,那么可以说这段代码相当于PID上的OpenProcess,这样就可以获得句柄。回到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;
}
希望第一个不需要解释,因为它是一个简单的包装,但让我们看看第二个。如果您熟悉Native API,您将立即认识到熟悉的NtSetInformationXxx类型的API,它用于在不同类型的NT对象(如文件、进程、线程等)上设置各种设置。这里有趣的是,XP不熟悉的是,调试本身现在也使用调试对象完成。不过,稍后将讨论这个对象的细节。现在,让我们看看函数。
第一个API DbgUiGetThreadDebugObject是对DbgUi的另一个调用,它将返回与线程相关联的调试对象的句柄(稍后我们将看到它存储在哪里)。一旦我们有了句柄,我们就调用一个直接与Dbgk(而不是DbgUi)通信的Native API,它将简单地更改内核调试对象结构中的一个标志。我们将看到,这个标志在分离时将被内核读取。
与此类似的函数是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;
}
如您所见,正在使用另一个NtQuery/SetInformationXxx API,但这次是用于进程的。虽然现在可能要检测调试,但是可以简单地检查NtCurrTeNPEB()-> BeingDebugged,是否存在另一种方式来执行这一操作,这是通过查询内核来实现的。由于内核需要在调试事件时与用户模式通信,因此它需要某种方式来实现这一点。在XP之前,这通常是通过LPC端口完成的,现在是通过一个Debug对象(但是,它共享相同的指针)。
由于位于内核模式下的EPROCESS结构中,我们使用DebugPort信息类进行查询。如果将EPROCESS->DebugPort设置为某个值,则此API将返回TRUE,这意味着正在调试进程。这个技巧也可以用于本地进程,但是简单地阅读PEB要快得多。人们可以注意到,尽管有些应用程序喜欢将Peb->BeingDebugged设置为FALSE来欺骗反调试程序,但是没有办法将DebugPort设置为NULL,因为内核本身不允许您调试(而且您也没有访问内核结构的权限)。
考虑到这一点,让我们看看如何实现整个Win32调试基础设施WaitForDebugEvent的要点。这需要在更简单的ContinueDebugEvent/DebugActiveProcessStop之前显示,因为它引入了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(-, 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;
}
首先,让我们看看现在的DbgUi api。首先,DbgUiWaitStateChange是WaitForDebugEvent的Native版本,它负责对调试对象执行实际的等待,并获取与此事件关联的结构。然而,DbgUi使用自己的内部结构(稍后我们将展示),以便内核能够理解它,而Win32使用Win9x方式定义了许多不同的结构。因此,需要将其转换为Win32表示形式,DbgUiConvertStateChange API将执行此转换,返回向后兼容并记录在MSDN上的LPDEBUG_EVENT Win32结构。
接下来是一个开关,它对新进程或线程的创建或删除感兴趣。使用四个API:SaveProcessHandle和SaveRead句柄,它们保存这些各自的句柄(记住一个新进程必须有一个关联的线程,因此线程句柄也被保存),以及MarkProcessHandle和MulthTrHead句柄,它标志着这些句柄被退出。让我们详细地看一下这个高级框架。
VOID
WINAPI
SaveProcessHandle(IN DWORD dwProcessId,
IN HANDLE hProcess)
{
PDBGSS_THREAD_DATA ThreadData; /* Allocate a thread structure */
ThreadData = RtlAllocateHeap(RtlGetProcessHeap(),
,
sizeof(DBGSS_THREAD_DATA));
if (!ThreadData) return; /* Fill it out */
ThreadData->ProcessHandle = hProcess;
ThreadData->ProcessId = dwProcessId;
ThreadData->ThreadId = ;
ThreadData->ThreadHandle = NULL;
ThreadData->HandleMarked = FALSE; /* Link it */
ThreadData->Next = DbgSsGetThreadData();
DbgSsSetThreadData(ThreadData);
}
这个函数分配一个新的结构DBGSS_THREAD_DATA,并简单地用发送的进程句柄和ID填充它。最后,它将它链接到当前的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;
因此,这种通用结构允许存储进程/线程句柄和id,以及我们讨论过的有关MarkProcess/ThreadHandle的标志。我们还看到了一些DbgSsSet/GetThreadData函数,这些函数将向我们展示这个循环结构数组的位置。让我们看看它们的实现:
#define DbgSsSetThreadData(d) \
NtCurrentTeb()->DbgSsReserved[] = d #define DbgSsGetThreadData() \
((PDBGSS_THREAD_DATA)NtCurrentTeb()->DbgSsReserved[]) 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: VOID
WINAPI
SaveThreadHandle(IN DWORD dwProcessId,
IN DWORD dwThreadId,
IN HANDLE hThread)
{
PDBGSS_THREAD_DATA ThreadData; /* Allocate a thread structure */
ThreadData = RtlAllocateHeap(RtlGetProcessHeap(),
,
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: 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;
}
}
请注意,唯一不太重要的实现细节是,需要分析数组才能找到匹配的进程和线程ID。现在,我们查看了这些结构,让我们看看关联的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;
}
同样,我们正在处理一个DbgUI API DbgUiContinue,它将为我们完成所有的工作。我们唯一的工作是调用RemoveHandles,它是包装DbgUi的高级结构的一部分。这个函数比我们看到的稍微复杂一些,因为我们得到了PID/TID,所以我们需要做一些查找:
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(), , ThreadData); /* Move to the next structure */
ThreadData = DbgSsGetThreadData();
continue;
}
} /* Move to the next one */
ThreadData = ThreadData->Next;
}
}
不需要太多解释。在解析循环缓冲区时,我们试图找到与给定的PID和TID匹配的结构。一旦找到它,我们就检查句柄是否与线程和进程关联。如果是的话,我们现在可以关上把手了。
因此,这个高级Win32机制的使用现在是显而易见的:我们可以将句柄与id关联起来,并在清理或继续时关闭它们。这是因为这些句柄不是由Win32打开的,而是由Dbgk在后面打开的。关闭句柄后,我们通过将TEB指针更改为数组中的下一个结构来取消此结构的链接,然后释放自己的数组。然后从上的下一个结构继续解析(因为有多个这样的结构可能与此PID/TID关联)。
最后,我们的分析中缺少了最后一块Win32拼图,这是XP中添加的detach函数。让我们看看它的简单实现:
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;
}
再简单不过了。就像附加一样,我们首先将PID转换为一个句柄,然后对这个进程句柄使用DbgUi调用(dbguistopdigung),以便从进程中分离自己。这里还有一个调用,即CloseAllProcessHandles。这是Win32在DbgUi上的高级调试的一部分,我们刚才已经看到了。此例程与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(), , ThreadData); /* Move to the next structure */
ThreadData = DbgSsGetThreadData();
continue;
} /* Move to the next one */
ThreadData = ThreadData->Next;
}
}
这就完成了我们对win32api的分析!让我们看看我们学到了什么:
- 实际的调试功能出现在内核中名为Dbgk的模块中。
- 它可以通过DbgUi Native API接口访问,该接口位于NT系统库ntdll中。
- Dbgk通过名为Debug对象的NT对象实现调试功能,该对象还提供NtSetInformation API以修改某些标志。
- 可以使用DbgUiGetThreadObject检索与线程关联的调试对象,但是我们还没有显示它存储在哪里。
- 可以使用NtQueryInformationProcess和DebugPort信息类来检查是否正在调试进程。没有rootkit就不能欺骗你。
- 由于Dbgk在调试事件期间打开某些句柄,Win32需要一种方法来关联id和句柄,并使用称为DBGSS_THREAD_DATA的结构循环数组将其存储在TEB的DbgSsReserved[0]成员中。