刘收获

导航

调用Nt函数内核模式切换问题

      很久不写博客了,笔记大多记在电脑上在,以后整理好了再搬运上来吧。

      今天记一下“进程内存管理器”这个小程序上遇到的一个问题——内核模式调用Nt*函数。

      使用的是内核中的NtQueryVirtualMemory函数,先看一下WinDbg:

  kd> u NtQueryVirtualMemory
  ntdll!NtQueryVirtualMemory:
  00000000`76e414e0 4c8bd1 mov r10,rcx
  00000000`76e414e3 b820000000 mov eax,20h
  00000000`76e414e8 0f05 syscall
  00000000`76e414ea c3 ret
  00000000`76e414eb 0f1f440000 nop dword ptr [rax+rax]
  ntdll!ZwOpenThreadToken:
  00000000`76e414f0 4c8bd1 mov r10,rcx
  00000000`76e414f3 b821000000 mov eax,21h
  00000000`76e414f8 0f05 syscall
  kd> u nt!NtQueryVirtualMemory

     

  kd> u nt!ZwQueryVirtualMemory l20
  nt!ZwQueryVirtualMemory:
  fffff800`03eb75c0 488bc4 mov rax,rsp
  fffff800`03eb75c3 fa cli
  fffff800`03eb75c4 4883ec10 sub rsp,10h
  fffff800`03eb75c8 50 push rax
  fffff800`03eb75c9 9c pushfq
  fffff800`03eb75ca 6a10 push 10h
  fffff800`03eb75cc 488d053d2e0000 lea rax,[nt!KiServiceLinkage (fffff800`03eba410)]
  fffff800`03eb75d3 50 push rax
  fffff800`03eb75d4 b820000000 mov eax,20h
  fffff800`03eb75d9 e962650000 jmp nt!KiServiceInternal (fffff800`03ebdb40)
  fffff800`03eb75de 6690 xchg ax,ax

  nt!NtQueryVirtualMemory:
  fffff800`041979f0 48895c2410 mov qword ptr [rsp+10h],rbx
  fffff800`041979f5 4889742420 mov qword ptr [rsp+20h],rsi
  fffff800`041979fa 48894c2408 mov qword ptr [rsp+8],rcx
  fffff800`041979ff 57 push rdi
  fffff800`04197a00 4154 push r12
  fffff800`04197a02 4155 push r13
  fffff800`04197a04 4156 push r14
  fffff800`04197a06 4157 push r15

     ntdll中的NtQueryVirtualMemory与ZwQueryVirtualMemory是同一个函数,也就是一个简单的包装函数,使用了syscall切入内核,调用了nt!ZwQueryVirtualMemory函数,0x20(NtQueryVirtualMemory的服务号)存入eax,然后在SSDT中查找相应的系统服务,然后调用NtQueryVirtualMemory。这个就不用多说了,写过内核的,人尽皆知。

  总而言之,Ntoskrnl导出的NtQueryVirtualMemory才是真正的执行函数。而直接使用内核中NtQueryVirtualMemory的好处在于,这样做的好处在于可以避免SSDTHook和InlineHook引起的错误调用,而且杀毒软件一般都会Hook掉SSDT的,这样直接调用也可以绕过监控。

 

     下面先来具体看看NtQueryVirtualMemory函数(本文如未特殊说明,则所谈NtQueryVirtualMemory函数皆为ntoskrnl.exe中的NtQueryVirtualMemory函数):

 

 NTSTATUS NTAPI NtQueryVirtualMemory(  

          IN HANDLE                  ProcessHandle,                 //目标进程句柄  

          IN PVOID                  BaseAddress,               //目标内存地址  

          IN MEMORY_INFORMATION_CLASS  MemoryInformationClass,       //查询内存信息的类别  

          OUT PVOID                  Buffer,                      //用于存储获取到的内存信息的结构地址  

          IN ULONG                  Length,                      //Buffer的最大长度  

          OUT PULONG                  ResultLength OPTIONAL);        //存储该函数处理返回的信息的长度的ULONG的地址   

  )

 

  第三个参数类型MEMORY_INFORMATION_CLASS是一个枚举类型其定义如下:

  

  1. //MEMORY_INFORMATION_CLASS定义  
  2. typedef enum _MEMORY_INFORMATION_CLASS  
  3. {  
  4.  MemoryBasicInformation,          //内存基本信息  
  5.  MemoryWorkingSetInformation,       //工作集信息  
  6.  MemoryMappedFilenameInformation    //内存映射文件名信息  
  7. } MEMORY_INFORMATION_CLASS;  

   0x00:使用MemoryBasicInformation时,Buffer应当指向的结构为MEMORY_BASIC_INFORMATION,其定义如下:

typedef struct _MEMORY_BASIC_INFORMATION {  
    PVOID       BaseAddress;  
    PVOID       AllocationBase;  
    DWORD       AllocationProtect;  
    SIZE_T      RegionSize;  
    DWORD       State;  
    DWORD       Protect;  
    DWORD       Type;  
} MEMORY_BASIC_INFORMATION, *PMEMORY_BASIC_INFORMATION;  

  0x01:使用MemoryWorkingSetInformation时,Buffer应当指向的结构为MEMORY_WORKING_SET_INFORMATION,其定义如下:

typedef struct _MEMORY_WORKING_SET_INFORMATION {  
    ULONG       SizeOfWorkingSet;  
    DWORD       WsEntries[ANYSIZE_ARRAY];  
} MEMORY_WORKING_SET_INFORMATION, *PMEMORY_WORKING_SET_INFORMATION;  

  0x02:当使用MemoryMappedFilenameInformation  时,Buffer应当指向结构为MEMORY_MAPPED_FILE_NAME_INFORMATION,其定义如下:

#define _MAX_OBJECT_NAME 1024/sizeof(WCHAR)  
typedef struct _MEMORY_MAPPED_FILE_NAME_INFORMATION {  
 UNICODE_STRING Name;  
 WCHAR     Buffer[_MAX_OBJECT_NAME];  
} MEMORY_MAPPED_FILE_NAME_INFORMATION, *PMEMORY_MAPPED_FILE_NAME_INFORMATION;   

  在程序中我对的NtQueryVirtualMemory函数的使用方法是,定义一个NtQueryVirtualMemory函数结构的指针,在通过SSDT表来找到内核层的NtQueryVirtualMemory的地址,用定义好的指针指向这个地址,从而调用这个函数。这样做的原因在于Ring0层也无法直接调用内核中的NtQueryVirtualMemory了,这一点看雪中的这篇文章有过讨论,并给出了上述的解决方案:

  http://bbs.pediy.com/thread-65392.htm

  

typedef
NTSTATUS
(*pfnNtQueryVirtualMemory)(HANDLE ProcessHandle, PVOID BaseAddress,
    MEMORY_INFORMATION_CLASS MemoryInformationClass,
    PVOID   MemoryInformation,
    SIZE_T  MemoryInformationLength,
    PSIZE_T ReturnLength);

 

  这也就引出了我想要解决的问题——直接调用内核中Nt*系列函数之前,是需要将Previous Mode切换为KernelMode的

  其原因可以参考这篇外文讲的很详细:

  http://www.osronline.com/article.cfm?article=257

 (文中摘录:) 

Previous Mode

 

Time to step back and figure out what all of this means. An important fact to know is that Kernel Mode components by default trust all other Kernel Mode components. Because system services are always processed in Kernel Mode, Windows keeps track of whether the request originated from User Mode or Kernel Mode to determine if the caller is to be implicitly trusted. The system uses the previous mode indicator to determine the mode from which a system service call came. When a call comes from User Mode, previous mode is set to User. When a system service processing routine needs to determine whether or not to implicitly trust its caller, it checks the value of previous mode. If previous mode is set to User, the system service processing routine knows the call came from User Mode and thus any parameters passed in to the function need to be validated before they can be used.

 

This is why the previous mode being set is really the most important part about what we have talked about so far. No matter what a User Mode application does, the system treats its system service request as a User request, coming from User Mode, and goes out of its way to validate the request. All buffers are subject to validation, all access checks are performed, and absolutely no part of the request is implicitly trusted. However, a Kernel Mode request is not as scrutinized and it is assumed that the passed in parameters are valid.

 

If a Kernel component calls the ZwXxx version of a native API, all is well. The previous mode is set to Kernel and the credentials of the Kernel are used. The system service processing routine that is called assumes that any parameters that are passed are valid, because the request came from a Kernel Mode component (and Kernel Mode components implicitly trust each other).

 

The NtXxxx version of the native system service is the name of the function itself. Thus, when a Kernel Mode component calls the NtXxxx version of the system service, whatever is presently set into previous mode is unchanged. Thus, it is quite possible that the Kernel component could be running on an arbitrary User stack, with the requestor mode set to User. The system service will not know any better, attempt to validate the request parameters, possibly using the credentials of the arbitrary User Mode thread, and thus possibly fail the request. Another problem here is that one step in the validation process for a User Mode request is that all passed in buffers have either ProbeForRead or ProbeForWrite executed on them, depending on the buffer’s usage. These routines raise exceptions if executed on Kernel Mode addresses. Therefore, if you pass in Kernel Mode buffers with your request mode set to User, your calls into the native API return STATUS_ACCESS_VIOLATION.

 

The moral of this bedtime story is that if you are in User Mode, use whatever variant you think makes your code look pretty. In Kernel Mode, use the ZwXxx routines and get your previous mode set properly, to Kernel Mode.

  

  简言之,调用NtQueryVirtualMemory中时,会检测当前调用来自用户态还是内核态 ,如果是来自内核态 ,不会检测参数, 而如果是来自用户态 ,就会做一系列的参数检测,  而内核组件可能运行在任意进程的上下文中 , 当它调用NtQueryVirtualMemory时 ,因为Previous Mode很可能是User Mode , 而我们的参数请求的内核态的地址 ,这时通常就会产STATUS_ACCESS_VIOLATION

  关于内核切换调用Nt系列函数的问题,看雪的这篇文章也有讨论,我们可以参考了解:

  http://bbs.pediy.com/thread-55142.htm

 

  

  

  

posted on 2017-05-21 15:26  沉疴  阅读(1860)  评论(0编辑  收藏  举报