Windows服务调用机制
一、序言
Windows系统服务调用是存在于Windows系统中的一个关键接口,常常称作System Call ,Sysem Service Call 或 System Service Dispatching等,在此我们就权且称之为Windows系统服务调用,它提供了操作系统环境由用户态切换到内核态的功能。虽然在国外关于Windows系统服务调用的讨论比较多,但却很少看到比较详细的中文资料,希望本文能够为和作者一样对Windows底层感兴趣并且是刚刚接触的朋友提供一些帮助。文章中将以一个内核级的进程监视/隐藏工具T-ProcMon为例来详细讨论Windows系统服务调用的相关技术细节。另需注意本文讨论的技术仅适用于基于Windows NT内核的操作系统,并以Windows 2000为例。
二、Windows 2000系统体系结构
微软Windows 2000是一个主要面向网络服务器的操作系统,因此它和以前大家比较熟悉的Windows 9x有很大的区别。但是对于讨论一个因商业策略而出现的个人桌面操作系统的确没有太大的价值。所以我们将主要介绍一些关于NT系统内部结构的细节。Windows 2000在实现其自身目标的过程中,我们有必要讲解一些它的特性。
1. 可扩展性(Extensibility)
Windows 2000操作系统是一个面向未来的系统,所以它非常注重自身的扩展性,因为在将来可能有许多市场等方面的原因导致我们必须添加或删除目前操作系统的一些组件,这就必须要求操作系统有较强的可扩展性。为了满足扩充/删除的各种需求,Windows 2000提供了一个重要的设计思想就是子系统(Subsystem)。我们可以将一些需要扩展的操作系统功能作为一个子系统添加到Windows 2000内,就像OS/2,POSIX等一样。当然还有一个特性就是,我们可以通过为系统服务调用添加钩子来修改系统的各项行为,这就为我们提供了一个了解系统内部并扩展系统功能的机会。
2. 可靠性和健壮性(Reliability and Robust)
一个系统存在的最基本的要求就是它的稳定性,没有稳定的环境就做不出任何满意的产品。为了满足这项要求,Windows 2000提出了基于对象的访问控制权限的措施。现代的大多数微处理器都支持两种模式:用户模式(User/Normal)和内核模式(Kernel/Privileged)。操作系统组件和关键的系统组件处于内核模式,而一般用户模式的程序只能访问私有地址空间和执行非特权等级的指令。如果用户要调用一些内核组件的功能,就得通过系统服务调用来实现。
3. 兼容性(Compatibility)
Intel和Microsoft能够做到今天的一个很重要的因素就是他们支持对过去存在系统的兼容。这一点非常的关键,没有人愿意三天两头的更换系统,当然也很少有人有这个经济实力。Windows 2000为了实现对其他系统的兼容,如Dos,16位Windows等,出现了环境子系统。而在Windows 2000中必须存在的环境子系统是Win32,它是其他子系统的基础,其他子系统都是一些表面的接口,而实际上是调用了Win32提供的接口,而Win32最终也是通过系统服务调用来与内核联系的。虽然操作系统为各种环境子系统提供了不同的动态链接库,而且其中的API函数名称往往也是不同的,不过这个函数的最终都是通过相同的系统服务调用进入内核来实现的。
4. 易维护性(Maintainability)
作为一个大型的项目,Windows 2000的维护也成为了一个大型的工程。而如此巨大的项目没有很好的维护性是无法发展下去的。为此,Windows 2000使用了分层的思想,这也是一种操作系统体系结构模型。其中,系统服务调用将系统的内核模式代码和用户模式代码隔离开来,子系统使用系统服务调用为用户提供应用程序编程接口(API),而系统服务调用向下调用执行体实现各项功能。
就像在上文我们提到的操作系统存在的两种模式,这是建立在处理器的基础之上的。按理说,一般处理器可以提供从Ring0到Ring3的四种处理器模式,但是它们必须提供至少两种,那就是Ring0和Ring3。而一些特殊处理器指令只能在内核模式执行,而一些地址空间必须在内核模式才可以被访问。Windows 2000就利用了这个特点,将操作系统和其他关键组件保护起来,只有在内核模式才可以访问执行,而一般的用户程序就只能在用户态执行咯,这样就可以避免一些用户程序对操作系统代码的破坏,也就是大家看到的Windows 2000明显比Windows 9x稳定得多的主要原因。下面我们给出了Windows 2000的体系结构简图:
系统支持进程,服务进程,应用程序,环境子系统
应用程序编程接口
基于
NTDLL.dll的本地系统服务 (用户模式)
-----------------------------------------
系统服务调用 (内核模式)
执行体
系统内核,设备驱动程序
硬件抽象层
三、Windows 2000本机系统服务(Native API)
Windows 2000本机系统服务又称为Windows本机应用程序编程接口,它是由执行体(Executive)为用户模式和内核模式的程序提供的系统服务集。它包含两种类型的函数:Windows 执行系统服务的系统服务调度占位程序;子系统,子系统DLL和其他本机映像使用的内部支持函数。
从用户模式调用本机系统服务是通过
NTDLL.dll来实现的。表面上,Win32函数为编程人员提供了很多接口来实现我们想要的功能,但是这些Win32函数只不过是本机应用程序编程接口的一个包装器而已,它们将本机API包装起来,调用本机系统服务来实现用户期望的功能。也就是说
NTDLL.dll只是系统服务调用接口在用户模式下的一个外壳。关于用户模式下的Windows本机系统服务的相关信息,请参见我以前写的一篇文章《探测Windows2K/XP/2003本机系统信息》。
我们再谈谈从内核模式调用系统服务吧,这时就不是由
NTDLL.dll导出系统服务调用的函数接口了,而是由ntoskrnl.exe来实现的,它会提供两种形式的函数:ZwXxx和NtXxx,在此我们就不多说了。大家应该注意到了,在上面我们介绍的Windows 2000系统体系结构中的系统服务调用,执行体和内核都是存在于ntoskrnl.exe(在多处理器中为ntkrnlmp.exe)之中,并且是分层的。
四、Windows 2000系统服务调用机制
Windows 2000的陷阱调度(Trap Dispatching)机制包括了:中断(Interrupt),延迟过程调用(Deferred Procedure Call),异步过程调用(Asynchronous Procedure Call),异常调度(Exception Dispatching)和系统服务调用。在Intel x86的Windows 2000系统中,处理器执行int 0x2e指令来激活Windows系统服务调用;在Intel x86的Windows XP系统中处理器却是通过执行sysenter指令使系统陷入系统服务调用程序中;而在AMD的Windows XP中使用了指令syscall来实现同样的功能。我们暂时使用x86的Windows 2000为例来演示。我们先给出一个系统服务调用的模型:
mov eax, ServiceId
lea edx, ParameterTable
int 2eh
ret ParamTableBytes
其中,ServiceId清楚的说明了传递给系统服务调用的系统服务号,内核使用这个标识符来查找系统服务调度表(System Service Dispath Table)中的对应系统服务信息。在系统服务调度表中的每一项包含了一个指向系统服务程序的指针,我们Hook时就是修改这个指针使其指向我们自定义的系统服务的地址。ParameterTable是传递的参数,系统服务调用程序KiSystemService必须严格校验传递的每一个参数,并将其参数从线程的用户堆栈中复制到系统的核心堆栈以备使用。由于执行int指令会导致陷阱发生,所以在Windows 2000内的中断描述表(IDT = Interrupt Descriptor Table)中的0x2e项指向了系统服务调用程序。最后返回的ParamTableBytes是关于参数个数的信息。
现在我们已经看得出来了,系统服务调用只是一个接口,它提供了将用户模式下的请求转发到Windows 2000内核的功能,并引发处理器模式的切换。在用户看来,系统服务调用接口就是Windows内核组件功能实现对外的一个界面。系统服务调用接口定义了Windows内核提供的大量服务。
五、Windows 2000系统服务调用类型
在Windows 2000中默认存在两个系统服务调度表,它们对应了两类不同的系统服务。这两个系统服务调度表分别是:KeServiceDescriptorTable和KeServiceDescriptorTableShadow。
Windows 2000执行程序服务对应于
NTDLL.dll为我们提供的系统服务调用。子系统通过调用
NTDLL.dll中的函数接口来实现它们需要的功能。系统服务调度表KeServiceDescriptorTable定义了在ntoskrln.exe中实现的系统服务,通常在kernel32.dll/advapi32.dll中提供的函数接口均是调用的这个系统服务调度表中。
同时存在于Windows 2000操作系统中还有在Win32k.sys中实现的相关Win32USER和GDI函数,它们是属于另一类系统服务调用。与之对应的系统服务调度表为KeServiceDescriptorTableShadow,它提供了内核模式实现的USER和GDI服务。函数KeAddSystemServiceTable允许Win32.sys和其他设备驱动程序添加系统服务表。除了Win32k.sys服务表外,使用KeAddSystemServiceTable添加的服务表会被同时复制到KeServiceDescriptorTable和KeServiceDescriptorTableShadow中去。
我们可以看出这两类函数实现在服务调度上的区别:Win32内核API经过Kernel32.dll/advapi32.dll进入
NTDLL.dll后使用int 0x2e中断进入内核,最后在Ntoskrnl.exe中实现了真正的函数调用;Win32 USER/GDI API直接通过User32.dll/Gdi32.dll进入了内核,最后却是在Win32k.sys中实现了真正的函数调用。在此我们只讨论与
NTDLL.dll相关的函数,也就是我们例子中处理的函数。
六、Hook系统服务调用的
作用
钩子(Hooking)是一种拦截/监听可执行代码在执行过程中相关信息的一种通用机制。它使我们了解系统内部结构,运作机制甚至修改系统行为的想法成为可能。在一个像M$存在的世界里,Windows的很多内部信息我们都是无法得知的,因为Windows不是Linux,但这并不意味着我们就此放弃!只要开动你的大脑,很多事情都会变成可能。
1. 事件追踪
你想知道Windows在什么时候会打开一个进程吗?你想知道Windows任务管理器中进程相关信息的获取调用了哪些函数吗?我们都可以使用Hook技术来实现这些你想要的信息。我们可以追踪ZwOpenProcess的执行情况,我们同样也可以追踪ZwQueryInformationProcess的执行情况,包括传递的参数和返回的结果。大家可以看到本文相关的程序T-ProcMon就是一个进程监视工具,它会追踪系统中与进程相关的各种信息。在某些我们期望的事件发生时,程序会通知用户发生了什么,这也是我们期望看到的结果。
2. 修改系统行为
操作系统为我们提供了一些通用的功能,如查询系统进程信息ZwQuerySystemInformation(SystemInformationClass == 5),它会返回系统中当前所有进程/线程的相关信息。如果我们希望隐藏一些特殊的进程那该怎么办呢?那就是修改系统服务调用,也就是修改ZwQuerySystemInformation的行为。在查询系统进程时,系统会返回一个进程信息队列,每个单元对应一个进程,如果我们想隐藏其中的某个进程,只须修改队列中的某些数据,然后返回给上层函数,它们就不会发现Xxx.exe进程存在于系统之中了。
3. 研究系统内部机制
微软提供的Windows操作系统是一个“封闭”的系统,很多内部资料都没有公布,我们可以通过Hook技术来探测系统的内部数据结构和运行机制,学习操作系统内部的操作方式。基于Hook的Windows内核黑客技术(Kernel Hacking)是非常之流行和有效,在我们探测系统的一些未公开,未文档化的技术细节时我们都可以使用钩子技术。
4. 其他
其他如我们要调试一个非常麻烦的程序时就可以使用Hook技术,这样就可以更好的帮助我们追踪系统的行动,更好的了解程序内部的执行过程。同样,为了获取系统的一些特殊性能数据,我们也可以在特定的情况下使用Hook技术。
七、Hook系统服务调用的实现
在此我们讨论Hook的对象仅限于由Windows 2000的ntoskrnl.exe提供的系统服务调用。Windows 2000系统服务调用为内核模式的代码,所以我们必须书写设备驱动程序来访问系统服务调度表。如果你对Windows 2000下基本设备驱动程序的书写不太清楚,请查阅相关的书籍,此处不做介绍。我们先回顾一下Win32内核API的实现流程。
Windows 2000系统服务调用向用户提供了经过包装的用户模式的函数接口(由
NTDLL.dll提供)。当Kernel32.dll/Advapi32.dll中的函数执行时,先调用
NTDLL.dll中对应的相关接口,经过参数检查后使用int 0x2e指令进入内核模式,传递相关的服务号和参数列表。在ntoskrnl.exe中维护着两个表系统服务调度表(System Service Dispath Table)和系统服务参数表(System Service Parameter Table),其中int 0x2e指令就是通过服务号在SSDT中查询相关系统服务程序指针的。现在我们已经清楚了每个系统服务调用都对应一个服务号,同时也对应一个服务程序的地址!如果我们修改SSDT中的某个系统服务程序的入口地址为指向我们自定义的函数地址,在执行完我们的代码后再执行原始系统服务地址处的代码,这不就实现了对系统服务调用的了Hook吗?
对我们来说,定位系统服务调度表是实现Hook的关键。在Windows 2000中有一个未公开的由ntoskrnl.exe导出的单元:KeServiceDescriptorTable,我们可以通过它来完成对SSDT的访问与修改。KeServiceDescriptorTable对应于一个数据结构,定义如下:
typedef struct SystemServiceDescriptorTable
{
UINT *ServiceTableBase;
UINT *ServiceCounterTableBase;
UINT NumberOfService;
UCHAR *ParameterTableBase;
}SystemServiceDescriptorTable,*PSystemServiceDescriptorTable;
其中ServiceTableBase指向系统服务程序的地址,ParameterTableBase则指向SSPT中的参数地址,它们都包含了NumberOfService这么多个单元。我们只要由KeServiceDescriptorTable找到了我们关注的系统服务调用程序,就可以修改它的ServiceTableBase参数来实现对相关系统服务调用的Hook了!
八、T-ProcMon-1.0 关键源码分析
1. 基于CUI的用户模式控制程序
由于在此之前我已经对Win32的系统服务进行了详细的介绍,现在就不做多说了,大家如果有什么疑问请参阅我以前写的文章,你可以到FZ5FZ的主页阅读相关文章,或下载相关源代码。
2. 基于设备驱动的Hook代码
定义在用户模式与内核模式程序间通信的命令代码:
#define PROCMON_MONITOR (ULONG) CTL_CODE(FILE_DEVICE_PROCMON,0x01,METHOD_BUFFERED,FILE_ANY_ACCESS)
#define PROCMON_HIDDEN (ULONG) CTL_CODE(FILE_DEVICE_PROCMON,0x02,METHOD_BUFFERED,FILE_ANY_ACCESS)
#define PROCMON_HOOK (ULONG) CTL_CODE(FILE_DEVICE_PROCMON,0x03,METHOD_BUFFERED,FILE_ANY_ACCESS)
#define PROCMON_UNHOOK (ULONG) CTL_CODE(FILE_DEVICE_PROCMON,0x04,METHOD_BUFFERED,FILE_ANY_ACCESS)
将KeServiceDescriptorTable与相关数据结构联系起来,定义系统调用:
__declspec(dllimport) ServiceDescriptorTableEntry KeServiceDescriptorTable;
#define SYSCALL(_function) KeServiceDescriptorTable.ServiceTableBase[*(PULONG)((PUCHAR)_function+1)]
定义各种未公开的函数,如ZwQuerySystemInformation:
typedef
NTSTATUS
(*ZWQUERYSYSTEMINFORMATION)(
IN ULONG SystemInformationClass,
IN OUT PVOID SystemInformation,
IN ULONG SystemInformaitonLength,
OUT PULONG ReturnLength OPTIONAL);
修改系统服务调用,保存原始的入口地址,修改为我们自定义的程序入口地址,如ZwQuerySystemInformation:
OldZwQuerySystemInformation = (ZWQUERYSYSTEMINFORMATION)(SYSCALL(ZwQuerySystemInformation));
_asm cli
(ZWQUERYSYSTEMINFORMATION)(SYSCALL(ZwQuerySystemInformation)) = NewZwQuerySystemInformation;
_asm sti
解除钩子,还原系统服务调用:
_asm cli
(ZWQUERYSYSTEMINFORMATION)(SYSCALL(ZwQuerySystemInformation)) = OldZwQuerySystemInformation;
_asm sti
调用原始的系统服务程序代码:
NtStatus = (OldZwQuerySystemInformation)
(SystemInformationClass,
SystemInformation,
SystemInformaitonLength,
ReturnLength);
隐藏进程,既是修改系统返回的数据队列中相关项的偏移量使起指向需要隐藏进程的下一个单元,也就是说跳过我们需要隐藏进程的单元:
if(RtlCompareUnicodeString(&pCurrentNK->Name,&ProcCur->ProcessName,TRUE) == 0)
{
RtlUnicodeStringToAnsiString(&ProcNameA,&pCurrentNK->Name,TRUE);
DbgPrint("Hidden Process Name: %s/n",ProcNameA.Buffer);
if(ProcPre != NULL)
{
if(ProcCur->NextEntryDelta != 0)
{
ProcPre->NextEntryDelta += ProcCur->NextEntryDelta;
}
else
{
ProcPre->NextEntryDelta = 0;
}
}
else
{
if(ProcCur->NextEntryDelta != 0)
{
SystemInformation = (PSYSTEM_PROCESSES)((PTSTR)ProcCur + ProcCur->NextEntryDelta);
}
else
{
SystemInformation = NULL;
}
}
break;
}