北航刘禹导师做网站,网站设计 用户心理研究,校园招聘,如何做网站的映射私有句柄表
实验环境
win7 x86
什么是私有句柄表#xff1f;
私有句柄表是操作系统内部的一种数据结构#xff0c;用于存储一个进程所拥有的句柄#xff08;或称为句柄对象#xff09;的信息。在操作系统中#xff0c;句柄是一个标识符#xff0c;用于唯一标识一个对…私有句柄表
实验环境
win7 x86
什么是私有句柄表
私有句柄表是操作系统内部的一种数据结构用于存储一个进程所拥有的句柄或称为句柄对象的信息。在操作系统中句柄是一个标识符用于唯一标识一个对象例如文件、套接字、管道等等。GPT是这样说的 那我就举个例子让大家更简单的理解比如我们使用OpenProcessAPI成功打开一个进程时我们便会得到一个返回值这个返回值就叫作句柄我们可以通过这个句柄来间接操作我们OpenProcess打开的那个进程句柄值会被放入我们打开者的私有句柄表里我们用到的时候就会去私有句柄表找出来用当我们不再使用的时候比如调用CloseHandle便可以把刚才那个句柄值从私有句柄表中移除随之我们也就无法以常规的方式去操作刚才打开的那个进程。
从内核中看私有句柄表
私有句柄表所在的位置如下 EPROCESS—ObjectTable[_HANDLE_TABLE]—TableCode0xFFFFFFF8—第一层句柄表或者句柄表指针表的地址
nt!_EPROCESS0x000 Pcb : _KPROCESS0x098 ProcessLock : _EX_PUSH_LOCK0x0a0 CreateTime : _LARGE_INTEGER0x0a8 ExitTime : _LARGE_INTEGER0x0b0 RundownProtect : _EX_RUNDOWN_REF0x0b4 UniqueProcessId : Ptr32 Void0x0b8 ActiveProcessLinks : _LIST_ENTRY0x0c0 ProcessQuotaUsage : [2] Uint4B0x0c8 ProcessQuotaPeak : [2] Uint4B0x0d0 CommitCharge : Uint4B0x0d4 QuotaBlock : Ptr32 _EPROCESS_QUOTA_BLOCK0x0d8 CpuQuotaBlock : Ptr32 _PS_CPU_QUOTA_BLOCK0x0dc PeakVirtualSize : Uint4B0x0e0 VirtualSize : Uint4B0x0e4 SessionProcessLinks : _LIST_ENTRY0x0ec DebugPort : Ptr32 Void0x0f0 ExceptionPortData : Ptr32 Void0x0f0 ExceptionPortValue : Uint4B0x0f0 ExceptionPortState : Pos 0, 3 Bits0x0f4 ObjectTable : Ptr32 _HANDLE_TABLE //进程对应私有句柄表有关结构地址nt!_HANDLE_TABLE0x000 TableCode : Uint4B0x004 QuotaProcess : Ptr32 _EPROCESS0x008 UniqueProcessId : Ptr32 Void0x00c HandleLock : _EX_PUSH_LOCK0x010 HandleTableList : _LIST_ENTRY0x018 HandleContentionEvent : _EX_PUSH_LOCK0x01c DebugInfo : Ptr32 _HANDLE_TRACE_DEBUG_INFO0x020 ExtraInfoPages : Int4B0x024 Flags : Uint4B0x024 StrictFIFO : Pos 0, 1 Bit0x028 FirstFreeHandle : Uint4B0x02c LastFreeHandleEntry : Ptr32 _HANDLE_TABLE_ENTRY0x030 HandleCount : Uint4B0x034 NextHandleNeedingPool : Uint4B0x038 HandleCountHighWatermark : Uint4B.TableCode的值的最后3bit位代表着这个私有句柄表有几层如果最后3bit位为0则代表仅有一层1的话为两层2的话最多为三层以此类推每张句柄表存有4096/8个句柄值但是我们日常使用的电脑中最多有两层就很够多了原因如下 当只有一层的时候那么TableCode直接指向了私有句柄表的首个值每个值占8字节一个表的内存大小为4096个字节一个句柄值为8字节所以此时能存下的最大句柄值个数为4096/8个当为两层的时候TableCode指向了一张句柄表指针的表TableCode4*(层数-1)存的就是对应句柄表的指针这张表依旧是4096个字节那也就是说总共有4096/4个句柄表指针那么一个句柄表有4096/8个句柄值那此时就有(4096/4)x(4096/8)个句柄值了一次类推有点类似呈现了短期的指数大爆炸。那通常情况下我们的常规进程是不会打开这么多对象的很多常规进程基本上就只有1层句柄表。
那句柄和对象的关系是啥呢 在内核里私有句柄的句柄值0xFFFFFFF8后便指向了一个_OBJECT_HEADER结构体结构体内存布局如下
nt!_OBJECT_HEADER0x000 PointerCount : Int4B0x004 HandleCount : Int4B0x004 NextToFree : Ptr32 Void0x008 Lock : _EX_PUSH_LOCK0x00c TypeIndex : UChar //对象类型0x00d TraceFlags : UChar0x00e InfoMask : UChar0x00f Flags : UChar0x010 ObjectCreateInfo : Ptr32 _OBJECT_CREATE_INFORMATION0x010 QuotaBlockCharged : Ptr32 Void0x014 SecurityDescriptor : Ptr32 Void0x018 Body : _QUAD //对象地址其中0xc的位置代表了该句柄对应的对象是一个什么类型的对象比如文件、进程、线程等0x18的位置便指向了该句柄对应的对象结构。若句柄对应的对象是一个进程对象那么0x18的位置存的就是对应进程对象的_EPROCESS的结构可以从这个结构便获得进程名、进程ID等等信息
有什么用
既然只要进程对象被打开那么打开者就会获得打开进程对象的句柄值并且放入私有句柄表进而以一定权限操作打开进程那当我们想知道我们的进程是否被打开或者有哪些进程打开了我们是否可以通过遍历所有进程的私有句柄表来判断我们进程是否被打开操作呢进而检测是否有非法内存操作的风险
逆向思路
第一我们需要的是私有句柄表那我们就需要找到对应的TableCode 第二TableCode存在ObjectTable中那我们就需要找到任意一个进程的ObjectTable随后通过该结构[HANDLE_TABLE]的HandleTableList 成员遍历得到所有进程的ObjectTable地址ObjectTable对象的HANDLE_TABLE结构如下:
nt!_HANDLE_TABLE0x000 TableCode : 0x968e8000 0x004 QuotaProcess : 0x864075e0 _EPROCESS0x008 UniqueProcessId : 0x00000a98 Void0x00c HandleLock : _EX_PUSH_LOCK// 所有进程私有句柄表的双向链表的某个节点0x010 HandleTableList : _LIST_ENTRY [ 0x83f50e68 - 0x8bec8960 ] 0x018 HandleContentionEvent : _EX_PUSH_LOCK0x01c DebugInfo : (null) 0x020 ExtraInfoPages : 0n0 0x024 Flags : 00x024 StrictFIFO : 0y00x028 FirstFreeHandle : 0xc40x02c LastFreeHandleEntry : 0x968e8ff8 _HANDLE_TABLE_ENTRY0x030 HandleCount : 0x300x034 NextHandleNeedingPool : 0x8000x038 HandleCountHighWatermark : 0x31第三要想获得任意一个进程ObjectTable的话我们就获取System进程的EPROCESS结构来获取System的ObjectTable地址
用代码来叙说细节
1 获取System进程的EPROCESS,驱动所属进程就是System
PEPROCESS pEprocess PsGetCurrentProcess();2 获取指定进程的私有句柄表地址ObjectTable
PULONG pHanldeForSystem *(PULONG)((PUCHAR)pEprocess 0xf4);3 获取进程ObjectTable的HandleTableList的地址这个成员是双向链表的某个节点所有正常进程的ObjectTable0x10的位置都在这个链表上
PLIST_ENTRY pPriListForSys (PLIST_ENTRY)((PUCHAR)pHanldeForSystem 0x10);4 遍历HandleTableList链表获得所有进程的ObjectTable并保存下来
PLIST_ENTRY pTmp pPriListForSys;
int cout 0;
do
{pTmp pTmp-Flink;cout;
} while (pTmp ! pPriListForSys);
pTmp pPriListForSys;
PULONG pHandleTable ExAllocatePool(NonPagedPool, cout*sizeof(PULONG));
RtlZeroMemory(pHandleTable, cout*sizeof(PULONG));
//保存ObjectTable
for (int i 0; i cout;i)
{pHandleTable[i] (PULONG)((PUCHAR)(pTmp-Flink) - 0x10);pTmp pTmp-Flink;
}5 获取所有进程的ObjectTable–TableCode并保存下来
PULONG pTableCode ExAllocatePool(NonPagedPool, cout*sizeof(PULONG));
RtlZeroMemory(pTableCode, cout*sizeof(PULONG));
for (int i 0; i cout;i)
{pTableCode[i] *(PULONG)pHandleTable[i];DbgPrintEx(77, 0, [db]tablecode地址为:%p\n, pTableCode[i]);
}6 筛选出只有一层的TableCode,因为超过两层的通常都是系统进程之类的恶意进程的通常只有一层
ULONG uTmpCode 0;
int count_2 0;
for (int i 0; i cout; i)
{uTmpCode pTableCode[i] 0x00000007;if (uTmpCode0){continue;}count_2;
}
count_2 count_2-1;
PULONG pOneTableCode ExAllocatePool(NonPagedPool, count_2*sizeof(PULONG));
RtlZeroMemory(pOneTableCode, count_2*sizeof(PULONG));
count_2 0;
for (int i 0; i cout; i)
{uTmpCode pTableCode[i] 0x00000007;if (uTmpCode0){continue;}pOneTableCode[count_2] pTableCode[i] 0xFFFFFFF8;//此时count_2是一层句柄TableCode的数量count_2;
}7 遍历出每个只有一层句柄表的表内句柄值,并打印出句柄值(上面忘记说了句柄值总共64位低32位才是我们目前说的句柄值)
类型值(我的系统上进程对象的类型是7你们可以通过取System的进程对应的类型值作比较只要类型值一样就是进程我为了方便直接就写了调试出来的0x7)进程名(前提是句柄对应的对象是进程)
count_2 count_2 - 1;
ULONG64 pTmpTableValue 0;
ULONG uHight32 0;
ULONG uLow32 0;
UCHAR uType 0;
PUCHAR pProcessName NULL;
for (int i 0; i count_2-2;i)
{DbgPrintEx(77, 0, [db]第%d个表内容,地址为:%p:\n, i, pOneTableCode[i]);for (int j 0; j 512;j){if (pOneTableCode[i]0){break;}pTmpTableValue *((PULONG64)((PUCHAR)pOneTableCode[i] j * 8));uHight32 pTmpTableValue 32;uLow32 pTmpTableValue 0x00000000ffffffff;uLow32 uLow32 0xFFFFFFF8;if (uLow32 ! 0){DbgPrintEx(77, 0, [db]第%d个句柄表的第%d个值为%lx%lx\n, i, j,uHight32, uLow32);uType *(PUCHAR)((PUCHAR)uLow32 0xc);DbgPrintEx(77, 0, [db]Type is %d\n, uType);if (uType 0x7){pProcessName (PUCHAR)((PUCHAR)uLow32 0x18 0x16c);DbgPrintEx(77, 0, [db][exeinfor]:Processname:%s\n, pProcessName);}}}
}8 判断是否被打开的话我们直接添加一个全局变量FLAG来计数我们进程对应的句柄在其他进程中出现的次数比如超过指定次便判断为风险或者也可以查看是那个进程打开的获取进程名我这里就只判断了打开次数接下来看一下完整代码,以判断123.exe是否被打开1次以上为风险来判断提示我用了两个exe去OpenProcess 123.exe去实验
#include ntifs.h
#include string.hINT openflag 0;VOID UnloadDriver(PDRIVER_OBJECT pDriver)
{}NTSTATUS DriverEntry(PDRIVER_OBJECT pDriver, PUNICODE_STRING pRegPath)
{//获取当前进程systemPEPROCESS pEprocess PsGetCurrentProcess();//获取指定进程的私有句柄表地址handle_tablePULONG pHanldeForSystem *(PULONG)((PUCHAR)pEprocess 0xf4);//获取handle_table中的Table_code PULONG pTableCodeForSystem *pHanldeForSystem;//获取所有进程私有句柄表的地址PLIST_ENTRY pPriListForSys (PLIST_ENTRY)((PUCHAR)pHanldeForSystem 0x10);//打印测试DbgPrintEx(77, 0, [db]地址:\npEprocess:%p\npHanldeForSystem:%p,\nTableCodeForSystem:%p\npPriListForSys:%p\n, pEprocess,pHanldeForSystem,pTableCodeForSystem,pPriListForSys);/*循环所有进程的私有句柄表定义一个tmp去遍历把所有私有handle_table的地址保存下来*/PLIST_ENTRY pTmp pPriListForSys;int cout 0;do {DbgPrintEx(77, 0, [db]第%d个handle_table list地址为:%p\n, cout, pTmp-Flink);pTmp pTmp-Flink;cout;} while (pTmp ! pPriListForSys);pTmp pPriListForSys;PULONG pHandleTable ExAllocatePool(NonPagedPool, cout*sizeof(PULONG));RtlZeroMemory(pHandleTable, cout*sizeof(PULONG));//保存handle_tablefor (int i 0; i cout;i){pHandleTable[i] (PULONG)((PUCHAR)(pTmp-Flink) - 0x10);DbgPrintEx(77, 0, [db]handle_table地址为:%p\n, pHandleTable[i]);pTmp pTmp-Flink;}//获取所有的tablecodePULONG pTableCode ExAllocatePool(NonPagedPool, cout*sizeof(PULONG));RtlZeroMemory(pTableCode, cout*sizeof(PULONG));for (int i 0; i cout;i){pTableCode[i] *(PULONG)pHandleTable[i];DbgPrintEx(77, 0, [db]tablecode地址为:%p\n, pTableCode[i]);}//筛选出只有一层的TableCodeULONG uTmpCode 0;int count_2 0;for (int i 0; i cout; i){uTmpCode pTableCode[i] 0x00000007;if (uTmpCode0){continue;}count_2;}count_2 count_2-1;PULONG pOneTableCode ExAllocatePool(NonPagedPool, count_2*sizeof(PULONG));RtlZeroMemory(pOneTableCode, count_2*sizeof(PULONG));count_2 0;for (int i 0; i cout; i){uTmpCode pTableCode[i] 0x00000007;if (uTmpCode0){continue;}pOneTableCode[count_2] pTableCode[i] 0xFFFFFFF8;DbgPrintEx(77, 0, [db]一层的tablecode:%p\n, pOneTableCode[count_2]);//此时count_2是一层句柄TableCode的数量count_2;}//遍历出每个一层句柄表内的值count_2 count_2 - 1;ULONG64 pTmpTableValue 0;ULONG uHight32 0;ULONG uLow32 0;UCHAR uType 0;PUCHAR pProcessName NULL;ULONG uFindNum 0;for (int i 0; i count_2-2;i){DbgPrintEx(77, 0, [db]第%d个表内容,地址为:%p:\n, i, pOneTableCode[i]);for (int j 0; j 512;j){if (pOneTableCode[i]0){break;}pTmpTableValue *((PULONG64)((PUCHAR)pOneTableCode[i] j * 8));uHight32 pTmpTableValue 32;uLow32 pTmpTableValue 0x00000000ffffffff;uLow32 uLow32 0xFFFFFFF8;if (uLow32 ! 0){DbgPrintEx(77, 0, [db]第%d个句柄表的第%d个值为%lx%lx\n, i, j,uHight32, uLow32);uType *(PUCHAR)((PUCHAR)uLow32 0xc);DbgPrintEx(77, 0, [db]Type is %d\n, uType);if (uType 0x7){pProcessName (PUCHAR)((PUCHAR)uLow32 0x18 0x16c);if (strcmp(pProcessName,123.exe)0){openflag;}DbgPrintEx(77, 0, [db][exeinfor]:Processname:%s\n, pProcessName);}}uFindNum;}}DbgPrintEx(77, 0, [db]总共翻越了%d坐山\n, uFindNum);if (openflag1){DbgPrintEx(77, 0, [db]很遗憾我们翻山越岭找到了它,它出现了%d次,有风险\n, openflag);}pDriver-DriverUnload UnloadDriver;return STATUS_SUCCESS;
}经过实验得到123.exe进程对象句柄被至少3个其他进程拥有其中两个是我自己写的exe另外一个应该是某个系统进程此时我们看到总过遍历了判断18944次内存区域打印了接近几分钟这也许就是翻山越岭的爱吧(主要是我打印费时了)