关于Windows系统调用实现的笔记
NetRoc
从Win XP开始,Windows的系统调用都是通过sysenter指令进入KiFastCallEntry或者_KiFastCallEntry2。最近稍微看了一下这部分的wrk代码实现,作了些笔记。
-
KiFastCallEntry中如何引用SSDT和SSDT Shadow
从sysenter进入KiFastCallEntry后,系统取得SSDT和SSDT Shadow的地址并不是直接通过内核导出的KeServiceDescriptorTable和未导出的KeServiceDescriptorTableShadow变量来引用的。而是从PCR取得_KTHREAD地址,通过KTHREAD中的ServiceTable来获得服务表地址的。I386处理器的KiFastCallEntry相关代码片段如下:
mov ebx, PCR[PcSelfPcr] ; 取得PRCB地址
push KGDT_R3_TEB OR RPL_MASK ; Push user mode FS
mov esi, [ebx].PcPrcbData+PbCurrentThread ; 获得当前线程地址
……
add edi, [esi]+ThServiceTable ; 计算出服务表的地址
通过这里,我们可以知道系统调用所使用的的服务表来自_KTHREAD这种"动态"数据,而非KeServiceDescriptorTable这种相对"静态"的数据。这一点在很多情况下都有较强的利用价值。比如XXX……
-
nt!_KTHREAD中ServiceTable字段的设置
NtCreateThread内部是通过PspCreateThread创建线程的。进行了线程的相关结构内存分配后,会调用KeInitThread初始化这些结构。KeInitThread中就会设置nt!_KTHREAD的ServiceTable字段。如下:
#if defined(_AMD64_)
Thread->ServiceTable = KeServiceDescriptorTable[SYSTEM_SERVICE_INDEX].Base;
Thread->KernelLimit = KeServiceDescriptorTable[SYSTEM_SERVICE_INDEX].Limit;
#else
Thread->ServiceTable = (PVOID)&KeServiceDescriptorTable[0];
#endif
I386下,线程的初始服务表就是KeServiceDescriptorTable。这可能会被改变。后面如果这个线程调用了ShadowTable中的服务,ServiceTable会被转变成ShadowTable。KiFastCallEntry会判断系统调用的服务号,如果发现是GUI服务,则调用PsConvertToGuiThread将线程转换成GUI线程。
PsConvertToGuiThread中如下处理:
#if defined(_AMD64_)
Thread->Tcb.Win32kTable = KeServiceDescriptorTableShadow[WIN32K_SERVICE_INDEX].Base;
Thread->Tcb.Win32kLimit = KeServiceDescriptorTableShadow[WIN32K_SERVICE_INDEX].Limit;
#else
Thread->Tcb.ServiceTable = (PVOID)&KeServiceDescriptorTableShadow[0];
#endif
-
SSDT和SSDT Shadow的初始化
两个SDT的初始化在KiInitSystem函数中:
KeServiceDescriptorTable[0].Base = &KiServiceTable[0];
KeServiceDescriptorTable[0].Count = NULL;
KeServiceDescriptorTable[0].Limit = KiServiceLimit;
KeServiceDescriptorTable[0].Number = KiArgumentTable;
for (Index = 1; Index < NUMBER_SERVICE_TABLES; Index += 1) {
KeServiceDescriptorTable[Index].Limit = 0;
……
RtlCopyMemory(KeServiceDescriptorTableShadow,
KeServiceDescriptorTable,
sizeof(KeServiceDescriptorTable));
ShadowTable的第一项被在这个时候初始化成和KeServiceDescriptorTable一样。而第二项会在后面的初始化过程中通过调用KeAddSystemServiceTable被设置。
-
ShadowTable的设置时机
上面提到了,由于KiInitSystem的初始化,KeServiceDescriptorTableShadow[0]和KeServiceDescriptorTable[0]总是一样的,但是KeServiceDescriptorTableShadow[1]会被设置成win32k的服务。这应该是win32k加载之后调用KeAddSystemServiceTable添加的。
-
AMD64和i386中的区别
AMD64下的实现和i386不同,AMD64下的ShadowTable在_KTHREAD中专门有一个字段,ServiceTable保存KeServiceDescriptorTable,Win32kTable保存KeServiceDescriptorTableShadow[WIN32K_SERVICE_INDEX].Base。详见PsConvertToGuiThread等函数。
-
有什么意义?
从上面记录的这些实现来看,很直接导致的结果就是,系统服务表可以比较容易的替换掉。这种替换有下面这些特点:
-
只会影响指定的线程,对整个系统来说影响小。
-
由于和传统SSDT Hook一样只需要改掉一个指针,比inline hook更加稳定。
-
传统的SSDT Hook被用得太滥,大家都在搞,某些人还敢卸载掉并且恢复Hook之前的指针,很容易造成崩溃。这种方法因为只针对单独线程,被使用得也少,相对来说比传统SSDT Hook和Hook KiFastCallEntry都要安全。
-
对可能转变成GUI线程的线程进行替换就需要特别的处理了。比如构造自己的ShadowTable并Hook掉PsConvertToGuiThread之类的方法。
没有什么隐蔽性,呵呵。干坏事的同学慎用。当然,用的时候再改,用过之后马上改回去,或者配合内存隐藏之类的技术还是不错滴。干好事的同学也可以用,但是不适合对所有系统线程都用上去,因为要枚举所有线程,或者Hook线程创建函数之类比较麻烦,到不如直接去干KiFastCallEntry。用来保护自己线程的服务表安全倒是不错。