x86系统调用(上)

 

windows API

Application Programming Interface,简称 API 函数。

Windows有多少个API?

主要是存放在 C:\WINDOWS\system32下面所有的dll

几个重要的DLL:

  • Kernel32.dll:最核心的功能模块,比如管理内存、进程和线程相关的函数等.
  • User32.dll:是Windows用户界面相关应用程序接口,如创建窗口和发送消息等.
  • GDI32.dll:全称是Graphical Device Interface(图形设备接口),包含用于画图和显示文本的函数.比如要显示一个程序窗口,就调用了其中的函数来画这个窗口.
  • Ntdll.dll:大多数API都会通过这个DLL进入内核(0环).

 

分析ReadProcessMemory

该API在Kernel32.dll中导出。

通过IDA分析可以看到,在函数中又调用了另一个导入的函数。

在kernel32.dll的导入函数中,准确的说在IAT表中,可以找到是用的哪个dll提供的函数。

可以看到是ntdll.dll,那么我们继续跟踪ntdll.dll。

在ntdll中,可以看到先传入给eax一个值,这个值实际上是个索引号。然后再call了0x7FFE0300这个值。

这个地址可以看到他是写死的,那么就意味着任何一个exe加载这个dll,都会去call这个位置,这个位置是所有进程共享的。实际上这里是一个结构体(_KUSER_SHARED_DATA),call的是其中一个成员。

kd> dt _KUSER_SHARED_DATA 0x7ffe0000

并且这个KUSER_SHARED_DATA结构体是共享的:在 User 层和 Kernel 层分别定义了一个 _KUSER_SHARED_DATA 结构区域,用于 User 层和 Kernel 层共享某些数据。

_它们使用固定的地址值映射,_KUSER_SHARED_DATA 结构区域在 User 和 Kernel 层地址分别为:

  • User 层地址为:0x7ffe0000
  • Kernel 层地址为:0xffdf0000

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

可以再windbg通过指令查看:

kd> !vtop 0a5c03c0 7ffe0000
X86VtoP: Virt 7ffe0000, pagedir a5c03c0
X86VtoP: PAE PDPE a5c03c8 - 0000000010dca001
X86VtoP: PAE PDE 10dcaff8 - 0000000010e3e067
X86VtoP: PAE PTE 10e3ef00 - 8000000000041025
X86VtoP: PAE Mapped phys 41000
Virtual address 7ffe0000 translates to physical address 41000.

物理地址为41000

PTE的属性最后是5,即为0101,R/W位为0,则属性为可写。

同样的查看kernel层:

kd> !vtop 0a5c03c0 ffdf0000
X86VtoP: Virt ffdf0000, pagedir a5c03c0
X86VtoP: PAE PDPE a5c03d8 - 0000000011448001
X86VtoP: PAE PDE 11448ff0 - 000000000038f163
X86VtoP: PAE PTE 38ff80 - 0000000000041163
X86VtoP: PAE Mapped phys 41000
Virtual address ffdf0000 translates to physical address 41000.

指向的是同一个物理页。

PTE的属性最后是3,即为0011,R/W位为1,可读可写。

这就意味着在三环,我们无法通过hook这个地址来hook所有进程的执行流,但当然是拦不住我们的。

在我们了解了KUSER_SHARED_DATA结构体后,就可以知道call的实际上是Systemcall的地址,通过反汇编查看

kd>  u 0x7c92e4f0
ntdll!KiFastSystemCall:
7c92e4f0 8bd4            mov     edx,esp
7c92e4f2 0f34            sysenter
ntdll!KiFastSystemCallRet:
7c92e4f4 c3              ret
7c92e4f5 8da42400000000  lea     esp,[esp]
7c92e4fc 8d642400        lea     esp,[esp]
ntdll!KiIntSystemCall:
7c92e500 8d542408        lea     edx,[esp+8]
7c92e504 cd2e            int     2Eh
7c92e506 c3              ret

通过sysenter指令(快速调用)进入0环。操作系统会在系统启动的时候在KUSER_SHARED_DATA结构体的+300的位置,写入一个函数,这个函数就是KiFastSystemCall或者KiIntSystemCall。

当通过eax=1来执行cpuid指令时,处理器的特征信息被放在ecx和edx寄存器中,其中edx包含了一个SEP位(11位),该位指明了当前处理器知否支持sysenter/sysexit指令。

支持:ntdll.dll!KiFastSystemCall()

不支持:ntdll.dll!KiIntSystemCall()。该方式通过中断门进入0环。

kd> u KiIntSystemCall
ntdll!KiIntSystemCall:
7c92e500 8d542408        lea     edx,[esp+8]
7c92e504 cd2e            int     2Eh
7c92e506 c3              ret
7c92e507 90              nop
ntdll!RtlRaiseException:
7c92e508 55              push    ebp
7c92e509 8bec            mov     ebp,esp
7c92e50b 9c              pushfd
7c92e50c 81ecd0020000    sub     esp,2D0h

因为我们比较了解中断门,我们先看看中断门是怎么进入0环的。

kd> dq 8003f400 + 0x2e*8
8003f570  8053ee00`0008e481 80548e00`00081780
8003f580  80538e00`0008db40 80538e00`0008db4a
8003f590  80538e00`0008db54 80538e00`0008db5e
8003f5a0  80538e00`0008db68 80538e00`0008db72
8003f5b0  80538e00`0008db7c 806d8e00`00082728
8003f5c0  80538e00`0008db90 80538e00`0008db9a
8003f5d0  80538e00`0008dba4 80538e00`0008dbae
8003f5e0  80538e00`0008dbb8 806d8e00`00083b70

该描述符对应的eip是8053e481,反汇编查看。

这里就已经进入到内核模块,函数为KiSystemService。

从r3到r0必然是需要提权的,替换的寄存器有:CS,EIP,SS,ESP(SS与ESP由TSS提供)。这是通过中断门的方式。

如果通过sysenter,即快速调用进入内核。

操作系统会提前将CS/SS/ESP/EIP的值存储在MSR寄存器中,sysenter指令执行时,CPU会将MSR寄存器中的值直接写入相关寄存器,没有读内存的过程,所以叫快速调用,本质是一样的!

MSR寄存器存储了很多值,微软只公布了一小部分。

我们可以通过RDMSR/WRMST来进行读写(操作系统使用WRMST写该寄存器):

kd> rdmsr 174   //查看CS
kd> rdmsr 175   //查看ESP
kd> rdmsr 176   //查看EIP
SS是被写死的,算出来的。cs=8 --》ss=0x10

所以一个是通过内存获取(IDT TSS),一个是通过寄存器获取。本质上没有区别,只是效率上的区别。

 

总结

我们在三环执行的api无非是一个接口,真正执行的功能在内核实现,我们便可以直接重写三环api,直接sysenter进内核,这样可以规避所有三环hook。

API通过中断门进0环:

  1. 固定中断号为0x2E
  2. CS/EIP由门描述符提供 ESP/SS由TSS提供
  3. 进入0环后执行的内核函数:NT!KiSystemService

API通过sysenter指令进0环:

1) CS/ESP/EIP由MSR寄存器提供(SS是算出来的)
2) 进入0环后执行的内核函数:NT!KiFastCallEntry

 

实验

自己实现直接通过sysenter 和 int 2e直接进入0环

这种方式可以绕过所有的三环钩子

sysenter

#include "StdAfx.h"
#include <windows.h>
#include <stdio.h>
#include <iostream>
#include <stdlib.h>

using namespace std;

BOOL __stdcall My_ReadProcessMemory(HANDLE hProcess, LPCVOID lpBaseAddress, LPVOID lpBuffer, DWORD nSize, LPDWORD lpNumberOfBytesRead);

#define EXIT_ERROR(x)                                 \
    do                                                \
    {                                                 \
        cout << "error in line " << __LINE__ << endl; \
        printf("errcode = %d\n", GetLastError());     \
        cout << x;                                    \
        system("pause");                              \
        exit(EXIT_FAILURE);                           \
    } while (0)

int main()
{
    int pid = 0;
    cout << "请输入要读取的进程的PID:";
    cin >> pid;
    HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
    if (hProcess == NULL)
        EXIT_ERROR("hProcess == NULL!");

    WORD t;
    DWORD dwSizeRead;

    ReadProcessMemory(hProcess, (LPCVOID)0x00400000,
                      &t, sizeof WORD, &dwSizeRead);
    cout << hex << t << " " << dwSizeRead << endl;
    getchar();
    system("pause");

    My_ReadProcessMemory(hProcess, (LPCVOID)0x00400000,
                         &t, sizeof WORD, &dwSizeRead);

    cout << hex << t << " " << dwSizeRead;

    getchar();
    system("pause");

    return 0;
}

BOOL __stdcall My_ReadProcessMemory(HANDLE hProcess, LPCVOID lpBaseAddress, LPVOID lpBuffer, DWORD nSize, LPDWORD lpNumberOfBytesRead)
{
    DWORD NtStatus;

    __asm {
        lea eax, [nSize]
        push eax
        push nSize // nsize值入栈
        push lpBuffer // buffer入栈
        push lpBaseAddress // lpBaseAddress入栈
        push hProcess // hProcess入栈

        sub esp, 0x04; // 模拟 ReadProcessMemory 里的 CALL NtReadVirtualMemory

        mov  eax, 0BAh  // NtReadVirtualMemory
               push 0x00401EBC //NtReadRet的地址

        // kifastcall
        mov    edx,esp
        _emit 0x0f
        _emit 0x34 // 没有sysenter,要硬编码0x0f34

NtReadRet:
        add esp, 0x18;                     // 模拟 NtReadVirtualMemory 返回到 ReadProcessMemory 时的 RETN 0x14
        mov NtStatus, eax;
    }

    printf("nsize = %d, status = %d\n", nSize, NtStatus);

    return 0;
}

int 2E

#include "StdAfx.h"
#include <windows.h>
#include <stdio.h>
#include <iostream>
#include <stdlib.h>

using namespace std;

BOOL __stdcall My_ReadProcessMemory_INT(HANDLE hProcess, LPCVOID lpBaseAddress, LPVOID lpBuffer, DWORD nSize, LPDWORD lpNumberOfBytesRead);

#define EXIT_ERROR(x)                                 \
    do                                                \
{                                                 \
    cout << "error in line " << __LINE__ << endl; \
    printf("errcode = %d\n", GetLastError());     \
    cout << x;                                    \
    system("pause");                              \
    exit(EXIT_FAILURE);                           \
    } while (0)

int main()
{
    int pid = 0;
    cout << "请输入要读取的进程的PID:";
    cin >> pid;
    HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
    if (hProcess == NULL)
        EXIT_ERROR("hProcess == NULL!");

    WORD t;
    DWORD dwSizeRead;

    ReadProcessMemory(hProcess, (LPCVOID)0x00400000,
        &t, sizeof WORD, &dwSizeRead);
    cout << hex << t << " " << dwSizeRead << endl;
    getchar();
    system("pause");

    My_ReadProcessMemory_INT(hProcess, (LPCVOID)0x00400000,
        &t, sizeof WORD, &dwSizeRead);

    cout << hex << t << " " << dwSizeRead;

    getchar();
    system("pause");

    return 0;
}

BOOL WINAPI My_ReadProcessMemory_INT(HANDLE hProcess, LPCVOID lpBaseAddress, LPVOID lpBuffer, DWORD nSize, LPDWORD lpNumberOfBytesRead)
{
    DWORD NtStatus;
    __asm
    {
        mov eax, 0xBA;
        lea     edx, hProcess // edx里面存储最后入栈的参数    
        int     2Eh    
        mov NtStatus, eax
    }
    *lpNumberOfBytesRead = nSize;

    if (NtStatus) return NtStatus;
    else return 0;
}

(完)