Linux内核监控

本文将简单罗列Linux监控的实现方式,安全及审计软件的前提就是能在关键点上接管系统的执行流程,而关键点的接管必须依赖Linux内核的监控能力:

1 LSM HOOK

Linux内核中为发实现其安全机制,已经预留了LSM HOOK埋点,以4.4.131的内核为例,系统共提供了多达198个监控点回调,4.4.131是当前麒麟操作操作系统的内核版本,而较新的5.4.2内核已经支持多达216监控回调:

类别 钩子函数个数 功能
ptrace 2 Ptrace操作的权限检查
能力机制 3 Capabilities相关操作的权限检查
磁盘配额 2 配盘配额管理操作的权限检查
超级块 13 mount/umount/文件系统/超级块相关操作的权限检查
dentry 2 dentry相关操作的权限检查
文件路径 11 文件名/路径相关操作的权限检查
inode 27 inode相关操作的权限检查
file 13 file相关操作的权限检查
程序加载 5 运行程序时的权限检查
进程 27 进程/进程相关操作的权限检查
IPC 21 IPC消息/共享内存/信号量等操作的权限检查
proc文件系统 2 proc文件系统相关操作的权限检查
系统设置 2 日志,时间等相关操作的权限检查
内存管理 1 内存管理相关操作的权限检查
安全属性 7 对安全相关数据结构的操作的权限检查
网络 37 网络相关操作的权限检查
XFRM 11 XFRM架构想要关操作的权限检查
密钥 4 密钥管理相关操作的权限检查
审计 4 内核审计相关操作的权限检查
Android Binder 4 Android Binder架构有关操作的权限检查
总计 198

其中我们最关心的是进程、程序加载、内存及文件相关的监控回调,从数量及功能上已足够强大,但具体能否满足我们的需求,还要在实现过程中进行分析和验证。比如以进程创建为例,Linux内核的进程创建是通过sys_clone(sys_fork/sys_vfork)及sys_execve实现的,但LSM的监控点为了避免多入口的问题,放在了task_alloc这个点上:调用栈如下所示:

backtrace
#0  security_task_alloc (task=0xffff923cad2f8000, clone_flags=4001536) at security/security.c:1472
#1  0xffffffffa0a88ff4 in copy_process (clone_flags=4001536, stack_start=<optimized out>, stack_size=<optimized out>, child_tidptr=<optimized out>, pid=0x0, trace=<optimized out>, tls=<optimized out>, node=-1) at kernel/fork.c:1746
#2  0xffffffffa0a8a59f in copy_process (node=<optimized out>, tls=<optimized out>, trace=<optimized out>, pid=<optimized out>, child_tidptr=<optimized out>, stack_size=<optimized out>, stack_start=<optimized out>, clone_flags=<optimized out>) at kernel/fork.c:1574
#3  _do_fork (clone_flags=4001536, stack_start=<optimized out>, stack_size=<optimized out>, parent_tidptr=<optimized out>, child_tidptr=<optimized out>, tls=<optimized out>) at kernel/fork.c:2056
#4  0xffffffffa0a8a969 in SYSC_clone (tls=<optimized out>, child_tidptr=<optimized out>, parent_tidptr=<optimized out>, newsp=<optimized out>, clone_flags=<optimized out>) at kernel/fork.c:2166
#5  SyS_clone (clone_flags=<optimized out>, newsp=<optimized out>, parent_tidptr=<optimized out>, child_tidptr=<optimized out>, tls=<optimized out>) at kernel/fork.c:2160
#6  0xffffffffa0a03ae3 in do_syscall_64 (regs=0xffff923cad2f8000) at arch/x86/entry/common.c:287
#7  0xffffffffa1400081 in entry_SYSCALL_64 () at arch/x86/entry/entry_64.S:237
#8  0x00007ffc00600600 in ?? ()
#9  0x000056066c0ceec0 in ?? ()
#10 0x0000000000000000 in ?? ()

在进程创建这个点上我们需要取到哪些信息:主进程、子进程、调用栈及相应的符号信息等等,但security_task_alloc这个点不能满足我们的需求,但再利用程序加载(BPRM)的相关回调我们即可将所有的信息串联起来,有了这个信息我们就能够做进详细的检测与判定,以上就是我们解决问题的主要思路。

2 Syscall_Table HOOK

这种方式就类似于 SSDT HOOK,但Linux内核并没有类似于Windows内核的PatchGuard (KPP)的防护机制,目前天擎国产化就是使用的此技术,但在PKS系统上,可信计算以及澜起安全内存对syscall table以及内核镜像都是有安全度量以及内存防改保护。另外为了解决Meltdown,Linux内核引入的KPTI以及ARM64系统上引入的Syscall Table Wrapper (4.19之后)都给Syscall Table HOOK带来挑战

需要注意的事,Linux内核中会存存多个syscall table,比如X64为了支持32位的x86程序会保留32位移程序的syscall table,另外Linux还有一项特殊的syscall tabke专为X64_32程序使用,X64_32类别程序内部直接使用的是64位的syscall id

3 代码Inline HOOK

代码Inline hook做为针对Syscall Table hook的延伸,但同样亦面临可信度量及内存保护的挑战

4 ftrace & kprobe机制

Linux内核为了提升在线解决问题的效率,实现了动态插桩机制,可以在内核”任意函数“点进行插桩,用以输出参数及函数变量,甚至改变函数执行流程。当然这种技术主要是用于排错应急,如果用于生产环境还需要更多的技术验证。这种技术的明显限制是针对同一个函数只能进行一个插桩,并且插桩行为是能够被可信计算发现的,亦可被阻止。

5 中断向量HOOK

X86体系下应用层通过syscall或sysenter切换至内核模式,ARM体系下通过SVC或SWI可以切换至内核,切换模式的指令最终是通过自陷阱(Trap)触使CPU进行模式切换,并自动执行事先指定位置处的代码。我们可以通过接管中断向量的方式实现Syscall的HOOK,但是此方式会有以下难点:

  1. 入口片要处理相当多的初始化工作,只能用汇编级指令实现
  2. 针对开启KPTI的内核,要保证入口代码在Shadow内核空间

6 GhostHOOK

依赖于硬件PMU的HOOK实现方式,Intel及AMD在各自的CPU上均实现了类似的机制,ARM架构上的CP15协处理器亦实现了类似的功能,但相对Intel的PMU在功能上要弱一些,但均可以达成类似的HOOK效果。

当然通过高精度时间中断亦可达成同样效果

7 内核hot patch

ARM64平台可利用 aarch64_insn_patch_text 进行函数层的hook。

Windows进程注入

0 前言

安全软件为了达成进程安全及行为审计的目标经常会采用进程注入的方式,即将自己的DLL注入至用户进程中,以对恶意的注入模块进行对抗。就进程注入方法来说有多种方式,木马及攻击方可使用的手段更多,毕竟攻击者对稳定性及善后部分不用做过多考虑。 本文将对注入方式做一个简单的汇总对比,下面分别从应用层及内核层的不同实现方案进行拆解:

1 应用层注入手段

本章节将主要介绍完全应用层的注入实现。

1.1 远程线程及APC注入、SetThreadContext

这两种方式的实现机制是不同的,但思路是一样的,都需要上传负载(Payload)至要注入的进程空间,一般的操作过程如下:

通过OpenProcess获取进程句柄(HANDLE),然后在目标进程空间申请内存(VirtualAlloc或VirtualAllocEx]),然后调用WriteProcessMemory将shellcode负载写入目标进程空间,最后调用CreateRemoteThread、NtCreateThreadEx、RtlCreateUserThread等创建用户线程,或者添加APC(QueueUserApc)至用户线程以完成shellcode的执行目的。

SetThreadContxt机制是将原线程挂起(SuspendThread),通过修改线程Context中的eip/rip指针至上传的shellcode地址。

Shellcode的设计一般只是简单的LdrLoadDll的调用,复杂的有如DoublePulsar木马所采用的,直接将Payload DLL进行展开并手工加载。

用户层注入的问题是权限受限,另外很容易被检测到,现在的杀软普遍都会特殊关照上述的这些特征函数。

1.2 Win32消息钩子

通过Win32 API SetWindowsHookEx注册系统级消息钩子,以截获同一桌面上所有线程的消息通信,从而实现了DLL模块的注入。当然消息钩子的局限也很明显:

  1. 被注入进程必须接受用户输入,使用了消息队列,如带GUI界面程序;服务进程一般无需用户输入,所以此种注入方法对服务进程无效 64位系统中,64位进程只能设置针对64位程序的消息HOOK,32位进程只针对32位程序,不可交叉混用。和SetWindowsHookEx类似的2. SetWinEventHook 虽然同样可以取到所有进程的消息,但并不能导致DLL注入。
  2. Windows Automation API:Windows Automation API提供给程序访问其它程序窗口及组件(UI elements)的能力,一般用于自动化测试。利用Windows Automation API实现注入的过程,和消息钩子的方式没有本质区别,也有着同样的限制。

    1.3 系统提供机制(注册表选项)

    1) App Init DLLs

    所有加载User32.dll的程序均会自动加载此键值下的DLL,主要针对Win7及之前的Windows版本,从Win8之后此机制不被推荐使用,在UEFI Secure Boot模式下此项被默认关闭。

所在注册表位置:

  • HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Windows
  • HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\Windows NT\CurrentVersion\Windows\AppInit_DLLs

使用此方式的木马及病毒:GinwuiCherry PickerT9000

2) App Cert DLLs

所有调用下面Win32 API的程序均会自动加载上述注册表中所列的DLL文件:

  • CreateProcess
  • CreateProcessAsUser
  • CreateProcessWithLoginW
  • CreateProcessWithTokenW
  • WinExec

所在注册表位置:

  • HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager

使用此方式的木马及病毒:HoneybeeFIN8 PUNCHBUGGY

3) Image File Execution Options

这是Windows系统提供的一个调试辅助机制,用以在特定进程启动或退出时启动指定程序(如调试器等)。用户通过更改IFEO键值达成启动不同进程的目的,从而导致原进程加载请求失败。

所在注册表位置:

  • HKLM\SOFTWARE{\Wow6432Node}\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\
  • HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\SilentProcessExit\

4) Shim Database (SDB)攻击

Windows系统通过Shim数据库(SDB文件,位于%windir%\AppPatch\sysmain.sdb)以提升应用程序的向后兼容(backward compatibility)。SDB数据库中包含针对上千个程序的上百种配置,只要有管理员权限就可操纵此数据库,就可以修改任意程序的各种属性,如以下多种属性:

  • InjectDll
  • LoadLibraryRedirectFlag
  • ForceAdminAccess
  • RelaunchElevated
  • WrpMitigation
  • DisableNX
  • ModifyShellLinkPath
  • VirtualRegistry
  • DisableAdvancedRPCClientHardening
  • CorrectFilePaths
  • DisableSeh
  • DisableWindowsDefender
  • ShellExecuteXP

涉及注册表项:

  • HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\AppCompatFlags\Custom
  • HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\AppCompatFlags\InstalledSDB

使用此方式的木马及病毒:BlackEnergy, GooKit, Roaming Tiger,QianSet.exe,VzQqgi.dll木马

1.4 PE程序IAT表静态修改

早期的Win32病毒常采用的就是静态感染方式,会将自身的代码写入被被感染程序,然后修改PE入口函数至病毒的代码段;同样的方式,静态修改PE文件的导入表以增加新的DLL注入亦不是难事。

针对有签名的PE程序,类似的修改会导致签名校验失败。

1.5 DLL替换

创建一个假个但导出一模一样的DLL文件用以替换原系统的,替换办法一般有两种办法:

  1. 系统原DLL文件改名,并替换系统DLL
  2. 更改DLL搜索路径(SetDLLDirectory)

这种方式要处理的问题:

  1. 系统模块签名问题无法处理:PPL进程将无法运行
  2. 不同版本的系统DLL处理堆积

2 内核层注入手段

内核R0层注入要比应用层R3的手段少了很多,实现难度会大一些,但内核实现更加隐蔽,难以被屏蔽。通过创建用户层线程或插入APC以实现注入的方式被普遍使用,原理上同R3的远程线程及APC注入的思路基本一致,均需要向目标进程空间上载参数及shellcode代码,只是R0及R3各自使用的支持函数不同。

2.1 IAT表注入

在进程创建过程中,内核驱动通过PsSetCreateProcessNotifyRoutine或PsSetCreateProcessNotifyRoutineEx得到通知,此时用户进程的创建过程是被阻塞的,在处理通知的回调中,内核驱动可以修改进程的内存镜像中的导入表,将要注入的驱动加入其中。

以wermgr.exe进程(PID: 0x6bc)的创建为例,此进程由services.exe(PID: 0x2bc)创建: Windows注入-IAT表 后续会收到模块加载的通知回调,加载模块的顺序依次为wermgr.exe自身、ntdll.dll、kernel32.dll、KernelBase.dll、msvcrt.dll等,回调时的调用栈为: Windows IAT注入 导入表修改动作是在进程镜像本身的模块加载回调中执行的,即程序本身模块加载的时机。

IAT表注入的主要问题:

  1. .NET程序的兼容性问题
  2. 托管代码与非托管DLL代码的混合
  3. 64位.NET程序没有导入表项
  4. 受保护进程的注入问题,必须要通过NTDLL HOOK来解决,因为创建Section对象时内核会验证DLL签名等,在IAT已注入的情况下签名又无法签证通过时会导致程序加载失败

2.2 Shellcode注入

创建用户线程及插入APC的注入手段均需要上载shellcode代码至目标进程,shellcode可以只是简单的LdrLoadDll调用用以加载HOOK引擎及工作模块,复杂一些的话可以在内核层将DLL手工加载至目标进程空间,正如木马DoublePulsar所实现的加载器【D23】"Generic Relective DLL Loader"或者内核层Turla Driver Loader(TDL)驱动加载器【D18】。

常用shellcode/payload构造(BlackBone的实现):

// shellcode for X64
UCHAR code[] =
{
0x48, 0x83, 0xEC, 0x28,             // sub rsp, 0x28
0x48, 0x31, 0xC9,                   // xor rcx, rcx
0x48, 0x31, 0xD2,                   // xor rdx, rdx
0x49, 0xB8, 0, 0, 0, 0, 0, 0, 0, 0, // mov r8, ModuleFileName   offset+12
0x49, 0xB9, 0, 0, 0, 0, 0, 0, 0, 0, // mov r9, ModuleHandle     offset+28
0x48, 0xB8, 0, 0, 0, 0, 0, 0, 0, 0, // mov rax, LdrLoadDll      offset+32
0xFF, 0xD0,                         // call rax
0x48, 0xBA, 0, 0, 0, 0, 0, 0, 0, 0, // mov rdx, COMPLETE_OFFSET offset+44
0xC7, 0x02, 0x7E, 0x1E, 0x37, 0xC0, // mov [rdx], CALL_COMPLETE
0x48, 0x83, 0xC4, 0x28,             // add rsp, 0x28
0xC3                                // ret
};

// shellcode for X86
UCHAR code[] =
{
0x68, 0, 0, 0, 0,                   // push ModuleHandle        offset+01
0x68, 0, 0, 0, 0,                   // push ModuleFileName      offset+06
0x6A, 0,                            // push Flags
0x6A, 0,                            // push PathToFile
0xE8, 0, 0, 0, 0,                   // call LdrLoadDll          offset+15
0xBA, 0, 0, 0, 0,                   // mov edx, COMPLETE_OFFSET offset+20
0xC7, 0x02, 0x7E, 0x1E, 0x37, 0xC0, // mov [edx], CALL_COMPLETE
0xC2, 0x04, 0x00                    // ret 4
};

1) 创建用户线程

创建用户线程一般通过NtCreateThreadEx(Visata及以后OS)或NtCreateThread(XP),这两个函数在内核中均未导出且是未公开的,其地址获取可以通过SSDT或者ntoskrnl镜像解析完成。

NTKERNELAPI NTSTATUS NTAPI
NtCreateThread(
__out PHANDLE ThreadHandle,
__in ACCESS_MASK DesiredAccess,
__in_opt POBJECT_ATTRIBUTES ObjectAttributes,
__in HANDLE ProcessHandle,
__out PCLIENT_ID ClientId,
__in PCONTEXT ThreadContext,
__in PINITIAL_TEB InitialTeb,
__in BOOLEAN CreateSuspended
);

NTKERNELAPI NTSTATUS NTAPI
NtCreateThreadEx(
OUT PHANDLE hThread,
IN ACCESS_MASK DesiredAccess,
IN PVOID ObjectAttributes,
IN HANDLE ProcessHandle,
IN PVOID lpStartAddress,
IN PVOID lpParameter,
IN ULONG Flags,
IN SIZE_T StackZeroBits,
IN SIZE_T SizeOfStackCommit,
IN SIZE_T SizeOfStackReserve,
OUT PVOID lpBytesBuffer
);

2) APC注入

在内核中构造一个APC结构并添加至用户线程的APC队列,等条件满足时系统会执行APC中设定的callback,从而达成进程DLL注入的目的。

相关函数原型如下:

NTKERNELAPI VOID NTAPI
KeInitializeApc (
__out PRKAPC    Apc,
__in PRKTHREAD  Thread,
__in KAPC_ENVIRONMENT   Environment,
__in PKKERNEL_ROUTINE   KernelRoutine,
__in_opt PKRUNDOWN_ROUTINE  RundownRoutine,
__in_opt PKNORMAL_ROUTINE   NormalRoutine,
__in_opt KPROCESSOR_MODE    ProcessorMode,
__in_opt PVOID  NormalContext
);

NTKERNELAPI BOOLEAN NTAPI
KeInsertQueueApc    (
__inout PRKAPC  Apc,
__in_opt PVOID  SystemArgument1,
__in_opt PVOID  SystemArgument2,
__in KPRIORITY  Increment
);

360安全卫士的驱动360fsflt.sys、木马DoublePulsar【D22】等所采用的方式就是Kernel APC注入,这种方式被普遍使用。但APC注入要注意以下问题:

  1. 时机问题:要求所注入进程已经创建了线程并且已经执行(主线程都是紧随进程创建的)
  2. 依赖线程状态:APC的执行依赖于所注入线程的状态:一般是从内核态切换回用户态时执行
  3. 必须与注入线程做同步,或者在所注入线程环境中插入APC,不然可能会注入至错误进程,另外与第三方驱动同时注入APC存在竞争及一致性问题,当然竞争窗口极小; 每个线程均有两个APC队列,当线程在内核态下调用KeAttachProcess时会做切换,切换过程中没有任何保护

3) HOOK NTDLL方式

内核驱动可以通过内核的回调机制截获用户进程创建及PE加载器加载并初始化DLL链的每个环节,可以监控到指定模块加载时同步进行inline hook,并将关注点的执行流程导向至已上载的shellcode负载。

回调本身是串行在程序的加载流程中的,即回调不返回,进程的创建及DLL的加载过程是被阻塞的,省却了同步与不一致性的问题的处理。

之所以要选择ntdll.dll的原因是,ntdll.dll是所有的Win32程序必须加载的,并且其执行过程比程序本身的入口执行要早;另外选择ntdll.dll而不是Kernel32.dll等模块的原因是,Kernel32.dll等模块并不是必须的,比如Native程序如csrss,exe、autochk.exe等,还有一些程序将Kernel32.dll等模块设置为Delay-Loading-DLL,并不会在程序启动之初就立即加载并执行。

为了尽早地获取控制权,常用的HOOK点一般放在ntdll!LdrInitializeThunk、ntdll!NtOpenDirectoryObject或者ntdll!LdrLoadDLL等关键函数点。Haiheiwang木马【D16】就是通过修改ntdll!NtTestAlert的流程将自己的工作模块加入被感染进程的,当然ntdll!NtTestAlert也存在时机较晚的问题,并不能满足咱们的需求。

这种方式在安全软件中也广泛使用,特别是对针对Win8系统年引入的受保护进程的注入问题,由内核注入Shellcode并通过HOOK ntdll!NtCreateSection来加载非系统模块是最实用且有效方式,目前360安全卫士的注入亦是采用此方式。受保护PPL进程只对用户层的Section对象创建有签名验证,我们通过将内核层创建Section对象映射至用户空间的方式达成向受保护PPL进程注入的目的。

其它的替代方案会导致受保护进行安全性的妥协等,比如短暂取消受保护进行的保护状态以达成注入目的,此种方式有可能会触发KPP/PG检测失效,另外也很容易被第三方恶意利用,比如MalwareFox AntiMalware的一个被曝光的漏洞【D24: MalwareFox AntiMalware 2.74.0.150 LPE】。

Shellcode内存映射可以在HOOK引擎模块加载之后进行销毁,存在窗口时间非常短,被其它安全软件查到或被其它恶意程序所利用的可能比较小。

3 参考资料

3.1 浏览器自身防护资讯

B1: About Google Chrome's incompatible applications warning

B2: Firefox will block DLL Injections

B3: Protecting Microsoft Edge against binary injection

3.2 进程DLL注入资料

D1: DLL Injection with SetThreadContext

D2: R3 DLL Injection: Inject All The Things

D3: COUNTERCEPT: Analyzing the DOUBLEPULSAR Kernel DLL Injection Technique

D4: COUNTERCEPT: Dynamic Shellcode Execution

D5: Ten Process Injection Techniques

D6: DLL Injector via Windows Automation API

D7: Microsoft: AppInit DLLs and Secure Boot

D8: BlackHat: Malicious Application Compatibility Shims

D9: To SDB, Or Not To SDB: FIN7 Leveraging Shim Databases for Persistence

D10: FreeBuf: 走近微软安全技术Shim

D11: ABICC: Windows API/ABI Changes Analysis

D12: MITRE ATT&CK: Application Shimming

D13: FireEye: The Real Shim Shady

D14: iSIGHTPARTNERS: 固守: 微软Fix It补丁机制原理及攻击利用

D15: 腾讯:木马牟利再出新招-恶意利用Windows shim技术锁主页

D16: 腾讯:Haiheiwang木马分析

D17: Windows Vista APC Internals

D18: TDL: Turla Driver Loader

D19: Scylla and API Set Map

D20: Microsoft Docs: Windows API Sets

D21: Windows_7_Kernel_Changes: api-ms-win-core DLLs

D22: DOUBLEPULSAR Kernel DLL Injection Technique

D23: DOUBLEPULSAR: Generic Reflective DLL Loader

D24: MalwareFox AntiMalware 2.74.0.150 - LPE

D25:DLL Injection via SetDLLDirectory

D26: CodeProject: Create your Proxy DLLs automatically

D27: Proxy WS2_32.DLL to create your own firewall

D28: CodeProject: API hooking revealed

3.3 DLL注入商业方案

D30: Shellter Pro: DLL Injection Kit

D31: madCodeHook: DLL Injection Kit

3.4 开源HOOK引擎

H1: Microsoft Detours

H2: Detours NT

H3: minhook

H4: mhook

H5: EasyHook

H6: Deviare Hook Engine

3.5 Instrumentation Callback

I1: 利用KPROCESS结构的InstrumentationCallback域实现Hook

I2: Hooking-via-InstrumentationCallback

I3: Alex Ionescu: Hooking Nirvana

3.6 PPL (Protected Process Light)

P1: Injecting Code into Windows Protected Processes using COM - Part 1

P2: Injecting Code into Windows Protected Processes using COM - Part 2

P3: The Evolution of Protected Processes Part 1: Pass-the-Hash Mitigations in Windows 8.1

P4: The Evolution of Protected Processes Part 2: Exploit/Jailbreak Mitigations, Unkillable Processes and Protected Services

P5: Protected Processes Part 3 : Windows PKI Internals (Signing Levels, Scenarios, Root Keys, EKUs & Runtime Signers)

P6: Protected Processes Light killer

P7: Why Protected Processes Are A Bad Idea

P8: Micorosft: Protecting Anti-Malware Services

P9: Microsoft: Early launch antimalware

P10: Microsoft Virus Initiative

P11: Early Launch Anti-Malware Driver

3.7 WSL (Windows Subsystem for Linux)

W1: The Linux kernel hidden inside Windows 10

W2: Fun with the Windows Subsystem for Linux (WSL/LXSS)

W3: Hunting for Windows Subsystem for Linux

W4: Exploring Windows Subsystem for Linux

W5: Windows 10 and the Anti-malware Ecosystem

W6: Pico Process Overview

W7: WSL System Calls

--- END ---

由Hook引起的XP Explorer崩溃

有用户反应XP系统上一拖动桌面上的图标即会导致桌面重启,验证后,证实和360安全的升级有关,360安全9.0及后续版本和我们的软件有冲突。

360安全本身太激进,作为一款安全及防毒软件来说,激进并不是什么好事,这也是我不喜欢用360安全软件的原因之一。但是在事情未查出原因之前,不能随便怪罪360,毕竟在二者不共存时,均不会有桌面崩溃的事情发生。

先通过排除法,发现冲突发生在Hook上,而且是在特定的响应拖拽事件的API的Hook上:

    ole32.dll: DoDragDrop

问题定位之后照理应该很直接了,但分析新的DoDragDrop的实现及相关代码,并没有发现明显问题。

我们的代码在一个DLL文件中,并根据需要注入被Hook的进程,此案中便是Explorer.exe。调试起来稍有些周折,但也不算太麻烦。对此类的应用层调试问题,用VS (Visual Studio)远程调试最方便。远程attach到目标XP系统上的explorer.exe进程,然后触发崩溃事件,VS直接响应,显示问题出在一条很正常的压栈(push)操作上,再查看堆栈指针,果然已经耗尽,esp所指内存为非法地址(空页)。

初步揣测可能是堆栈不够所致,explorer.exe默认的size of stack reserve是0x400000,即4M字节。为了验证是不是堆栈的问题,直接将修改explorer.exe的PE文件头,将reserved stack size改成了64M(0x4000000)。

再测试时,explorer.exe在拖动桌面图标时还是会崩溃,只是会等上一会,不像之前一拖动便立即崩溃,看来问题并不是堆叠不够所致。

通过VS中断,发现Call stack中全是iNetSafe.dll (360安全的一个模块),由此断定出现了嵌套调用,死循环。

后面继续跟踪代码的调用过程发现了这样一个有趣的事,先将相关代码列出来:

ole32.dll地址空间 【774E0000 - 7761E000】:
_DoDragDrop@16:
775D0DC0 E9 0B DA 13 EE       jmp         6570E7D0
775D0DC5 83 EC 4C                sub         esp,4Ch
。。。。。。

iNetSafe.dll的地址空间 【65700000 - 65737000】:
6570E7D0 E9 AB 3A 1E 9B       jmp         CDragDropObj::NewDoDrapDrop (8F2280h) 
6570E7D5 57                           push        edi 
。。。。。。
6570E7F5 8B 4C 24 18          mov         ecx,dword ptr [esp+18h] 
6570E7F9 8B 54 24 14          mov         edx,dword ptr [esp+14h] 
6570E7FD 51                         push        ecx 
6570E7FE 8B 4C 24 14          mov         ecx,dword ptr [esp+14h] 
6570E802 52                   push        edx 
6570E803 51                   push        ecx 
6570E804 56                   push        esi 
6570E805 FF D0                call        eax                     [eax: 6FFE00A0]
6570E807 5F                   pop         edi 
6570E808 5E                   pop         esi 
6570E809 C2 10 00             ret         10h 
6570E80C 8B C7                mov         eax,edi 
6570E80E 5F                   pop         edi 
6570E80F 5E                   pop         esi 
6570E810 C2 10 00             ret         10h

360 iNetSafe.dll 所维护的Hook地址信息:
6FFE00A0 E9 DB 21 91 90       jmp         CDragDropObj::NewDoDrapDrop (8F2280h) 
6FFE00A5 E9 1B 0D 5F 07       jmp         _DoDragDrop@16+5 (775D0DC5h) 

MyHook32.dll地址空间 【008F0000 - 00C5F000】:
CDragDropObj::NewDoDrapDrop:
008F2280 55                   push        ebp 
008F2281 8B EC                mov         ebp,esp 
008F2283 8B 0D 38 70 C1 00    mov         ecx,dword ptr […::m_OldDoDragDrop 0C17038h] 
                                                                  0C17038h:0EC9002C  ECX = 0EC9002C 
008F2289 33 C0                xor         eax,eax 
008F228B 85 C9                test        ecx,ecx 
008F228D 74 03                je          CDragDropObj::NewDoDrapDrop+12h (8F2292h) 
008F228F 5D                   pop         ebp 
008F2290 FF E1                jmp         ecx                              [ecx = 0EC9002C]
008F2292 5D                   pop         ebp 
008F2293 C2 10 00             ret         10h 

MyHook32.dll: 原地址及代码
0EC9002C 56                         push        esi 
0EC9002D 8B 74 24 08          mov         esi,dword ptr [esp+8] 
0EC90031 E9 9F E7 A7 56      jmp         6570E7D5 

流程分析:

当Explorer调用_DoDragDrop()函数时,将跳转到iNetSafe的DoDragDrop实现,地址为6570E7D0。iNetSafe的DoDragDrop函数又被MyHook32 Hook了,所以还要继续跳至地址008F2280。MyHook32执行完还会跳转至被Hook前的_DoDragDrop函数,地址存放于0EC90032处,跳转地址为:6570E7D5,即iNetSafe的DoDragDrop函数入口加上5个字节的偏移。同理,iNetSafe的DoDragDrop函数也要调用它所Hook的原DoDragDrop地址,此地址的跳转指令存放于6FFE00A0处。但奇怪的是,位置6FFE00A0所存放的地址竟指向MyHoo32的
NewDoDrapDrop(即8F2280),所以任何对_DoDragDrop的调用,将会陷入一个嵌套的死循环里再也出不来了,直到堆栈被耗尽。

从上面代码上来看,iNetSafe在前,MyHook32在后,所以iNetSafe于6FFE00A0中所保存的原_DoDragDrop地址不应该是MyHook32的,而应该是ole32的_DoDragDrop才是。

但根据Explorer.exe进程的加载模块表,MyHook32.dll的加载order为40,而iNetSafe则为103,明显是MyHook32.dll先被加载,所以Hook的顺序明显违背上面的分析。

至此,方想到问题可能出在MyHook32的Anti-UnHook检查机制上。MyHook32在一个线程里面会实时检测被Hook的函数入口代码,如果不是MyHook32自己的,则会再次尝试Hook,因为360的Hook,便导致了MyHook32的再Hook,所以结果便导致了嵌套Hook的乌龙。

问题已经明确,并不是360的问题,而是出在我们自己的程序上,实在不应该先入为主的怪罪360,尽管其中还是有不少感情因素。

调试终于结束,剩下的的事情便是思考出一个万全的Anti-UnHook机制了。

下图为Explorer.exe所加载的模块(截屏自VS2010):

HookModules-01HookModules-02HookModules-03

注:有几个DLL的图标上均有红叹号,表示是此DLL被加载到的虚拟地址空间并不是此DLL所指定的地址空间。如MyHook32.dll(此版本为DEBUG版本,故体积及占用的地址范围也比较大)其默认的虚拟地址为10000000 - 1036EFFF,而实际上却被加载至008F0000 - 00C5F000。对于DLL文件的加载及从Vista之后所支持的ASLR(Address Space Layout Randomization),以后有机会再单独介绍。