|
不幸的是,Windows XP内核完全不同于Windows 2000内核。XP下的调度程序(scheduler)只有两个线程链表:KiWaitListHead和
KiDispatcherReadyListHead。我们可以通过搜索KeDelayExecutionThread函数来查找KeWaitListHead:
Code:
.text:004055B5 mov dword ptr [ebx], offset KiWaitListHead
.text:004055BB mov [ebx+4], eax
搜索代码如下:
Code:
void XPGetKiWaitListHead()
{
PUCHAR cPtr, pOpcode;
ULONG Length;
for (cPtr = (PUCHAR)KeDelayExecutionThread;
cPtr < (PUCHAR)KeDelayExecutionThread + PAGE_SIZE;
cPtr += Length)
{
Length = SizeOfCode(cPtr, &pOpcode);
if (!Length) break;
if (*(PUSHORT)cPtr == 0x03C7 && *(PUSHORT)(pOpcode + 6) == 0x4389)
{
KiWaitInListHead = *(PLIST_ENTRY *)(pOpcode + 2);
break;
}
}
return;
}
最困难的是查找KiDispatcherReadyListHead。主要的问题是KiDispatcherReadyListHead的地址并没有被任何一个导出的函数使用。因此就要
用更加复杂的搜索算法搞定它。就从KiDispatchInterrupt函数开始,我们感兴趣的地方只有这里:
Code:
.text:00404E72 mov byte ptr [edi+50h], 1
.text:00404E76 call sub_404C5A
.text:00404E7B mov cl, 1
.text:00404E7D call sub_404EB9
这段代码中的第一个函数调用指向的就是包含KiDispatcherReadyListHead引用的函数。尽管如此,搜索KiDispatcherReadyListHead的地址却
变的更加复杂,因为这个函数的相关代码在Windows XP SP1和SP2中是不同的。在SP2中它是这个样子:
Code:
.text:00404CCD add eax, 60h
.text:00404CD0 test bl, bl
.text:00404CD2 lea edx, KiDispatcherReadyListHead[ecx*8]
.text:00404CD9 jnz loc_401F12
.text:00404CDF mov esi, [edx+4]
And in SP1:
SP1中是这样的:
Code:
.text:004180FE add eax, 60h
.text:00418101 cmp [ebp+var_1], bl
.text:00418104 lea edx, KiDispatcherReadyListHead[ecx*8]
.text:0041810B jz loc_418760
.text:00418111 mov esi, [edx]
仅仅查找一个“lea”指令是不可靠的,因此我们也应该检查“lea”后面的指令(LDasm中的IsRelativeCmd函数)。搜索
KiDispatcherReadyListHead的全部代码如下:
Code:
void XPGetKiDispatcherReadyListHead()
{
PUCHAR cPtr, pOpcode;
PUCHAR CallAddr = NULL;
ULONG Length;
for (cPtr = (PUCHAR)KiDispatchInterrupt;
cPtr < (PUCHAR)KiDispatchInterrupt + PAGE_SIZE;
cPtr += Length)
{
Length = SizeOfCode(cPtr, &pOpcode);
if (!Length) return;
if (*pOpcode == 0xE8 && *(PUSHORT)(pOpcode + 5) == 0x01B1)
{
CallAddr = (PUCHAR)(*(PULONG)(pOpcode + 1) + (ULONG)cPtr + Length);
break;
}
}
if (!CallAddr || !MmIsAddressValid(CallAddr)) return;
for (cPtr = CallAddr; cPtr < CallAddr + PAGE_SIZE; cPtr += Length)
{
Length = SizeOfCode(cPtr, &pOpcode);
if (!Length) return;
if (*(PUSHORT)pOpcode == 0x148D && *(pOpcode + 2) == 0xCD && IsRelativeCmd(pOpcode + 7))
{
KiDispatcherReadyListHead = *(PLIST_ENTRY *)(pOpcode + 3);
break;
}
}
return;
}
找到线程链表地址之后我们就可以非常简单地枚举出那些进程了,代码如下:
Code:
void ProcessListHead(PLIST_ENTRY ListHead)
{
PLIST_ENTRY Item;
if (ListHead)
{
Item = ListHead->Flink;
while (Item != ListHead)
{
CollectProcess(*(PEPROCESS *)((ULONG)Item + WaitProcOffset));
Item = Item->Flink;
}
}
return;
}
CollectProcess是一个非常有用的函数,它可以增加一个进程到进程列表中去。
通过拦截系统调用得到进程列表。
任何一个进程都要通过API来和系统进行交互,而大多数交互都通过系统调用传递给了内核。当然,进程也可以不使用任何API而存在,但是这
样一来它也就不能做任何有用(或有害)的事情。一般而言,我们的思路是使用系统调用管理器拦截系统调用,然后得到管理器中当前进程的
EPROCESS指针。应该在某段时间收集指针列表,这个表不会包含信息收集时没有使用任何系统调用的进程(比如,进程的线程都处于等待状态
)。
Windows 2000平台使用2Eh中断进行系统调用,因此我们需要修改IDT中的相应的中断描述符来拦截系统调用,这就要用sidt指令得到IDT在内存
中的位置。该指令返回这样一个结构:
Code:
typedef struct _Idt
{
USHORT Size;
ULONG Base;
} TIdt;
修改2Eh中断向量的代码如下:
Code:
void Set2kSyscallHook()
{
TIdt Idt;
__asm
{
pushad
cli
sidt [Idt]
mov esi, NewSyscall
mov ebx, Idt.Base
xchg [ebx + 0x170], si
rol esi, 0x10
xchg [ebx + 0x176], si
ror esi, 0x10
mov OldSyscall, esi
sti
popad
}
}
当然在卸载驱动之前还要保存原始状态的信息:
Code:
void Win2kSyscallUnhook()
{
TIdt Idt;
__asm
{
pushad
cli
sidt [Idt]
mov esi, OldSyscall
mov ebx, Idt.Base
mov [ebx + 0x170], si
rol esi, 0x10
mov [ebx + 0x176], si
sti
xor eax, eax
mov OldSyscall, eax
popad
}
}
Windows XP使用sysenter/sysexit指令(出现在Pentium 2处理器中)实现系统调用。这些指令的功能由model-specific registers(MSR)控制
。系统调用管理器的地址保存在MSR寄存器,SYSENTER_EIP_MSR(0x176)中。用rdmsr指令读取MSR寄存器,同时设置ECX = 要读取的寄存器的号
码,结果保存在两个积存器EDX:EAX中。在我们这里,SYSENTER_EIP_MSR积存器是32位积存器,所以EDX为0,EAX内是系统调用管理器的地址。
同样地,我们也可以用wrmsr指令写MSR积存器。有一个地方需要注意:当写32位MSR积存器的时候,EDX应该被清空,否则将引起异常并且导致
系统立即崩溃。
考虑到所有的事情之后,替代系统调用管理器的代码如下:
Code:
void SetXpSyscallHook()
{
__asm
{
pushad
mov ecx, 0x176
rdmsr
mov OldSyscall, eax
mov eax, NewSyscall
xor edx, edx
wrmsr
popad
}
}
恢复原始的系统调用管理器代码:
Code:
void XpSyscallUnhook()
{
__asm
{
pushad
mov ecx, 0x176
mov eax, OldSyscall
xor edx, edx
wrmsr
xor eax, eax
mov OldSyscall, eax
popad
}
}
Windows XP的另外一个特性是它既可以使用sysenter也可以使用int 2Eh来进行系统调用,所以我们要替换这两种情况下的系统调用管理器。
我们的新的系统调用管理器应该得到当前进程的EPROCESS指针,并且如果是一个新的进程,我们要把这个新的进程加到我们的进程列表中。
新的系统调用管理器代码如下:
Code:
void __declspec(naked) NewSyscall()
{
__asm
{
pushad
pushfd
push fs
mov di, 0x30
mov fs, di
mov eax, fs:[0x124]
mov eax, [eax + 0x44]
push eax
call CollectProcess
pop fs
popfd
popad
jmp OldSyscall
}
}
得到进程列表的这段代码应该在某个时间段内工作,所以我们有这样的问题:如果在列表中的进程被删除掉,在随后的时间内我们将保留一些
无效指针,结果就是检测隐藏进程失败或者导致系统BSOD。解决这个问题的办法是,用PsSetCreateProcessNotifyRoutine函数注册我们的回调
函数,这个回调函数将会在系统创建或者销毁一个进程的时候被调用。当进程被销毁时,我们也应该把它从我们的表中删除掉。
回调函数的原型如下:
Code:
VOID
(*PCREATE_PROCESS_NOTIFY_ROUTINE) (
IN HANDLE ParentId,
IN HANDLE ProcessId,
IN BOOLEAN Create
);
安装回调函数的代码如下:
Code:
PsSetCreateProcessNotifyRoutine (NotifyRoutine, FALSE);
取消回调函数的代码:
Code:
PsSetCreateProcessNotifyRoutine (NotifyRoutine, TRUE);
这里有一个问题,回调函数总是在系统被销毁的时候创建,因此我们不可能直接在这个回调函数中删除进程列表中的相应进程。这样我们就要
用系统的work items,首先调用IoAllocateWorkItem函数为work item分配内存,然后调用IoQueueWorkItem函数(俄文翻译者kao注:这一句我
不太确定...)将任务放置到工作线程队列中。在处理过程中我们不仅仅从进程列表中删除掉已经终止的进程,而且还要加入新创建的线程。处
理代码如下:
Code:
void WorkItemProc(PDEVICE_OBJECT DeviceObject, PWorkItemStruct Data)
{
KeWaitForSingleObject(Data->pEPROCESS, Executive, KernelMode, FALSE, NULL);
DelItem(&wLastItem, Data->pEPROCESS);
ObDereferenceObject(Data->pEPROCESS);
IoFreeWorkItem(Data->IoWorkItem);
ExFreePool(Data);
return;
}
void NotifyRoutine(IN HANDLE ParentId,
IN HANDLE ProcessId,
IN BOOLEAN Create)
{
PEPROCESS process;
PWorkItemStruct Data;
if (Create)
{
PsLookupProcessByProcessId(ProcessId, &process);
if (!IsAdded(wLastItem, process)) AddItem(&wLastItem, process);
ObDereferenceObject(process);
} else
{
process = PsGetCurrentProcess();
ObReferenceObject(process);
Data = ExAllocatePool(NonPagedPool, sizeof(TWorkItemStruct));
Data->IoWorkItem = IoAllocateWorkItem(deviceObject);
Data->pEPROCESS = process;
IoQueueWorkItem(Data->IoWorkItem, WorkItemProc, DelayedWorkQueue, Data);
}
return;
}
这是一个相对可靠的隐藏进程的检测方式,然而虽然没有进程能够不倚赖系统调用,但还是有一些进程可以在很长一段时间处于等待状态不进
行系统调用,我们无法检测出这样的进程。
只要想做,躲避开这种检测方式还是很容易的。想要做到这一点,那就需要改变隐藏进程的系统调用方式(重定向到另外一个中断或者GDT中的
调用门)。在Windows XP下做这个工作是相当简单的,因为可以给ntdll.dll中的KiFastSystemCall函数打补丁和创建一个相应的系统调用门。
在Windows 2000平台下就稍微有点难度了,因为int 2Eh调用分散遍及整个ntdll,但是找到并patch所有的地方也并不是很复杂。综上所述,依
赖于这种检测方式可不是聪明之举。
通过遍历句柄表得到进程列表。
如果你曾经尝试过利用删除PsActiveProcesses链表中的进程节点来隐藏进程,可能你会注意到当你调用ZwQuerySystemInformation函数枚举句
柄的时候,隐藏进程的句柄也会被枚举出来,并且还能被检测出它的ProcessId。这是因为为了方便枚举句柄,所有的句柄表都是由一个双向链
表HandleTableList维护的。Windows 2000下HANDLE_TABLE结构在链表中的偏移等于0x054,Windows XP下为0x01C,链表由
HandleTableListHead开始。HANDLE_TABLE结构包括它的宿主进程的指针(QuotaProcess),Windows 2000下这个偏移等于0x00C,Windows XP
下这个偏移为0x004。通过遍历这个句柄链表我们就可以构建进程列表了。
首先我们得找到HandleTableListHead。反汇编内核显示它的引用定位在函数的深处,所以前面我们用过的反汇编代码的方法已经不能在这里使
用了。要找到HeadleTableListHead,我们要注意到HandleTableListHead是一个全局的内核变量,因此它一定是在内核文件的某一个段
(Section)里面,并且HandleTableList的其他成员是在动态分配的内存中,所以总是受到内核地址空间的限制。根据这些,我们需要得到任
何一个进程的HandleTable的指针,然后遍历链表直到找到定位在这个内核地址空间的成员,那么这个成员就是HandleTableListHead了。
我们使用ZwQuerySystemInformation和SystemModuleInformation类计算系统内核的基址和大小。它将返回一个所有已经加载了的模块的描述符
表,并且这个表的第一个成员始终是"system"。综上所述,查找HandleTableListHead的代码如下:
Code:
void GetHandleTableListHead()
{
PSYSTEM_MODULE_INFORMATION_EX Info = GetInfoTable(SystemModuleInformation);
ULONG NtoskrnlBase = (ULONG)Info->Modules[0].Base;
ULONG NtoskrnlSize = Info->Modules[0].Size;
PHANDLE_TABLE HandleTable = *(PHANDLE_TABLE *)((ULONG)PsGetCurrentProcess() + HandleTableOffset);
PLIST_ENTRY HandleTableList = (PLIST_ENTRY)((ULONG)HandleTable + HandleTableListOffset);
PLIST_ENTRY CurrTable;
ExFreePool(Info);
for (CurrTable = HandleTableList->Flink;
CurrTable != HandleTableList;
CurrTable = CurrTable->Flink)
{
if ((ULONG)CurrTable > NtoskrnlBase && (ULONG)CurrTable < NtoskrnlBase + NtoskrnlSize)
{
HandleTableListHead = CurrTable;
break;
}
}
}
这段代码是非常通用的,它可以运行于任何Windows NT版本的系统上,并且不仅可以用来查找HandleTableListHead,也可以用于其他类似的结
构。
得到HandleTableListHead地址后我们就可以遍历句柄表并基于这些信息来构建进程列表了。
Code:
void ScanHandleTablesList()
{
PLIST_ENTRY CurrTable;
PEPROCESS QuotaProcess;
for (CurrTable = HandleTableListHead->Flink;
CurrTable != HandleTableListHead;
CurrTable = CurrTable->Flink)
{
QuotaProcess = *(PEPROCESS *)((PUCHAR)CurrTable - HandleTableListOffset + QuotaProcessOffset);
if (QuotaProcess) CollectProcess(QuotaProcess);
}
}
F-Secure Black Light和KProcCheck的最后一个版本用的就是这种检测方法。我想你将会很轻松地找到对付这种检测的方法。
通过扫描PspCidTable得到进程列表。
有一件有趣的事情需要注意:如果仅仅把进程节点从PsActiveProcesses链表中删除,它不能够防止使用API函数OpenProcess打开进程。这样就
有一种检测进程的方法就是尝试穷举Pid然后调用OpenProcess。我不推荐这个方法,因为它没有任何优点,我甚至想说这是一种“狗屁”方案
。不过它的存在意味着在系统中除了通过PsActiveProcesses得到进程列表之外还可以通过调用OpenProcess。当穷举ProcessId的时候我们会注
意到一个进程可以被几个不同的Pid打开,这暗示可能存在着有点像HANDLE_TABLE的另一个进程列表。为了证明这个的猜想,我们来看看
ZwOpenProcess函数:
Code:
PAGE:0049D59E ; NTSTATUS __stdcall NtOpenProcess(PHANDLE ProcessHandle, ACCESS_MASK DesiredAccess,
POBJECT_ATTRIBUTES ObjectAttributes,PCLIENT_ID ClientId)
PAGE:0049D59E public NtOpenProcess
PAGE:0049D59E NtOpenProcess proc near
PAGE:0049D59E
PAGE:0049D59E ProcessHandle = dword ptr 4
PAGE:0049D59E DesiredAccess = dword ptr 8
PAGE:0049D59E ObjectAttributes= dword ptr 0Ch
PAGE:0049D59E ClientId = dword ptr 10h
PAGE:0049D59E
PAGE:0049D59E push 0C4h
PAGE:0049D5A3 push offset dword_413560 ; int
PAGE:0049D5A8 call sub_40BA92
PAGE:0049D5AD xor esi, esi
PAGE:0049D5AF mov [ebp-2Ch], esi
PAGE:0049D5B2 xor eax, eax
PAGE:0049D5B4 lea edi, [ebp-28h]
PAGE:0049D5B7 stosd
PAGE:0049D5B8 mov eax, large fs:124h
PAGE:0049D5BE mov al, [eax+140h]
PAGE:0049D5C4 mov [ebp-34h], al
PAGE:0049D5C7 test al, al
PAGE:0049D5C9 jz loc_4BE034
PAGE:0049D5CF mov [ebp-4], esi
PAGE:0049D5D2 mov eax, MmUserProbeAddress
PAGE:0049D5D7 mov ecx, [ebp+8]
PAGE:0049D5DA cmp ecx, eax
PAGE:0049D5DC jnb loc_520CDE
PAGE:0049D5E2 loc_49D5E2:
PAGE:0049D5E2 mov eax, [ecx]
PAGE:0049D5E4 mov [ecx], eax
PAGE:0049D5E6 mov ebx, [ebp+10h]
PAGE:0049D5E9 test bl, 3
PAGE:0049D5EC jnz loc_520CE5
PAGE:0049D5F2 loc_49D5F2:
PAGE:0049D5F2 mov eax, MmUserProbeAddress
PAGE:0049D5F7 cmp ebx, eax
PAGE:0049D5F9 jnb loc_520CEF
PAGE:0049D5FF loc_49D5FF:
PAGE:0049D5FF cmp [ebx+8], esi
PAGE:0049D602 setnz byte ptr [ebp-1Ah]
PAGE:0049D606 mov ecx, [ebx+0Ch]
PAGE:0049D609 mov [ebp-38h], ecx
PAGE:0049D60C mov ecx, [ebp+14h]
PAGE:0049D60F cmp ecx, esi
PAGE:0049D611 jz loc_4CCB88
PAGE:0049D617 test cl, 3
PAGE:0049D61A jnz loc_520CFB
PAGE:0049D620 loc_49D620:
PAGE:0049D620 cmp ecx, eax
PAGE:0049D622 jnb loc_520D0D
PAGE:0049D628 loc_49D628:
PAGE:0049D628 mov eax, [ecx]
PAGE:0049D62A mov [ebp-2Ch], eax
PAGE:0049D62D mov eax, [ecx+4]
PAGE:0049D630 mov [ebp-28h], eax
PAGE:0049D633 mov byte ptr [ebp-19h], 1
PAGE:0049D637 loc_49D637:
PAGE:0049D637 or dword ptr [ebp-4], 0FFFFFFFFh
PAGE:0049D63B loc_49D63B:
PAGE:0049D63B
PAGE:0049D63B cmp byte ptr [ebp-1Ah], 0
PAGE:0049D63F jnz loc_520D34
PAGE:0049D645 loc_49D645:
PAGE:0049D645 mov eax, PsProcessType
PAGE:0049D64A add eax, 68h
PAGE:0049D64D push eax
PAGE:0049D64E push dword ptr [ebp+0Ch]
PAGE:0049D651 lea eax, [ebp-0D4h]
PAGE:0049D657 push eax
PAGE:0049D658 lea eax, [ebp-0B8h]
PAGE:0049D65E push eax
PAGE:0049D65F call SeCreateAccessState
PAGE:0049D664 cmp eax, esi
PAGE:0049D666 jl loc_49D718
PAGE:0049D66C push dword ptr [ebp-34h] ; PreviousMode
PAGE:0049D66F push ds:stru_5B6978.HighPart
PAGE:0049D675 push ds:stru_5B6978.LowPart ; PrivilegeValue
PAGE:0049D67B call SeSinglePrivilegeCheck
PAGE:0049D680 test al, al
PAGE:0049D682 jnz loc_4AA7DB
PAGE:0049D688 loc_49D688:
PAGE:0049D688 cmp byte ptr [ebp-1Ah], 0
PAGE:0049D68C jnz loc_520D52
PAGE:0049D692 cmp byte ptr [ebp-19h], 0
PAGE:0049D696 jz loc_4CCB9A
PAGE:0049D69C mov [ebp-30h], esi
PAGE:0049D69F cmp [ebp-28h], esi
PAGE:0049D6A2 jnz loc_4C1301
PAGE:0049D6A8 lea eax, [ebp-24h]
PAGE:0049D6AB push eax
PAGE:0049D6AC push dword ptr [ebp-2Ch]
PAGE:0049D6AF call PsLookupProcessByProcessId
PAGE:0049D6B4 loc_49D6B4:
正如你看到的,这段代码拷贝给定的指针,检查是否指向用户地址空间,核对访问权限和是否有“SetDebugPrivilege”的权限,然后从
CLIENT_ID结构中找到ProcessId并传递给PsLookupProcessByProcessId函数,PsLookupProcessByProcessId的功能是得到ProcessId的EPROCESS
。函数的其余部分对我们来说没什么用,现在我们来看看PsLookupProcessByProcessId:
Code:
PAGE:0049D725 public PsLookupProcessByProcessId
PAGE:0049D725 PsLookupProcessByProcessId proc near
PAGE:0049D725
PAGE:0049D725
PAGE:0049D725 ProcessId = dword ptr 8
PAGE:0049D725 Process = dword ptr 0Ch
PAGE:0049D725
PAGE:0049D725 mov edi, edi
PAGE:0049D727 push ebp
PAGE:0049D728 mov ebp, esp
PAGE:0049D72A push ebx
PAGE:0049D72B push esi
PAGE:0049D72C mov eax, large fs:124h
PAGE:0049D732 push [ebp+ProcessId]
PAGE:0049D735 mov esi, eax
PAGE:0049D737 dec dword ptr [esi+0D4h]
PAGE:0049D73D push PspCidTable
PAGE:0049D743 call ExMapHandleToPointer
PAGE:0049D748 mov ebx, eax
PAGE:0049D74A test ebx, ebx
PAGE:0049D74C mov [ebp+ProcessId], STATUS_INVALID_PARAMETER
PAGE:0049D753 jz short loc_49D787
PAGE:0049D755 push edi
PAGE:0049D756 mov edi, [ebx]
PAGE:0049D758 cmp byte ptr [edi], 3
PAGE:0049D75B jnz short loc_49D77A
PAGE:0049D75D cmp dword ptr [edi+1A4h], 0
PAGE:0049D764 jz short loc_49D77A
PAGE:0049D766 mov ecx, edi
PAGE:0049D768 call sub_4134A9
PAGE:0049D76D test al, al
PAGE:0049D76F jz short loc_49D77A
PAGE:0049D771 mov eax, [ebp+Process]
PAGE:0049D774 and [ebp+ProcessId], 0
PAGE:0049D778 mov [eax], edi
PAGE:0049D77A loc_49D77A:
PAGE:0049D77A push ebx
PAGE:0049D77B push PspCidTable
PAGE:0049D781 call ExUnlockHandleTableEntry
PAGE:0049D786 pop edi
PAGE:0049D787 loc_49D787:
PAGE:0049D787 inc dword ptr [esi+0D4h]
PAGE:0049D78D jnz short loc_49D79A
PAGE:0049D78F lea eax, [esi+34h]
PAGE:0049D792 cmp [eax], eax
PAGE:0049D794 jnz loc_52388A
PAGE:0049D79A loc_49D79A:
PAGE:0049D79A mov eax, [ebp+ProcessId]
PAGE:0049D79D pop esi
PAGE:0049D79E pop ebx
PAGE:0049D79F pop ebp
PAGE:0049D7A0 retn 8
以上我们所看到的,证实了存在像HANDLE_TABLE一样组织结构的第2个进程列表。这个表叫做PspCidTable,它包括进程和线程的列表,
PsLookupProcessThreadByCid函数和PsLookupThreadByThreadId函数都用到了这个表。我们看到,句柄和句柄表的指针被传递给了
ExMapHandleToPointer函数,该函数(在句柄有效的情况下)返回一个指向描述给定句柄的表的一个元素 - HANDLE_TABLE_ENTRY。当我们用
PDBdump分析完ntoskrnl.pdb并且得到分析日志后,会得到如下结果:
Code:
struct _HANDLE_TABLE_ENTRY {
// static data ------------------------------------
// non-static data --------------------------------
/*<thisrel this+0x0>*/ /*|0x4|*/ void* Object;
/*<thisrel this+0x0>*/ /*|0x4|*/ unsigned long ObAttributes;
/*<thisrel this+0x0>*/ /*|0x4|*/ struct _HANDLE_TABLE_ENTRY_INFO* InfoTable;
/*<thisrel this+0x0>*/ /*|0x4|*/ unsigned long Value;
/*<thisrel this+0x4>*/ /*|0x4|*/ unsigned long GrantedAccess;
/*<thisrel this+0x4>*/ /*|0x2|*/ unsigned short GrantedAccessIndex;
/*<thisrel this+0x6>*/ /*|0x2|*/ unsigned short CreatorBackTraceIndex;
/*<thisrel this+0x4>*/ /*|0x4|*/ long NextFreeTableEntry;
};// <size 0x8>
We can recover HANDLE_TABLE_ENTRY structure from this:
我们还原一下HANDLE_TABLE_ENTRY结构的C代码:
Code:
typedef struct _HANDLE_TABLE_ENTRY
{
union
{
PVOID Object;
ULONG ObAttributes;
PHANDLE_TABLE_ENTRY_INFO InfoTable;
ULONG Value;
};
union
{
union
{
ACCESS_MASK GrantedAccess;
struct
{
USHORT GrantedAccessIndex;
USHORT CreatorBackTraceIndex;
};
};
LONG NextFreeTableEntry;
};
} HANDLE_TABLE_ENTRY, *PHANDLE_TABLE_ENTRY;
怎么使用它呢?首先,我们比较感兴趣的是Object域的内容,它是被句柄描述的目标指针和这个表的给定元素的用法标志(我将稍后解释这句
话)。GrantedAccess域指定了通过这个句柄对目标的访问权限许可,这个很有趣。比如,以只读方式打开一个文件,修改这个域之后就可以写
这个文件了。这个方法可以用在对一些正在被读/写的文件的访问上(比如,正在被其他进程锁定的文件)。应该回到我们的问题上来了 - 通
过对PspCidTable的分析得到进程句柄列表。
要分析它我们得了解句柄表的格式,这样才能遍历这个列表。在这个地方Windows 2000和Windows XP有着巨大的不同。由于句柄表格式不尽相
同,所以我们应该把操作系统分类进行分析。
因为Windows 2000的句柄表相对简单一些,所以我们先分析它。先来看看ExMapHandleToPointer函数:
Code:
PAGE:00493285 ExMapHandleToPointer proc near
PAGE:00493285
PAGE:00493285
PAGE:00493285 HandleTable = dword ptr 8
PAGE:00493285 Handle = dword ptr 0Ch
PAGE:00493285
PAGE:00493285 push esi
PAGE:00493286 push [esp+Handle]
PAGE:0049328A push [esp+4+HandleTable]
PAGE:0049328E call ExpLookupHandleTableEntry
PAGE:00493293 mov esi, eax
PAGE:00493295 test esi, esi
PAGE:00493297 jz short loc_4932A9
PAGE:00493299 push esi
PAGE:0049329A push [esp+4+HandleTable]
PAGE:0049329E call ExLockHandleTableEntry
PAGE:004932A3 neg al
PAGE:004932A5 sbb eax, eax
PAGE:004932A7 and eax, esi
PAGE:004932A9 loc_4932A9:
PAGE:004932A9 pop esi
PAGE:004932AA retn 8
PAGE:004932AA ExMapHandleToPointer endp
这里我们调用搜索HANDLE_TABLE的函数ExMapHandleToPointer以及设置Lock Bit的ExLockHandleTableEntry函数。要了解句柄表的内部结构我
们必须反汇编这些函数。先从ExpLookupHandleTableEntry函数开始:
Code:
PAGE:00493545 ExpLookupHandleTableEntry proc near
PAGE:00493545
PAGE:00493545
PAGE:00493545 HandleTable = dword ptr 0Ch
PAGE:00493545 Handle = dword ptr 10h
PAGE:00493545
PAGE:00493545 push esi
PAGE:00493546 push edi
PAGE:00493547 mov edi, [esp+Handle]
PAGE:0049354B mov eax, 0FFh
PAGE:00493550 mov ecx, edi
PAGE:00493552 mov edx, edi
PAGE:00493554 mov esi, edi
PAGE:00493556 shr ecx, 12h
PAGE:00493559 shr edx, 0Ah
PAGE:0049355C shr esi, 2
PAGE:0049355F and ecx, eax
PAGE:00493561 and edx, eax
PAGE:00493563 and esi, eax
PAGE:00493565 test edi, 0FC000000h
PAGE:0049356B jnz short loc_49358A
PAGE:0049356D mov eax, [esp+HandleTable]
PAGE:00493571 mov eax, [eax+8]
PAGE:00493574 mov ecx, [eax+ecx*4]
PAGE:00493577 test ecx, ecx
PAGE:00493579 jz short loc_49358A
PAGE:0049357B mov ecx, [ecx+edx*4]
PAGE:0049357E test ecx, ecx
PAGE:00493580 jz short loc_49358A
PAGE:00493582 lea eax, [ecx+esi*8]
PAGE:00493585 loc_493585:
PAGE:00493585 pop edi
PAGE:00493586 pop esi
PAGE:00493587 retn 8
PAGE:0049358A loc_49358A:
PAGE:0049358A xor eax, eax
PAGE:0049358C jmp short loc_493585
PAGE:0049358C ExpLookupHandleTableEntry endp
除此之外,我们来看看从ntoskrnl.pdb中得到的HANDLE_TABLE结构:
Code:
struct _HANDLE_TABLE {
// static data ------------------------------------
// non-static data --------------------------------
/*<thisrel this+0x0>*/ /*|0x4|*/ unsigned long Flags;
/*<thisrel this+0x4>*/ /*|0x4|*/ long HandleCount;
/*<thisrel this+0x8>*/ /*|0x4|*/ struct _HANDLE_TABLE_ENTRY*** Table;
/*<thisrel this+0xc>*/ /*|0x4|*/ struct _EPROCESS* QuotaProcess;
/*<thisrel this+0x10>*/ /*|0x4|*/ void* UniqueProcessId;
/*<thisrel this+0x14>*/ /*|0x4|*/ long FirstFreeTableEntry;
/*<thisrel this+0x18>*/ /*|0x4|*/ long NextIndexNeedingPool;
/*<thisrel this+0x1c>*/ /*|0x38|*/ struct _ERESOURCE HandleTableLock;
/*<thisrel this+0x54>*/ /*|0x8|*/ struct _LIST_ENTRY HandleTableList;
/*<thisrel this+0x5C>*/ /*|0x10|*/ struct _KEVENT HandleContentionEvent;
}; // <size 0x6c>
根据这些数据我们用C语言还原这个结构:
Code:
typedef struct _WIN2K_HANDLE_TABLE
{
ULONG Flags;
LONG HandleCount;
PHANDLE_TABLE_ENTRY **Table;
PEPROCESS QuotaProcess;
HANDLE UniqueProcessId;
LONG FirstFreeTableEntry;
LONG NextIndexNeedingPool;
ERESOURCE HandleTableLock;
LIST_ENTRY HandleTableList;
KEVENT HandleContentionEvent;
} WIN2K_HANDLE_TABLE , *PWIN2K_HANDLE_TABLE ;
显而易见,句柄表由对象表的三个层次的索引组成。现在我们再来看看ExLookhandleTableEntry函数:
Code:
PAGE:00492E2B ExLockHandleTableEntry proc near
PAGE:00492E2B
PAGE:00492E2B
PAGE:00492E2B var_8 = dword ptr -8
PAGE:00492E2B var_4 = dword ptr -4
PAGE:00492E2B HandleTable = dword ptr 8
PAGE:00492E2B Entry = dword ptr 0Ch
PAGE:00492E2B
PAGE:00492E2B push ebp
PAGE:00492E2C mov ebp, esp
PAGE:00492E2E push ecx
PAGE:00492E2F push ecx
PAGE:00492E30 push ebx
PAGE:00492E31 push esi
PAGE:00492E32 xor ebx, ebx
PAGE:00492E34 loc_492E34:
PAGE:00492E34 mov eax, [ebp+Entry]
PAGE:00492E37 mov esi, [eax]
PAGE:00492E39 test esi, esi
PAGE:00492E3B mov [ebp+var_8], esi
PAGE:00492E3E jz short loc_492E89
PAGE:00492E40 jle short loc_492E64
PAGE:00492E42 mov eax, esi
PAGE:00492E44 or eax, 80000000h // set WIN2K_TABLE_ENTRY_LOCK_BIT
PAGE:00492E49 mov [ebp+var_4], eax
PAGE:00492E4C mov eax, [ebp+var_8]
PAGE:00492E4F mov ecx, [ebp+Entry]
PAGE:00492E52 mov edx, [ebp+var_4]
PAGE:00492E55 cmpxchg [ecx], edx
PAGE:00492E58 cmp eax, esi
PAGE:00492E5A jnz short loc_492E64
PAGE:00492E5C mov al, 1
PAGE:00492E5E loc_492E5E:
PAGE:00492E5E pop esi
PAGE:00492E5F pop ebx
PAGE:00492E60 leave
PAGE:00492E61 retn 8
PAGE:00492E64 loc_492E64:
PAGE:00492E64 mov eax, ebx
PAGE:00492E66 inc ebx
PAGE:00492E67 cmp eax, 1
PAGE:00492E6A jb loc_4BC234
PAGE:00492E70 mov eax, [ebp+HandleTable]
PAGE:00492E73 push offset unk_46D240 ; Timeout
PAGE:00492E78 push 0 ; Alertable
PAGE:00492E7A push 0 ; WaitMode
PAGE:00492E7C add eax, 5Ch
PAGE:00492E7F push 0 ; WaitReason
PAGE:00492E81 push eax ; Object
PAGE:00492E82 call KeWaitForSingleObject
PAGE:00492E87 jmp short loc_492E34
PAGE:00492E89 loc_492E89:
PAGE:00492E89 xor al, al
PAGE:00492E8B jmp short loc_492E5E
PAGE:00492E8B ExLockHandleTableEntry endp
这段代码检查了HANDLE_TABLE_ENTRY结构的Object成员的第31位,设置该位,如果该位被设置,意味着等待HANDLE_TABLE的
HandleContentionEvent。对我们来说设置TABLE_ENTRY_LOCK_BIT才是最重要的,因为它是目标地址的一部分,如果标志位没有设置,我们就会
得到无效句柄。现在我们明白了句柄表的格式,可以写代码来遍历这个表了:
Code:
void ScanWin2KHandleTable(PWIN2K_HANDLE_TABLE HandleTable)
{
int i, j, k;
PHANDLE_TABLE_ENTRY Entry;
for (i = 0; i < 0x100; i++)
{
if (HandleTable->Table)
{
for (j = 0; j < 0x100; j++)
{
if (HandleTable->Table[j])
{
for (k = 0; k < 0x100; k++)
{
Entry = &HandleTable->Table[j][k];
if (Entry->Object)
ProcessObject((PVOID)((ULONG)Entry->Object | WIN2K_TABLE_ENTRY_LOCK_BIT));
}
}
}
}
}
}
这段代码处理了所有表中的成员,并且为每一个成员调用了ProcessObject函数。ProcessObject函数检测成员类型并且恰当地处理了它们。这
个函数代码如下:
Code:
void ProcessObject(PVOID Object)
{
POBJECT_HEADER ObjectHeader = OBJECT_TO_OBJECT_HEADER(Object);
if (ObjectHeader->Type == *PsProcessType) CollectProcess(Object);
if (ObjectHeader->Type == *PsThreadType) ThreadCollect(Object);
}
我们已经了解了Windows 2000下的句柄表结构,现在开始分析Windows XP的表结构。从反汇编ExpLookupHandleTableEntry函数开始:
Code:
PAGE:0048D3C1 ExpLookupHandleTableEntry proc near
PAGE:0048D3C1
PAGE:0048D3C1
PAGE:0048D3C1 HandleTable = dword ptr 8
PAGE:0048D3C1 Handle = dword ptr 0Ch
PAGE:0048D3C1
PAGE:0048D3C1 mov edi, edi
PAGE:0048D3C3 push ebp
PAGE:0048D3C4 mov ebp, esp
PAGE:0048D3C6 and [ebp+Handle], 0FFFFFFFCh
PAGE:0048D3CA mov eax, [ebp+Handle]
PAGE:0048D3CD mov ecx, [ebp+HandleTable]
PAGE:0048D3D0 mov edx, [ebp+Handle]
PAGE:0048D3D3 shr eax, 2
PAGE:0048D3D6 cmp edx, [ecx+38h]
PAGE:0048D3D9 jnb loc_4958D6
PAGE:0048D3DF push esi
PAGE:0048D3E0 mov esi, [ecx]
PAGE:0048D3E2 mov ecx, esi
PAGE:0048D3E4 and ecx, 3 // ecx - table level
PAGE:0048D3E7 and esi, not 3 // esi - pointer to first table
PAGE:0048D3EA sub ecx, 0
PAGE:0048D3ED jnz loc_48DEA4
PAGE:0048D3F3 lea eax, [esi+eax*8]
PAGE:0048D3F6 loc_48D3F6:
PAGE:0048D3F6 pop esi
PAGE:0048D3F7 loc_48D3F7:
PAGE:0048D3F7 pop ebp
PAGE:0048D3F8 retn 8
PAGE:0048DEA4 loc_48DEA4:
PAGE:0048DEA4 dec ecx
PAGE:0048DEA5 mov ecx, eax
PAGE:0048DEA7 jnz loc_52F57A
PAGE:0048DEAD shr ecx, 9
PAGE:0048DEB0 mov ecx, [esi+ecx*4]
PAGE:0048DEB3 loc_48DEB3:
PAGE:0048DEB3 and eax, 1FFh
PAGE:0048DEB8 lea eax, [ecx+eax*8]
PAGE:0048DEBB jmp loc_48D3F6
PAGE:0052F57A loc_52F57A:
PAGE:0052F57A shr ecx, 13h
PAGE:0052F57D mov edx, ecx
PAGE:0052F57F mov ecx, [esi+ecx*4]
PAGE:0052F582 shl edx, 13h
PAGE:0052F585 sub eax, edx
PAGE:0052F587 mov edx, eax
PAGE:0052F589 shr edx, 9
PAGE:0052F58C mov ecx, [ecx+edx*4]
PAGE:0052F58F jmp loc_48DEB3
再来看看ntoskrnl.pdb中的HANDLE_TABLE:
Code:
struct _HANDLE_TABLE {
// static data ------------------------------------
// non-static data --------------------------------
/*<thisrel this+0x0>*/ /*|0x4|*/ unsigned long TableCode;
/*<thisrel this+0x4>*/ /*|0x4|*/ struct _EPROCESS* QuotaProcess;
/*<thisrel this+0x8>*/ /*|0x4|*/ void* UniqueProcessId;
/*<thisrel this+0xc>*/ /*|0x10|*/ struct _EX_PUSH_LOCK HandleTableLock[4];
/*<thisrel this+0x1c>*/ /*|0x8|*/ struct _LIST_ENTRY HandleTableList;
/*<thisrel this+0x24>*/ /*|0x4|*/ struct _EX_PUSH_LOCK HandleContentionEvent;
/*<thisrel this+0x28>*/ /*|0x4|*/ struct _HANDLE_TRACE_DEBUG_INFO* DebugInfo;
/*<thisrel this+0x2c>*/ /*|0x4|*/ long ExtraInfoPages;
/*<thisrel this+0x30>*/ /*|0x4|*/ unsigned long FirstFree;
/*<thisrel this+0x34>*/ /*|0x4|*/ unsigned long LastFree;
/*<thisrel this+0x38>*/ /*|0x4|*/ unsigned long NextHandleNeedingPool;
/*<thisrel this+0x3c>*/ /*|0x4|*/ long HandleCount;
/*<thisrel this+0x40>*/ /*|0x4|*/ unsigned long Flags;
/*<bitfield this+0x40>*/ /*|0x1|*/ unsigned char StrictFIFO:0:1;
}; // <size 0x44>
利用以上信息还原该结构:
Code:
typedef struct _XP_HANDLE_TABLE
{
ULONG TableCode;
PEPROCESS QuotaProcess;
PVOID UniqueProcessId;
EX_PUSH_LOCK HandleTableLock[4];
LIST_ENTRY HandleTableList;
EX_PUSH_LOCK HandleContentionEvent;
PHANDLE_TRACE_DEBUG_INFO DebugInfo;
LONG ExtraInfoPages;
ULONG FirstFree;
ULONG LastFree;
ULONG NextHandleNeedingPool;
LONG HandleCount;
LONG Flags;
UCHAR StrictFIFO;
} XP_HANDLE_TABLE, *PXP_HANDLE_TABLE;
从上面的表中可以很明显看出ExpLookupHandleTableEntry函数从HANDLE_TABLE结构中得到TableCode的值,并基于其低2位的内容计算出表的层
次数。其余的位指向第1层表。因此Windows XP下的HANDLE_TABLE可以拥有1到3个层次,每一个层次的表的大小为1FFh。当表中记录的数量增加
时,系统会自动增加层数。很明显,当表中记录的数量超过0x200的时候表就会拥有两层,当大于0x40000时增加到第3层。不知道当销毁对象时
系统会不会减少表的层数,我没有注意到这件事。
Windows XP下没有ExLockHandleTableEntry函数,因此表中相应的模块被定位在ExMapHandleToPointer函数中。反汇编这个函数看看它做了什
么?
Code:
PAGE:0048F61E ExMapHandleToPointer proc near
PAGE:0048F61E
PAGE:0048F61E
PAGE:0048F61E var_8 = dword ptr -8
PAGE:0048F61E var_4 = dword ptr -4
PAGE:0048F61E HandleTable = dword ptr 8
PAGE:0048F61E Handle = dword ptr 0Ch
PAGE:0048F61E
PAGE:0048F61E mov edi, edi
PAGE:0048F620 push ebp
PAGE:0048F621 mov ebp, esp
PAGE:0048F623 push ecx
PAGE:0048F624 push ecx
PAGE:0048F625 push edi
PAGE:0048F626 mov edi, [ebp+Handle]
PAGE:0048F629 test di, 7FCh
PAGE:0048F62E jz loc_4A2A36
PAGE:0048F634 push ebx
PAGE:0048F635 push esi
PAGE:0048F636 push edi
PAGE:0048F637 push [ebp+HandleTable]
PAGE:0048F63A call ExpLookupHandleTableEntry
PAGE:0048F63F mov esi, eax
PAGE:0048F641 test esi, esi
PAGE:0048F643 jz loc_4A2711
PAGE:0048F649 mov [ebp+var_4], esi
PAGE:0048F64C loc_48F64C:
PAGE:0048F64C mov ebx, [esi]
PAGE:0048F64E test bl, 1
PAGE:0048F651 mov [ebp+var_8], ebx
PAGE:0048F654 jz loc_508844
PAGE:0048F65A lea eax, [ebx-1]
PAGE:0048F65D mov [ebp+Handle], eax
PAGE:0048F660 mov eax, [ebp+var_8]
PAGE:0048F663 mov ecx, [ebp+var_4]
PAGE:0048F666 mov edx, [ebp+Handle]
PAGE:0048F669 cmpxchg [ecx], edx
PAGE:0048F66C cmp eax, ebx
PAGE:0048F66E jnz loc_50884C
PAGE:0048F674 mov eax, esi
PAGE:0048F676 loc_48F676:
PAGE:0048F676 pop esi
PAGE:0048F677 pop ebx
PAGE:0048F678 loc_48F678:
PAGE:0048F678 pop edi
PAGE:0048F679 leave
PAGE:0048F67A retn 8
PAGE:0048F67A ExMapHandleToPointer endp
ExpLookuphandleTableEntry函数返回指向HANDLE_TABLE_ENTRY的指针后,我们要检查Object域的低字节,如果是被设置了的,说明是被清除了
的(俄文翻译者kao注:希望我翻译的对...),如果该位没有被设置,我们要等到它被设置了为止。因此当得到对象地址的时候我们不应该设
置高位(Windows 2000平台),而是要清除低位。综上所述,扫描表的代码如下:
Code:
void ScanXpHandleTable(PXP_HANDLE_TABLE HandleTable)
{
int i, j, k;
PHANDLE_TABLE_ENTRY Entry;
ULONG TableCode = HandleTable->TableCode & ~TABLE_LEVEL_MASK;
switch (HandleTable->TableCode & TABLE_LEVEL_MASK)
{
case 0 :
for (i = 0; i < 0x200; i++)
{
Entry = &((PHANDLE_TABLE_ENTRY)TableCode);
if (Entry->Object) ProcessObject((PVOID)((ULONG)Entry->Object & ~XP_TABLE_ENTRY_LOCK_BIT));
}
break;
case 1 :
for (i = 0; i < 0x200; i++)
{
if (((PVOID *)TableCode))
{
for (j = 0; j < 0x200; j++)
{
Entry = &((PHANDLE_TABLE_ENTRY *)TableCode)[j];
if (Entry->Object) ProcessObject((PVOID)((ULONG)Entry->Object & ~XP_TABLE_ENTRY_LOCK_BIT));
}
}
}
break;
case 2 :
for (i = 0; i < 0x200; i++)
{
if (((PVOID *)TableCode))
{
for (j = 0; j < 0x200; j++)
{
if (((PVOID **)TableCode)[j])
{
for (k = 0; k < 0x200; k++)
{
Entry = &((PHANDLE_TABLE_ENTRY **)TableCode)[j][k];
if (Entry->Object)
ProcessObject((PVOID)((ULONG)Entry->Object & ~XP_TABLE_ENTRY_LOCK_BIT));
}
}
}
}
}
break;
}
}
我们已经明白对象表格式了。现在想要枚举进程我们还需要得到PspCidTable的地址。也许你已经猜到了,我们要在
PsLookupProcessByProcessId函数中搜索,这个函数中的第1个函数调用包含着PspCidTable的地址。代码如下:
Code:
void GetPspCidTable()
{
PUCHAR cPtr, pOpcode;
ULONG Length;
for (cPtr = (PUCHAR)PsLookupProcessByProcessId;
cPtr < (PUCHAR)PsLookupProcessByProcessId + PAGE_SIZE;
cPtr += Length)
{
Length = SizeOfCode(cPtr, &pOpcode);
if (!Length) break;
if (*(PUSHORT)cPtr == 0x35FF && *(pOpcode + 6) == 0xE8)
{
PspCidTable = **(PVOID **)(pOpcode + 2);
break;
}
}
}
现在我们知道怎样处理PspCidTable了,可以非常容易地查看到所有进程的表中的所有元素,分析那些属于隐藏进程的对象,就像我们在用户态
做的一样,如果你已经理解了前面所讲的东西,你一定可以做得到。
中文参考:《JIURL玩玩Win2k进程线程篇 HANDLE_TABLE》(http://jiurl.nease.net/document/JiurlPlayWin2k/PsHandleTable.htm)
感谢:firstrose, JIURL, linhanshi.
-----------------------------------------------------
比较长,轰炸大家一下。希望 Syscheck 做得更好!!
并向 Wangsea 等高风亮节的程序员们致敬!!! |
|