系统调用001 API从三环进零环的过程

前言

在三环操作系统提供了各种API,这些API实际上只是一个暴露在三环的接口,真正的功能实现部分,最终都是要进到零环。

逆向分析ReadProcessMemory

###ReadProcessMemory

以ReadProcessMemory这个函数为例,来看一下三环的API的执行流程大致是什么样的。用IDA打开kernel32.dll,找到ReadProcessMemory函数。

在这里插入图片描述

ReadProcessMemory在内部调用了NtReadVirtualMemory函数,这个函数来自于kernel32.dll的导入表。接下来我们在导入表中找到这个函数

在这里插入图片描述

从导入表中可以看到,NtReadVirtualMemory这个函数来自于ntdll。接着我们用IDA打开ntdll.dll。并找到NtReadVirtualMemory函数。

NtReadVirtualMemory

在这里插入图片描述

NtReadVirtualMemory这个函数只有下面的几行代码

.text:7C92E2BB                 mov     eax, 0BAh       ; NtReadVirtualMemory
.text:7C92E2C0                 mov     edx, 7FFE0300h
.text:7C92E2C5                 call    dword ptr [edx]
.text:7C92E2C7                 retn    14h

这里call了一个[edx],那么接下来我们就要去找[edx]指向的是哪个函数,而edx的内容则取决于7FFE0300h这个地址里面是什么。

而想要了解7FFE0300h这个地址里的内容,需要先了解一个结构体->_KUSER_SHARED_DATA。

_KUSER_SHARED_DATA

在用户层和内核层分别定义了一个_KUSER_SHARED_DATA结构区域,用于在用户层和内核层共享某些数据。它们使用固定的地址值映射,_KUSER_SHARED_DATA结构区域在User和Kernel层地址分别为:

  • User层地址为:0x7ffe0000
  • Kernel层地址为:0xffdf0000

User层和Kernel层映射同一个物理页。虽然它们指向的是同一个物理页,但在User层是只读的,在Kernel层是可写的。

直接在windbg里查看一下这两个地址的内容,首先挂载到任意一个进程

PROCESS 88049c68  SessionId: 0  Cid: 0930    Peb: 7ffd9000  ParentCid: 05b8
    DirBase: 7f4b64c0  ObjectTable: a7725ab8  HandleCount: 14034.
    Image: OEM8.exe

kd> .process 88049c68  
Implicit process is now 88049c68
WARNING: .cache forcedecodeuser is not enabled

接着查看这两个地址的内容

kd> dd 0x7ffe0000
7ffe0000  00000000 0f99a027 5283733f 00000000
7ffe0010  00000000 22ad355a 01d5b7d1 01d5b7d1
7ffe0020  f1dcc000 ffffffbc ffffffbc 014c014c
7ffe0030  003a0043 0057005c 006e0069 006f0064
7ffe0040  00730077 00000000 00000000 00000000
7ffe0050  00000000 00000000 00000000 00000000
7ffe0060  00000000 00000000 00000000 00000000
7ffe0070  00000000 00000000 00000000 00000000
kd> dd 0xffdf0000
ffdf0000  00000000 0f99a027 5283733f 00000000
ffdf0010  00000000 22ad355a 01d5b7d1 01d5b7d1
ffdf0020  f1dcc000 ffffffbc ffffffbc 014c014c
ffdf0030  003a0043 0057005c 006e0069 006f0064
ffdf0040  00730077 00000000 00000000 00000000
ffdf0050  00000000 00000000 00000000 00000000
ffdf0060  00000000 00000000 00000000 00000000
ffdf0070  00000000 00000000 00000000 00000000

两块地址空间的内容完全相同。接着再查看一下两个地址的属性

kd> !vtop 7f4b64c0 0x7ffe0000 
X86VtoP: Virt 000000007ffe0000, pagedir 000000007f4b64c0
X86VtoP: PAE PDPE 000000007f4b64c8 - 000000004fe09801
X86VtoP: PAE PDE 000000004fe09ff8 - 000000004fa07867
X86VtoP: PAE PTE 000000004fa07f00 - 80000000001e2025
X86VtoP: PAE Mapped phys 00000000001e2000
Virtual address 7ffe0000 translates to physical address 1e2000.
kd> !vtop 7f4b64c0 0xffdf0000
X86VtoP: Virt 00000000ffdf0000, pagedir 000000007f4b64c0
X86VtoP: PAE PDPE 000000007f4b64d8 - 000000004c00b801
X86VtoP: PAE PDE 000000004c00bff0 - 000000000018a063
X86VtoP: PAE PTE 000000000018af80 - 00000000001e2163
X86VtoP: PAE Mapped phys 00000000001e2000
Virtual address ffdf0000 translates to physical address 1e2000.

0x7ffe0000这个三环的地址PTE属性是只读的,而0xffdf0000这个零环的地址的PTE属性是可读可写的。

SystemCall

接下来回到NtReadVirtualMemory这个函数

.text:7C92E2BB                 mov     eax, 0BAh       ; NtReadVirtualMemory
.text:7C92E2C0                 mov     edx, 7FFE0300h
.text:7C92E2C5                 call    dword ptr [edx]
.text:7C92E2C7                 retn    14h

现在我们已经知道了0x7ffe0000这个内存是一块共享的内存区域,接下来看一下偏移0x300的位置也就是7FFE0300这个地址的值是什么。

kd> dt _KUSER_SHARED_DATA 0x7ffe0000
nt!_KUSER_SHARED_DATA
   +0x000 TickCountLowDeprecated : 0
   +0x004 TickCountMultiplier : 0xf99a027
   +0x008 InterruptTime    : _KSYSTEM_TIME
   +0x014 SystemTime       : _KSYSTEM_TIME
   +0x020 TimeZoneBias     : _KSYSTEM_TIME
   +0x02c ImageNumberLow   : 0x14c
   +0x02e ImageNumberHigh  : 0x14c
   +0x030 NtSystemRoot     : [260]  "C:\Windows"
   +0x238 MaxStackTraceDepth : 0
   +0x23c CryptoExponent   : 0
   +0x240 TimeZoneId       : 0
   +0x244 LargePageMinimum : 0x200000
   +0x248 Reserved2        : [7] 0
   +0x264 NtProductType    : 1 ( NtProductWinNt )
   +0x268 ProductTypeIsValid : 0x1 ''
   +0x26c NtMajorVersion   : 6
   +0x270 NtMinorVersion   : 1
   +0x274 ProcessorFeatures : [64]  ""
   +0x2b4 Reserved1        : 0x7ffeffff
   +0x2b8 Reserved3        : 0x80000000
   +0x2bc TimeSlip         : 0
   +0x2c0 AlternativeArchitecture : 0 ( StandardDesign )
   +0x2c4 AltArchitecturePad : [1] 0
   +0x2c8 SystemExpirationDate : _LARGE_INTEGER 0x0
   +0x2d0 SuiteMask        : 0x310
   +0x2d4 KdDebuggerEnabled : 0x3 ''
   +0x2d5 NXSupportPolicy  : 0x2 ''
   +0x2d8 ActiveConsoleId  : 1
   +0x2dc DismountCount    : 0
   +0x2e0 ComPlusPackage   : 0xffffffff
   +0x2e4 LastSystemRITEventTickCount : 0
   +0x2e8 NumberOfPhysicalPages : 0x7ff7e
   +0x2ec SafeBootMode     : 0 ''
   +0x2ed TscQpcData       : 0 ''
   +0x2ed TscQpcEnabled    : 0y0
   +0x2ed TscQpcSpareFlag  : 0y0
   +0x2ed TscQpcShift      : 0y000000 (0)
   +0x2ee TscQpcPad        : [2]  ""
   +0x2f0 SharedDataFlags  : 0xc
   +0x2f0 DbgErrorPortPresent : 0y0
   +0x2f0 DbgElevationEnabled : 0y0
   +0x2f0 DbgVirtEnabled   : 0y1
   +0x2f0 DbgInstallerDetectEnabled : 0y1
   +0x2f0 DbgSystemDllRelocated : 0y0
   +0x2f0 DbgDynProcessorEnabled : 0y0
   +0x2f0 DbgSEHValidationEnabled : 0y0
   +0x2f0 SpareBits        : 0y0000000000000000000000000 (0)
   +0x2f4 DataFlagsPad     : [1] 0
   +0x2f8 TestRetInstruction : 0xc3
   +0x300 SystemCall       : 0x776c70b0
   +0x304 SystemCallReturn : 0x776c70b4
   +0x308 SystemCallPad    : [3] 0
   +0x320 TickCount        : _KSYSTEM_TIME
   +0x320 TickCountQuad    : 0x22a9
   +0x320 ReservedTickCountOverlay : [3] 0x22a9
   +0x32c TickCountPad     : [1] 0
   +0x330 Cookie           : 0xe0c0696a
   +0x334 CookiePad        : [1] 0
   +0x338 ConsoleSessionForegroundProcessId : 0n1600
   +0x340 Wow64SharedInformation : [16] 0
   +0x380 UserModeGlobalLogger : [16] 0
   +0x3a0 ImageFileExecutionOptions : 0
   +0x3a4 LangGenerationCount : 1
   +0x3a8 Reserved5        : 0
   +0x3b0 InterruptTimeBias : 0
   +0x3b8 TscQpcBias       : 0
   +0x3c0 ActiveProcessorCount : 1
   +0x3c4 ActiveGroupCount : 1
   +0x3c6 Reserved4        : 0
   +0x3c8 AitSamplingValue : 0
   +0x3cc AppCompatFlag    : 1
   +0x3d0 SystemDllNativeRelocation : 0xff7c0000
   +0x3d8 SystemDllWowRelocation : 0
   +0x3dc XStatePad        : [1] 0
   +0x3e0 XState           : _XSTATE_CONFIGURATION

找到+300的位置

 +0x300 SystemCall       : 0x776c70b0

这个地方是一个SystemCall,查看一下对应的反汇编代码,看看NtReadVirtualMemory函数的call dword ptr [edx]具体是做了什么。

kd> u 0x776c70b0
ntdll!KiFastSystemCall
776c70b0 8bd4            mov     edx,esp
776c70b2 0f34            sysenter
776c70b4 c3              ret

这个函数叫KiFastSystemCall,实际上就只有三行代码,首先把esp保存到edx,目的是为了在零环能够方便的找到三环的堆栈。接着用sysenter指令进到零环,最后通过ret指令返回。

然而并不是所有的CPU都支持sysenter快速调用指令。这就要了解一下另外一个问题?0x7ffe0300到底存储的是什么?

###两种从三环进零环的方式

操作系统在启动的时候,需要初始化_KUSER_SHARED_DATA这个结构体,其中最重要的就是初始化0x300这个位置。操作系统要往这里面写一个函数,这个函数决定了所有的三环的API进入零环的方式。

操作系统在写入之前会通过cpuid这个指令来检查当前的CPU是否支持快速调用,如果支持的话,就往0x300这个位置写入KiFastSystemCall。如果不支持,则写入KiIntSystemCall。

我们可以在IDA中看到KiIntSystemCall的函数内容

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uUumiE8A-1576921324496)(assets/1576917420769.png)]

.text:7C92EBA5                 lea     edx, [esp+arg_4]
.text:7C92EBA9                 int     2Eh            
.text:7C92EBAB                 retn

KiIntSystemCall就只有三行代码,利用int 0x2E这条指令通过中断门的方式进入零环。

也就是说Windows使用了两种从三环进零环的方式。一种是中断门,一种是sysenter快速调用。

通过int 0x2E中断门进入零环

如果是通过中断门的方式进入到零环的话,最终EIP会指向哪呢?这就要查看IDT表了。

首先查看idt表的基址

kd> r idtr
idtr=80b95400

接着查看IDT表项0x2E的位置的段描述符

kd> dq 80b95400+0x2E*8
80b95570  83e8ee00`00083fee 83e88e00`000876b0
80b95580  83e88e00`000836b0 83e88e00`000836ba
80b95590  83e88e00`000836c4 83e88e00`000836ce
80b955a0  83e88e00`000836d8 83e88e00`000836e2
80b955b0  83e88e00`000836ec 83e28e00`00089104
80b955c0  83e88e00`00083700 83e88e00`0008370a
80b955d0  83e88e00`00083714 83e88e00`0008371e
80b955e0  83e88e00`00083728 83e88e00`00083732

通过拆分83e8ee00`00083fee这个中断门描述符可以得出CS段选择子为0008,EIP为83e83fee。也就是说API通过中断门的方式最终会跳转到0x83e83fee。接着查看一下这个地址的反汇编

kd> u 83e83fee
nt!KiSystemService:
83e83fee 6a00            push    0
83e83ff0 55              push    ebp
83e83ff1 53              push    ebx
83e83ff2 56              push    esi
83e83ff3 57              push    edi
83e83ff4 0fa0            push    fs
83e83ff6 bb30000000      mov     ebx,30h
83e83ffb 668ee3          mov     fs,bx

KiSystemService函数的地址是8开头的,而且模块是nt不再是ntdll。到这里,API已经完成了从三环进入零环的过程。

通过sysenter快速调用进入零环

想要从三环进入到零环首先必须要提权,提权需要切换CS SS EIP ESP。如果通过中断门进入零环,门描述符里保存有CS和EIP,而SS和ESP来自于TSS。

在了解sysenter指令之前,要先了解一个寄存器,叫MSR。操作系统并没有公开这个寄存器的内部细节。但是我们可以知道这个寄存器的部分含义:

  • 0x174保存的是CS
  • 0x175保存的是ESP
  • 0x176保存的是EIP

如果想查看msr寄存器174就可以使用下面的指令

kd> rdmsr 174
msr[174] = 00000000`00000008

sysenter快速调用指令完成的事情就是从msr寄存器里拿到174 175和176的值,覆盖原来寄存器的值。int 0x2E和sysenter两种进入零环的方式的本质都是切换寄存器。

还有一个问题在于,通过msr寄存器只能拿到三个值,分别是CS ESP和EIP,那么SS来自于哪呢?这个SS的值实际上是写死的。举个例子来说,如果提权之后的CS的值为8,那么SS=CS+8=0x10。(具体细节请参考Intel白皮书第二卷 搜索sysenter)

总结

API从三环进到零环过程如图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vBdovwgd-1576921324505)(assets/API三环进零环的过程.png)]

sysenter快速调用指令完成的事情就是从msr寄存器里拿到174 175和176的值,覆盖原来寄存器的值。int 0x2E和sysenter两种进入零环的方式的本质都是切换寄存器。

还有一个问题在于,通过msr寄存器只能拿到三个值,分别是CS ESP和EIP,那么SS来自于哪呢?这个SS的值实际上是写死的。举个例子来说,如果提权之后的CS的值为8,那么SS=CS+8=0x10。(具体细节请参考Intel白皮书第二卷 搜索sysenter)

总结

API从三环进到零环过程如图:

在这里插入图片描述

发布了99 篇原创文章 · 获赞 89 · 访问量 7万+

猜你喜欢

转载自blog.csdn.net/qq_38474570/article/details/103646726