API调用探寻
首先我们选择OpenProcess作为目标API,进而分析3环进0环的过程。
实验代码:
1 2 3 4 5 6 7 8 9
| #include <Windows.h> #include <stdio.h> int main() { printf("%p", main); getchar(); HANDLE hProc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, GetCurrentProcessId()); return 0; }
|
笔者这里一开始代码有些问题后面修改了一下应该使用GetCurrentProcessId()函数,下文图片没更改,但不影响理解
我们用x64dbg来看一下。
可以看出调用了Kernel32.dll
中的OpenProcess
,我们继续步入。
可以发现Kernel32.dll
中的OpenProcess
只是个代理函数,真正的函数在KernelBase.dll
中,我们继续步入。
可以看出OpenProcess
只是处理了一些参数,然后调用了ntdll.dll
中的ZwOpenProcess
,我们转到IDA中分析一下,我们可以发现ZwOpenProcess
只是NtOpenProcess
的一个别名,其导出位置都是一个函数。
其中可以发现在进入函数后,通过检测7FFE0308
地址的值,来判断是否使用syscall
或int 2E
指令进入内核。
那么7FFE0308
是什么呢?如果涉猎过x86就会发现,7FFE0308
这个地址与x86下_KUSER_SHARED_DATA
结构中的SystemCall
地址很接近,我们合理猜测7FFE0308
也是_KUSER_SHARED_DATA
结构中的SystemCall
。
实验1——自写R3进内核
在看完R3下进内核的过程后,我们可以尝试编写一段代码来代替ntdll.dll
完成从R3到R0的过程。
当我们完成这一切后,R3层的APIHOOK将对我们无效,且无法通过导入表的方式查看到我们引用的WindowsAPI。
要想完成这一切,我们就需要去KernelBase.dll
中查看如何ntdll.dll
的NtOpenProcess
函数,并完成复现。
我们只需要按照这个结构进行编写并完成上文中所示的NtOpenProcess
函数即可。
代码如下:
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
| #include <stdio.h> #include <Windows.h> typedef struct _CLIENT_ID { HANDLE UniqueProcess; HANDLE UniqueThread; }*PCLIENT_ID; typedef struct _OBJECT_ATTRIBUTES { ULONG Length; HANDLE RootDirectory; PVOID ObjectName; ULONG Attributes; PVOID SecurityDescriptor; PVOID SecurityQualityOfService; }*POBJECT_ATTRIBUTES;
EXTERN_C NTSTATUS OpenProcessNT(PHANDLE pH, ACCESS_MASK da, POBJECT_ATTRIBUTES oba, PCLIENT_ID cid); HANDLE OpenProc(DWORD dwDesiredAccess, BOOL bInheritHandle, DWORD dwProcessId) { _OBJECT_ATTRIBUTES obj = { 0 }; _CLIENT_ID cid = { 0 }; HANDLE retHandle = 0;
obj.Length = 48; obj.RootDirectory = 0; obj.Attributes = bInheritHandle ? 2 : 0; obj.ObjectName = 0; obj.SecurityDescriptor =(PVOID) 0x000EAB000000000; cid.UniqueProcess = (HANDLE)dwProcessId; NTSTATUS sta = OpenProcessNT(&retHandle, dwDesiredAccess, &obj, &cid); return retHandle; } int main() { HANDLE h = OpenProc(PROCESS_ALL_ACCESS, FALSE, GetCurrentProcessId()); printf("Handle is %p", h); CloseHandle(h); return 0; }
|
1 2 3 4 5 6
| OpenProcessNT PROC mov r10, rcx mov eax, 26h syscall ; or int 2Eh ret OpenProcessNT ENDP
|
编译运行后发现其确实可以正常完成OpenProcess
的功能。
KUSER_SHARED_DATA
使用windbg查询一下该结构的信息:
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 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102
| 0: kd> dt _KUSER_SHARED_DATA nt!_KUSER_SHARED_DATA +0x000 TickCountLowDeprecated : 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] Wchar +0x238 MaxStackTraceDepth : Uint4B +0x23c CryptoExponent : Uint4B +0x240 TimeZoneId : Uint4B +0x244 LargePageMinimum : Uint4B +0x248 AitSamplingValue : Uint4B +0x24c AppCompatFlag : Uint4B +0x250 RNGSeedVersion : Uint8B +0x258 GlobalValidationRunlevel : Uint4B +0x25c TimeZoneBiasStamp : Int4B +0x260 NtBuildNumber : Uint4B +0x264 NtProductType : _NT_PRODUCT_TYPE +0x268 ProductTypeIsValid : UChar +0x269 Reserved0 : [1] UChar +0x26a NativeProcessorArchitecture : Uint2B +0x26c NtMajorVersion : Uint4B +0x270 NtMinorVersion : Uint4B +0x274 ProcessorFeatures : [64] UChar +0x2b4 Reserved1 : Uint4B +0x2b8 Reserved3 : Uint4B +0x2bc TimeSlip : Uint4B +0x2c0 AlternativeArchitecture : _ALTERNATIVE_ARCHITECTURE_TYPE +0x2c4 BootId : Uint4B +0x2c8 SystemExpirationDate : _LARGE_INTEGER +0x2d0 SuiteMask : Uint4B +0x2d4 KdDebuggerEnabled : UChar +0x2d5 MitigationPolicies : UChar +0x2d5 NXSupportPolicy : Pos 0, 2 Bits +0x2d5 SEHValidationPolicy : Pos 2, 2 Bits +0x2d5 CurDirDevicesSkippedForDlls : Pos 4, 2 Bits +0x2d5 Reserved : Pos 6, 2 Bits +0x2d6 CyclesPerYield : Uint2B +0x2d8 ActiveConsoleId : Uint4B +0x2dc DismountCount : Uint4B +0x2e0 ComPlusPackage : Uint4B +0x2e4 LastSystemRITEventTickCount : Uint4B +0x2e8 NumberOfPhysicalPages : Uint4B +0x2ec SafeBootMode : UChar +0x2ed VirtualizationFlags : UChar +0x2ee Reserved12 : [2] UChar +0x2f0 SharedDataFlags : Uint4B +0x2f0 DbgErrorPortPresent : Pos 0, 1 Bit +0x2f0 DbgElevationEnabled : Pos 1, 1 Bit +0x2f0 DbgVirtEnabled : Pos 2, 1 Bit +0x2f0 DbgInstallerDetectEnabled : Pos 3, 1 Bit +0x2f0 DbgLkgEnabled : Pos 4, 1 Bit +0x2f0 DbgDynProcessorEnabled : Pos 5, 1 Bit +0x2f0 DbgConsoleBrokerEnabled : Pos 6, 1 Bit +0x2f0 DbgSecureBootEnabled : Pos 7, 1 Bit +0x2f0 DbgMultiSessionSku : Pos 8, 1 Bit +0x2f0 DbgMultiUsersInSessionSku : Pos 9, 1 Bit +0x2f0 DbgStateSeparationEnabled : Pos 10, 1 Bit +0x2f0 SpareBits : Pos 11, 21 Bits +0x2f4 DataFlagsPad : [1] Uint4B +0x2f8 TestRetInstruction : Uint8B +0x300 QpcFrequency : Int8B +0x308 SystemCall : Uint4B +0x30c Reserved2 : Uint4B +0x310 SystemCallPad : [2] Uint8B +0x320 TickCount : _KSYSTEM_TIME +0x320 TickCountQuad : Uint8B +0x320 ReservedTickCountOverlay : [3] Uint4B +0x32c TickCountPad : [1] Uint4B +0x330 Cookie : Uint4B +0x334 CookiePad : [1] Uint4B +0x338 ConsoleSessionForegroundProcessId : Int8B +0x340 TimeUpdateLock : Uint8B +0x348 BaselineSystemTimeQpc : Uint8B +0x350 BaselineInterruptTimeQpc : Uint8B +0x358 QpcSystemTimeIncrement : Uint8B +0x360 QpcInterruptTimeIncrement : Uint8B +0x368 QpcSystemTimeIncrementShift : UChar +0x369 QpcInterruptTimeIncrementShift : UChar +0x36a UnparkedProcessorCount : Uint2B +0x36c EnclaveFeatureMask : [4] Uint4B +0x37c TelemetryCoverageRound : Uint4B +0x380 UserModeGlobalLogger : [16] Uint2B +0x3a0 ImageFileExecutionOptions : Uint4B +0x3a4 LangGenerationCount : Uint4B +0x3a8 Reserved4 : Uint8B +0x3b0 InterruptTimeBias : Uint8B +0x3b8 QpcBias : Uint8B +0x3c0 ActiveProcessorCount : Uint4B +0x3c4 ActiveGroupCount : UChar +0x3c5 Reserved9 : UChar +0x3c6 QpcData : Uint2B +0x3c6 QpcBypassEnabled : UChar +0x3c7 QpcShift : UChar +0x3c8 TimeZoneBiasEffectiveStart : _LARGE_INTEGER +0x3d0 TimeZoneBiasEffectiveEnd : _LARGE_INTEGER +0x3d8 XState : _XSTATE_CONFIGURATION +0x710 FeatureConfigurationChangeStamp : _KSYSTEM_TIME +0x71c Spare : Uint4B
|
其中不难发现 在0x308处发现了SystemCall
,那么7FFE0000
则是KUSER_SHARED_DATA
的地址。
在x86下,ntdll会直接使用SystemCall
中的地址来执行KiFastSystemCall
或KiIntSystemCall
,其中分别对应着sysenter
/ syscall
(快速调用指令)或int
中断指令进行调用。
我们继续看x64下,其中就判断了SystemCall
中是否为1,来选择使用是syscall
或int
中断指令调用(为1使用中断,为0使用快速调用),相当于是把原来KiFastSystemCall
和KiIntSystemCall
函数中的指令直接展开到了此处。
我猜测这里是防止该结构变化而权衡的一种方法,毕竟4字节塞不下x64的地址
地址
KUSER_SHARED_DATA
结构的地址是固定的,在r3下是0x7FFE0000
,在r0下是0xFFFFF78000000000
。正如其名字中的SHARED,他是一个r0,r3的共享结构。
我们可以在r0下查看KUSER_SHARED_DATA
的物理内存。
以及附加到一个程序查看KUSER_SHARED_DATA
的物理内存。
其中可以看到除了内存属性不一样以外,物理地址是一样的,均为0x10B5000
(保护模式-页中有讲怎么转换)。
我们可以发现R3下只有读权限,而R0下则有写权限。
快速系统调用
在x86下KiFastSystemCall
函数对应着快速系统调用,而在x64下则没有这个函数转而直接把函数内部的指令syscall
展开到了每个函数处
快速系统调用,顾名思义就是快,快速系统调用指令是由CPU进行直接处理的。
CPU的操作
SYSCALL
当我们执行了syscall
指令后CPU帮我们做了许多操作,其中概括如下:
- 将
RFLAGS
保存到R11
,将下一条指令的RIP
保存到RCX
- 将
CS
段寄存器的选择子设置为MSR
寄存器中的IA32_STAR
[47:32] - 将
RIP
变为MSR
寄存器中的IA32_LSTAR
- 将
SS
段寄存器的选择子设置为MSR
寄存器中的IA32_STAR
[47:32] - 将
RFLAGS
设置为其当前值与MSR
寄存器中的IA32_FMASK
值的补码的逻辑与值。
SYSRET
当我们执行了sysret
指令后CPU帮我们做了许多操作,其中概括如下:
- 将
CS
段寄存器的选择子设置为MSR
寄存器中的IA32_STAR
[63:43]+ 16 - 将
RIP
变为RCX
寄存器中存储的值 - 将
SS
段寄存器的选择子设置为MSR
寄存器中的IA32_STAR
[63:43]+ 8 - 将
EFLAGS
变为R11
寄存器中存储的值
我们可以发现CPU对于syscall
指令来说不保存栈指针,sysret
指令不恢复栈指针。
分析
翻阅手册可以得知IA32_LSTAR
寄存器的地址为C0000082
我们使用windbg进行分析。
1 2 3 4 5 6 7 8 9 10 11 12
| 0: kd> rdmsr 0xC0000082 msr[c0000082] = fffff807`08414000 0: kd> u fffff807`08414000 nt!KiSystemCall64: fffff807`08414000 0f01f8 swapgs fffff807`08414003 654889242510000000 mov qword ptr gs:[10h],rsp fffff807`0841400c 65488b2425a8010000 mov rsp,qword ptr gs:[1A8h] fffff807`08414015 6a2b push 2Bh fffff807`08414017 65ff342510000000 push qword ptr gs:[10h] fffff807`0841401f 4153 push r11 fffff807`08414021 6a33 push 33h fffff807`08414023 51 push rcx
|
可以发现其中调用了KiSystemCall64
函数进入R0层。
此处可以看出笔者并没有开启KPTI(页表隔离)
援引看雪博客 的内容,此处分析ntoskrnl.exe
时注意,为了下载合适的pdb可以将其按如下改名:
- ntoskrnl - 单处理器,不支持PAE
- ntkrnlpa - 单处理器,支持PAE
- ntkrnlmp - 多处理器,不支持PAE
- ntkrpamp - 多处理器,支持PAE
此处我们分析代码更多的KiSystemCall64Shadow
函数来解析。
继续往下看,后面有一大堆修栈的指令,我们直接看最底下
其直接跳跃到了KiSystemServiceUser
函数,我们跟过去看一下
可以发现往下走则是处理线程处在调试状态时的情况,我们简化一下,直接去看正常状态下的调用。
我们只关注一下非GDI线程的情况,也就是走SSDT的情况。
继续跟。
其中我们看一下KiSystemServiceCopyStart
,该块是用来复制参数的,其中由上图的jmp r11
指令来控制复制多少个参数。
复制完参数后,便进入真正调用的地方。
在调用后,便进入KiSystemServiceExit
返回给R3。
KiSystemCall64
函数就是KiSystemCall64Shadow
在不开页表隔离机制的情况下实现,代码大同小异
总结
- 通过API调用进入
ntdll.dll
或者其他,调用syscall
(快速调用)指令。 - 快速调用进入
KiSystemCall64
或者KiSystemCall64Shadow
函数。 - 切换内核栈,使用
_KTRAP_FRAME
结构保存寄存器信息 - 找到
SSDT
表,并调用
在SSDT
转换的过程中:
- 读取
KeServiceDescriptorTable
存的值,获取SSDT
基址。 - 使用公式
Offset = SSDTbase + 4 * 调用号
获取4字节的偏移。 - 使用公式
(Offset ÷ 16) + SSDTBase
直接获取函数地址。
INT 2E 中断调用
既然是中断调用,那么其信息应该在IDT
中,直接转到IDT
可以查看到其对应的函数为KiSystemServiceShadow
(开启页表隔离)或KiSystemService
。
KiSystemServiceShadow
根据上图可以发现其结构与KiSystemCall64Shadow
函数一开始的处理过程十分相似,在设置好内核Cr3
与一些必要操作后便进入了KiSystemService
函数。
KiSystemService
该函数与KiSystemCall64
的大致流程极为相似,几乎都是对那些东西进行操作。故此不做细致分析。
由图可知,其最后是调用了KiSystemServiceUser
函数,与快速系统调用的最后结果殊途同归。
实验2——手动解析SSDT
在上文中,我们针对OpenProcess
API进行了分析,我们在实验中将手动解析SSDT表并找到内核所对应的函数。
首先我们在上文中知道了调用ZwOpenProcess
的函数的调用号(Eax
)为0x26
,结合上述分析,我们需要先获取到SSDT基地址。
1 2 3
| 1: kd> dq KeServiceDescriptorTable fffff802`3360b8c0 fffff802`328d19f0 00000000`00000000 ....
|
我们可以发现其基地址为fffff802`328d19f0
。
然后我们使用公式Offset = SSDTbase + 4 * 调用号
。
1 2 3
| 1: kd> dd fffff802`328d19f0 + 0x26 * 4 fffff802`328d1a88 05f1e600 01484a01 0559ba06 051bf707 ....
|
请注意这里是取4字节作为偏移进行下一步处理(movsxd的含义),也就是这一步Offset = 0x5f1e600
。
下一步需要将(Offset ÷ 16) + SSDTBase
算出最终的函数地址。
1 2 3 4 5 6 7 8 9 10
| 1: kd> u fffff802`328d19f0 + 5f1e600 / 0x10 nt!NtOpenProcess: fffff802`32ec3850 4883ec38 sub rsp,38h fffff802`32ec3854 65488b042588010000 mov rax,qword ptr gs:[188h] fffff802`32ec385d 448a9032020000 mov r10b,byte ptr [rax+232h] fffff802`32ec3864 4488542428 mov byte ptr [rsp+28h],r10b fffff802`32ec3869 4488542420 mov byte ptr [rsp+20h],r10b fffff802`32ec386e e83d8bf4ff call nt!PsOpenProcess (fffff802`32e0c3b0) fffff802`32ec3873 4883c438 add rsp,38h fffff802`32ec3877 c3 ret
|
可以发现我们也是成功的获取到了SSDT
中的函数。
参考资料
- 《Intel® 64 和 IA-32 架构软件开发人员手册》
- 《Qforst个人博客》
- 《看雪博客》