上周 hBifTs 在折腾他的文件映射封装类的时候,碰到了不能在 ASP.NET 中直接打开由桌面程序创建的内核对象的问题。
他当时是的方法是修改 ASP.NET 配置文件,让 ASP.NET 扮演系统管理员帐号运行来访问对象。我在水木上回帖说这是非常不好的编程习惯,因为这样一来会是的 ASP.NET 页面运行在失控的安全上下文中。一旦有恶意代码攻击此页面,将具有系统管理员权限,这是非常危险的。而 IIS 5/6 之所以要提出各种隔离模型,正式为了减少以前 IIS 4/5 缺省模型下类似的问题,将页面被攻击后的损失降到最低。
hBifTs 在接受了改进意见后,采用了另外一种危害较小但也是不应推荐的方式,通过建立 NULL DACL 方式解决问题。
创建一个EveryOne SECURITY_ATTRIBUTES
这种方式可以说已经非常接近正确解决方法了,呵呵,虽然也有一定的安全隐患,但比前一种来说已经有个质的突破。
我当时曾答应写一篇文章介绍一下解决办法,可惜因为工作和其他一些原因最近一直很忙,拖到现在 :P
而从 hBifTs 摸索的过程,以及很多朋友的回帖来看,对 DACL 以及相关的权限控制很多人都非常陌生。下面就简要地介绍一下这方面的基础性知识和一些基本概念,让大家在用的时候,能够知其然也知其所以然 :P
话说开天辟地之初,NT Loader 载入内核,系统开始启动各种系统进程。各个进程在此时大都由系统支配,说同一种语言,彼此之间也没什么隔阂。可坏就怀在 WinLogon 此进程不老实,非要提供登陆界面让外人登陆到系统,进而衍生出 Explorer, CStrike 等七七八八的进程,说不同的语言干不同的事,对文件网络等资源胡乱访问。NT 系统岂是象 9x 之辈那么好欺负的:“想在我地盘上混,就先报上名来,能干什么不能干什么也得由我说了算,威胁的事情还得打个报告留个底先”。
于是用户登陆系统之时必须提供凭证,至于是缺省的密码还是认证令牌、指纹、虹膜之类由 WinLogon 的小兄弟 GINA 说了算。但如果每次访问对象都得重新认证,敲敲密码按按手印,实在是太麻烦了。于是自登陆到注销期间,开始每个帐号发一个临时的良民证,这个就是 Session 的由来。为了统一管理,NT 系统对计算机自身的进程、服务进程等等也都建立了相应的 Session,每个帐号每次每种登陆建立一个。平时你看着好像只有一个人用一个帐号登陆到系统,但实际上后台还有一堆 Session 的一堆进程在跑。
一个典型的系统运行时 Session 情况如下,此工具的开发以及字段的详细解释我会随后写一篇文章单独介绍。
KeSession - 1.0.0.0 - Show Kernel Sessions @ Jul 12 2004
(C) 1999-2004 NSFOCUS Corporation. All rights Reserved
Logon session 00000000:000003e7[999]
User name : SKY\FLIER$
Auth package : NTLM
Logon type : (none)
Session : 0
SID : S-1-5-18
Account Name : NT AUTHORITY\SYSTEM
Logon time : 2004-7-14 11:12:20
Logon server :
DNS Domain :
UPN :
Process File Name
388 : smss.exe
460 : winlogon.exe
504 : services.exe
516 : lsass.exe
736 : svchost.exe
...
3624 : inetinfo.exe
Logon session 00000000:0000cda3[52643]
User name : NT AUTHORITY\ANONYMOUS LOGON
Auth package : NTLM
Logon type : Network
Session : 0
SID : S-1-5-7
Account Name : NT AUTHORITY\ANONYMOUS LOGON
Logon time : 2004-7-14 11:12:32
Logon server :
DNS Domain :
UPN :
Logon session 00000000:0000cfc1[53185]
User name : FLIER\Administrator
Auth package : NTLM
Logon type : Interactive
Session : 0
SID : S-1-5-21-3978760259-2706837669-145014315-500
Account Name : FLIER\Administrator
Logon time : 2004-7-14 11:12:32
Logon server : FLIER
DNS Domain :
UPN :
Process File Name
1628 : explorer.exe
...
1612 : KeSession.exe
Logon session 00000000:0000a56a[42346]
User name :
Auth package : NTLM
Logon type : (none)
Session : 0
SID :
Account Name :
Logon time : 2004-7-14 11:12:20
Logon server :
DNS Domain :
UPN :
可以看到第一个登陆会话 999 就是用户名 SKY\FLIER$ 以帐号 NT AUTHORITY\SYSTEM 登陆到系统,也就是我的计算机的帐号。以前 NT 系统里面是没有计算机这个实体概念的,2000 开始计算机本身也可以作为独立实体存在,特别是在 AD 中体现得最明显。而计算机本身的帐号就是我们熟悉的 SYSTEM 帐号了。而系统进程 smss, winlogon 等等也大多再次会话中执行。另一个则是用户登陆的帐号 FLIER\Administrator,其登陆类型 Interactive 表示其是通过鼠标键盘在控制台登陆的。此外还有有些其他会话,如网络登陆的 NT AUTHORITY\ANONYMOUS LOGON 是用于匿名登陆获取信息的。以前 NT4 不支持计算机实体登陆的时候,只能通过匿名连接这种变通的方式获取其他机器信息。
另外一个值得注意的是每个会话都有一个 SID 用来标示用户帐号,有固定的 S-1-5-18, S-1-5-7 这种 well-known SID,也有 S-1-5-21-3978760259-2706837669-145014315-500 这种机器相关 SID,就不详细解释了,不然就话长了,呵呵
每个帐号在登陆系统时,会获取它本身的权限相关信息,也就是能访问什么不能访问什么。但这样似乎还不够,就好像 ASP.NET 进程某个页面有时需要扮演其他帐号来访问特殊资源,或者某个进程在允许范围内把自己的权限调整调整。如果帐号权限保存在会话一级,则就会出现一个进程要把权限改大,另一个又要改小的扯皮问题(竞争情况)。要调解纠纷,最后只能各大三个大板,每个进程发一个良民证副本(Token),各取所需自己去改吧,呵呵。进程中的线程一级也是有类似问题的,只是缺省情况下不发,有需要的时候自己去申请。
这个良民证(Token)用处很多,它上面登记了当前用户的用户名和组的情况、此帐号的特权情况、以及我们这儿需要关注的:缺省的对象权限情况。Jeffrey Richter 和 Jason Clark 在 Programming Server-Side Applications for Microsoft Windows一书的第11章 User Context 中,提供了一个非常强大的工具,Token Master,可以查看当前系统任意进程、线程的良民证 (Token)。
下面是一个典型的允许在系统管理员帐号下的进程的缺省对象权限设置:
********************************************************************************
Token Default DACL
********************************************************************************
ACCESS ALLOWED BUILTIN/Administrators
ACCESS_MASK: 00010000000000000000000000000000
GENERIC_ALL
ACCESS ALLOWED NT AUTHORITY/SYSTEM
ACCESS_MASK: 00010000000000000000000000000000
GENERIC_ALL
也就是说,在一个系统管理员帐号中,在建立一个新的内核对象时,如果 LPSECURITY_ATTRIBUTES lpAttributes 参数为 NULL,则此对象缺省是只有此帐号自己或管理员组和 SYSTEM 帐号具有完全访问权限的。如果一个运行在其他帐号下的
程序视图打开并访问此对象,就会被拒绝。这就是为什么hBifTs无法在 ASP.NET 中打开桌面程序建立的文件映射对象的原因,因为他的桌面程序八成是在系统管理员帐号下运行,建立的对象只有系统管理员组和系统帐号能够打开访问。而 ASP.NET 为安全起见,是在 ASPNET 帐号下运行 ASP.NET 页面的,其并不是系统管理员组成员,也就无法访问此对象。
hBifTs提出的第一种扮演的方法其实也是一种解决方法。通过设置 ASP.NET 的脚本,让 ASP.NET 在系统管理员帐号下运行,这样此页面运行所在线程的良民证就会暂时被换为系统管理员的,也就有权限进行处理了。但因为是通过配置文件设置,扮演的粒度太大,整个页面的生命期都会扮演管理员帐号,非常不安全。即使要用这种方法,也应该通过 WindowsIdentity.Impersonate 方法这种细粒度的控制,在需要访问对象的时候,动态扮演系统管理员角色,完成操作后恢复到原本帐号安全上下文中。不过就是这样也不安全,呵呵,还是存在安全隐患。
正规的思路是在创建对象的时候,就指定其访问权限。但 hBifTs 的第二种解决方法又太过了一些,它建立了一个 NULL DACL 的安全描述符,也就是允许任何人进行任何访问的对象。特别是对文件映射这种用于交互数据的对象,开启这种程度的权限,就给恶意代码留了巨大的空子,他们可以简单的打开这个对象,在里面填充一些垃圾数据或攻击数据,搞定使用此对象进行通讯的两方程序。以前我就用类似的方式成功搞定某种个人防火墙,通过操作它内部互斥量,欺骗引擎和界面的通讯,让你以外防火墙还在工作,但实际上已经被停止了,呵呵 :P
要缓解这种问题,就需要在建立对象的时候,为 ASP.NET 运行的帐号指定特殊的权限。可以使用配置文件提供系统 ASP.NET 帐号名,然后在建立对象是为此帐号增加访问权限。jiangsheng在其《跨进程访问共享内存的权限问题》一文中就是通过强制指定 everyone (SECURITY_WORLD_SID_AUTHORITY)都可以访问此对象来解决问题的。虽然这种权限管理也比较粗放,但比起前文中的 NULL DACL 稍好一些,NULL DACL 是真正的连 everyone 以外随便什么都能访问的无设防对象 :P
而较好的方式是细粒度地控制目标进程的访问,大概分为以下几个步骤:
1.获取当前线程或进程的令牌
2.获取令牌使用者的 SID
3.获取 ASP.NET 运行帐号的 SID
4.设置安全描述符的 DACL
5.使用安全描述符创建对象
实例代码如下(为演示简便,忽略了调试和错误处理代码,只保留最基本的检查):
第一步需要先尝试获取线程令牌,因为上面曾经提到,令牌是可以设置到线程一级的,而且线程本身还可能在扮演其他帐号。如果线程没有独立令牌则尝试获取进程令牌。至于获取令牌句柄的权限,能够查询足以,为后面进一步获取令牌信息做准备。
HANDLE hToken;
if(!OpenThreadToken(GetCurrentThread(), TOKEN_QUERY, TRUE, &hToken))
{
if(!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &hToken))
{
std::cerr << "无法获得进程令牌 " << std::endl;
return -1;
}
}
第二步需要获取令牌使用者的 SID。GetTokenInformation 能够从令牌查询各种关联信息,如这儿我们需要的用户 SID。
typedef struct _TOKEN_USER {
SID_AND_ATTRIBUTES User;
} TOKEN_USER;
typedef struct _SID_AND_ATTRIBUTES {
PSID Sid;
DWORD Attributes;
} SID_AND_ATTRIBUTES, *PSID_AND_ATTRIBUTES;
DWORD dwUserSize = 0;
GetTokenInformation(hToken, TokenUser, NULL, 0, &dwUserSize);
std::string user(dwUserSize, '\0');
BOOL ret = GetTokenInformation(hToken, TokenUser, (void *)user.c_str(), user.size(), &dwUserSize);
CloseHandle(hToken);
PSID sidUser = ((PTOKEN_USER)user.c_str())->User.Sid;
第三步则根据 ASP.NET 运行帐号获取其 SID。这里为简便,使用硬编码的帐号名 ASPNET,具体使用时,可以通过配置文件或直接读取系统设置和配置文件的方式获取灵活性。
const char *ASPNET_ACCOUNT = "ASPNET";
DWORD dwSidSize = 0, dwDomainSize = 0;
SID_NAME_USE use;
LookupAccountName(NULL, ASPNET_ACCOUNT, NULL, &dwSidSize, NULL, &dwDomainSize, &use);
std::string sid(dwSidSize-1, '\0'), domain(dwDomainSize-1, '\0');
ret = LookupAccountName(NULL, ASPNET_ACCOUNT, (PSID)sid.c_str(), &dwSidSize, (char *)domain.c_str(), &dwDomainSize, &use);
PSID sidAspNet = (PSID)sid.c_str();
第四步则根据前面获取两个 SID 来初始化一个安全描述符。InitializeAcl 初始化一个 ACL(任意访问控制列表),然后以 AddAccessAllowedAce 增加允许的 ACE(访问控制项)。这里将缺省用户和ASP.NET用户都赋给 GENERIC_ALL 访问权限,具体使用中可根据需求进一步细化。最后使用此 ACL 构造一个安全描述符,设置其 DACL(自由访问控制列表)。
DWORD dwACLSize = sizeof(ACL) + sizeof(ACCESS_ALLOWED_ACE) * 2 +
GetLengthSid(sidUser) + GetLengthSid(sidAspNet);
std::string acl(dwACLSize, '\0');
InitializeAcl((PACL)acl.c_str(), acl.size(), ACL_REVISION);
AddAccessAllowedAce((PACL)acl.c_str(), ACL_REVISION, GENERIC_ALL, sidUser);
AddAccessAllowedAce((PACL)acl.c_str(), ACL_REVISION, GENERIC_ALL, sidAspNet);
SECURITY_DESCRIPTOR sd;
InitializeSecurityDescriptor(&sd, SECURITY_DESCRIPTOR_REVISION);
SetSecurityDescriptorDacl(&sd, TRUE, (PACL)acl.c_str(), TRUE);
此外也可以考虑使用 GetTokenInformation 配合 TokenDefaultDacl 选项直接获取令牌的缺省 DACL,再使用 SetEntriesInAcl 直接对其进行修改,生成新的安全描述符。不过这个就比较麻烦了,MSDN 里面提供了一个完整的代码示例,有兴趣的朋友可以看看。
第五步使用此安全描述符创建内核对象。可以使用 sysinternal 提供的 Process Explorer 工具查看此例子程序中创建的对象的权限,可以看到如我们所希望,当前用户和ASPNET用户被加入 GENERIC_ALL 权限。
SECURITY_ATTRIBUTES sa = { sizeof(sa), &sd, FALSE };
HANDLE hMutex = CreateMutex(&sa, FALSE, "TestMutex");
char ch;
std::cin >> ch;
CloseHandle(hMutex);
大致的使用思路如上,其中很多细节还可以深入展开谈,但因为时间精力有限,这儿就暂且略过,有兴趣的朋友可以进一步探讨。