• API调用过程


    一、API调用过程(3环部分)

    什么是API?

    API,Application Programming Interface 应用程序接口,操作系统的API主要放在C:WINDOWSsystem32 。绝大多数的API都是在Ring0实现的,极少数是在3环,3环只是提供一个接口。

    几个重要的DLL

    1、Kernel32.dll

    最核心的功能模块:如管理内存、进程、线程相关函数

    2、User3.dll

    是Windows用户界面相关应用程序接口,如创建窗口和发送消息

    3、GDI32.dll

    Graphical Device Interface,图形设备接口,包含用于画图和显示文本的函数:如要显示一个程序窗口

    4、Ntdil.dll

    大多数API都会通过这个DLL进入内核

    IDA分析ReadProcessMemory API

    1、Kernel32.dll中搜索ReadProcessMemory

    //5个参数压栈
    .text:7C8021D0 ProcessHandle   = dword ptr  8 	
    .text:7C8021D0 BaseAddress     = dword ptr  0Ch
    .text:7C8021D0 Buffer          = dword ptr  10h
    .text:7C8021D0 BufferLength    = dword ptr  14h
    .text:7C8021D0 lpNumberOfBytesRead= dword ptr  18h
    .text:7C8021D0
    .text:7C8021D0                 mov     edi, edi
    .text:7C8021D2                 push    ebp
    .text:7C8021D3                 mov     ebp, esp
    .text:7C8021D5                 lea     eax, [ebp+BufferLength]
    .text:7C8021D8                 push    eax             ; ReturnLength
    .text:7C8021D9                 push    [ebp+BufferLength] ; BufferLength
    .text:7C8021DC                 push    [ebp+Buffer]    ; Buffer
    .text:7C8021DF                 push    [ebp+BaseAddress] ; BaseAddress
    .text:7C8021E2                 push    [ebp+ProcessHandle] ; ProcessHandle
    .text:7C8021E5                 call    ds:NtReadVirtualMemory ;核心功能在这里
    .text:7C8021EB                 mov     ecx, [ebp+lpNumberOfBytesRead]
    .text:7C8021EE                 test    ecx, ecx
    .text:7C8021F0                 jnz     short loc_7C8021FD
    .text:7C8021F2
    .text:7C8021F2 loc_7C8021F2:                           ; CODE XREF: ReadProcessMemory+32j
    .text:7C8021F2                 test    eax, eax  	
    .text:7C8021F4                 jl      short loc_7C802204 ;小于0则跳转
    .text:7C8021F6                 xor     eax, eax
    .text:7C8021F8                 inc     eax
    .text:7C8021F9
    .text:7C8021F9 loc_7C8021F9:                           ; CODE XREF: ReadProcessMemory+3Cj
    .text:7C8021F9                 pop     ebp
    .text:7C8021FA                 retn    14h
    .text:7C8021FD ; ---------------------------------------------------------------------------
    .text:7C8021FD
    .text:7C8021FD loc_7C8021FD:                           ; CODE XREF: ReadProcessMemory+20j
    .text:7C8021FD                 mov     edx, [ebp+BufferLength]
    .text:7C802200                 mov     [ecx], edx
    .text:7C802202                 jmp     short loc_7C8021F2
    .text:7C802204 ; ---------------------------------------------------------------------------
    .text:7C802204
    .text:7C802204 loc_7C802204:                           ; CODE XREF: ReadProcessMemory+24j
    .text:7C802204                 push    eax             ; NtStatus
    .text:7C802205                 call    sub_7C8093FD
    .text:7C80220A                 xor     eax, eax
    .text:7C80220C                 jmp     short loc_7C8021F9
    

    2、参数压栈后,调用了另外一个函数,返回结果在eax中,小于0的话就跳到这里

    ReadProcessMemory+24j
    .text:7C802204                 push    eax             
    .text:7C802205                 call    sub_7C8093FD	;这是一个API用于设置错误号
    .text:7C80220A                 xor     eax, eax
    .text:7C80220C                 jmp     short loc_7C8021F9
    

    3、Call一个用于设置错误号(BaseSetLastNTError )函数,将eax清0后,又跳回去。

    .text:7C8021F9
    .text:7C8021F9 loc_7C8021F9:                           ; CODE XREF: ReadProcessMemory+3Cj
    .text:7C8021F9                 pop     ebp
    .text:7C8021FA                 retn    14h
    

    跳到这,直接返回,所以失败返回的结果是0.

    4、如果eax>0,eax清0,eax+1,返回1

    分析完这个函数,发现:主要的功能实现全面在NtReadVirtualMemory 这个API里了。

    我们先来看看NtReadVirtualMemory 在哪个DLL中。

    打开导入表,发现在ntdll.dll里。

    7C801418  NtReadVirtualMemory ntdll
    

    在这个dll里搜索一下NtReadVirtualMemory

    发现这个函数里也没什么实现细节,只是提供一个操作码,通过这种方式实现从3环进入0环,而真正的函数实现是在0环。

    .text:7C92D9E0                 mov     eax, 0BAh       ; NtReadVirtualMemory
    .text:7C92D9E5                 mov     edx, 7FFE0300h  ;决定了什么方式进0环
    .text:7C92D9EA                 call    dword ptr [edx]
    .text:7C92D9EC                 retn    14h
    

    我们自己在3环实现一个ReadProcessMemory ,自己实现的好处就在于 能够防止被人来通过API断点来分析我们的程序,对我们的程序进行hook检测。

    具体的实现如下:

    #include<windows.h>
    #include<stdio.h>
    
    void __declspec(naked) read(HANDLE hProcess,LPVOID addr,LPVOID buffer,DWORD len)
    {
    	_asm
    	{
    		mov   eax, 0BAh
    		mov   edx, 7FFE0300h
    		call  dword ptr[edx]
    
    		ret
    	}
    }
    
    int main(int argc, char* argv[])
    {
    	HWND hwnd = FindWindow("#32770","xxx");
    	DWORD pid;
    	GetWindowThreadProcessId(hwnd,&pid);
    	HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
    	DWORD val = 0;
    
    	read(hProcess,(LPVOID)0x001D6EBC,&val,4);
    
    	printf("val: %d", val);
    
    	getchar();
    	return 0;
    }
    
    

    (二)API调用过程(3环进0环)

    1、_KUSER_SHARED_DATA 结构

    在用户层和核心层分别定义了一个 _KUSER_SHARED_DATA 结构区域,用于用户层和内核层共享某些数据。

    它们使用固定的地址值映射,_KUSER_SHARED_DATA 结构区域在用户层和内核层的线性地址分别为:

    用户层: 0x7ffe0000

    内核层: 0xffdf0000

    这两个线性地址对应的物理页是一样的。如图在windbg里查看这两个地址的数据:

    备注:虽然指向的是同一个物理页,但在User层是只读的,在Kernel层是可读可写的。

    再来看看_KUSER_SHARED_DATA 这个结构,windbg 中输入 dt _KUSER_SHARED_DATA

    0: kd> dt _KUSER_SHARED_DATA
    nt!_KUSER_SHARED_DATA
       +0x000 TickCountLow     : Uint4B
       +0x004 TickCountMultiplier : Uint4B
       +0x008 InterruptTime    : _KSYSTEM_TIME
       +0x014 SystemTime       : _KSYSTEM_TIME
       +0x020 TimeZoneBias     : _KSYSTEM_TIME
       +0x02c ImageNumberLow   : Uint2B
       +0x02e ImageNumberHigh  : Uint2B
       +0x030 NtSystemRoot     : [260] Uint2B
       +0x238 MaxStackTraceDepth : Uint4B
       +0x23c CryptoExponent   : Uint4B
       +0x240 TimeZoneId       : Uint4B
       +0x244 Reserved2        : [8] Uint4B
       +0x264 NtProductType    : _NT_PRODUCT_TYPE
       +0x268 ProductTypeIsValid : UChar
       +0x26c NtMajorVersion   : Uint4B
       +0x270 NtMinorVersion   : Uint4B
       +0x274 ProcessorFeatures : [64] UChar
       +0x2b4 Reserved1        : Uint4B
       +0x2b8 Reserved3        : Uint4B
       +0x2bc TimeSlip         : Uint4B
       +0x2c0 AlternativeArchitecture : _ALTERNATIVE_ARCHITECTURE_TYPE
       +0x2c8 SystemExpirationDate : _LARGE_INTEGER
       +0x2d0 SuiteMask        : Uint4B
       +0x2d4 KdDebuggerEnabled : UChar
       +0x2d5 NXSupportPolicy  : UChar
       +0x2d8 ActiveConsoleId  : Uint4B
       +0x2dc DismountCount    : Uint4B
       +0x2e0 ComPlusPackage   : Uint4B
       +0x2e4 LastSystemRITEventTickCount : Uint4B
       +0x2e8 NumberOfPhysicalPages : Uint4B
       +0x2ec SafeBootMode     : UChar
       +0x2f0 TraceLogging     : Uint4B
       +0x2f8 TestRetInstruction : Uint8B
       +0x300 SystemCall       : Uint4B
       +0x304 SystemCallReturn : Uint4B
       +0x308 SystemCallPad    : [3] Uint8B
       +0x320 TickCount        : _KSYSTEM_TIME
       +0x320 TickCountQuad    : Uint8B
       +0x330 Cookie           : Uint4B
    

    2、分析7FFE0300h进Ring0的方式

    在之前分析的NtReadVirtualMemory中的代码如下:

    .text:7C92D9E0                 mov     eax, 0BAh       ; NtReadVirtualMemory
    .text:7C92D9E5                 mov     edx, 7FFE0300h  ;决定了什么方式进0环
    .text:7C92D9EA                 call    dword ptr [edx]
    .text:7C92D9EC                 retn    14h
    

    当通过eax=1来执行cpuid指令时,处理器的特征信息被放在ecx和edx寄存器中,其中edx包含了一个SEP位,该位处于第11位,指明了当前CPU是否支持 sysenter、sysexit 指令,如果支持说明支持快速调用。

    当SEP=1时,表明支持快速调用 , ntdll.dll!KiFastSystemCall()

    当SEP=0时,表明不支持快速调用,ntdll.dll!KiIntSystemCall()。

    7FFE0300h决定了以什么样的方式进入Ring0,那么7FFE0300h到底是如何决定的呢?

    接下来我们来拆解一下7FFE0300h:

    7FFE0300
    ‭0111  1111  1111  1110  0000  0011  0000  0000‬
    

    第11位是0,所以不支持快速调用。

    进入0环需要修改的寄存器

    1、CS的权限由Ring3->Ring0,CS会发生改变

    2、SS与CS的权限一致

    3、权限发生切换的时候,堆栈也一定会切换,需要新的ESP

    4、进Ring0后的代码位置

    3、两种进Ring0的方式

    3.1 API通过中断门进Ring0

    如上所示的7FFE0300h就不支持快速调用,那么这种情况该如何进内核呢?

    我们可以通过中断门进Ring0:

    .text:7C92E500                 public KiIntSystemCall
    .text:7C92E500 KiIntSystemCall proc near               ; DATA XREF: .text:off_7C923428o
    .text:7C92E500
    .text:7C92E500 arg_4           = byte ptr  8
    .text:7C92E500
    .text:7C92E500                 lea     edx, [esp+arg_4] ;edx是参数指针,系统调用号在eax寄存器
    .text:7C92E504                 int     2Eh             ; DOS 2+ internal - EXECUTE COMMAND
    .text:7C92E504                                         ; DS:SI -> counted CR-terminated command string
    .text:7C92E506                 retn
    .text:7C92E506 KiIntSystemCall endp
    

    eax里的值是内核操作码,edx存的是参数的指针,api统一中断号0x2e,因为idt表在2e的位置。

    3.2 通过int 0x2e进Ring0

    具体步骤如下:

    1、在IDT表中找到0x2e号的描述符

    2、分析CS/SS/ESP/EIP的来源

    3、分析EIP是什么

    通过 >dq idtr L30 找到0x2e处的描述符,如图所示:

    0: kd> dq idtr L30
    8003f400  80548e00`000831a0 80548e00`0008331c
    8003f410  00008500`0058113e 8054ee00`00083730
    8003f420  8054ee00`000838b0 80548e00`00083a10
    8003f430  80548e00`00083b84 80548e00`000841fc
    8003f440  00008500`00501198 80548e00`00084600
    8003f450  80548e00`00084720 80548e00`00084860
    8003f460  80548e00`00084ac0 80548e00`00084dac
    8003f470  80548e00`000854a8 80548e00`000857e0
    8003f480  80548e00`00085900 80548e00`00085a3c
    8003f490  80548500`00a057e0 80548e00`00085ba4
    8003f4a0  80548e00`000857e0 80548e00`000857e0
    8003f4b0  80548e00`000857e0 80548e00`000857e0
    8003f4c0  80548e00`000857e0 80548e00`000857e0
    8003f4d0  80548e00`000857e0 80548e00`000857e0
    8003f4e0  80548e00`000857e0 80548e00`000857e0
    8003f4f0  80548e00`000857e0 806e8e00`0008710c
    8003f500  00000000`00080000 00000000`00080000
    8003f510  00000000`00080000 00000000`00080000
    8003f520  00000000`00080000 00000000`00080000
    8003f530  00000000`00080000 00000000`00080000
    8003f540  00000000`00080000 00000000`00080000
    8003f550  8054ee00`000829ce 8054ee00`00082ad0
    8003f560  8054ee00`00082c80 8054ee00`0008360c
    8003f570  8054ee00`00082451 80548e00`000857e0
    

    如图所示的最后一行的位置:8054ee00`00082451

    0008就是CS,高4字节+低4字节就是EIP:80542451 ,SS和ESP是TSS提供的,如图所示:

    通过 >u 80542451 查看反汇编,如图:

    0: kd> u 80542451
    nt!KiSystemService:
    

    EIP:KiSystemService(),这个函数在内核模块里:

    1、固定中断号为0x2e

    2、CS/EIP由门描述符提供,ESP/SS由TSS提供

    3、进入Ring0后执行内核函数: NT!KiSystemService

    4、通过iret/iretd指令返回到用户模式

    3.3 sysenter进Ring0

    通过快速调用进Ring0,分析KiFastSystemCall()函数

    .text:7C92E4F0                 public KiFastSystemCall
    .text:7C92E4F0 KiFastSystemCall proc near              ; DATA XREF: .text:off_7C923428o
    .text:7C92E4F0                 mov     edx, esp ;edx 3环栈顶,系统调用号在eax寄存器
    .text:7C92E4F2                 sysenter ;寄存器数据传递
    .text:7C92E4F2 KiFastSystemCall endp ; sp-analysis failed
    

    eax是操作码,edx是返回地址以及参数的指针。

    1、为什么叫快速调用

    中断门进Ring0需要CS、EIP在IDT表中,需要查内存。

    假设CPU支持sysenter指令时,OS会提前将CS、SS、EIP、ESP的值存储在MSR寄存器中,sysenter指令执行时,CPU会将MSR寄存器中的值直接写入相关的寄存器,没有读取内存的过程,所以叫快速调用。

    2、在执行sysenter指令前,OS必须指定0环的CS段、SS段、EIP以及ESP。

    MSR寄存器 MSR地址 含义
    IA32_SYSENTER_CS 174h 低16位值指定了特权级0的代码段和栈段的段选择符
    IA32_SYSENTER_ESP 175h 内核栈指针的32位偏移
    IA32_SYSENTER_EIP 176h 目前例程的32位偏移

    如上表所示:0x174位置存放的是新的CS,0x175位置存放的是新的ESP,0x176位置存放的是新的EIP。SS并没有存在MSR寄存器里,执行sysenter后,CS+8就是SS。

    可以通过RDMSR/WRMST来进行读写。

    windbg查看这几个值:

       rdmsr 174	 //查看CS
       rdmsr 175	//查看ESP
       rdmsr 176	//查看EIP
    
    0: kd> rdmsr 174
    msr[174] = 00000000`00000008
    0: kd> rdmsr 175
    msr[175] = 00000000`bacd0000
    0: kd> rdmsr 176
    msr[176] = 00000000`80542520
    

    EIP:KiFastCallEntry()

    0: kd> u 80542520
    nt!KiFastCallEntry:
    80542520 b923000000      mov     ecx,23h
    80542525 6a30            push    30h
    80542527 0fa1            pop     fs
    80542529 8ed9            mov     ds,cx
    8054252b 8ec1            mov     es,cx
    8054252d 648b0d40000000  mov     ecx,dword ptr fs:[40h]
    80542534 8b6104          mov     esp,dword ptr [ecx+4]
    80542537 6a23            push    23h
    

    用户代码调用sysenter指令以前,必须将要返回的指令地址和栈指针保存到edx和ecx寄存器中,否则,内核模式代码将来无法设置正确的值,以使sysexit还能返回到用户模式代码原来的位置。

    API通过sysenter指令进0环:

    1、CS、ESP、EIP由MSR寄存器提供

    2、进0环后执行的内核函数: NT!KiCallEntry

    3、通过sysexit指令返回用户模式

    sysexit:

    将IA32_SYSENTER_CS+16装载到CS寄存器;将edx寄存器中的指针装载到eip寄存器中;(指定用户模式代码段)

    将IA32_SYSENTER_CS+24装载到SS寄存器;将ecx寄存器中的指针装载到esp寄存器中;(指定用户模式栈段)

    切换到特权级3,执行eip寄存器中指定的用户代码。

    (三)API调用过程(保护现场)

    为什么要保存现场?

    要理解保存现场的必要性,首先得明白:

    1、API调用过程中发生了什么?

    内核API调用的过程,会涉及到进程的切换,如从3环切换到0环。

    2、这些东西在调用前后有什么变化?

    3、保存现场在这其中的作用?

    如何保存现场?

    先来熟悉一个结构:_KTrap_Frame 结构

    0: kd> dt _KTrap_Frame
    nt!_KTRAP_FRAME
    
    //调试系统服务
       +0x000 DbgEbp           : Uint4B
       +0x004 DbgEip           : Uint4B
       +0x008 DbgArgMark       : Uint4B
       +0x00c DbgArgPointer    : Uint4B
      
     //当需要调整栈时,以下作为临时变量
       +0x010 TempSegCs        : Uint4B
       +0x014 TempEsp          : Uint4B
     
     //调试寄存器
       +0x018 Dr0              : Uint4B
       +0x01c Dr1              : Uint4B
       +0x020 Dr2              : Uint4B
       +0x024 Dr3              : Uint4B
       +0x028 Dr6              : Uint4B
       +0x02c Dr7              : Uint4B
       
     //段寄存器
       +0x030 SegGs            : Uint4B
       +0x034 SegEs            : Uint4B
       +0x038 SegDs            : Uint4B
       
     //易失寄存器
       +0x03c Edx              : Uint4B
       +0x040 Ecx              : Uint4B
       +0x044 Eax              : Uint4B
       
     //非易失寄存器需要在中断历程中先保存
       +0x048 PreviousPreviousMode : Uint4B
       +0x04c ExceptionList    : Ptr32 _EXCEPTION_REGISTRATION_RECORD
       +0x050 SegFs            : Uint4B
       
     //非易失寄存器
       +0x054 Edi              : Uint4B
       +0x058 Esi              : Uint4B
       +0x05c Ebx              : Uint4B
       +0x060 Ebp              : Uint4B
       
      //硬件填充,通过中断门进入的话,这个值就是NULL
       +0x064 ErrCode          : Uint4B
       
      //中断发生时保存被中断的代码段和地址,iret返回到此地址
       +0x068 Eip              : Uint4B //硬件填充
       +0x06c SegCs            : Uint4B //硬件填充
       +0x070 EFlags           : Uint4B //硬件填充
       
       +0x074 HardwareEsp      : Uint4B
       +0x078 HardwareSegSs    : Uint4B
       +0x07c V86Es            : Uint4B
       +0x080 V86Ds            : Uint4B
       +0x084 V86Fs            : Uint4B
       +0x088 V86Gs            : Uint4B
    

    无论是通过中断门进入0环,还是通过快速调用,3环所有的寄存器都会保存在这个结构里。

    通过快速调用是不需要68~78号寄存器的。

    IDA打开ntkrnlpa.exe, 查看KiSystemService函数细节如下:

    (如果找不到这个函数,那可能就是符号库没加载成功,可以将调试机上的pdb文件拷到对应的被调试机的ntkrnlpa.exe目录)

    .text:0046A451 _KiSystemService proc near              ; CODE XREF: ZwAcceptConnectPort(x,x,x,x,x,x)+Cp
    .text:0046A451                                         ; ZwAccessCheck(x,x,x,x,x,x,x,x)+Cp ...
    .text:0046A451
    .text:0046A451 arg_0           = dword ptr  4
    .text:0046A451
    .text:0046A451                 push    0	;保存ErrCode到esp0
    .text:0046A453                 push    ebp	;保存ebp到esp0
    .text:0046A454                 push    ebx	;保存ebx到esp0
    .text:0046A455                 push    esi	;保存esi到esp0
    .text:0046A456                 push    edi	;保存edi到esp0
    .text:0046A457                 push    fs	;保存fs到esp
    .text:0046A459                 mov     ebx, 30h ;FS寄存器,指向KPCR
    .text:0046A45E                 mov     fs, bx
    
    

    权限发生切换时,堆栈也会发生改变。

    内核层的ESP=TSS+4

    Windows操作系统在每个处理器初始化时,会在GDT中为它构造一个TSS段,然后利用ltr指令设置处理器的任务环境段。

    另外,Windows每次切换线程时,总会设置好TSS中0环的ESP,使其指向当前线程的内核栈。

    保存3环寄存器值到_KTrap_Frame的0x50~0x64 的位置

    将fs:0030做拆分,0030转化成二进制 0000 0000 0011 0000,RPL=0,index=6,即查GDT表的第6组:【ffc093df`f0000001 】

    ffdff000这个地址刚好指向的就是CPU的KPCR结构,fs在3环指向的事PEB结构,在0环指向的就是KPCR。

    .text:0046A461                 assume fs:nothing
    .text:0046A461                 push    large dword ptr fs:0 ;保存老的ExceptionList,KPCR+0x00-> _NT_TIB -> ExceptionList
    .text:0046A468                 mov     large dword ptr fs:0, 0FFFFFFFFh ;新的ExceptionList是空白的
    .text:0046A473                 mov     esi, large fs:124h ;得到当前正在执行的线程信息:KPCR
    .text:0046A47A                 push    dword ptr [esi+140h] ;保存老的“先前模式”到堆栈
    														;KTHREAD
    														;+0x140 PreviousMode
    .text:0046A480                 sub     esp, 48h 			;ESP _KTRAP_FRAME 结构指针
    .text:0046A483                 mov     ebx, [esp+68h+arg_0] ;取出3环压入的参数CS _KTRAP_FRAME + 0x6C
    .text:0046A487                 and     ebx, 1 ;0环最低为0, 3环最低为1
    .text:0046A48A                 mov     [esi+140h], bl  ;新的先前模式
    .text:0046A490                 mov     ebp, esp		   ;ESP==EDP  _KTRAP_FRAME结构指针
    .text:0046A492                 mov     ebx, [esi+134h]  ;_KTHREAD中TrapFrame
    .text:0046A498                 mov     [ebp+3Ch], ebx	;将_KTHREAD中的TrapFrame暂存在这个位置,后面会将这个值取出来,重新恢复给_KTHREAD中TrapFrame
    
    

    Exceptionlist是异常链表

    把老的Exceptionlist保存到_KTRAP_FRAME的 +0x4c位置,然后把它清0.

    将KPCR+0x124放在esi中,也就是说将CurrentThread放在esi中。

    将[esi+140]保存先前模式(_KTHREAD.PreviousMode )到_KTRAP_FRAME 的+0x48的位置。

    再将堆栈提升0x48.

    取出3环的CS放到ebx,然后将 esi+0x140=ebx &1,此时esi+0x140存储的就是新的先前模式。

    那么这里反复提到的先前模式到底有什么用呢?

    当我们调用这段代码的时候,如果是0环程序的先前模式就存0,3环就存1。OS通过先前模式就可以知道到底是哪一环的程序在调用自己。

    提升栈底ebp与esp指向同一个位置 _KTRAP_FRAME 最开始的位置。

    取出esi+0x134的位置是_KTHREAD.TrapFrame ,放在ebp+3c ???

    .text:0046A49B                 mov     [esi+134h], ebp ;将堆栈中形成的_KTRAP_FRAME的结构指针赋值给_KTHREAD中的TrapFrame
    .text:0046A4A1                 cld
    .text:0046A4A2                 mov     ebx, [ebp+60h] ;3环的EBP
    .text:0046A4A5                 mov     edi, [ebp+68h] ;3环的EIP
    .text:0046A4A8                 mov     [ebp+0Ch], edx  ;edx存储的是3环参数的指针
    
    

    _KTRAP_FRAME 放到 _KTHREAD.TrapFrame

    把3环的ebp放到ebx

    把3环的eip放到edi

    把3环的参数指针放到 _KTRAP_FRAME.DbgArgPointer

    .text:0046A4AB                 mov     dword ptr [ebp+8], 0BADB0D00h
    .text:0046A4B2                 mov     [ebp+0], ebx  ;3环的ebp存储到_KTRAP_FRAME +0x00  DbgEbp 的位置
    .text:0046A4B5                 mov     [ebp+4], edi  ;3环的eip存储到_KTRAP_FRAME+0x004 DbgEip的位置
    .text:0046A4B8                 test    byte ptr [esi+2Ch], 0FFh ;判断_KTHREAD的+0x2c  DebugActive 是否为-1
    .text:0046A4BC                 jnz     Dr_kss_a	;如果处于调试状态,跳转
    .text:0046A4C2
    .text:0046A4C2 loc_46A4C2:                           ; CODE XREF: Dr_kss_a+10j
    .text:0046A4C2                                       ; Dr_kss_a+7Cj
    .text:0046A4C2                 sti				    ;关闭中断
    .text:0046A4C3                 jmp     loc_46A5AF	 ;取出从3环传进来的系统调用号
    .text:0046A4C3 _KiSystemService endp
    

    0BADB0D00h 这是操作系统用到的一个标志,将它放到 _KTRAP_FRAME.DbgArgMark 。

    ebp+0x00 = _KTRAP_FRAME.DbgEbp
    ebp+0x04 = _KTRAP_FRAME.DbgEip

    比较 _KTHREAD.DebugActive ,判断当前线程是否处于调试状态,如果是这个值就不是0ff。处于调试状态中的代码就会跳转到下面的代码。

    .text:0046A34C Dr_kss_a        proc near               ; CODE XREF: _KiSystemService+6Bj
    .text:0046A34C                 test    dword ptr [ebp+70h], 20000h ;是否是虚拟8086模式(Eflags标志寄存器的VM位)
    .text:0046A353                 jnz     short loc_46A362 ;不是跳转
    .text:0046A355                 test    dword ptr [ebp+6Ch], 1
    .text:0046A35C                 jz      loc_46A4C2	;关闭中断
    .text:0046A362
    .text:0046A362 loc_46A362:                             ; CODE XREF: Dr_kss_a+7j
    .text:0046A362                 mov     ebx, dr0
    .text:0046A365                 mov     ecx, dr1
    .text:0046A368                 mov     edi, dr2
    .text:0046A36B                 mov     [ebp+18h], ebx ;存储Dr0寄存器到 _KTRAP_FRAME+0x18
    .text:0046A36E                 mov     [ebp+1Ch], ecx ;存储Dr1寄存器到 _KTRAP_FRAME+0x1C
    .text:0046A371                 mov     [ebp+20h], edi ;存储Dr0寄存器到 _KTRAP_FRAME+0x20
    .text:0046A374                 mov     ebx, dr3
    .text:0046A377                 mov     ecx, dr6
    .text:0046A37A                 mov     edi, dr7
    .text:0046A37D                 mov     [ebp+24h], ebx ;存储Dr3寄存器到_KTRAP_FRAME
    .text:0046A380                 mov     [ebp+28h], ecx ;存储Dr6寄存器到_KTRAP_FRAME
    .text:0046A383                 xor     ebx, ebx
    .text:0046A385                 mov     [ebp+2Ch], edi ;存储Dr7寄存器到_KTRAP_FRAME
    .text:0046A388                 mov     dr7, ebx	;将Dr7寄存器清零
    .text:0046A38B                 mov     edi, large fs:20h ;得到_KPRCD指针
    .text:0046A392                 mov     ebx, [edi+2F8h]
    .text:0046A398                 mov     ecx, [edi+2FCh]
    .text:0046A39E                 mov     dr0, ebx
    .text:0046A3A1                 mov     dr1, ecx
    

    就是dr0~dr7存到_KTRAP_FRAME +0x18~+0x2c 。

    如果当前线程的状态不处于调试的状态,就会转到下面这段代码:

    .text:0046A5AF loc_46A5AF:                             ; CODE XREF: _KiBBTUnexpectedRange+18j
    .text:0046A5AF                                         ; _KiSystemService+72j
    .text:0046A5AF                 mov     edi, eax
    .text:0046A5B1                 shr     edi, 8
    .text:0046A5B4                 and     edi, 30h
    .text:0046A5B7                 mov     ecx, edi
    .text:0046A5B9                 add     edi, [esi+0E0h]
    .text:0046A5BF                 mov     ebx, eax
    .text:0046A5C1                 and     eax, 0FFFh
    .text:0046A5C6                 cmp     eax, [edi+8]
    .text:0046A5C9                 jnb     _KiBBTUnexpectedRange
    .text:0046A5CF                 cmp     ecx, 10h
    .text:0046A5D2                 jnz     short loc_46A5EF
    .text:0046A5D4                 mov     ecx, large fs:18h
    .text:0046A5DB                 xor     ebx, ebx
    .text:0046A5DD
    .text:0046A5DD loc_46A5DD:                             ; DATA XREF: _KiTrap0E+117o
    .text:0046A5DD                 or      ebx, [ecx+0F70h]
    .text:0046A5E3                 jz      short loc_46A5EF
    .text:0046A5E5                 push    edx
    .text:0046A5E6                 push    eax
    .text:0046A5E7                 call    ds:_KeGdiFlushUserBatch
    .text:0046A5ED                 pop     eax
    .text:0046A5EE                 pop     edx
    .text:0046A5EF
    .text:0046A5EF loc_46A5EF:                             ; CODE XREF: _KiFastCallEntry+B2j
    .text:0046A5EF                                         ; _KiFastCallEntry+C3j
    

    我们跟踪代码发现,无论是KiFastCallEntry 还是 KisystenService ,最终都会调用一下这个代码。

    kiSystenService:
    进入0环后原来3环的寄存器保存在_KTrap_Frame的0x50~0x64
    把3环的参数指针放到_KTRAP_FRAME.DbgArgPointer

    以上过程是kiSystenService从0环进入3环的填表过程。

    KiFastCallEntry的填表过程

    .text:0046A520                 mov     ecx, 23h
    .text:0046A525                 push    30h
    .text:0046A527                 pop     fs       ;修改fs寄存器为30
    .text:0046A529                 mov     ds, ecx
    .text:0046A52B                 mov     es, ecx
    .text:0046A52D                 mov     ecx, large fs:40h ;获取当前TSS
    .text:0046A534                 mov     esp, [ecx+4] ;TSS中得到ESP
    .text:0046A537                 push    23h		;原SS压栈
    .text:0046A539                 push    edx		;原ESP压栈
    .text:0046A53A                 pushf			;EFLAGS压栈
    

    TSS = KPCR+40,ESP0=TSS+4(0表示0环)

    push到 _KTrap_Frame

    +0x078 HardwareSegSs : Uint4B
    
    +0x074 HardwareEsp : Uint4B
    
    +0x070 EFlags : Uint4B
    

    完整的IDA分析代码如下:

    .text:0046A520
    .text:0046A520                 mov     ecx, 23h
    .text:0046A525                 push    30h
    .text:0046A527                 pop     fs		;修改fs寄存器为30
    .text:0046A529                 mov     ds, ecx
    .text:0046A52B                 mov     es, ecx
    .text:0046A52D                 mov     ecx, large fs:40h  ;获取当前TSS
    .text:0046A534                 mov     esp, [ecx+4]		  ;TSS中得到ESP
    .text:0046A537                 push    23h				;原ss压栈
    .text:0046A539                 push    edx				;原esp压栈
    .text:0046A53A                 pushf				    ;EFLAGS压栈
    .text:0046A53B
    .text:0046A53B loc_46A53B:                             ; CODE XREF: _KiFastCallEntry2+23j
    .text:0046A53B                 push    2
    .text:0046A53D                 add     edx, 8	;当前保存着systener进入前的esp的值,esp+8=参数指针
    .text:0046A540                 popf
    .text:0046A541                 or      byte ptr [esp+1], 2 ;KtrapFrame->Eflags.if = 1
    .text:0046A546                 push    1Bh  	; KtrapFrame->CS=0x1b 保存r3的cs
    .text:0046A548                 push    dword ptr ds:0FFDF0304h  ; KtrapFrame->EIP =返回地址
    .text:0046A54E                 push    0  ; KtrapFrame->Error = 0
    .text:0046A550                 push    ebp	; KtrapFrame->ebp = ebp
    .text:0046A551                 push    ebx	; KtrapFrame->ebx = ebx
    .text:0046A552                 push    esi  ; KtrapFrame->esi = esi
    .text:0046A553                 push    edi   ; KtrapFrame->edi = edi
    .text:0046A554                 mov     ebx, large fs:1Ch
    .text:0046A55B                 push    3Bh		; KtrapFrame->SegFs = 0x3b 保存r3的fs
    .text:0046A55D                 mov     esi, [ebx+124h]  ; 得到当前线程结构
    .text:0046A563                 push    dword ptr [ebx]    ; KtrapFrame->0x4c 保存原异常链
    .text:0046A565                 mov     dword ptr [ebx], 0FFFFFFFFh  ; 设置为空的异常链
    .text:0046A56B                 mov     ebp, [esi+18h]   ; 得到初始堆栈KtrapFrame->0x48
    .text:0046A56E                 push    1   ; KtrapFrame->PreviousPreviousMode = 1
    .text:0046A570                 sub     esp, 48h    ; 提升栈顶指针到_Ktrap_Frame
    .text:0046A573                 sub     ebp, 29Ch	; Ktrap_Frame
    .text:0046A579                 mov     byte ptr [esi+140h], 1   ; 先前模式
    .text:0046A580                 cmp     ebp, esp
    .text:0046A582                 jnz     short loc_46A511
    .text:0046A584                 and     dword ptr [ebp+2Ch], 0   ; 清零dr7
    .text:0046A588                 test    byte ptr [esi+2Ch], 0FFh  ; 检查是否处于调试状态
    .text:0046A58C                 mov     [esi+134h], ebp
    .text:0046A592                 jnz     Dr_FastCallDrSave
    .text:0046A598
    .text:0046A598 loc_46A598:                             ; CODE XREF: Dr_FastCallDrSave+10j
    .text:0046A598                                         ; Dr_FastCallDrSave+7Cj
    .text:0046A598                 mov     ebx, [ebp+60h]  ; ebp = KtrapFrame->ebp
    .text:0046A59B                 mov     edi, [ebp+68h]   ; edi = KtrapFrame->eip
    .text:0046A59E                 mov     [ebp+0Ch], edx   ; KtrapFrame->DbgArgPointer = 参数指针
    .text:0046A5A1                 mov     dword ptr [ebp+8], 0BADB0D00h
    .text:0046A5A8                 mov     [ebp+0], ebx    ; KtrapFrame->DbgEbp = ebx
    .text:0046A5AB                 mov     [ebp+4], edi    ; KtrapFrame->DbgEip = edi
    .text:0046A5AE                 sti
    

    (四)API调用(系统服务表)

    1、前言

    API从Ring3到Ring0需要带两个寄存器:eax,edx。其中eax保存的是系统的服务号,edx保存的事Ring3的esp,我们可以通过esp找到三环的参数。

    那我们该如何找到Ring0的参数呢?

    本篇主要解决的是,如何通过eax找到Ring0的函数,Ring0的函数是如何被调用的,并且在这个过程中,Ring0的函数是如何使用Ring3的参数的?

    2、系统服务表

    在OS内核里,有一张系统服务表(SystemServiceTable),如下图:

    在这里有4个成员分别为:

    • ServiceTable 这个成员是个地址,通过这个地址可以找到一个函数地址表,从三环进0环的eax服务号就是函数地址表的索引。

    • count当前的系统服务表被调用了多少次。

    • ServiceLimit 保存的是函数地址表的大小,即服务函数的个数。

    • ArgmentTable 函数参数表,里面保存的是函数地址表对应的参数个数。

    • 总共有两份系统服务表,其中一份函数来自于Ntoskrl.exe 内核模块的导出函数,里面保存常用的系统服务;另一份来自Win32k.sys的导出函数,里面是与图形和用户界面相关的系统服务。它们向三环提供的内核函数全在这两张表里。

    3、如何找到系统服务表

    系统服务表位于_KTHREAD结构体0xE0的位置。

    4、判断调用的函数在哪个表

    • 要找到两张表取决于eax系统服务号,这个系统服务号总共有32位,但是真正只使用了13位。

    • 系统服务号在使用的时候分为两部分,低12位表示的是函数地址表的索引,下标为12的位置的值,决定使用哪张表。

    • 如果第12位为0,则找第一张表(图中的白色区域);如果第12位为1,则找第二张表(图中的灰色区域)。

    5、分析API函数的调用过程

    通过以上的系统服务表分析,思考一下0环代码是如何通过服务号找到0环的函数的?0环的函数是如何使用在三环的参数的?

    用IDA打开 ntkrnlpa.exe ,找到 KiFastCallEntry 、KiSystemService 函数,KiFastCallEntry 和 KiSystemService前面的代码都是用于保存现场,代码如下:

    保存现场后,取出3环传进来的系统调用号:

    .text:0046A5AF                 mov     edi, eax ;取出3环传进来的系统调用号
    

    首先将3环传递过来的系统调用号保存到edi里:

    .text:0046A5B1                 shr     edi, 8  ;系统调用号右移8位
    .text:0046A5B4                 and     edi, 30h ;判断调用号的第12位是0还是1
    .text:0046A5B7                 mov     ecx, edi	;ecx存储的值是00 或 0x10
    

    然后将系统调用号右移8位,然后再和0x30做与运算。此时,edi的结果只能有两种,要么是0x0,要么是0x10。如果是0的话,就说明调用号下标为12的位置是0,如果edi的结果是0x10,那么调用号下标为12的位置是1。

    .text:0046A5B9                 add     edi, [esi+0E0h] ;edi指向KTHREAD-->ServiceTable
    

    esi指向的是ETHREAD线程结构体,ETHREAD+0xE位置存的就是ServiceTable。

    当edi=0时,ServiceTable+edi 指向的就是第一张ServiceTable;

    当edi=0x10时,ServiceTable+edi 指向的就是第二张ServiceTable。

    .text:0046A5BF                 mov     ebx, eax ;把三环的系统服务号存到ebx备份
    .text:0046A5C1                 and     eax, 0FFFh ;系统调用号,只保留后12位
    

    接着把系统调用号备份到ebx,然后 &0FFFh,保留后12位。

    .text:0046A5C6                 cmp     eax, [edi+8] 
    .text:0046A5C9                 jnb     _KiBBTUnexpectedRange
    

    [edi+8]存的是ServiceLimit(服务函数个数),如果传入的调用号的低12位>ServiceLimit,就jmp到_KiBBTUnexpectedRange。 这里是判断是否越界,即要找的函数地址有没有超过函数地址表的范围,如果没有越界就继续往下走。

    .text:0046A5CF                 cmp     ecx, 10h ;判断是否是查第二张系统服务表
    .text:0046A5D2                 jnz     short loc_46A5EF ;如果是查第一张系统服务表,则jmp
    

    这里将ecx和0x10进行比较,ecx存储的是 0x00或0x10,如果ecx是0x10,就说明要查第二张系统服务表。

    如果要查第一张服务表,就会jmp;如果查第一张表则继续往下执行。

    .text:0046A5D4                 mov     ecx, large fs:18h
    .text:0046A5DB                 xor     ebx, ebx
    .text:0046A5DD
    .text:0046A5DD loc_46A5DD:                             ; DATA XREF: _KiTrap0E+117o
    .text:0046A5DD                 or      ebx, [ecx+0F70h]
    .text:0046A5E3                 jz      short loc_46A5EF
    .text:0046A5E5                 push    edx
    .text:0046A5E6                 push    eax
    .text:0046A5E7                 call    ds:_KeGdiFlushUserBatch
    .text:0046A5ED                 pop     eax
    .text:0046A5EE                 pop     edx
    

    调用_KeGdiFlushUserBatch函数后,假设我们查找的是第一张系统服务表

    .text:0046A5EF                                         ; _KiFastCallEntry+C3j
    .text:0046A5EF                 inc     large dword ptr fs:638h ; _KPCRB->0x518 KeSystemCall增加1
    .text:0046A5F6                 mov     esi, edx  ; edx存储的三环的参数指针
    

    这里将edx保存到esi,edx存储的是三环的参数指针。

    .text:0046A5F8                 mov     ebx, [edi+0Ch] ;ebx-->参数起始位置
    

    edi->系统服务表的起始位置,[edi+0Ch]存的是ParamTableBase 参数表的基址 ,ebx-->参数起始位置。

    .text:0046A5FD                 mov     cl, [eax+ebx] ;eax->函数地址表索引,ebx->参数表的起始位置 ,cl->参数的个数
    

    参数表的基址+函数地址表的索引,再取得到的值就是要调用的函数的参数个数。

    .text:0046A600                 mov     edi, [edi] ;edi->系统服务表,第一个成员是函数地址表
    

    取出函数地址表,放到edi。

    .text:0046A602                 mov     ebx, [edi+eax*4]  ;ebx->零环的函数地址
    

    edi为函数地址表+eax索引*4,实现将0环的函数地址存到ebx。

    .text:0046A605                 sub     esp, ecx ;提升堆栈高度到cl
    

    为了能够存储3环的参数,提升堆栈到cl(参数个数)。

    .text:0046A607                 shr     ecx, 2    ;参数总长度/4 = 参数的个数
    .text:0046A60A                 mov     edi, esp	
    .text:0046A60C                 cmp     esi, ds:_MmUserProbeAddress
    .text:0046A612                 jnb     loc_46A7C0
    .text:0046A618
    .text:0046A618 loc_46A618:                             ; CODE XREF: _KiFastCallEntry+2A4j
    .text:0046A618                                         ; DATA XREF: _KiTrap0E+10Do
    .text:0046A618                 rep movsd	;开始拷贝参数
    .text:0046A61A                 call    ebx
    

    将ecx右移2位,即将ecx/4=参数的个数。我们知道rep的次数,取决于ecx,而movsd每次复制4个字节,所以需要将ecx/4。

    .text:0046A60C                 cmp     esi, ds:_MmUserProbeAddress ;判断3环参数的地址范围是否越界
    .text:0046A612                 jnb     loc_46A7C0 ;越界跳转到错误处理模块
    

    _MmUserProbeAddress是一个全局变量,是用户程序能访问地址的最大范围。esi指向的是3环的函数指针,将esi与_MmUserProbeAddress比较,是为了判断3环参数的地址范围是否越界,如果越界则跳转到错误处理模块。

    .text:0046A61A                 call    ebx ;调用函数
    

    最后将3环的参数赋值到0环,开始真正的内核函数。

    (五)API函数的调用过程(SSDT)

    在前一章,我们逆向分析得出:KTHREAD +0xE0 -->系统服务表。实际上,Windows提供了一个到处的全局变量,通过这个变量,就可以直接访问系统服务表。

    1、分析KeServiceDescriptorTable

    • IDA打开ntkrnlpa.exe ,在export中搜索KeServiceDescriptorTable

    • 在Windbg中查看这个变量

    2

    这就是所谓的SSDT了。

    2、系统服务描述表,SSDT

    SSDT(System Service Despcriptor Table,系统服务描述表),SSDT结构包含4个成员,这4个成员都是一个系统服务表的结构体。

    接下来,在windbg里看一下每个成员:

    3

    用SSDT查看只有一张表,这个表就是Ntoskrl.exe![]
    导出的,后面的3个成员都是空的。

    3、SSDT Shadow

    通过SSSDT就能同时看到Ntoskrl.exe与win32k.sys导出的表 。

    4

    typedef struct _KSYSTEM_SERVICE_TABLE
    {
    	PULONG ServiceTableBase;		//这个指向系统服务函数地址表
    	PULONG ServiceCounterTableBase;	//系统这个服务表调用了几次
    	ULONG NumberOfService;			//服务函数的个数
    	PULONG ParamTableBase;			//参数表 
    
    }KSYSTEM_SERVICE_TABLE, *PKSYSTEM_SERVICE_TABLE;
    
    PSYSTEM_SERVICE_TABLE Ntoskr_ssdt;
    
    //这4个参数就是对应第一个系统服务表导出的值
    Ntoskr_ssdt->ServiceTableBase = 80505450;
    Ntoskr_ssdt->ServiceCounterTableBase = 00000000;
    Ntoskr_ssdt->NumberOfService = 0000011c;
    Ntoskr_ssdt->ParamTableBase = 805058c4;
    
    

    系统服务表基址+0xBA--->ReadVirtualMemory:

    查看ReadVirtualMemory函数反汇编:

    查看参数表

    0x14的十进制是20,它当前所有的参数加起来有20个字节,每个参数4字节,所以它有5个参数。

  • 相关阅读:
    三、FileStream 文件流基本操作
    NetCore3.1+Flurl..Http_FluentHttp 增删改查
    CryptoJS与C#AES加解密互转
    C#生成随机字符串
    IIS发布网站之后,页面图片和js未加载出错
    web中浏览PDF文件
    ASP.NET下使用Combres对JS、CSS合并和压缩
    jQuery截取字符串、日期字符串转Date、获取html中的纯文本
    jquery格式化时间
    jQuery限制文本框的输入长度
  • 原文地址:https://www.cnblogs.com/Erma/p/13039986.html
Copyright © 2020-2023  润新知