系统调用篇——3环层面调用过程

写在前面 ??此系列是本人一个字一个字码出来的,包括示例和实验截图。由于系统内核的复杂性,故可能有错误或者不全面的地方,如有错误,欢迎批评指正,本教程将会长期更新。 如有好的建议,欢迎反馈。码字不易,如果本篇文章有帮助你的,如有闲钱,可以打赏支持我的创作。如想转载,请把我的转载信息附在文章后面,并声明我的个人信息和本人博客地址即可,但必须事先通知我。

你如果是从中间插过来看的,请仔细阅读 羽夏看Win系统内核——简述 ,方便学习本教程。
??看此教程之前,问一个问题,你明确学系统调用的目的了吗? 没有的话就不要继续了,请重新学习 羽夏看Win系统内核——系统调用篇 里面的内容。
?
华丽的分割线
?
Windows API 【系统调用篇——3环层面调用过程】??API全称为Application Programming Interface,至于概念我就不多说了。下面我将介绍几个比较重要的Dll,我们调用的很多重要的函数都在这些动态链接库里面:
  • Kernel32.dll:最核心的功能模块,比如管理内存、进程和线程相关的函数等。
  • User32.dll:是Windows用户界面相关应用程序接口,如创建窗口和发送消息等。
  • GDI32.dll:全称是Graphical Device Interface,即图形设备接口,包含用于画图和显示文本的函数.比如要显示一个程序窗口,就调用了其中的函数来画这个窗口。
  • Ntdll.dll:大多数API都会通过这个DLL进入内核(0环)。
??这里提一句,并不是所有的API必须进0环的,可以在3环完全实现。比如Ntdll.dll导出的memcmp函数,感兴趣的自己可以逆向一下。有关API在3环层面调用过程将以我们最常用的ReadProcessMemory这个函数来进行讲解。
函数解析 ??ReadProcessMemory这个函数由Kernel32.dll导出,然后我们拖到IDA进行分析。至于怎么用IDA分析不会的话,请参考前面的教程(我也忘了在那篇文章写过了)。我们在IDA中定位到这个函数:
.text:7C8021D0 ; BOOL __stdcall ReadProcessMemory(HANDLE hProcess, LPCVOID lpBaseAddress, LPVOID lpBuffer, SIZE_T nSize, SIZE_T *lpNumberOfBytesRead) .text:7C8021D0public _ReadProcessMemory@20 .text:7C8021D0 _ReadProcessMemory@20 proc near; CODE XREF: GetProcessVersion(x)+2F12F↓p .text:7C8021D0; GetProcessVersion(x)+2F14E↓p ... .text:7C8021D0 .text:7C8021D0 hProcess= dword ptr8 .text:7C8021D0 lpBaseAddress= dword ptr0Ch .text:7C8021D0 lpBuffer= dword ptr10h .text:7C8021D0 nSize= dword ptr14h .text:7C8021D0 lpNumberOfBytesRead= dword ptr18h .text:7C8021D0 .text:7C8021D0movedi, edi .text:7C8021D2pushebp .text:7C8021D3movebp, esp .text:7C8021D5leaeax, [ebp+nSize] .text:7C8021D8pusheax; NumberOfBytesRead .text:7C8021D9push[ebp+nSize]; NumberOfBytesToRead .text:7C8021DCpush[ebp+lpBuffer]; Buffer .text:7C8021DFpush[ebp+lpBaseAddress] ; BaseAddress .text:7C8021E2push[ebp+hProcess]; ProcessHandle .text:7C8021E5callds:__imp__NtReadVirtualMemory@20 ; NtReadVirtualMemory(x,x,x,x,x) .text:7C8021EBmovecx, [ebp+lpNumberOfBytesRead] .text:7C8021EEtestecx, ecx .text:7C8021F0jnzshort loc_7C8021FD .text:7C8021F2 .text:7C8021F2 loc_7C8021F2:; CODE XREF: ReadProcessMemory(x,x,x,x,x)+32↓j .text:7C8021F2testeax, eax .text:7C8021F4jlshort loc_7C802204 .text:7C8021F6xoreax, eax .text:7C8021F8inceax .text:7C8021F9 .text:7C8021F9 loc_7C8021F9:; CODE XREF: ReadProcessMemory(x,x,x,x,x)+3C↓j .text:7C8021F9popebp .text:7C8021FAretn14h .text:7C8021FD ; --------------------------------------------------------------------------- .text:7C8021FD .text:7C8021FD loc_7C8021FD:; CODE XREF: ReadProcessMemory(x,x,x,x,x)+20↑j .text:7C8021FDmovedx, [ebp+nSize] .text:7C802200mov[ecx], edx .text:7C802202jmpshort loc_7C8021F2 .text:7C802204 ; --------------------------------------------------------------------------- .text:7C802204 .text:7C802204 loc_7C802204:; CODE XREF: ReadProcessMemory(x,x,x,x,x)+24↑j .text:7C802204pusheax; Status .text:7C802205call_BaseSetLastNTError@4 ; BaseSetLastNTError(x) .text:7C80220Axoreax, eax .text:7C80220Cjmpshort loc_7C8021F9 .text:7C80220C _ReadProcessMemory@20 endp

??从上面的代码可知,这个函数啥也没做,只是调用了NtReadVirtualMemory这个函数去实现读取内存。我们跟过去看看:
.idata:7C801418 ; NTSTATUS __stdcall NtReadVirtualMemory(HANDLE ProcessHandle, PVOID BaseAddress, PVOID Buffer, SIZE_T NumberOfBytesToRead, PSIZE_T NumberOfBytesRead) .idata:7C801418extrn __imp__NtReadVirtualMemory@20:dword

??不幸的是,这个函数是人家导入的,如何查到从哪里导入的呢?我们可以按照如下图所示的操作找到:
系统调用篇——3环层面调用过程
文章图片

??我们知道NtReadVirtualMemory这个函数是来自ntdll.dll。然后我们重新定位到IDA的位置:
.text:7C92D9E0 ; __stdcall NtReadVirtualMemory(x, x, x, x, x) .text:7C92D9E0public _NtReadVirtualMemory@20 .text:7C92D9E0 _NtReadVirtualMemory@20 proc near; CODE XREF: LdrFindCreateProcessManifest(x,x,x,x,x)+1CC↓p .text:7C92D9E0; LdrCreateOutOfProcessImage(x,x,x,x)+7C↓p ... .text:7C92D9E0moveax, 0BAh; NtReadVirtualMemory .text:7C92D9E5movedx, 7FFE0300h .text:7C92D9EAcalldword ptr [edx] .text:7C92D9ECretn14h .text:7C92D9EC _NtReadVirtualMemory@20 endp

??我们发现这个函数给eax赋个值,然后给edx个地址,然后call一下地址的内容,然后就平栈(由于STDCALL调用约定)返回了。至此,你或许就看不懂了。我们来看看这个地址到底存着什么。
_KUSER_SHARED_DATA ??当你看到这个时,你猜测这个地址存储的是_KUSER_SHARED_DATA结构体,对的。它的结构如下图所示:
nt!_KUSER_SHARED_DATA +0x000 TickCountLow: Uint4B +0x004 TickCountMultiplier : Uint4B +0x008 InterruptTime: _KSYSTEM_TIME +0x014 SystemTime: _KSYSTEM_TIME +0x020 TimeZoneBias: _KSYSTEM_TIME +0x02c ImageNumberLow: Uint2B +0x02e ImageNumberHigh: Uint2B +0x030 NtSystemRoot: [260] Uint2B +0x238 MaxStackTraceDepth : Uint4B +0x23c CryptoExponent: Uint4B +0x240 TimeZoneId: Uint4B +0x244 Reserved2: [8] Uint4B +0x264 NtProductType: _NT_PRODUCT_TYPE +0x268 ProductTypeIsValid : UChar +0x26c NtMajorVersion: Uint4B +0x270 NtMinorVersion: Uint4B +0x274 ProcessorFeatures : [64] UChar +0x2b4 Reserved1: Uint4B +0x2b8 Reserved3: Uint4B +0x2bc TimeSlip: Uint4B +0x2c0 AlternativeArchitecture : _ALTERNATIVE_ARCHITECTURE_TYPE +0x2c8 SystemExpirationDate : _LARGE_INTEGER +0x2d0 SuiteMask: Uint4B +0x2d4 KdDebuggerEnabled : UChar +0x2d5 NXSupportPolicy: UChar +0x2d8 ActiveConsoleId: Uint4B +0x2dc DismountCount: Uint4B +0x2e0 ComPlusPackage: Uint4B +0x2e4 LastSystemRITEventTickCount : Uint4B +0x2e8 NumberOfPhysicalPages : Uint4B +0x2ec SafeBootMode: UChar +0x2f0 TraceLogging: Uint4B +0x2f8 TestRetInstruction : Uint8B +0x300 SystemCall: Uint4B +0x304 SystemCallReturn : Uint4B +0x308 SystemCallPad: [3] Uint8B +0x320 TickCount: _KSYSTEM_TIME +0x320 TickCountQuad: Uint8B +0x330 Cookie: Uint4B

??在User层和Kernel层分别定义了一个_KUSER_SHARED_DATA结构区域,用于User层和Kernel层共享某些数据。它们使用固定的地址值映射,_KUSER_SHARED_DATA结构区域在User层地址为0x7ffe0000,在Kernel层地址为0xffdf0000。虽然它们指向的是同一个物理页,但在User层是只读的,在Kernnel层是可写的,通过页的限制保证在3环的安全性。因为里面有几个成员是十分重要的,有一个成员就是3环API进入内核的入口。
??根据0x7FFE0300这个地址,我们不难看出它是在调用SystemCall里面的代码,接下来看看这个函数到底是干啥的。
??我们先!process 0 0遍历一下进程:
kd> !process 0 0 **** NT ACTIVE PROCESS DUMP **** (部分进程快照略……)Failed to get VadRoot PROCESS 896ffda0SessionId: 0Cid: 0a7cPeb: 7ffde000ParentCid: 08bc DirBase: 16840680ObjectTable: e1ac9078HandleCount:36. Image: cmd.exe

??我们想要读取0x7FFE0300这个地址的内容,这个地址是3环应用的地址。如果读取某个进程的内存,必须有它的CR3,即和这个进程关联起来,我们需要.process + PROCESS 的地址进行:
kd> .process 896ffda0 ReadVirtual: 896ffdb8 not properly sign extended Implicit process is now 896ffda0 WARNING: .cache forcedecodeuser is not enabled

??然后我们dd一下这两个地址,看看内容是否一样:
kd> dd 0x7ffe0000 7ffe0000000f3594 0a03afb7 3daf17c0 00000017 7ffe001000000017 8b7792b3 01d7d56a 01d7d56a 7ffe0020f1dcc000 ffffffbc ffffffbc 014c014c 7ffe0030003a0043 0057005c 004e0049 004f0044 7ffe004000530057 00000000 00000000 00000000 7ffe005000000000 00000000 00000000 00000000 7ffe006000000000 00000000 00000000 00000000 7ffe007000000000 00000000 00000000 00000000kd> dd 0xffdf0000 ReadVirtual: ffdf0000 not properly sign extended ffdf0000000f3594 0a03afb7 3daf17c0 00000017 ffdf001000000017 8b7792b3 01d7d56a 01d7d56a ffdf0020f1dcc000 ffffffbc ffffffbc 014c014c ffdf0030003a0043 0057005c 004e0049 004f0044 ffdf004000530057 00000000 00000000 00000000 ffdf005000000000 00000000 00000000 00000000 ffdf006000000000 00000000 00000000 00000000 ffdf007000000000 00000000 00000000 00000000

??既然内容是一样的,我们再看看它们的物理页是不是一样的:
kd> !vtop 16840680 0x7ffe0000 X86VtoP: Virt 000000007ffe0000, pagedir 0000000016840680 X86VtoP: PAE PDPE 0000000016840688 - 00000000823e5001 X86VtoP: PAE PDE 00000000823e5ff8 - 00000000814bf067 X86VtoP: PAE PTE 00000000814bff00 - 0000000000041025 X86VtoP: PAE Mapped phys 0000000000041000 Virtual address 7ffe0000 translates to physical address 41000.kd> !vtop 16840680 0xffdf0000 X86VtoP: Virt 00000000ffdf0000, pagedir 0000000016840680 X86VtoP: PAE PDPE 0000000016840698 - 00000000823e3001 X86VtoP: PAE PDE 00000000823e3ff0 - 0000000000af3163 X86VtoP: PAE PTE 0000000000af3f80 - 0000000000041163 X86VtoP: PAE Mapped phys 0000000000041000 Virtual address ffdf0000 translates to physical address 41000.

??!vtop这个指令可以帮我们拆分虚拟地址到物理地址。为什么不在段页的部分讲是因为怕你懒,缺少练习。可以验证它们的物理页是一样的。
??我们先看看0xffdf0300这个地址里面存的是什么,先dd一下:
kd> dd 0xffdf0300 ffdf03007c92e4f0 7c92e4f4 00000000 00000000 ffdf031000000000 00000000 00000000 00000000 ffdf032000000000 00000000 00000000 00000000 ffdf033043dc3855 00000000 00000000 00000000 ffdf034000000000 00000000 00000000 00000000 ffdf035000000000 00000000 00000000 00000000 ffdf036000000000 00000000 00000000 00000000 ffdf037000000000 00000000 00000000 00000000

??然后我们uf一下看看汇编:
kd> uf 7c92e4f0 7c92e4f0 8bd4movedx,esp 7c92e4f2 0f34sysenter 7c92e4f4 c3ret

??可以发现,这个函数只是把esp的值交给了edx,然后调用sysenter。这个汇编就是快速调用。为什么叫快速调用?中断门进0环,需要的CSEIPIDT表中,需要查内存(SSESPTSS提供),而CPU如果支持sysenter指令时,操作系统会提前将CS/SS/ESP/EIP的值存储在MSR寄存器中,sysenter指令执行时,CPU会将MSR寄存器中的值直接写入相关寄存器,没有读内存的过程,所以叫快速调用,但本质是一样的。
??其实,快速调用并不是一直存在的,在比较古老的CPU是不支持快速调用的。它们进入内核的方式很简单粗暴,就是使用中断门。
??CPU如何知道是否支持快速调用呢?当通过eax=1来执行cpuid指令时,处理器的特征信息被放在ecxedx寄存器中,其中edx包含了一个SEP位(11位),该位指明了当前处理器知否支持sysenter/sysexit指令,具体细节可以查看白皮书。
??通过逆向汇编代码可以看出,不管CPU是否支持快速调用,它都是调用该地址。这就说明操作系统在初始化该结构体的时候必须先判断支不支持,然后填入适当的值。如果CPU支持快速调用,操作系统就会填入KiFastSystemCall函数的地址,我们可以看一下:
.text:7C92E4F0 ; _DWORD __stdcall KiFastSystemCall() .text:7C92E4F0public _KiFastSystemCall@0 .text:7C92E4F0 _KiFastSystemCall@0 proc near; DATA XREF: .text:off_7C923428↑o .text:7C92E4F0movedx, esp .text:7C92E4F2sysenter .text:7C92E4F2 _KiFastSystemCall@0 endp

??如果CPU不支持快速调用,操作系统就会填入KiIntSystemCall函数的地址,我们可以看一下:
.text:7C92E500 ; _DWORD __stdcall KiIntSystemCall() .text:7C92E500public _KiIntSystemCall@0 .text:7C92E500 _KiIntSystemCall@0 proc near; DATA XREF: .text:off_7C923428↑o .text:7C92E500 .text:7C92E500 arg_4= byte ptr8 .text:7C92E500 .text:7C92E500leaedx, [esp+arg_4] ; 参数指针 .text:7C92E504int2Eh; DOS 2+ internal - EXECUTE COMMAND .text:7C92E504; DS:SI -> counted CR-terminated command string .text:7C92E506retn .text:7C92E506 _KiIntSystemCall@0 endp .text:7C92E506

??本篇内容就先讲解这么多,进入0环的部分将在下一篇进行讲解。接下来我们将用代码重写ReadProcessMemory的3环部分,代码如下:
#include "stdafx.h" #include #include const int test=0x1234; BOOL __declspec(naked) __stdcall ReadProcMem0(DWORD handle,DWORD addr,unsigned char* buffer,DWORD len,DWORD sizeread) { _asm { mov eax, 0BAh ; mov edx, 7FFE0300h; call dword ptr [edx]; retn 14h; } }BOOL __declspec(naked) __stdcall ReadProcMem1(DWORD handle,DWORD addr,unsigned char* buffer,DWORD len,DWORD sizeread) { _asm { mov eax, 0BAh; lea edx, [esp+4]; int 2Eh; retn 14h; } }int main(int argc, char* argv[]) { int buffer = 0; ReadProcMem0((DWORD)GetCurrentProcess(),(DWORD)&test,(unsigned char*)&buffer,4,NULL); printf("第一次 buffer的值为:%x\n",buffer); buffer=0; ReadProcMem1((DWORD)GetCurrentProcess(),(DWORD)&test,(unsigned char*)&buffer,4,NULL); printf("第二次 buffer的值为:%x\n",buffer); system("pause"); return 0; }

??从上面的代码可以看出ReadProcMem0是还通过SystemCall进0环,ReadProcMem1直接重写了SystemCall进入0环(为什么没用sysenter?编译不通过)。如下是结果:
第一次 buffer的值为:1234 第二次 buffer的值为:1234 请按任意键继续. . .

本节练习
本节的答案将会在下一节进行讲解,务必把本节练习做完后看下一个讲解内容。不要偷懒,实验是学习本教程的捷径。
??俗话说得好,光说不练假把式,如下是本节相关的练习。如果练习没做好,就不要看下一节教程了,越到后面,不做练习的话容易夹生了,开始还明白,后来就真的一点都不明白了。本节练习不多,请保质保量的完成。
1?? 自己编写WriteProcessMemory函数(不使用任何DLL,直接调用0环函数)并在代码中使用。
下一篇 ??系统调用篇——0环层面调用过程

    推荐阅读