Windows 窗口站的基本原理
在Windows 下面有一个比较特殊的概念,就是Windows Station;对于会话,桌面这些东西还算比较好理解(对于会话的基本概念可以参考Windows 会话内存隔离原理),但是什么是Windows Station呢?本文来讨论一下Windows Station的基本知识。
注意:下面的代码来自REACTOS的代码分析,Windows的真实代码可能和其有出入,但是实现原理基本一致,我们探讨原理使用的是REACTOS源码(因此下面的源码只是说明原理,并不代表Windows的真实实现)。
1. 基本概念
Window station 主要的一个作用是安全特性,它被设计成为一个限制操作系统中窗口环境的沙箱。我们知道,对于一个文件或者内核对象,都通过一个安全描述符的东西来控制棋访问权限,Windows通过对访问控制列表(ACL)的控制,使得每个用户(TOKEN)具有不同的访问权限。
但是,如果将ACL这一套放置到每个GUI窗口对象中,并且对每一个窗口消息都进行相关性的权限检查的话,将会导致性能大大的下降。那么Windows就引入了Windows Station,Windows让窗口相互通知而不需要执行任何的安全检查,当然我们是在一个Windows Station的私有环境中才允许这样做。
Windows Station有如下的特性:
- 每个窗口站包含一个剪贴板,一个原子表,一个或者多个桌面对象。
- 每个窗口站对象都是一个安全对象。
- 当一个窗口站创建时,它被关联到创建它的进程并且赋给当前的会话。
- 交互的窗口站,
WinSta0
是唯一的一个可以显示用户界面和接收用户输入的窗口站。它被赋给交互用户的登录会话,包括键盘,鼠标,显示设备。其他所有的窗口站都是非交互的,也就说它们不能显示用户界面,也不能接收用户输入。 - windows station基本上可以被描述为包含桌面和进程的安全边界。因此,一个session可以包含多个windows station,而每个windows station又可拥有多个桌面。
2. Station的一个例子
这里我们用一个例子展示session0的基本组成(如下图),其中有个名为Bob的用户登入。正如你所看到的,Winsta0包含用户控制台中的所有进程还有任何被标记为可交互(Interactive)的任何服务。
本例中,Winsta0包括winlogon.exe,explorer.exe和其他需要与用户交互的服务。
名为service-0x0-3e7$的Windows station 拥有在Local system帐号下且不与用户交互的所有服务。本例中service.exe正是这样的服务。
SQL进程被载入到其自身的windows station并且使用自己的证书认证,所以它不属于其他两个windows station。
因此,我们可以将上图总结如下:
- 整个图展示的是session0.
- 在Bob帐号下的所有进程都载入到Winsta0。
- 在local system帐号下可交互进程载入到winsta0。
- 在local system帐号下不可交互进程载入到Service-0x0-3e7$ windows station 。
- 在私有证书下启动的进程载入到其自己的windows station(像SQL)。
3. WINSTATION_OBJECT的基本结构
typedef struct _WINSTATION_OBJECT
{
DWORD dwSessionId;
LIST_ENTRY DesktopListHead;
PRTL_ATOM_TABLE AtomTable;
HANDLE ShellWindow;
HANDLE ShellListView;
ULONG Flags;
struct _DESKTOP* ActiveDesktop;
PTHREADINFO ptiClipLock;
PTHREADINFO ptiDrawingClipboard;
PWND spwndClipOpen;
PWND spwndClipViewer;
PWND spwndClipOwner;
PCLIP pClipBase; // Not a clip object.
DWORD cNumClipFormats;
INT iClipSerialNumber;
INT iClipSequenceNumber;
INT fClipboardChanged : 1;
INT fInDelayedRendering : 1;
PWND spwndClipboardListener;
LUID luidEndSession;
LUID luidUser;
PVOID psidUser;
} WINSTATION_OBJECT, *PWINSTATION_OBJECT;
在这里我们可以看到如下信息:
dwSessionId
: 窗口站所处的会话。AtomTable
: 原子表。DesktopListHead;
窗口站下面的桌面通过这个结构体连接。ActiveDesktop;
当前活动的桌面。
在Windows下面真实的窗口站的类型为tagWINDOWSTATION
,与上面的声明不太一致,但是并不影响整个流程的理解。
3.1 原子表
这里有一个概念就是原子表,那么什么是原子表呢?
Win32系统中,为了实现信息共享,系统维护了一张全局原子表,用于保存字符串与之对应的标志符的组合。应用程序在原子表中可以放置字符串,并接收一个16位整数值(叫做原子,即atom),它可以用来提取该字符串,放在原子表中的字符串叫做原子的名字。
系统提供许多原子表,每个原子表用于不同的目的;例如,动态数据交换(DDE)应用程序使用全局原子表(global atom table)与其他应用程序共享项目名称和主题名称字符串,不用传递实际的字符串,一个DDE应用程序传递全局原子给它的父进程,父进程使用原子提取原子表中的字符串。
关于对原子的操作,有一组专门的API函数:
GlobalAddAtom
: 在表中增加全局原子。GlobalDeleteAtom
: 在表中删除全局原子。GlobalFindAtom
: 在表中搜索全局原子。GlobalGetAtomName
: 从表中获取全局原子。
其中原子表RTL_ATOM_TABLE
的结构如下:
typedef struct _RTL_ATOM_TABLE
{
ULONG Signature;
union
{
#ifdef NTOS_MODE_USER
RTL_CRITICAL_SECTION CriticalSection;
#else
FAST_MUTEX FastMutex;
#endif
};
union
{
#ifdef NTOS_MODE_USER
RTL_HANDLE_TABLE RtlHandleTable;
#else
PHANDLE_TABLE ExHandleTable;
#endif
};
ULONG NumberOfBuckets;
PRTL_ATOM_TABLE_ENTRY Buckets[1];
} RTL_ATOM_TABLE, *PRTL_ATOM_TABLE;
typedef struct _RTL_ATOM_TABLE_ENTRY
{
struct _RTL_ATOM_TABLE_ENTRY *HashLink;
USHORT HandleIndex;
USHORT Atom;
USHORT ReferenceCount;
UCHAR Flags;
UCHAR NameLength;
WCHAR Name[1];
} RTL_ATOM_TABLE_ENTRY, *PRTL_ATOM_TABLE_ENTRY;
从这里我们可以看到原子表大概类似一个哈希表或者一个句柄表之类的数据结构来管理(其实就是可ATOM到字符串的转换)。
4. WinSta0
我们经常说,只有WinSta0
这个窗口站,才能外接交互式输入设备,这个是为什么呢?我们知道交互式输入线程是RawInputThread
,我们看一下这个线程的相关实现流程:
VOID NTAPI
RawInputThreadMain(VOID)
{
//...
Status = ObOpenObjectByPointer(InputWindowStation,
0,
NULL,
MAXIMUM_ALLOWED,
ExWindowStationObjectType,
UserMode,
(PHANDLE)&hWinSta);
if (NT_SUCCESS(Status))
{
UserSetProcessWindowStation(hWinSta);
}
else
{
ASSERT(FALSE);
}
//...
}
InputWindowStation
这个就是我们的WinSta0
窗口站,输入线程通过UserSetProcessWindowStation
将进程进行绑定窗口站,因此所有交互式输入设备被限制在改窗口站中生效了。
5. 沙箱隔离
Windows Station是怎么隔离相关操作的呢?我们有一个操作可以看一下,这个操作就是剪切板(为什么使用剪切板而不是窗口消息呢?因为窗口消息在桌面就被隔离了)。
HANDLE NTAPI
UserSetClipboardData(UINT fmt, HANDLE hData, PSETCLIPBDATA scd)
{
//...
IntAddFormatedData(pWinStaObj, fmt, hData, scd->fGlobalHandle, FALSE);
//...
}
static PCLIP NTAPI
IntAddFormatedData(PWINSTATION_OBJECT pWinStaObj, UINT fmt, HANDLE hData, BOOLEAN fGlobalHandle, BOOL bEnd)
{
PCLIP pElement = NULL;
//...
pElement = IntGetFormatElement(pWinStaObj, fmt);
//...
}
static PCLIP FASTCALL
IntGetFormatElement(PWINSTATION_OBJECT pWinStaObj, UINT fmt)
{
DWORD i;
for (i = 0; i < pWinStaObj->cNumClipFormats; ++i)
{
if (pWinStaObj->pClipBase[i].fmt == fmt)
return &pWinStaObj->pClipBase[i];
}
return NULL;
}
从这里可以发现,剪切板是从窗口站中取出的相关结构体,那么剪切板也就会达到相应隔离的效果。
在每一个进程中存在一个窗口站的管理结构,表示进程属于哪个窗口站,通过如下方法获取进程所属的窗口站:
HWINSTA FASTCALL
UserGetProcessWindowStation(VOID)
{
PPROCESSINFO ppi = PsGetCurrentProcessWin32Process();
return ppi->hwinsta;
}