从ZwProtectVirtualMemory到NtProtectVirtualMemory-内核函数调用

0x01 前言

  我们知道R3层中,Zw系列函数和Nt系列函数函数是一样的,但是在内核Zw系列函数调用了Nt系列函数,但是为什么要在内核设置一个Zw系列函数而不是直接调用Nt函数呢?Zw系列函数又是怎么调用Nt系列函数的呢?我们利用IDA分析NtosKrnl.exe文件。

0x02 ZwProtectVirtualMemory

  我们先看看ZwProtectVirtualMemory的实现

.text:00406170 ; NTSTATUS __stdcall ZwProtectVirtualMemory(HANDLE ProcessHandle, PVOID *BaseAddress, PULONG ProtectSize, ULONG NewProtect, PULONG OldProtect)
.text:00406170 [email protected]20 proc near    ; CODE XREF: RtlpCreateStack(x,x,x,x,x)+FAp
.text:00406170
.text:00406170 ProcessHandle   = dword ptr  4
.text:00406170 BaseAddress     = dword ptr  8
.text:00406170 ProtectSize     = dword ptr  0Ch
.text:00406170 NewProtect      = dword ptr  10h
.text:00406170 OldProtect      = dword ptr  14h
.text:00406170
.text:00406170                 mov     eax, 89h  //Nt函数的系统调用号
.text:00406175                 lea     edx, [esp+ProcessHandle] //使用EDX指向堆栈上的参数块
.text:00406179                 pushf        //EFLAGS
.text:0040617A                 push    8    //CS   KGDT_R0_CODE
.text:0040617C                 call    _KiSystemService
.text:00406181                 retn    14h    //5个参数,20字节
.text:00406181 [email protected]20 endp

  这里89h为NtProtectVirtualMemory函数在SSDT函数中的调用号,CS寄存器,最后位为0表示当前处于内核态,然后调用KiSystemService函数

0x03 KiSystemService

  我们接着看KiSystemService的函数实现

.text:00407631 _KiSystemService proc near              ; CODE XREF: ZwAcceptConnectPort(x,x,x,x,x,x)+Cp
.text:00407631                                         ; ZwAccessCheck(x,x,x,x,x,x,x,x)+Cp ...
.text:00407631
.text:00407631 arg_0           = dword ptr  4
.text:00407631
.text:00407631                 push    0
.text:00407633                 push    ebp
.text:00407634                 push    ebx
.text:00407635                 push    esi
.text:00407636                 push    edi
.text:00407637                 push    fs              ; 保存用户空间的fs
.text:00407639                 mov     ebx, 30h        ; KGDT_R0_PCR
.text:0040763E                 mov     fs, ebx         ; 使FS段的起点与KPCR数据结构对齐
.text:00407640                 push    dword ptr ds:0FFDFF000h
.text:00407646                 mov     dword ptr ds:0FFDFF000h, 0FFFFFFFFh
.text:00407650                 mov     esi, ds:0FFDFF124h ; #define KPCR_CURRENT_THREAD 0x124
.text:00407650                                         ; 指向当前cpu正在运行的线程
.text:00407650                                         ; FS:0x124
.text:00407650                                         ; PCR的大小只有0x54,这里偏移到了KPRCB中的CurrentThread
.text:00407656                 push    dword ptr [esi+140h]
.text:0040765C                 sub     esp, 48h
.text:0040765F                 mov     ebx, [esp+68h+arg_0] ; 系统调用前夕的CS映像
.text:00407663                 and     ebx, 1          ; 0环的最低位为0,3环的最低位为1
.text:00407666                 mov     [esi+140h], bl  ; 新的"先前模式"  [esi+KTHREAD_PREVIOUS_MODE]
.text:0040766C                 mov     ebp, esp
.text:0040766E                 mov     ebx, [esi+134h] ; KTHREAD结构中的指针TrapFrame [esi+KTHREAD_TRAP_FRAME]
.text:00407674                 mov     [ebp+3Ch], ebx  ; 暂时保存在这里 [ebp+KTRAP_FRAME_EDX]
.text:00407677                 mov     [esi+134h], ebp ; 新的TrapFrame,指向堆栈上的框架 [esi+KTHREAD_TRAP_FRAME]
...
.text:0040769E                 sti
.text:0040769F                 jmp     loc_407781
.text:0040769F _KiSystemService endp

  这里首先要在系统态堆栈上构建一个系统调用"框架Frame",或称为"自陷框架",其作用主要是用来保存发生自陷时CPU中各寄存器的"现场",或者说"上下文",以备返回用户空间时予以恢复。
  Windows内核有个特殊的基本要求,只要CPU在内核运行,FS寄存器就指向一个KPCR的数据结构,FS的值为0x30,其0-1位为0,表示0环,第2位为0,表示GDT表,为1则表示LDT表,3-15位为6,表示在GDT的下标为6的表项中的地址即为KPCR的地址。KPCR是处理器控制块,在单处理器中只有一个KPCR,在多CPU的系统中,每个CPU都有自己的KPCR结构。
  CPU从用户空间进入系统空间时会将当时寄存器CS的内容压入系统态堆栈,CS的最低位就可以说明当时运行于何种模式的标志位。这里取出CS最低位保存在ETHREAD的PREVIOUS_MODE上。
  更新ETHREAD中的TrapFrame框架,保存旧的框架。

0x04 KiFastCallEntry

  KiSystemService中的jmp loc_407781跳转到KiFastCallEntry函数中,代码如下:

.text:004076F0 _KiFastCallEntry proc near              ; DATA XREF: _KiTrap01+6Fo
.text:004076F0                                         ; KiLoadFastSyscallMachineSpecificRegisters(x)+24o
.text:004076F0
.text:004076F0 var_B           = byte ptr -0Bh
.text:004076F0
.text:004076F0 ; FUNCTION CHUNK AT .text:004076C8 SIZE 00000023 BYTES
.text:004076F0 ; FUNCTION CHUNK AT .text:00407990 SIZE 00000014 BYTES
.text:004076F0
.text:004076F0                 mov     ecx, 23h
.text:004076F5                 push    30h
.text:004076F7                 pop     fs
.text:004076F9                 mov     ds, ecx
...
.text:00407781 loc_407781:                             ; CODE XREF: _KiBBTUnexpectedRange+18j
.text:00407781                                         ; _KiSystemService+6Ej
.text:00407781                 mov     edi, eax        ; 系统调用号
.text:00407783                 shr     edi, 8          ; NtProtectVirtualMemory 89h = 10001001
.text:00407783                                         ; shr右移8位为0
.text:00407786                 and     edi, 30h
.text:00407789                 mov     ecx, edi        ; 将ECX变成0x00(SSDT)或者0x10(ShadowSSDT)
.text:0040778B                 add     edi, [esi+0E0h] ; 本线程的系统调用表
.text:0040778B                                         ; EDI指向描述块0或描述块1
.text:00407791                 mov     ebx, eax        ; 将eax中的索引值,赋值给ebx
.text:00407793                 and     eax, 0FFFh      ; SERVICE_NUMBER_MASK定义为0xFFF
.text:00407798                 cmp     eax, [edi+8]    ; 检查系统调用号是否越界
.text:00407798                                         ; SERVICE_DESCRIPTOR_LIMIT定义为8
.text:0040779B                 jnb     _KiBBTUnexpectedRange ; 系统调用号越界,有可能是大于0x1000

  这里eax保存着系统调用号,在KTHREAD中有一个指针ServiceTable,如果是gui线程则指向KeServiceDescriptorTableShadow[],如果不是则指向KeServiceDescriptor[]。这里检查了系统调用号是否越界。多数情况下不会越界,我们继续往下看:

.text:004077A1                 cmp     ecx, 10h        ; 检查ECX的内容是否0x10
.text:004077A4                 jnz     short NotWin32K
...//使用Win32k系统调用表
.text:004077C0 NotWin32K:                              ; CODE XREF: _KiFastCallEntry+B4j
.text:004077C0                                         ; _KiFastCallEntry+C4j
.text:004077C0                 inc     dword ptr ds:0FFDFF638h
.text:004077C6                 mov     esi, edx        ; 使ESI指向用户空间堆栈上的参数块
.text:004077C8                 mov     ebx, [edi+0Ch]  ; [edi+SERVICE_DESCRIPTOR_NUMBER]
.text:004077CB                 xor     ecx, ecx
.text:004077CD                 mov     cl, [eax+ebx]   ; 寄存器ECX
.text:004077D0                 mov     edi, [edi]      ; EDI指向具体的系统调用表
.text:004077D0                                         ; [edi+SERVICE_DESCRIPTOR_BASE]
.text:004077D2                 mov     ebx, [edi+eax*4] ; 函数指针
.text:004077D5                 sub     esp, ecx        ; 系统堆栈上留出空间
.text:004077D7                 shr     ecx, 2          ; 右移2位
.text:004077DA                 mov     edi, esp        ; 目标在系统空间堆栈上
.text:004077DC                 cmp     esi, ds:_MmUserProbeAddress ; 参数块的位置不得高于MmSystemRangeStart-0x10000
.text:004077E2                 jnb     AccessViolation
.text:004077E8
.text:004077E8 loc_4077E8:                             ; CODE XREF: _KiFastCallEntry+2A4j
.text:004077E8                                         ; DATA XREF: _KiTrap0E+106o
.text:004077E8                 rep movsd               ; 复制参数,以ESI为源,EDI为目标,ECX为循环次数
.text:004077EA                 call    ebx             ; 调用目标函数 

  这里将ECX与0x10比较,如果不是0x10则为基本调用表(SSDT函数),转到NotWin32K处。这里ecx的cl保存着KSERVICE_TABLE_DESCRIPTOR结构体中的Number,将cl右移2位就是参数的个数,后面重复执行的movsd的次数就是参数的个数,不过复制之前要调整堆栈指针,将ESP与移位前的ECX相减,在系统空间堆栈上 空出相应的字节数。注意movsd指令以ESI所指处为源,以EDI所指处为目标,另一方面,指令获得函数的指针赋值为ebx,最后call ebx实现了对目标函数的调用。
  一些安全软件对KiFastCallEntry通过Hook实现过滤SSDT框架的时候,通常是在ebx完成赋值之后,在call ebx之前,替换这中间的地方,进入fake1函数,将保存好的参数push,比如edi保存的SSDT表地址,ebx保存函数地址,eax保存调用号,ecx保存参数个数,在这中间hook,可以直接利用系统初始化好的寄存器,然后调用filter函数,通过寄存器的值,过滤指定的SSDT函数,替换ebx的值,然后继续执行KiFastCallEntry中的call ebx,这样就可以过滤整个SSDT系统调用了。

  当执行完成call ebx,从目标函数返回时我们继续看下面的指令:

.text:004077EC loc_4077EC:                             ; CODE XREF: _KiFastCallEntry+2AFj
.text:004077EC                                         ; DATA XREF: _KiTrap0E+126o ...
.text:004077EC                 mov     esp, ebp        ; 回到自陷(系统调用框架)
.text:004077EE
.text:004077EE KeReturnFromSystemCall:                 ; CODE XREF: _KiBBTUnexpectedRange+38j
.text:004077EE                                         ; _KiBBTUnexpectedRange+43j
.text:004077EE                 mov     ecx, ds:0FFDFF124h ; 使ECX只想当前线程的KTHREAD
.text:004077F4                 mov     edx, [ebp+3Ch]  ; 从堆栈中取出保存着的框架指针
.text:004077F4                                         ; [ebp+KTRAP_FRAME_EDX]
.text:004077F7                 mov     [ecx+134h], edx ; 恢复KTHREAD结构中的框架指针
.text:004077F7 _KiFastCallEntry endp ; sp-analysis failed ; [exc+KTHREAD_TRAP_FRAME]

  首先将堆栈指针恢复指向系统调用框架即自陷框架的底部,因为这些参数已经失去意义,然后把原先保存在堆栈上的先前自陷框架指针恢复到当前线程的控制块中。

0x05 KiServiceExit

  然后继续执行KiServiceExit函数

.text:004077FD _KiServiceExit  proc near               ; CODE XREF: _KiSetLowWaitHighThread+7Cj
.text:004077FD                                         ; NtContinue(x,x)+42j ...
.text:004077FD
.text:004077FD arg_C           = dword ptr  10h
.text:004077FD arg_10          = dword ptr  14h
.text:004077FD arg_40          = dword ptr  44h
.text:004077FD arg_44          = dword ptr  48h
.text:004077FD arg_48          = dword ptr  4Ch
.text:004077FD arg_60          = dword ptr  64h
.text:004077FD arg_64          = dword ptr  68h
.text:004077FD arg_68          = dword ptr  6Ch
.text:004077FD arg_6C          = dword ptr  70h
.text:004077FD
.text:004077FD ; FUNCTION CHUNK AT .text:00407908 SIZE 00000088 BYTES
.text:004077FD
.text:004077FD                 cli                     ; 关中断
.text:004077FE                 test    dword ptr [ebp+70h], 20000h
.text:00407805                 jnz     short CHECK_FOR_APC_DELIVER
.text:00407807                 test    byte ptr [ebp+6Ch], 1
.text:0040780B                 jz      short loc_407864
.text:0040780D
.text:0040780D CHECK_FOR_APC_DELIVER:                  ; CODE XREF: _KiServiceExit+8j
.text:0040780D                                         ; _KiServiceExit+63j
.text:0040780D                 mov     ebx, ds:0FFDFF124h
.text:00407813                 mov     byte ptr [ebx+2Eh], 0
.text:00407817                 cmp     byte ptr [ebx+4Ah], 0
.text:0040781B                 jz      short loc_407864 ; 如果先前模式是内核模式,就往前跳转到下面,不递交APC请求
.text:0040781D                 mov     ebx, ebp
.text:0040781F                 mov     [ebx+44h], eax
.text:00407822                 mov     dword ptr [ebx+50h], 3Bh
.text:00407829                 mov     dword ptr [ebx+38h], 23h
.text:00407830                 mov     dword ptr [ebx+34h], 23h
.text:00407837                 mov     dword ptr [ebx+30h], 0
.text:0040783E                 mov     ecx, 1          ; APC_LEVEL,参数
.text:00407843                 call    ds:[email protected]@4 ; 这是快速调用模式的函数,通过寄存器传递参数
.text:00407849                 push    eax             ; 将返回值(老的运行级别)压入堆栈
.text:0040784A                 sti
.text:0040784B                 push    ebx
.text:0040784C                 push    0
.text:0040784E                 push    1
.text:00407850                 call    [email protected]12 ; 执行内核APC,并未用户空间APC的执行进行准备
.text:00407855                 pop     ecx             ; 从堆栈恢复老的运行级别
.text:00407856                 call    ds:[email protected]@4 ; 恢复原来的运行级别,在这里应该是PASSIVE_LEVEL
.text:0040785C                 mov     eax, [ebx+44h]
.text:0040785F                 cli
.text:00407860                 jmp     short CHECK_FOR_APC_DELIVER
.text:00407860 ; ---------------------------------------------------------------------------
.text:00407862                 align 4
.text:00407864
.text:00407864 loc_407864:                             ; CODE XREF: _KiServiceExit+Ej
.text:00407864                                         ; _KiServiceExit+1Ej
...

在KiServiceExit执行的时候,首先关闭中断,然后检查是否有APC请求,如果有就通过KiDeliverApc递交APC请求(插入线程apc队列)。

最后会通过TrapFrame返回r3或者返回内核调用Zw函数的地方。

0x06 总结

一般而言在内核里面不能直接调用Nt函数,一方面是内核并不导出,只能通过ssdt表获得,另一方面,这些函数往往要求在系统空间堆栈上有个属于本次调用的自陷框架,而Zw->KiSystemService->KiFastCallEntry->Nt->KiSystemExit的流程中就有这样一个框架。

时间: 2024-11-08 00:04:24

从ZwProtectVirtualMemory到NtProtectVirtualMemory-内核函数调用的相关文章

使用ftrace学习linux内核函数调用

http://www.cnblogs.com/pengdonglin137/articles/4752082.html 转载: http://blog.csdn.net/ronliu/article/details/6446251 linux中大量使用函数指针钩子,导致阅读代码困难.比如想知道一个函数的调用路径,那么就只能用source insight之类的工具看代码了.有没有办法可以迅速获得调用关系的整体印象?ftrace是内核提供的一种调试工具,可以对内核中发生的事情进行跟 踪.比如函数的调

源码分析:动态分析 Linux 内核函数调用关系

源码分析:动态分析 Linux 内核函数调用关系 时间 2015-04-22 23:56:07  泰晓科技 原文  http://www.tinylab.org/source-code-analysis-dynamic-analysis-of-linux-kernel-function-calls/ 主题 Linux源码分析 By Falcon ofTinyLab.org 2015/04/18 缘由 源码分析是程序员离不开的话题. 无论是研究开源项目,还是平时做各类移植.开发,都避免不了对源码的

查看内核函数调用的调试方法【原创】

方法一. 通过打印函数地址,可以查看函数在哪里调用 例如: Core.c   drivers\pwm int pwm_config(struct pwm_device *pwm, int duty_ns, int period_ns) { if (!pwm || period_ns == 0 || duty_ns > period_ns) return -EINVAL; printk("%s  drivers\pwm Core.c----(%d)\r\n", __func__,

使用 ftrace 调试 Linux 内核,第1部分

ftrace 是 Linux 内核中提供的一种调试工具.使用 ftrace 可以对内核中发生的事情进行跟踪,这在调试 bug 或者分析内核时非常有用.本系列文章对 ftrace 进行了介绍,分为三部分.本文是第一部分,介绍了内核相关的编译选项.用户态访问 ftrace 的接口.ftrace 的数据文件,并对 ftrace 提供的跟踪器的用途进行了介绍,以使读者更好的了解和使用该工具. ftrace 是内建于 Linux 内核的跟踪工具,从 2.6.27 开始加入主流内核.使用 ftrace 可以

分析 kernel32.dll中函数调用流程分析

闲来无事,用IDA, windbg, OD分析kernel32.dll函数调用流程笔记 1.先用分析 CreateFileW,CreateFileA 函数: CreateFileA流程: kernel32.dll!CreateFileA 流程 kernel32.dll!Basep8BitStringToDynamicUnicodeString kernel32.dll!CreateFileWImplementation ; 实际就是导出表 CreateFil 地址 kernel32.dll!Rt

Linux内核调试方法总结之ftrace

ftrace [用途] ftrace包含一系列跟踪器,用于不同的场合,比如跟踪内核函数调用(function tracer).跟踪上下文切换(sched_switch tracer).查看中断被关闭的时长(irqsoff tracer).跟踪内核中的延迟以及性能问题等.Ftrace是内建于Linux的内核跟踪工具,依赖于内核配置宏(Kernel Hacking->Tracers)和debugfs. [原理]                         ftrace本质上是一种静态代码插装技术

2019-举例跟踪分析Linux内核5.0系统调用处理过程

简介 学号520 实验环境基于ubuntu18.04 选择系统调用号20 getpid()分析 实验目的 学会使用gdb工具跟踪linux内核函数调用 学会使用C代码和嵌入式汇编使用系统中断 分析system_call中断处理过程 实验步骤 1.下载linux5.0.1内核并编译 wget https://mirrors.aliyun.com/linux-kernel/v5.x/linux-5.0.1.tar.xz xz -d linux-5.0.1.tar.xz tar -xvf linux-

2.2CUDA-Memory(存储)

在CUDA基本概念介绍有简单介绍CUDA memory.这里详细介绍: 每一个线程拥有自己的私有存储器,每一个线程块拥有一块共享存储器(Shared memory):最后,grid中所有的线程都可以访问同一块全局存储器(global memory).除此之外,还有两种可以被所有线程访问的只读存储器:常数存储器(constant memory)和纹理存储器(Texture memory),它们分别为不同的应用进行了优化.全局存储器.常数存储器和纹理存储器中的值在一个内核函数执行完成后将被继续保持,

对Linux 虚拟内存和物理内存的理解以及Linux下怎样增加虚拟内存

首先,让我们看下linux虚拟内存: 第一层理解 1.         每个进程都有自己独立的4G内存空间,各个进程的内存空间具有类似的结构 2.       一个新进程建立的时候,将会建立起自己的内存空间,此进程的数据,代码等从磁盘拷贝到自己的进程空间,哪些数据在哪里,都由进程控制表中的task_struct记录,task_struct中记录中一条链表,记录中内存空间的分配情况,哪些地址有数据,哪些地址无数据,哪些可读,哪些可写,都可以通过这个链表记录 3.       每个进程已经分配的内存

CUDA并行存储模型

CUDA将CPU作为主机(Host),GPU作为设备(Device).一个系统中可以有一个主机和多个设备.CPU负责逻辑性强的事务处理和串行计算,GPU专注于执行高度线程化的并行处理任务.它们拥有相互独立的存储器(主机端的内存和显卡端的显存). 运行在GPU上的函数称为kernel(内核函数).一个完整的CUDA程序是由一些列的kernel函数和主机端的串行处理步骤共同完成的.CPU串行代码的工作包括在kernel启动前进行的数据准备.设备初始化以及在kernel之间进行一些串行化计算. ker