系统调用篇——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
??不幸的是,这个函数是人家导入的,如何查到从哪里导入的呢?我们可以按照如下图所示的操作找到:
文章图片
??我们知道
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环,需要的CS
、EIP
在IDT
表中,需要查内存(SS
与ESP
由TSS
提供),而CPU
如果支持sysenter
指令时,操作系统会提前将CS
/SS
/ESP
/EIP
的值存储在MSR
寄存器中,sysenter
指令执行时,CPU
会将MSR
寄存器中的值直接写入相关寄存器,没有读内存的过程,所以叫快速调用,但本质是一样的。??其实,快速调用并不是一直存在的,在比较古老的
CPU
是不支持快速调用的。它们进入内核的方式很简单粗暴,就是使用中断门。??
CPU
如何知道是否支持快速调用呢?当通过eax=1
来执行cpuid
指令时,处理器的特征信息被放在ecx
和edx
寄存器中,其中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环层面调用过程
推荐阅读
- 2018年11月19日|2018年11月19日 星期一 亲子日记第144篇
- 《魔法科高中的劣等生》第26卷(Invasion篇)发售
- 拍照一年啦,如果你想了解我,那就请先看看这篇文章
- 亲子日记第186篇,2018、7、26、星期四、晴
- SpringBoot调用公共模块的自定义注解失效的解决
- 如何在Mac中的文件选择框中打开系统隐藏文件夹
- 漫画初学者如何学习漫画背景的透视画法(这篇教程请收藏好了!)
- thinkphp|thinkphp 3.2 如何调用第三方类库
- 两短篇
- 单点登陆