click to login
某不知名程序员的网上巢穴。稍安勿躁,正在开发。

内核函数的可选参数和 PreviousMode 的关联性

在驱动中实现对 Windows 7 x86 系统平台的 nt!NtWriteVirtualMemory 的 Inline Hook 并捕获各进程调用该 API 时的参数,并在 Hook 处理函数中调用原函数执行,并获取返回值等回调数据。由于需要对参数 Buffer 的内容数据进行提取,所以它的最后一个参数 NumberOfBytesWritten 是必然不能少的。

NtWriteVirtualMemory 的函数原型如下:

NTSTATUS
NTAPI
NtWriteVirtualMemory (
    IN  HANDLE ProcessHandle,
    IN  PVOID  BaseAddress,
    OUT PVOID  Buffer,
    IN  ULONG  NumberOfBytesToWrite,
    OUT PULONG NumberOfBytesWrite OPTIONAL
    );

由上可知,最后一个参数是指向 ULONG 类型变量的指针,是可选的,意味着调用者在调用时可将该参数置位 NULL,在 API 内部会忽略本应输出给该参数的数据,那么我们在捕获参数数据时必然就无法获取到所要获取的数据了。

可以通过在 Hook 处理函数中判断 NumberOfBytesWrite 指针为 NULL 时定义 ULONG 类型的局部变量并将 NumberOfBytesWrite 手动指向该局部变量,以此来捕获该参数的值。

可是在实际应用中,出现了问题。大量对 NtWriteVirtualMemory 的调用在 Hook 处理函数指定了上述 ULONG 类型变量之后,返回 0xC0000005 非法访问的错误,并造成操作系统各进程执行的异常。所幸系统内核并未造成 BSOD 的错误,推测仅是用户层的调用出现问题。

单步跟踪执行的汇编指令如下:

85a63c6f 6a18            push    18h
8407b71e 68001ae783      push    offset nt! ?? ::FNODOBFM::`string'+0x3e80 (83e71a00)
8407b723 e880e4e1ff      call    nt!_SEH_prolog4 (83e99ba8)
8407b728 648b3d24010000  mov     edi,dword ptr fs:[124h]
8407b72f 8a873a010000    mov     al,byte ptr [edi+13Ah]
8407b735 8845e4          mov     byte ptr [ebp-1Ch],al
8407b738 8b7514          mov     esi,dword ptr [ebp+14h]
8407b73b 84c0            test    al,al
8407b73d 746a            je      nt!NtWriteVirtualMemory+0x8d (8407b7a9)
8407b73f 8b450c          mov     eax,dword ptr [ebp+0Ch]
8407b742 8d1430          lea     edx,[eax+esi]
8407b745 3bd0            cmp     edx,eax
8407b747 7259            jb      nt!NtWriteVirtualMemory+0x86 (8407b7a2)
8407b749 8b4510          mov     eax,dword ptr [ebp+10h]
8407b74c 8d0c30          lea     ecx,[eax+esi]
8407b74f 3bc8            cmp     ecx,eax
8407b751 724f            jb      nt!NtWriteVirtualMemory+0x86 (8407b7a2)
8407b753 a11447f883      mov     eax,dword ptr [nt!MmHighestUserAddress (83f84714)]
8407b758 3bd0            cmp     edx,eax
8407b75a 7746            ja      nt!NtWriteVirtualMemory+0x86 (8407b7a2)
8407b75c 3bc8            cmp     ecx,eax
8407b75e 7742            ja      nt!NtWriteVirtualMemory+0x86 (8407b7a2)
8407b760 8b5d18          mov     ebx,dword ptr [ebp+18h]
8407b763 85db            test    ebx,ebx
8407b765 7445            je      nt!NtWriteVirtualMemory+0x90 (8407b7ac)
8407b767 8365fc00        and     dword ptr [ebp-4],0
8407b76b 8bcb            mov     ecx,ebx
8407b76d a11c47f883      mov     eax,dword ptr [nt!MmUserProbeAddress (83f8471c)]
8407b772 3bd8            cmp     ebx,eax
8407b774 7202            jb      nt!NtWriteVirtualMemory+0x5c (8407b778)
8407b776 8bc8            mov     ecx,eax
8407b778 8b01            mov     eax,dword ptr [ecx]

在执行 mov eax,dword ptr [ecx] 语句时直接跳出无法继续单步向下。下面针对上面的指令进行分析。

对 nt!_SEH_prolog4 的调用负责构造当前栈帧的 SEH 结构。FS 寄存器在 Windows x86 平台中存储当前线程的 KPCR 处理器控制区数据块,该结构在 Windows 7 x86 中的定义如下:

kd> dt _KPCR
ntdll!_KPCR
   +0x000 NtTib            : _NT_TIB
   +0x000 Used_ExceptionList : Ptr32 _EXCEPTION_REGISTRATION_RECORD
   +0x004 Used_StackBase   : Ptr32 Void
   +0x008 Spare2           : Ptr32 Void
   +0x00c TssCopy          : Ptr32 Void
   +0x010 ContextSwitches  : Uint4B
   +0x014 SetMemberCopy    : Uint4B
   ...
   +0x0d8 Spare1           : UChar
   +0x0dc KernelReserved2  : [17] Uint4B
   +0x120 PrcbData         : _KPRCB

其最后一个域是 _KPRCB 类型的 PrcbData 结构体成员。_KPRCB 的定义如下:

kd> dt _KPRCB
ntdll!_KPRCB
   +0x000 MinorVersion     : Uint2B
   +0x002 MajorVersion     : Uint2B
   +0x004 CurrentThread    : Ptr32 _KTHREAD
   +0x008 NextThread       : Ptr32 _KTHREAD
   +0x00c IdleThread       : Ptr32 _KTHREAD
   +0x010 LegacyNumber     : UChar
   ...

第三个域是 _KTHREAD 类型的 CurrentThread 结构体成员,该成员相对于 _KPCR 结构体中的偏移位置是 _KPCR + 0x120 + 0x04 = _KPCR + 0x124。所以通过 dword ptr fs:[124h] 获取的是当前线程的 KTRHEAD 结构体对象。

结构体类型 _KTHREAD 定义如下:

kd> dt _KTHREAD
ntdll!_KTHREAD
   +0x000 Header           : _DISPATCHER_HEADER
   +0x010 CycleTime        : Uint8B
   +0x018 HighCycleTime    : Uint4B
   +0x020 QuantumTarget    : Uint8B
   +0x028 InitialStack     : Ptr32 Void
   ...
   +0x138 AdjustReason     : UChar
   +0x139 AdjustIncrement  : Char
   +0x13a PreviousMode     : Char
   +0x13b Saturation       : Char
   +0x13c SystemCallNumber : Uint4B
   +0x140 FreezeCount      : Uint4B
   ...

其中 0x13A 偏移位置的域是 PreviousMode 成员。该成员用来记录当前线程调用目前所处 API 或 SysCall 之前的线程处理器模式。很多 API 会根据这个成员的不同的值进行不同的处理,例如上面说到的 NtWriteVirtualMemory 函数。PreviousMode 可取的值有:

typedef enum _MODE {
    KernelMode,
    UserMode,
    MaximumMode
} MODE;

PreviousMode 更多信息: https://msdn.microsoft.com/zh-cn/windows/desktop/ff559860

判断 PreviousMode 的值是否为零,即判断是否 KernelMode 内核模式。根据上面的指令执行列表可知,显然未命中该条件,那么在当前环境中 PreviousMode 是 UserMode 用户模式,则向下进入用户模式的处理流程。

判断参数 BaseAddress,Buffer,NumberOfBytesToWrite 的有效性。另外这里需要提到一个内核全局变量 MmHighestUserAddress 以及后面提到的 MmUserProbeAddress 变量。

MmHighestUserAddress 指定当前系统的用户层地址空间的上限,MmUserProbeAddress 指定当前系统的用户层地址空间探针地址阈值。这两个变量都是在内核初始化时赋值的。

在 x86 平台的 Windows 操作系统中,这两个值分别是:

MmUserProbeAddress = (PVOID)0x7fff0000;
MmHighestUserAddress = (PVOID)0x7ffeffff;

这意味着,用户态进程在用户空间可以访问的虚拟地址上限是 0x7ffeffff,但从 0x7fff0000 开始就不能访问了。一般文献中讲 Windows x86 系统中用户空间与系统空间的分界线是 0x80000000,而这里之所以是 0x7fff0000 而不是 0x80000000,是因为在分界下面留了 64KB 的空间不让访问,以此作为隔离区。

NtWriteVirtualMemory 根据 MmHighestUserAddress 变量确定 BaseAddress 和 Buffer 地址的有效性。如果无效则直接返回 0xC0000005 非法访问的错误码。如果有效,那么进行后续的操作。

这时判断 NumberOfBytesWrite 参数是否指定了接受写入字节数的 ULONG 变量的地址。如果该参数并非指向 NULL,进入更进一步的判断。

这里有一个异常处理机制。在 Windows 内核中应用了 __try __except 机制的函数,调用时其栈帧中第一个局部变量会是类型为 CPPEH_RECORD 的结构体对象。该结构体的原型如下:

typedef struct _EH3_EXCEPTION_REGISTRATION {
  DWORD Next;             // off 0x00
  DWORD ExceptionHandler; // off 0x04
  DWORD ScopeTable;       // off 0x08
  DWORD TryLevel;         // off 0x0C
};

typedef struct _CPPEH_RECORD {
  DWORD old_esp;                            // off 0x00
  DWORD exc_ptr;                            // off 0x04
  _EH3_EXCEPTION_REGISTRATION registration; // off 0x08
};

通过 TryLevel 成员域指定是否进入 __try __except 机制。将 TryLevel 置为 0 表示进入,置为 0xFFFFFFFE 表示离开。CPPEH_RECORD 结构大小为 0x18 字节,该对象位于 ebp-0x18 的位置。而 TryLevel 域相对于 CPPEH_RECORD 首地址偏移为 0x14,所以在代码中直接对 dword ptr [ebp-4] 进行操作了。

接下来会判断参数 NumberOfBytesWritten 是否小于 MmUserProbeAddress 变量;如果不小于,那么意味着在不久之前用户模式下的调用者将该参数指针指向了非用户层地址空间范围内的地址。

这时 NtWriteVirtualMemory 会判断当前线程环境是否有操作非用户态地址空间范围地址的能力。这是通过 *MmUserProbeAddress = *MmUserProbeAddress 的赋值操作来实现的。

显然,MmUserProbeAddress 变量指向的地址无法在用户态访问。绝大多数情况下到这一步都会触发异常,从而返回 0xC0000005 的错误码。

解决方法是在 Hook 处理函数中向下调用原函数之前存储当前线程的原 PreviousMode 的值,并将 KTHREAD 中的该域改为 KernelMode 内核模式;在调用原函数返回后,再根据存储的原 PreviousMode 值改回 KTHREAD 中 PreviousMode 域的值。

但这样可能会造成一些不可预料的问题,不建议直接修改。通常来自用户态的调用都会通过 Zw 系列 API 来进行,在 Zw 系列 API 中会进行一些预处理,这部分后续再做分析。

没有回答

评论:

版权所有 © 2010-2016 小刀志 · 本站基于 WordPress 构建 · 原创内容转载请取得作者同意和授权