今天开始我们来分析系统api进0环的过程
先写一个应用程序然后我们调用一个readprocessmemory的系统api
#include
#include
DWORD32 testVal = 111;
int main() {
HANDLE h = GetCurrentProcess();
DWORD32 t;
size_t len;
ReadProcessMemory(h, &testVal, &t, 4, &len);
printf("%d\r\n", t);
return 0;
}
注意编译为release版x64环境下的应用程序
首先使用x64dbg来调试这个进程寻找到readprocessmemory的地方看一下
可以看到他是调用了ZwReadVirtualMemory,我们鼠标选中后会看到是ntdll中的导出函数,我们跟进去看一下
可以看到ZwReadVirtualMemory的代码很短只有几行,先是传入3F给eax然后他会检查0x7ffe0308的位置是否为1然后调用syscall指令否则执行int 2e,如果大家看过winxp内核的话就会觉得很熟悉,因为xp中也差不多是这样的流程只不过是直接调用0x7ffe0300的位置然后判断怎么执行而且执行sysenter和int 2e的调用没有放在同一个函数里而已
syscall让我想起了sysenter指令那么我们可以推测这俩指令是不是差不多的作用
而且不知道大家有没有学过x86的内核,在x86里也是使用了0x7ffe0000这个位置来作为一个叫做KUSER_SHARED_DATA的数据结构共享给0环和3环一起用我们可不可以认为Windows为了保持兼容所以这个位置还是跟之前一样的作用呢
现在我们发现了两个要去验证的问题
首先来验证一下0x7ffe0000的位置
通过windbg可以看到这个结构体仍旧在使用,而且308的位置 是一个叫做SystemCall 的指针
nt!DbgBreakPointWithStatus:
fffff801`476036b0 cc int 3
0: kd> dt _KUSER_SHARED_DATA
ntdll!_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
我们在回顾刚才的代码思考一下很明显x64里没有像xp一样直接跳到0x7ffe0308的位置去执行而是判断了一下这个位置是否为1,然后根据这个位置判断cpu是否支持syscall指令,然后通过int 2e进0环
3环进0环的3环部分流程大体明白了我们再看一下syscall是干嘛的,在白皮书的5.8.8章有详细介绍
与sysenter/sysexit类似,syscall和sysret是一对配套指令, 是快速在R3和R0之间转换的指令。
syscall
{
合法性检测
rcx = rip
rip = IA32_LSTAR
r11 = RFLAGS
RFLAGS = RFLAGS AND NOT(IA32_FMASK)
CS.Selector = IA32_STAR[47:32] AND 0FFFCh
修正CS,SS,CPL
}
sysret
{
合法性检测
rip = ecx/ecx
根据R11的值对RFLAGS赋值,且RF,VM,部分reserved位复位。
CS.Selector = IA32_STAR[63:48] + 16/IA32_STAR[63:48]
修正CS,SS,CPL
总结一下syscall主要会做下面几个事情
页表隔离机制是为了解决cpu的熔断bug而在软件层面做出的保护机制,他的主要动作就是在应用层和内核层使用的cr3变为两个不同的cr3,在xp的机制是一个进程一个cr3而现在是一个进程两个cr3分别给应用层用和给内核用,也就是在应用进内核的时候会进行cr3的切换因为现在的内存页面映射不在是像xp一样将高2G完整的映射到所有的进程中,下面贴出一个图来说明
如上图所示,在内核使用的cr3中会将内核所有的内容映射进去并将应用数据改为不可执行的属性映射进来
在应用层只会映射内核文件中的KVASCODE段的内容,这部分内容主是跳板代码,在这部分代码中切换cr3保存现场等,切换完cr3之后就进入到了真正的内核cr3所映射的整体中然后在jmp这类的跳转到真正的内核代码中去执行然后在返回回来切换cr3之后ret
这样做就可以隔离掉内核和应用层防止借助于cpu的熔断直接读取内核数据
下面我们找一个中断进0环看一下代码是怎么样的
我们在windbg中看一下int3中断的代码
ida的注释中我加了详细的解释,就不再文章中赘述了
现在我们了解了kpti机制和syscall的动作,下面我们就可以真正的去看系统api调用的代码了
之前我们知道syscall进0环时候的cs是存在IA32_STAR这个位置的
我们到windbg里看一下msr寄存器的值
0: kd> rdmsr 0xc0000082
msr[c0000082] = fffff801`4760d400
fffff803`7520d400这个地址我们反汇编一下可以发现KiSystemCall64这个函数就是我们进0环时候的地址
0: kd> u fffff803`7520d400
nt!KiSystemCall64:
那下面我们看一下先来看一下KiSystemCall64Shadow的代码是什么
ntoskrnl - 单处理器,不支持PAE
ntkrnlpa - 单处理器,支持PAE
ntkrnlmp - 多处理器,不支持PAE
ntkrpamp - 多处理器,支持PAE
上面是内核文件名字的区别大家根据自己的环境去找内核文件进行逆向分析,关于符号的话可以设置内核符号表地址或者直接load pdb文件都可以
然后alt+t全局搜索KiSystemCall64就可以找到跳板函数和真正的函数了
KiSystemCall64Shadow:
进来之后就看到切换gs然后保存现场到代码
然后在往下看就会跳到KiSystemServiceUser这个函数里,注意到这个函数的时候已经不是KVASCODE段了,而是跳到了.text段,说明已经进入到了真正的内核中而不是跳板代码
下面的过程就跟xp系统里的流程差不多了,判断是不是GDI线程如果是的话用Shdow表并且要转换线程位GDI线程,这部分我们不看我们现在直接看调用函数的位置
在中间过程里KiSystemServiceCopyStart这边函数负责复制参数,有兴趣的可以看一下
大家可以再看一下KiSystemCall64函数会发现这两个函数长得差不多而且也是走的KiSystemServiceUser这个函数,我这边就不带大家看了
可以发现系统api的过程大致如下:
实验内容:驱动实现遍历ssdt表并打印出来,目前我们还没办法hookssdt表因为Windows系统有pg机制
(关于ssdt表的结构等知识大家到网上搜一下就有了,跟xp时候基本一样)
驱动代码我过两天会贴出来,国庆结束了这几天会比较忙
temCall64不需要
4. 在KiSystemCall64(Shadow)函数里切换栈并保存现场到0环栈把0环栈底作为KTRAP_FRAME结构体用
5. 保存完现场就是找到ssdt表然后调用对应序号的函数即可
实验内容:驱动实现遍历ssdt表并打印出来,目前我们还没办法hookssdt表因为Windows系统有pg机制
(关于ssdt表的结构等知识大家到网上搜一下就有了,跟xp时候基本一样)
驱动代码我过两天会贴出来,国庆结束了这几天会比较忙
另外说一下我其实本来是想着在页机制的研究那章写页表隔离机制和如何找自映射的,不过后来在做这套教程的时候又觉得不太合适,我也是一边找自己之前自学时候的笔记一边回顾去整理出来可能发出来的文章并没有跟之前说的文章顺序一致不过最后所有的内容肯定都会有的
这是我提前整理出的目录结构,现在在一点一点填充内容,后面会慢慢发出来的