Windows2000 服务器端应用程序开发设计指南-存取控制(8)
修改DACL
转自:http://mickorguo.blog.163.com/blog/static/621948020074122179787/
建立安全对象牵涉到与需要实作服务一样多的存取控制。您可以实作许多服务,而从来不用修改现存对象的安全性,这是令人惊异的。当然,某些程序必须处理已经拥有应用安全性的现存对象。在此种情况下,您的软件必须能够读取及修改对象的DACL。以下是您可以采取的步骤:
- 撷取您要修改之对象的DACL。
- 假如您正在移除ACEs,在DACL中搜寻这些ACEs并加以删除。
- 收集您将加入ACEs的信任成员SIDs。
- 建立您将加入的ACEs。
- 在DACL中搜寻是否有与您将加入相同的ACEs。若有任何一个存在,将它们从您加入的ACEs群组中移除。
- 计算新DACL的大小。
- 分配内存及初始化新的DACL。
- 复制旧的DACL到新的DACL。
- 在新的DACL中将新的ACEs插入适当的位置。
- 指派DACL回对象。
如您所见,读取及修改对象DACL的程序并没有那么简单,然而我应告诉您,我已经提供了最糟情况的方案。通常您并非真的移除ACEs,所以可以忽略步骤二的动作。而一般的情形下,您只增加了一个个别的ACE;假如该ACE已经存在DACL中,那么您可以中止整个程序,并简化步骤五的动作。
说明
在ACLs中搜寻已经存在的ACEs是很重要的,虽然完全一样的ACEs并不会影响对象的 安全性 。其原因是ACEs占用了系统中的内存,而某些系统对象(例如,window站台)会出乎意料的持有少数ACEs。每次执行就增加ACE,没有检查工作之必要性的任何软件,最后将耗尽系统的资源。
现在让我告诉您处理读取及修改DACL之工作必须使用的工具。让我们从一个简单的函数开始(您已经知道如何读取DACL中的资讯,这些技巧将在此处开始产生作用)。
BOOL DeleteAce(
PACL pACL,
DWORD dwACEIndex);
DeleteAce函数会从DACL中移除一个ACE,您必须传递ACL及您要移除的ACE索引值给它。
说明
如果您对这个对象所做的唯一修改只是移除ACEs,那么您不用为新的DACL分配内存。您可以简单地移除经由GetSecurityInfo传回之DACL中的ACEs,然后再放置修改过的DACL回对象。
处理程序的下一个步骤即是要求您拥有便于使用的ACE,因为您将使用到AddAce函数,它会要求一个已建立的ACE;再者,因为您可能会在ACL中搜寻已存在的ACE,此时有个ACE结构可对照DACL之ACEs是方便的。
可惜系统不提供让您使用简便方法建立ACEs的函数。建立ACEs的工作是复杂的,因为每个ACE皆包括了一个SID结构,而SID结构的长度是可变的。最好使用您的SID长度及ACE结构长度分配一个缓冲器给ACE,并复制SID到ACE中,然后设定其ACE栏位。以下的函数显示了实行的方法:
PACE_UNION AllocateACE(ULONG bACEType, ULONG bACEFlags,
ULONG lAccessMask,PSID pSID ){
PACE_UNION pReturnACE = NULL;
PBYTE pbBuffer = NULL;
try{
// 取得ACE中SID的偏移量
ULONG lSIDOffset = (ULONG)(&((ACCESS_ALLOWED_ACE*)0)->SidStart);
// 取得没有SID的ACE大小
ULONG lACEStructSize = sizeof(ACCESS_ALLOWED_ACE)-
sizeof(((ACCESS_ALLOWED_ACE*)0)->SidStart);
// 取得SID的长度
ULONG lSIDSize = GetLengthSid(pSID);
// 分配一个缓冲器给ACE
pbBuffer = (PBYTE)LocalAlloc(LPTR, lACEStructSize + lSIDSize);
if (pbBuffer == NULL)
goto leave;
// 复制SID到ACE
if(!CopySid(lSIDSize, (PSID)(pbBuffer+lSIDOffset), pSID)){
goto leave;
}
pReturnACE = (PACE_UNION) pbBuffer;
pReturnACE->aceHeader.AceSize = (USHORT)(lACEStructSize + lSIDSize);
pReturnACE->aceHeader.AceType = (BYTE)bACEType;
pReturnACE->aceHeader.AceFlags = (BYTE)bACEFlags;
pReturnACE->aceAllowed.Mask = lAccessMask;
leave:;
}catch(...){}
// 在错误实例中释放缓冲器
if (pbBuffer != (PBYTE)pReturnACE){
LocalFree(pbBuffer);
}
return (pReturnACE);
}
当在使用AllocateAce时,您只须在执行完成时传递回传的ACE给LocalFree。
说明
AllocateAce会传回一个PACE_UNION指标,它是本章先前定义的类型,我选择使用此技巧并以一般的方式表示ACEs。选择特定ACE类型之其中一个也是常见的方法,例如ACCESS_ ALLOWED_ACE,可使用在您的ACE之一般操作上。两种方法都行得通;然而,在本章的一些范例函数中,所使用的是PACE_UNION。
现在您已经建立了ACE,接下来您必须在对象现存的DACL中搜寻符合的ACEs。您可以经由使用本章先前谈论的ACL读取技巧来实作。在我编写执行此工作程序代码时使用了两个函数:一个用来对照ACEs,另一个则是在ACL中搜寻符合的ACE。
BOOL IsEqualACE(PACE_UNION pACE1,PACE_UNION pACE2 ){
BOOL fReturn =FALSE;
try{{
if(pACE1->aceHeader.AceType !=pACE2->aceHeader.AceType)
goto leave;
//取得ACE中SID的偏移量
ULONG lSIDOffset =(ULONG)(&((ACCESS_ALLOWED_ACE*)0)->SidStart);
//取得没有SID的ACE大小
ULONG lACEStructSize =sizeof(ACCESS_ALLOWED_ACE)-
sizeof(((ACCESS_ALLOWED_ACE*)0)->SidStart);
PBYTE pbACE1 =(PBYTE)pACE1;
PBYTE pbACE2 =(PBYTE)pACE2;
fReturn =TRUE;
while(lACEStructSize--)
fReturn =(fReturn &&((pbACE1 [lACEStructSize ] ===
pbACE2 [lACEStructSize ])));
/检查SIDs
fReturn =fReturn &&EqualSid((PSID)(pbACE1+lSIDOffset),
(PSID)(pbACE2+lSIDOffset));
}leave:;
}catch(...){
}
return (fReturn);
}
以下是第二个函数的内容:
int FindACEInACL(PACL pACL, PACE_UNION pACE ){
int nACEIndex =-1;
try{{
ACL_SIZE_INFORMATION aclSize;
if (!GetAclInformation(pACL, &aclSize, sizeof(aclSize),
AclSizeInformation)){
goto leave;
}
while (aclSize.AceCount--){
PACE_UNION pACETemp;
if(!GetAce(pACL, aclSize.AceCount, (PVOID *)&pACETemp))
goto leave;
if(IsEqualACE(pACETemp, pACE)){
nACEIndex = (int)aclSize.AceCount;
break;
}
}
}leave:;
}catch(...){
}
return (nACEIndex);
}
FindACEInACL函数会传回DACL中符合的ACE索引值,如果没有找到,则传回 -1值。您应该使用此函数或类似的函数,以找出您打算加到对象DACL的ACEs是否已经存在DACL中(FindACEInACL函数对于寻找您想要使用DeleteAce删除的ACEs也是很有帮助的)。
现在您已经知道哪些ACEs必须加入DACL及已建立的ACEs,现在是计算新DACL大小的时候了。您可以使用在本章前面出现过的CalculateACLSize函数。它可让您使用现存的ACL、SIDs阵列及ACEs阵列来计算ACL大小。您可以传递NULL给这些参数。在我们的范例中,您有可能会传递一个现存的ACL及ACEs阵列,以在加入ACEs后用来计算新ACL的大小。
在您分配内存给新的ACL后,应使用InitializeAcl(本章前面讨论过)来初始化新的ACL。接下来您必须从旧的ACL复制ACEs到新的ACL。
以下的函数显示其实行的方法:
BOOL CopyACL(PACL pACLDestination, PACL pACLSource ){
BOOL fReturn = FALSE;
try{{
// 取得原始ACL中ACEs的数量
ACL_SIZE_INFORMATION aclSize;
if (!GetAclInformation(pACLSource, &aclSize,
sizeof(aclSize), AclSizeInformation)){
goto leave;
}
// 使用GetAce及AddAce复制ACEs
for(ULONG lIndex=0;lIndex < aclSize.AceCount;lIndex++){
ACE_HEADER* pACE;
if(!GetAce(pACLSource, lIndex, (PVOID*)&pACE))
goto leave;
if(!AddAce(pACLDestination, ACL_REVISION, MAXDWORD,
(PVOID*)pACE, pACE->AceSize))
goto leave;
}
fReturn = TRUE;
}leave:;
}catch(...){
}
return (fReturn);
}
CopyACL函数相当简单,它只不过重复着通过现存DACL之ACEs并复制每一个到新DACL的动作而已。CopyACL使用了AddAce系统函数,其定义如下:
BOOL AddAce(
PACL pACL,
DWORD dwACERevision,
DWORD dwStartingACEIndex,
PVOID pACEList,
DWORD dwACEListLength);
请注意,AddAce与AddAccessAllowedAce及AddAccessDeniedAce不同。首先您必须提供ACE;系统不会为您收集ACE并放置到DACL里,这意味着您应要定义ACEs的类型,所以AddAce可以把任意类型的ACE加至DACL中;第二,经由使用以0开始的索引值,AddAce可让您决定想要插入新ACE到DACL的位置;第叁,AddAce可让您增加更多的ACE,经由传递连续的ACEs清单给pACEList参数以及清单大小给dwACEListLength参数(不要被清单中的ACEs数量混淆)。
说明
经由允许AddAce复制一个个别函数以呼叫之中所有的原始ACEs,CopyACL范例函数可以更有效地被实作。然而,如此做而取得的ACL结构资讯,应该被视为不透明的。
在您复制旧DACL到更宽敞的新DACL之后,可以使用AddAce开始加入ACEs。请注意,必须将您的ACEs插入适当的新DACL索引中,以便维护适当的ACE顺序(关于ACE顺序,请参阅 表10-6 )。您可以使用以下的函数为ACL中的新ACE决定适当的索引值:
ULONG GetACEInsertionIndex(PACL pDACL, PACE_UNION pACENew){
ULONG lIndex = (ULONG)-1;
try{{
// ACL顺序的ACE类型
ULONG lFilterType[] = { ACCESS_DENIED_ACE_TYPE,
ACCESS_DENIED_OBJECT_ACE_TYPE,
ACCESS_ALLOWED_ACE_TYPE,
ACCESS_ALLOWED_OBJECT_ACE_TYPE};
// 决定新的ACE应该隶属的群组
ULONG lNewAceGroup;
for(lNewAceGroup = 0; lNewAceGroup<4 ; lNewAceGroup++){
if(pACENew->aceHeader.AceType == lFilterType[lNewAceGroup])
break;
}
// 假如群组 == 4,则这个ACE类型不好
if(lNewAceGroup==4)
goto leave;
// 假如新的ACE是个继承的ACE,那么它会追求其他的ACEs
if((pACENew->aceHeader.AceFlags & INHERITED_ACE) != 0)
lNewAceGroup+=4;
// 取得ACE总数
ACL_SIZE_INFORMATION aclSize;
if (!GetAclInformation(pDACL, &aclSize,
sizeof(aclSize), AclSizeInformation)){
goto leave;
}
// 重复通过ACEs
lIndex = 0;
for(lIndex = 0;lIndex < aclSize.AceCount;lIndex++){
ACE_HEADER* pACE;
if(!GetAce(pDACL, lIndex, (PVOID*)&pACE))
goto leave;
// 取得ACL之ACE的群组
ULONG lAceGroup;
for(lAceGroup = 0; lAceGroup<4 ; lAceGroup++){
if(pACE->AceType == lFilterType[lAceGroup])
break;
}
// 测试不好的ACE
if(lAceGroup==4){
lIndex = (ULONG)-1;
goto leave;
}
// 调整继承
if((pACE->AceFlags & INHERITED_ACE) != 0)
lAceGroup+=4;
// 假如是相同的群组,那么找出插入点
if(lAceGroup>=lNewAceGroup)
break;
}
}leave:;
}catch(...){
}
return (lIndex);
}
GetACEInsertionIndex函数会找出您插入新ACE的索引值,并在过程中考虑对象ACEs及ACE继承的部份。在您知道索引值之后,可以呼叫AddAce,以增加新的ACE到ACL中。
每个新的ACE都会如此做,然后再使用适当的函数设定新的DACL到安全对象。不要忘了在您使用后清理并释放已经分配的任何内存。
在本章稍早,我曾答应要展示一个在DACL中安排ACEs顺序的范例函数。假如在您彻底完成加入ACEs到DACL前,一点也不想要担心ACE顺序的问题,那么此函数对您可能会很有帮助。以下的OrderDACL函数相当简单,它建立在GetACEInsertionIndex范例函数上:
BOOL OrderDACL(PACL pDACL ){
BOOL fReturn = FALSE;
try{{
// 取得ACL大小及ACE总数
ACL_SIZE_INFORMATION aclSize;
if (!GetAclInformation(pDACL, &aclSize,
sizeof(aclSize), AclSizeInformation)){
goto leave;
}
// 为暂时的ACL取得内存
PACL pTempDACL = (PACL)_alloca(aclSize.AclBytesInUse);
if (pTempDACL==NULL)
goto leave;
// 初始暂时的ACL
if (!InitializeAcl(pTempDACL, aclSize.AclBytesInUse,
ACL_REVISION))
goto leave;
// 重复通过ACEs
for (ULONG lAceIndex = 0;
lAceIndex < aclSize.AceCount ; lAceIndex++){
// 取得ACE
PACE_UNION pACE;
if (!GetAce(pDACL, lAceIndex, (PVOID*)&pACE))
goto leave;
// 找出位置,并且把ACE加入temp DACL
ULONG lWhere = GetACEInsertionIndex(pTempDACL, pACE);
if (!AddAce(pTempDACL, ACL_REVISION,
lWhere, pACE, pACE->AceSize))
goto leave;
}
// 复制temp DACL到原始的
CopyMemory(pDACL, pTempDACL, aclSize.AclBytesInUse);
fReturn = TRUE;
}
leave:;
}catch(...){
}
return (fReturn);
}
让我们看看真实世界实作讨论过的技巧范例。
修改DACL范例
我们的范例提供了一组有用的函数给服务开发人员,它显示如何修改一个现存DACL的方法。我使用了Windows中的window站台及桌面安全对象。
window站台 是Windows的安全对象,包含了剪贴簿、一组通用元素及桌面对象的集合。window站台可以是互动式的,意味着使用者可以看到它的「桌面」,一个互动式windows站台也包含键盘及滑鼠资讯。一个程序可以有一个与它相关联的个别windows站台。
桌面 是一个包含在window站台内的安全对象。一个桌面维护着一个逻辑的显示外观,并包含功能表、视窗及萤幕上的其他可视对象。
不与使用者互动的服务和互动式桌面没有关联。当使用者登入系统时,互动式window站台(称为WinSta0)的DACL及其预设桌面(称为Default)会被重新设定,并且把此对象的存取权给予使用者。最后,只有已登录的使用者及系统会被授予对此对象的存取权。
问题描述 服务有时必须在任一信任成员帐户下(使用下一章要讨论的CreateProcessAsUser函数)建立另一个程序,以在目前没有与系统互相作用的使用者环境下建立一个程序。假如此程序需要与使用者互动,而使用者帐户对互动式window站台及预设桌面没有存取权,则系统呼叫CreateProcessAsUser函数时会失败。
由于发现了这个问题,在建立程序之前,您必须检查这些对象之DACLs的使用者存取权利。假如没有找到这些权利,则它们必须被加入。首先检查权利是很重要的,因为盲目地增加ACE到使用者对象,最后可能会耗尽系统中的资源(通常您只能增加约80个ACEs到window站台)。
解决方案 现在让我们开始着手处理解决方案。我使用这一节所讨论的工具及观念(和一些范例函数)实作了两个函数:一个可让信任成员存取window站台,而另一个可让信任成员存取桌面。这些函数非常简单。尽管修改对象DACL的程序看起来有点令人怯步,但真实世界中的程序并没有我们所想像的复杂。以下的程序代码可让信任成员存取window站台:
BOOL AllowAccessToWinSta(PSID psidTrustee, HWINSTA hWinSta ){
BOOL fReturn = FALSE;
PSECURITY_DESCRIPTOR psdWinSta = NULL;
PACE_UNION pACENew = NULL;
try{{
// 取得window站台的DACL
PACL pDACLWinSta;
if(GetSecurityInfo(hWinSta, SE_WINDOW_OBJECT,
DACL_SECURITY_INFORMATION, NULL, NULL, &pDACLWinSta,
NULL, &psdWinSta) != ERROR_SUCCESS)
goto leave;
// 分派新的ACE
// 这个存取授予互动地登录的使用者
PACE_UNION pACENew = AllocateACE(ACCESS_ALLOWED_ACE_TYPE, 0,
DELETE|WRITE_OWNER|WRITE_DAC|READ_CONTROL|
WINSTA_ENUMDESKTOPS|WINSTA_READATTRIBUTES|
WINSTA_ACCESSCLIPBOARD|WINSTA_CREATEDESKTOP|
WINSTA_WRITEATTRIBUTES|WINSTA_ACCESSGLOBALATOMS|
WINSTA_EXITWINDOWS|WINSTA_ENUMERATE|WINSTA_READSCREEN,
psidTrustee);
// ACE是否已经在DACL中?
if (FindACEInACL(pDACLWinSta,pACENew) == -1){
// 假如没有,计算新的DACL大小
ULONG lNewACL = CalculateACLSize(pDACLWinSta, NULL, 0,
&pACENew, 1 );
// 分派内存给新的DACL
PACL pNewDACL = (PACL)_alloca(lNewACL);
if (pNewDACL == NULL)
goto leave;
// 初始ACL
if (!InitializeAcl(pNewDACL, lNewACL, ACL_REVISION))
goto leave;
// 复制ACL
if (!CopyACL(pNewDACL, pDACLWinSta))
goto leave;
// 取得新ACE的位置
ULONG lIndex = GetACEInsertionIndex(pNewDACL, pACENew);
// 增加新的ACE
if (!AddAce(pNewDACL, ACL_REVISION, lIndex,
pACENew, pACENew->aceHeader.AceSize))
goto leave;
// 设定DACL回到window站台
if (SetSecurityInfo(hWinSta, SE_WINDOW_OBJECT,
DACL_SECURITY_INFORMATION, NULL, NULL,
pNewDACL, NULL)!=ERROR_SUCCESS)
goto leave;
}
fReturn = TRUE;
}leave:;
}catch(...){
}
// 清除
if(pACENew != NULL)
LocalFree(pACENew);
if(psdWinSta != NULL)
LocalFree(psdWinSta);
return (fReturn);
}
下一个范例函数可让信任成员存取桌面:
BOOL AllowAccessToDesktop( PSID psidTrustee, HDESK hDesk ){
BOOL fReturn = FALSE;
PSECURITY_DESCRIPTOR psdDesk = NULL;
PACE_UNION pACENew = NULL;
try{{
// 取得桌面的DACL
PACL pDACLDesk;
if(GetSecurityInfo(hDesk, SE_WINDOW_OBJECT,
DACL_SECURITY_INFORMATION, NULL, NULL, &pDACLDesk,
NULL, &psdDesk) != ERROR_SUCCESS)
goto leave;
// 分派新的ACE
// 这个存取授予互动地登录的使用者
PACE_UNION pACENew = AllocateACE(ACCESS_ALLOWED_ACE_TYPE, 0,
DELETE|WRITE_OWNER|WRITE_DAC|READ_CONTROL|
DESKTOP_READOBJECTS|DESKTOP_CREATEWINDOW|
DESKTOP_CREATEMENU|DESKTOP_HOOKCONTROL|
DESKTOP_JOURNALRECORD|DESKTOP_JOURNALPLAYBACK|
DESKTOP_ENUMERATE|DESKTOP_WRITEOBJECTS|DESKTOP_SWITCHDESKTOP,
psidTrustee);
// ACE是否已经在DACL中?
if (FindACEInACL(pDACLDesk, pACENew) == -1){
// 假如没有,计算新的DACL大小
ULONG lNewACL = CalculateACLSize(pDACLDesk, NULL, 0,
&pACENew, 1 );
// 分配内存给新的DACL
PACL pNewDACL = (PACL)_alloca(lNewACL);
if (pNewDACL == NULL)
goto leave;
// 初始ACL
if (!InitializeAcl(pNewDACL, lNewACL, ACL_REVISION))
goto leave;
// 复制ACL
if (!CopyACL(pNewDACL, pDACLDesk))
goto leave;
// 取得新ACE的位置
ULONG lIndex = GetACEInsertionIndex(pNewDACL, pACENew);
// 增加新的ACE
if (!AddAce(pNewDACL, ACL_REVISION, lIndex,
pACENew, pACENew->aceHeader.AceSize))
goto leave;
// 设定DACL回到桌面
if (SetSecurityInfo(hDesk, SE_WINDOW_OBJECT,
DACL_SECURITY_INFORMATION, NULL, NULL,
pNewDACL, NULL)!=ERROR_SUCCESS)
goto leave;
}
fReturn = TRUE;
}leave:;
}catch(...){
}
// 清除
if(pACENew != NULL)
LocalFree(pACENew);
if(psdDesk != NULL)
LocalFree(psdDesk);
return (fReturn);
}
以下的程序片段显示如何使用这些函数的范例。此程序代码为内建的Everyone群组建立了一个SID,并将它传递到AllowAccessToWinSta及AllowAccessToDesktop函数中:
PSID psidEveryone;
// 为内建的「Everyone」群组建立一个SID
SID_IDENTIFIER_AUTHORITY sidAuth = SECURITY_WORLD_SID_AUTHORITY;
if (!AllocateAndInitializeSid(&sidAuth, 1, SECURITY_WORLD_RID,
0, 0, 0, 0, 0, 0, 0, &psidEveryone )){
// 错误
}
HWINSTA hWinSta = GetProcessWindowStation();
if (hWinSta == NULL){
// 错误
}
AllowAccessToWinSta(psidEveryone, hWinSta);
HDESK hDesk = GetThreadDesktop(GetCurrentThreadId());
if (hDesk == NULL){
// 错误
}
AllowAccessToDesktop(psidEveryone, hDesk);
说明
这里有个秘诀。AllowAccessToWinSta及AllowAccessToDesktop函数使用了标头文件Malloc.h中的C执行时期(Run-Time)程序库定义的_alloca函数,_alloca函数会在线程的堆叠上分配一块内存。此函数的优点是非常快速,不需内部的线程同步化及传回不必由应用程序释放的内存。当您跳出被呼叫的函数时,系统就会释放内存。 对于必须重复小型配置的安全性程序设计师而言,这样的函数会是个救命的工具,它将会加快程序代码的速度并帮助您避免内存缺乏的情形。
在此强烈地建议您找出时间察看AllowAccessToWinSta及AllowAccessToDesktop函数的内容,直到您了解它们的运作方式并有自信使用它们所采用的技巧为止。这些范例函数所执行的任务大约和您在存取控制程序设计中看到的一样复杂,假如您对它们感到自在,则实作符合您所需的存取控制大概就不会有困难。
实作存取控制的选择
Microsoft及其他厂商试图经由建立更高阶的函数来封装我们曾讨论过的低阶函数,以减轻存取控制程序设计师的重担。协力厂商也已经产生了减轻存取控制工作的解决方案。认识Windows之低阶存取控制的功能以及这些高阶函数的某些陷阱,应该能够让您做出满足程序代码需求的决定。
更高阶套件(Package)的实作器已经面临到这些挑战:
- 简化非常有弹性的存取控制系统,不用限制弹性或在专案中可能需要的特色。
- 建立健全且合用的程序代码。
第一个挑战-即维持弹性-是最难克服的。大多数的存取控制需求,可以使用Windows存取控制直接实作。然而,随着弹性而产生的复杂性,以及移除复杂性时就会移除了某些开发人员所期待的特色。
看起来第二个挑战似乎比较可能达成,但是这个实例还未被证明。Microsoft实作了一组「高阶」安全性的函数,并且已加入Win32 API中。这些函数彻底地简化了某些安全性程序设计的观点,以及某些具有历史性的错误及固有的缺点。以下举出一个范例:
DWORD GetEffectiveRightsFromAcl(
PACL pACL,
PTRUSTEE pTrustee,
PACCESS_MASK pAccessRights);
GetEffectiveRightsFromAcl函数为了搜寻ACL及传回DACL指出允许之存取权利到信任成员的存取遮罩而准备。听起来非常方便!这样的函数可以潜在中将我们的需求移到加入ACEs之前,先在DACL中搜寻ACEs。然而,GetEffective RightsFromAcl试图做太多的事,如些一来,会产生做出来的成果几乎不能使用的情形。
GetEffectiveRightsFromAcl函数指出允许存取权是以一致的允许存取ACEs之组合为基础,然后再减去一致的拒绝存取ACEs的组合。这意味着GetEffective RightsFromAcl可以传回一组存取权利,它会指出ACL没有授予想要的存取权给信任成员部份,在我离开时仍旧不知道如何处理这个问题。没有授予存取权是因为缺少允许存取ACEs,或是因为存在拒绝存取ACEs呢?我厌恶增加允许存取ACE的方法,只想找出无效拒绝存取ACE使我仍然没有存取权的原因。
GetEffectiveRightsFromAcl不只会搜寻符合您提供的信任成员ACEs,也搜寻信任成员所属之群组帐户的任何ACEs。但是这个函数并没有包括内建的群组,例如Everyone及受认证的使用者。假如它需要寻找群组的ACEs,而信任您程序代码的群组成员并没有列举权利时,此动作会失败。最后,没有方法可以限制搜寻程序只找出明确指派给信任成员的存取权。
GetEffectiveRightsFromAcl是为了使您的程序代码能够查出信任成员对象上的存取检查是否成功或失败而准备。然而,存取检查需要一个权杖而非信任成员SID。权杖包含权限;权杖也可以被调整或限制(请参阅 第十一章 )。在检查存取时,GetEffectiveRightsFromAcl不会把权限带到帐户里。存取检查能够执行成功是以对象所有权为基础,但是GetEffectiveRightsFromAcl并不了解对象所有权。这些方法使得GetEffectiveRightsFromAcl的使用有限。
更有帮助的函数SetEntriesInAcl及GetExplicitEntriesFromAcl是为了帮助您在卸下为ACEs及ACLs分配的内存责任的同时,也可以直接处理ACL而准备。这些函数的目的很重要,但是这个函数具有历史性的错误及效能上的争议。某些问题已经被清除了,不过如果您选择在专案中使用这些函数,彻底测试其程序代码是很重要的。
应该与SetEntriesInAcl一起使用的BuildExplicitAccessWithName函数不会传回任何值,它会潜在地把错误方案(当使用低阶函数呼叫LookupAccountName时,可以撷取)委托给SetEntriesInAcl函数。由于SetEntriesInAcl没有回报失败项目的方法,因此您会因为无法从失败案例中恢复而离开。
您可以有其他的选择。Active Template Library(ATL)的开发人员设计了一个称为CSecurityDescriptor的C++ 类别,定义在标头档AtlCom.h中。CSecurityDescriptor之额外的好处是提供全部的类别原始码。尽管此类别提供了大量的功能,但本身也有陷阱。例如,此函数会以一向不遵守Windows 2000提出的ACE顺序的方式增加ACEs,虽然它们遵守Windows NT 4.0的方针。此外,从核心对象撷取安全性的AttachObject函数则用GetKernelObjectSecurity代替GetSecurityInfo,它是与Windows 2000一起使用所建议的函数。
如前所述,CSecurityDescriptor类别最好的东西即是拥有原始码。假如它朝您需要的方向去运作,那会很好!如果没有的话,您便可以选择修改类别。而且若您找到任何错误,可以选择使用您拥有与Windows之低阶存取控制相关的知识来修改它们。
安全的私有对象
我已经提过好几次,您可以经由使用Windows存取控制来保护软件建立的私有对象。这个特色真的很强大,尤其是它可能对服务开发人员有用。到目前为止您已经学习的DACL建立技巧,适用于保护私人对象和系统对象,所以在您自己的对象上实作存取控制所需学习的部份并不多。
这个工作牵涉到需要保护的对象在您的软件及系统间共用的情形。您的软件必须执行以下的工作,以实作对象的私有安全性:
- 您的软件必须为您的对象定义特定的权利(使用存取遮罩的低16位元)。
- 您的软件必须决定哪个标准权利与您的对象有关。
- 您的软件必须决定通用权利对应到哪个标准及特定权利。
- 您的软件必须把安全描述项(系统建立的)与对象联系在一起。
- 您的软件必须将适合您对象的安全描述项与对象储存到永续性储存体中。
- 您的软件在安全对象上执行安全性活动前,必须先执行存取检查。
系统提供了以下的特色:
- 为您的对象建立及删除安全描述项的函数。
- 取得及设定这些安全描述项之特殊部分函数。
- 在某方面来说,一个执行存取检查的函数会与Windows之其他安全对象一致。
在我们开始讨论特定函数前,必须阐明以下两点:
- 私人对象安全性应该能被不同安全性环境中执行服务之客户端服务软件使用。
- 私人对象安全性要求您的软件使用权杖指出客户端之安全性环境。请记得权杖包含了一个信任帐户的SID与它的群组SIDs、权限及预设DACL(第十一章将会更详细讨论权杖的内容)。
建立一个私下被保护的对象时,您应该建立对象的安全描述项。可呼叫CreatePrivateObjectSecurity函数实作:
BOOL CreatePrivateObjectSecurity(
PSECURITY_DESCRIPTOR psdParentDescriptor,
PSECURITY_DESCRIPTOR psdCreatorDescriptor,
PSECURITY_DESCRIPTOR *ppsdNewDescriptor,
BOOL fIsDirectoryObject,
HANDLE hToken,
PGENERIC_MAPPING gmGenericMapping);
CreatePrivateObjectSecurity函数有一个非必要的父安全描述项与建立者描述项,这两个参数都可以为NULL。假如两者都没有提供的话,CreatePrivateObjectSecurity函数会透过所提供之权杖中找到的预设DACL而建立一个安全描述项。在预设DACL中找到的通用权利,可使用从被传递之GENERIC_MAPPING结构中找到的资讯,并将它们对应到对象的特定及标准权利。CreatePrivateObjectSecurity函数经由指派它的位址给PSECURITY_DESCRIPTOR变数,以传回新的安全描述项,您所传递的位址为ppsdNewDescriptor参数值。这是一个新的安全描述项,现在您应该把它与您的私有安全对象联系在一起。当您需要释放此函数所分配的内存时,应该传递指向想要释放之安全描述项的变数位址给DestroyPrivateObjectSecurity。
BOOL DestroyPrivateObjectSecurity(
PSECURITY_DESCRIPTOR *ppsd ObjectDescriptor );
被您初始化及传递给CreatePrivateObjectSecurity的GENERIC_MAPPING结构定义如下:
typedef struct _GENERIC_MAPPING {
ACCESS_MASK GenericRead;
ACCESS_MASK GenericWrite;
ACCESS_MASK GenericExecute;
ACCESS_MASK GenericAll;
}GENERIC_MAPPING;
您可经由将每个成员设定到每个通用权利之适当的标准及特定权利组合,以填写这个简单的结构。从权杖的预设DACL建立DACL时,系统会使用这个资讯。
说明
您不应直接修改从CreatePrivateObjectSecurity传回的安全描述项。尽管系统提供从此描述项要求特定安全性资讯的函数,您的软件应该把这个传回的指标视为黑箱,犹如安全描述项被储存在系统内存一般。
当客户端试图对软件中的安全对象作用时,首先客户端必须接受存取检查。为此,您应传递执行安全工作所需的权利、安全描述项及客户端的权杖到AccessCheck函数中。
BOOL AccessCheck(
PSECURITY_DESCRIPTOR pSecurityDescriptor,
HANDLE hClientToken,
DWORD dwDesiredAccess,
PGENERIC_MAPPING gmGenericMapping,
PPRIVILEGE_SET pPrivilegeSet,
PDWORD pdwPrivilegeSetLength,
PDWORD pdwGrantedAccess,
PBOOL pfAccessStatus);
您也必须包含此对象的GENERIC_MAPPING结构位址以及PRIVILEGE_SET结构阵列的位址。系统使用PRIVILEGE_SET结构来描述用来授予存取的权限。系统以几个实例来授予使用权限的存取权,而您应该提供够大的缓冲器以接收数个回传的权限。PRIVILEGE_SET结构定义如下:
typedef struct _PRIVILEGE_SET {
DWORD PrivilegeCount;
DWORD Control;
LUID_AND_ATTRIBUTES Privilege [ANYSIZE_ARRAY];
}PRIVILEGE_SET;
ANYSIZE_ARRAY值被定义为1,而您应该建立一个够大的缓冲器以接收有关的权限。一旦收到权限缓冲器要求的大小后,您可以呼叫AccessCheck,然后再配置一个够大的缓冲器。
说明
当您的客户端要求对象的WRITE_OWNER、READ_CONTROL、WRITE_DAC或ACCESS_SYSTEM_SECURITY的存取时,该权限可以被用来授予存取。假如这些权利没有明确地指派给您客户端的信任成员帐户,系统会为覆盖对象安全性的权限检查客户端的权杖。这种权限的一个例子为SE_TAKE_OWNERSHIP_NAME,它可让客户端设定系统中任何对象的拥有权。
您的客户端被授予的存取遮罩会经由pdwGrantedAccess参数传回,而经由pfAccessStatus参数传回的Boolean值会指出AccessCheck函数是否成功。
在实作私有对象之安全性时,AccessCheck函数是其中心,假如您的软件是安全的,而它一定会在对象执行安全工作前呼叫AccessCheck,则当您软件的客户端被允许存取时,Windows会处理剩下的细节部份。
说明
您也可以使用AccessCheckAndAuditAlarm函数建立稽核事件及私有安全对象。稽核的内容将在本章稍后讨论。
到此为止,您已经知道如何建立及删除私有安全性的方法,您也知道如何及应在何时呼叫AccessCheck函数。所以私有对象主题唯一剩下的部分即是如何修改安全描述项的元件。为了修改私人对象的安全性,首先应该取得您要调整的安全描述项元件。这通常是DACL,它可以是对象的拥有者或SACL。可使用GetPrivateObjectSecurity实作。
BOOL GetPrivateObjectSecurity(
PSECURITY_DESCRIPTOR psdObjectDescriptor,
SECURITY_INFORMATION secInfo,
PSECURITY_DESCRIPTOR psdResultantDescriptor,
DWORD dwDescriptorLength,
PDWORD pdwReturnLength);
GetPrivateObjectSecurity函数取得一个指向私有安全对象的安全描述项指标,并传回所要求的安全描述项。您可以使用熟悉的secInfo参数以指出您想撷取对象安全描述项的哪个部分。
您必须提供一个够大的缓冲器来包含持有要求资讯的安全描述项。一旦得到要求的缓冲器大小时,便可以呼叫GetPrivateObjectSecurity函数,并且再一次撷取资讯。
在您拥有对象的安全描述项后,必须修改它,然后再使用SetPrivateObjectSecurity函数重新设定修改过的安全描述项给私有对象。
BOOL SetPrivateObjectSecurity{
SECURITY_INFORMATION secInfo,
PSECURITY_DESCRIPTOR psdModificationDescriptor,
PSECURITY_DESCRIPTOR *ppsdObjectsSecurityDescriptor,
PGENERIC_MAPPING gmGenericMapping,
HANDLE hClientToken);
secInfo参数指出「修改安全描述项」中的哪些资讯已被设定到对象的私有安全描述项中。您可以传递PSECURITY_DESCRIPTOR变数的位址,它包含一个指向对象私有安全描述项的指标。SetPrivateObjectSecurity函数会释放安全描述项,并以新的安全描述项取代它,其位址从提供的指标变数中传回。您也必须传递hClientToken参数,以使系统可以确认对象拥有者的设定,及其通用的对应结构。
安全描述项的修改通常会使用以下这些步骤执行:
- 撷取对象的安全描述项。
- 建立及初始新的安全描述项(本章稍早曾示范)。
- 从原始的安全描述项复制安全性资讯到新的安全描述项。
- 修改新的安全描述项(使用遍及本章所叙述的技巧)。
- 把新的安全描述项放回私人对象。
为了取得安全描述项的个别元件,您应该使用以下的函数:即GetSecurityDescriptorOwner、GetSecurityDescriptorDacl、GetSecurityDescriptorSacl及GetSecurityDescriptorGroup。为了设定新安全描述项的内部元件,您可以使用SetSecurityDescriptorOwner、SetSecurityDescriptorDacl、SetSecurity DescriptorSacl、SetSecurityDescriptorGroup来设定。
这些函数都很相似,所以我准备展示及讨论最常见与复杂GetSecurityDescriptorDacl及SetSecurityDescriptorDacl函数。
以下是GetSecurityDescriptorDacl函数原型:
BOOL GetSecurityDescriptorDacl(
PSECURITY_DESCRIPTOR pSecurityDescriptor,
PBOOL pfDACLPresent,
PACL *pDACL,
PBOOL pfDACLDefaulted);
这个函数撷取一个指向安全描述项的DACL指标,也指出DACL是否为当前的,以及它起初是否已透过预设安全性建立。您所提供之安全描述项与Boolean值的位址会指出DACL是否为当前的,而PACL变数的位址则用来撷取DACL;另一个Boolean值位址,系统用它来指出DACL是否被设为预设值。
为了将DACL放回安全描述项,您可以使用SetSecurityDescriptorDacl函数:
BOOL SetSecurityDescriptorDacl(
PSECURITY_DESCRIPTOR pSecurityDescriptor,
BOOL bDaclPresent,
PACL pDacl,
BOOL bDaclDefaulted);
这个函数仅取得指向您想修改之安全描述项对象的指标,而Boolean值则指出DACL是否为当前的,以及它是否为预设安全性的结果(您通常会传递FALSE值)。SetSecurityDescriptorDacl函数要求一个指向DACL的指标,这个DACL会成为安全描述项的新DACL。
说明
您不该试图设定DACL或任何其他系统之任何对象传回的安全描述项元件。您应该总是建立一个新的安全描述项,并对它设定DACL及SACL元件。这是由于系统传回的安全描述项被封装至selfrelative格式的缘故。这意味着进入单一连续内存区块的资料,并没有留下可修改的空间。
当您使用InitializeSecurityDescriptor函数分配内存及初始化一个新的安全描述项时,系统会初始一个absolute格式的安全描述项。一个absolute格式的安全描述项使用了指标,以参考其他的元件,它可让元件被设定及重新设定。
RoboService范例服务程序
RoboService范例服务程序(「10 RoboService.exe」)示范如何利用私有安全对象及主从架构存取控制使用命名管道的方法。此应用程序的原始码及文件存放于随书光碟上的10-RoboService目录中。
当您使用「/install」参数启动RoboService时,应用程序会把它自己当成服务,并安装在系统上(如图10-8),然后您就可以使用任何服务控制程序启动及停止服务。同样地,如果您正在执行除错工具的服务程序,则您可以传递「/debug」参数,使服务在除错模式下执行,并绕过服务功能。
图10-8 RoboService的命令列选项
在执行服务后,您可以执行RoboClient范例应用程序,它的执行画面显示于图10-9,然后再输入执行服务的系统电脑名称。假如您不输入电脑名称的话,客户端会试图连接本机上的服务。
一旦连接成功,您将会看到服务所建立的「虚拟机器」清单。您可以增加及移除机器。使用服务端的私有安全功能保护这些机器。您可以让机器执行几个已定义的行为,包括编辑机器的安全性等等。
| 图10-9 RoboClient范例应用程序的使用者介面 |
建议您执行这个服务并从多重使用者环境下启动客户端程序,或许可以使用封装在Windows内的RunAs.exe公用程序执行。这可让您实验对象所有权及存取控制的部份。
客户端程序非常小,几乎已经把所有的功能皆委托给服务了。所有安全性的程序代码(除了ACL编辑外)皆在服务端实作。
服务使用 模拟(impersonation) 以取得每个连接客户端的权杖(关于模拟的内容在第十一章有更详细的讨论),然后服务会为来自客户端之未来的安全性要求,以及在机器上建立的使用权储存权杖。服务使用了CreatePrivate ObjectSecurity、DestroyPrivateObjectSecurity及AccessCheck,以及许多与安全性相关的函数来实作。
除了安全性功能以外,服务使用了第二章所讨论的I/O完成连接埠,有效地与客户端通讯,并为可调整的通讯实作了一个模组。
稽核与SACL
最后,我们准备开始讨论稽核和SACL的内容。与DACL不同的是,对象的SACL不影响可存取对象的人。
当对象被要求存取时,SACL会产生一个被加入事件日志的事件。特别的是,您可以增加ACEs到对象的SACL中,两种情况下会增加事件到事件日志中:
- 当任何一组存取权利的存取检查成功时。
- 当任何一组存取权利的存取检查失败时。
举例来说,您可以为NTFS分割区上包含个别SYSTEM_AUDIT_ACE的文件建立一个SACL。这表示若发生因为拒绝存取而使每次某个信任成员写入文件失败的情形时,应该由系统将此事件加到事件日志中。
Windows 2000的预设稽核选项是失效的,所以如果您想要开始编写稽核软件,应该加入稽核的能力。以下即是赋予稽核至系统的步骤:
- 使用 /a参数mmc /a,执行Microsoft管理主控台(MMC)。
- 在主控台功能表上,选择新增/移除嵌入式管理单元,然后点选新增按钮。
- 在新增独立嵌入式管理单元对话方块中,选择群组原则,然后点选新增按钮。
- 在选取群组对象的对话方块中,应选取本机电脑,然后点选完成按钮。
- 点选关闭按钮,然后再点选确定钮。此时本机电脑原则对象应该被显示在MMC中。
- 以下展示本机电脑的原则对象:本机电脑原则\电脑设定\Windows设定\安全性设定\本机原则\稽核原则。
- 从右边窗格的稽核对象存取上点滑鼠右键,然后选择安全性钮。
- 在本机安全性原则设定对话方块中,如图10-10所示,核取成功及失败核取方块,然后点选确定钮。
图10-10 授予成功及失败稽核给对象存取
在采用这些步骤之后,系统会开始增加稽核事件到安全性日志下的事件日志。
说明
网域原则可以覆盖您的本机原则,并阻止稽核被设定。您必须有网域管理的权利才能调整网域原则。
相不相信,您已经完成实作稽核最难的工作。您知道如何从我们处理DACLs的工作中建立ACEs及修改ACLs。目前为止,您已经学会应用于建立ACEs及SACLs的每件事。
以下是SACL使用的ACE结构:
typedef struct _SYSTEM_AUDIT_ACE {
ACE_HEADER Header;
ACCESS_MASK Mask;
DWORD SidStart;
}SYSTEM_AUDIT_ACE;
这个结构看起来应该很熟悉,因为它与我们已经讨论过的ACCESS_ALLOWED_ACE及ACCESS_DENIED_ACE结构完全一样。ACE_HEADER结构的AceFlags成员通常会包含继承资讯,稽核ACEs也确实为真。而您也该含入SUCCESSFUL_ACCESS_ACE_FLAG及FAILED_ACCESS_ACE_FLAG稽核标记的其中一或两个,并分别指出成功及失败的存取稽核。
为了修改对象的SACLs,可以使用您已经熟悉的函数,例如InitializeAcl及AddAce。为仍旧使用GetSecurityInfo及SetSecurityInfo处理的系统对象取得及设定SACL使用几乎相同的方式,您可以将它们使用在DACL上。建立一个SACL比建立DACL稍微简单,由于SACL中的ACEs顺序在对象上没有影响,因此您可以用任何方便的顺序增加ACEs。
说明
处理DACLs时要注意一个差异点,那就是信任成员不能设定对象的SACL,除非它拥有SE_AUDIT_NAME权限。
设计存取控制程序应考虑的因素
您现在已经了解什么样的存取控制是合适的,以及如何在您自己的软件中实作存取控制,然而您还要学习更多,以成为一个成功的安全性程序设计师-Windows存取控制的弹性使安全性开发人员因为太多规则而绑住了自己。本节会讨论一些在做安全性程序开发时应考虑的议题,在此对您的劝告是尽早计划您服务的存取控制,提前做计划会替您省去大量的麻烦。
内存管理
和在百分之九十的应用层程序设计不同的是,安全性程序设计需要大量的缓冲器管理机制,包括分配及移动内存内的结构。尽管这是许多开发人员在学校专注的部分,但它并不是Windows开发中常见的工作。
在处理安全性时,您应该提供简化内存管理的方法,因为有很多函数皆要求分配及重分配暂时缓冲器。以下是一些秘诀:
- 考虑使用不需要释放内存的函数,例如_alloca。
- 考虑编写一个类别,例如CautoBuf类别,此类别在附录B中讨论(您也会在附录B中找到范例应用程序),这个类别会自动稽核它的缓冲器,并在它离开范围时释放它自己。
- 避免使用static缓冲器;它们是懒惰程序设计师的技巧,最后将会破坏您的应用程序。
- 考虑手边的工作,并设计一个尽可能要求少数缓冲器分配的方法。尽可能在开始演算时放置大量的缓冲器分配机制。
- 在您使用后利用结构化的例外处理清理它。
重复利用的程序代码
编写越多的程序代码,就会有越多的错误。就存取控制程序来说,这绝对是真的。试着以服务的不同观点来设计彼此一致的安全性,以使您依据某些样式及重复使用函数,这种函数越多越好。重复使用的机制会大大地减轻侦错程序代码的工作。
最后,我无法充分的表达出用一组小的C++ 类别实作您的安全性程序代码是多么有利,这么做会使您换取重复使用的程序代码及封装(Encapsulation),并且可以大大地简化存取控制这类的工作。
保持简单
简单的安全性往往是最好的安全性。假如一个对象拥有许多ACEs,您的软件及使用者要监视确实有存取对象的存取时可能会有困难。假如对象只有一些简单的ACEs,则软件和使用者会很清楚谁做了存取的动作。一个被系统拥有且只有一个空的DACL对象(不是NULL DACL)和从Windows中取得的对象一样简单及安全。增加一个个别的ACE可让信任成员存取及对象仍旧是安全的,并且可以很清楚谁拥有对象的存取权。
试着只使用允许存取的ACEs来实作大量的存取控制。请记得除非明确地允许存取权,否则即表示它被暗示性地拒绝。若您使用允许存取ACEs及拒绝存取ACEs时,即表示允许群组及个人的存取权以及拒绝个人及较小群组的存取权。这样一来,您将会发现自己不必为群组中的个别使用者覆盖群组的拒绝存取ACE。所以尽量使它保持简单以避免存取控制产生漏洞。
使用预设安全性及继承的安全性
假如有可能的话,请使用预设安全性及继承的安全性。假如您的服务可以避免在个别对象上操作ACLs,这样会很好!许多服务可以只建立个别的DACL并将它设定为服务的预设DACL(第十二章有更详细的讨论)。
由服务建立的对象可以使用预设安全性,以避免为每个建立的对象产生一个DACL的需求。这个方法可以大大地简化必须建立许多安全对象的服务。
说明
继承安全性的对象不使用来自预设DACL的ACEs。您会发现如果要使用子对象的预设安全性时,必须保护父目录或登录机码的安全描述项。
继承的安全性是另一个取得免费DACL的方法。您可以设定文件结构(或登录树)中个别父对象上的DACL,在节点下建立的每个对象皆可以继承全部DACL的ACEs。这是一个非常有效的技巧,它可以使您的程序代码免于建立大量的个别DACLs。
说明
不可继承ACEs的这些对象本身即可以拥有可继承的ACEs。所以您可以建立一个受保护的根目录或登录机码,以避免使用继承的ACEs,而对象的ACEs可以为下面的每个对象定义继承的安全性。这是一个在文件及登录机码上实作继承安全性之常见且有效的策略。




浙公网安备 33010602011771号