【注意!】
此漏洞不具有直接危害性,仅供个人学习研究调试,大牛飘过~~
1. 漏洞概述
在通常情况下,当一个用户退出时,运行在该用户下的进程都将被终止。但是我们在研究过程中发现,能够在受限用户下运行特制的程序,在该受限用户退出的时候,所运行的特制的程序不会被终止,将继续在系统中运行。当管理员用户或者其他高权限用户登录时,该特制的程序能进行枚举窗口、记录键盘信息、进行截屏等一系列操作,因此能够造成权限提升。
本文所分析的操作系统为Windows XP Professional SP3,已经更新截至2010年9月29日微软发布的所有补丁。该漏洞位于CSRSrv.DLL中,版本号为:5.1.2600.5915 (xpsp_sp3_gdr.091211-1412)。
2. 原理
2.1. Windows LPC通信机制
LPC(Local Inter-Process Communication)是一种高效的进程间通信机制,在Windows 2000、XP、2003操作系统中普通使用,能用于用户态程序之间、用户态程序与驱动、驱动与驱动间的通信。使用LPC通信的基本步骤如下:
① 服务端调用NtCreatePort函数创建一个连接端口;
② 服务端通过NtListenPort函数监听所创建的连接端口,获取新的连接请求,必须始终有一个线程在这个端口上等待;
③ 客户端调用NtConnectPort或者NtSecureConnectPort连接到服务端在步骤1中所创建的连接端口;
④ 服务端分析连接请求,通过调用NtAcceptConnectPort和NtCompleteConnectPort确定接收客户端的连接,并创建一个对应的通信端口,客户端和服务端都将通过这个通信端口来通信;
⑤ 服务器启动一个循环,通过NtReplyWaitReceivePort来接收客户端的消息,处理消息后调用NtReplyPort回复客户端;
⑥ 客户端调用NtRequestWaitReplyPort 发送一个新的请求到服务端,等待服务端处理。在此过程中,客户端线程将阻塞,直到收到服务端的回复。
LPC消息结构体如下所示:
typedef struct LpcMessage
{
WORD ActualMessageLength;
WORD TotalMessageLength;
DWORD MessageType;
DWORD ClientProcessId;
DWORD ClientThreadId;
DWORD MessageId;
DWORD SharedSectionSize;
BYTE MessageData[MAX_MESSAGE_DATA];
} LPCMESSAGE, *PLPCMESSAGE;
结构体LPCMESSAGE的成员MessageType指定了LPC消息的类型,有如下几类:
typedef enum _LPC_MSG_TYPE
{
LPC_NEW_MSG,
LPC_REQUEST,
LPC_REPLY,
LPC_DATAGRAM,
LPC_LOST_REPLY,
LPC_PORT_CLOSED,
LPC_CLIENT_DIED,
LPC_EXCEPTION,
LPC_DEBUG_EVENT,
LPC_ERROR_EVENT,
LPC_CONNECTION_REQUEST,
} LPC_MSG_TYPE;
消息类型由系统设置,以下是一些比较重要的类型:
① LPC_REQUEST 当客户端使用 NtRequestWaitReplyPort() 函数发送请求时,服务端接收此消息。
② LPC_REPLY 当服务器回复此请求时,客户从 NtRequestWaitReplyPort() 函数接收此类消息。
③ LPC_PORT_CLOSED 当客户端关闭端口句柄时,服务端接收此消息。
④ LPC_CLIENT_DIED 当客户端退出时,服务端接受此程序。
⑤ LPC_CONNECTION_REQUEST 当客户端调用NtConnectPort或者NtSecureConnectPort连接端口时,相应的服务端接收此消息。
2.2. Csrss
Csrss(客户端/服务器运行时子系统)是 Win32 子系统的用户模式部分,在桌面管理、终端登录、控制台管理、错误报告报告和DOS虚拟机等方面起着重要作用,另外还监控着系统内所有Win32 子系统进程和线程的运行,进程的创建与退出,都需要通知Csrss。
CSRSrv.DLL由Csrss.exe加载,是Csrss.exe的核心模块。在CSRSrv.dll 里面有一个未导出符号叫做 CsrRootProcess,指向一个 CSR_PROCESS 结构,该结构如下所示:
typedef struct _CSR_PROCESS
{
CLIENT_ID ClientId;
LIST_ENTRY ListLink;
LIST_ENTRY ThreadList;
struct _CSR_PROCESS *Parent;
PCSR_NT_SESSION NtSession;
ULONG ExpectedVersion;
HANDLE ClientPort;
ULONG_PTR ClientViewBase;
ULONG_PTR ClientViewBounds;
HANDLE ProcessHandle;
ULONG SequenceNumber;
UChar Flags[4];
ULONG DebugFlags;
CLIENT_ID DebugCid;
ULONG ReferenceCount;
ULONG ProcessGroupId;
ULONG ProcessGroupSequence;
ULONG fVDM;
ULONG ThreadCount;
ULONG PriorityClass;
ULONG Reserved;
ULONG ShutdownLevel;
ULONG ShutdownFlags;
PVOID ServerData[ANYSIZE_ARRAY];
} CSR_PROCESS, *PCSR_PROCESS;
每一个进程都对应着一个CSR_PROCESS结构体,所有进程的CSR_PROCESS结构体通过成员struct _LIST_ENTRY ListLink构成了一个链表,通过CsrRootProcess可以遍历这个链表。当一个进程创建时,Csrss.exe会新建一个CSR_PROCESS结构体,加入到这个链表中;当一个进程退出时,Csrss.exe会将该进程对应的CSR_PROCESS结构体从链表中删除。
在CSRSrv.dll 里面另外还有一个未导出符号CsrThreadHashTable,它是一个有 256 个元素的数组,每一个元素都指向一个 CSR_THREAD 结构,该结构如下所示:
typedef struct _CSR_THREAD
{
union _LARGE_INTEGER CreateTime;
struct _LIST_ENTRY Link;
struct _LIST_ENTRY HashLinks;
struct _CLIENT_ID ClientId;
struct _CSR_PROCESS* Process;
struct _CSR_WAIT_BLOCK* WaitBlock;
void* ThreadHandle;
unsigned long Flags;
unsigned long ReferenceCount;
unsigned long ImpersonateCount;
} CSR_THREAD, *PCSR_THREAD;
每个线程都对应着一个CSR_THREAD结构,成员struct _CSR_PROCESS* Process指向该线程所属进程对应的CSR_PROCESS结构。一个进程所有线程对应的 CSR_THREAD通过成员struct _LIST_ENTRY Link构成了一个链表,该链表表头为CSR_PROCESS.ThreadList
2.3. Win32 子系统进程与CSRSS的通信
Csrss建立了一个名为/Windows/ApiPort的LPC端口,在进程创建的时候,必须连接这个端口通知Csrss。在进程正常运行中,某些函数(例如与控制台相关的一些函数)的底层实现也将通过ApiPort端口与Csrss通信,完成特定的操作。
在Csrss中,对ApiPort端口所接收到的LPC消息的处理,主要是由csrsrv.dll中的CsrApiRequestThread函数完成。CsrApiRequestThread函数调用NtReplyWaitReceivePort接收消息,根据消息的类型执行特定的操作。
LPCMESSAGE结构体ClientProcessId、ClientThreadId成员记录了发送消息进程的PID和TID,CsrApiRequestThread函数调用CsrLocateThreadByClientId函数,由LPCMESSAGE结构体ClientProcessId、ClientThreadId确定发送消息进程的CSR_PROCESS结构体和线程的 CSR_THREAD结构体。
除开csrsrv.dll之外,Csrss还加载了basesrv.dll、winsrv.dll,这三个dll是Csrss的核心模块。Csrsrv.dll提供了一个CsrServerApiDispatchTable函数分发表,basesrv.dll提供了BaseServerApiDispatchTable函数分发表,winsrv.dll提供了UserServerApiDispatchTable、ConsoleServerApiDispatchTable两个函数分发表,这四个分发表中包含大量的函数,如下图所示:
在进程调用四个分发表中的函数时,其消息类型为LPC_REQUEST,并且在LPC消息偏移0x1C处指定了一个DWORD值,高16位指定是哪个分发表,低16位为分发表中函数的索引值,CsrApiRequestThread函数根据这个值调用四个分发表中对应的函数。。
四个分发表序号如下:
CsrServerApiDispatchTable:0
BaseServerApiDispatchTable:1
ConsoleServerApiDispatchTable:2
UserServerApiDispatchTable:3
例如,如果某进程需要调用UserServerApiDispatchTable中的SrvEndTask函数,由于UserServerApiDispatchTable序号为3,SrvEndTask函数在UserServerApiDispatchTable分发表中索引为1,那么该值就是0x30001。
3. 漏洞分析
在上文中提到,Csrss中保存了Win32 子系统进程的信息,这些信息保存在名为CsrRootProcess的链表中,我们在研究中发现,在Windows XP、2003中,如果将进程的CSR_PROCESS从CsrRootProcess链表中摘除,那么在用户退出时,该进程不会被终止。但Csrss.exe以SYSTEM权限运行,对于普通用户,无法直接读写Csrss.exe的内存修改数据。
在正常情况下,当一个进程退出时,它的信息理所当然应该从CsrRootProcess链表中摘除。我们编写了一个普通的应用程序test.exe,它什么也没做,我们跟踪它的退出流程,希望能找到它的信息是如何从CsrRootProcess链表中摘除的。test.exe代码如下所示:
#include <Windows.h>
#include <stdio.h>
void main()
{
}
3.1. ApiPort的连接
在test.exe进程创建的时候,将会连接Csrss建立的ApiPort端口,传递给Csrss的LPC消息类型为LPC_CONNECTION_REQUEST(0xA)。在CsrApiRequestThread函数中处理该类型消息所调用的函数为CsrApiHandleConnectionRequest,它的大概工作流程如下:
① 调用CsrSrvAttachSharedSection函数,在它内部调用NtMapViewOfSection将名为CsrSrvSharedSection的共享内存区映射到进程地址空间中。如果映射成功,则返回0,否则则返回NtMapViewOfSection的出错状态;
② 如果CsrSrvAttachSharedSection返回0,在调用NtAcceptConnectPort时将创建一个通信端口,将该通信端口的句柄填充到CSR_PROCESS结构体的HANDLE ClientPort成员;
③ 在创建通信端口成功的情况下,调用NtCompleteConnectPort完成连接。并将CSR_PROCESS.Flags[1]第6比特置为1,反汇编代码如下所示:
3.2. ExitProcess函数分析
在Windows系统中,当进程结束时,都将调用到ExitProcess函数,ExitProcess函数位于kernel32.dll中(文件版本号为:5.1.2600.5781 (xpsp_sp3_gdr.090321-1317) ),它进一步调用位于地址0x7C81CA6C处的_ExitProcess函数,该函数在地址0x7C81CAC3处调用CsrClientCallServer函数。
CsrClientCallServer函数是ntdll.dll的一个导出函数,内部将调用NtRequestWaitReplyPort函数向ApiPort端口发送LPC消息。在地址0x7C81CAB6处的push 10003h做为CsrClientCallServer函数的第三个参数,将填充到LPC消息偏移0x1C处,根据“2.3 Win32 子系统进程与CSRSS的通信”的分析,0x10003对应了BaseServerApiDispatchTable分发表中的索引值为3的函数,即BaseSrvExitProcess函数。当CsrClientCallServer返回时,test.exe对应的CSR_PROCESS结构体中的引用计数为1(CSR_PROCESS.ReferenceCount = 1),此时还没有从CsrRootProcess链表中摘除。
3.3. BaseSrvExitProcess、CsrDereferenceThread函数分析
BaseSrvExitProcess是在CsrApiRequestThread函数中调用的,BaseSrvExitProcess返回后,进程CSR_PROCESS的引用计数为2。接下来会调用CsrDereferenceThread,使test.exe发送LPC消息的线程CSR_THREAD引用计数减一,为0,此时将调用CsrThreadRefcountZero函数,它将调用CsrRemoveThread、CsrDeallocateProcess、CsrDereferenceProcess三个函数。这三个函数的功能如下:
① CsrRemoveThread函数从CsrThreadHashTable数组中移除线程CSR_THREAD信息;从CsrThreadHashTable数组中移除线程信息,将线程所属进程的线程计数减一,如果进程的线程计数为0,并且CSR_PROCESS.Flags[1]第6比特为0则调用CsrLockedDereferenceProcess函数;
② CsrDeallocateProcess释放线程CSR_THREAD占据的内存空间,该函数也可以用于释放CSR_PROCESS占据的内存空间;
③ CsrDereferenceProcess与CsrLockedDereferenceProcess函数功能相似,都是将进程CSR_PROCESS的引用计数减一,如果为0,则调用CsrProcessRefcountZero函数,并进一步调用CsrRemoveProcess函数从CsrRootProcess链表中摘除进程的CSR_PROCESS结构;
3.4. ApiPort的关闭
接下来将执行0x7C81CACC处的NtTerminateProcess函数,test.exe进程在这里被终止,它在创建的时候连接的ApiPort端口将被系统自动关闭,此时系统将会向Csrss发送一个类型为LPC_PORT_CLOSED(0x5)的消息。在CsrApiRequestThread函数中处理该类型消息有两处代码,一处是从0x75AA4769开始的代码:
这一处的代码在系统关闭ApiPort端口时执行,此时进程CSR_PROCESS的线程计数已为0,但引用计数为1,进程中已经没有了活动线程,CsrLocateThreadByClientId函数返回值为0。接下来将调用CsrLockedDereferenceProcess函数,test.exe对应的CSR_PROCESS的引用计数将减1,变为0,此时将会调用CsrProcessRefcountZero函数,进而调用CsrRemoveProcess函数将test.exe的CSR_PROCESS从CsrRootProcess链表中摘除。
另一处是从地址0x75AA4918开始的代码:
这一处的代码在CsrLocateThreadByClientId函数返回值不为0的时候执行,此时进程CSR_PROCESS的线程计数不为0,即进程中仍然存在有线程。
我们注意到在地址0x75AA4780和地址0x75AA491B处有一句代码:and byte ptr [eax+39h], 0DFh,这条语句的作用是将CSR_PROCESS.Flags[1]第6比特置为0。
3.5. 漏洞利用思路
通过调试,并综合上文的分析,我们可以得出这样的结论:
① 任何用户的进程都可以调用CsrClientCallServer函数,设置第三个参数为0x10003,可以调用BaseSrvExitProcess函数,在BaseSrvExitProcess函数返回后,进程CSR_PROCESS的引用计数为2;
② 紧接着在调用CsrDereferenceThread时,如果CSR_PROCESS.Flags[1]第6比特为0,我们能调用CsrLockedDereferenceProcess函数、CsrDereferenceProcess函数各一次,进程CSR_PROCESS的引用计数将减为0;
③ 进程CSR_PROCESS的引用计数为0时,将调用CsrRemoveProcess函数从CsrRootProcess链表中摘除。
④ 关闭端口时,可以将CSR_PROCESS.Flags[1]第6比特置为0;
我们只需要先执行第④步,然后再激活BaseSrvExitProcess函数,就可以实现进程CSR_PROCESS结构的摘除!
具体的利用思路如下:
① 在进程正常运行的情况下,调用NtSecureConnectPort函数连接ApiPort端口,建立第二次连接;
② 调用CloseHandle函数关闭第二次连接的ApiPort端口句柄,这样就能激活CsrApiRequestThread函数中0x75AA4918开始的代码,执行地址0x75AA491B处的代码:and byte ptr [eax+39h], 0DFh后,进程的CSR_PROCESS.Flags[1]第6比特会置为0;
③ 调用CreateThread新建一个线程,在新建的线程中调用CsrClientCallServer函数,第三个参数设为0x10003,激活BaseSrvExitProcess函数,实现进程CSR_PROCESS结构的摘除;
④ 进入睡眠,等待管理员用户或者其他高权限用户登录,可以进行枚举窗口、记录键盘信息、进行截屏等一系列操作。
在编写攻击代码时,需要注意以下两个问题:
① 在进程创建时必须连接ApiPort端口,共享内存区CsrSrvSharedSection将被映射到进程地址空间。在第二次连接ApiPort端口前,必须调用NtUnmapViewOfSection释放这个内存区,否则在Csrss处理连接请求时,调用NtMapViewOfSection函数会出错,将无法建立连接;
② 在建立第二次ApiPort端口连接后,Csrss会把新建立的通信端口句柄填充到进程CSR_PROCESS结构体的HANDLE ClientPort成员,在回复客户端进程时,使用的是新建立的通信端口句柄。由于我们需要在客户端进程中先关闭新建立的通信端口,然后才调用CsrClientCallServer函数,CsrClientCallServer函数利用进程创建时的ApiPort句柄向Csrss发送消息,Csrss在调用NtReplyPort函数回复消息时,使用的是新建立的通信端口句柄,但这个通信句柄被我们关闭了,所以NtReplyPort函数将返回错误,客户端进程调用CsrClientCallServer函数的线程也将一直阻塞,无法继续运行。所以需要新建一个新线程,在这个新线程中调用CsrClientCallServer函数,激活BaseSrvExitProcess函数,新线程阻塞,但主线程仍然可以继续工作。
4. 攻击代码
/* * 作者:KiDebug * 空间:http://hi.baidu.com/KiDebug/ */
#include <stdio.h> typedef struct LpcMessage typedef struct LpcSectionInfo typedef struct LpcSectionMapInfo typedef struct _UNICODE_STRING typedef VOID (__stdcall *NtRequestWaitReplyPort_)( typedef DWORD (__stdcall *NtSecureConnectPort_)( typedef VOID (__stdcall *NtUnmapViewOfSection_)( typedef VOID (__stdcall *CsrClientCallServer_)( PDWORD u1,DWORD u2,DWORD u3,DWORD u4 ); HMODULE ntdll; NtRequestWaitReplyPort_ NtRequestWaitReplyPort;
HANDLE handle; //ApiPort LPCSECTIONMAPINFO mapInfo; //建立System SID //Unmap Section,以便重连ApiPort //重连ApiPort //关闭ApiPort,去掉0x39处的2 DWORD lpThreadId; Sleep(60000); |
代码验证步骤:
① 编译代码,生成exploit.exe程序;
② 更新截至2010年9月29日微软发布的所有补丁;
③ 建立一普通用户
④ 以普通用户登录,运行exploit.exe程序;
⑤ 退出,以管理员用户登录,稍等片刻即可显示由exploit.exe程序弹出的对话框。可以在任务管理器中查看到由普通用户运行的exploit.exe程序。
5. 修补建议
在CsrApiHandleConnectionRequest函数处理ApiPort的连接请求时,首先判断进程是否建立过连接,可以通过CSR_PROCESS.ClientPort判断。如果该成员为空,说明没有建立过ApiPort连接,接收连接请求;如果该成员不为空,说明已经建立过ApiPort连接,新的连接请求不会被接收。这样就确保进程从创建到退出,只有一个ApiPort的连接,上述攻击代码也将失效。
【背景(可以略过)】
去年的这个时候我只会点C,汇编就知道mov,后来花了两个月时间,按网上大牛们的文章自己敲代码做了个ARK,算是入了点门。在做ARK时,知道CSRSS里面有CsrRootProcess这条链表,可以用来查进程,调试的时候在WinDBG中手动对一个进程脱链,发现用户注销后,这个进程没被自动终结,当时也没在意。7月份分析MS10-059时,意识到了LPC/ALPC的存在。9月份看到MS10-011时,觉得可以深入了解下CsrRootProcess和LPC,于是拿WinDBG调、拿IDA看汇编。看到存在有两处and byte ptr [eax+39h], 0DFh指令时,觉得MS10-011是可以绕过的。从开始接触到MS10-011到写出绕过补丁的代码,花了4天的时间。当从admin登入后,在任务管理器里面看到普通用户下的进程依然存在时,激动了很久,咱也要受微软“公开致谢”了。。。这是我第一次发现漏洞,对我来说意义很大。虽然这个漏洞不具有直接危害,但用来练习下WinDBG调试和汇编倒是不错的。
【鸣谢】
感谢j00ru!
【参考资料】
LPC:
1.对于LPC通信机制,可以访问j00ru的博客:http://j00ru.vexillium.org/,里面讲得很详细;
2.Undocumented Windows NT.chm;
3.WRK
CsrRootProcess:http://forum.sysinternals.com/forum_posts.asp?TID=15457