API调用探寻首先我们选择OpenProcess作为目标API,进而分析3环进0环的过程。实验代码:
123456789#include
我们用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函数即可。
代码如下:
123456789101112131415161718192021222324252627282930313233343536373839#include
KUSER_SHARED_DATA使用windbg查询一下该结构的信息:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011020: kd> dt _KUSER_SHARED_DATAnt!_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进行分析。
1234567891011120: kd> rdmsr 0xC0000082msr[c0000082] = fffff807`084140000: kd> u fffff807`08414000nt!KiSystemCall64:fffff807`08414000 0f01f8 swapgsfffff807`08414003 654889242510000000 mov qword ptr gs:[10h],rspfffff807`0841400c 65488b2425a8010000 mov rsp,qword ptr gs:[1A8h]fffff807`08414015 6a2b push 2Bhfffff807`08414017 65ff342510000000 push qword ptr gs:[10h]fffff807`0841401f 4153 push r11fffff807`08414021 6a33 push 33hfffff807`08414023 51 push rcx可以发现其中调用了KiSystemCall64函数进入R0层。此处可以看出笔者并没有开启KPTI(页表隔离)
援引看雪博客 的内容,此处分析ntoskrnl.exe时注意,为了下载合适的pdb可以将其按如下改名:
ntoskrnl - 单处理器,不支持PAEntkrnlpa - 单处理器,支持PAEntkrnlmp - 多处理器,不支持PAEntkrpamp - 多处理器,支持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在上文中,我们针对OpenProcessAPI进行了分析,我们在实验中将手动解析SSDT表并找到内核所对应的函数。
首先我们在上文中知道了调用ZwOpenProcess的函数的调用号(Eax)为0x26,结合上述分析,我们需要先获取到SSDT基地址。
1231: kd> dq KeServiceDescriptorTablefffff802`3360b8c0 fffff802`328d19f0 00000000`00000000....我们可以发现其基地址为fffff802`328d19f0。
然后我们使用公式Offset = SSDTbase + 4 * 调用号。
1231: kd> dd fffff802`328d19f0 + 0x26 * 4fffff802`328d1a88 05f1e600 01484a01 0559ba06 051bf707....请注意这里是取4字节作为偏移进行下一步处理(movsxd的含义),也就是这一步Offset = 0x5f1e600。
下一步需要将(Offset ÷ 16) + SSDTBase算出最终的函数地址。
123456789101: kd> u fffff802`328d19f0 + 5f1e600 / 0x10nt!NtOpenProcess:fffff802`32ec3850 4883ec38 sub rsp,38hfffff802`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],r10bfffff802`32ec3869 4488542420 mov byte ptr [rsp+20h],r10bfffff802`32ec386e e83d8bf4ff call nt!PsOpenProcess (fffff802`32e0c3b0)fffff802`32ec3873 4883c438 add rsp,38hfffff802`32ec3877 c3 ret可以发现我们也是成功的获取到了SSDT中的函数。
参考资料《Intel® 64 和 IA-32 架构软件开发人员手册》 《Qforst个人博客》 《看雪博客》