进程强杀 因为ring0的特权级别是比ring3高的,那么我们肯定不能在ring3调用windows提供的api杀死ring0特权级别的进程,那么这时候我们就需要使用ring0的函数来强行结束一些处于ring0级别的进程。
ZwTerminateProcess 该函数是一个Ring0函数,结构参数如下:
1 2 3 4 NTSYSAPI NTSTATUS ZwTerminateProcess ( [in, optional] HANDLE ProcessHandle, [in] NTSTATUS ExitStatus ) ;
但是这个函数在RIng0层面下,已经被杀软hook掉了,所以如果调用这个函数Kill杀软会被拒绝。
PspTerminateProcess
这个函数是没有被导出的,要调用该函数有两种方法
暴力搜索,提取该函数的特征码,全盘搜索。
如果有已文档化的函数调用了PspTerminateProcess,那我们就可以通过指针加偏移的方式获取到他的地址,同样可以调用。
我们的驱动在被系统加载的同时,内存中会出现一个描述我们驱动信息的对象:DRIVER OBJECT
,而这个对象的地址,其实就保存在我们驱动的入口函数 Driver Entry
的第1个参数中。
在 DriverObject
对象中,有一个 Driver Section
成员,它所指向的是一个名叫 _LDR_DATA_TABLE_ENTRY
的结构体,该结构体每个驱动模块都有一份,在这个结构体中就保存着一个驱动模块的所有信息。
系统中有一个双向链表,其中每一个节点都保存着一个驱动模块的所有信息,而 InLoadOrderLinks
就是该链表中的节点类型,Flink
指向下一个驱动对象的 _LDR_DATA_TABLE_ENTRY
,Blink
指向上一个驱动对象的 _LDR_DATA_TABLE_ENTRY
。
因此,我们只要遍历这个 InLoadOrderLinks
,就能获取系统中所有驱动的模块信息。
接下来就是特征码的搜索匹配,在上图中可以看到,前一部分压栈操作不能作为特征码,所以可以选取中间部分,知道后再减去相应偏移。
遍历代码如下:
结束进程代码:
特征码查找:
上图中最后减去5是特征匹配字段与函数开头的距离(示例代码是在x86位下)
1.将程序编译成sys文件后安装驱动,即可关闭进程;
2.可以利用ring3常规方式传输数据到ring0的方式结束进程(其实就是多写一个客户端程序与驱动通信)
请注意第一个红框,这是老写法,查阅资料如下
总结 总结一下,这篇进程强杀,主要用到了PspTerminateProcess
函数,该函数并未导出,采取的是DRIVER_OBJECT
的Driver Section
成员,它所指向的是一个名叫 _LDR_DATA_TABLE_ENTRY
的结构体,该结构体大家都比较熟悉,通过遍历其中的 InLoadOrderLinks
加上匹配特征码的方式就能得到函数地址。
Ring0下的进程保护 SSDT表 主要用到SSDT HOOK,SSDT表并不仅仅只包含一个庞大的地址索引表,它还包含着一些其它有用的信息,诸如地址索引的基地址、服务函数个数等。通过修改此表的函数地址可以对常用 Windows 函数及 API 进行 Hook,从而实现对一些关心的系统动作进行过滤、监控的目的。一些 HIPS、防毒软件、系统监控、注册表监控软件往往会采用此接口来实现自己的监控模块。
因为x64位中ssdt表是加密的,ssdt中的每一项占4个字节但并不是对应的系统服务的地址,因为x64中地址为64位,而ssdt每一项只有4个字节所以无法直接存放服务的地址。其实际存储的4个字节的前28位表示的是对应的系统服务相对于SSDT表基地址的偏移,而后4位如果对应的服务的参数个数小于4则其值为0,不小于4则为参数个数减去4。
所以我们在ssdt hook时向ssdt表项中填入的函数得在ntoskrnl.exe模块中,原因是因为函数到SSDT表基地址的偏移大小小于4个字节。所以我们需要选取一个ntoskrnl.exe中很少使用的函数KeBugCheckEx
作为中转函数,将需要hook的ssdt项的改为KeBugCheckEx
函数,然后在Inlinehook KeBugCheck 函数,jmp到我们的函数中进行过滤。
KeServiceDescriptorTable
和 KeServiceDescriptorTableShadow
,其中 KeServiceDescriptorTable
主要是处理来自 Ring3 层 Kernel32.dll
中的系统调用,而 KeServiceDescriptorTableShadow
则主要处理来自 User32.dll
和 GDI32.dll
中的系统调用,并且KeServiceDescriptorTable
在ntoskrnl.exe
(Windows 操作系统内核文件,包括内核和执行体层)是导出的,而 KeServiceDescriptorTableShadow
则是没有被 Windows 操作系统所导出。
通过SSDT表中OpenProcess
函数的索引(系统调用号)来找到该函数。或者利用Terminateprocess
函数也是可以的。
CR0寄存器 修改内存页属性,SSDT表所在的内存页属性是只读,没有写入的权限,所以需要把该地址设置为可写入,这样才能写入自己的函数,使用的是CR0寄存器关闭只读属性。
获取SSDT表基址 在x86下,可以通过代码的方式直接读取 KeServiceDescriptorTable
这个被导出的表结构从而可以直接读取到SSDT表的基址,而在Win64系统中 KeServiceDescriptorTable
这个表并没有被导出,所以我们必须手动搜索到它的地址。
1.这里我们可以通过MSR(特别模块寄存器),读取C0000082寄存器,从而得到KiSystemCall64
的地址,在内核调试模式下直接输入 rdmsr c0000082
即可读取到该地址,反汇编可看到 nt!KiSystemCall64Shadow
函数。(Win10中如果开启了KVA Shadow那么这个地址是nt!KiSystemCall64Shadow,否则是nt!KiSystemCall64。 )
在KiSystemCall64Shadow
函数的最后,有一个jmp指令,会跳转到nt!KiSystemServiceUser
函数。
继续跟进KiSystemServiceUser
函数,可以看到对nt!KeServiceDescriptorTable
的读取。KeServiceDescriptorTable
就是SSDT表。
总结一下,获取SSDT基址流程:
MSR寄存器(0xC0000082)-> nt!KiSystemCall64Shadow ->nt!KiSystemServiceUser -> nt!KeServiceDescriptorTable。具体查找也是通过字节码匹配。
具体实现 下面是InlineHook KeBugCheckEx()的部分
关键匹配部分
获取函数部分
总结 这篇主题是进程保护,实际上主要知识点是关于SSDT_HOOK ,需要注意的是x86和x64下操作方式不同,现在网上很多的x64下的也是基于win7的,并且x64下需要绕过PG。
参考链接:x64下的进程保护 ,x64下的SSDTHOOK
Ring3下API的逆向分析及重构 首先分析ReadProcessMemory
函数整个调用过程,我们知道函数虽然是Kernel32.dll中的函数,但是只不过是封装好的函数,最终调用还是会到ntdll.dll中去。
我们先随便写个程序,然后生成64位exe动态分析。
ReadProcessMemory
实际上会调用红框中的函数
NtReadVirtualMemory
函数如下图,test执行与操作,结果为0,ZF被置位,所以不执行下面的跳转(注意与cmp指令的差别 )
这个地方首先是将内核函数的编号给了eax,然后test命令比较判断是采取中断的方式还是syscall的方式进入0环(以前x86的方式是调用KiFastSystemCall
函数,也就是0x7ffe0300
这个地址,该函数决定了采取哪种方式进入0环)。
_KUSER_SHARED_DATA 前面说到了0x7ffe0308
这个地址,如下图,其实就是shareddata+0x308
在 User 层和 Kernel 层分别定义了一个 _KUSER_SHARED_DATA
结构区域,用于 User 层和 Kernel 层共享某些数据,它们使用固定的地址值映射,_KUSER_SHARED_DATA
结构区域在 User 和 Kernel 层地址分别为:
User 层地址为:0x7ffe0000
Kernel 层地址为:0xffdf0000
虽然指向的是同一个物理页,但在ring3层是只读的,在ring0层是可写的,在0x308偏移处SystemCall
存放的地址就是真正进入ring0的实现方法。
总结 这篇API逆向,主要涉及到了3环进入0环的详细结构,关于重构其实网上示例代码是有关call KiFastSystemCall的,但是x64并没有调用这个函数,而是直接进行了判断然后选取以哪种方式进入0环。
基于PEB断链实现隐藏 模块隐藏 前面提到的关于DRIVER_OBJECT
结构中的Driver Section
成员,实际上是一个结构体
偏移处有一个DllBase
,这里存放的就是dll的地址,所以这里我们如果要想隐藏某个指定的dll,就可以通过DllBase
的方式,通过GetModuleHandleA
获取dll的句柄,来进行比对。
前面三个双向链表结构都是一样的,我们以InloadOrderModuleList来进行断链示范(其他两个一样的操作),要实现断链,最简单的做法就是让Head的Flink和Blink指向它自己。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 PPEB_LDR_DATA ldr; PLDR_DATA_TABLE_ENTRY ldte; PLIST_ENTRY Head, Cur; Head = &(ldr->InLoadOrderModuleList); Cur = Head->Flink; do { ldte = CONTAINING_RECORD( Cur, LDR_DATA_TABLE_ENTRY, InLoadOrderModuleList); if (ldte->BaseAddress == hModule) { ldte->InLoadOrderModuleList.Blink->Flink = ldte->InLoadOrderModuleList.Flink; ldte->InLoadOrderModuleList.Flink->Blink = ldte->InLoadOrderModuleList.Blink; } Cur = Cur->Flink; } while (Head != Cur);
进程隐藏 在windows系统中,所有的活动进程都是连在一起的,构成一个双链表,表头是全局变量PsActiveProcessHead
,当一个进程被创建时,其ActiveProcessList
域将被作为节点加入到此链表中;当进程被删除时,则从此链表中移除,如果windows需要枚举所有的进程,直接操纵此链表即可。
前八个字节指向的是下一个EPROCESS
的偏移0x448处
如下图,可以看到进程名为system
我们继续看下一个,可以看到进程名Registry
实现 在驱动中通过 PsGetCurrentProcess()
,来获取当前进程的EPROCESS
结构体,然后通过链表遍历其余的EPROCESS
,可以通过比较ImageFileName
字段来筛选自己想要隐藏的进程。然后对其进行断链操作。
1 2 3 4 5 6 7 curNode = (PLIST_ENTRY)((ULONG)pCurProcess + 0x448 ); nextNode = curNode->Flink; preNode = curNode->Blink; preNode->Flink = curNode->Flink; nextNode->Blink = curNode->Blink;
总结 这样的隐藏技术主要是基于关键结构体的成员,来进行摸链操作,可以详细复习一下EPROCESS、ETHREAD等关键数据结构
全局句柄表发现隐藏进程 前面说了0环进行PEB断链可以达到隐藏进程的效果,但是这只是作为权限维持的一种方法。所以这篇讲了基于全局句柄表PsdCidTable
,来找到隐藏进程的效果。
在EPROCESS中有一个成员ObjectTable
TableCode
就是句柄表地址
句柄表结构如下,句柄表以分级的方式存储,最多三级
句柄共八个字节,64位
(bit48~bit63)这一块共计两个字节,16位;高位字节是给SetHandleInformation这个函数用的,例如当执行如下语句:
1 SetHandleInformation(Handle,HANDLE_FLAG_PROTECT_FROM_CLOSE,HANDLE_FLAG_PROTECT_FROM_CLOSE);
这个位置将会被写入0x02
(bit32~bit47)这一块也是两个字节,16位;这块是访问掩码,是给OpenProcess
这个函数用的,即OpenProcess
的第一个参数 dwDesiredAccess
的值
(bit0~bit31)这两块共计四个字节,32位,各个位主要含义如下:
bit0:OBJ_PROTECT_CLOSE,表示调用者是否允许关闭该句柄;默认值为1
bit1:OBJ_INHERIT,指示该进程创建的子进程是否可以继承该句柄 ,即是否将该句柄项拷贝到它们的 句柄表中
bit2:OBJ_AUDIT_OBJECT_CLOSE,指示关闭该对象时是否产生一个审计事件;默认值为0
bit3~31:存放该内核对象在内核中的具体地址
句柄表中存放的是指针,32位就是32位指针
x64 算法: 进程ID(句柄索引) / 4 = 表索引 表索引 * 16 = 全局句柄表条目
x86算法:
进程ID / 4 = 表索引 表索引 * 8 = 全局句柄表条目
为什么 * 8 (16)因为每一项占8(16)字节,前4(8)字节为头,后4(8)字节为地址
当一个进程创建或者打开一个内核对象 时,将获得一个句柄 ,通过这个句柄可以访问对应的内核对象。上面知道可以从句柄低32位获取到内核对象的地址,内核对象在开头都有一个0x30字节的_OBJECT_HEADER结构 ,这是内核对象的头部,也就是说从0x30字节开始, 才是进程结构体开始的位置。
全局句柄表 全局变量 PspCidTable
存储了全局句柄表 _HANDLE_TABLE
的地址,全局句柄表存储了所有 EPROCESS
和 ETHREAD
,全局句柄表项低32位指向的就是内核对象,而非 OBJECT_HEADER
。
TableCode
最后一位为1,代表句柄表中有两层,当最后一位为0时,代表直接指向句柄表,当最后一位为1时代表指向一个数组,数组中的值才指向的是句柄,这个数组可以存放句柄表的指针,为2时就变成了一个三维数组
X86下_HANDLE_TABLE_ENTRY结构大小为8,x64下结构大小为16,每张表4k大小4k/16=256,所以一张表可以存256个handle。二级三级表存放指针,x64下一个指针地址占用8字节,x86下占用4字节。4k/8=512
1 2 3 4 5 6 7 Handle Handle_table[256 ] = {}; Handle_table Handle_table[512 ][0 ] = Handle_table;
根据上面的分析我们知道,当前句柄表存储的是512个句柄表数组(大小为0x8)的数组指针,每个句柄表数组有256个成员,每个成员大小为0x10大小, 这里我们查询的目标明显在第一张句柄表中,我们查看目标内存。
这里我们查看PID为4的进程对象,先4/4 = 1(0x01)计算地址公式为:
1 ffffad09`de6b0000 + 0x01 * 16 = ffffad09`de6b0010
**(note:前面提到过PspCidTable表中其表项低32位指向目标的Object,内核句柄表中存储的是Object_Header。)**所以:
1 (c40ae409`80401 dc5 >> 0x10 ) & 0xFFFFFFFFFFFFFFF0 == ffffc40ae4098040
我们成功在全局句柄表中查了目标进程对象,这里我们可以看其对应的Object_Header
。圈起来的地方就是64与32的区别,64位Object_Header
中已经没有Object_Type
结构体指针,取而代之的是TypeIndex
其是一个XOR加密的数据,我们可以逆向解出其数据,公式如下:
1 0x2 ^ 0x80(上图中OBJECT_HEADER地址的倒数第二字节) ^ ObHeaderCookie
其中 ObHeaderCookie 为一个BYTE字节的全局变量,我么可以通过WinDbg查看
计算结果如下:
1 0x2 ^ 0x80 ^ ObHeaderCookie(0x85) == 0x7
这里的0x7对应的是Object_Type
中的Index
字段(外提一句线程是0x8),我们查看PsProcessType
(其为Object_Type
结构)
第二种方法:
TypeIndex
就在说明内核对象的类型,说明放在了ObTypeIndexTable
中,将ObTypeIndexTable
首地址:fffff807670fce80
,取出来然后+0x2* 8(x86为0x2 * 4):
然后使用_OBJECT_TYPE
查看是什么类型(也不知道为啥我这里是Type,大家也可以试试hhh ):
第三种方法 :既然Object_Header
中去除了Object_Type
, 那么我们如何快速获取Object对应的Object_Type
呢,这里我们可以利用内核导出函数ObGetObjectType
,来获取对象对应的OBJECT_TYPE
, 函数原型如下:
1 2 NTKERNELAPI PVOID NTAPI ObGetObjectType (IN PVOID pObject) ;
可以看一下IDA:
如上图可以看到其是在ObTypeIndexTable
中获取对应的OBJECT_TYPE
指针, 可以知道其是个OBJECT_TYPE
指针数组, 获取公式:
1 ObTypeIndexTable[OBJECT_HEADER.TypeIndex ^ BYTE((OBJECT_HEADER地址 >> 8 )) ^ ObHeaderCookie]
实现 首先需要解决的问题是,获取PspCidTable
win7:
1 2 PsLookupProcessByProcessId(被导出) -> PspCidTable 1
win10:
1 PsLookupProcessByProcessId(被导出) -> PspReferenceCidTableEntry -> PspCidTable
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 BOOLEAN get_PspCidTable (ULONG64* tableAddr) { UNICODE_STRING uc_funcName; RtlInitUnicodeString(&uc_funcName, L"PsLookupProcessByProcessId" ); ULONG64 ul_funcAddr = MmGetSystemRoutineAddress(&uc_funcName); if (ul_funcAddr == NULL ) { return FALSE; } ULONG64 ul_entry = 0 ; for (INT i = 0 ; i < 40 ; i++) { if (*(PUCHAR)(ul_funcAddr + i) == 0xe8 ) { ul_entry = ul_funcAddr + i; break ; } } if (ul_entry != 0 ) { INT i_callCode = *(INT*)(ul_entry + 1 ); ULONG64 ul_callJmp = ul_entry + i_callCode + 5 ; for (INT i = 0 ; i < 20 ; i++) { if (*(PUCHAR)(ul_callJmp + i) == 0x48 && *(PUCHAR)(ul_callJmp + i + 1 ) == 0x8b && *(PUCHAR)(ul_callJmp + i + 2 ) == 0x05 ) { INT i_movCode = *(INT*)(ul_callJmp+i + 3 ); ULONG64 ul_movJmp = ul_callJmp+i + i_movCode + 7 ; *tableAddr = ul_movJmp; return TRUE; } } } else { for (INT i = 0 ; i < 70 ; i++) { if (*(PUCHAR)(ul_funcAddr + i) == 0x49 && *(PUCHAR)(ul_funcAddr + i + 1 ) == 0x8b && *(PUCHAR)(ul_funcAddr + i + 2 ) == 0xdc && *(PUCHAR)(ul_funcAddr + i + 3 ) == 0x48 && *(PUCHAR)(ul_funcAddr + i + 4 ) == 0x8b && *(PUCHAR)(ul_funcAddr + i + 5 ) == 0xd1 && *(PUCHAR)(ul_funcAddr + i + 6 ) == 0x48 && *(PUCHAR)(ul_funcAddr + i + 7 ) == 0x8b ){ INT i_movCode = *(INT*)(ul_funcAddr+i+6 + 3 ); ULONG64 ul_movJmp = ul_funcAddr+i+6 + i_movCode + 7 ; *tableAddr = ul_movJmp; return TRUE; } } } return FALSE; } VOID parse_table_1 (ULONG64 BaseAddr,INT index1,INT index2) { RTL_OSVERSIONINFOEXW OSVersion = { 0 }; OSVersion.dwOSVersionInfoSize = sizeof (RTL_OSVERSIONINFOEXW); RtlGetVersion((PRTL_OSVERSIONINFOW)&OSVersion); PEPROCESS p_eprocess = NULL ; PETHREAD p_ethread = NULL ; INT i_id = 0 ; for (INT i = 0 ; i < 256 ; i++) { if (!MmIsAddressValid((PVOID64)(BaseAddr + i * 16 ))) { continue ; } if (OSVersion.dwMajorVersion == 10 && OSVersion.dwMinorVersion == 0 ) { ULONG64 ul_recode = *(PULONG64)(BaseAddr + i * 16 ); ULONG64 ul_decode = (LONG64)ul_recode >> 0x10 ; ul_decode &= 0xfffffffffffffff0 ; i_id = i*4 + 1024 *index1 + 512 *index2*1024 ; if (PsLookupProcessByProcessId(i_id , &p_eprocess) == STATUS_SUCCESS) { DbgPrint("[LYSM] PID:%d , i:%d , addr:%p , object:%p\n" , i_id , i, BaseAddr + i*0x10 , ul_decode); } else if (PsLookupThreadByThreadId(i_id , &p_ethread) == STATUS_SUCCESS) { DbgPrint("[LYSM] TID:%d , i:%d , addr:%p , object:%p\n" , i_id , i, BaseAddr + i*0x10 , ul_decode); } } if (OSVersion.dwMajorVersion == 6 && OSVersion.dwMinorVersion == 1 ) { ULONG64 ul_recode = *(PULONG64)(BaseAddr + i * 16 ); ULONG64 ul_decode = ul_recode & 0xfffffffffffffff0 ; i_id = i*4 + 1024 *index1 + 512 *index2*1024 ; if (PsLookupProcessByProcessId(i_id , &p_eprocess) == STATUS_SUCCESS) { DbgPrint("[LYSM] PID:%d , i:%d , addr:%p , object:%p\n" , i_id , i, BaseAddr + i*0x10 , ul_decode); } else if (PsLookupThreadByThreadId(i_id , &p_ethread) == STATUS_SUCCESS) { DbgPrint("[LYSM] TID:%d , i:%d , addr:%p , object:%p\n" , i_id , i, BaseAddr + i*0x10 , ul_decode); } else { continue ; } } } } VOID parse_table_2 (ULONG64 BaseAddr, INT index2) { ULONG64 ul_baseAddr_1 = 0 ; for (INT i = 0 ; i < 512 ; i++) { if (!MmIsAddressValid((PVOID64)(BaseAddr + i * 8 ))) { continue ; } if (!MmIsAddressValid((PVOID64)*(PULONG64)(BaseAddr + i * 8 ))) { continue ; } ul_baseAddr_1 = *(PULONG64)(BaseAddr + i * 8 ); parse_table_1(ul_baseAddr_1, i, index2); } } VOID parse_table_3 (ULONG64 BaseAddr) { ULONG64 ul_baseAddr_2 = 0 ; for (INT i = 0 ; i < 512 ; i++) { if (!MmIsAddressValid((PVOID64)(BaseAddr + i * 8 ))) { continue ; } if (!MmIsAddressValid((PVOID64) * (PULONG64)(BaseAddr + i * 8 ))) { continue ; } ul_baseAddr_2 = *(PULONG64)(BaseAddr + i * 8 ); parse_table_2(ul_baseAddr_2, i); } } BOOLEAN enum_PspCidTable (ULONG64 cidTableAddr) { ULONG64 ul_tableCode = *(PULONG64)(((ULONG64)*(PULONG64)cidTableAddr) + 8 ); INT i_low2 = ul_tableCode & 3 ; if (i_low2 == 0 ) { parse_table_1(ul_tableCode & (~3 ),0 ,0 ); } else if (i_low2 == 1 ) { parse_table_2(ul_tableCode & (~3 ),0 ); } else if (i_low2 == 2 ) { parse_table_3(ul_tableCode & (~3 )); } else { DbgPrint("[LYSM] i_low2 非法!\n" ); return FALSE; } return TRUE; }
tips 除了进程遍历外,还可以用于反调试。 一个进程加载进内存后,可以起一个线程,专门去遍历其他所有进程的句柄表,如果发现,某个进程的句柄表中有自己进程的句柄,说明自己的这个进程可能正在被调试,就算没有在被调试,也至少被打开了,这时就可以强行关闭自己的程序,不被调试,达到反调试的目的。
总结 这一篇知识点比较多,其实主要还是围绕进程句柄表和全局句柄表,现在大多基于64位进行开发,但是网上很多资料都是32位的,参考性较小,所以这里参考了几篇博客进行了总结归纳。
首先从句柄表入手,X86下_HANDLE_TABLE_ENTRY结构大小为8,x64下结构大小为16,每张表4k大小4k/16=256,所以一张表可以存256个handle。二级三级表存放指针,x64下一个指针地址占用8字节,x86下占用4字节。4k/8=512。
EPROCESS
偏移0x570处有一个成员ObjectTable
,查看该结构类型可以看到偏移0x8处是TableCode
也就是句柄表地址,该地址后三位是有关句柄表是几级表的,该地址指向的是OBJECT_HEADER
,该结构相当于链表的头部,需要加上0x30的偏移才能定位到真正的链表。
全局变量 PspCidTable
存储了全局句柄表 _HANDLE_TABLE
的地址,全局句柄表存储了所有 EPROCESS
和 ETHREAD
,全局句柄表项低32位指向的就是内核对象,而非 OBJECT_HEADER
。
关于全局句柄表的ObjectHeader
有一个TypeIndex字段,采用了XOR加密,可以采用公式解密,也可以利用导出函数ObGetObjectType
,(0x7为进程,0x8为线程 )
参考链接:x64下的全局句柄表解析 ,x64遍历枚举 ,解析全局句柄表
x64下隐藏可执行内存 前面讲了PEB断链实现隐藏进程的效果,但是只是表面的进程隐藏,所有内存的详细信息都会被存储在vad树里面。
vad 内存管理器使用需求分页算法 来知道何时将页面加载到内存中,直到线程引用地址并导致页面故障,然后再从磁盘中检索页面。与写时复制一样,需求分页是一种惰性评估的形式(等待直到需要时才执行任务。)
内存管理器使用惰性计算不仅可以将页面带入内存,还可以构造描述新页面所需的页面表。例如,当一个线程使用VirtualAlloc
提交一个大区域的虚拟内存时,内存管理器可以立即构造访问已分配的整个内存范围所需的页面表。但如果其中一些范围从未被访问过呢?为整个范围创建页面表将是一个浪费时间的工作。相反,内存管理器等待创建一个页面表,直到线程导致页面故障。然后,它将为该页面创建一个页面表。这种方法显著提高了保留以及提交大量内存但访问量稀疏的进程的性能。
使用惰性计算算法,即使是很大的内存块也是一个快速的操作。当一个线程分配内存时,内存管理器必须响应该线程要使用的地址范围。为此,内存管理器维护另一组数据结构,以跟踪哪些虚拟地址已保留在进程的地址空间中,而哪些没有保留。这些数据结构被称为虚拟地址描述符(VADs)。VADs被分配在非分页池中。
VAD(虚拟地址描述符)是管理虚拟内存的,每一个进程有自己单独的一个VAD树,使用VirtualAlloc
申请一个内存,则会在VAD树上增加一个结点,其是_MMVAD
结构体
StartingVpn
和这个EndingVpn
是以页为单位
在EPROCESS
结构中偏移0x7d8处有一个VadRoot
成员,其是根结点
VADs被组织成一个自平衡的AVL tree
(以其发明者阿德尔森-维尔斯基和兰迪斯的名字命名,其中任何节点的两个子子树的高度最多相差1;这使得插入、查找和删除非常快)。
通过下面可以看到VadRoot
,采用指令!vad 红框中地址
就可以查看到该进程的vad树
由以上可知,可以通过_EPROCESS.VadRoot
遍历VAD二叉树。进程内的所有用户模式线程使用Thread Control Stack
上的不同内存区域(Shadow Stack),可以通过遍历进程的VAD自平衡二叉树(self-balancing AVL tree)获取描述进程Thread Control Stack的_MMVAD
结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 typedef struct _MMVAD { struct _MMVAD_SHORT Core ; union { union { unsigned long LongFlags2; struct _MMVAD_FLAGS2 VadFlags2 ; }; } u2; long Padding_; struct _SUBSECTION * Subsection ; struct _MMPTE * FirstPrototypePte ; struct _MMPTE * LastContiguousPte ; struct _LIST_ENTRY ViewLinks ; struct _EPROCESS * VadsProcess ; union { union { struct _MI_VAD_SEQUENTIAL_INFO SequentialVa ; struct _MMEXTEND_INFO * ExtendedInfo ; }; } u4; struct _FILE_OBJECT * FileObject ; } MMVAD, *PMMVAD; typedef struct _MMVAD_SHORT { union { struct _RTL_BALANCED_NODE VadNode ; struct _MMVAD_SHORT * NextVad ; }; unsigned long StartingVpn; unsigned long EndingVpn; unsigned char StartingVpnHigh; unsigned char EndingVpnHigh; unsigned char CommitChargeHigh; unsigned char SpareNT64VadUChar; long ReferenceCount; struct _EX_PUSH_LOCK PushLock ; union { union { unsigned long LongFlags; struct _MMVAD_FLAGS VadFlags ; }; } u; union { union { unsigned long LongFlags1; struct _MMVAD_FLAGS1 VadFlags1 ; }; } u1; struct _MI_VAD_EVENT_BLOCK * EventList ; } MMVAD_SHORT, *PMMVAD_SHORT; typedef struct _RTL_BALANCED_NODE { union { struct _RTL_BALANCED_NODE * Children [2]; struct { struct _RTL_BALANCED_NODE * Left ; struct _RTL_BALANCED_NODE * Right ; }; }; union { unsigned char Red : 1 ; unsigned char Balance : 2 ; unsigned __int64 ParentValue; }; } RTL_BALANCED_NODE, *PRTL_BALANCED_NODE; typedef struct _RTL_AVL_TREE { struct _RTL_BALANCED_NODE * Root ; } RTL_AVL_TREE, *PRTL_AVL_TREE; typedef struct _EPROCESS { … struct _RTL_AVL_TREE VadRoot ; … }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 typedef struct _MMVAD_FLAGS { struct /* bitfield */ { unsigned long VadType : 3 ; unsigned long Protection : 5 ; unsigned long PreferredNode : 6 ; unsigned long NoChange : 1 ; unsigned long PrivateMemory : 1 ; unsigned long PrivateFixup : 1 ; unsigned long ManySubsections : 1 ; unsigned long Enclave : 1 ; unsigned long DeleteInProgress : 1 ; unsigned long PageSize64K : 1 ; unsigned long RfgControlStack : 1 ; unsigned long Spare : 10 ; }; } MMVAD_FLAGS, *PMMVAD_FLAGS; typedef struct _MI_VAD_EVENT_BLOCK { struct _MI_VAD_EVENT_BLOCK * Next ; union { struct _KGATE Gate ; struct _MMADDRESS_LIST SecureInfo ; struct _RTL_BITMAP_EX BitMap ; struct _MMINPAGE_SUPPORT * InPageSupport ; struct _MI_LARGEPAGE_IMAGE_INFO LargePage ; struct _ETHREAD * CreatingThread ; struct _MI_SUB64K_FREE_RANGES PebTebRfg ; struct _MI_RFG_PROTECTED_STACK RfgProtectedStack ; }; unsigned long WaitReason; long __PADDING__[1 ]; } MI_VAD_EVENT_BLOCK, *PMI_VAD_EVENT_BLOCK; typedef struct _MI_RFG_PROTECTED_STACK { void * ControlStackBase; struct _MMVAD_SHORT * ControlStackVad ; } MI_RFG_PROTECTED_STACK, *PMI_RFG_PROTECTED_STACK;
虚拟内存分为两类:
(1)通过VirtualAlloc/VirtualAllocEx
申请的:Private Memory ,独享物理页
(2)通过CreateFlieMapping
映射的:Mapped Memory,多个进程共享物理页。
分页机制 在32位里面有2-9-9-12
、10-10-12
两种分页模式,而在64位下只有一种分页模式,即9-9-9-9-12
分页模式。在64位系统中,实际上CPU只使用了其中的48位用于寻址。
9-9-9-9-12
分页表示物理地址拥有四级页表,在Intel开发手册中,将这四级页表分别称为PML4E
、PDPTE
、PDE
、PTE
,但微软的命名方式略有不同,将这四级页表分别称为PXE
、PPE
、PDE
、PTE
,WinDbg中也是如此
启用分页模式条件:cr0.PG = 1
且 cr0.PE = 1
根据不同CPU架构及特性主要分为三种模式,处于哪种模式视寄存器属性不同:
32-bit paging(32位OS): cr0.PG = 1
、 cr4.PAE = 0
PAE paging(32位OS且开启了PAE): cr0.PG = 1
、 cr4.PAE = 1
、 IA32_EFER.LME = 0
IA-32e paging(64位OS): cr0.PG = 1
、 cr4.PAE = 1
、 IA32_EFER.LME = 1
主要研究的是IA-32e
模式下的内存,这里IA-32e
提供了三种页转换模型:
4k:PML4T,PDPT,PDT和PT
2M:PML4T,PDPT和PDT
1G:PML4T和PDPT
在4kb小页的情况下,64位可以拆分为以下几段,即9-9-9-9-9-12分页
sign extended – 符号扩展位 — 在线性地址48~63bit
PML4 entry – 在线性地址39~47bit用于索引PML4 entry,指向PDP
PDP entry – 在线性地址的30~38bit用来索引PDP entry,指向PDE
PDE entry – 在线性地址的21~29bit用来索引PDEentry,指向PTE
PTE entry – 在线性地址的12~20bit用来索引PTE entry,指向page offset
page offse t – 在线性地址的0~11bit提供在页中的offset
页表基址 CR3中保存的页表基址是物理地址,程序如果直接访问这个地址,实际上访问的是一个线性地址,会被虚拟内存管理器解析成另一个地址.实际上,操作系统会将当前进程的物理页映射在某个线性地址中,以供程序读取自己的页表内容。
在x86系统中,页表基址是固定的,位于0xC0000000
,将这个线性地址进行解析,访问其物理页的内容,会发现从这个地址开始,里面保存的数据为当前程序的所有物理页地址。
而在x64系统中,页表基址不再是固定的值,而是每次系统启动后随机生成的可以在WinDbg中查看0地址对应的线性地址来确定当前的页表基址(!pte 0)。每个物理页占8个字节,例如,第一个物理页地址位于线性地址0xFFFFF38000000000
,第二个物理页地址位于线性地址0xFFFF800000000008
,每个物理页中包含1024个字节的数据。
MmIsAddressValid 探索系统的分页机制最好的办法是对内核中的 MiIsAddressValid
函数的实现进行分析。win7和win10的64位系统下使用的是两种不同的查找 pte
、 pde
、 pdpte
、 pml4e
的方式。接下来分别对两个系统的该函数进行相应的分析
win7 64位下的函数分析
首先通过移位,查看前16位是否为全0或者全1,如果不是,返回不合法。然后通过移位分别分出9-9-9-9-12这5部分,然后加上相应的基址,找到对应的 pte,pde,pdpte,pml4e
,并判断P位是否为0。如果为0返回不合法。
该函数较为简单,将减去的数值取反加一可以得到对应的基址。可得在win7 64位下,其 PTE_BASE
为 fffff68000000000
。
win10 64位下的函数分析
在 win10 1607
以上版本,微软为分页基址加上了随机页目录基址,这让定位 pte
变得更加困难。 PTE_BASE
不再是之前写死的值,而是每次开机都会随机在一定的范围内挑选一个值。而在 win10 64
位下的 MmIsAddressValidEx
函数中,其更换了另外一套寻找 pte,pde,pdpte,pml4e
的方法,如下
可以看到这里计算 pte,pde,pdpte,pml4e
的时候用的都只有一个 pte_base
,不再像win7那样求每个值的时候都使用一个不同的基址。
实现 在申请一块不可执行的内存后通过修改 pte
与 pde
手动将页面设置为可执行,达到隐藏可执行内存的目的。
首先是定位定位PTE
、PDE
、PPE
、PXE
,PTE_BASE
有两种情况,当是Win10 1607以上,就需要自己通过逆向的方式提取硬编码进行定位,这里通过MmGetVirtualForPhysical
函数加偏移的方式进行定位。
然后是一个分配内存的函数,可以往指定pid的进程中写入shellcode,并隐藏其可执行属性,使这块内存在vad树中看来是不可执行的。
我们找到目标进程,然后通过KeStackAttachProcess
函数实现进程挂靠,即把自己的cr3换成目标进程的cr3(CR3含有存放页目录表页面的物理地址 )。
然后使用ZwAllocateVirtualMemory
先分配一块可读可写的内存。
首先将前3位符号位去掉得到内存的起始地址和结束地址,然后循环判断,必须每一块内存都需要修改。结合MmIsAddressValid
并判断valid
是否为1,这里如果valid
为0则该块内存无效,然后将no_execute
置0即可获得可执行权限
总结 其实就是通过修改pde和pte属性隐藏可执行内存,在编写这样的驱动的过程中也能巩固对x64分页机制的认识。
参考链接:x64下隐藏可执行内存 ,hide_excute_memory
参考链接:主要参考博客