Windows NT Session 概念的原理与应用浅析 [1] 遍历并获取信息 [草稿]
Posted on 2004-07-19 23:00 Flier Lu 阅读(5781) 评论(7) 编辑 收藏 举报我在上一篇文章《DACL, NULL or not NULL》中曾简要地介绍了 Windows 系统中 Session 的概念,并且通过一个自己编写的小工具 KeSession 列出当前系统 Session 信息。本文中我将就遍历并获取当前系统 Session 信息的方法进行详细分析,并借此机会让大家能够从 Session 的角度对 WinNT 底层帐号和权限管理机制有更深入的了解。因为文章是以随笔方式写的,可能在内容上有所遗漏,希望大家补充和指正。
虽然 Session 的概念非常重要,但因为其过于底层,除了处理用户登陆和安全相关事务的程序员,它对高层开发基本上是透明的。故而这方面的介绍文章和工具实在不多。
而要学习研究 Session,必要的文章和工具肯定是必不可少的,首推就是 www.sysinternals.com 提供的 LoggonSessions。这个小工具能够将当前系统所有正在使用的 Session 以及相关信息列举出来,加上 -p 参数还可以进一步列出 Session 包括的进程列表。大概因为 www.sysinternals.com 的牛人认为程序太简单,就没有给出源码,呵呵,待会我们来看看如何自己实现一个功能完全相同的 KeSession 工具。
此外 Keith Brown 的 Handle Logons in Windows NT and Windows 2000 with Your Own Logon Session Broker 一文,以及文章所附源码工具 cmdasuser 也是必不可少的。此工具能够以指定用户身份,建立新的 Session,并载入用户配置和环境,创建一个完整的 Session 试验环境。
最后就是 Jeffrey Richter 和 Jason Clark 在 Programming Server-Side Applications for Microsoft Windows 一书的第11章 User Context 中,提供的非常强大的令牌管理工具,Token Master,可以查看当前系统任意进程、线程的令牌 (Token)。
LoggonSessions 程序的功能实际上很容易实现,通过调用本机安全授权服务 (LSASS) 提供的 LsaEnumerateLogonSessions 函数枚举当前登陆会话,对每个会话调用 LsaGetLogonSessionData 函数获取进一步的会话信息。
NTSTATUS NTAPI LsaEnumerateLogonSessions(
PULONG LogonSessionCount,
PLUID* LogonSessionList
);
NTSTATUS LsaFreeReturnBuffer(
PVOID Buffer
);
LsaEnumerateLogonSessions 函数使用非常简单,直接返回会话数量和保存每个会话ID的数组。此数组每个元素是一个 LUID 类型,实际上就是一个 64bit 整数,使用完后通过 LSAFreeReturnBuffer 函数释放缓冲区。
NTSTATUS NTAPI LsaGetLogonSessionData(
PLUID LogonId,
PSECURITY_LOGON_SESSION_DATA* ppLogonSessionData
);
对每个会话的 LUID,可以调用 LsaGetLogonSessionData 函数获取进一步的信息。此函数将返回一个保存会话信息的结构,使用完后通过 LSAFreeReturnBuffer 函数释放缓冲区。
typedef struct _SECURITY_LOGON_SESSION_DATA {
ULONG Size;
LUID LogonId;
LSA_UNICODE_STRING UserName;
LSA_UNICODE_STRING LogonDomain;
LSA_UNICODE_STRING AuthenticationPackage;
ULONG LogonType;
ULONG Session;
PSID Sid;
LARGE_INTEGER LogonTime;
LSA_UNICODE_STRING LogonServer;
LSA_UNICODE_STRING DnsDomainName;
LSA_UNICODE_STRING Upn;
} SECURITY_LOGON_SESSION_DATA, *PSECURITY_LOGON_SESSION_DATA;
SECURITY_LOGON_SESSION_DATA 结构保存的会话数据包括:
Size 保存此结构的大小,为兼容以后大小变化提供的可变数据模式
LogonId 保存此 Session 的 LUID 内部编号,用户全局定位,如计算机登陆的 SYSTEM 帐号的 LogonId 为 999(0x3e7)
UserName 和 LogonDomain 保存此 Session 的登陆帐号和域,以 LogonDomain\UserName 组合后就是完整帐号名,如 SKY\FLIER$ 或 FLIER\Administrator
AuthenticationPackage 保存此 Session 的认证方式。一般来说本机都是 NTLM,网络会话是 Negotiate
LogonType 则相对复杂,除了最常见的交互登陆和网络登陆外,还有批处理和服务登陆,以及更少见的代理等方式
typedef enum _SECURITY_LOGON_TYPE {
Interactive = 2, // Interactively logged on (locally or remotely)
Network, // Accessing system via network
Batch, // Started via a batch queue
Service, // Service started by service controller
Proxy, // Proxy logon
Unlock, // Unlock workstation
NetworkCleartext, // Network logon with cleartext credentials
NewCredentials, // Clone caller, new default credentials
RemoteInteractive, // Remote, yet interactive. Terminal server
CachedInteractive // Try cached credentials without hitting the net.
} SECURITY_LOGON_TYPE, *PSECURITY_LOGON_TYPE;
更为完整的登陆类型列表在 LogonUserEx 函数的参数介绍中可以看到。
交互式(Interactive)登陆就是用户通过计算机控制台以物理方式登陆到系统的会话;
网络(Network)登陆则是通过网络邻居等方式远程访问此计算机时的会话;
批处理(Batch)登陆一般在使用批处理队列时使用,如 COM SCM
服务(Service)登陆则适用于后台服务以系统帐号运行,如 System SCM
以上四种登陆类型是最为常见的,而附加的登陆类型适用于特殊情况
代理(Proxy)登陆基本上很少被用到,MSDN里面干脆说此类型不被支持
解锁(Unlock)登陆是当用户锁屏后,GINA与LogonUser函数配合进行解锁时用到
网络明文(NetworkCleartext)登陆允许用户在通过 LogonUserEx 登陆到系统后,用户名和密码被保存以再次登陆到其他服务器
新凭证(NewCredentials)登陆允许用户对本地访问使用克隆的当前令牌,当对外部网络连接使用单独的凭证
远程交互(RemoteInteractive)登陆是终端服务连接到服务器时使用的登陆方式,允许与交互登陆类似的桌面操作,当无需在本地进行
缓存交互(CachedInteractive)登陆则对用户的凭证进行缓存,避免冗余网络验证等等
登陆类型直接决定了此会话的能力以及所受限制,后面有机会再分别解析。
Session 字段保存了一个显式会话编号。此编号一般是用户终端服务器,本地会话和交互登陆会话此编号都为 0,而远程交互登陆则能够获取新的编号。系统通过此编号,在对象管理器中,将 \BaseNamedObjects 映射到 \Sessions\1\ 等等名字空间中,有兴趣的朋友可以进一步参考 NT 对象管理器相关文章。
Sid 字段保存了此会话登陆帐号的 Sid,与前面的登陆名对应;
LogonTime 则是一个非本地时区的 FILETIME 格式的时间,表示会话开始时间;
最后的 LogonServer、DnsDomainName 和 Upn 则一般在使用活动目录时,提供帐号的进一步信息。
在了解这些信息后,很容易就能写出一个遍历登陆会话信息的程序来,MSDN 中也提供了遍历和信息获取的两个例子。例如:
void __fastcall TSessionManager::Refresh(void)
{
LsaData<PLUID> sessions;
ULONG count = 0;
m_sessions.clear();
Win32Check(STATUS_SUCCESS == LsaEnumerateLogonSessions(&count, &sessions));
for(int i=0; i<(int)count; i++)
{
LsaData<PSECURITY_LOGON_SESSION_DATA> data;
Win32Check(STATUS_SUCCESS == LsaGetLogonSessionData(&sessions[i], &data));
if(data) m_sessions.push_front(TKernelSession(data));
}
}
值得注意的是如何将进程和 Session 关联起来。这需要使用 NTDLL::ZwQuerySystemInformation 或 PSAPI::EnumProcesses 遍历系统进程,对每个进程获取其令牌,并使用 GetTokenInformation 函数从令牌中获取统计信息 TokenStatistics,其中 TOKEN_STATISTICS::AuthenticationId 就是此令牌所在 Session 的 ID。
const LUID __fastcall TSystemProcess::GetLogonID(void) const
{
if(FID == 0 || FID == 4) return LUID();
TSystemHandle hProc = ::OpenProcess(MAXIMUM_ALLOWED, FALSE, FID);
if(!hProc.valid()) return LUID();
TSystemHandle hToken;
if(!::OpenProcessToken(hProc, TOKEN_QUERY, &hToken)) return LUID();
TOKEN_STATISTICS stat;
DWORD dwSize = 0;
Win32Check(::GetTokenInformation(hToken, TokenStatistics, &stat, sizeof(stat), &dwSize));
return stat.AuthenticationId;
}
另外一个值得注意的是如何获取 Session 的 SID。每个 Session 在启动时,系统会自动为其注册一个 SID,便于对 WinStation 等对象进行权限控制。因为一个 WinStation 是可以由多个 Session 公用的,但又存在一个相同帐号的多个 Session 可能对此 WinStation 访问权限不同的情况。所以系统为每个 Session 建立一个全局唯一的 SID,以使 WinStation 等用户对象的控制粒度,能够细到针对每个 Session,哪怕这些 Session 使用相同的登陆帐号。
使用 www.sysinternals.com 提供的 Process Explorer 工具,查看 winlogon.exe 进程打开的 WinStation 对象,可以发现除了普通的 Administrator 和 SYSTEM 等帐号外,还会有一个 S-1-5-5-xxx-yyy 的 ACE。这个 S-1-5-5 开头的就是 Session 的 SID,而 xxx-yyy 一般是 Session 的编号。
此 SID 可以通过在此 Session 中的任意进程令牌获取。每个令牌可以有多个组 SID,而其中会有一个 Group SID 的类型属性包括 SE_GROUP_LOGON_ID 标志,这个就是此令牌所属 Session 的 SID,通过此 SID 可以将对权限的控制在 Session 这个粒度进行。可以通过 TokenMaster 工具查看令牌的此 Group SID。
Sid #8
SID: S-1-5-5-0-51085
Use: Logon SID
Name: [Logon SID]
Domain Name:
SID Attribs:
SE_GROUP_MANDATORY
SE_GROUP_ENABLED_BY_DEFAULT
SE_GROUP_ENABLED
SE_GROUP_LOGON_ID
至此,枚举并获取系统 Session 信息的基本功能已经介绍完了,下一节将进一步深入探讨系统内部是如何实现与使用 Session 概念。