This page looks best with JavaScript enabled

Windows系统调用学习笔记(1)—— API函数调用过程

 ·  ☕ 7 min read · 👀... views

转眼间又到了假期了欸,想来,寒假好像就在上个月(

咳咳,在寒假里粗粗学完了Windows保护模式,虽然感觉这些知识没有在实际重运用到,但是对知识体系的构建有着非常大的作用。同时,在前几天和B师傅聊了聊,做了一个生涯的规划,让我更加明白了Kernel的学习重要性。所以,准备在暑假完成Windows系统调用、事件等待和进程线程的学习,并着手开始学习驱动开发。(希望暑假结束时不要被打脸

0x01 Windows API

API(Application Programming Interface),是Microsoft Windows 平台的应用程序编程接口。Windows是一个很大的服务中心,调用这个服务中心的各种服务(每一种服务就是一个函数),可以帮应用程式达到开启视窗、描绘图形、使用周边设备等目的,由于这些函数服务的对象是应用程序(Application), 所以便称之为 Application Programming Interface,简称 API 函数。

那么,一个Windows系统中有多少个API呢?一般的,Windows API的数量可以理解为 C:\WINDOWS\system32 下面所有的dll所包含的函数数量的总和。而system32目录下的dll有部分是Windows自带的,也有部分是后期由用户自行安装的。而Windows的系统调用的学习主要关注以下几个最为重要的DLL:

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

下面会通过一个API函数的调用过程来分析一下在x86架构的系统中API函数是如何调用的

0x01 分析ReadProcessMemory

首先先去system32目录下取出kernel32.dll载入IDA看一下代码

通过代码,不难看出,在这里其实就是将参数全部传给了 NtReadVirtualMemory ,然后后面的代码就是在对返回值做判断。若返回值小于0,则会跳转到 loc_7C802204
而在 loc_7C802204 里 做了一个 call sub_7C809419,而在 sub_7C809419 内,可以看到,调用了 RtlNtStatusToDosError API。

RtlNtStatusToDosError API的作用是设置错误号,执行完sub_7C809419后xor eax,eax,即将返回值置为0,然后ret。因此,我们可以知道,当调用ReadProcessMemory失败时,返回结果为0,若是成功,则会执行inc eax,即返回值为1。

从上面对kernel32.dll的ReadProcessMemory函数分析可以看出来,ReadProcessMemory函数在kernel32.dll中实际上并没有做什么事情。它只是调用了另一个函数,然后设置了一些返回值。其实大部分API都是这样,在kernel32.dll中调用了其他的函数,然后仅做一个返回值处理的功能。那么,kernel32.dll,我们可以将其理解为一个中转站,或者一个分发器,我们通过它,可从ring3层进入ring0层。

因此,我们想分析ReadProcessMemory究竟做了什么功能,需要分析NtReadVirtualMemory函数。

0x02 分析NtReadVirtualMemory

在kernel32.dll中可以看到NtReadVirtualMemory函数是粉色的,即对于kernel32.dll来说,NtReadVirtualMemory是一个外部函数,从导入表中可以看到其来自ntdll.dll

然后我们用IDA载入ntdll.dll,在Exports窗口中找到NtReadVirtualMemory函数,双击就可以看到在ntdll中NtReadVirtualMemory的所有代码,也就是下面这4行

下面对这4行代码做一个分析:

  1. mov eax, 0BAh 这里0xBA是一个编号,也可以称之为索引,其对应操作系统内核中某个函数的编号
  2. mov edx, 7FFE0300h 这里0x7FFE0300是一个函数地址,该函数决定了我们用什么方式进0环(这个地址是什么将在下一节介绍)
  3. call dword ptr [edx]
  4. retn 14h 平衡栈

所以我们可以看出,ntdll里其实仅仅也只是做了一个提供一个编号和一个函数的功能,通过提供的这个函数进入0环。真正读取进程内存的函数在0环实现,我们所用的函数只是系统提供给我们的函数接口。

0x03 0x7FFE0300

那现在大家一定疑惑,这个0x7FFE0300地址是什么呢,它究竟有着什么功能?

_KUSER_SHARED_SYSCALL指向用户空间的一个地址(0x7ffe0300),这个位置存储了一个函数指针,指向KiIntSystemCall()或KiFastSystemCall()两个函数其中之一。系统在初始化的时候根据CPU是否支持快速系统调用而使该指针指向KiIntSystemCall()或KiFastSystemCall(),其对应的分别是中断门进入0环和快速调用进入0环两种方式。这里我们演示的是INT2E(中断门方式),所以应为KiIntSystemCall,至于快速调用, 会在下节介绍。

可以看到,只有两行代码,我们做一个分析分析:

  1. 第一行将参数的地址放入EDX中
  2. 第二行调用了0x2e中断(所有的API通过中断门进内核时,统一的中断号为0x2e,其作用是通过EAX中的值为索引调用系统未公开函数,即进入0环)

然后这里有一个要注意的点,我们可以看到,这里并没有对EAX做赋值的操作,说明在执行KiIntSystemCall函数前,编号已被写入EAX

0x04 编写一个ReadProcessMemory函数

通过前面的学习,可以知道,我们调用API函数的时候会先调用kernel32.dll,然后中转到ntdll.dll,在ntdll.dll里进入0环。那么,是不是意味这么我们可以绕过kernel32.dll和ntdll.dll中的中介函数,直接进入0环?这样就相当于绕过了API调用的过程,自己实现了一个API函数,可以有效防止被hook等情况。下面就用这个例子展示一下,因为ReadProcessMemory函数的作用是读取内存,所以需要两个进程,一个进程用于提供一个变量和一个地址,另一个进程直接利用这个地址进0环获取到值。

进程1

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#include <stdio.h>
int main()
{
	int num = 0x12345678;

	printf("0x&num = %x \n", &num);

	getchar();
	return 0;
}

进程2

 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
#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, 0BAh
		mov  edx, esp
		int  02eh				; Windows操作系统提供IDT[0x2e]给用户进行内核调用
		
		add  esp, 20
	}
}

int main()
{
	DWORD pBuffer;							// 接收数据的缓冲区
	HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, 0, PID);			// 获得进程句柄,PID修改为进程1的PID
	MyReadProcessMemory(hProcess, (PVOID)0x12ff7c, &pBuffer, 4, 0);		// 调用自定义的ReadProcessMemory

	printf("pBuffer = %x \n", pBuffer);		// 打印从其他进程中读取的数据

	getchar();
	return 0;
}

这里有一个难点就是0x2E号系统调用的用法。0x2E号中断同样是一个软中断,其作用是调用Undocument API函数,例如NtWriteVirtualMemory, ZwCreateProcess 等等都属于它的调用范畴。这个中断号在下一节还会做一个详细的讨论,下面看一下它的用法:

mov eax, service_id
lea edx, service_param
int 2eh

;EAX = function number
;EDX = address of parameter block

注意!系统服务分发表根据系统版本的不同,有很大的差异
0x10 AllocateVirtualMemory
0x11 AreMappedFilesTheSame
0x12 AssignProcessToJobObject
0x13 CallbackReturn
0x14 CancelIoFile
0x15 CancelTimer
0x16 CancelDeviceWakeupRequest
0x17 ClearEvent
0x18 Close
0x19 CloseObjectAuditAlarm
0x1a CompleteConnectPort
0x1b ConnectPort
0x1c Continue
0x1d CreateDirectoryObject
0x1e CreateEvent 
.............

这样其实就非常明了了,Windows已经注册好了适合的服务例程,使用时,我们先将函数id传入EAX,再将参数列表的指针传入EDX,触发0x2E中断,系统就会根据EAX中的函数ID调用与之对应的未公开API函数,同样的,在32位系统里,NtReadVirtualMemory函数的id是0xBA,所以就通过这样的方式,调用了0环的NtReadVirtualMemory函数。运行后效果如图

0x05 参考资料

  1. lzyddf师傅的博客
Share on

Qfrost
WRITTEN BY
Qfrost
CTFer, Anti-Cheater, LLVM Committer