Windows系统调用中API的3环部分

一、R3环API分析的重要性

  1. Windows所提供给R3环的API,实质就是对操作系统接口的封装,其实现部分都是在R0实现的。
  2. 很多恶意程序会利用钩子来钩取这些API,从而达到截取内容,修改数据的意图。
  3. 现在我们使用olldbg对ReadProcessMemory进行跟踪分析,查看其在R3的实现,并根据我们的分析来重写一个ReadProcessMemory。
  4. 重写ReadProcessMemory之后,这就会加大恶意代码截获的难度。
  5. 当然,对于自己来说也有很多弊端,比如只能在指定的操作系统中运行(32位与64位操作系统,其运行ReadProcessMemory的执行动作是不一样的,在64位运行32位程序,其中间会调用wow64cpu.dll来进行转换)

二、调试代码

#include "pch.h"
#include <iostream>
#include <algorithm>
#include <Windows.h>

int main() {
    getchar();
    getchar();
    int a[4],t;
    printf("hello world!");
    getchar();
    getchar();
    // 依次往 p 指针中写入数据,再用ReadProcessMemory读取数据
    for (int i = 0; i < 4; i++) {
        WriteProcessMemory(INVALID_HANDLE_VALUE, &a[i], &i, sizeof(int),NULL);
        
    }
    for (int i = 0; i < 4; i++) {
        ReadProcessMemory(INVALID_HANDLE_VALUE, &a[i], &t, sizeof(int), NULL);
        printf("%d\n", t);
    }
    getchar();
    getchar();
    
}

三、调试中的关键汇编代码(系统环境:在Windows7 32位操作系统 / 调试器:olldbg)

1. 在exe 中 调用 kernel32.ReadProcessMemroy函数
  01314E3E    8BF4         mov esi,esp
  01314E40    6A 00        push 0x0
  01314E42    6A 04        push 0x4
  01314E44    8D45 DC      lea eax,dword ptr ss:[ebp-0x24]
  01314E47    50           push eax
  01314E48    8B4D C4      mov ecx,dword ptr ss:[ebp-0x3C]
  01314E4B    8D548D E8    lea edx,dword ptr ss:[ebp+ecx*4-0x18]
  01314E4F    52           push edx
  01314E50    6A FF        push -0x1
  01314E52    FF15 64B0310>call dword ptr ds:[<&KERNEL32.ReadProcessMemory>]; kernel32.ReadProcessMemory
  01314E58    3BF4         cmp esi,esp

2. 在 kernel32.ReadProcessMemroy函数 中调用 jmp.&API-MS-Win-Core-Memory-L1-1-0.ReadProcessMemory> 函数
  // 该函数相当于什么也没做...
  7622C1CE >  8BFF             mov edi,edi
  7622C1D0    55               push ebp
  7622C1D1    8BEC             mov ebp,esp
  7622C1D3    5D               pop ebp                                                           ;
  7622C1D4  ^ E9 F45EFCFF      jmp <jmp.&API-MS-Win-Core-Memory-L1-1-0.ReadProcessMemory>

3. 在 API-MS-Win-Core-Memory-L1-1-0.ReadProcessMemo 中调用 KernelBa.ReadProcessMemory 函数
  761F20CD  - FF25 0C191F7>jmp dword ptr ds:[<&API-MS-Win-Core-Memory-L1-1-0.ReadProcessMemo>; KernelBa.ReadProcessMemory

4. 在KernelBa.ReadProcessMemory 调用 <&ntdll.NtReadVirtualMemory> 函数
  75DA9A0A >  8BFF         mov edi,edi
  // 这两部分在编写函数时就会使用
  75DA9A0C    55           push ebp
  75DA9A0D    8BEC         mov ebp,esp
  75DA9A0F    8D45 14      lea eax,dword ptr ss:[ebp+0x14]
  75DA9A12    50           push eax
  75DA9A13    FF75 14      push dword ptr ss:[ebp+0x14]
  75DA9A16    FF75 10      push dword ptr ss:[ebp+0x10]
  75DA9A19    FF75 0C      push dword ptr ss:[ebp+0xC]
  75DA9A1C    FF75 08      push dword ptr ss:[ebp+0x8]
  75DA9A1F    FF15 C411DA7>call dword ptr ds:[<&ntdll.NtReadVirtualMemory>] ; ntdll.ZwReadVirtualMemory

5. 在 <&ntdll.NtReadVirtualMemory> 中调用 ntdll.KiFastSystemCall 函数
  77A162F8 >  B8 15010000  mov eax,0x115  // 对应操作系统内核中某一函数的编号。
  77A162FD    BA 0003FE7F  mov edx,0x7FFE0300  // 该地方是一个函数,该函数决定了什么方式进零环。
  77A16302    FF12         call dword ptr ds:[edx]  ; ntdll.KiFastSystemCall

6. 在 ntdll.KiFastSystemCall 中 调用sysenter
  77A170B0 >  8BD4         mov edx,esp
  77A170B2    0F34         sysenter
  77A170B4 >  C3           retn

四、汇编代码分析解读(根据三中的序号依次解读)

  1. 这部分是我们程序中调用ReadProcessMemory后编译器直接编译后的汇编代码,传入参数与API调用
  2. 在kenel32.dll中,mov edi,edi 是用于热补丁技术所保留的(函数开始处的MOV EDI, EDI的作用),这段代码仔细看其实除了jmp什么也没干。
  3. 转到kernelBase.dll中实现ReadProcessMemory。
  4. 这段汇编代码,将ReadProcessMemory中传入的参数再次入栈,调用ntdll.ZwReadVirtualMemory函数。
  5. 这段汇编代码看注释,eax中存放了一个编号,其就是在内核中的ReadProcessMemory实现;在 0x7FFE0300 处存放了一个函数指针,该函数指针决定了以什么方式进入0环(中断/快速调用)。
  6. 在ntdll.KiFastSystemCall调用sysenter。

五、重写ReadProcessMemory函数的思路

  我们所看到的汇编代码,本质就是Windows所执行的步骤,我们依据上面的分析,完全可以重新写一个该函数,只需要关键部分。

  1) 退而求其次

    我们希望可以在自己的代码中直接使用 "sysenter",但经过编写发现其并没有提供这种指令。

    因此在"sysenter"无法直接使用的情况下,只能退而求其次,调用ntdll.KiFastSystemCall函数。

  2)传递参数,模拟call指令

    ntdll.KiFastSystemCall函数需要借助ntdll.NtReadVirtualMemory传递过来的参数,然后执行call指令。

    我们并不希望执行call指令执行,因为执行call指令意味着又上了一层。(多一层被钩取的风险)

    我们希望自己的代码中直接传递参数,并且直接调用调用ntdll.KiFastSystemCall函数。

    因此我们需要模拟call指令。call指令的本质就是将返回地址入栈,并跳转。我们不需要跳转,只需要将返回地址入栈(四个字节 使用 sub esp,4 模拟)

  3)手动实现栈平衡

    我们内嵌汇编代码后,需要手动平衡栈,我们只需要分析esp改变了多少(push、pop以及直接对esp的计算)。

    经过分析共减少了24字节,所以代码最后应该有 add esp,24 来平衡栈。

六、ReadProcessMemory函数重写的实现(重点看汇编代码)

该代码是使用快速调用所编写的,如果想使用中断实现调用内核函数,请移步这里:Windows系统调用中API从三环到零环(下)

(执行结果)

#include "pch.h"
#include <iostream>
#include <algorithm>
#include <Windows.h>
void  ReadMemory(HANDLE hProcess, PVOID pAddr, PVOID pBuffer, DWORD dwSize, DWORD  *dwSizeRet)
{

    _asm
    {
        lea     eax, [ebp + 0x14]
        push    eax
        push[ebp + 0x14]
        push[ebp + 0x10]
        push[ebp + 0xc]
        push[ebp + 8]
        sub esp, 4
        mov eax, 0x115
        mov edx, 0X7FFE0300   //sysenter不能直接调用,我间接call的
        CALL DWORD PTR[EDX]
        add esp, 24

    }
}
int main()
{
    HANDLE hProcess = 0;
    int t = 123;
    DWORD pBuffer;
    //hProcess = OpenProcess(PROCESS_ALL_ACCESS, 0,a);
    ReadMemory((HANDLE)-1, (PVOID)&t, &pBuffer, sizeof(int), 0);
    printf("%X\n", pBuffer);
    ReadProcessMemory((HANDLE)-1, &t, &pBuffer, sizeof(int), 0);
    printf("%X\n", pBuffer);

    getchar();
    return 0;
}

一、结构体 _KUSER_SHARED_DATA

  该结构体看名字可知是用于内核层与用户层来共享数据所使用的结构体。

   kd > dt _KUSER_SHARED_DATA
          ntdll!_KUSER_SHARED_DATA
          + 0x000 TickCountLowDeprecated : Uint4B
          + 0x004 TickCountMultiplier : Uint4B
          + 0x008 InterruptTime : _KSYSTEM_TIME
          + 0x014 SystemTime : _KSYSTEM_TIME
          + 0x020 TimeZoneBias : _KSYSTEM_TIME
          + 0x02c ImageNumberLow : Uint2B
          + 0x02e ImageNumberHigh : Uint2B
          ·······

   1)在User层和KerNel层分别定义了一个_KUSER_SHARED_DATA结构区域,用于User层和Kernel层共享某些数据。

   2)它们使用同一段页,只是映射位置不同。虽然同一页,但User只读,Kernnel层可写。

   3)它们使用固定的地址值映射,_KUSER_SHARED_DATA结构在User为:0x7ffe0000,在Kernel层为:0xffdf0000。

   通过windbg可以查看其中两块内存完全一样(实质查看的是同一物理页,挂在两块页表PTT中。

    

二、分析 0X7FFE0300  这个地址

在 <&ntdll.NtReadVirtualMemory> 中调用 ntdll.KiFastSystemCall 函数 ,实质就是调用 0X7FFE0300 这个地址。
    77A162F8 >  B8 15010000  mov eax,0x115  // 对应操作系统内核中某一函数的编号。
    77A162FD    BA 0003FE7F  mov edx,0x7FFE0300  // 该地方是一个函数,该函数决定了什么方式进零环。
    77A16302    FF12         call dword ptr ds:[edx]  ; ntdll.KiFastSystemCall

  1)_KUSER_SHARED_DATA 在用户层的位置为 0x7FFE0000,该地址为其+0x300位置

     +0x300 SystemCall       : Uint4B

  2)该成员保存着系统调用的函数入口,如果当前CPU支持快速调用。

    则存储着ntdll.dll!KiFastSystemCall()函数地址;

    如果不支持快速调用,则存储着ntdll.dll!KiIntSystemCall()函数地址。

  3)通过实验验证当前CPU是否支持快速调用:

    当通过eax=1来执行cupid指令时,处理器特征信息被存放在ecx和edx寄存器中,

    其中edx包含了SEP位(11位),该位指明了当前处理器是否支持sysenter/sysexit指令。

    

    如下图,我们执行cupid指令,获取edx 178BFBFF,拆分 11-8位 B 1011,故其11位为1,支持快速调用。

    这也验证了我们上一篇文章中的分析结果。(最后是快速调用并非使用中断门)

三、从3环进0环需要哪些寄存器改变

  1. CS的权限由3变为0,意味着需要新的CS
  2. SS与CS的权限永远一致,需要新的SS
  3. 权限发生切换的时候,堆栈也一定会改变,需要新的ESP
  4. 进0环后的代码位置,需要EIP

四、ntdll.dll!KiIntSystemCall() 分析

  我们使用ida来分析ntdll.dll!KiIntSystemCall()

  .text:77F070C0
  .text : 77F070C0                 public KiIntSystemCall
  .text : 77F070C0 KiIntSystemCall proc near; DATA XREF : .text : off_77EF61B8↑o
  .text : 77F070C0
  .text : 77F070C0 arg_4 = byte ptr  8
  .text : 77F070C0            // 之前调用该函数时 mov eax, 0x115,向eax传入一个函数号
  .text : 77F070C0                 lea     edx, [esp + arg_4] // 当前参数的指针存储在 edx中
  .text : 77F070C4                 int     2Eh; // 通过中断门的形式进入到内核中
  .text:77F070C4; DS:SI->counted CR - terminated command string
  .text : 77F070C6                 retn
  .text : 77F070C6 KiIntSystemCall endp

  其在触发 int 2eh中断前用到两个寄存器,一个是内核中调用函数的函数号,另外一个就是传入参数的指针。

五. ntdll.dll!KiFastSystemCall()函数分析
  当CPU支持快速调用,则使用这个函数。(我们在上篇文章中已经用到了这个来重构WriteProcessMemory函数)
  .text:77F070B0                 public KiFastSystemCall
  .text:77F070B0 KiFastSystemCall proc near              ; DATA XREF: .text:off_77EF61B8↑o
  .text:77F070B0            // 之前调用该函数时 mov eax, 0x115,向eax传入一个函数号
  .text:77F070B0                 mov     edx, esp // 将当前堆栈放入edx,用它来存储参数
  .text:77F070B2                 sysenter
  .text:77F070B2 KiFastSystemCall endp

  触发sysenter指令后,也用到两个寄存器eax,edx,作用与使用中断一样。

  为什么叫快速调用?

    中断门进入0环,需要的CS、EIP在IDT表中,需要查内存(SS与ESP由IDT表提供);

    而CPU如果支持sysenter指令时,操作系统会提前将CS/SS/ESP/EIP的值存储在MSR寄存器中,

    sysenter指令执行时,CPU会将MSR寄存器中的值直接写入寄存器中,没有读内存过程,本质时一样的。

一、INT 0x2E进0环

  .text : 77F070C0            // 之前调用该函数时 mov eax, 0x115,向eax传入一个函数号
  .text : 77F070C0                 lea     edx, [esp + arg_4] // 当前参数的指针存储在 edx中
  .text : 77F070C4                 int     2Eh; // 通过中断门的形式进入到内核中

  1)在GDT表中查看0x2eh

    在保护模式的门这一节中,我们了解到当发生中断时,操作系统会查找idt表,根据中断号在idt表中找到中断门描述符,从中断门描述符中读取CS:EIP的信息。

    之后,SS EIP 通过搜索GDT表,该表中存放着各个TSS描述符(每个进程一个TSS,内核一个TSS,TSS存放各种寄存器用于任务切换),来查找内核的 SS ESP。

    如图:我们通过windbg来查找出该地址 gdt+2e*8

      

     根据中断门描述符属性将 83e8ee00`00082fee 拆分拼接之后可知SS:08 / EIP:83e82fee

      

  2)查看 EIP:83e82fee 这个函数

    kd> u 83e82fee
    nt!KiSystemService:
    83e82fee 6a00            push    0
    83e82ff0 55              push    ebp
    83e82ff1 53              push    ebx
    83e82ff2 56              push    esi
    83e82ff3 57              push    edi
    83e82ff4 0fa0            push    fs
    83e82ff6 bb30000000      mov     ebx,30h
    83e82ffb 668ee3          mov     fs,bx

    该 nt!KiSystemService函数是真正的内核函数,并不是ntdll.dll模块下,其存在于ntoskrnl.exe / ntkrnlpa.exe中(根据分页模式不同选用不同的程序)

二、通过 systenter进入0环

  MSR寄存器存着进入内核的 CS、ESP、EIP的寄存器的值,SS=IA32_SYSENTER_CS+8。

  

  1)windbg查看这个MSR寄存器的值

    rdmsr 174     //查看CS

    rdmsr 175    //查看ESP

    rdmsr 176    //查看EIP

     

  2)查看EIP这个函数

   kd> u 83e830c0
    nt!KiFastCallEntry:
    83e830c0 b923000000      mov     ecx,23h
    83e830c5 6a30            push    30h
    83e830c7 0fa1            pop     fs
    83e830c9 8ed9            mov     ds,cx
    83e830cb 8ec1            mov     es,cx
    83e830cd 648b0d40000000  mov     ecx,dword ptr fs:[40h]
    83e830d4 8b6104          mov     esp,dword ptr [ecx+4]
    83e830d7 6a23            push    23h
    其是调用nt!KiFastCallEntry这个函数,跟nt!KiSystemService一样,该函数是真正的内核函数。

三、通过中断来重写ReadProcessMemory函数(通过快速调用时的实现可以查看这篇Windows系统调用中的API三环部分(依据分析重写ReadProcessMemory函数))

#include "pch.h"
#include <iostream>
#include <algorithm>
#include <Windows.h>
void  ReadMemory(HANDLE hProcess, PVOID pAddr, PVOID pBuffer, DWORD dwSize, DWORD  *dwSizeRet)
{

    _asm
    {
        
        lea     eax, [ebp + 0x14]
        push    eax
        push[ebp + 0x14]
        push[ebp + 0x10]
        push[ebp + 0xc]
        push[ebp + 8]
        mov eax, 0x115
        mov edx,esp
        int 0x2e
        add esp, 20
    }
}
int main()
{
    HANDLE hProcess = 0;
    int t = 123;
    DWORD pBuffer;
    //hProcess = OpenProcess(PROCESS_ALL_ACCESS, 0,a);
    ReadMemory((HANDLE)-1, (PVOID)&t, &pBuffer, sizeof(int), 0);
    printf("%X\n", pBuffer);
    ReadProcessMemory((HANDLE)-1, &t, &pBuffer, sizeof(int), 0);
    printf("%X\n", pBuffer);

    getchar();
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值