hook目的
360hookport驱动所实现的功能就是在不修改系统的SSDT表/ShadowSSDT表的情况下拦截一些重要的系统调用,并进行根据一定的过滤规则进行参数和返回值的检查,阻止一些不安全或危险的系统调用。
hook思路
360hookport的hook思路就是 :因为32位系统下所有的API调用一般都是通过sysenter快速系统调用指令进入到内核中,然后在内核中调用的第一个函数就是ntoskrnl.exe的KiFastCallEntry函数,然后根据从3环传进的系统服务号查询系统描述符表(SSDT/ShadowSSDT表)得到其对应的地址后然后调用它。
360hookport做的就是将就是利用inline hook将这个KiFastCallEntry拦截,然后jmp到hookport模块中,调用ssdt_fiter()函数根据SERVICE_FILTER_INFO_TABLE和FILTERFUN_RULE_TABLE表中的hook开关和代{过}{滤}理函数开关是否打开来判断是否需要对此次调用进行hook,如果需要hook就返回SERVICE_FILTER_INFO_TABLE表中对应的服务例程的代{过}{滤}理函数,如果不需要hook就返回原始例程的地址,然后通过push KiFastCallEntryhook位置的下一条指令的地址,然后执行ret指令返回到KiFastCallEntryhook位置的下一条指令处继续执行。
这样如果返回的是我们代{过}{滤}理函数的地址,接下来KiFastCallEntry就会调用我们的代{过}{滤}理函数,如果返回的是原始例程的地址KiFastCallEntry就会调用原始例程。
hook细节
两个重要的数据结构
00000000 _SERVICE_FILTER_INFO_TABLE struc ; (sizeof=0x5DDC, align=0x4, copyof_10)
00000000 SSDTCnt dd ? ; Shadowssdt的最大服务数目(shadowssdt中服务要包含ssdt中的)
00000004 SavedSSDTServiceAddress dd 1001 dup(?) ; 原始SSDT例程地址
00000FA8 ProxySSDTServiceAddress dd 1001 dup(?) ; SSDT调用的代{过}{滤}理函数地址
00001F4C SavedShadowSSDTServiceAddress dd 1001 dup(?) ; 原始shadowSSDT例程地址
00002EF0 ProxyShadowSSDTServiceAddress dd 1001 dup(?) ; ShadowSSDT调用的代{过}{滤}理函数地址
00003E94 SwitchTableForSSDT dd 1001 dup(?) ; SSDT代{过}{滤}理开关(HOOK开关)
00004E38 SwitchTableForShadowSSDT dd 1001 dup(?) ; ShadowSSDT代{过}{滤}理函数开关
00005DDC _SERVICE_FILTER_INFO_TABLE ends
00000000 _FILTERFUN_RULE_TABLE struc ; (sizeof=0x1A8, mappedto_12)
00000000 bSize dd ? ; FILTERFUN_RULE_TABLE结构的的大小
00000004 pNextRuleTable dd ? ; 指向下一个张FILTERFUN_RULE_TABLE表包含新的过滤规则
00000008 IsFilterFunFilledReady dd ? ; 过滤函数开关(表示过滤函数是否准备好)
0000000C CheckServiceRoutine dd 101 dup(?) ; 过滤函数表,一共有101(0x65)个
000001A0 ShadowSSDTRuleTableBase dd ? ; ShadowSSDT过滤规则表的基地址
000001A4 SSDTRuleTableBase dd ? ; SSDT过滤规则表的基地址
000001A8 _FILTERFUN_RULE_TABLE ends
ssdt_fiter()
看一下ssdt_fiter()函数,其内部会先判断是ShadowSSDT调用还是SSDT调用,接着判断代{过}{滤}理函数hook开关是否打开,过滤函数表是否准备好。只有都打开后才会返回代{过}{滤}理函数地址,并将原函数的地址保存在SERVICE_FILTER_INFO_TABLE的原程序例程数组中
ULONG __stdcall ssdt_filter(unsigned int call_index, int ori_pfn, int ServiceBase)
{
if ( ServiceBase == g__ssdt_bast && call_index <= g_max_shadowIndex )
goto LABEL_15;
if ( ServiceBase == g_shadow_ssdt_base && call_index <= g_max_SSDT_index ) // 如果是ShadowSSDT调用
{
if ( g_myServiceBase->SwitchTableForShadowSSDT[call_index] && check_needto_filter(call_index, 1) ) //判断代{过}{滤}理函数hook开关是否打开,过滤表是否准备好
{
g_myServiceBase->SavedShadowSSDTServiceAddress[call_index] = ori_pfn; //将原始例程的地址保存到我们的SERVICE_FILTER_INFO_TABLE表中
return g_myServiceBase->ProxyShadowSSDTServiceAddress[call_index]; //返回代{过}{滤}理函数地址
}
return ori_pfn; // 返回原例程地址
}
if ( ServiceBase == *(_DWORD *)addr_g_KeServiceDescriptorTable ) //如果是SSDT调用
{
LABEL_15:
if ( g_myServiceBase->SwitchTableForSSDT[call_index] && check_needto_filter(call_index, 0) ) //判断理函数hook开关是否打开,过滤表是否准备好
{
g_myServiceBase->SavedSSDTServiceAddress[call_index] = ori_pfn; //将原始例程的地址保存到我们的SERVICE_FILTER_INFO_TABLE表中
return g_myServiceBase->ProxySSDTServiceAddress[call_index]; // 返回代{过}{滤}理函数地址
}
}
return ori_pfn; //返回原例程地址
}
一般的代{过}{滤}理函数
我们来看一下一般的代{过}{滤}理函数是怎么处理的,有一些代{过}{滤}理函数还会进行额外的处理。我们要检查系统调用是否安全肯定要检查其对应的参数,代{过}{滤}理函数会先调用pre_check函数,第一个参数是此服务在FILTERFUN_RULE_TABLE的CheckServiceRoutine过滤函数数组中的对应的索引(一共有0x65个过滤函数对应0-0x64过滤索引),第二个参数为对应服务的参数数组(参数以数组的形式传递)。
在pre_check()函数内部其会先判断FILTERFUN_RULE_TABLE的IsFilterFunFilledReady过滤规则表是否准备就绪,然后会根据传入的过滤函数表索引获取所有FILTERFUN_RULE_TABLE表中对应的过滤函数并调用,(过滤函数的实现不在hookport模块中,其实现在其他模块中,然后通过hookport提供的接口将过滤函数的地址填写到FILTERFUN_RULE_TABLE的过滤函数数组中)。过滤函数中会对参数进行检查,如果有问题就返回错误,没问题就返回一个函数地址CheckReturn()。
光检查参数不行,有时候还需要检查返回值,CheckReturn()这个函数就是从来检测返回值的。接着会继续遍历FILTERFUN_RULE_TABLE的pNextRuleTable寻找下一张FILTERFUN_RULE_TABLE表继续调用过滤函数。然后返回返回值。最多有16张FILTERFUN_RULE_TABLE表。
当返回值都没问题后,将调用各个FILTERFUN_RULE_TABLE表中过滤函数返回的CheckReturn()函数的地址保存到数组中并返回到代{过}{滤}理函数中。
然后代{过}{滤}理函数会判断此次调用时SSDT调用还是ShadowSSDT调用,然后从SERVICE_FILTER_INFO_TABLE中获得原始的服务例程并调用,然后检查返回值,如果调用出错肯定就不用管了,如果调用成功就调用我们刚刚调用pre_check函数返回的CheckReturn函数数组,依次调用各个CheckReturn函数,此函数会在内部检查返回值然后返回,如果所有的返回值都没问题代{过}{滤}理函数就可以将调用原始例程的返回值返回了。
具体细节
DriverEntry入口处先判断操作系统的版本信息,然后查看是否处于安全模式下,如果是就返回错误代码。
接着调用init_and_hook_KiFastCallEntry_KeUserModeCallback()函数初始化,并hook_KiFastCallEntry和KeUserModeCallback。
在函数内部其会先调用findmodule()查看是否加载了win32k.sys模块,findmodule是通过ZwQuerySystemInformation传入SystemModuleInformation枚举内核模块。注意得到的第一个模块就是ntoskrnl.sys的模块。获得SYSTEM_MODULE_INFORMATION_ENTRY结构数组,然后根据模块的名称进行判断得到模块的基地址和大小
NTSTATUS
ZwQuerySystemInformation (
SYSTEM_INFORMATION_CLASS SystemInformationClass,
PVOID SystemInformation,
ULONG SystemInformationLength,
ULONG *ReturnLength);
如果加载了此模块就调用search_ssdtTable_byHardCode()得到shadowSSDT表的基地址。search_ssdtTable_byHardCode()函数时在函数KeAddSystemServiceTable根据特征码0x888d搜寻KeServiceDescriptorTableShadow地址,或者在函数KeRemoteSystemServiceTable中根据特征码0x8889得到KeServiceDescriptorTableShadow地址
接着调用GetProcAddress得到KeServiceDescriptorTable地址进一步得到SSDT表的基地址。
GetProcAddress内部通过findmodule得到第一个内核模块的基地址,第一个内核模块也就是ntoskrnl.exe,然后解析ntoskrnl.exePE头文件根据导出表得到KeServiceDescriptorTable的地址。
因为KeServiceDescriptorTable是由ntoskrnl.exe导出的,而win32k.sys并没有导出KeServiceDescriptorTableShadow
接着调用CollectAllHookFun_Index()得到所有需要hook的服务例程的服务索引,对于有对应ZW系列导出函数的索引我们直接通过获得ZW系列地址然后,在函数开头位置获得索引号,而如果hook的函数没有对应的ZW系列导出函数则判断系统的版本信息,根据硬编码得到对应的服务索引
如果索引号获取不到,或者得到了大于1000的索引号,统一将服务索引号设置为1000,(后面所有服务号为1000的hook开关都会被关闭,防止错误的调用)。
因为ShadowSSDT表中包含了SSDT表,所以得到ShadowSSDT表中最多有多少项就得到了所有服务的最大数目。获得服务的最大数目后申请内存存放SERVICE_FILTER_INFO_TABLE表并将最大的服务数存到第一个字段中SSDTCnt
接着根据刚刚获得的各个需要hook的服务例程的索引将对应的代{过}{滤}理函数的地址写到SERVICE_FILTER_INFO_TABLE中代{过}{滤}理函数数组ProxySSDTServiceAddress对应的位置中。
然后就是调用hook_KiFastCallEntry()了,其是通过ssdt hook NtSetEvent函数,然后主动调用zwSetEvent函数并传入特殊的句柄值0x288C58F1,此函数就会调用我们在ssdt hook中安装的MyNtSetEvent。然后在MySetEvent中进行KiFastCallEntry inline hook的安装。
看一下MySetEvent,其会先判断传入的句柄值是否为0x288c58f1,如果不是那就不是我们的调用直接调用原始例程NtSetEvent,如果是证明这是我们自己的调用就进行KiFastCallEntry inlinehook 的安装,
360 hookport的KiFastCallEntry inlinehook位置很好,向8053e621地址处写入一级jmp跳转指令跳到360hookport的二级跳转指令处。 然后二级跳转指令又跳转到KiFastCallEntryFiter入口处。一级地址跳转前eax为服务号,而ebx为从系统服务表中取出的服务例程地址。
那么我们如何在MySetEvent中找到这个inline hook的位置呢,因为我们的MySetEvent是通过call ebx调用的,那么在栈中就一定存在call ebx下一条指令的地址[ebp +4]。我们通过栈回溯找到call ebx下一条指令的地址然后往上进行硬编码匹配,当找到0x02E9C1E12B时表示我们找到了hook 的位置
然后将一级跳转指令写入此处。inline hook安装完成后恢复NtSetEvent 的ssdt hook。返回到hook_KiFastCallEntry()函数中检查刚刚调用MyNtSetEvent()是否完成了KiFastCallEntry 的inline hook,如果没有下面做的和MyNtSetEvent做的差不多,就是进行inline hook KiFastCallEntry。
nt!KiFastCallEntry+0xcc:
8053e60c ff0538f6dfff inc dword ptr ds:[0FFDFF638h]
8053e612 8bf2 mov esi,edx
8053e614 8b5f0c mov ebx,dword ptr [edi+0Ch]
8053e617 33c9 xor ecx,ecx
8053e619 8a0c18 mov cl,byte ptr [eax+ebx]
8053e61c 8b3f mov edi,dword ptr [edi] ;edi为系统服务表基地址
8053e61e 8b1c87 mov ebx,dword ptr [edi+eax*4] ;eax =系统服务号,ebx为对应的系统服务地址
8053e621 2be1 sub esp,ecx
8053e623 c1e902 shr ecx,2
8053e626 8bfc mov edi,esp
8053e628 3b35d4995580 cmp esi,dword ptr [nt!MmUserProbeAddress (805599d4)]
8053e62e 0f83a8010000 jae nt!KiSystemCallExit2+0x9f (8053e7dc)
8053e634 f3a5 rep movs dword ptr es:[edi],dword ptr [esi]
8053e636 ffd3 call ebx ;调用相应的系统服务
8053e638 8be5 mov esp,ebp
接着调用PsSetCreateProcessNotifyRoutine创建了一个进程通知回调,其内不会调用pre_check()并传入0x45过滤函数索引。对应的过滤函数会检查进程创建是否存在问题。
然后会判断win32k.sys模块是否加载,如果没加载就将NtSetSystemInformation的hook开关先打开,如果win32k.sys已经加载了就直接获取csrss.exe进程的PID,然后将进程空间切到csrss.exe所在的进程空间中,然后IAThook win32k.sys 模块中的KeUserModeCallback函数。
这里注意为什么将进程切换到csrss.exe所在的进程空间中再hook_KeUserModeCallback呢,实际没必要非得切换都csrss.exe进程空间中,主要不是在System和smss.exe进程空间中都可以,因为在在System和smss.exe进程空间中win32k.sys其虚拟地址并没有被映射物理内存,访问无效
NTSTATUS
ZwSetSystemInformation (
SYSTEM_INFORMATION_CLASS SystemInformationClass,
PVOID SystemInformation,
ULONG SystemInformationLength);
那么如果win32k.sys没有加载为什么要先打开NtSetSystemInformation的hook开关呢,我们分析一下NtSetSystemInformation对应的代{过}{滤}理函数,发现其会在调用完过滤函数后判断参数SystemInformationClass是否为SystemExtendedServiceTableInformation(0x26),
如果是说明win32k.sys正在加载然后对KeAddSystemServiceTable进行所在的ntoskrnl.exe的EAT_hook。 因为windows系统再加载时运行的第一个用户进程就是smss.exe会话管理器,其会调用NtSetSystemInformation并传入参数SystemExtendedServiceTableInformation(0x26)来加载win32k.sys
随后win32k.sys就会在DriverEntry驱动入口调用KeAddSystemServiceTable函数添加ShadowSSDT表。 HOOK_KeAddSystemServiceTable的原因是判断添加的是否为win32k.sys的ShadowSSDT表,防止攻击者加载自己的ShadowSSDT替换正确的ShadowSSDT表
然后NtSetSystemInformation的代{过}{滤}理函数会继续判断win32k.sys是否已经加载,如果加载了就hook_KeUserModeCallback函数。
那么为什么要hook_KeUserModeCallback函数呢,调用KeUserModeCallback函数时在内核中调用用户层代码的一种手段,例如我们利用全局钩子注入dll,或者是利用输入法入dll,或者是设置鼠标键盘消息记录钩子WH_JOURNALRECORD,都会调用win32k.sys模块的KeUserModeCallback函数
我们将此函数hook了就可以监控这些这些操作。我们看一看hook后的MyKeUserModeCallback,其通过判断apiNumber是否为ClientLoadLibrary,ClientImmLoadLayout或fnHkOPTINLPEVENTMSG来监控这些操作。如果没问题就正常返回。
做完这些之后在设置过滤函数索引与此服务的实际服务索引之间的对应关系数组FilterToServiceIndex[0x65]。例如NtCreateKey的过滤函数索引为0,那么FilterToServiceIndex[0]就等于NtCreateKey在服务表中的索引。
最后最后在调用CmRegisterCallback函数注册注册表回调来检测KiFastCallEntry 的inline hook是否安装成功。在注册表通知回调函数中如果检测到KiFastCallEntry 的inline hook还没有安装就再次进行KiFastCallEntry 的inline hook
驱动向外提供了3个扩展接口:
1.g_port_extension_v1_for_AddRule :向FILTERFUN_RULE_TABLE规则表中添加新的规则(即添加一张新的FILTERFUN_RULE_TABLE表,FILTERFUN_RULE_TABLE的pNextRulTable就指向下一张FILTERFUN_RULE_TABLE表)
2.g_port_extension_v2_register_ssdt_check_handle_callback:设置FILTERFUN_RULE_TABLE中过滤函数以及对应的SERVICE_FILTER_INFO_TABLE表中的SSDT/ShadowSSDThook开关(其会将所有服务索引为1000的服务的hook开关关闭)
3.g_port_extension_v3_register_ruletable_base:设置过滤规则表对应的过滤规则
参考:https://bbs.pediy.com/thread-99460.htm