内容简介:SDT是Ring3层函数调用通往内核层的“大门”,SSDT表里面的每一项是服务函数的函数地址(对于32位windows系统来说是绝对地址,但在64位windows操作系统中的SSDT并不是绝对地址),那么通过这个函数地址定位到服务函数所在的位置,就可以对其进行Inline Hook。目录0x00 Hook过程概述
SDT是Ring3层函数调用通往内核层的“大门”,SSDT表里面的每一项是服务函数的函数地址(对于32位windows系统来说是绝对地址,但在64位windows操作系统中的SSDT并不是绝对地址),那么通过这个函数地址定位到服务函数所在的位置,就可以对其进行Inline Hook。
目录
0x00 Hook过程概述
0x01详细的Hook过程
0x02注意
0x03 关于SSDT(System Service Descriptor Table)
0x00 Hook过程概述:
1. 获取服务函数地址:
(1)、通过全局变量KeServiceDescriptorTable获得SSDT表的起始地址;
(2)、映射ntdll.dll到ring0空间,获得要Hook的函数的服务索引号;
(3)、根据获得的函数的服务索引号,从SSDT表中获得函数地址;
2. 进行Hook(这里采用的Inline Hook是修改函数前五个字节的方法):
(1)、申请一块五字节大小的内存用来备份原函数的前五个字节,用于驱动卸载的时候恢复原函数;
(2)、申请一块十字节大小的内存用来构造“跳板代码”;
(3)、拷贝原函数的前五个字节到跳板代码的前五个字节处;
(4)、计算由跳板代码处跳转到原函数开始处过五个字节的偏移(
记为offset1),将jmp offset1指令写入跳板代码的后五个字节处;
(5)、编写Fake函数,也就是我们需要执行的代码;
(6)、计算原函数开始处跳转到Fake函数开始处的偏移(offset2),将jmp offset2指令写入原函数开始处;
3. 驱动卸载时,
恢复被Hook的函数,也就是将一开始备份的原函数开始处的五个字节拷贝到原函数开始处。
0x01详细的Hook过程
1. 第一步就是定位SSDT表中服务函数的地址:
(1)、通过导出的全局变量KeServiceDescriptorTable就可以定位到SSDT表处
//获取SSDT的地址
_ServiceTableBase = KeServiceDescriptorTable->ServiceTableBase;
(2)、有了基地址,那么我们要Hook的函数究竟是SSDT表中的第几项呢?一种方法是通过windbg等调试 工具 静态获得服务函数索引(就是函数在SSDT表中的第几项),但这种方法不具有通用性,所以这里采用动态获得函数索引的方法。其原理是通过映射ntdll.dll模块到ring0内存空间,从它的导出表中获得函数地址,如下图所示,第一条指令mov eax,0BEh,其机器码为b8be000000,所以函数地址处过一个字节后的四字节的内容就是函数索引,也就是这里的000000be。
3)、获得了函数索引后,我们就可以定位到函数了
//从SSDT表中获得函数地址
g_NtOpenProcess = (LPFN_NTOPENPROCESS)(g_ServiceTableBase[NtOpenProcessIndex]);
2. 定位到函数后,就利用修改函数的前五个字节的方法对其进行inlinehook
(1)、备份原函数的前五个字节
//申请五个字节大小的内存用来备份原函数的前五个字节,PatchedCodeLength = 5
g_OriginalNtOpenProcessCode = ExAllocatePool(NonPagedPool, PatchedCodeLength);
if (g_OriginalNtOpenProcessCode == NULL)
{
return STATUS_INSUFFICIENT_RESOURCES;
}
//拷贝函数的前五个字节保存到__OriginalNtOpenProcessCode
EnableWrite();
memcpy(g_OriginalNtOpenProcessCode, (PVOID)OriginalFunctionAddress, PatchedCodeLength);
DisableWrite();
(2)、构造跳板代码,跳板代码的前五个字节是原函数的前五个字节,后五个字节是一个跳转指令,用于跳转回到原函数入口过五个字节处,构造跳板的主要目的是用于执行正确的原函数,即不执行我们的Fake函数。对于跳板代码,前五个字节从原函数开始处拷贝五个字节就行了,而后五个字节是
jmp offset(offset为当前地址处到原函数开始过五个字节处的偏移)也就是E9 xxxxxxxxh。offset的计算过程如下:
构造跳板的代码如下:
//构造跳板代码 g_TrampolineCode = ExAllocatePool(NonPagedPool, PatchedCodeLength + 5); if (g_TrampolineCode == NULL) { //该步申请失败的时候,释放之前申请的内存,然后再返回 if (g_OriginalNtOpenProcessCode != NULL) { ExFreePool(g_OriginalNtOpenProcessCode); g_OriginalNtOpenProcessCode = NULL; } return STATUS_INSUFFICIENT_RESOURCES; } //__TrampolineCode[0x90 0x90 0x90 0x90 0x90 0x90 0x90 0x90 0x90 0x90] (0x90:NOP指令) RtlFillMemory(g_TrampolineCode, PatchedCodeLength + 5, 0x90); //NtOpenProcessPassCode[8bff558bec 0x90 0x90 0x90 0x90 0x90] memcpy((PUCHAR)g_TrampolineCode, g_OriginalNtOpenProcessCode, PatchedCodeLength); Temp = (PUCHAR)OriginalFunctionAddress + PatchedCodeLength; //第一个+5是过前五个字节,计算跳转偏移,从跳板处跳到函数入口处(过五个字节) *((ULONG*)&v2[1]) = (PUCHAR)Temp - ((PUCHAR)g_TrampolineCode + 5 + 5); memcpy((PUCHAR)g_TrampolineCode + PatchedCodeLength, v2, 5);
(3)、编写Fake函数,Fake函数就是我们设置的需要执行的代码,当调用NtOpenProcess进入该函数的时候,就会跳转到Fake函数处,执行我们的代码。
NTSTATUS FakeNtOpenProcess( OUT PHANDLE ProcessHandle, IN ACCESS_MASK DesiredAccess, IN POBJECT_ATTRIBUTES ObjectAttributes, IN PCLIENT_ID ClientID OPTIONAL ) { __try { PEPROCESS EProcess = PsGetCurrentProcess(); if (EProcess != NULL && MmIsAddressValid(EProcess) && AnIsRealProcess(EProcess) == TRUE) { //通过EProcess获得进程完整路径 WCHAR ProcessFullPath[MAX_PATH] = { 0 }; if (AnGetProcessFullPathByEProcess(EProcess, ProcessFullPath, MAX_PATH) == TRUE) { DbgPrint("%S\r\n", ProcessFullPath); if (wcsstr(ProcessFullPath, L"explorer.exe") != 0) { return STATUS_ACCESS_DENIED; //黑名单 } } } } except(EXCEPTION_EXECUTE_HANDLER) { } if (g_TrampolineCode != NULL) { //调用正常的NtOpenProcess return ((LPFN_NTOPENPROCESS)g_TrampolineCode)(ProcessHandle, DesiredAccess, ObjectAttributes, ClientID); //白名单 } }
(4)、计算原函数开始处跳转到Fake函数开始出的偏移(offset2),将jmp offset2指令写入原函数开始处
//设置fake函数,jmp fakefunctionaddress Temp = (PUCHAR)FakeFunctionAddress; *((ULONG*)&v1[1]) = (PUCHAR)Temp - ((PUCHAR)OriginalFunctionAddress + 5); EnableWrite(); memcpy(OriginalFunctionAddress, v1, PatchedCodeLength); DisableWrite();
(5)、至此,对NtOpenProcess函数的SSDT Inline Hook就完后成了。当驱动程序卸载的时候,需要恢复被修改的NtOpenProcess函数的前五个字节:
EnableWrite(); memcpy(OriginalFunctionAddress, OriginalCode, PatchedCodeLength); DisableWrite();
0x02 注意
值得注意的是服务函数的内存是只读,可执行的,但不可写,要想修改服务函数的代码就需要一些措施了。
1. 改变CR0寄存器的第16(WP)位
CR0寄存器在Windows内存管理中发挥着重要的作用,其结构如下图所示:
其中第16位WP(Write Protect)叫做写保护属性位,当该位为1的时候禁止只读内存页的写操作,当该位为0的时候允许写入只读内存页。通过这种方式就可以实现修改服务函数代码的目的了。
//关闭写保护 void EnableWrite() { __try { _asm { cli //禁止中断发生 mov eax, cr0 and eax, not 10000h //cr0寄存器中第16位 WP位 mov cr0, eax } } __except (1) { } } //恢复写保护 void DisableWrite() { __try { _asm { mov eax, cr0 or eax, 10000h mov cr0, eax sti //允许中断发生 } } __except (1) { } }
2. 通过MDL(Memory Descriptor List)
MDL表示一种内存描述符列表结构,它描述已被页面锁定的用户或内核模式内存。对于MDL的操作系统提供了一系列的接口函数,这里就不再详述MDL的使用了。
0x03 关于SSDT(System Service Descriptor Table):
1、32位系统API函数调用过程跟踪分析
(1)、用户调用kenel32.dll中的ReadFile,kenel32.dll中都是包装函数,kenel32.dll会用这些包装函数完成参数的有效性检查,将所有东西转换为unicode,接着调用ntdll.dll中的NtReadFile函数。
(2)、当调用ntdll.dll中的中的NtReadFile时,该函数将所需的Servcie ID送入EAX寄存器,然后调用SharedUserData!SystemCallStub (7ffe0300)
SharedUserData是一个数据结构
该数据结构的0x300(也就是地址0x7ffe0300)处,就是KiFastSystemCall的地址
所以在将Index放入eax寄存器之后,调用KiFastSystemCall,然后执行sysenter指令;执行sysenter指令时,会把寄存器SYSENTER_CS_MSR的内容复制到段寄存器CS中,把寄存器SYSENTER_EIP_MSR的内容复制到寄存器EIP中,把寄存器SYSENTER_CS_MSR的内容+8写入堆栈段寄存器SS中,把寄存器SYSENTER_ESP_MSR的内容复制到堆栈指针ESP中,
(3)、执行sysenter指令后进入系统空间并从预定的地址执行程序,同时开始使用系统空间的堆栈,windbg下使用rdmsr命令可以查看MSR寄存器的值,查看Inter指令手册,
SYSENTER_EIP_MSR位于MSRs寄存器的0x176处,windbg输入rdmsr 176即可得SYSENTER_EIP_MSR,可以看到SYSENTER_EIP_MSR对应的是KiFastCallEntry函数,该函数是系统空间中快速系统调用的入口。其中调用KiSystemService(),该函数根据之前保存在eax的索引,在SSDT表中搜索相关函数的地址,然后调用该函数。
2、SSDT表的结构
typedef struct _SYSTEM_SERVICE_DESCRIPTOR_TABLE
{
PVOID ServiceTableBase;
PVOID ServiceCounterTableBase;
ULONG NumberOfServices;
PVOID ParamTableBase;
} SYSTEM_SERVICE_DESCRIPTOR_TABLE, *PSYSTEM_SERVICE_DESCRIPTOR_TABLE;
导出的全局变量KeServiceDescriptorTable便指向了SSDT表,下图中红框内的便是SYSTEM_SERVICE_DESCRIPTOR_TABLE结构的四成员,
第一成员ServiceTableBase就是服务函数地址表的起始地址,可以看到从该起始地址处开始,都是以四字节为单位的很有规律的数字,这些数字就是服务函数的绝对地址。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
首席产品官1 从新手到行家
车马 / 机械工业出版社 / 2018-9-25 / 79
《首席产品官》共2册,旨在为产品新人成长为产品行家,产品白领成长为产品金领,最后成长为首席产品官(CPO)提供产品认知、能力体系、成长方法三个维度的全方位指导。 作者在互联网领域从业近20年,是中国早期的互联网产品经理,曾是周鸿祎旗下“3721”的产品经理,担任CPO和CEO多年。作者将自己多年来的产品经验体系化,锤炼出了“产品人的能力杠铃模型”(简称“杠铃模型”),简洁、直观、兼容性好、实......一起来看看 《首席产品官1 从新手到行家》 这本书的介绍吧!