SSDT是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的地址
g_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)
{
returnSTATUS_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;
}
returnSTATUS_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)
{
returnSTATUS_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的时候允许写入只读内存页。通过这种方式就可以实现修改服务函数代码的目的了。
//关闭写保护
voidEnableWrite()
{
__try
{
_asm
{
cli//禁止中断发生
mov eax, cr0
andeax,not10000h//cr0寄存器中第16位 WP位
mov cr0, eax
}
}
__except (1)
{
}
}
//恢复写保护
voidDisableWrite()
{
__try
{
_asm
{
mov eax, cr0
oreax,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表的结构
typedefstruct_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就是服务函数地址表的起始地址,可以看到从该起始地址处开始,都是以四字节为单位的很有规律的数字,这些数字就是服务函数的绝对地址。
原文作者: aniquest
原文链接:https://bbs.pediy.com/thread-247983.htm%E3%80%81
转载请注明转自看雪学院
更多阅读:
1、[原创]BugkuCTF-WEB-矛盾
2、[原创]2018东华杯momo_server详解
3、体验加壳过程
4、[翻译]虚拟化是如何工作的(第二部分)