CSRSS 基础知识
0. 前言
这是一篇不完整的 csrss 介绍,因为前段时间在看 csrss 相关的漏送,所以对相关的知识做了一下整理,因此你无法只通过这篇文章完全了解 csrss,但是如果这篇文章能够解答你在学习 csrss 时的一些疑惑,那么它的目的就达成了。
1. 历史背景
1.1 微内核(microkernel)的概念
微内核指一个现代化、模块化操作系统中最核心的部分,微内核操作系统有两个原则:
- 模块化、封装性和数据隐藏。即操作系统中有且仅有一部分对一个特定功能具有系统范围内的责任,操作系统中的其他部分,包括应用程序只能通过定义好的接口对使用该功能;
- 操作系统中的大部分功能在用户/应用程序模式下执行,只有微内核本身及硬件相关的小部分代码在内核态下执行。
遵循以上两个原则的操作系统叫做纯微内核系统,仅遵循第一个原则但不遵循第二个原则的操作系统叫做修改后的微内核(modified microkernel)或者宏内核(macrokernel)操作系统。商业性质的操作系统一般不会基于纯微内核进行设计,因为这样的架构计算成本太大。
Windows NT 就是修改后的微内核系统,具有非常好的模块化和封装性,另一方面,从最初版本开始,Windows NT 就在内核态实现了高性能操作系统子系统,这些子系统可以在内核模式下与硬件交互以及相互交互,而不会影响处理器模式以及进程/线程转换的性能。作为内核态子系统实现的有内存管理器、集成缓存管理器、文件系统、对象/安全管理器、网络协议、网络服务器以及所有的线程/进程管理。
然而在 Windows NT 4.0 之前,有一个领域一直采取纯微内核模式,也就是窗口管理器以及图形化子系统,它们用于实现 Win32 API 中的 GUI 部分。在 Windows NT 3.51 及之前的版本中,窗口管理器和 GDI 作为一个单独的用户态进程 Client-Server Subsystem(csrss.exe) 实现。
1.2 Win32 子系统
在 Windows NT 的最初设计阶段,Win32 被设计为等同于 OS/2、POSIX 以及其他计划中的操作系统环境。每个这样的子系统都作为一个独立的环境运行。但是这种设计会导致存在大量重复的系统功能,影响系统规模以及性能。为了避免重复,以及响应市场趋势,最终发布的 Windows NT 3.1 中,Win32 子系统成为一个特殊的特权“程序”,其他所有的子系统以及整个操作系统都依赖该程序。
在 Windows NT 4.0 之前,Win32 子系统包含 5 个模块,存在于不同 DLL 文件中:
- 窗口管理器:处理输入以及屏幕 I/O;
- 图形设备接口(GDI):图形输出设备的绘画库;
- 图形设备驱动(GDD):硬件相关的图形驱动;
- 控制台:提供文字窗口支持;
- 操作系统功能:支持子系统中的所有组件。
将图形功能放在单独的 Win32 服务器进程中会造成很大的内存开销以及大量的线程/进程上下文转换操作,极为影响系统性能。而 Windows NT 是一个基于窗口的操作系统,会有大量的图形和窗口操作,因此自 Windows NT 4.0 开始,设计团队将这部分常用的功能从用户态移入了内核态。
改变之后的系统中,窗口管理器、GDI 和 GDD 功能移入了内核态,即 win32k.sys,控制台、GUI 关闭以及硬错误处理功能仍然保留在用户态的 csrss.exe 中。
也就是说,在我们现在使用的操作系统中,csrss.exe + win32.sys 共同构成了 Win32 这个操作系统环境,而大多数人(包括我自己)都没有接触过 OS/2、 POSIX 等其他的操作系统环境,所以一开始理解 csrss.exe 的功能定位很困难。
2. 通信
Windows 进程或线程启动时,以及任一 Windows 子系统的操作,都会使用 ALPC 与子系统进程 CSRSS 进行通信,所有和 会话管理器 SMSS 的子系统通信也通过 ALPC 进行。
ALPC,即高级(或异步)本地过程调用,是一种用于传递任意大小消息的高速、可扩展且安全的消息传递机制。一般用在一个服务器进程和多个该服务器的客户端进程之间。ALPC 连接可以建立在多个用户态进程间,也可以建立在一个内核态组件以及多个用户态进程之间,也可以建立在两个内核态组件之间。ALPC 使用一个叫做端口对象(port object)的执行对象来维护通信所需状态,该对象可以表示多种 ALPC 端口:
- 服务器连接端口:作为服务器连接请求点的命名端口,客户端可以通过连接该端口连接服务器;
- 服务器通信端口:作为服务器与其中一个客户端通信的未命名端口,服务器为每个活跃的客户端提供一个这样的端口;
- 客户端通信端口:作为客户端与它的服务器通信的未命名端口;
- 未连接通信端口:作为客户端在本地与自己通信的未命名端口,该模型在从 LPC 发展到 ALPC 的过程中已经废弃了,但是由于兼容性问题,仍然为 Legacy LPC 模拟了该模型。
ALPC 遵循类似 BSD 套接字编程的连接和通信模型。服务器先建立一个服务器连接端口(NtAlpcCreatePort
),客户端可以连接该端口(NtAlpcConnectPort
),如果服务器处于监听模式(NtAlpcSendWaitReceivePort
),就会接收到一个连接请求消息,并且可以选择接受该请求(NtAlpcAcceptConnectPort
)。此时会建立客户端和服务器的通信端口,并且每个对应的端点进程会收到其通信端口的句柄,消息会通过该句柄进行传递(NtAlpcSendWaitReceivePort
)。在最简单的情境下,一个单独的服务器会循环调用 NtAlpcSendWaitReceivePort
,通过调用该函数接受连接请求或者处理/回复消息。服务器通过 PORT_HEADER
结构体对消息进行区分,该结构体位于每个消息的最外层,其中包含消息的类型。
一旦连接建立,会使用一个连接信息结构(blob)存储所有不同端口之间的链接。
在上面的描述中,客户端和服务器只能通过消息阻塞的方式轮流循环调用 NtAlpcSendWaitReceivePort
发送请求并等待响应。但实际上 ALPC 也支持异步消息,这样通信双方就不需要进行阻塞,而可以选择执行其他任务并在之后对消息进行检查。ALPC 支持以下消息交换的方式:
- 标准的双缓冲机制。内核通过从源进程复制的方式保留一份消息拷贝,然后转换到目标进程,从内核缓冲区中复制数据。出于兼容性考虑,如果使用的是旧版 LPC,这种方式最多发送 256 字节的消息,而 ALPC 可以为最大 64 KB 的消息分配扩展缓冲区;
- 将消息保存在 ALPC section object 中,客户端和服务器通过映射视图对消息进行处理;
在异步消息的模式下,消息可以被取消,例如请求花费时间过长,或者用户表示要取消之前的操作。在 ALPC 中可以使用 NtAlpcCancelMessage
系统调用取消消息。
一个 ALPC 消息可以位于以下五种由 ALPC 端口对象实现的队列中:
- 主队列:消息已被发出,客户端正在处理;
- 等待队列:消息已被发出,发送方等待响应,但是响应还未发出;
- 大消息队列:消息已被发出,但是发送方缓冲区太小无法容纳该消息,此时发送方有机会分配一个更大的缓冲区并再次请求该消息;
- 取消队列:消息发送到该端口,但是已经被取消;
- 直接队列:发送时附加了直接事件的消息。
3. 具体功能
通过在 Process Hacker 中查看 csrss.exe 加载的模块名称,可以发现 csrss 功能涉及到的 DLL 文件有:basesrv.dll(Windows NT BASE API Server DLL)、winsrv.dll(Multi-User Windows Server DLL)、csrsrv.dll(Client Server Runtime Process)、sxssrv.dll(Windows SxS Server DLL)、sxs.dll。总结这些文件中包含的 ServerApiDispatchTable
以及导出函数:
| basesrv.dll | winsrv.dll | csrsrv.dll | sxssrv.dll | sxs.dll |
----------------------------------------------------------------------------------------------------------------------------------
| BaseSrvCreateProcess | | | | |
| BaseSrvDeadEntry | | | | |
| BaseSrvCheckVDM | | | | |
| BaseSrvUpdateVDMEntry | | | | |
| BaseSrvGetNextVDMCommand | | | | |
| BaseSrvExitVDM | | | | |
| BaseSrvIsFirstVDM | | | | |
| BaseSrvGetVDMExitCode | | | | |
| BaseSrvSetReenterCount | | | | |
| BaseSrvSetProcessShutdownParam | | | | |
| BaseSrvGetProcessShutdownParam | | | | |
| BaseSrvSetVDMCurDirs | | | | |
Dispatch Table| BaseSrvGetVDMCurDirs | SrvEndTask | CsrSrvClientConnect | | |
| BaseSrvBatNotification | | | | |
| BaseSrvRegisterWowExec | | | | |
| BaseSrvSoundSentryNotification | | | | |
| BaseSrvRefreshIniFileMapping | | | | |
| BaseSrvDefineDosDevice | | | | |
| BaseSrvSetTermsrvAppInstallMode| | | | |
| BaseSrvSetTermsrvClientTimeZone| | | | |
| BaseSrvCreateActivationContext | | | | |
| BaseSrvRegisterThread | | | | |
| BaseSrvDeferredCreateProcess | | | | |
| BaseSrvNlsGetUserInfo | | | | |
| BaseSrvNlsUpdateCacheCount | | | | |
| BaseSrvCreateProcess2 | | | | |
| BaseSrvCreateActivationContext2| | | | |
----------------------------------------------------------------------------------------------------------------------------------
| | | CsrAddStaticServerThread | | |
| | | CsrCallServerFromServer | | |
| | | CsrConnectToUser | | |
| | | CsrCreateProcess | | |
| | | CsrCreateRemoteThread | | |
| | | CsrCreateThread | | |
| | | CsrDeferredCreateProcess | | |
| | | CsrDereferenceProcess | | |
| | | CsrDereferenceThread | | |
| | | CsrDestroyProcess | | |
| | | CsrDestroyThread | | |
| | | CsrExecServerThread | | |
| BaseGetProcessCrtlRoutine | | CsrGetProcessLuid | | |
| BaseSetProcessCreateNotify | SrvEndTask | CsrImpersonateClient | | |
| BaseSrvNlsLogon | UserCreateCallbackThread | CsrIsClientSandboxed | | |
Export Func | BaseSrvNlsUpdateRegistryCache | UserHardError | CsrLockProcessByClientId | | |
| BaseSrvRegisterSxS | UserServerDllInitialization| CsrLockThreadByClientId | | |
| ServerDllInitialization | | CsrLockedReferenceProcess | | |
| | | CsrQueryApiPort | | |
| | | CsrReferenceThread | | |
| | | CsrRegisterClientThreadSetup| | |
| | | CsrReplyToMessage | | |
| | | CsrRevertToSelf | | |
| | | CsrServerInitialization | | |
| | | CsrSetBackgroundPriority | | |
| | | CsrSetForegroundPriority | | |
| | | CsrShutdownProcesses | | |
| | | CsrUnhandledExceptionFilter | | |
| | | CsrUnlockProcess | | |
| | | CsrUnlockThread | | |
| | | CsrValidateMessageBuffer | | |
| | | CsrValidateMessageString | | |
----------------------------------------------------------------------------------------------------------------------------------
Windows Vista 及之后的系统中会存在多个 csrss.exe 进程,其中一个 csrss.exe 进程由 Session Manager(smss.exe) 在系统启动时创建,负责处理来自 session-0 模块的请求,并存在于整个系统生命周期,如果该进程崩溃,则整个系统崩溃;其余 csrss.exe 进程在某用户登录时由 smss.exe 创建,负责处理来自该用户会话下应用程序的请求,并存在于该用户登录期间,进程崩溃不会导致系统崩溃。所有 csrss.exe 进程都具有最高用户权限 NT AUTHORITY\SYSTEM。
1: kd> dt csrss!_csr_process
+0x000 ClientId : _CLIENT_ID
+0x010 ListLink : _LIST_ENTRY
+0x020 ThreadList : _LIST_ENTRY
+0x030 NtSession : Ptr64 _CSR_NT_SESSION
+0x038 ClientPort : Ptr64 Void
+0x040 ClientViewBase : Ptr64 Char
+0x048 ClientViewBounds : Ptr64 Char
+0x050 ProcessHandle : Ptr64 Void
+0x058 SequenceNumber : Uint4B
+0x05c Flags : Uint4B
+0x060 DebugFlags : Uint4B
+0x064 ReferenceCount : Int4B
+0x068 ProcessGroupId : Uint4B
+0x06c ProcessGroupSequence : Uint4B
+0x070 LastMessageSequence : Uint4B
+0x074 NumOutstandingMessages : Uint4B
+0x078 ShutdownLevel : Uint4B
+0x07c ShutdownFlags : Uint4B
+0x080 Luid : _LUID
+0x088 ServerDllPerProcessData : [1] Ptr64 Void
4. Side-by-side
上面的 sxssrv.dll 和 sxs.dll 和 Windows 的 Side-by-side 功能有关,这里学习一下 sxs。
4.1 基本概念
在不使用 sxs 功能的一般情况下,一个程序要加载它需要的 DLL 文件,需要按照以下顺序搜索 DLL 文件名:
- 被加载应用程序所在目录
- native Windows 系统目录 (
C:\Windows\System32
) - 16-bit Windows 系统目录 (
C:\Windows\System
) - Windows 目录 (
C:\Windows
) - 应用程序加载时的当前目录
%PATH
环境变量中指定目录
这种加载方式很容易带来安全问题,为此 Windows 也提供了很多种机制进行防护,这里不再列举。
考虑更复杂的情况,由于 DLL 文件中的功能会出于安全、性能等原因进行修改,DLL 文件的版本会更新,但是开发者开发的程序要求可能比较苛刻,只能使用某版本 DLL 文件中提供的功能,在这种情况下,应用程序需要有一种方式指定使用某版本的 DLL 文件,同时 Windows 也需要以某种机制向该程序提供该版本的 DLL 文件。为此,引入了 side-by-side assembly 这一概念。
根据 MSDN, 程序集 (assembly) 是用于命名、绑定、版本控制、部署或配置编程代码块的基本单元。具有常见功能的应用程序可能会运行共享的编程代码块,这些代码块叫做模块或者代码程序集,代码程序集可能被放在 DLL 文件或者 COM 程序集中。用于程序集安全共享的基础结构就叫做 side-by-side 程序集共享。一个典型的 side-by-side assembly 就是一个单独的 DLL 文件加上一个单独的 manifest。Manifest 中包含了用于描述 side-by-side asembly 及其依赖的元数据。
上面的解释在第一次接触的时候其实比较抽象,我在学习的时候直接把 assembly 当作是 DLL 文件的另一种叫做,都是一个某一通用功能相关代码的集合。
sxs 程序集有私人的,也有共享的,Windows 的共享程序集保存在 C:\Windows\Winsxs
目录下。如果应用程序想要使用 sxs 功能,需要提供一个 manifest 文件,里面包含了 DLL 文件的版本信息,这样系统在加载 DLL 文件的时候,就会根据 manifest 中的内容,首先在 Winsxs 目录下进行搜索。
4.2 manifest (清单)
程序集清单实际上是一个 XML 文件
4.3 Activation context (激活上下文)
manifest 保存在二进制文件的资源段中,系统加载 manifest 内容的时候,会将这些依赖信息打包成一个叫做 activation context 的搜索结构中。
系统会在引导以及进程启动的时候对应创建系统和进程级别的默认激活上下文,除此之外,每个线程也会有一个关联的激活上下文栈,栈顶部的激活上下文结构是当前活跃的。每个线程的激活上下文栈可以通过函数 ActivateActCtx
和 DeactivateActCtx
显示管理,也可以通过系统在某个时间点进行隐式管理,例如当一个带有依赖信息的二进制文件的 DLL 主例程被调用的时候。
系统在解析 DLL 文件路径的时候,会首先查找当前线程激活上下文栈顶部的激活上下文内容,如果没找到,再依次查找进程和系统的激活上下文。
5. 启动流程
启动参数:
%SystemRoot%\system32\csrss.exe ObjectDirectory=\Windows SharedSection=1024,20480,768 Windows=On SubSystemType=Windows ServerDll=basesrv,1 ServerDll=winsrv:UserServerDllInitialization,3 ServerDll=sxssrv,4 ProfileControl=Off MaxRequestThreads=16
IDA 查看 csrss.exe,它会调用位于 csrsrv.dll 中的 CsrServerInitialization
函数,该函数会调用 CsrParseServerCommandLine
,找到其中的 !_stricmp(name, "ServerDLL")
,可以看到当参数为 ServerDLL=
时,函数会根据冒号以及逗号对参数值做分割,并调用 result = CsrLoadServerDll(value_idx, func_name, num);
加载对应 DLL 文件:
...
else if ( !_stricmp(name, "ServerDLL") )
{
cur_char_1 = *value_idx;
func_name = 0i64;
idx_1 = value_idx;
v8 = 0xC000000D;
if ( !*value_idx )
return v8;
while ( 1 )
{
if ( cur_char_1 == ':' )
{
cur_char_1 = ':';
if ( !func_name )
{
*idx_1++ = 0;
func_name = idx_1;
cur_char_1 = *idx_1;
}
}
++idx_1;
if ( cur_char_1 == ',' )
break;
cur_char_1 = *idx_1;
if ( !*idx_1 )
return v8;
}
v8 = RtlCharToInteger(idx_1, 0xAu, &num);
if ( v8 < 0 )
return v8;
*(idx_1 - 1) = 0;
result = CsrLoadServerDll(value_idx, func_name, num);
v8 = result;
if ( result < 0 )
return result;
}
...
CsrLoadServerDll
构造了一个新的结构体,这里暂且叫做 ServerDllStruct
,并以此为参数调用了对应 DLL 文件中的 func_name
函数,如果 func_name
没有指定,就调用 ServerDllInitialization
函数:
v7 = dll_name_2.MaximumLength + 0x78;
heap = RtlAllocateHeap(CsrHeap, CsrBaseTag + 0x40000, v7);
server_dll_struct = heap;
Parameters[0] = heap;
if ( heap )
{
memset_0(heap, 0, v7);
server_dll_struct->pCsrSrvSharedSectionHeap = CsrSrvSharedSectionHeap;
server_dll_struct->NameLength = dll_name_2.Length;
server_dll_struct->NameMaximumLength = dll_name_2.MaximumLength;
server_dll_struct->pName = &server_dll_struct[1];
if ( dll_name_2.Length )
strncpy_s(&server_dll_struct[1], server_dll_struct->NameMaximumLength, dll_name_2.Buffer, dll_name_2.Length);
server_dll_struct->value = num;
server_dll_struct->pAddress = dll_addr;
if ( dll_addr )
{
if ( func_name )
func_name_1 = func_name;
else
func_name_1 = "ServerDllInitialization";
RtlInitString(&func_name_2, func_name_1);
status_1 = LdrGetProcedureAddress(dll_addr, &func_name_2, 0, &func_addr);
if ( status_1 < 0 )
{
v15 = dll_addr;
if ( !dll_addr )
goto LABEL_35;
goto LABEL_34;
}
v12 = (func_addr)(server_dll_struct);
}
else
{
func_addr = CsrServerDllInitialization;
v12 = (CsrServerDllInitialization)(server_dll_struct);
}
status_1 = v12;
if ( v12 >= 0 )
{
LODWORD(CsrTotalPerProcessDataLength) = ((server_dll_struct->num3 + 7) & 0xFFFFFFF8)
+ CsrTotalPerProcessDataLength;
v13 = server_dll_struct->value;
CsrLoadedServerDll[v13] = server_dll_struct;
v14 = server_dll_struct->pCsrSrvSharedSectionHeap;
if ( v14 != CsrSrvSharedSectionHeap )
*(v13 * 8 + CsrSrvSharedStaticServerData) = v14;
return status_1;
}
...
}
根据以上代码判断,在 csrss.exe 的启动参数中,ServerDLL
数值中各字段意义为:ServerDll=[Dll文件名]:[函数名],[对应ServerDllStruct索引值]