爱情故事(1):我要找到你,哪怕南北东西-私有句柄表
概述:通过暴力枚举私有句柄表判断指定进程是否被打开
实验环境:Windows 7 x86
实验工具:windbg、vs2013
tip:保持良好的心态阅读,毕竟对抗有攻击就有防守,这里只是阐述自己的一些思路与想法
0x1:什么是私有句柄表?
.私有句柄表是操作系统内部的一种数据结构,用于存储一个进程所拥有的句柄(或称为句柄对象)的信息。在操作系统中,句柄是一个标识符,用于唯一标识一个对象,例如文件、套接字、管道等等。GPT是这样说的,那我就举个例子让大家更简单的理解:比如我们使用OpenProcess
API成功打开一个进程时,我们便会得到一个返回值,这个返回值就叫作句柄,我们可以通过这个句柄来间接操作我们OpenProcess打开的那个进程,句柄值会被放入我们打开者的私有句柄表里,我们用到的时候就会去私有句柄表找出来用,当我们不再使用的时候,比如调用CloseHandle
便可以把刚才那个句柄值从私有句柄表中移除,随之我们也就无法以常规的方式去操作刚才打开的那个进程。
0x2:从内核中看私有句柄表
.私有句柄表所在的位置如下:
EPROCESS--->ObjectTable[_HANDLE_TABLE]--->TableCode&0xFFFFFFF8--->第一层句柄表或者句柄表指针表的地址
核心结构体如下,较大的结构体只截取到用到的成员位置,其他成员感兴趣请自己上网冲浪
nt!_EPROCESS
+0x000 Pcb : _KPROCESS
+0x098 ProcessLock : _EX_PUSH_LOCK
+0x0a0 CreateTime : _LARGE_INTEGER
+0x0a8 ExitTime : _LARGE_INTEGER
+0x0b0 RundownProtect : _EX_RUNDOWN_REF
+0x0b4 UniqueProcessId : Ptr32 Void
+0x0b8 ActiveProcessLinks : _LIST_ENTRY
+0x0c0 ProcessQuotaUsage : [2] Uint4B
+0x0c8 ProcessQuotaPeak : [2] Uint4B
+0x0d0 CommitCharge : Uint4B
+0x0d4 QuotaBlock : Ptr32 _EPROCESS_QUOTA_BLOCK
+0x0d8 CpuQuotaBlock : Ptr32 _PS_CPU_QUOTA_BLOCK
+0x0dc PeakVirtualSize : Uint4B
+0x0e0 VirtualSize : Uint4B
+0x0e4 SessionProcessLinks : _LIST_ENTRY
+0x0ec DebugPort : Ptr32 Void
+0x0f0 ExceptionPortData : Ptr32 Void
+0x0f0 ExceptionPortValue : Uint4B
+0x0f0 ExceptionPortState : Pos 0, 3 Bits
+0x0f4 ObjectTable : Ptr32 _HANDLE_TABLE //进程对应私有句柄表有关结构地址
nt!_HANDLE_TABLE
+0x000 TableCode : Uint4B
+0x004 QuotaProcess : Ptr32 _EPROCESS
+0x008 UniqueProcessId : Ptr32 Void
+0x00c HandleLock : _EX_PUSH_LOCK
+0x010 HandleTableList : _LIST_ENTRY
+0x018 HandleContentionEvent : _EX_PUSH_LOCK
+0x01c DebugInfo : Ptr32 _HANDLE_TRACE_DEBUG_INFO
+0x020 ExtraInfoPages : Int4B
+0x024 Flags : Uint4B
+0x024 StrictFIFO : Pos 0, 1 Bit
+0x028 FirstFreeHandle : Uint4B
+0x02c LastFreeHandleEntry : Ptr32 _HANDLE_TABLE_ENTRY
+0x030 HandleCount : Uint4B
+0x034 NextHandleNeedingPool : Uint4B
+0x038 HandleCountHighWatermark : Uint4B
.TableCode的值的最后3bit位代表着这个私有句柄表有几层,如果最后3bit位为0则代表仅有一层,1的话为两层,2的话最多为三层,以此类推,每张句柄表存有4096/8个句柄值,但是我们日常使用的电脑中最多有两层就很够多了,原因如下:
当只有一层的时候那么TableCode直接指向了私有句柄表的首个值,每个值占8字节,一个表的内存大小为4096个字节,一个句柄值为8字节,所以此时能存下的最大句柄值个数为4096/8个;当为两层的时候,TableCode指向了一张句柄表指针的表,TableCode+4*(层数-1)存的就是对应句柄表的指针,这张表依旧是4096个字节,那也就是说总共有4096/4个句柄表指针,那么一个句柄表有4096/8个句柄值,那此时就有(4096/4)x(4096/8)个句柄值了,一次类推,有点类似呈现了短期的指数大爆炸。那通常情况下我们的常规进程是不会打开这么多对象的,很多常规进程基本上就只有1层句柄表。
.一直说句柄,那句柄和对象的关系是啥呢?
nt!_OBJECT_HEADER
+0x000 PointerCount : Int4B
+0x004 HandleCount : Int4B
+0x004 NextToFree : Ptr32 Void
+0x008 Lock : _EX_PUSH_LOCK
+0x00c TypeIndex : UChar //对象类型
+0x00d TraceFlags : UChar
+0x00e InfoMask : UChar
+0x00f Flags : UChar
+0x010 ObjectCreateInfo : Ptr32 _OBJECT_CREATE_INFORMATION
+0x010 QuotaBlockCharged : Ptr32 Void
+0x014 SecurityDescriptor : Ptr32 Void
+0x018 Body : _QUAD //对象地址
其中,0xc的位置代表了该句柄对应的对象是一个什么类型的对象,比如文件、进程、线程等,0x18的位置便指向了该句柄对应的对象结构。若句柄对应的对象是一个进程对象那么0x18的位置存的就是对应进程对象的_EPROCESS
的结构,可以从这个结构便获得进程名、进程ID等等信息。
0x3:我想干嘛?
.刚才说了,既然只要进程对象被打开,那么打开者就会获得打开进程对象的句柄值,并且放入私有句柄表,进而以一定权限操作打开进程,那当我们想知道我们的进程是否被打开或者有哪些进程打开了,我们是否可以通过遍历所有进程的私有句柄表来判断我们进程是否被打开操作呢?进而检测是否有非法内存操作的风险。
0x4:逆向推导看大需求(思路)
.第一:我们需要的是私有句柄表,那我们就需要找到对应的TableCode;
.第二:TableCode存在ObjectTable中,那我们就需要找到任意一个进程的ObjectTable,随后通过该结构[HANDLE_TABLE]的HandleTableList
成员遍历得到所有进程的ObjectTable地址,ObjectTable对象的HANDLE_TABLE结构如下:
nt!_HANDLE_TABLE
+0x000 TableCode : 0x968e8000
+0x004 QuotaProcess : 0x864075e0 _EPROCESS
+0x008 UniqueProcessId : 0x00000a98 Void
+0x00c HandleLock : _EX_PUSH_LOCK
+0x010 HandleTableList : _LIST_ENTRY [ 0x83f50e68 - 0x8bec8960 ] //所有进程私有句柄表的双向链表的某个节点
+0x018 HandleContentionEvent : _EX_PUSH_LOCK
+0x01c DebugInfo : (null)
+0x020 ExtraInfoPages : 0n0
+0x024 Flags : 0
+0x024 StrictFIFO : 0y0
+0x028 FirstFreeHandle : 0xc4
+0x02c LastFreeHandleEntry : 0x968e8ff8 _HANDLE_TABLE_ENTRY
+0x030 HandleCount : 0x30
+0x034 NextHandleNeedingPool : 0x800
+0x038 HandleCountHighWatermark : 0x31
.第三:要想获得任意一个进程ObjectTable的话我们就获取System进程的EPROCESS结构来获取System的ObjectTable地址;
0x5:用代码来叙说细节
.获取System进程的EPROCESS,驱动所属进程就是System
PEPROCESS pEprocess = PsGetCurrentProcess();
.获取指定进程的私有句柄表地址ObjectTable
PULONG pHanldeForSystem = *(PULONG)((PUCHAR)pEprocess + 0xf4);
.获取进程ObjectTable的HandleTableList的地址,这个成员是双向链表的某个节点,所有正常进程的ObjectTable+0x10的位置都在这个链表上
PLIST_ENTRY pPriListForSys = (PLIST_ENTRY)((PUCHAR)pHanldeForSystem + 0x10);
.遍历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;
}
.获取所有进程的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]);
}
.筛选出只有一层的TableCode,因为超过两层的通常都是系统进程之类的,恶意进程的通常只有一层
ULONG uTmpCode = 0;
int count_2 = 0;
for (int i = 0; i < cout; i++)
{
uTmpCode =pTableCode[i] & 0x00000007;
if (uTmpCode>0)
{
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 (uTmpCode>0)
{
continue;
}
pOneTableCode[count_2] = pTableCode[i] & 0xFFFFFFF8;
//此时count_2是一层句柄TableCode的数量
count_2++;
}
.遍历出每个只有一层句柄表的表内句柄值,并打印出句柄值(上面忘记说了,句柄值总共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);
}
}
}
}
.判断是否被打开的话我们直接添加一个全局变量FLAG来计数我们进程对应的句柄在其他进程中出现的次数,比如超过指定次便判断为风险,或者也可以查看是那个进程打开的,获取进程名,我这里就只判断了打开次数,接下来看一下完整代码,以判断123.exe是否被打开1次以上为风险来判断提示,我用了两个exe去OpenProcess 123.exe去实验
#include <ntifs.h>
#include <string.h>
INT openflag = 0;
VOID UnloadDriver(PDRIVER_OBJECT pDriver)
{
}
NTSTATUS DriverEntry(PDRIVER_OBJECT pDriver, PUNICODE_STRING pRegPath)
{
//获取当前进程system
PEPROCESS pEprocess = PsGetCurrentProcess();
//获取指定进程的私有句柄表地址handle_table
PULONG 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_table
for (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;
}
//获取所有的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]);
}
//筛选出只有一层的TableCode
ULONG uTmpCode = 0;
int count_2 = 0;
for (int i = 0; i < cout; i++)
{
uTmpCode =pTableCode[i] & 0x00000007;
if (uTmpCode>0)
{
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 (uTmpCode>0)
{
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 (openflag>1)
{
DbgPrintEx(77, 0, "[db]很遗憾,我们翻山越岭找到了它,它出现了%d次,有风险\n", openflag);
}
pDriver->DriverUnload = UnloadDriver;
return STATUS_SUCCESS;
}
.结果
1
2
3
4
经过实验得到123.exe进程对象句柄被至少3个其他进程拥有,其中两个是我自己写的exe,另外一个应该是某个系统进程,此时我们看到总过遍历了判断18944次内存区域,打印了接近几分钟,这也许就是翻山越岭的爱吧!(主要是我打印费时了)
0x6:总结