来学习一下windows的系统调用

简介

API,应用程序接口(Application Programming Interface)。Windows API 顾名思义就是 Windows 提供的应用程序接口,为了在 Windows 上实现一定的功能,我们需要学习 API 的用途、传参、返回值等,微软对这部分都提供了大量的文档说明,因此详细学习 API 的实现原理对我们而言是很有必要的。

Windows API

先介绍几个比较重要的 DLL,几乎所有的 Windows 应用程序都会有对这些 DLL 的使用。

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

并不是所有的 API 都需要进 Ring0,比如 strlen 之类的函数,Ring3 就能实现

1
2
3
4
5
6
7
8
size_t strlen(char *s){
size_t len=0;
while(*s){
len++;
s++;
}
return len;
}

但是大部分跨进程操作,调度等都是需要进入 Ring0 的,比如跨进程读API:ReadProcessMemory

这里可以直接调试看看链路,入口点通常是 kernel32.dll 的 ReadProcessMemory,最后调用了 kernelbase.dll的 ReadProcessMemory。

这里的 NtReadVirtualMemory 导入自 ntdll.dll,同样来到这里

发现这里的 NtReadVirtualMemory 仅仅是通过一个标志位选择走 syscall 指令或者 int 0x2E 指令。

这里的标志位在 0x7ffe0000 页内,是用于保存一个 _KUSER_SHARED_DATA 结构体使用的,不过这里看到指向了一个 SystemCallPad 保留结构,不太清楚为啥。通过中断门去调用这个好说,毕竟前面学过中断门是 r3 到 r0 的一个途径之一。

syscall实现

主要来讲讲 syscall 的实现,CPU有一个 MSR 寄存器,每个 MSR寄存器都有一个 id ,叫 MSR Index。

MSR Index
IA32_SYSENTER_CS 174H
IA32_SYSENTER_ESP 175H
IA32_SYSENTER_EIP 176H

来到windbg可以通过 0x176 的rdmsr命令得到系统调用入口。

0x174 和 0x175 则分别表示进入系统调用后新的 CS 和 ESP 的值,SS 默认为 CS+8。

中断门实现

直接查看 idt 表看看入口点即可。

1
2
3
4
5
kd> !idt 0x2e

Dumping IDT: 80b93000

2e: 83c77fea nt!KiSystemService

为什么说直接的 syscall 或者 sysenter 叫快速系统调用,因为中断门跳过去的过程涉及到读内存(IDT表),而直接 syscall 或者 sysenter 跳过去仅仅是读取了 MSR 寄存器。


这里也算摸清楚了,_KUSER_SHARED_DATA+0x300 上存了 KiIntSystemCallKiFastSystemCall 其中之一,而 KiInitSystemCall 就是 int 0x2e;ret,而另一个则是 sysenter;ret

Windows系统调用约定

这玩意跟之前的都不一样,其主要原因便是,Intel 给的中断门不支持传参,不像调用门那样,给定传参个数在执行 call far 指令的时候就会把 R3 栈中的参数全部搬到 R0 栈,那么直接在 R0 层写代码跟 R3 层写代码没什么不同了,可以使用同样类型的调用约定。

Windows 的所有系统调用都要求我们把参数按顺序排列放到一个内存中,将该内存的指针给到 EDX 寄存器。

回顾一下 Linux 的系统调用,同样也是不同于 R3 层的各种调用约定,通常参数小于五个的系统调用会依次把参数传给 EBX,ECX,EDX,ESI,EDI 中,而超过五个参数的系统调用则与 Windows 的做法一样,不过是将参数地址保存到了 EBX 中。

据此可以编写不依赖任何动态库的 ReadProcessMemory 函数。

测试程序如下

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>
#include <Windows.h>

int main()
{
int a = 0x12345678;
printf("&a = %p\n", &a);
DWORD PID = GetCurrentProcessId();
printf("PID = %d\n", PID);
getchar();
}

根据该程序运行结果,编写第二个程序,通过中断门调用 ReadProcessMemory

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include "stdafx.h"
#include<stdio.h>
#include<windows.h>
void MyReadProcessMemory(
HANDLE hProcess,
LPCVOID lpBaseAddress,
LPVOID lpBuffer,
DWORD nSize,
LPDWORD lpNumberOfBytesRead)
{
__asm
{
lea eax, [ebp + 0x14]
push eax; ReturnLength
push[ebp + 0x14]; BufferLength
push[ebp + 0x10]; Buffer
push[ebp + 0x0C]; BaseAddress
push[ebp + 0x08]; ProcessHandle
mov eax, 0115h
mov edx, esp
int 0x2e
add esp, 20
}
printf("read=%d\n",*lpNumberOfBytesRead);
}

int main()
{
DWORD PID = 3112;
DWORD pBuffer;
DWORD reads=0;
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, 0, PID);
printf("hProcess = %x \n", hProcess);
memset(&pBuffer, 0, 4);
MyReadProcessMemory(hProcess, (PVOID)0x19f82C, &pBuffer, 4, &reads);
printf("pBuffer = %x \n", pBuffer);
getchar();
return 0;
}

运行结果:

参考文献