avatar

Catalog
API函数的调用过程(3环部分)

保护模式暂时告一段落了,接下来开始API函数调用的学习,来一步步分析Windows在调用API的过程中到底做了些什么事,函数到底是如何实现的。

Windows API

  1. Application Programming Interface,简称API函数
  2. Windows有多少个API? 上万个,主要存放在 C:\WINDOWS\system32 下面所有的dll中
  3. 几个重要的DLL
    • Kernel32.dll:最核心的功能模块, 比如管理内存、进程和线程相关的函数等。
    • User32.dll:是Windows用户界面相关应用程序接口,如创建窗口和发送消息等。
    • GDI32.dll:全称是Graphical Device Interface(图形设备接口),包含用于画图和显示文本的函数。例如,要显示一个程序窗口,就调用了其中的函数来画这个窗口。
    • Ntdll.dll:大多数API都会通过这个DLL进入内核(0环)。

分析ReadProcessMemory

为了能够直观的了解API的调用过程,我们来分析一个Windows API函数,ReadProcessMemory,这个API函数位于Kernel32.dll,功能是读取指定进程的内存,打开IDA我们来看看它都做了些什么。

ReadProcessMemory

在Kernel32.dll中选择导出函数,按下Ctrl+F,然后搜索ReadProcessMemory

找到后进入函数主体

我们可以看到,ReadProcessMemory函数总体分为3个部分,首先是参数的压栈,其次调用了一个函数NtReadVirtualMemory,接着就开始处理函数的返回值了,可以发现,真正读取内存的功能并不是在ReadProcessMemory中实现的,所以我们需要进一步去查看NtReadVirtualMemory。

把鼠标放在NtReadVirtualMemory上,发现该函数是外部函数,不属于Kernel32.dll,所以我们得去Kernel32.dll的导入函数中找一下这个函数属于哪个dll。

可以见得,NtReadVirtualMemory属于Ntdll.dll,接下来进入NtReadVirtualMemory继续分析。

NtReadVirtualMemory

找到函数主体的步骤和上面一样,不再赘述。

NtReadVirtualMemory的函数主体部分只有4行,其中最关键的是前两行:

  • mov eax, 0BAh:这一步给eax赋值了一个编号,这个编号的作用是在进入0环后,找到真正需要调用的函数。记住,这个编号存在eax中。
  • mov edx, 7FFE03000h:这一步同样关键,这是一个函数地址。它决定了进入0环的方式(具体在下一篇中会详细分析),同样,也要记住edx存了这个值。

经过简单的分析,可以发现,在3环层面上, 并没有真正实现函数的功能,API函数的实现,大部分都在0环(只有少部分函数是在3环实现)。拿ReadProcessMemory来说,只是相对于0环给上层提供的一个接口,通过这个接口,我们可以实现读取指定地址的内存

重写API函数

现在我们知道,API函数的真正实现实际上是在底层(0环),3环上的API函数实际上只是起到一个接口的作用。那么我们可以自己重写3环的API,自己去调用0环函数,这样做的好处是,可以避免3环恶意挂钩(例如有黑客Hook了OpenFile函数,每次我们调用OpenFile时,黑客就知道我们打开了什么文件,如果重写API函数,黑客就无法通过Hook OpenFile函数来获取我们打开的文件内容,除非黑客在0环动手脚)

实现功能

实现的功能大致如此,读取变量a所在地址的内容,将内容改写后,再写入该地址,先用Windows API提供的ReadProcessMemory和WriteProcessMemory实现一遍。可以看到,原本变量a的值为0x123,随后被修改成了0x567

重写ReadProcessMemory

这里以ReadProcessMemory为例,在先前的分析中, 我们知道ReadProcessMemory仅仅做了参数压栈的工作,而NtReadVirutalMemory先给eax赋值了一个编号,接着给edx赋了一个函数地址,并调用此函数,然后平衡堆栈。所以我们只需要将这些功能组合一下即可:

c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void _stdcall MyReadProcessMemory(
HANDLE hProcess,
LPCVOID lpBaseAddress,
LPVOID lpBuffer,
DWORD nSize,
LPDWORD lpNumberOfBytesRead)
{
_asm {
//ReadProcessMemory
lea eax, [ebp+0x14]
push eax //lpNumberOfBytesRead
push [ebp+0x14] //nSize
push [ebp+0x10] //lpBuffer
push [ebp+0xC] //lpBaseAddress
push [ebp+0x8] //hProcess

//NtReadVirtualMemory
mov eax, 0xBA
mov edx, 0x7FFE0300
call dword ptr [edx]
add esp, 0x14
}
}

当然,仅仅这样做还不够,这样虽然编译能过,但是执行会报错。因为在ReadProcessMemory中调用NtReadVirutalMemory用了call语句,call语句的使用会导致返回地址压栈,也因此,我们重写的API函数在执行

Code
1
call dword ptr [edx]

这条语句时,esp处的值为hProcess,而Windows在执行这条语句时,[esp+4]处的值才是hProcess!如果这里不做修改,后面函数返回时,堆栈会不平衡,因此我们需要手动修改一下堆栈:

增加了这两行后,我们自己重写的ReadProcessMemory就算完成了。同理,WriteProcessMemory也是如此。

实验结果

可以看到,我们使用了自己重写的API函数,但是实现了同样的功能。同理,别的函数也可以通过重写,从而防止3环的恶意挂钩。

完整代码

c
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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#include "stdafx.h"
#include
void _stdcall MyReadProcessMemory(
HANDLE hProcess,
LPCVOID lpBaseAddress,
LPVOID lpBuffer,
DWORD nSize,
LPDWORD lpNumberOfBytesRead)
{
_asm {
//ReadProcessMemory
lea eax, [ebp+0x14]
push eax //lpNumberOfBytesRead
push [ebp+0x14] //nSize
push [ebp+0x10] //lpBuffer
push [ebp+0xC] //lpBaseAddress
push [ebp+0x8] //hProcess

//NtReadVirtualMemory
sub esp, 0x4 //Call NtReadVirtualMemory
mov eax, 0xBA
mov edx, 0x7FFE0300
call dword ptr [edx]
add esp, 0x18
}
}

void _stdcall MyWriteProcessMemory(
HANDLE hProcess,
LPVOID lpBaseAddress,
LPVOID lpBuffer,
DWORD nSize,
LPDWORD lpNumberOfBytesWritten)
{
_asm {
//WriteProcessMemory
lea eax, [ebp+0x8] //hProcess
push eax //lpNumberOfBytesRead
push [ebp+0x14] //NumberOfBytesToWrite
push [ebp+0x10] //lpBuffer
push [ebp+0xC] //lpBaseAddress
push [ebp+0x8] //hProcess

//NtWriteVirtualMemory
sub esp, 0x4 //Call NtWriteirtualMemory
mov eax, 0x115
mov edx, 0x7FFE0300
call dword ptr [edx]
add esp, 0x18
}
}

int main(int argc, char* argv[])
{
int a = 0x123;
int buffer = 0;
printf("Before: a=%x", a);
MyReadProcessMemory(GetCurrentProcess(), &a, &buffer, 4, NULL);
// printf("%x", buffer);

buffer = 0x567;
getchar();
MyWriteProcessMemory(GetCurrentProcess(), &a, &buffer, 4, NULL);
printf("After: a=%x\n", a);
getchar();
return 0;
}

总结

对于API函数的调用过程,我们对三环的部分有了一定的了解,发现,大部分API的实现都是在0环,接下来的文章中,我们就跟进去,找找API函数在0环中的实现在哪。

参考教程:https://www.bilibili.com/video/BV1NJ411M7aE?p=37

参考文章:https://blog.csdn.net/qq_41988448/article/details/102786700

参考资料:Joney的笔记,张嘉杰的笔记,XIAOYSHIJI的代码

Author: cataLoc
Link: http://cata1oc.github.io/2020/03/25/API%E5%87%BD%E6%95%B0%E7%9A%84%E8%B0%83%E7%94%A8%E8%BF%87%E7%A8%8B%EF%BC%883%E7%8E%AF%E9%83%A8%E5%88%86%EF%BC%89/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.
Donate
  • 微信
    微信
  • 支付寶
    支付寶