【旧文章搬运】Windows句柄表分配算法分析(实验部分)
原文发表于百度空间,2009-03-31
==========================================================================
理论结合实践,这是我一贯的学习方法~~
实验目的:以实验的方式观察PspCidTable的变化,从中了解Windows句柄表的分配过程.
实验器材:Windbg,RunIt(一个可控的不断创建线程的程序),DebugView
知识回顾:
如图所示,句柄表的结构根据TableLevel来确定:
TableLevel为0时,CapturedTable是一级表(蓝箭头)
TableLevel为1时,CapturedTable是二级表,CapturedTable[i]是一级表(绿箭头)
TableLevel为2时,CapturedTable是三级表,CapturedTable[i]是二级表,CapturedTable[i][j]是一级表(红箭头)
三级表的内容是二级表指针,二级表的内容是一级表指针,一级表中放的才是对象及访问属性(HANDLE_TABLE_ETNRY结构)
准备工作:获取PspCidTable的基本信息
lkd> dd PspCidTable l1 8055a360 e1001810 //获取PspCidTable的地址 lkd> dt _HANDLE_TABLE e1001810 nt!_HANDLE_TABLE +0x000 TableCode : 0xe1003000 //表基址为0xe1003000,一级表 +0x004 QuotaProcess : (null) +0x008 UniqueProcessId : (null) +0x00c HandleTableLock : [4] _EX_PUSH_LOCK +0x01c HandleTableList : _LIST_ENTRY [ 0xe100182c - 0xe100182c ] +0x024 HandleContentionEvent : _EX_PUSH_LOCK +0x028 DebugInfo : (null) +0x02c ExtraInfoPages : 0 +0x030 FirstFree : 0x308 +0x034 LastFree : 0x34c +0x038 NextHandleNeedingPool : 0x800 //当前的句柄上限 +0x03c HandleCount : 329 +0x040 Flags : 1 +0x040 StrictFIFO : 0y1
此时可以观察到PspCidTable=e1001810,当前TableCode为0xe1003000,低两位表明是一级表,表地址为0xe1003000
lkd> dd 0xe1003000 e1003000 00000000 fffffffe 821bb661 00000000 e1003010 821bb3e9 00000000 821ba021 00000000 e1003020 821bad21 00000000 821baaa9 00000000 e1003030 821ba831 00000000 821ba5b9 00000000 e1003040 821ba341 00000000 821b9021 00000000 e1003050 821b9da9 00000000 821b9b31 00000000 e1003060 821b98b9 00000000 821b9641 00000000 e1003070 821b93c9 00000000 821b8021 00000000
这时可以看到一级表存放的进线程对象了
实验一:观察句柄表的升级
由于二级表升级为三级表需要极大的句柄容量,因此我们通常只能观察到句柄表由一级表升为二级表的过程
运行RunIt.exe,按回车不断创建线程,直至新线程的ThreadId大于当前句柄表的上限0x800.
此时再观察PspCidTable:
lkd> dt _HANDLE_TABLE e1001810 nt!_HANDLE_TABLE +0x000 TableCode : 0xe11a4001 //这时已经为二级表了 +0x004 QuotaProcess : (null) +0x008 UniqueProcessId : (null) +0x00c HandleTableLock : [4] _EX_PUSH_LOCK +0x01c HandleTableList : _LIST_ENTRY [ 0xe100182c - 0xe100182c ] +0x024 HandleContentionEvent : _EX_PUSH_LOCK +0x028 DebugInfo : (null) +0x02c ExtraInfoPages : 0 +0x030 FirstFree : 0x860 +0x034 LastFree : 0x38c +0x038 NextHandleNeedingPool : 0x1000 //句柄上限达到了0x800*2=0x1000 +0x03c HandleCount : 529 +0x040 Flags : 1 +0x040 StrictFIFO : 0y1
这时的TableCode低两位表时现在是二级表,掩去低两位就是二级表的地址0xe11a4000了
lkd> dd 0xe11a4000 //观察二级表的内容 e11a4000 e1003000 e11b5000 00000000 00000000 e11a4010 00000000 00000000 00000000 00000000 e11a4020 00000000 00000000 00000000 00000000 e11a4030 00000000 00000000 00000000 00000000 e11a4040 00000000 00000000 00000000 00000000 e11a4050 00000000 00000000 00000000 00000000 e11a4060 00000000 00000000 00000000 00000000 e11a4070 00000000 00000000 00000000 00000000
可以看到,原来的一级表e1003000已经成为了二级表中的第一个元素.同时新分配了一个一级表为e11b5000.这样,句柄表的升级就完成了
实验二:观察新分配的句柄表是如何填充的
前面已经分析过,新分配的句柄表被填充成一个有序的FreeHandle序列.
观察新分配的这个二级表:
lkd> dd e11b5000 e11b5000 00000000 fffffffe 81f008b9 00000000 e11b5010 81f00641 00000000 81f003c9 00000000 e11b5020 81f5d021 00000000 81f5dda9 00000000 e11b5030 81f5db31 00000000 81f5d8b9 00000000 e11b5040 81f5d641 00000000 81f5d3c9 00000000 e11b5050 81eff021 00000000 81effda9 00000000 e11b5060 81effb31 00000000 81eff8b9 00000000 e11b5070 81eff641 00000000 81eff3c9 00000000 //RunIt创建的最后一个线程的ETHREAD在e11b5078处 lkd> dd e11b5080 82012921 00000000 00000000 00000220 //这里的一部分句柄也被使用过了,因为可能别的进程也创建了线程 e11b5090 00000000 00000000 00000000 00000478 e11b50a0 00000000 0000038c 81f5cda9 00000000 e11b50b0 00000000 00000850 00000000 0000084c e11b50c0 00000000 00000864 00000000 00000868 e11b50d0 00000000 0000086c 00000000 00000870 e11b50e0 00000000 00000874 00000000 00000878 e11b50f0 00000000 0000087c 00000000 00000880 lkd> e11b5100 00000000 00000884 00000000 00000888 e11b5110 00000000 0000088c 00000000 00000890 e11b5120 00000000 00000894 00000000 00000898 e11b5130 00000000 0000089c 00000000 000008a0 e11b5140 00000000 000008a4 00000000 000008a8 e11b5150 00000000 000008ac 00000000 000008b0 e11b5160 00000000 000008b4 00000000 000008b8 e11b5170 00000000 000008bc 00000000 000008c0
由图可知,最后一个ThreadId=0x83c,那么它在第二个表中的偏移是e11b5000+(0x83c-0x800)*2=e11b5078
从e11b5080到e11b50c0这部分的内容表明该范围内的部分句柄已经被使用过且又释放了(如果想避免该问题,你可以使用livekd的方式进行本实验,这样中断到调试器时就不会有其它动作来干扰我们的观察),但是尚未影响到e11b50c0之后的部分.
来观察这里:
e11b50c0 00000000 00000864 00000000 00000868 e11b50d0 00000000 0000086c 00000000 00000870 e11b50e0 00000000 00000874 00000000 00000878 e11b50f0 00000000 0000087c 00000000 00000880
e11b50c0作为二级表中的第二个一级表,它所对应的句柄为:
(e11b50c0-e11b5000)/2+0x800*(2-1)=0x860 //如果了解了句柄表的基本结构,这个计算很容易理解
而它的NextFreeHadleTableEntry则指向它紧挨着的下一个HANDLE_TABLE_ENTRY的所对应的句柄0x864
而且很容易看出0x864,0x868,0x86c...构成了一个等差数列.
这个结果可以与前面对ExpAllocateLowLevelTable函数的分析对比,两者是完全一致的.
附Runit程序的源码:
// RunIt.cpp : Defines the entry point for the console application. // #include <windows.h> #include <stdio.h>
DWORD WINAPI ThreadProc(LPVOID lpParameter ); HANDLE hEvent = NULL;
int main(int argc, char* argv[]) { int i=0; DWORD tid=0,oldtid=0; printf("MyPID=%d 0x%x\n",GetCurrentProcessId(),GetCurrentProcessId()); printf("每按一次回车键将产生一个新线程.\n"); hEvent=CreateEvent(NULL,FALSE,TRUE,"TEST"); for (i=0;i<2000;i++) { getchar(); CreateThread(NULL,0,ThreadProc,NULL,NULL,&tid); printf("Runing %d...\tTID=%4d\t0x%x",i,tid,tid); if (tid==oldtid)//已经达到当前进程所能创建线程数量的上限 { break; } oldtid=tid; //Sleep(20); } while (1) { Sleep(100);//停在这里 } return 0; } DWORD WINAPI ThreadProc(LPVOID lpParameter ) { WaitForSingleObject(hEvent,INFINITE); return 0; }