分析版本:360保险箱v3.0正式版(2010.1.1下载)
分析工具:syser debugger、ida
使用平台:XP sp2虚拟机
 
 
一、原理概述
 
360保险箱并没有直接对SSDT做hook或对系统服务做inline hook,而是将重新设计的、带规则检查的系统服务例程(以下简称代理服务)地址放入一个代理服务表中做统一管理和重新调度,重新调度功能是通过hook系统内核中未导出函数KiFastCallEntry来实现的。
 
我们知道,Win XP的API调用是经sysenter指令进入函数KiFastCallEntry来完成系统调用的,KiFastCallEntry基本流程可以概括为:
A.初始化系统堆栈、
B.保存现场、
C.计算服务例程入口参数、
D.复制用户态堆栈参数、
E.调用服务例程、
F.恢复现场
 
360保险箱选取的inline hook点为流程D处、覆盖了用于计算用户态堆栈参数长度和个数的两条指令,流程C计算完成服务号、服务表基址、服务例程入口地址参数,流程D的hook点指向的代码(以下称hooker)将根据流程C得到的三个参数及其他相关条件来判断是否将当前调用的服务例程入口地址替换成相对应的代理服务地址,如果替换发生,该代理服务会在流程e处被调用,否则,原系统服务例程被调用。
 
代理服务的流程:
1、调用对应的过滤函数做入口参数检查,依据从堆栈复制过来的用户态参数及保险箱相关规则的设置来判定是否非法操作、是否可疑注入、是否拦截或放行等。
2、根据需要确定是否调用原系统服务例程。
3、根据需要确定是否做出口参数检查。
 
 
二、hook安装
 
使用anti rootkit检测工具可以看到360保险箱对系统内核做了如下两处inling hook,并设置了两处系统回调,如图:

图1
 
图1挂钩对象KeUserModeCallback在前文《360保险箱反注入分析》已做过介绍,另一处空白的挂钩对象即为KiFastCallEntry。
根据图中线索可知KiFastCallEntry inline hook及两处入口回调的安装都是在驱动hookport.sys中完成的。(代码来自IDA)
 
1、KiFastCallEntry inline hook安装
 
函数sub_17A02首先使用sub_17852来替换SSDT中的NtSetEvent服务地址,然后调用一次函数ZwSetEvent,该函数执行KiFastCallEntry的call ebx指令时,sub_17852得到调用。
 
.text:00017A02 sub_17A02 proc near
.text:00017A02
.text:00017A02 DestinationString= STRING ptr -18h
.text:00017A02 var_10 = dword ptr -10h
.text:00017A02 var_C = dword ptr -0Ch
.text:00017A02 var_8 = dword ptr -8
.text:00017A02 SpinLock = dword ptr -4
.text:00017A02
.text:00017A02 mov edi, edi
.text:00017A04 push ebp
.text:00017A05 mov ebp, esp
.text:00017A07 sub esp, 18h
.text:00017A0A push ebx
.text:00017A0B push esi
.text:00017A0C push edi
.text:00017A0D push offset aZwsetevent ; "ZwSetEvent"
.text:00017A12 lea eax, [ebp+DestinationString]
.text:00017A15 push eax ; DestinationString
.text:00017A16 call ds:RtlInitAnsiString 
.text:00017A1C lea eax, [ebp+DestinationString]
.text:00017A1F push eax
.text:00017A20 call sub_1655A //得到ZwSetEvent函数地址
.text:00017A25 xor esi, esi
.text:00017A27 cmp eax, esi
.text:00017A29 jz loc_17B5E
.text:00017A2F mov eax, [eax+1] //得到服务号
.text:00017A32 mov dword_18F40, eax //保存服务号
.text:00017A37 lea eax, [ebp+SpinLock]
.text:00017A3A push eax ; SpinLock
.text:00017A3B call ds:KeInitializeSpinLock //初始化自旋锁
.text:00017A41 mov edi, ds:KfAcquireSpinLock
.text:00017A47 lea ecx, [ebp+SpinLock] ; SpinLock
.text:00017A4A call edi ; KfAcquireSpinLock //置自旋锁
.text:00017A4C mov cl, al
.text:00017A4E push eax
.text:00017A4F push ebx
.text:00017A50 push edx
.text:00017A51 cli
.text:00017A52 mov eax, cr0
.text:00017A55 mov [ebp+var_C], eax //存保护状态
.text:00017A58 and eax, 0FFFEFFFFh
.text:00017A5D mov cr0, eax //允许写
.text:00017A60 mov eax, dword_18F40
.text:00017A65 mov edx, dword_18F3C
.text:00017A6B mov edx, [edx]
.text:00017A6D mov ebx, [edx+eax*4] //得到NtSetEvent服务地址
.text:00017A70 mov dword_18F54, ebx //保存NtSetEvent服务地址
.text:00017A76 mov ebx, offset sub_17852
.text:00017A7B mov [edx+eax*4], ebx //替换NtSetEvent服务地址
.text:00017A7E mov eax, [ebp+var_C]
.text:00017A81 mov cr0, eax //恢复保护状态
.text:00017A84 sti
.text:00017A85 pop edx
.text:00017A86 pop ebx
.text:00017A87 pop eax
.text:00017A88 mov dl, cl ; NewIrql
.text:00017A8A lea ecx, [ebp+SpinLock] ; SpinLock
.text:00017A8D call ds:KfReleaseSpinLock //解锁
.text:00017A93 mov eax, 288C58F1h //伪句柄
.text:00017A98 push esi ; PreviousState
.text:00017A99 push eax ; EventHandle
.text:00017A9A mov dword_19064, eax
.text:00017A9F call ds:ZwSetEvent //触发服务sub_17852执行安装过程
。 。
。 。
.text:00017BD9 sub_17A02 endp
 
----------------------------------------------------------------------------
 
sub_17852通过传入的伪句柄来保证安装过程只被执行一次并恢复原系统服务入口地址。KiFastCallEntry的inline hook安装需要覆盖shr ecx,2和mov edi,esp两条指令,定位这两条指令是通过执行进入sub_17852服务后,从堆栈内的函数返回地址做这两条指令的逆向搜索来完成的。
 
.text:00017852 sub_17852 proc near
.text:00017852
.text:00017852 var_8 = dword ptr -8
.text:00017852 SpinLock = dword ptr -4
.text:00017852 arg_0 = dword ptr 8
.text:00017852 arg_4 = dword ptr 0Ch
.text:00017852
.text:00017852 mov edi, edi
.text:00017854 push ebp
.text:00017855 mov ebp, esp
.text:00017857 push ecx
.text:00017858 push ecx
.text:00017859 mov eax, [ebp+arg_0]
.text:0001785C cmp eax, dword_19064 //比较伪句柄
.text:00017862 push ebx
.text:00017863 push esi
.text:00017864 push edi
.text:00017865 jnz loc_17975 //如果不是伪句柄
.text:0001786B call ds:ExGetPreviousMode//来自用户态还是内核心态
.text:00017871 test al, al
.text:00017873 jnz loc_17975 //如果来自用户态
.text:00017879 push 206B6444h ; Tag
.text:0001787E push 5 ; NumberOfBytes
.text:00017880 push 0 ; PoolType
.text:00017882 call ds:ExAllocatePoolWithTag //申请5字节用于中间跳转
.text:00017888 test eax, eax
.text:0001788A mov P, eax
.text:0001788F jz loc_17971
.text:00017895 mov byte ptr [eax], 0E9h //jmp
.text:00017898 mov eax, ds:NtBuildNumber
.text:0001789D cmp word ptr [eax], 1770h //XP or Vista
.text:000178A2 mov eax, P
.text:000178A7 mov ecx, offset byte_1782E //hooker for vista or later
.text:000178AC jge short loc_178B3
.text:000178AE mov ecx, offset byte_1780A //hooker for XP
.text:000178B3
.text:000178B3 loc_178B3:
.text:000178B3 sub ecx, eax
.text:000178B5 sub ecx, 5
.text:000178B8 mov [eax+1], ecx //中间跳转用到的地址
.text:000178BB lea eax, [ebp+SpinLock]
.text:000178BE push eax ; SpinLock
.text:000178BF call ds:KeInitializeSpinLock //初始化自旋锁
.text:000178C5 lea ecx, [ebp+SpinLock] ; SpinLock
.text:000178C8 call ds:KfAcquireSpinLock //置自旋锁
.text:000178CE mov bl, al
.text:000178D0 push eax
.text:000178D1 push ecx
.text:000178D2 push edx
.text:000178D3 push esi
.text:000178D4 push edi
.text:000178D5 cli
.text:000178D6 mov eax, cr0
.text:000178D9 mov [ebp+arg_0], eax //存保护状态
.text:000178DC and eax, 0FFFEFFFFh
.text:000178E1 mov cr0, eax //允许写
.text:000178E4 mov eax, dword_18F3C //KeServiceDescriptorTable
.text:000178E9 mov eax, [eax]
.text:000178EB mov edx, dword_18F40 //NtSetEvent服务号
.text:000178F1 lea eax, [eax+edx*4] 
.text:000178F4 mov edx, dword_18F54 //保存的原NtSetEvent服务地址
.text:000178FA mov [eax], edx //恢复原NtSetEvent服务地址
.text:000178FC mov eax, [ebp+4]
.text:000178FF mov edx, eax
.text:00017901 mov dword_18F4C, edx //保存栈中服务返回地址
.text:00017907
.text:00017907 loc_17907:
.text:00017907 mov edi, eax
.text:00017909 lea esi, unk_18F00 // 即shr ecx,2和mov edi,esp
.text:0001790F mov ecx, 5
.text:00017914 repe cmpsb //比较是否为unk_18F00处的两条指令
.text:00017916 push eax
.text:00017917 setz al
.text:0001791A test al, al
.text:0001791C pop eax
.text:0001791D jnz short loc_1792B //如果搜索到这两条指令
.text:0001791F dec eax //逆向搜索
.text:00017920 push edx
.text:00017921 sub edx, eax
.text:00017923 cmp edx, 64h //搜索100字节
.text:00017926 pop edx
.text:00017927 ja short loc_1795A //如搜索越限
.text:00017929 jmp short loc_17907 //继续搜索
.text:0001792B
.text:0001792B loc_1792B:
.text:0001792B add eax, 5 //hooker要返回的地址
.text:0001792E mov dword_18F50, eax //保存该返回地址
.text:00017933 mov edx, P //中间跳地址
.text:00017939 sub edx, eax
.text:0001793B lea eax, aSrrrr
.text:00017941 mov [eax+1], edx
.text:00017944 mov edi, dword_18F50
.text:0001794A sub edi, 5 //-5后为KiFastCallEntry内的hook点
.text:0001794D lea esi, aSrrrr
.text:00017953 mov ecx, 5
.text:00017958 rep movsb //hook KiFastCallEntry
.text:0001795A
.text:0001795A loc_1795A:
.text:0001795A mov eax, [ebp+arg_0]
.text:0001795D mov cr0, eax //恢复保护状态
.text:00017960 sti
.text:00017961 pop edi
.text:00017962 pop esi
.text:00017963 pop edx
.text:00017964 pop ecx
.text:00017965 pop eax
.text:00017966 mov dl, bl ; NewIrql
.text:00017968 lea ecx, [ebp+SpinLock] ; SpinLock
.text:0001796B call ds:KfReleaseSpinLock //解锁
.text:00017971
.text:00017971 loc_17971:
.text:00017971 xor eax, eax
.text:00017973 jmp short loc_17989
.text:00017975
.text:00017975 loc_17975: //如果不是伪句柄或者来自用户态调用转到这里
.text:00017975 push eax
.text:00017976 push [ebp+arg_4]
.text:00017979 push [ebp+arg_0]
.text:0001797C call dword_18F54 //调用原NtSetEvent服务
.text:00017982 mov [ebp+var_8], eax
.text:00017985 pop eax
.text:00017986 mov eax, [ebp+var_8]
.text:00017989
.text:00017989 loc_17989:
.text:00017989 pop edi
.text:0001798A pop esi
.text:0001798B pop ebx
.text:0001798C leave
.text:0001798D retn 8
.text:0001798D sub_17852 endp
 
2、回调函数安装
 
sub_180FC调用sub_18056完成CmpCallback(sub_17F38)回调函数的安装,该函数监视KiFastCallEntry的hook点(dword_18FA8)指令是否被改变,如有改变调用函数sub_17EE6进行恢复。
.text:000180FC sub_180FC proc near
.text:000180FC
.text:000180FC mov edi, edi
.text:000180FE push ebp
.text:000180FF mov ebp, esp
.text:00018101 sub esp, 0Ch
.text:00018104 cmp dword_18F4C, 0 //是否执行过inline hook安装
.text:0001810B jz short locret_1813A 
.text:0001810D push 0 ; CSDVersion
.text:0001810F lea eax, [ebp+BuildNumber]
.text:00018112 push eax ; BuildNumber
.text:00018113 lea eax, [ebp+MinorVersion]
.text:00018116 push eax ; MinorVersion
.text:00018117 lea eax, [ebp+MajorVersion]
.text:0001811A push eax ; MajorVersion
.text:0001811B call PsGetVersion
.text:00018120 mov eax, dword_18F50
.text:00018125 add eax, 0FFFFFFFBh
.text:00018128 mov dword_18FA8, eax //保存inline hook点地址
.text:0001812D mov eax, [eax+1]
.text:00018130 mov dword_18F98, eax //保存hook点jmp指令的操作数
.text:00018135 call sub_18056 //安装CmpCallback
.text:0001813A
.text:0001813A locret_1813A:
.text:0001813A leave
.text:0001813B retn
.text:0001813B sub_180FC endp
 
函数sub_1832C结尾处调用PsSetCreateProcessNotifyRoutine(NotifyRoutine, 0)完成CreateProcess回调函数安装,该函数在进程创建时做相关规则检查。
 
 
三、流程分析
 
此处通过调试一个注入保护进程的例子,尝试分析保险箱进程保护机制。(代码来自syser debugger)
 
1、KiFastCallEntry分析
 
向保险箱中添加保护对象calc.exe,使用注入器(DLL注入器.rar)向calc.exe中注入一个DLL,当我们单步调试注入器时会发现:WriteProcessMemory函数会导致保险箱弹出提示:“可疑进程企图注入”。
Syser可以方便的从本地r3跟入r0,使用Syser跟入注入器函数WriteProcessMemory --> ntdll!NtWriteProcessMemory  sysenter。。。跟到KiFastCallEntry处:
 
8053E60C FF0538F6DFFF INC [FFDFF638] //调用记数
8053E612 8BF2 MOV ESI,EDX //栈顶指针
8053E614 8B5F0C MOV EBX,[EDI+0C] //指向SSDT
8053E617 33C9 XOR ECX,ECX //
8053E619 8A0C18 MOV CL,[EAX+EBX] //参数长度
8053E61C 8B3F MOV EDI,[EDI] //ServiceTableBase
8053E61E 8B1C87 MOV EBX,[EDI+EAX*4] //EAX序号,EBX服务地址
8053E621 E9D2DA2501 JMP 8179C0F8 //inline hook指向中间跳转
8053E626 8BFC MOV EDI,ESP //复制参数的目标地址
8053E628 3B35D4995580 CMP ESI,MmUserProbeAddress //地址有效检测
8053E62E 0F83A8010000 JNC 8053E7DC //无效转
8053E634 F3A5 REPZ MOVSD //参数复制
8053E636 FFD3 CALL EBX //调用系统服务
8053E638 8BE5 MOV ESP,EBP //恢复堆栈
 
8053E621处就是图1中KiFastCallEntry的inline hook点,被覆盖的原指令为: 
sub esp,ecx //为复制参数开辟空间
shr ecx,2 //参数个数
hook后的指令JMP 8179C0F8指向:
8179C0F8 E90D071E78 JMP F997C80A //jmp hooker
这是个中间跳转,指向保险箱的hookport.sys驱动内的hooker代码;使用中间跳转会导致一些rootkit检测工具无法辨析hook点指向了哪个模块。
 
2、hooker分析
 
继续跟入中间跳进入hooker代码:
F997C80A 8BFF MOV EDI,EDI 
F997C80C 9C PUSHFD
F997C80D 60 PUSHAD
F997C80E 57 PUSH EDI //服务表基址
F997C80F 53 PUSH EBX //服务地址
F997C810 50 PUSH EAX //服务序号
F997C811 E840FFFFFF CALL F997C756 //服务调度函数
F997C816 89442410 MOV [ESP+10],EAX //新的服务地址写回堆栈
F997C81A 61 POPAD
F997C81B 9D POPFD
F997C81C 2BE1 SUB ESP,ECX //补回inline hook处被覆盖的指令
F997C81E C1E902 SHR ECX,02 //补回inline hook处被覆盖的指令
F997C821 FF3550DF97F9 PUSH [F997DF50] 
F997C827 C3 RET //push ret方式跳回KiFastCallEntry
 
hooker的核心是F997C811处的服务调度函数CALL F997C756,该函数根据传入的服务号、服务表基址、服务例程入口地址参数及相关规则来判断返回代理服务例程还是原始服务例程,返回的服务例程地址通过F997C816处的指令:MOV [ESP+10],EAX存入堆栈中,并在KiFastCallEntry的8053E636 CALL EBX处得到调用。
 
F997C811处调用的服务调度函数:
 
F997C756 8BFF MOV EDI,EDI
F997C758 55 PUSH EBP
F997C759 8EBC MOV EBP,ESP
F997C75E 3B052CDF97F9 CMP EAX,[F997DF2C] //是否SSDT基址
F997C764 56 PUSH ESI
F997C765 8B7508 MOV ESI,[EBP+08]
F997C768 7508 JNZ F997C772
F997C76A 3B3528DF97F9 CMP ESI,[F997DF28] //SSDT服务序号是否在范围内
F997C770 7654 JNA F997C7C6
F997C772 3B0524DF97F9 CMP EAX,[F997DF24] //是否SSDTShadow基址
F997C778 7542 JNZ F997C7BC
F997C77A 3B3530DF97F9 CMP ESI,[F997DF30] //SSDTShadow服务序号是否在范围内
F997C780 773A JA F997C7BC
F997C782 A1E4E097F9 MOV EAX,[F997E0E4]
F997C787 83BCB0384E00 CMP [EAX+ESI*4+00004E38],+00 //代理服务表中对应SSDTShadow服务的开关
F997C78F 746B JZF997C7FC
F997C791 33D2 XOR EDX,EDX
F997C793 42 INC EDX
F997C794 8BCE MOV ECX,ESI
F997C796 E8D993FFFF CALL F9975B74 //得到过滤规则表中过滤函数开关(SSDTShadow)
F997C79B 85C0 TEST EAX,EAX
F997C79D 745D JZF997C7FC
F997C79F A1E4E097F9 MOV EAX,[F997E0E4]
F997C7A4 8B4D0C MOV ECX,[EBP+0C]
F997C7A7 898CB04C1F00 MOV [EAX+ESI*4+00001F4C],ECX //保存原SSDTShadow服务地址
F997C7AE A1E4E097F9 MOV EAX,[F997E0E4]
F997C7B3 8B84B0F02E00 MOV EAX,[EAX+ESI*4+00002EF0] //返回SSDTShadow代理服务地址
F997C7BA EB43 JMP F997C7FF
F997C7BC 8B0D3CDF97F9 MOV ECX,ntkrnlpa_KeServiceDescriptor
f997C7C2 3B01 CMP EAX,[ECX]
F997C7C4 7536 JNZ F997C7FC
F997C7C6 A1E4E097F9 MOV EAX,[F997E0E4]
F997C7CB 83BCB0943E00 CMP [EAX+ESI*4+00003E94],+00 //代理服务表中对应SSDT服务的开关
F997C7D3 7427 JZ F997C7FC
F997C7D5 33D2 XOR EDX,EDX
F997C7D7 8BCE MOV EAX,ESI
F997C7D9 E89693FFFF CALL F9975B74 //得到过滤规则表中过滤函数开关(SSDT)
F997C7DE 85C0 TEST EAX,EAX
F997C7E0 741A JZ F997C7FC
F997C7E2 A1E4E097F9 MOV EAX,[F997E0E4]
F997C7E7 8B4D0C MOV ECX,[EBP+0C]
F997C7EA 894CB004 MOV [EAX+ESI*4+04],ECX //保存原SSDT服务地址
F997C7EE A1E4E097F9 MOV EAX,[F997E0E4]
F997C7F3 8B84B0A80F00 MOV EAX,[EAX+ESI*4+0000FA8] //返回SSDT代理服务地址
F997C7FA EB03 JMP F997C7FF
F997C7FC 8B450C MOV EAX,[EBP+0C]
F997C7FF 5E POP ESI
F997F800 5D POP EBP
F997C801 C20C00 RET 000C
 
该服务调度函数本质上是根据代理服务表及过滤规则表中当前系统服务对应的开关来决定返回的是代理服务地址还是原始服务地址。
 
使用IDA的hex_rays分析hookport.sys的代理服务表创建函数(sub_1832c)及过滤规则表创建函数(subb_105AA)可以归纳出该服务调度函数用到的两个数据结构:
 
typedef struct _PROXY_SERVICE_TABLE{ 
DWORD SSDTCount; 
DWORD OrgSSDTServiceAddress[1001]; //保存原始SSDT服务的地址 
DWORD PrxSSDTServiceAddress[1001]; //保存Hook后SSDT代理服务地址 
DWORD OrgShadowSSDTServiceAddress[1001]; //保存原始ShadowSSDT服务地址 
DWORD PrxShadowSSDTServiceAddress[1001]; //保存Hook后ShadowSSDT代理服务地址 
DWORD ReplSwitchForSSDT [1001]; //原始服务是否应被替换的开关(for SSDT)
DWORD ReplSwitchForSSDTShadowSSDT[1001];//同上(for ShadowSSDT)
}SERVICE_EXTEND_TABLE; 
 
 
typedef struct _FILTER_RULE_TABLE{ 
DWORD Size; //自结构尺寸
DWORD Reserve
DWORD IsFilterFunExist; //过滤函数存在 
DWORD FilterFun[0x4B] //过滤函数数组//
PULONG SSDTRuleTableBase; //过滤规则表(for SSDT)
PULONG ShadowSSDTRuleTableBase; //过滤规则表(for ShadowSSDT)
}FILTER_RULE_TABLE;
 
(未完)