0

x86系统调用(上)

 2 years ago
source link: https://www.anquanke.com/post/id/266826
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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;
}



About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK