概述 在本文中,我们会探索句柄在windows内核中表现形式,帮助了解句柄在windows中的作用。通过调试与逆向内核代码的方式,介绍句柄是如何关联到内核对象,并在此基础上介绍一种利用句柄来保护我们的进程不被读写的技术方案。 句柄是什么?本节主要介绍句柄在R3的作用,如果你了解,可以跳过,不影响后面的阅读。 如果你经常使用windows的OpenProcess、CreateFile等API来操作进程或文件,那么你应该非常熟悉句柄的使用,这些函数的返回值类型都是HANDLE,即句柄。在《windows内核原理与实现》3.4.1节对句柄有如下描述: 当一个进程利用名称来创建或打开一个对象时,将获得一个句柄,该句柄指向所创建或打开的对象。以后,该进程无须使用名称来引用该对象,使用此句柄即可访问。这样做可以显著地提高引用对象的效率。句柄是一个在软件设计中被广泛使用的概念。例如,在C运行库中,文件操作使用句柄来表示,每当应用程序创建或打开一个文件时,只要此创建或打开操作成功,则C运行库返回一个句柄。以后应用程序对文件的读写操作都使用此句柄来标识该文件。而且,如果两个应用程序以共享方式打开了同一个文件,那么,它们将分别得到各自的句柄,且都可以通过句柄操作该文件。尽管两个应用程序得到的句柄的值并不相同,但是这两个句柄所指的文件却是同一个。因此,句柄只是一个对象引用,同一个对象在不同的环境下可能有不同的引用(句柄)值。
上文中的"对象"指的是内核对象,我们在R3中所使用的文件、进程、线程在内核中都有对应内核对象。应用层每次创建或打开进程、文件都会对相应的内核对象创建一个句柄。当多个进程同时打开一个文件时,该文件在内核中只会存在一个文件内核对象,但每个进程都有一个各自的文件句柄,每个句柄会增加内核对象的引用计数,只有当内核对象的引用计数为0时,内核对象才会释放。 方便在R3查看句柄信息的小工具我们可以使用微软官方的工具Process Explorer来查看文件或进程被哪个进程使用着,它的官方文档在这里。 比如我使用CE打开了notepad.exe,使用Process Explorer查找句柄功能,搜索notepad.exe,即可找到对应占用它的进程。这在我们想删除一个文件却提示"该文件已被占用"时非常有用,可以直接搜索到占用它的进程。
探索Windows内核系列——句柄,利用句柄进行进程保护
image-20220721235948764.png
句柄在内核中的存在形式本节将通过实时内核调试结合逆向查看内核源代码的方式,来探索内核中的句柄结构。句柄结构经历各个windows版本到现在,变化非常大,这里使用的系统是Win10 1809。 Windows系统的句柄表主要有三种,第一种是系统全局PspCidTable句柄表,保存所有进程和线程的句柄,第二种是进程内部句柄表,保存该进程所打开的内核对象句柄,最后一种是系统进程system的全局句柄表,这几种句柄表对应的格式都是一样的。我们主要介绍进程中的句柄表。 _EPROCESS是进程在内核中内核对象结构,每个进程在内核中都有一个这样对应的内核结构来记录进程的信息。EPOCESS结构中的ObjectTable即该进程所有的句柄表,ObjectTable中的TableCode指向handle_table_entrys的首地址。 dt _EPROCESS
nt!_EPROCESS
+0x000 Pcb : _KPROCESS
+0x2d8 ProcessLock : _EX_PUSH_LOCK
+0x2e0 UniqueProcessId : Ptr64 Void
+0x2e8 ActiveProcessLinks : _LIST_ENTRY
+0x2f8 RundownProtect : _EX_RUNDOWN_REF
+0x300 Flags2 : Uint4B
......
+0x418 ObjectTable : Ptr64 _HANDLE_TABLE //我们要找的句柄表
......
+0x848 MmHotPatchContext : Ptr64 Void
0: kd> dt _HANDLE_TABLE
nt!_HANDLE_TABLE
+0x000 NextHandleNeedingPool : Uint4B
+0x004 ExtraInfoPages : Int4B
+0x008 TableCode : Uint8B //指向存放handle_table_entry的地方
+0x010 QuotaProcess : Ptr64 _EPROCESS
+0x018 HandleTableList : _LIST_ENTRY
+0x028 UniqueProcessId : Uint4B
+0x02c Flags : Uint4B
+0x02c StrictFIFO : Pos 0, 1 Bit
+0x02c EnableHandleExceptions : Pos 1, 1 Bit
+0x02c Rundown : Pos 2, 1 Bit
+0x02c Duplicated : Pos 3, 1 Bit
+0x02c RaiseUMExceptionOnInvalidHandleClose : Pos 4, 1 Bit
+0x030 HandleContentionEvent : _EX_PUSH_LOCK
+0x038 HandleTableLock : _EX_PUSH_LOCK
+0x040 FreeLists : [1] _HANDLE_TABLE_FREE_LIST
+0x040 ActualEntry : [32] UChar
+0x060 DebugInfo : Ptr64 _HANDLE_TRACE_DEBUG_INFO以一个简单的图形先概述三者的结构如下:
探索Windows内核系列——句柄,利用句柄进行进程保护
image-20220723165617484.png
EPROCESS -> HANDLE_TABLE -> handle_table_entryTableCode事实上比上图描述的要复杂一点点,句柄表是一个多层结构,TableCode的低两位代表了句柄表的层数。若低两位为0,则TableCode直接指向一个存放handle_table_entrys的表,若低两位为1则TableCode指向一个中间的句柄表,该表里存放的也是"TableCode";同理若TableCode低两位为3,则往后还有两层TableCode表。
探索Windows内核系列——句柄,利用句柄进行进程保护
image-20220723170108513.png
TableCode结构——截图自《windows内核原理与实现》如果还不是很明白的话,我们实际操作找一下。首先找到notepad.exe的EPROCESS 0: kd> !process 0 0 notepad.exe
PROCESS ffffb38116ac9080
SessionId: 1 Cid: 1d24 Peb: e96dff2000 ParentCid: 10f0
DirBase: 94319000 ObjectTable: ffffa08ca139b780 HandleCount: 266.
Image: notepad.exe从EPROCESS中找到ObjectTable,即句柄表 _HANDLE_TABLE 0: kd> dt _EPROCESS ffffb38116ac9080 -y object
nt!_EPROCESS
+0x418 ObjectTable : 0xffffa08c`a139b780 _HANDLE_TABLE查看句柄表的TableCode 0: kd> dt _HANDLE_TABLE 0xffffa08c`a139b780
nt!_HANDLE_TABLE
+0x000 NextHandleNeedingPool : 0x800
+0x004 ExtraInfoPages : 0n0
+0x008 TableCode : 0xffffa08c`9ec4f001
+0x010 QuotaProcess : 0xffffb381`16ac9080 _EPROCESS
+0x018 HandleTableList : _LIST_ENTRY [ 0xffffa08c`a5a4b598 - 0xffffa08c`a3a718d8 ]
+0x028 UniqueProcessId : 0x1d24
+0x02c Flags : 0
+0x02c StrictFIFO : 0y0
+0x02c EnableHandleExceptions : 0y0
+0x02c Rundown : 0y0
+0x02c Duplicated : 0y0
+0x02c RaiseUMExceptionOnInvalidHandleClose : 0y0
+0x030 HandleContentionEvent : _EX_PUSH_LOCK
+0x038 HandleTableLock : _EX_PUSH_LOCK
+0x040 FreeLists : [1] _HANDLE_TABLE_FREE_LIST
+0x040 ActualEntry : [32] ""
+0x060 DebugInfo : (null) 这里TableCode为0xffffa08c`9ec4f001,最低为1,则表示后续还有一层TableCode表。去除TableCode的低两位,查看一下内容,表里只有两项TableCode,且它们的低两位均为0,表示它们指向的就是handle_table_entrys的基址。 0: kd> dq 0xffffa08c`9ec4f000
ffffa08c`9ec4f000 ffffa08c`9e0e3000 ffffa08c`9e5fd000
ffffa08c`9ec4f010 00000000`00000000 00000000`00000000
ffffa08c`9ec4f020 00000000`00000000 00000000`00000000任意查看其中一个表,其中第0项为Reserved,无意义(或许有其他解释,请指正)。 0: kd> dq ffffa08c`9e0e3000
ffffa08c`9e0e3000 00000000`00000000 00000000`00000000 ;handle_table_entry 0
ffffa08c`9e0e3010 b381188a`63b0ffff 00000000`001f0003 ;handle_table_entry 1
ffffa08c`9e0e3020 b381188b`9b30fffb 00000000`001f0003 ;handle_table_entry 2
ffffa08c`9e0e3030 b38114d5`4860fffd 00000000`00000001
ffffa08c`9e0e3040 b3811b00`a2d0ffd1 00000000`001f0003
ffffa08c`9e0e3050 b381144d`e290ff79 00000000`000f00ff
ffffa08c`9e0e3060 b3811da0`2b60ffff 00000000`00100002
ffffa08c`9e0e3070 b38114d5`3340ffff 00000000`00000001我们查看handle_table_entry的结构,它并不是那么好理解,与《windows内核原理与实现》中描述的有很大差别。 0: kd> dt _handle_table_entry
nt!_HANDLE_TABLE_ENTRY
+0x000 VolatileLowValue : Int8B
+0x000 LowValue : Int8B
+0x000 InfoTable : Ptr64 _HANDLE_TABLE_ENTRY_INFO
+0x008 HighValue : Int8B
+0x008 NextFreeHandleEntry : Ptr64 _HANDLE_TABLE_ENTRY
+0x008 LeafHandleValue : _EXHANDLE
+0x000 RefCountField : Int8B
+0x000 Unlocked : Pos 0, 1 Bit
+0x000 RefCnt : Pos 1, 16 Bits
+0x000 Attributes : Pos 17, 3 Bits
+0x000 ObjectPointerBits : Pos 20, 44 Bits
+0x008 GrantedAccessBits : Pos 0, 25 Bits
+0x008 NoRightsUpgrade : Pos 25, 1 Bit
+0x008 Spare1 : Pos 26, 6 Bits
+0x00c Spare2 : Uint4B从进程的handle找到对应的handle_table_entry通过上文,我们知道了EPROCESS与handle_table_entry的关系,那么句柄的作用是什么呢?如何通过句柄找到对应的handle_table_entry呢。这里有一个函数ExpLookupHandleTableEntry,它刚好是通过句柄从handle_table中找对应的handle_table_entry。看伪代码: unsigned __int64 __fastcall ExpLookupHandleTableEntry(_HANDLE_TABLE *handle_table, __int64 handle)
{
unsigned __int64 my_handle; // rdx
volatile unsigned __int64 TableCode; // r8
__int64 v4; // rax
my_handle = handle & 0xFFFFFFFFFFFFFFFCui64; // 从这里句柄的使用,我们可以得出句柄是以4为增长单位的结论
if ( my_handle >= handle_table->NextHandleNeedingPool )
return 0i64;
TableCode = handle_table->TableCode;
if ( (TableCode & 3) == 1 ) //TableCode低两位为1,代表下一层是TableCode表
{
v4 = *(_QWORD *)(TableCode + 8 * (my_handle >> 10) - 1);
return v4 + 4 * (my_handle & 0x3FF);
}
if ( (TableCode & 3) != 0 ) //TableCode低两位为2,代表下两层都是TableCode表
{
v4 = *(_QWORD *)(*(_QWORD *)(TableCode + 8 * (my_handle >> 19) - 2) + 8 * ((my_handle >> 10) & 0x1FF));
return v4 + 4 * (my_handle & 0x3FF);
}
return TableCode + 4 * my_handle; //TableCode低两位为0
}通过伪代码中的注释描述,我们知道了句柄实际上是下标,在最简单的情况下,使用时"handle×4+TableCode"即为对应的handle_table_entry所在的位置。到这里,我们清楚的知道了句柄的值在进程中的作用,使用它我们可以找到handle_table_entry,也就能找到对应的内核对象。 return TableCode + 4 * my_handle; //TableCode低两位为0从handle_table_entry找到句柄对应的内核对象通过上一节,我们知道了如何从进程的Handle找到进程内核中的handle_table_entry,本节我们将介绍如何从handle_table_entry找到对应的内核对象,完成从进程的handle找到对应内核对象的过程。 先以一个简单的图概述一下:
探索Windows内核系列——句柄,利用句柄进行进程保护
image-20220723173358452.png
handle_table_entry -> object在Win7中,handle_table_entry结构中的object直接指向对应的内核对象地址,但在win10并没有这么简单。接着前文的调试内容继续查看ffffa08c`9e0e3010地址处handle_table_entry结构的具体内容。通常我们会去google这些新结构的含义,但这些都是未公开的,假设在google找不到的情况下,我们如何去做呢。 0: kd> dt _handle_table_entry ffffa08c`9e0e3010
nt!_HANDLE_TABLE_ENTRY
+0x000 VolatileLowValue : 0n-5512097486267678721
+0x000 LowValue : 0n-5512097486267678721
+0x000 InfoTable : 0xb381188a`63b0ffff _HANDLE_TABLE_ENTRY_INFO
+0x008 HighValue : 0n2031619
+0x008 NextFreeHandleEntry : 0x00000000`001f0003 _HANDLE_TABLE_ENTRY
+0x008 LeafHandleValue : _EXHANDLE
+0x000 RefCountField : 0n-5512097486267678721
+0x000 Unlocked : 0y1
+0x000 RefCnt : 0y0111111111111111 (0x7fff)
+0x000 Attributes : 0y000
+0x000 ObjectPointerBits : 0y10110011100000010001100010001010011000111011 (0xb381188a63b)
+0x008 GrantedAccessBits : 0y0000111110000000000000011 (0x1f0003)
+0x008 NoRightsUpgrade : 0y0
+0x008 Spare1 : 0y000000 (0)
+0x00c Spare2 : 0这个时候我们会去IDA查看内核中相关的实现,思路是找到handle与object相关联的函数。如ObFindHandleForObject,ObReferenceObjectByHandle
探索Windows内核系列——句柄,利用句柄进行进程保护
image-20220723174439084.png
探索Windows内核系列——句柄,利用句柄进行进程保护
image-20220723174604167.png这里我选择ObFindHandleForObject来介绍,因为它流程相对简单一些。函数声明来自官方。
BOOLEAN WINAPI ObFindHandleForObject(
_In_ PEPROCESS Process,
_In_ PVOID Object,
_In_opt_ PVOID Reserved1,
_In_opt_ PVOID Reserved2,
_Out_ PHANDLE Handle
);这帮助了我们从IDA中查看它的伪代码。 bool __fastcall ObFindHandleForObject(
struct _EX_RUNDOWN_REF *eprocess,
__int64 object,
__int64 a3,
__int64 a4,
__int64 *handle)
{
bool v9; // bl
__int64 handle_table; // rcx
__int64 object_header[5]; // [rsp+20h] [rbp-28h] BYREF
v9 = 0;
handle_table = ObReferenceProcessHandleTable(eprocess);
if ( handle_table )
{
if ( object )
object_header[0] = object - 0x30; // 我使用的win10版本中,object_header的大小为0x30,因此这里用object-0x30是获取了object_header
else
object_header[0] = 0i64;
object_header[1] = a3;
object_header[2] = a4;
v9 = (unsigned __int8)ExEnumHandleTable(
handle_table,
(__int64 (__fastcall *)(__int64, signed __int64 *, __int64, __int64))ObpEnumFindHandleProcedure,
(__int64)object_header,// EnumParameter
handle) != 0;
ExReleaseRundownProtection_0(eprocess + 95);
}
return v9;
}
_HANDLE_TABLE *__fastcall ObReferenceProcessHandleTable(_EPROCESS *a1)
{
struct _EX_RUNDOWN_REF *p_RundownProtect; // rdi
_HANDLE_TABLE *ObjectTable; // rbx
p_RundownProtect = &a1->RundownProtect;
ObjectTable = 0i64;
if ( ExAcquireRundownProtection_0(&a1->RundownProtect) )
{
ObjectTable = a1->ObjectTable;
if ( !ObjectTable )
ExReleaseRundownProtection_0(p_RundownProtect);
}
return ObjectTable;
}该函数首先使用ObReferenceProcessHandleTable从EPROCESS的中获取HANDLE_TABLE,然后使用ExEnumHandleTable遍历该HANDLE_TABLE中的handle_table_entry。关于ExEnumHandleTable这个未公开的函数,在WRK中有如下声明: BOOLEAN
ExEnumHandleTable(
IN PHANDLE_TABLE HandleTable,
IN EX_ENUMERATE_HANDLE_ROUTINE EnumHandleProcedure,
IN PVOID EnumParameter,
OUT PHANDLE Handle OPTIONAL
);结合伪代码,我们可以看出在ObFindHandleForObject函数中ExEnumHandleTable函数的EnumParameter参数为object_header;回调函数为ObpEnumFindHandleProcedure。接下来我们继续查看ObpEnumFindHandleProcedure函数中是如何使用object_header的。 在WRK中,ObpEnumFindHandleProcedure的声明如下: BOOLEAN
ObpEnumFindHandleProcedure (
PHANDLE_TABLE_ENTRY ObjectTableEntry,
HANDLE HandleId,
PVOID EnumParameter
)ObpEnumFindHandleProcedure的伪代码: __int64 __fastcall ObpEnumFindHandleProcedure(
__int64 handle_table,
__int64 handle_table_entry,
__int64 a3,
_QWORD *object_header)
{
unsigned __int8 v5; // bl
__int64 v7; // rbx
_DWORD *v8; // rcx
__int64 v9; // r11
int v10[10]; // [rsp+0h] [rbp-28h] BYREF
if ( !*object_header || *object_header == ((*(__int64 *)handle_table_entry >> 0x10) & 0xFFFFFFFFFFFFFFF0ui64) )
{
v7 = object_header[1];
if ( !v7
|| v7 == ObTypeIndexTable[(unsigned __int8)ObHeaderCookie ^ *(unsigned __int8 *)(((*(__int64 *)handle_table_entry >> 0x10) & 0xFFFFFFFFFFFFFFF0ui64)
+ 0x18) ^ (unsigned __int64)(unsigned __int8)((unsigned __int16)(WORD1(*(_QWORD *)handle_table_entry) & 0xFFF0) >> 8)] )
{
v8 = (_DWORD *)object_header[2];
if ( !v8 )
goto LABEL_11;
v9 = (*(__int64 *)handle_table_entry >> 17) & 7;
if ( (*(_DWORD *)(handle_table_entry + 8) & 0x2000000) != 0 )
LOBYTE(v9) = v9 | 8;
if ( *v8 == (v9 & 7) && v8[1] == (*(_DWORD *)(handle_table_entry + 8) & 0x1FFFFFF) )
LABEL_11:
v5 = 1;
else
v5 = 0;
}
else
{
v5 = 0;
}
}
else
{
v5 = 0;
}
_InterlockedExchangeAdd64((volatile signed __int64 *)handle_table_entry, 1ui64);
_InterlockedOr(v10, 0);
if ( *(_QWORD *)(handle_table + 48) )
ExfUnblockPushLock(handle_table + 48, 0i64);
return v5;
}对比伪代码与WRK中的声明发现,似乎多了一个a3参数,但是a3在整个代码中也没有被使用,暂且认为为保留值,我们不关心它。重点在于我们发现这样一个判断,找到了从handle_table_entry找到object_header的计算公式。 *object_header == ((*(__int64 *)handle_table_entry >> 0x10) & 0xFFFFFFFFFFFFFFF0ui64)
汇编:
PAGE:000000014065FA82 sar r8, 10h ;算数右移
PAGE:000000014065FA86 and r8, 0FFFFFFFFFFFFFFF0h ;得到object_header地址我们尝试去计算一下,注意这里的>>0x10是sar 算数右移,保持符号位。 0: kd> dq ffffa08c`9e0e3010 l1
ffffa08c`9e0e3010 b381188a`63b0ffff
// 针对 b381188a`63b0ffff 应用上述公式计算得到 ffffb381`188a63b0
dt _object_header ffffb381`188a63b0
nt!_OBJECT_HEADER
+0x000 PointerCount : 0n32768
+0x008 HandleCount : 0n1
+0x008 NextToFree : 0x00000000`00000001 Void
+0x010 Lock : _EX_PUSH_LOCK
+0x018 TypeIndex : 0x61 'a'
+0x019 TraceFlags : 0 ''
+0x019 DbgRefTrace : 0y0
+0x019 DbgTracePermanent : 0y0
+0x01a InfoMask : 0x8 ''
+0x01b Flags : 0 ''
+0x01b NewObject : 0y0
+0x01b KernelObject : 0y0
+0x01b KernelOnlyAccess : 0y0
+0x01b ExclusiveObject : 0y0
+0x01b PermanentObject : 0y0
+0x01b DefaultSecurityQuota : 0y0
+0x01b SingleHandleEntry : 0y0
+0x01b DeletedInline : 0y0
+0x01c Reserved : 0
+0x020 ObjectCreateInfo : 0xffffb381`16be4cc0 _OBJECT_CREATE_INFORMATION
+0x020 QuotaBlockCharged : 0xffffb381`16be4cc0 Void
+0x028 SecurityDescriptor : (null)
+0x030 Body : _QUAD现在,我们可以做到从handle_table_entry找到对应的内核对象了,其实也就是可以做到从handle找到对应的内核对象了。 这里还可以找到计算typeindex的公式,对比win7,它也复杂的多了,我们这里不多继续介绍。 ObTypeIndexTable[(unsigned __int8)ObHeaderCookie ^ *(unsigned __int8 *)(((*(__int64 *)handle_table_entry >> 0x10) & 0xFFFFFFFFFFFFFFF0ui64)
+ 0x18) ^ (unsigned __int64)(unsigned __int8)((unsigned __int16)(WORD1(*(_QWORD *)handle_table_entry) & 0xFFF0) >> 8)]另外,如果只是单纯的想查看进程中的所有句柄信息,在windbg中只需要输入!handle就能看到了,这里不做演示。 利用句柄来保护进程不被读写本节介绍一种利用句柄降权来保护进程不被读写的技术。 我们知道在R3中,一个进程打开另一个进程进行读写,其过程为:A进程获取B进程句柄 -> A进程操作B进程句柄进行读/写。很明显,句柄是这里的关键介质。在阅读完上文后,我们知道了进程中的句柄对进程意味着什么。回顾handle_table_entry的结构,其中的GrantedAccessBits表示这个该进程可以通过该句柄所使用的权限。这意味着,如果我们修改A进程中的B进程句柄对应的handle_table_entry->GrantedAccessBits,便可以修改A进程对B进程的操作权限。 0: kd> dt _handle_table_entry
nt!_HANDLE_TABLE_ENTRY
+0x000 VolatileLowValue : Int8B
+0x000 LowValue : Int8B
+0x000 InfoTable : Ptr64 _HANDLE_TABLE_ENTRY_INFO
+0x008 HighValue : Int8B
+0x008 NextFreeHandleEntry : Ptr64 _HANDLE_TABLE_ENTRY
+0x008 LeafHandleValue : _EXHANDLE
+0x000 RefCountField : Int8B
+0x000 Unlocked : Pos 0, 1 Bit
+0x000 RefCnt : Pos 1, 16 Bits
+0x000 Attributes : Pos 17, 3 Bits
+0x000 ObjectPointerBits : Pos 20, 44 Bits
+0x008 GrantedAccessBits : Pos 0, 25 Bits // 该句柄的操作权限
+0x008 NoRightsUpgrade : Pos 25, 1 Bit
+0x008 Spare1 : Pos 26, 6 Bits
+0x00c Spare2 : Uint4B思路清晰,就开始编码。我们这里使用CE打开notepad.exe,正常情况下,CE是可以修改notepad.exe进程的内存的。
探索Windows内核系列——句柄,利用句柄进行进程保护
image-20220723211843514.png首先我们找到NOTEPAD.EXE进程的内核对象,这里我们直接使用PID获取
NTSTATUS DriverEntry(PDRIVER_OBJECT pDriver, PUNICODE_STRING pReg)
{
PEPROCESS eprocess = NULL;
NTSTATUS status = PsLookupProcessByProcessId((HANDLE)0x1D24, &eprocess);
if (!NT_SUCCESS(status))
{
DbgPrintEx(DPFLTR_IHVDRIVER_ID, 0, "Open process unsuccessfully!");
return ;
}
ObDereferenceObject(eprocess);
ProtectProcessByEprocess(eprocess);
pDriver->DriverUnload = DriverUnload;
return STATUS_SUCCESS;
}虽然我们已经会手工查找,但是还记得ObFindHandleForObject函数中如何遍历handle_table中的hanlde_table_entry吗,使用未公开的函数ExEnumHandleTable。 BOOLEAN
ExEnumHandleTable(
IN PHANDLE_TABLE HandleTable,
IN EX_ENUMERATE_HANDLE_ROUTINE EnumHandleProcedure,
IN PVOID EnumParameter,
OUT PHANDLE Handle OPTIONAL
);这里我们使用的回调函数为enumRoutine,参数为protected_eprocess。在enumRoutine中,我们通过handle_table_entry找到对应的object_header,再找到对应的object。如果我们找到了notepad.exe进程对应的句柄,那么该object也就是notepad.exe的EPROCESS。这时,我们只需要将该handle_table_entry中的GrantedAccess权限修改即可达到进程保护的效果。 BOOLEAN NTAPI enumRoutine(
IN PVOID HandleTable,
IN PHANDLE_TABLE_ENTRY HandleTableEntry,
PVOID Unknow,
IN PVOID eprocess
)
{
BOOLEAN result = FALSE;
if (HandleTableEntry)
{
ULONG_PTR object_header = (*(PLONG_PTR)(HandleTableEntry) >> 0x10) & 0xFFFFFFFFFFFFFFF0;
ULONG_PTR object = object_header + 0x30;
// 若该对象类型为进程,则该对象为该进程的EPROCESS
if (object == (ULONG_PTR)eprocess)
{
//DbgBreakPoint();
DbgPrintEx(DPFLTR_IHVDRIVER_ID, 0, "[db]:HandleTableEntry= %llx ", HandleTableEntry);
HandleTableEntry->GrantedAccess &= ~(PROCESS_VM_READ | PROCESS_VM_WRITE);
result = TRUE;
}
}
_InterlockedExchangeAdd64(HandleTableEntry, 1); // 注意 这里必须释放,参考ObpEnumFindHandleProcedure的实现
if (*(PULONG_PTR)((ULONG_PTR)HandleTable + 0x30)) {
ExfUnblockPushLock((ULONG_PTR)HandleTable + 0x30, 0); // 同上
}
return result;
}
VOID ProtectProcessByEprocess(PEPROCESS protected_eprocess)
{
PEPROCESS eprocess = NULL;
// 这里作为测试,直接写死CE的pid为0x464
NTSTATUS status = PsLookupProcessByProcessId((HANDLE)0x464, &eprocess);
if (!NT_SUCCESS(status))
{
KdPrint(("Open process unsuccessfully!"));
return;
}
// 在我使用的windows版本中 handle_table的偏移为0x418
PVOID handle_table = *(PVOID*)((LONG_PTR)eprocess + 0x418);
PVOID Handle = NULL;
ExEnumHandleTable(handle_table, enumRoutine, protected_eprocess, Handle);
ObDereferenceObject(eprocess);
}执行后,notepad.exe的内存在CE中已经不可见了。完整的项目代码已上传至 git仓库
探索Windows内核系列——句柄,利用句柄进行进程保护
image-20220723220949204.png
参考《windows内核原理与实现》 |