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系统通用回调

Windows系统通用回调 本文将针对Windows操作系统所提供系统回调操作做逐一分析,以了解每个回调所能达成什么样的功能及效果。比如部分回调只是用于通知,我们只能通过此回调来了解某一类型事件的发生,只能做审计,拦截动作需要通过其它途径实现;也有回调可以实现即时拦截,通过回调即可阻止恶意程序的攻击企图。 Windows系统在内核及应用层均有提供回调机制,下面分为两部分作介绍:

1 内核回调

内核回调机制 OS版本 功能描述 备注
PsSetCreateProcessNotifyRoutine WIN2K 只是进程创建及退出通知
PsSetCreateProcessNotifyRoutineEx VISTA SP1/SRV2008 可通过返回值阻止进程创建
PsSetCreateProcessNotifyRoutineEx2 WIN10 1703 可接收到WSL/PICO进程创建及退出通知;可阻止进程创建
PsSetCreateThreadNotifyRoutine (PsRemoveCreateThreadNotifyRoutine) WIN2K 线程创建及退出通知 创建时的context为创建者,可用以判断是不是远程线程
PsSetCreateThreadNotifyRoutineEx (PsRemoveCreateThreadNotifyRoutine) WIN10 线程创建及退出通知 创建时的context为新线程环境
PsSetLoadImageNotifyRoutine (PsRemoveLoadImageNotifyRoutine) WIN2K 模块加载通知:支持驱动及应用层DLL加载通知 无模块卸载通知
PsSetLoadImageNotifyRoutineEx (PsRemoveLoadImageNotifyRoutine) WIN10 1709 模块加载通知:支持驱动及应用层DLL加载通知 无模块卸载通知
IoRegisterBootDriverCallback (IoUnRegisterBootDriverCallback) WIN8 Boot-Start驱动加载通知,可拦截,但拦截的后果即BSOD 加载顺序问题;ELAM驱动最早加载,需要特殊签名
SeRegisterImageVerificationCallback (SeUnregisterImageVerificationCallback) WIN8.1 ? 驱动加载回调,可以验证驱动签名信息 函数未公开,目前Windows Defender会使用
ObRegisterCallbacks (ObUnRegisterCallbacks) VISTA SP1 SRV2008 针对进程及线程对象句柄的创建提供前置及后置回调,可修改句柄权限,如针对进程对象无VM映射权限。子进程继承句柄不会通知 WIN7系统可监控文件句柄的创建,需要修改未公开的系统结构(ObjectType->SupportsObjectCallbacks),WIN8后PG会监控。WIN10系统增加了桌面对象的监控支持
CmRegisterCallback (CmUnRegisterCallback) XP 注册表操作回调,可修改、转向或拦截原访问请求 XP/SRV2003/VISTA行为有差异
CmRegisterCallbackEx (CmUnRegisterCallback) VISTA 注册表操作回调,可修改、转向或拦截原访问请求
ExRegisterCallback
-- \Callback\SetSystemTime WIN2K 系统时钟修改回调
-- \Callback\PowerState WIN2K 电源状态改变回调(AC/DC,电源策略,待机或关机) 早于Power Manager的IRP_SET_POWER请求
-- \Callback\ProcessorAdd VISTA 动态增加新CPU事件通知
KeRegisterBoundCallback (KeDeregisterBoundCallback) WIN10 用以捕获应用层边界检测异常,可用于应用层HOOK
Standard Event Objects for VM : 所有 主要用于监控内存状态
-- \KernelObjects\HighMemoryCondition
-- \KernelObjects\LowMemoryCondition
-- \KernelObjects\HighPagedPoolCondition
-- \KernelObjects\LowPagedPoolCondition
-- \KernelObjects\HighNonPagedPoolCondition
-- \KernelObjects\LowNonPagedPoolCondition
-- \KernelObjects\LowCommitCondition
-- \KernelObjects\HighCommitCondition
-- \KernelObjects\MaximumCommitCondition
KeRegisterBugCheckCallback (KeDeregisterBugCheckCallback) WIN2K 系统出错准备蓝屏时的回调通知
KeRegisterBugCheckReasonCallback (KeDeregisterBugCheckReasonCallback) XP SP1 SRV2003 蓝屏后DUMP生成通知,可写入额外1-PAGE信息
FsRtlRegisterFileSystemFilterCallbacks XP 文件系统或过滤驱动回调,一般由I/o Manager,VM及CM发起
IoRegisterFsRegistrationChange (IoUnregisterFsRegistrationChange) 所有 文件系统注册事件通知回调 WIN2K: 已注册文件系统驱动不再通知;XP: 均会通知
IoRegisterFsRegistrationChangeEx (IoUnregisterFsRegistrationChange) WIN2K SP4 文件系统注册事件通知回调,忽略RAW设备 同IoRegisterFsRegistrationChange
IoRegisterFsRegistrationChangeMountAware (IoUnregisterFsRegistrationChange) WIN7 文件系统注册事件通知回调,忽略RAW设备 会额外处理与挂载/Mount的竞争问题
TmEnableCallbacks VISTA 文件事务操作回调
IoRegisterContainerNotification (IoUnregisterContainerNotification) WIN7 会话/Session改变通知,如新用户登录或登出,功能类似于用户层的WTSRegisterSessionNotification
KeRegisterProcessorChangeCallback (KeDeregisterProcessorChangeCallback) WIN2K 动态CPU添加事件回调 有Pre和Post处理
KeRegisterNmiCallback (KeDeregisterNmiCallback) SRV2003 发生不可屏蔽中断时的回调通知
IoRegisterShutdownNotification (IoUnregisterShutdownNotification) WIN2K IRP_MJ_SHUTDOWN请求回调
IoRegisterLastChanceShutdownNotification (IoUnregisterShutdownNotification) WIN2K IRP_MJ_SHUTDOWN请求回调 调用时机晚,在文件系统处理完毕之后
PoRegisterPowerSettingCallback (PoUnregisterPowerSettingCallback) VISTA 电源管理事件响应回调
IoRegisterPlugPlayNotification (IoUnregisterPlugPlayNotification[Ex]) WIN2K PNP事件回调(设备热插拔)
IoWMISetNotificationCallback XP WMI事件回调
DbgSetDebugPrintCallback 未公开 VISTA 捕获内核打印事件(DbgPrint)
IoRegisterPriorityCallback (IoUnregisterPriorityCallback) 未公开 WIN7 SP1 ? I/O 请求优先级调整
PoRegisterCoalescingCallback (PoUnregisterCoalescingCallback) 未公开 WIN8.1 CM和文件系统相关:在系统空闲的时候实施cache的聚合刷新
PsSetLegoNotifyRoutine 未公开 XP 类似TLS,可以实现线程终结时的回调通知

2 应用层回调

应用层回调 OS 功能 备注
LdrRegisterDllNotification (LdrUnregisterDllNotification) VISTA 可接收进程内DLL模块加载及卸载事件,不可拦截
ProcessInstrumentationCallback WIN7 可在程序每次Ring 0至3切换后收到通知,在系统服务调用结束时刻 需要进程注入后主动调用NtSetInformationProcess,需要SeDebugPrivilege等
RegisterServiceCtrlHandler(Ex) XP 在应用层服务中接收系统事件:设备热插拔、用户登录、电源管理等
RegisterDeviceNotification 所有 GUI程序获取硬件改变通知
RegNotifyChangeKeyValue WIN2K 监控注册表指定键的子键及值的改变
ReadDirectoryChange 所有 监控指定目录中的文件及子目录的变化
AddSecureMemoryCacheCallback (RemoveSecureMemoryCacheCallback) VISTA SP1 可监控Secure类型的共享内存的释放
LdrSetDllManifestProber 未公开 XP 可收到DLL模块加载事件,可用于拦截
LdrInitShimEngineDynamic 未公开 WIN7 可接收Shim Engine通知,可接管DLL加载等 调用时机有要求,需要R3 HOOK; 不同Windows版本如WIN7及8.1差别较大
RtlSetThreadPoolStartFunc 未公开 WIN2K 设置线程初始化及退出函数,kernel32内部使用

3 通用回调的限制

Windows系统虽然提供了回调接口,但因为性能等各种因素的原因还会限定回调的数量:

  • CmRegisterCallback限制为最多100个,无高度号设置,后注册的回调可能收不到请求
  • PsSetLoadImageNotifyRoutine限制为最多8个,64位系统共64个
  • PsSetCreateProcessNotifyRoutine限制为最多8个
  • PsSetCreateThreadNotifyRoutine限制为最多8个 不少驱动安装为了防止注册失败的情况选择了尽早启动并完成相关回调的注册,也有一些工具如PcHunter会通过DKOM方式进行动态扩展。 另外不同的OS版本的支持限制,在上表中也有说明,如Vista之后的Windows系统才开始支持注册表事务操作,这部分在使用中也要注意,Ob Callback也有类似的问题。

4 参考链接

  1. Magnesium Object Manager Sandbox, A More Effective Sandbox Method for Windows 7
  2. MSDN: Filtering Registry Calls
  3. Filter Manager Support for Minifilter Drivers
  4. MSDN: REG_NOTIFY_CLASS enumeration
  5. CodeMachine: Kernel Callback Functions
  6. OSR: Kernel Mode Extensions Driver
  7. WDK Samples: Registry Logging
  8. https://blog.csdn.net/whatday/article/details/52623878
  9. https://docs.microsoft.com/en-us/windows-hardware/drivers/kernel/handling-notifications

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 ---

1394 is back !

从XP时代开始就一直在用1394 Kernel Debugging,虽然Windows相继增加支持了USB2、USB3或者网络调试,但1394因其方便及更好的适用性一直是我的最爱,这也是为什么没有将我的工作笔记本升级到超级本的原因。

但从Windows 10发布以后,微软不再提供Kernel Debugging over 1394,但好在有相应的Workaround延长了1394的寿命,参见 KD 1394 Work-Around。但此办法无法解决boot debugging的问题,好在我目前都是系统级的内核调试,用不着boot debugging。

只是好景不长,随着Win10版本升级,1394又不行了。曾经为了测试新版Win10的兼容性,装了Windows 10 Pro Insider Preview 16226,结果发现Kernel Debugging over 1394彻底罢工,试过几个版本的Kd1394.dll均不行。

本以为1394就此死翘翘的时候,看到M$发布的一则消息:New WinDbg available in preview!。安装只能通过Windows Store,安装后习惯性的检查了一下文件目录,发现了新版的Kd1394.dll,随即在Win10 Pro 16226系统上做替换测试,然后 bing bing bing ! Windbg host端不断吐出字符来!

后来在Win10 Pro 15063上做了验证,发现此版Kd1394.dll并不适用,但老版的Kd1394.dll还是可以正常适配Win10 15063系统的。作为一个重度1394使用者,又可以欢呼了,但此好景能延续到什么时候呢?

Win10升级导致的休眠故障

一切都是因为手贱,看到Win10的升级引入了不少新的特性,心痒就赶了个时髦。升级过程花了不短的时间,但总体顺利,使用上也没有发现异常,也没有发现改进,比如Hyper-V照常和Vmware Workstation冲突。

下班后将电脑休眠从公司带回家,但等到再开机却发现系统重启了,日志中没有看到明显异常。之后就发现休眠时不时就会出问题,不是休眠中当机,就是休眠后无法正常开机。

用驱动大师升级了所有能升级的驱动,问但题还是没有解决,只得回退系统,但回退之后发现还是不能正常休眠。看来只能上Windbg来动动手术了:

开始调试时休眠正常:过程中曾中断系统枚举了所有的进程、IRP及ERESOURCE锁的,只发现dxgkrnl.sys驱动中有明显的锁等待,不少线程都因尝试获取一个已经Exclusively Owned ERESOURCE锁。dxgkrnl是DirectX显示驱动,与此相关的只有intel显卡,等系统恢复后卸载了intel显卡驱动,用Windows来自动安装。

重试几次后windbg端忽然显示出熟悉的输出:

*** Fatal System Error: 0x0000009f
           (0x0000000000000003,0xFFFF980487D7B060,0xFFFFD3810AFB68F0,0xFFFF980490101010)

果然有坑!分析下来发现是蓝牙驱动的问题:

2: kd> !devobj ffff980487d7b060; !irp  ffff980490101010 1
Device object (ffff980487d7b060) is for:
   USBPDO-8 \Driver\usbhub DriverObject ffff980487f6b300
   ...

2: kd> !devnode ffff98048901c010
DevNode 0xffff98048901c010 for PDO 0xffff980487d7b060
  Parent 0xffff980488186d30   Sibling 0xffff98048940f790   Child 0xffff98048929dd30   
  InstancePath is "USB\VID_0A5C&PID_21E6\7CE9D3E86CA0"
  ServiceName is "BTHUSB"
  ...

2: kd> !dh -f bthport
File Type: DLL
FILE HEADER VALUES
    8664 machine (X64)
       C number of sections
CED98754 time date stamp Wed Dec 20 22:34:12 2079
...

bthport的timestamp竟然是2079 !? 什么鬼!先不管这些,重启系统后将蓝牙禁掉后,再测试休眠,试了几次均正常。然后开启蓝牙,什么都不做再次尝试休眠,果然又中招。

问题解决起来倒也简单:将所有的蓝牙设备都删除,重新安装Thinkpad Bluetooth,之后又测试了几次休眠,再没有出问题,世界终于又大同了!

在检查系统加载驱动过程中还有个新发现,国内的公司还有各大银行简直是驱动开发专业户,每家都有几个驱动,就连迅雷,你个下载软件,竟然有两个内核驱动。不由分说,直接咔嚓了此类驱动。从此和谐社会!比新闻联播还鸡血!

解析TopLevelIrp

何为TopLevelIrp

TopLevelIrp是当前内核线程结构中的一个指针,可以是一个数值标识(ULONG_PTR),也可以是指向一个内存块的指针(内核IRP结构或用户自定义结构),其目的就是给文件系统驱动记录当前线程其内核栈最早传入且正在处理的I/O请求。因为处理一个I/O请求的过程中可能引入新的I/O,这样就会导致文件系统的重入(嵌套调用),重入问题很有可能会带来对资源依赖的死锁,所以文件系统驱动需要在重入的情况也能甄别出当前正处理的请求是不是最早的,即当前线程内核栈及资源的占用情况,从而采取不同的策略以避免死锁的发生。在时间顺序上最早的请求,从调用栈上来看就是处于最高地址(最高位置)的请求。

TopLevelIrp在内核线程结构中的定义如下:

typedef struct _ETHREAD {
    KTHREAD Tcb;
    ...
    //
    //  File Systems
    //

    ULONG_PTR TopLevelIrp;  // either NULL, an Irp or a flag defined in FsRtl.h
    struct _DEVICE_OBJECT *DeviceToVerify;
    ...
} ETHREAD, *PETHREAD;

TopLevelIrp及DevcieToVerify等域均是留给文件系统驱动专用的,可以作为一个当前线程的环境变量或各层调用栈间的共享变量。

系统操作函数

ETHREAD是内核私有结构,系统内核提供给外部组件(主要是文件系统驱动)两个接口:

函数 功能
VOID IoSetTopLevelIrp(IN PIRP Irp); 设置(改变)当前线程的TopLevelIrp值
PIRP IoGetTopLevelIrp(VOID); 读取当前线程的TopLevelIrp值

TopLevelIrp的操作过程

文件系统驱动与操作系统内核组件:Cache Manager(缓存子系统)、Virtual Memory Manager(虚拟内存管理子系统)及I/O Manager (I/O子系统)间交互相非常紧密且调用关系也非常复杂,调用嵌套及重入的情形非常多,分辨并正确处理不同的重入情景正是文件系统驱动不得不面对的复杂难题。

TopLevelIrp就是为了解决这个问题而设计的。其最直接的操纵及访问者也是文件系统驱动,因为用户层I/O请求最初的响应都是由I/O Manager直接传给文件系统的。文件系统驱动在响应过程中会调用操作系统的其它子系统,在调用前会针对TopLevelIrp做不同的设置。另外还有几种常见情况,如Cache Manager及Virtual Memory Manager会根据内存压力来发起I/O操作,所以内核子系统也需要修改、设置TopLevelIrp域以标识出本次I/O操作的发起者。

IoGetTopLevelIrp()返回值 (当前线程的TopLevelIrp域) 含义
FSRTL_CACHE_TOP_LEVEL_IRP 由缓存管理器触发的i/o请求:延迟写(Lazy-write)或预读(Readahead)
FSRTL_MOD_WRITE_TOP_LEVEL_IRP 由系统脏页回写线程触发的写请求(在MiMappedPageWriter回写映射文件的脏页时)
FSRTL_FAST_IO_TOP_LEVEL_IRP 由I/O Manager及文件系统自身处理Fast i/o请求时设置
FSRTL_FSP_TOP_LEVEL_IRP 通常由文件系统本身触发,用以标识文件系统驱动自身的i/o处理线程(Workitem线程池)。由于此情况下TopLevelIrp完全由文件系统自已管理,IRP或私有结构,通常由文件系统自已构造并使用

操作场景

  1. 用户态切换到内核态 这是最通常的情形,即请求直接来自于用户,用户请求经过syscall之后到达I/O Manager,而后I/O Manager会构造Irp并直接传给文件系统。 文件系统最通常的做法就是将当前的Irp设为TopLevelIrp。文件系统当然也可以用自己私有的结构,因为此线程在Irp没有完成之前将完全由文件系统所控制,TopLevelIrp也只有文件系统驱动会查询。
  2. Fast I/O 用户态的请求通过syscall到达I/O Manager后,I/O Manager发现文件已经被打开过且可以通过文件系统驱动所提供的callback直接来处理这个请求,不必从内存池中分配Irp等结构。 文件系统被调用后又会调用内核所提供的文件系统支持函数(FsRtl...),此操作将有可能涉及Cache Manager、VM,或重入文件系统驱动等,所以文件系统支持函数会设置TopLevelIrp为FSRTL_FAST_IO_TOP_LEVEL_IRP以标示出最初的请求响应是通过Fast I/O来做的。
  3. 文件系统自身的Workitem线程 类似第一种情形(用户态切换到内核态),Workitem线程本身是由文件系统发起并且是文件系统所私有的内核线程,但Workitem中的操作却是有可能重入文件系统的(这个由文件系统的设计决定)。 TopLevelIrp的值可以由文件系统驱动自行决定,如可以设为FSRTL_FSP_TOP_LEVEL_IRP,也可以设为此Workitem当前要处理的请求Irp,也可以设成是文件系统驱动的私有结构。
  4. 缓存管理器:延迟写 Cache Manager的Lazy-write操作(延迟写)是在Cache Manager私有的内核线程中执行的。在调用文件系统进行I/O操作之前会设置TopLevelIrp为FSRTL_CACHE_TOP_LEVEL_IRP,因为Lazy-write操作会通过Cache Manager callback来获取相应的锁资源,并且最终的I/O操作总要由文件系统来完成。
  5. 虚拟内存管理器:MiMappedPageWriter 内存映射文件的I/O不会经过Cache Manager,而是由Virtual Memory Manager直接处理的,并且是在Virtual Memory Manager私有的内核线程中做的,此线程的处理函数就是MiMappedPageWriter。同Cache Manager延迟写的流程类似,最终的I/O操作总要由文件系统来完成,所以移交控制权之前,VM会将当前线程的TopLevelIrp设置为FSRTL_MOD_WRITE_TOP_LEVEL_IRP,以通知文件系统驱动当前请求是由VM发起的。

不同文件系统驱动的处理差异

前面说过TopLevelIrp的读操作及检测只有文件系统驱支在做,虽然其设置可以是多方的。对于文件系统驱动来说,TopLevelIrp的具体值是由文件系统自己来解释的,所以针对 FSRTL_FSP_TOP_LEVEL_IRP,不同的文件驱动对TopLevelIrp的处理也不尽相同:

文件系统驱动 TopLevelIrp所指向的结构
FastFat nt!_IRP
Cdfs cdfs!_THREAD_CONTEXT
Ntfs ntfs!_TOP_LEVEL_CONTEXT

案例分析

TopLevelIrp的设置是一项精巧的手艺活,稍有不慎就会阴沟里翻船,下面就介绍两个开发中曾遇到的案例:

NTFS:Valid Data Length的设置

Ntfs文件系统驱动在其内部维护着每个数据流的Valid Data Length(VDL,有效数据长度),且VDL是管理于Ntfs的私有结构中的。VDL的更新一般是在cached i/o过程中做的。但实际操作中我们有个特殊的需求:要截获所有的cached i/o请求,只会通过paging i/o请求和Ntfs文件系统驱动交与,这样操作的后果就导致VDL不能被正常更新,虽然数据已经写到了磁盘上但是当试图读回数据的时候,Ntfs会根据VDL对数据进行清零化处理。

最后的解决办法也是一个不得已的投巧的办法:将我们的paging writing 请求模拟成mmap i/o (memory mapping i/o),即映射文件的数据刷写方式。mmap i/o操作本身就是个top level操作,并且也是仅有的top level paging i/o的情形。所以针对mmap i/o,Ntfs会更新VDL,这也是唯一的可以更新VDL的机会。

CDFS: 换了光盘但不能正确加载

这个问题比较妖怪,本来以为是文件系统过滤驱动的问题,但仔细分析过滤驱动又没有发现可疑点,最后经过代码对比及对WDK中cdfs的动态调试才定位到问题症结:原来是top level irp的问题。

我们都知道文件系统对于可移动设备及可移除介质设备都会有个检验(即verify)操作,但cdfs的verify操作会检测当前的TopLevelIrp状态,并根据这个状态来决定是否继续verify操作。当我们设置了TopLevelIrp之后,cdfs会认为它不在最高的调用栈上,故直接跳过了verify操作,最终就导致了换光盘后新光盘无法被正常识别的问题。

参考链接

  1. TopLevelIrp是如何设置的 - blog: Oops Kernel
  2. More on Maintaining Valid Data Length - OSR

<本文用时约5小时>

Pages:  1 2 3 4 5