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