内核模式之所以有别于用户模式,在于内核模式应该是安全、可信的。用户系统调用可以传入各式各样的参数,可能是代码无意写错或因不预期的内存覆盖"暗地修改"参数,也可能是Hack有意传入,内核都应当妥善处理,避免内核读写到不预期的地址,造成内核被破解或不稳定。
NTSTATUS
NtCreateProcessEx(__out PHANDLE ProcessHandle,__in ACCESS_MASK DesiredAccess,__in_opt POBJECT_ATTRIBUTES ObjectAttributes,__in HANDLE ParentProcess,__in ULONG Flags,__in_opt HANDLE SectionHandle,__in_opt HANDLE DebugPort,__in_opt HANDLE ExceptionPort,__in ULONG JobMemberLevel)
{NTSTATUS Status;PAGED_CODE(); /* 判断是否在可访问分页内存的IRQL */if (KeGetPreviousMode() != KernelMode) {//// Probe all arguments//try {ProbeForWriteHandle (ProcessHandle); /* 检查handle是否可写 */} except (EXCEPTION_EXECUTE_HANDLER) {return GetExceptionCode ();}}if (ARGUMENT_PRESENT (ParentProcess)) { /* 判断父进程handle是否存在 */Status = PspCreateProcess (ProcessHandle,DesiredAccess,ObjectAttributes,ParentProcess,Flags,SectionHandle,DebugPort,ExceptionPort,JobMemberLevel);} else {Status = STATUS_INVALID_PARAMETER;}return Status;
}
FORCEINLINE
VOID
ProbeForWriteHandle (IN PHANDLE Address)
{if (Address >= (HANDLE * const)MM_USER_PROBE_ADDRESS) {Address = (HANDLE * const)MM_USER_PROBE_ADDRESS;}*((volatile HANDLE *)Address) = *Address;return;
}
比较有意思的是,先判断地址是否越过默认的MM_USER_PROBE_ADDRESS,如果超过,说明在内核空间,不能向内核空间随便写数据测试权限,先把地址改成MM_USER_PROBE_ADDRESS做读写确认。
这种情况,似乎检查了个寂寞。
在PspCreateProcess结尾有如下这段code再次确认写ProcessHandle是否有异常:
try/except处理异常的方式是NOTHING! 这个应该是不恰当的处理方式。
VOID
ProbeForWrite (__inout_bcount(Length) PVOID Address,__in SIZE_T Length,__in ULONG Alignment)
{ULONG_PTR EndAddress;ULONG_PTR StartAddress;#define PageSize PAGE_SIZEif (Length != 0) { /* Length如果是0, 就不用检查了! *///// If the structure is not properly aligned, then raise a data// misalignment exception.///* Alignment必须是1/2/4/8/16之一 */ASSERT((Alignment == 1) || (Alignment == 2) ||(Alignment == 4) || (Alignment == 8) ||(Alignment == 16));StartAddress = (ULONG_PTR)Address;if ((StartAddress & (Alignment - 1)) == 0) {//// Compute the ending address of the structure and probe for// write accessibility.//EndAddress = StartAddress + Length - 1;if ((StartAddress <= EndAddress) &&(EndAddress < MM_USER_PROBE_ADDRESS)) {EndAddress = (EndAddress & ~(PageSize - 1)) + PageSize;do {*(volatile CHAR *)StartAddress = *(volatile CHAR *)StartAddress;StartAddress = (StartAddress & ~(PageSize - 1)) + PageSize;} while (StartAddress != EndAddress);return;} else { /* 地址范围不在用户空间可探测区域,抛出访问异常 */ExRaiseAccessViolation();}} else { /* 起始地址未对齐, 抛出未对齐异常 */ExRaiseDatatypeMisalignment();}}return;
}
官方介绍: ProbeForWrite.
ProbeForWrite例程检查的是用户模式缓冲区权限是否正确:是否可写且正确对齐。
内核和驱动对于用户模式参数必须用ProbeFor*系列函数确保权限正确,而且是任何需要访问用户模式参数的位置。这是因为调用者可能之后用另一个线程修改参数,导致之前的检测不能挡住所有可能的非法访问。
Drivers must call ProbeForWrite inside a try/except block. If the routine raises an exception, the driver should complete the IRP with the appropriate error. Note that subsequent accesses by the driver to the user-mode buffer must also be encapsulated within a try/except block: a malicious application could have another thread deleting, substituting, or changing the protection of user address ranges at any time (even after or during a call to ProbeForRead or ProbeForWrite). For more information, see Handling Exceptions.
核心代码
if ((StartAddress & (Alignment - 1)) == 0) { /* 起始地址对齐 */
//
// Compute the ending address of the structure and probe for
// write accessibility.
//
EndAddress = StartAddress + Length - 1;
if ((StartAddress <= EndAddress) && /* 起始地址肯定要小于结束地址 *//* 地址不能超过用户空间可Probe范围*/
(EndAddress < MM_USER_PROBE_ADDRESS)) {
EndAddress = (EndAddress & ~(PageSize - 1)) + PageSize;
do {/* 每次只读写StartAddr第一个字节确认即可, 因为是分页为单位管理! */
*(volatile CHAR *)StartAddress = *(volatile CHAR *)StartAddress;/* 以PageSize为单位, 每个PageSize只读写首字节即可确认! */
StartAddress = (StartAddress & ~(PageSize - 1)) + PageSize;
} while (StartAddress != EndAddress);return;
}
}
核心在于MM_USER_PROBE_ADDRESS.
内存管理模块初始化会设置初始值。
/* Amd64 */
#define MI_HIGHEST_USER_ADDRESS (PVOID) (ULONG_PTR)((0x80000000000 - 0x10000 - 1)) // highest user address
#define MI_SYSTEM_RANGE_START (PVOID)(0xFFFF080000000000) // start of system space
#define MI_USER_PROBE_ADDRESS ((ULONG_PTR)(0x80000000000UI64 - 0x10000)) // starting address of guard page
/* x86 */
#define KSEG0_BASE 0x80000000
当然,用户空间也不能访问用于Probe的区域, 上图可以看到:MmHighestUserAddress被赋值为MI_USER_PROBE_ADDRESS - 1.
//
// Determine if an argument is present by testing the value of the pointer
// to the argument value.
//#define ARGUMENT_PRESENT(ArgumentPointer) (\(CHAR *)((ULONG_PTR)(ArgumentPointer)) != (CHAR *)(NULL) )
32位和64位系统指针默认长度不一样,统一用可自定义长度的ULONG_PTR类型。