保护模式暂时告一段落了,接下来开始API函数调用的学习,来一步步分析Windows在调用API的过程中到底做了些什么事,函数到底是如何实现的。
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的调用过程,我们来分析一个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赋了一个函数地址,并调用此函数,然后平衡堆栈。所以我们只需要将这些功能组合一下即可:
1 | void _stdcall MyReadProcessMemory( |
当然,仅仅这样做还不够,这样虽然编译能过,但是执行会报错。因为在ReadProcessMemory中调用NtReadVirutalMemory用了call语句,call语句的使用会导致返回地址压栈,也因此,我们重写的API函数在执行
1 | call dword ptr [edx] |
这条语句时,esp处的值为hProcess,而Windows在执行这条语句时,[esp+4]处的值才是hProcess!如果这里不做修改,后面函数返回时,堆栈会不平衡,因此我们需要手动修改一下堆栈:
增加了这两行后,我们自己重写的ReadProcessMemory就算完成了。同理,WriteProcessMemory也是如此。
实验结果
可以看到,我们使用了自己重写的API函数,但是实现了同样的功能。同理,别的函数也可以通过重写,从而防止3环的恶意挂钩。
完整代码
1 |
|
总结
对于API函数的调用过程,我们对三环的部分有了一定的了解,发现,大部分API的实现都是在0环,接下来的文章中,我们就跟进去,找找API函数在0环中的实现在哪。
参考教程:https://www.bilibili.com/video/BV1NJ411M7aE?p=37
参考文章:https://blog.csdn.net/qq_41988448/article/details/102786700
参考资料:Joney的笔记,张嘉杰的笔记,XIAOYSHIJI的代码