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系统虽然提供了回调接口,但因为性能等各种因素的原因还会限定回调的数量:

  1. CmRegisterCallback限制为最多100个,无高度号设置,后注册的回调可能收不到请求
  2. PsSetLoadImageNotifyRoutine限制为最多8个,64位系统共64个
  3. PsSetCreateProcessNotifyRoutine限制为最多8个
  4. 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. RegistryCallback routines
  9. MSDN:Handling Notifications
Continue reading » · Written on: 05-04-19 · No Comments »

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界面程序;服务进程一般无需用户输入,所以此种注入方法对服务进程无效
  2. 64位系统中,64位进程只能设置针对64位程序的消息HOOK,32位进程只针对32位程序,不可交叉混用。和SetWindowsHookEx类似的SetWinEventHook 虽然同样可以取到所有进程的消息,但并不能导致DLL注入。

  3. 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)创建:

后续会收到模块加载的通知回调,加载模块的顺序依次为wermgr.exe自身、ntdll.dll、kernel32.dll、KernelBase.dll、msvcrt.dll等,回调时的调用栈为:

导入表修改动作是在进程镜像本身的模块加载回调中执行的,即程序本身模块加载的时机。

IAT表注入的主要问题:

  1. .NET程序的兼容性问题
    • 托管代码与非托管DLL代码的混合
    • 64位.NET程序没有导入表项
  2. 受保护进程的注入问题,必须要通过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 浏览器自身防护资讯

  1. B1: About Google Chrome's incompatible applications warning
  2. B2: Firefox will block DLL Injections
  3. B3: Protecting Microsoft Edge against binary injection

3.2 进程DLL注入资料

  1. D1: DLL Injection with SetThreadContext
  2. D2: R3 DLL Injection: Inject All The Things
  3. D3: COUNTERCEPT: Analyzing the DOUBLEPULSAR Kernel DLL Injection Technique
  4. D4: COUNTERCEPT: Dynamic Shellcode Execution
  5. D5: Ten Process Injection Techniques
  6. D6: DLL Injector via Windows Automation API
  7. D7: Microsoft: AppInit DLLs and Secure Boot
  8. D8: BlackHat: Malicious Application Compatibility Shims
  9. D9: To SDB, Or Not To SDB: FIN7 Leveraging Shim Databases for Persistence
  10. D10: FreeBuf: 走近微软安全技术Shim
  11. D11: ABICC: Windows API/ABI Changes Analysis
  12. D12: MITRE ATT&CK: Application Shimming
  13. D13: FireEye: The Real Shim Shady
  14. D14: iSIGHTPARTNERS: 固守: 微软Fix It补丁机制原理及攻击利用
  15. D15: 腾讯:木马牟利再出新招-恶意利用Windows shim技术锁主页
  16. D16: 腾讯:Haiheiwang木马分析
  17. D17: Windows Vista APC Internals
  18. D18: TDL: Turla Driver Loader
  19. D19: Scylla and API Set Map
  20. D20: Microsoft Docs: Windows API Sets
  21. D21: Windows_7_Kernel_Changes: api-ms-win-core DLLs
  22. D22: DOUBLEPULSAR Kernel DLL Injection Technique
  23. D23: DOUBLEPULSAR: Generic Reflective DLL Loader
  24. D24: MalwareFox AntiMalware 2.74.0.150 - LPE
  25. D25:DLL Injection via SetDLLDirectory
  26. D26: CodeProject: Create your Proxy DLLs automatically
  27. D27: Proxy WS2_32.DLL to create your own firewall
  28. D28: CodeProject: API hooking revealed

3.3 DLL注入商业方案

  1. D30: Shellter Pro: DLL Injection Kit
  2. D31: madCodeHook: DLL Injection Kit
Continue reading » · Written on: 04-01-19 · No Comments »

奔跑在理想之国 — 2018梅里100赛记

“他是两个世界之间的流浪者,注定要永远流浪。”

James Hilton在其所著的《消失的地平线》一书中用来形容主人翁Conway的这句话,同样用来形容每一个跑者也是恰如其分!很多人生活在繁杂拥闹的大城市,所向往的却是野外大自然的宁静悠然。我们在两个世界间不停地穿梭与奔跑,大自然就是我们心中的“香格里拉”。

香格里拉、虎跳、梅里、雨崩、神瀑,2010年7月我曾经来过,不知8年之后的今天,又会变成什么模样?那个与世隔绝的村子是否还保有着仙境般的梦幻?卡瓦格博是否依然还是那个羞涩的小姑娘,始终不肯摘下紧裹的云纱?

万千往事揉着纷杂思绪潮水般涌向心头,而我却无从下笔,就像拿起了一根烟放在嘴里却尴尬地发现没有了火,只能呆呆地望着窗外......

起心

1月11日UTMB抽签结果新鲜出炉,可惜没有带来好消息,几个朋友都没有中签。“不跑个168都不好意思”的小群中4个报了UTMB168的人中,只有过客一人中签,另外天路报了TDS也顺利收获了一张入场券。虽然遗憾,但不中自有不中的好处,就在结果出来后仅几个小时,我和老大便已报名梅里100。

决心一旦下定,旋即就进入了一种亢奋:去赴它个一场与雨崩的8年之约!去跑它个一场高海拔的100公里!连从未体验过高海拔的老大也同样踌躇满志。

只要有了目标,对我们而言,就自会有行动!我们是一群最不缺乏行动力的人。

动念

考虑到老大没有高海拔的经验,我们计划早于比赛几天到达以适应海拔,先去徒步虎跳峡,然后去雨崩村。后来赛事时间变更,结果我们前面可安排的时间更充裕了,多出了一天半的时间,我开始盘算这一天半的时间完全可以去爬个哈巴,毕竟来一趟不容易,即使爬不成雪山,在大本营做个高海拔适应也好。

在香格里拉等老大到来的空,我给哈巴向导武桑打了电话,问询了哈巴山上的情况及近期的天气,得知山上已连着下了三天雨,武桑相信后面会有一个好的天气窗口,另外所需的技术装备如冰镐、冰爪等均有出租。听到这些,我更是按奈不住地兴奋,要玩就不妨玩大的,上次雪山攀登还是2011年的雀儿山。从接触户外之初就对雪山有着一莫名的向往,一连串地爬了几座之后却又对当前的商业登山失望之至,而今又能够亲近雪山了,虽然哈巴只是一座初级山峰,依然莫名兴奋。

等老大到达汇合后,和他说了我的想法,我们一拍即合。眼前,除了兴奋与鸡血。没有别的!

买好了去桥头镇的车票,还有20分钟发车,老大说有点饿,我们遂到车站边上的小饭馆问最快的吃的是什么,老板娘得知我们只有20分钟时她很会意地答曰:米粉。一阵风卷残云,仅过了7分钟,与满脸笑容的老板娘道别,我们俩扬长而去。

28道拐前没有了赌你爬不动的马夫,徒步的小径很多已被宽阔的水泥路面所替代,走到了路上才知道准备的不足,我下载的轨迹虽是2017年的,但已然过时,其中一段腰绕的线路因为修隧道的缘故被破坏了。我和老大在铁丝网上爬了好一阵子,被工地上的人叫了下来,告知路在下面。经过几经探寻之后,终于对上了轨迹走上了正途,但1个小时的时间已飘然而逝。

8年后再走虎跳,已经记不得当时走过的路,反而觉得脚下的路和尼泊尔EBC很像很像。记忆就像一张网,咋一想起便牵动了所有的枝枝叶叶,但一旦仔细回味起来,却只剩下了破碎而又坚硬的梗。

天开始下雨,我们穿好冲锋衣准备好了一场雨战,但高原的天气就是喜欢捉弄你,一会雨又一会晴,湿闷得不行,再加上已好久没有重装徒步,爬起28道拐来相当绝望,远处的玉龙雪山一直藏在云层里,峡谷中蜿蜒着的混浊泥流发出轰鸣的水声却响彻山谷,像是对你的嘲笑。

在茶马客栈补了瓶水之后,我们继续出发,很快到达中途客栈(Half Way G.H.),此时已近8点,天色开始变暗。经过6个小时的行走,我们觉得相当疲惫,毕竟背着近14公斤的包,而且还是几经压缩精简后的。

客栈里已经来了不少客人,我们到达时他们正聊着天。第一眼看去,Half Way还是以前的Half Way,安静的小园子,可以眺望峡谷的天台,但房间内早已升级换代,以前的公共浴室已被各个房间内的沐浴设施取代,不知“天下第一厕”还安否?已然没了去参观它的兴致。

不群

也不知道为什么,反正莫名地就将不群岳兄和中途的老板联系在一起了,为了想明白这个问题,着实费了不少脑细胞但还是没有想明白个缘由。

途经茶马时就得知整个虎跳峡因输电线杆被撞已全部停电,到达中途客栈后问老板洗澡的事,还以为是以前的公共沐浴间,结果老板高逼格说了句:“洗澡是必备的最基本的要求”,结果晚上发电机还没发动就爆了,亏得热水器中存有不少热水,足够我们俩冲个澡;老板还说过早上7点准点有早饭,第二天我们起来后不见人影,我不得不找遍了客栈去逐个敲门,

结完账,我和老大快速撤离,50分钟后在中峡客栈Tina's吃了顿超丰盛的早餐。

行定

和武桑约好11点在山白脸汇合,我想着两个半小时的时间足够我们在中虎跳游玩了。谁知老大竟然这么爱拍照,每过一个点都要正拍10张再反拍10张,然后回过身来再来10张,搞得我好不紧张,毕竟后面的哈巴才是重点。

好在过了一线天之后,走出峡谷,风景不在,我们遂加快了脚步。

外面,武桑和车已经等候多时!到山白脸走了一圈,权算故地重游。

车子发动,沿着金沙江峡谷急驰。

哈巴,我们来了!

在武桑家稍做停留,我们即从彝族村出发去哈巴大本营。路上我叮嘱老大要慢点,慢慢上升慢慢适应海拔,可最后我们还是快过了马,用了3个小时到达海拔约4100的大本营后依然还是一脸的兴奋劲。马儿虽慢,但马儿更会休息,马儿更会保持速度,马儿能够跑得更远,但我们比它更有激情!特别是对于老大来说,高海拔绝对是个正儿八经的新鲜事。

在大本营转了几圈,在营房内烤了几个小时的火,拍照、聊天、烤火、喝水、撒尿,看着山上厚厚的云层被风卷来卷去,山体在云层的包裹中幻生幻灭,木头在火中噼啪作响,火星似流星般向上划出道道弧线,然后湮灭在房间窒息的空气中。我,静静地望着它们,百无聊奈。

是夜,我如期的头痛着入睡,做着各种努力让自己好好睡觉的梦!

是夜,老大如我所叮嘱的一样,躺着一动不动,只是他一丁点儿都没睡着。他说也不头痛也不胸闷,可就是睡不着!连续着三天只睡了两个多小时,超级铁人一个!

凌晨3点半起来吃了碗咸面片粥,觉得超级香,现在想起来还直流口水,看来有必要向武桑索要配方。3:52出发,我们俩是第一队,后面有大部队正在吃饭。

天上滴着小雨,好天气窗口并没有因为前面连续三天的雨而特别眷顾我们,相反而是继续让坏天气来考验我们向上的决心。走过了一小段灌木丛路,我们踏上了光亮的岩石,岩缝中还夹有雪渍,但哈巴的砾岩一点不滑,走起来并不费力。但随着海拔的上升,部分路段覆盖了一层薄薄的冰,这部分走起来要格外小心。

过了雪线后绑上了冰爪,虽然天色渐亮,但我们逐渐走进了浓雾中,天是白的,地上白的,分不清界限,透过雪镜中依稀能看到两侧的山脊的模糊轮廓,山脊两侧就是悬崖,而我们就在悬崖间窄窄的通道上艰难爬行,凭着感觉向上、再向上,只有感觉到确实向上才能明确路线是对的。

前面一段路老大一直很活跃,冲在前面,但到了这一段,开始有点跟不上了,毕竟连续几夜没能休息好再加上连续几天的高强度运动,着实让人吃不消。

从C1营地出发的队伍和我们汇合在一起,一个女生和我们不停地穿插行走交替领先,令我惊讶的是,她和老大一样,也是第一次攀登雪山。

浓雾中两侧山脊渐渐收窄,上山的坡度依然延伸在前面的路上,每慢走50步就要停下来喘口大气,尽我最大的贪婪吸进面前的浓雾,但呼出的热气却更妨碍了前面的视野。

在第N个50步,或者在喘过第N个粗气之后,终于看到了顶峰标志,像是一个刻着字的简陋木板,注意到右侧的山顶更高,我向右上侧挪去,走到一半的时候,向导安牛上来了,安牛叫停了我,说右侧危险,到这里就可以了,这里就是哈巴顶峰。

似乎还不过瘾,似乎是因为天不作美,顶峰上的风景也就4-5米的视野内的一片白茫茫......

时间正好8点半,从大本营出发到现在共4小时38分。后面的女生也上来了,帮她和向导拍照后,我和老大及安牛便开始下撤。

浓浓的大雾中,上来的脚印已经看不出了,天好像还下着雪,小心翼翼地循着山势向下探寻了好长一段,终于遇见了向上攀登的大队伍,一下子热闹了起来,相互间打着招呼,我向他们广播着此地到顶峰的大体所需时间,终于能够放开步子开走。

下至雪线,撤了冰爪,撒欢向下跑,一溜跑到大本营,从凌晨出发到回到大本营共6小时20分。哈巴之行现在可以说是圆满。

发心

和武桑沟通了一下,他说先休息会吃了饭就下山,然后去香格里拉。

老大吵着要好好睡一觉,我却兴奋地没有一点疲倦,在营房里和从广东来哈巴考察做科研的土豪(耗)烤火、聊天。

他们一行几人是考察高海拔上老鼠的种类及分布的,篝火边的木板上全是老鼠的标本,他们准备要将标本烤干,回去还要做肉质分析。问了他不少问题,也聊了不少以前徒步的经历,以至临走时已有几分不舍。

海拔4100的大本营,虽然条件比较艰苦,但这里的人来自全国各地,每个人背后都有着一串串精彩的故事,每个人都有着一颗开放的心。虽然相见短暂,但相聊甚欢,几言几语间已成知己,更多的话不必发出声即能颜传意会,如同《消失的地平线中》所述的康维或亨舍尔与佩劳尔特相逢相知和相互欣赏。

然而时间很快,道别时刻终于还是到来,虽然心里不舍,手上却是轻轻一挥,故作轻松 ...

老大,他 终究 还是 没能 睡着!

随性

重担已卸,警觉亦歇,下山过程全无负担,心意飞悦。我们一行4人(我、老大、武桑、安牛)加上马儿轻松自在,路上比较泥汀,我们不急于赶路,边走边聊,用了2个多小时到达武桑家。

见到了武桑的妈妈,一位慈祥硬朗的彝族老妈妈。我们的哈巴之行就定格在了她满脸的笑容里。

梅里100准备会上古神播放的路线视频让我印象深刻,以至于跑完之后我又看了几遍,但其中几个CP点,纵使我每一个都曾经走过,但怎么也想不起来它的模样了,就好像我压根没看到过似的,难道是因为高海拔缺氧所致!?

30号中午到达飞来寺,入住了老大早早预定好的梅里往事。路过白马雪山时还飘着雪,飞来寺这边则是下着小雨,看到明天将是一场艰苦卓绝的泥战了。

准备会后是丰盛的自助餐,吃得非常非常非常饱,必须对得起赛事组织方的盛情和用心。

下午时分,雨已停,但梅里雪山方向依然密布着云层,白马雪山方向风云幻变,一会亮了半边的天空,一会又升起了浓浓的白雾。

所有人都在祈祷 明天天会变好。

时钟转到了早上8点,发令枪响,人流开始旋转,山在动,地已醒,鞋子上的泥点在飞,雨点也在盘旋着下降,汗一滴滴钻出皮肤而后又钻入土地,等待着变成雨点的轮回。

过了永宗桥后开始上升, 高海拔上升路段已没有了冲下山的那股势气。路上追上了王同伟,30号刚刚认识的北京跑友,同住在梅里往事;晚上在跑神爆的路上又曾碰到他,赛后得知他是30公里的第一名。

过西当村,不时传来加油的吆喝声,我则回应“扎西德勒”,然后就是更多的“扎西德勒”的回声。经过八一茶馆,古神在门口忙碌着,我则只是加满热水即冲向下一站。

去冰湖的路太过漫长,关键回来也要走同样多的路,高海拔的漫长上坡真是让人崩溃,感觉从中午一直到傍晚。至冰川一段和天路同行,冰川上又偶遇古神,顿觉世界真小。

正好在天就要变黑之前,我走到了神瀑脚下,水量比上次来看她的时候要少很多,但现在的她更轻盈活泼,躲在似透非透的薄雾里婀娜着缓缓流下,我凝视许久,天又开始下雨,瀑布迸溅的水滴混着雨点打在我身上,一身清凉。

飞奔而下,走出峡谷的时候碰到了老大,看他状态还不错,他说在上雨崩休息了挺长时间。

从下雨崩出来,志愿者伸出手掌,我们击掌相别,黑夜中我向着白塔跑去,然后就是一路的狂奔,左侧咆哮着的雨崩河无怨无悔地投向澜沧江的怀抱,而我也顺着她无心无肺地奔跑,一路上只有雨崩河的陪伴,好不惬意。直至神秘客栈前才看到志愿者,后又追上了两名选手。

也许是海拔下降之故,晚上的气温相当高,我只得在尼龙村的取水渠里用水浇在头上及身上,顿觉一身的舒服,此时1052号的李灏赶上来,我的号码是1025。从此之后30多公里我们一路偕行,直到终点飞来寺。

从尼龙出发到拥用的机耕路真得要走死人,无语!漫长的路总有尽头,但到了尽头之后又发现黑暗中还有新的尽头在等待着你。

6月1号早上5点48我们从曲登阁出发,向终点发起冲击,天逐渐变亮,然后太阳出来了,直辣辣地照在身上,让我们毫无防备,后果当然不言而喻,左手腕被晒红,鼻子、嘴及下巴区域成了黑三角。

最后关头,冲进24小时的希望越来越大,但是不到终点,你永远不会知道还有什么样的路,我一边快步走着,一边催促着李灏。

在奔跑了近24小时之后,我们俩同时冲线!23小时28分。

情切

6月2号早上,我和老大拖着麻木的双腿走到楼下,正遇到梅里往事的老板,他问我们:你们吃过我们的早饭没?“没有”,我们回答说。接着老板很自豪地说:那你们必须要尝一尝!

于是我们坐下,老板则忙开了,陆续端上来牛奶、奶昔,烤好的面包片及黄油、草莓酱,然后煎蛋和青菜卷,好丰盛的早餐。

临走的时候,正吃饭的老板看到了我们,放下碗筷起身相送。

那一刻,真得不想走,真得想留下来。

素蚁、土豆、老大、我,我们四人将自己塞进了一辆小速腾,任由它飞驰着将我们带离这个世界:这个世界里我们曾挥洒汗水,这个世界里我们曾兴奋而又绝望,这个世界里我们狂奔着叫嚣着狂热着,这个世界里我们又曾无比安静和虔诚地凝视着、祈祷着...

不舍,情切,甚切!

锚点

凡事你曾用心,事必然也会铸造着你。做了但没有一点反响,岂不是等同于从未做过!

8年前的雨崩之行,是我开始独立走长线的锚点;

8年后的梅里之行,是我最惬意的一次高海奔跑体验。

还记得20年前的哈尔滨的一个冬夜,跑过了第一个10000米;

还记得在华政操场上的第一个60圈 ....

太多,太多,早已被抛在了脑后,因为一直有新的锚点在筑起新的高度。

结束即开始

迪庆,是一个奇迹之地,民族的融合,三江并流,山川壮丽,每个人心中的香巴拉圣境,理想之国!每一个跑者的奔跑之地!转梅里,穿三江,跑出自我!

就如在西奈山上上帝对摩西所言:"I AM WHO I AM ..." (From Exodus 3.14)

跑我所跑 !

爱跑而跑 !

无所不能跑 !

WE ARE RUNNING WHAT WE ARE RUNNING !

WE ARE RUNNING WHY (BECAUSE) WE ARE RUNNING !

WE ARE RUNNING WHERE WE ARE RUNNING !

Continue reading » · Written on: 06-08-18 · No Comments »

1394 is back !

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

但从Windows 10发布以后,微软不再提供Kernel Debugging over 1394,但好在有相应的Workaround延长了1394的寿命,参见 [KD 1394 Work-Around](https://blogs.msdn.microsoft.com/windbg/2016/08/11/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!](https://blogs.msdn.microsoft.com/windbg/2017/08/28/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使用者,又可以欢呼了,但此好景能延续到什么时候呢?

Continue reading » · Written on: 09-10-17 · 1 Comment »

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

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

    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,之后又测试了几次休眠,再没有出问题,世界终于又大同了!

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

Continue reading » · Written on: 09-01-17 · 1 Comment »

解析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小时>

Continue reading » · Written on: 02-28-17 · 1 Comment »