除了系统需要维持身分识别外,安全的服务也必须能够维持它的客户端身分识别。您的服务软件应有两个如下所述的目标:
- 识别连接到您服务的客户端。
- 确保服务为连接到您服务的客户端执行 合适 的程序代码,不管客户端产生何种要求。
本章会叙述可以让您用来开发服务的特色,使Windows安全性符合这两个目标。
认识使用者环境
系统在权杖(Token)中维持使用者环境资讯(在第十章中介绍过)。在讨论系统如何使用权杖维持使用者身分识别之前,我们先来讨论系统如何建立权杖的部份。
验证与权杖内容
当您登入一个执行Microsoft Windows 2000的工作站或伺服机器时,通常会输入一个使用者名称及密码。使用者名称是一个信任成员帐户,存放在网域控制站的Active Directory中,或是由一个本地端机器上之安全性资料库所维持的本机使用者帐户。密码则是使用者帐户的 凭证 ,系统用它来 验证 登入者是否合法。
使用者登入系统后,系统会开始收集有关使用者帐户的资讯(除非使用者名称或密码不正确,此种情况表示登入失败)。假如使用者帐户为网域帐户,则系统会收集网域等级的资讯,例如使用者为网域群组的成员。不管登入的帐户是否为网域帐户,系统皆会收集与本地端机器有关的使用者资讯,例如使用者为本机群组的成员(包括经由本机群组成员之网域群组的巢状成员资格)。系统会建立一个被指派给使用者帐户的权限清单,以及所有使用者的群组帐户清单。所有的资讯皆会被编译为系统核心物件内维持的资料结构,也就是权杖。
就Windows安全性而言,在系统验证登入尝试后,权杖是使用者的身分识别。这个身分识别由使用者信任成员帐户的安全识别码(SID)及表格11-1显示之所有资讯构成(有关SIDs及信任成员帐户的资讯,请参阅
如您所见,权杖中维持着大量的资讯。您可以把权杖视为一个非常详细的「虚拟安全性标志」,权杖使用者SID是印刷在标志上的名称,而群组SIDs则是印刷在标志上的成员资格资讯。权杖中的权限指出某些广泛的系统权利,其馀资料则是主要资讯。在您每次企图存取系统中的安全元件时,它就会参考这个虚拟标志。
权杖及执行程序代码
截至目前为止已经说明了如何建立权杖(一般的规定)及权杖中储存的资讯。现在我必须阐明如何用权杖中的资讯来确保程序代码可被安全地执行。
Windows 2000中的每个程序皆拥有各别的权杖,如您所知,这个权杖即是处理程序的使用者环境。当一个处理程序呼叫CreateProcess时,系统会产生呼叫程序的权杖副本,并把新的权杖与新的处理程序联系在一起。此种方法使您的使用者环境可以传播到整个系统,从您一登入系统到最后的处理程序拥有其中一个权杖为止。在您每次登入系统时,系统便会为您的使用者环境建立一个权杖,然后再启动Windows Explorer,并把权杖与Explorer的程序联系在一起。每次您启动来自Explorer的程序时,新的处理程序就会继承一个原始权杖的副本。直到您的每个处理程序结束,而且Explorer处理程序已离开时,才登出系统、关闭您的使用者环境之最后权杖物件的handle。
表格11-1 权杖的内容 |
权杖资讯 | 说明 |
---|---|
权杖使用者SID | 一个代表使用者帐户的SID,为它建立权杖。这个帐户通常被称为 权杖使用者 。 |
权杖群组SIDs | 群组的SIDs,权杖使用者是群组的成员。这包括内建帐户,例如Everyone及受验证的使用者。 |
登入SID | 在验证时建立的唯一SID。这个SID从其他登入工作阶段以唯一的识别登入本工作阶段,即使其他工作阶段为相同的权杖使用者。本章稍后将讨论更多登入SID的细节。 |
权限 | 由权杖使用者及群组信任成员帐户持有的权限清单。在第九章曾讨论过权限的细节,本章稍后也有讨论。 |
预设拥有者SID | 一个SID,指出在此权杖下执行的程序代码使用预设安全性建立物件的拥有者。在第十章曾讨论预设安全性的内容,本章稍后也有讨论。 |
预设群组SID | 一个SID,指出在此权杖下执行的程序代码使用预设安全性建立物件的主要群组。第十章曾讨论预设安全性的内容,本章稍后也有讨论。 |
预设DACL | 一个SID,指出在此权杖下执行的程序代码使用预设安全性建立物件的判别存取控制清单(DACL)。第十章曾讨论过预设安全性的内容,本章稍后也有讨论。 |
权杖来源 | 一个由建立权杖之系统元件所设定的8个字元「来源名称」。 |
权杖类型 | 权杖的类型可以是主要或者模拟的类型,在本章稍后有更详细的讨论。 |
限制SIDs | 选择信任成员帐户用来为在此权杖下执行的程序代码限制存取。受限的SIDs在本章稍后有详细的讨论。 |
模拟等级 | 权杖的模拟等级指出服务器可充当客户端的等级,本章稍后亦有讨论。 |
安全描述项 | 权杖是Windows中的安全物件,而且就如同所有的安全物件,它拥有一个安全描述项以控制对物件的存取。 |
凭证资讯 | 某些权杖有凭证资讯,这些权杖可以扮演网路和本地端机器上的权杖使用者。凭证资讯在本章各处有详细的讨论。 |
当您程序中的任何线程执行一个需要了解安全性的系统函数时,系统会检查该程序的权杖资讯。这意味着每次您向系统要求安全物件的handle时(例如登录机码或Mutex(互斥)),系统就会执行存取检查,并查询权杖中必需的资讯。同样地,每次您呼叫例如 GetUserName 的函数或任何其他必需识别执行程序代码之使用者环境函数时,系统会从权杖中撷取资讯。
尽管您可以为处理程序撷取权杖物件的handle,但是为处理程序设定权杖是不可行的(实际上,系统有权为程序设定权杖,但是并不让开发人员经由API使用)。所以在处理程序结束之前,它无法摆脱权杖,或身分识别。Windows的情形也是一样,然而它提供一个非常棒的特色可让您改变在其之下的执行程序代码权杖,称为 模拟 。
模拟的概述
模拟是联系权杖与处理程序中执行的线程所实作的。当您如此做时,此线程(大部分)会用它的 模拟权杖 来呈现身分识别。这个线程被称为「模拟」一个使用者或一个安全性环境。处理程序中的所有其他线程将会继续执行代表处理程序权杖的程序代码,除非它们也模拟了另一个使用者环境。当线程不再需要被视为另一个使用者而工作时,就可以回复到标准状态,这会导致线程再次变回原样并使用处理程序的权杖。如您所想像,模拟在主从架构环境中是非常方便的,在这个环境中,服务器使用不同的安全性环境执行代表客户端的工作。
在继续之前,应该先阐明我的主张,即一个线程(大部分)具有与它的模拟权杖相关之身份识别。通常,在您呼叫要求权杖资讯的函数时,系统会检查线程权杖,如果没有找到,系统会预设为处理程序的权杖。由于线程并非天生就拥有权杖,所以处理程序的权杖即是处理程序原始的安全性环境。然而,在某些个案中,系统函数仍旧会使用处理程序的权杖,甚至是在模拟权杖之下执行呼叫线程的情形。
例如,一个模拟线程呼叫CreateProcess在系统中建立一个新的处理程序,此时新的处理程序会继承与呼叫与处理程序关联的handle,而不是与呼叫线程关联的模拟权杖。此外,任何要求SE_TCB_NAME或SE_ AUDIT_NAME权限的函数(本章稍后至少会讨论一个,即LogonUser)使用程序的权杖而不是检查模拟权杖的线程。本章稍后将会有更多关于模拟的讨论。
您现在可以了解当您登入时,系统如何为您建立权杖。您知道系统如何在您的权杖(或者安全性环境)之下执行程序代码,也知道线程可以使用模拟暂时地呈现另一个身分识别。然而,截至目前为止,我们只讨论过因使用者帐户登入而建立的权杖,例如「JClark」或是「v-JeffrR」。现在您要开始学习在使用者帐户下执行的程序代码如何取得它的权杖。
本机(LocalSystem)帐户
假如您读过第叁章,就应该知道不管使用者是否以互动的方式登入系统皆可以设定让服务执行。您也可以设定让服务像使用者帐户一样地执行,而且要求您输入帐户名称及信任成员帐户的密码。我们可以轻易想像系统秘密地登入这个使用者的情形,以及当执行服务程序代码的程序执行时建立一个权杖来使用。然而,您也可以设定让服务像本机帐户一样的执行,而不需要帐户名称或密码。
本机帐户是Windows 2000系统上的特殊帐户,它不理会服务及其他程序,而就像「操作系统的一部份」一样地执行。结果,本机帐户中的任何程序皆会被指派给权杖,它确实非常有效。以下是一些本机帐户的特性:
- 本机帐户几乎拥有系统中指派的每个权限。
- 本机帐户是内建管理者群组的暗示性成员,而且本身拥有管理者的所有存取权。
- 假如本机帐户没有明确地被授予物件的存取权,则必要时,它可以一直取得物件的拥有权及修改它的DACL。
- 本机帐户拥有SE_TCB_NAME或者「扮演操作系统一部份」权限,所以它可以登入使用者(以获得权杖),并且任意地执行代表使用者的程序代码。
- 本机帐户拥有本地端机器上每件事的直接或间接存取权。
说明
Microsoft Windows NT里的本机帐户经历了一个讽刺的生活-它拥有本地端机器的无限制存取权,而且几乎没有存取网路上其他机器的安全物件。原因是某台机器的本机帐户在另一台机器上没有身分识别(同样的方法,某台机器的管理者帐户在另一台机器上没有意义)。所以其他机器上的物件当然会被保护,以预防本机帐户的存取。
这个限制已经受到注意,然而在网路环境中,网域控制站是使用Active Directory执行Windows 2000的服务器。本机帐户现在于Active Directory中拥有信任成员帐户的状态,因此可以给予网域中其他机器上安全物件的权利。
本机帐户现在没有个别机器被用来做它的监狱。这个改变大大地增加了本机帐户的弹性,但是也出现了潜在的安全性风险,必需被严密地管理。
注意一些提供权杖操作的函数和其他使用者环境—相关的活动通常需要只授予本机帐户的权限。这不是个真实的问题,因为您的服务很有可能在本机帐户中执行。然而,当您正在测试程序代码及试验函数时,例如LogonUser及CreateProcessAsUser(两者在本章稍后都有讨论),这种权限需求会是个麻烦。我们将讨论处理权限议题的方法,以及稍后将讨论其他由本机帐户程序代码引起的问题。
使用者环境及存取控制
尽管前面的章节专注于研究存取控制的部份,而只花了很少的时间集中在使用者环境的重要性。您的处理程序(或者线程)权杖所代表的安全性环境,对存取控制的函数来说绝对是必要的。
每次系统执行存取检查时,它会在物件的DACL中查看符合使用者SID以及权杖中的群组SIDs之存取控制项目(ACEs)。假如您没有明确地允许读取或修改物件的安全性,却要求允许这样做时,系统会比较物件拥有者SID与您权杖中的SIDs,并寻找相配的一组。假如有找到,您会被授予存取权,因为您是物件的拥有者。
如您所见,存取控制完全依赖于权杖。事实上,我们将讨论您如何利用杖严重的影响您的软件及系统中存取控制的方法。愈深入了解这些元件如何一起运作以及在Windows 2000下实作安全性,将会使您确实成为强大的安全性开发人员。
程序设计使用者环境
我们将学习一些强大且有趣的技巧,它可以经由操作Windows 2000的权杖而执行。第一个步骤是取得权杖的handle。让我们经由获得处理程序权杖的handle来开始我们的旅程:
BOOL OpenProcessToken( HANDLE hProcessHandle, DWORD dwDesiredAccess, PHANDLE phTokenHandle);
OpenProcessToken函数会撷取处理程序权杖的handle。hProcessHandle参数是我们想要开启权杖的处理程序handle。不幸地,系统并不让您传递NULL值,所以请指出您要求撷取自己的处理程序权杖之handle。迅速的呼叫GetCurrentProcess传回一个可被传递作为hProcessHandle参数的虚拟handle。dwDesiredAccess参数指出如何使用权杖。您应该只要求程序代码需要的权利。表格11-2叙述可用的权杖存取权利。许多表格中列出的函数,将在本章稍后讨论。
这个变数的位址被您传递作为phTokenHandle,它收到要求的权杖handle。假如OpenProcessToken的传回值是TRUE,则表示此函数执行成功;否则表示失败,这种实例应该呼叫GetLastError以得到更多资讯。
表格11-2 权杖存取权利 |
值 | 叙述 |
---|---|
TOKEN_ADJUST_DEFAULT 当呼叫SetTokenInformtion | (简短地讨论)改变权杖的特色时要求,例如预设的拥有者、主要的群组或是预设的DACL。 |
TOKEN_ADJUST_GROUPS | 在呼叫AdjustTokenGroups中要求。 |
TOKEN_ADJUST_PRIVILEGES | 在呼叫AdjustTokenPrivileges中要求。 |
TOKEN_ADJUST_SESSIONID | 要求调整权杖的工作阶段ID以及SE_TCB_NAME权限。 |
TOKEN_ASSIGN_PRIMARY | 在呼叫CreateProcessAsUser中使用权杖时要求。 |
TOKEN_DUPLICATE | 要求复制权杖。 |
TOKEN_EXECUTE | 等于STANDARD_RIGHTS_EXECUTE。有关标准权利更进一步的讨论,请参阅 |
TOKEN_IMPERSONATE | 要求与ImpersonateLoggedOnUser一起使用这个权杖。 |
TOKEN_QUERY | 要求读取任何的权杖资讯,除了使用GetTokenInformation读取它的来源外。 |
TOKEN_QUERY_SOURCE | 要求使用GetTokenInformation读取权杖的来源。 |
TOKEN_READ | 结合STANDARD_RIGHTS_READ及TOKEN_QUERY。有关标准权利之更进一步讨论,请参阅 第十章 。 |
TOKEN_WRITE | 结合STANDARD_RIGHTS_WRITE、TOKEN_ADJUST_ PRIVILEGES、TOKEN_ADJUST_GROUPS及TOKEN_ADJUST_DEFAULT。有关标准权利之更进一步讨论,请参阅 第十章 。 |
TOKEN_ALL_ACCESS | 完整的存取权杖,结合了所有的权利。 |
OpenProcessToken执行失败的可能原因是存取权利不够;然而,假如您的服务在本机帐户使用者环境中执行,或许它对任何程序的权杖皆会拥有足够的存取(有关存取控制之更多资讯,请参阅
权杖物件是一个核心物件,而且跟最核心的物件一样,当您结束使用资源时,应该传递权杖物件的handle到CloseHandle中。
说明
当您呼叫OpenProcessToken时收到的handle即是权杖的handle,它会影响该处理程序的使用者环境。调整权杖时所做的任何事,皆会严重且立即影响撷取权杖的处理程序之安全行为。稍后马上会讨论到可对权杖做些什么。有关如何获得处理程序handle的资讯(除了当前的程序外),请参阅《Programming Applications for Microsoft Windows, Fourth Edition (Jeffrey Richter, Microsoft Press, 1999)》一书之第二十二章有关OpenProcess的讨论。
假如您认为一定也有一个获得线程权杖的方法(假定线程在那时是模拟的),那么您是正确的。您可以使用OpenThreadToken:
BOOL OpenThreadToken( HANDLE hThreadHandle, DWORD dwDesiredAccess, BOOL fOpenAsSelf, PHANDLE phTokenHandle);
注意,OpenThreadToken与OpenProcessToken非常相似,除了第一个参数是线程而非处理程序的handle外,它拥有附加的参数fOpenAsSelf。这个参数向系统指出您想要 为谁 开启权杖,让我来做个说明。
请记住当您呼叫OpenThreadToken时,会要求一个Windows中的安全物件-即权杖。这意味着您可能或不可能拥有物件的存取权。您也应该记住除了处理程序的安全性环境外,也能够在可以被模拟成安全性环境的线程中执行。因为当它为模拟时,撷取自己的权杖handle之线程是非常常见的,系统可根据您的处理程序权杖指出开启一个线程权杖在执行时所需的所有存取检查。假如您想要对程序权杖执行存取检查时,应该传递TRUE给OpenThreadToken的fOpenAsSelf参数。假如您想对您的线程模拟权杖执行存取检查时,应该传递FALSE。
假如您的线程现在没有模拟,这些参数就没有意义(本章稍后将讨论更多模拟的细节)。
就像OpenProcessToken一样, OpenThreadToken的phTokenHandle参数会传回要求的权杖handle。假如OpenThreadToken的传回值为TRUE,表示此函数成功;否则函数失败,您可以呼叫GetLastError得到更多资讯。
假如线程没有模拟时,OpenThreadToken函数将失败而且GetLastError将传回ERROR_NO_TOKEN。
您现在拥有了权杖的handle。您可能会问,可以对它作什么事呢?请继续读下去。
读取权杖资讯
表格11-1列出的大部分权杖资讯也可以从权杖中读取,假定呼叫的程序代码拥有对物件的TOKEN_QUERY(或是TOKEN_QUERY_SOURCE)存取权。您应该使用GetTokenInformation函数找出权杖的内容:
BOOL GetTokenInformation( HANDLE hTokenHandle, TOKEN_INFORMATION_CLASS tokenInformationClass, PVOID pTokenInformation, DWORD dwTokenInformationLength, PDWORD pdwReturnLength);
您应该传递权杖的handle为hTokenHandle参数。 tokenInformationClass参数指出您想从权杖获得的资讯,而pTokenInformation参数是指向缓冲器的指标,缓冲器填满要求的资讯。dwTokenInformationLength参数指出您传递的缓冲器长度,而pdwReturnLength参数指向一个收到撷取资讯必需的缓冲器大小的变数。
依据tokenInformationClass参数,经由pTokenInformation参数指向多种的缓冲器型别。以下的清单叙述您可从权杖中撷取的资讯:资讯类别值及使用的资料类型(TOKEN_INFORMATION_CLASS也与SetTokenInformation函数一起使用,在本章稍后讨论)。
-
- TokenUser 传回权杖使用者的SID。登入系统时,帐户名称被用来建立权杖的使用者(这个值不与SetTokenInformation一起使用)。
typedef struct _TOKEN_USER { SID_AND_ATTRIBUTES User; }TOKEN_USER;
- TokenUser 传回权杖使用者的SID。登入系统时,帐户名称被用来建立权杖的使用者(这个值不与SetTokenInformation一起使用)。
- TokenDefaultDacl 用来读取或设定权杖的预设DACL。有关预设DACLs之更进一步讨论以及建立DACL的详细讨论,请参阅 第十章 的内容。在DACL中设定资讯时需要拥有TOKEN_ADJUST_ DEFAULT存取权利。
typedef struct _TOKEN_DEFAULT_DACL { PACL DefaultDacl; }TOKEN_DEFAULT_DACL;
- TokenOwner 用来读取或设定权杖物件的预设拥有者(有关物件所有权的讨论,请参阅 第十章 的内容)。TOKEN_ADJUST_DEFAULT存取权利对设定权杖的拥有者来说是必要的。
typedef struct _TOKEN_OWNER { PSID Owner; }TOKEN_OWNER;
- TokenPrimaryGroup 读取或设定权杖的主要群组。在呼叫SetTokenInformation时,会要求TOKEN_ADJUST_DEFAULT存取权利。
typedef struct _TOKEN_PRIMARY_GROUP { PSID PrimaryGroup; }TOKEN_PRIMARY_GROUP;
- TokenGroups 传回与权杖关联的群组SIDs(这个值不与SetTokenInformation一起使用)。
typedef struct _TOKEN_GROUPS { DWORD GroupCount; SID_AND_ATTRIBUTES Groups [ANYSIZE_ARRAY ]; }TOKEN_GROUPS;
- TokenPrivileges 传回与权杖关联的权限(这个值不与SetTokenInformation一起使用)。
typedef struct _TOKEN_PRIVILEGES { DWORD PrivilegeCount; LUID_AND_ATTRIBUTES Privileges [ANYSIZE_ARRAY ]; }TOKEN_PRIVILEGES;
- TokenSource 传回权杖的来源。这是一个表示建立权杖项目的文字字串。TOKEN_QUERY_SOURCE存取权利对撷取这个资讯来说是必要的(这个值不与SetTokenInformation一起使用)。
typedef struct _TOKEN_SOURCE { CHAR SourceName [8 ]; LUID SourceIdentifier; }TOKEN_SOURCE;
- TokenType 传回权杖的类型。可能的值为TokenPrimary及Token Impersonation。GetTokenInformation的pTokenInformation参数将传回一个个别的TOKEN_TYPE指出权杖的类型(这个值不与SetTokenInformation一起使用)。
typedef enum _TOKEN_TYPE { TokenPrimary =1, TokenImpersonation }TOKEN_TYPE;
- TokenImpersonationLevel 传回模拟等级。更多相关资讯请参阅 表格11-3 (这个值不与SetTokenInformation一起使用)。
typedef enum _SECURITY_IMPERSONATION_LEVEL { SecurityAnonymous, SecurityIdentification, SecurityImpersonation, SecurityDelegation }SECURITY_IMPERSONATION_LEVEL;
- TokenStatistics 传回有关权杖的一般资讯。它的成员包括GroupCount、PrivilegeCount及ModifiedId。ModifiedId成员是本机唯一识别元(LUID),每当权杖被修改时就会改变。您的程序代码可以使用这个值来检查自从上次检查后,权杖是否已被改变。这种侦查能力在编写呼叫到协力厂商DLLs或程序库的容错程序代码时非常有帮助(这个值不与SetTokenInformation一起使用)。
typedef struct _TOKEN_STATISTICS { LUID TokenId; LUID AuthenticationId; LARGE_INTEGER ExpirationTime; TOKEN_TYPE TokenType; SECURITY_IMPERSONATION_LEVEL ImpersonationLevel; DWORD DynamicCharged; DWORD DynamicAvailable; DWORD GroupCount; DWORD PrivilegeCount; LUID ModifiedId; }TOKEN_STATISTICS;
- TokenRestrictedSids 传回权杖的受限SIDs。本章稍后将讨论受限权杖的细节(这个值不与SetTokenInformation一起使用)。
typedef struct _TOKEN_GROUPS { DWORD GroupCount; SID_AND_ATTRIBUTES Groups [ANYSIZE_ARRAY ]; }TOKEN_GROUPS;
- TokenSessionId 指出终端服务器权杖的工作阶段ID。GetTokenInformation的pTokenInformation参数将传回一个DWORD值。假如终端服务器没有安装在本地端机器或权杖与终端服务器主控台关联的话,DWORD的值会是0。若权杖与终端服务器的客户端关联,则DWORD的值是客户端的工作阶段ID(这个值不与SetTokenInformation一起使用)。
如同前面清单显示,您可以从权杖中读取大量的资讯。以下是从权杖撷取的最常见项目:
- 权杖使用者SID 权杖所代表的使用者帐户。通常会从权杖收到这个资讯,并找出谁正在执行程序代码。您可以传递这个SID到LookupAccountSid中(在第九章讨论),以取得使用者帐户的文字名称。
- 登入SID 这个SID与权杖群组一起掩藏,所以会花费一些时间挖掘,以取回它(我们将会马上讨论)。然而,登录SID对于唯一地识别工作阶段是非常方便的。假如某位使用者不只一次登入执行Windows 2000的机器(以互动式或者透过其他方法)时,系统会为每个工作阶段建立一个唯一的登入SID,不管权杖使用者从一个工作阶段到另一个工作阶段是否相同。
- Token groups 对找出与权杖关联的群组很有帮助。然而,假如您想找出权杖是否有个别的群组,可以使用CheckTokenMembership函数。
- Token default DACL 建立物件时,假如您传递NULL给安全性属性时,对找出最近建立物件的DACL很有帮助。
从权杖撷取资讯时,您通常必需呼叫GetTokenInformation,以找出被要求的缓冲器长度,然后再次呼叫它,实际撷取资讯。为了产生真正的容错程序代码,您应该有多次呼叫GetTokenInformation的准备,直到您已经成功地撷取要求的资讯为止。这是因为权杖资讯的大小(例如预设DACL)可以在您呼叫GetTokenInformation撷取缓冲器大小之间,以及您在呼叫GetTokenInformation撷取实际资料时改变。这种多重线程程序设计的条件即是竞赛条件(race condition),而且会导致难以发现的错误,这些错误大约每几个月才出现一次。
以下的函数显示如何正确地呼叫GetTokenInformation,以撷取有关权杖的资讯。它也传回使用LocalAlloc分配之缓冲器里面的权杖资讯。假如您在程序代码中使用了这个函数,则当您用完缓冲器时,应该使用LocalFree释放传回的缓冲器。
LPVOID AllocateTokenInformation(HANDLE hToken, TOKEN_INFORMATION_CLASS tokenClass ){ PVOID pvBuffer =NULL; __try{ BOOL fSuccess; //初始缓冲器大小 ULONG lSize =0 ; do { //我们拥有缓冲器大小了吗? if (lSize !=0) { //我们已经拥有缓冲器吗? if (pvBuffer !=NULL) LocalFree(pvBuffer);//Then free it //分配新的缓冲器 pvBuffer =LocalAlloc(LPTR,lSize); if(pvBuffer ==NULL) __leave; } //再试一次 fSuccess =GetTokenInformation(hToken,tokenClass, pvBuffer,lSize,&lSize ); //缓冲器仍旧不够? }while(!fSuccess &&(GetLastError()== ERROR_INSUFFICIENT_BUFFER)); //假如为了某些其他原因而失败,退出 if(!fSuccess) { if(pvBuffer) LocalFree(pvBuffer); pvBuffer =NULL; } }__finally{} //传回本地端分配的缓冲器 return (pvBuffer); }
以下的程序代码片段,显示使用此函数撷取权杖之使用者SID及当前处理程序的预设DACL。
HANDLE hToken; if(!OpenProcessToken(GetCurrentProcess(),TOKEN_QUERY,&hToken)){ //错误 } TOKEN_USER*ptUser =(TOKEN_USER*)AllocateTokenInformation(hToken, TokenUser); if (ptUser !=NULL){ //对经由ptUser->User指向的SID作些事情 } TOKEN_DEFAULT_DACL*ptDACL = (TOKEN_DEFAULT_DACL*)AllocateTokenInformation(hToken, TokenDefaultDacl); if (ptDACL !=NULL){ //对经由ptDACL->DefaultDacl指向的DACL作些事情 }
请注意,有一些经由GetTokenInformation传回的结构,使用了SID_AND_ATTRIBUTES结构,其定义如下:
typedef struct _SID_AND_ATTRIBUTES {
PSID Sid; DWORD Attributes; }SID_AND_ATTRIBUTES ;
这个结构包括一个信任成员帐户的SID及一位属性 成员,它包括有关SID的资讯。有关包含一个SID_AND_ATTRIBUTES结构的特殊结构,其属性 成员的含义请参阅《Platform SDK》文件。
以下的函数,AllocateTokenLogonSID,使用AllocateTokenInformation范例函数撷取权杖的群组SIDs,然后反覆浏览SIDs,以找到本节先前讨论的登录SID。这个函数经由检查属性 成员的SE_GROUP_LOGON_ID标记找到登录SID。应该使用LocalFree释放传回的PSID。
PSID AllocateTokenLogonSID(HANDLE hToken){ PSID psidLogon =NULL; TOKEN_GROUPS*ptGroups =NULL; __try{ //取得权杖群组 ptGroups =(TOKEN_GROUPS*) AllocateTokenInformation(hToken,TokenGroups); if (ptGroups ==NULL) __leave; //找到登录SID int nCount =ptGroups->GroupCount; while (nCount--){ if ((ptGroups->Groups [nCount ].Attributes &SE_GROUP_LOGON_ID) !=0) break; } if (nCount ==-1) __leave;//No logon SID found //为传回SID的取得内存 ULONG lLen =GetLengthSid(ptGroups->Groups [nCount ].Sid); psidLogon =(PSID)LocalAlloc(LPTR,lLen); if (psidLogon ==NULL) __leave; //复制登录SID if(!CopySid(lLen,psidLogon,ptGroups->Groups [nCount ].Sid)){ LocalFree(psidLogon); psidLogon =NULL; __leave; } }__finally{ if (ptGroups !=NULL) LocalFree(ptGroups); } return (psidLogon); }
TokenMaster范例应用程序
TokenMaster范例应用程序(「11 TokenMaster.exe」)示范与权杖相关之系统函数的使用,包括GetTokenInformation及SetTokenInformation。范例应用程序的原始程序代码及文件存放在随书光碟的11-TokenMaster目录中。这个程序可让您从四个来源之一获得权杖:处理程序、线程、使用者凭证或者经由复制。这个程序也可让您检视及修改权杖资讯。当使用者执行TokenMaster时,会出现如图11-1中的对话盒画面。
图11-1 TokenMaster范例应用程序的使用者界面 |
这个范例的原始程序代码将能帮助您了解如何运用本章讨论过的观念。作为TokenMaster范例应用程序的使用者,您可以浏览Windows 2000中的许多使用者环境功能。在讨论某些编写程序的技巧之前,会先叙述TokenMaster的部份。
当TokenMaster被执行时,它的第一个工作即是检查它自己的使用者身分识别。假如它在本机帐户之外的任何帐户下执行,会进行以下步骤,试图提升自己:
- 列举系统中执行的处理程序,并且找出系统处理程序。
- 使用OpenProcessToken获得系统处理程序权杖的handle。
- 使用CreateProcessAsUser(在本章稍后讨论)在本机帐户使用者环境下再执行它自己。
- 假如这叁个步骤都成功,则TokenMaster会知道存在于它本身之中的一个新的且更强大的实例已经被启动。
假如您在本机帐户下执行了范例应用程序,则TokenMaster的特色对您有更大的教育意义,但是执行前叁个步骤时,需要特定的权利。假如您对执行TokenMaster所有的潜在功能有兴趣,必须作以下步骤:
- 以系统的管理者登录。
- 使用第九章的TrusteeMan范例应用程序或者Microsoft管理主控台(MMC)群组原则嵌入式管理单元,增加「增加配额」及「更换程序层Token」权限到您的使用者帐户中。
- 登出,然后再次登录,以使新的权限生效。
- 启动TokenMaster。
假如您执行成功,当您执行TokenMaster时,应用程序中的状态视窗应该会包含以下的讯息:「Token Master, Status - Token Master running as SYSTEM」。否则视窗将会指示TokenMaster应以您用来启动应用程序的帐户身分执行。
应用程序的主要函数是检视有关权杖的资讯,以及修改可以被改变的资讯。以下是获得权杖的方法:
- 从现行的处理程序或线程中开启一个权杖 您可以经由使用Processes及Threads清单方块中选择一个处理程序及附加的线程(假如您对开启一个模拟权杖有兴趣)。点选名称为OpenProcessToken或OpenThreadToken的按钮以抓取权杖。
- 经由用使用者的凭证从系统撷取一个处理程序 您可以输入一个使用者名称及密码,以及选择一个登入类型及提供者。TokenMaster使用LogonUser(在本章稍后讨论)从系统撷取权杖。
- 复制一个现存的权杖 假如您已经由TokenMaster中获得一个可使用的权杖,则您可以点选DuplicateTokenEx按钮来复制权杖。这个复制选项可让您设定模拟等级和新权杖的权杖类型。假如TokenMaster中有一个可被复制的权杖,则权杖资讯视窗将会显示权杖资讯。
在您取得一个权杖后,您将会在Token Information视窗中看到权杖资讯清单。您可以经由使用GetTokenInformation函数,以检视所有可取得的权杖资讯。
使用TokenMaster,您可以使用多种方法来修改及调整权杖:
- 调整权杖群组及权限,包括使个别项目启动或失效的能力。
- 修改权杖的预设DACL。有关预设DACLs的更多资讯,请参阅
- 建立一个受限的权杖。
- 使用权杖来启动可执行档。这是TokenMaster的一个特别有用的特色。您可以经由使用TokenMaster修改过的权杖建立一个新的处理程序。这可让您改变权杖或者以某些方法限制它,然后再启动程序代码,看看程序如何受影响。
强烈地建议您花些时间使用TokenMaster范例应用程序,熟悉有关Windows使用者环境的特色。
您也将发现TokenMaster可以被用来减轻侦错的处理程序。例如,我常常从本机帐户处理程序中启动应用程序以及「窃取」权杖。然后再使用这个权杖执行Microsoft Visual Studio开发环境,就可以从本机使用者帐户编译及测试程序代码。就安全性而论,在一个服务中执行类似的测试程序代码时,应使用本机帐户权杖。
这个范例应用程序的原始程序代码中示范了一些有用的程序设计策略。首先,它几乎呼叫了本章所涵盖的每个系统函数。如果您对Windows的安全性程序设计没有经验,它也提供一些对您可能有帮助的秘诀。
Windows安全性函数时常需要配置及较小缓冲器的重新配置。这个要求时常被开发人员视为麻烦,包括我自己。结果,就会很想固定缓冲器的大小,或作些能导致程序代码不密闭的其他让步。TokenMaster范例应用程序经由利用一个简单的范本类别,即CAutoBuf,提出这个要求。此时您将会看到这个类别大大地简化了程序代码的部份,在那里,系统函数可以要求可变大小的缓冲器。
修改权杖资讯
大部分的权杖资讯是固定的。例如,您不能设定一位权杖使用者的SID与它所拥有的不同,您也不能增加群组SIDs或权限给权杖。而您可以做的修改属于两类:
- 可以被调整 的资讯
- 可以被设定 的资讯
说明
尽管已经存在的权杖可改变的部份很少,但是以现存的权杖为基础而产生一个增加限制的新权杖是可能的。这种技巧被称为建立一个受限的权杖,在本章稍后有详细的讨论。
设定资讯的处理程序,让它以您能想像的的方法运作。您可以建立一个结构并把它及权杖的handle传递到系统函数中,然后由系统设定权杖中的资讯。
调整资讯与设定资讯的运作有点不同。权杖中的权限及群组都可以有两种状态:即启用及停用。例如,虽然您不能在权杖中「设定」权限清单,但是您可以使已经存在于清单的个别权限启用及停用。
我们最常对权杖做的修改是设定权杖预设的DACL(为了预设的安全性而使用,在前面章节中有描述过),以及调整权杖的权限。您会发现设定权杖之预设拥有者及调整群组的处理程序,分别与设定预设DACL及调整权杖的权限非常类似。
调整权杖的权限
在讨论如何调整权杖的权限前,让我们扼要地重述在第九章已经学习过的权限部份,然后再慢慢扩大范围:
- 权限被分派到信任成员中(使用者及群组)。
- 当使用者登入系统时,使用者的权限会被复制到使用者的权杖中。
- 权限被显示的名称、程序名称及LUID识别。
在您可以调整权杖的权限之前,必须知道如何从权限的程序名称中取得系统使用的LUID。这必须经由呼叫LookupPrivilegeValue函数来完成:
BOOL LookupPrivilegeValue( LPCTSTR lpSystemName, LPCTSTR lpName, PLUID lpLuid);
您应该传递一个字串给lpSystemName参数,这个字串代表您想要收到LUID的系统名称。传递NULL以指出本地端机器。lpName参数则指出系统权限的程序名称,您应该传递出自《Platform SDK》的#define(例如,SE_TCB_NAME或SE_DEBUG_NAME)。您必须传递一个指向LUID类型变数的指标给lpLuid参数,系统将会为要求的权限填入LUID。
现在我们需要探索权限如何从安全性资料库取得我们的权杖。您必须了解权杖中找到的权限,是授予使用者帐户的权限和授予使用者为成员之每个群组权限的混合。
一旦系统建立权杖后,权杖中的权限就被会固定-意即它们不能被增加或移除。假如我们可以增加一个权限到权杖中,Windows 2000的安全性将被大大地破坏。然而,权限维持一个指出它们是否为启用或停用的状态。
当系统呼叫PrivilegeCheck时,假如被要求的权限是无效的,这个函数将会执行失败。为了使问题复杂化,有些系统在试图呼叫PrivilegeCheck前,会自动地呼叫您权杖的启用权限。在这些实例中,保持权限就可以使函数执行成功。
为了要了解哪个函数要求一个启用的权限,您必须阅读文件并偶尔从事一些试验及尝试错误行为。
在呼叫安全函数之前,您必需调整权杖的权限,让它成为启用或停用必要的权限。这可经由呼叫AdjustTokenPrivileges完成:
BOOL AdjustTokenPrivileges( HANDLE hTokenHandle, BOOL fDisableAllPrivileges, PTOKEN_PRIVILEGES pNewState, DWORD dwBufferLength, PTOKEN_PRIVILEGES pPreviousState, PDWORD pdwReturnLength);
第一个参数为hTokenHandle,指出您想要修改权限状态的权杖。fDisableAllPrivileges参数为TRUE时,会导致所有在权杖中的权限都被停用,不理会传递进pNewState参数的任何东西。在这种情形下,您应该传递NULL给pNewState。当您只启用或停用授予权杖的子权限时,您通常会使用pNewState参数。pNewState参数是一个指向TOKEN_PRIVILEGES结构的指标,以下是它的函数定义:
typedef struct _TOKEN_PRIVILEGES { DWORD PrivilegeCount; LUID_AND_ATTRIBUTES Privileges [ANYSIZE_ARRAY ]; }TOKEN_PRIVILEGES;
请注意,权限是一个LUID_AND_ATTRIBUTES结构的可变大小阵列。您想要在权杖中调整的每个权限结构皆应该在阵列中,而且PrivilegeCount 成员应该反映出权限的数量。以下是LUID_AND_ATTRIBUTES结构的定义:
typedef struct _LUID_AND_ATTRIBUTES { LUID Luid; DWORD Attributes; }LUID_AND_ATTRIBUTES;
阵列中每个结构的Luid成员应该被设定为考虑之权限LUID。最后, 属性 成员应该被设定为0,用来停用权限,或被设定为SE_PRIVILEGE_ENABLED,用来启用权限。
AdjustTokenPrivileges函数的dwBufferLength参数指出经由pPreviousState指向缓冲器以位元组计算的长度,它会指向一个接收权限之先前状态的缓冲器。假如不需要先前的状态资讯,可以传递NULL给pPreviousState。如果需要先前的状态资讯,就必须提供一个充分大小的缓冲器。一个指向需要大小的指标会被传回到pdwReturnLength参数中。经由pPreviousState指向的缓冲器与TOKEN_PRIVILEGES结构与您为pNewState参数建立的嵌入阵列一样被建立。
如您所见,启用及停用个别权限是很重要的工作。然而,您将发现它也是很常见的工作。我常认为Windows的开发人员应该提供一个shorthand函数,就像以下的EnablePrivilege函数,定义了某些重要的事,并提供容易的权杖权限存取。
BOOL EnablePrivilege( HANDLE hToken, PTSTR szPriv, BOOL fEnabled);
szPriv参数是个字串,表示启用及停用权限的程序名称,fEnabled参数简单地指出是否启用及停用权限。TokenMaster范例应用程序中实作了这样的函数,如下所示。
BOOL EnablePrivilege(HANDLE hToken,LPTSTR szPriv,BOOL bEnabled) { TOKEN_PRIVILEGES tp; LUID luid; BOOL bRet =FALSE ; __try{ //首先查询系统唯一的权限LUID if(!LookupPrivilegeValue(NULL,szPriv /*SE_DEBUG_NAME*/,&luid)) { //假如名称是膺造的... __leave ; } //建立我们的权杖权限「阵列」(我们的实例是一个阵列) tp.PrivilegeCount =1; tp.Privileges [0 ].Luid =luid; tp.Privileges [0 ].Attributes =bEnabled?SE_PRIVILEGE_ENABLED:0; //经由启用及停用这一个权限,调整我们的权杖权限 if(!AdjustTokenPrivileges( hToken, FALSE, &tp, sizeof(TOKEN_PRIVILEGES), NULL, NULL )) { __leave ; } bRet =TRUE ; }__finally{} return(bRet); }
说明
系统已经实作了另一个名称为AdjustTokenGroups的函数,除了可让权杖群组的启用及停用取代权限外,它的呼叫惯例与AdjustTokenPrivileges非常类似。然而,权杖群组的启用及停用并不常用。TokenMaster范例应用程序展示了一个AdjustTokenGroups的例子。
设定预设的DACL
较少见的设定权杖预设DACL工作,比调整权杖的权限简单。权杖的预设DACL定义了以预设安全性建立安全物件的安全性存取。安全物件包括建构如文件、Mutexes(互斥)及线程的部份。安全物件通常会经由传递NULL以作为建立函数的LPSECURITY_ATTRIBUTES参数,并以预设安全性建立(有关这个主题的更多资讯,请参阅
经由使用SetTokenInformation函数,您可以设定权杖的预设DACL和它的预设拥有者,以及主要的群组(它们都不应该混淆权杖使用者的身分识别,并且不能被改变):
BOOL SetTokenInformation( HANDLE hTokenHandle, TOKEN_INFORMATION_CLASS TokenInformationClass, PVOID pTokenInformation, DWORD dwTokenInformationLength);
hTokenHandle参数是您要修改的权杖handle。TokenInformatonClass被用来指出您想改变的权杖部分。 前面已经讨论过您可选择的部分,在〈读取权杖资讯〉一节中所列的项目符号清单中。
SetTokenInformation的dwTokenInformationLength参数指出由pTokenInformation指向的缓冲器长度。有关作用中的SetTokenInformation完整范例,请参阅 TokenMaster范例应用程序 的内容。
使用权杖执行程序代码
到目前为止,我们已经讨论过从权杖中可以找到什么以及您可以对权杖做调整的内容。然而直到您开始在除了自己的处理程序权杖之外的权杖下执行程序代码,权杖才会开始产生作用。
以下有两种方法:
- 用新的权杖建立一个新的程序。
- 用您程序中的线程模拟一个权杖。
首先我们讨论如何在与您不同的使用者环境下建立一个新的程序,因为它是除了您以外的使用者执行程序代码时所用方法中较不复杂的一种。最后我们将会讨论到模拟的部份。
使用权杖建立一个程序时,应该呼叫CreateProcessAsUser:
BOOL CreateProcessAsUser( HANDLE hToken, PCTSTR pszApplicationName, PTSTR pszCommandLine, PSECURITY_ATTRIBUTES psaProcessAttributes, PSECURITY_ATTRIBUTES psaThreadAttributes, BOOL fInheritHandles, DWORD dwCreationFlags, PVOID pEnvironment, PCTSTR pszCurrentDirectory, PSTARTUPINFO pStartupInfo, PPROCESS_INFORMATION pProcessInformation);
除了第一个hToken参数外, CreateProcessAsUser与CreateProcess的参数完全相同,它接收使用者环境的主要权杖,新的处理程序将在这个使用者环境下执行(有关CreateProcess的广泛讨论,请参考《Programming Applications for Microsoft Windows, Fourth Edition》一书的内容)。
现在让我们看看它们的差别。假如新的处理程序被建立,则Create ProcessAsUser会传回TRUE,反之,函数执行失败则传回FALSE。在叁种常见的情形下,CreateProcessAsUser可能会执行失败,而CreateProcess则不会。
在第一个案例中,呼叫处理程序可能没有可执行的文件或可执行之文件目录的存取权。在本机帐户中执行的服务不太可能会碰到这个问题,虽然它的确有可能发生。为了避开这个问题,您必须暂时模拟经由传递到CreateProcessAsUser的相同权杖建立新处理程序的使用者。这种暂时的模拟必需在呼叫CreateProcessAsUser前完成。当然,如果新的使用者没有可执行档或目录的存取权,则函数执行失败。
在第二个案例中,CreateProcessAsUser要求呼叫的程序拥有被授予权杖的SE_ASSIGNPRIMARYTOKEN_NAME及SE_INCREASE_QUOTA_NAME权限。这个规则的例外是案例中传递给CreateProcessAsUser的权杖,它是一个用呼叫程序权杖建立的受限权杖。在这种情况下,不需要SE_ASSIGNPRIMARYTOKEN_NAME权限(使用受限权杖是个非常强大的技巧,本章稍后会讨论一些细节)。这个情形的理由是受限权杖拥有比当前程序还少的系统存取权。
在第二个案例中,当传递给hToken参数的权杖不是一个主要的权杖时,CreateProcessAsUser会执行失败。假如您持有一个模拟权杖的handle,则您可以呼叫DuplicateTokenEx将它转变成主要的权杖:
BOOL DuplicateTokenEx( HANDLE hExistingToken, DWORD dwDesiredAccess, PSECURITY_ATTRIBUTES pTokenAttributes, SECURITY_IMPERSONATION_LEVEL ImpersonationLevel, TOKEN_TYPE TokenType, PHANDLE phNewToken);
hExistingToken参数是您要复制的权杖。这个权杖可以是主要权杖或是模拟权杖。就新的权杖而言,dwDesiredAccess参数指出您想要的存取权。您应该只要求完成手边工作所需要的权利即可。有关存取权利清单,请参阅
pTokenAttributes参数指出新权杖的安全描述项及其继承属性。不要把这个参数与权杖的预设DACL混淆了-此参数为新的物件设定存取控制(安全描述项及存取控制在 第十章 有详细地讨论)。
ImpersonationLevel参数指出新权杖的模拟等级,它可以是表格11-3中的任何值。
表格11-3 SECURITY_IMPERSONATION_LEVEL列举类型的成员 |
值 | 叙述 |
---|---|
SecurityAnonymous | 用SecurityAnonymous模拟等级建立的权杖,不能用模拟建立一个处理程序。 |
SecurityIdentification | 用此模拟等级建立的权杖,只能被用来作为身分识别的方法。权杖的使用者及群组可以被查询,但是权杖不能与模拟一起使用,或在呼叫CreateProcessAsUser函数中使用。 |
SecurityImpersonation | 这个模拟等级建立一个具有完全功能的权杖,它不但可以被查询,而且可以用来透过模拟执行程序代码。权杖在线程可模拟之前,必须是个模拟权杖。 |
SecurityDelegation | 有SecurityDelegation模拟等级的权杖,可以被用来存取网路来源。这被称为 委派 ,而且在连接次要的伺服端里,可让伺服端变成代表它客户的客户端。Windows NT 4.0及较早的版本不支援委派。Windows 2000也是,但是它需要委派在服务器试图建立一个委派类型的权杖时被允许。 |
TokenType可以是TokenPrimary或者TokenImpersonation,都是TOKEN_TYPE列举型别的成员。最后一个参数phNewToken,它会收到新权杖的handle。当您用完此物件时,请记得呼叫CloseHandle关闭它。
假如DuplicateTokenEx传回FALSE,则表示函数已经执行失败。它失败的原因通常会是原始权杖上的存取权不够(有关存取控制及存取权利的完整讨论,请参阅
说明
Windows先前的版本中,为了代表客户端行为的目的而在伺服端机器上建立的权杖被称为网路权杖,它通常拥有SecurityImpersonation模拟等级。这些权杖不包含信任成员凭证的副本,并且不能被用来存取建立此权杖之机器外的资源。
然而,Windows 2000使用Kerberos安全性协定以支援SecurityDelegation模拟等级(在第十二章中讨论)。现在要拥有一个不限制存取本地端机器的网路权杖是可能的。
使用LogonUser取得权杖
现在您已经知道从程序中取得权杖,以及如何使用任一权杖建立处理程序的方法。然而我们尚未讨论当您只有使用者名称及密码,要如何撷取权杖handle的方法。您可以经由呼叫LogonUser来达成它:
BOOL LogonUser( PTSTR pszUsername, PTSTR pszDomain, PTSTR pszPassword, DWORD dwLogonType, DWORD dwLogonProvider, PHANDLE phToken);
pszUsername及pszDomain参数指出您想要收到的使用者名称及网域的权杖。您可以传递句点字元(「.」)给pszDomain参数,使它只搜寻本机系统的帐户资料库。假如您传递NULL给pszDomain参数,则本机系统会在信任的网域中搜寻使用者。第叁个参数pszPassword,是这个帐户的密码。
LogonUser的dwLogonType参数,会向系统描述权杖如何被使用以及您想收到的权杖类型。表格11-4中列出您可以传递给dwLogonType参数的值。
表格11-4 您可以传递给LogonUser的dwLogonType参数的值 |
值 | 叙述 |
---|---|
LOGON32_LOGON_ INTERACTIVE | 传递这个值给LogonUser,使系统检查要求权杖的使用者帐户中,是否有SE_INTERACTIVE_LOGON_NAME权限存在。假如使用者没有被指派这个权限,则LogonUser执行将会失败。此外,用这个登入收到的权杖会与系统一起快取。这意味着本机系统可能会与验证机器失去联络,但是将来使用快取凭证呼叫LogonUser时,仍旧会执行成功。LogonUser将传回一个主要的权杖。 |
LOGON32_LOGON_ BATCH | 传递这个值给LogonUser,使系统检查要求权杖的使用者帐户中,是否存在SE_BATCH_LOGON_NAME权限。假如使用者没有这个权限,则LogonUser将执行失败。用这个登入类型而收到的权杖不会被快取,它会提高LogonUser的效能并使这个登入类型适合高效能的服务器。LogonUser会传回一个主要的权杖。 |
LOGON32_LOGON_NETWORK | 传递这个值给LogonUser,使系统检查要求权杖的使用者帐户中,是否存在SE_NETWORK_LOGON_NAME权限。假如使用者没有这个权限,则LogonUser将失败。用这个登入类型而收到的权杖不会被快取。另外,这个权杖将会是个模拟权杖及「网路权杖」。 |
LOGON32_LOGON_ NETWORK_ CLEARTEXT | 跟LOGON32_LOGON_NETWORK登入类型一样,这个登入类型会检查讨论中帐户的SE_NETWORK_LOGON_NAME帐户权利。然而,不像LOGON32_LOGON_NETWORK一般,当preserving信任成员凭证的副本使用产生的权杖存取网路时,这个登入类型会传回一个模拟权杖。该权杖必需在它可以被用来呼叫CreateProcessAsUser前,即被复制成主要的权杖 。 |
LOGON32_LOGON_ NEW_CREDENTIALS | 这个是Windows2000的新结构登入类型。它要求您使用LOGON32_PROVIDER_WINNT50作为呼叫LogonUser中的dwLogonProvider值。这个登入类型会复制呼叫线程的程序权杖,并且将次要的身分识别加到权杖中。这个次要的身分识别将是所有网路存取权杖的身分识别,本地端机器的权杖身分识别仍然会跟原始权杖一样。这使得LOGON32_LOGON_NEW_凭证为唯一的,因为它使用现行的权杖,并用额外的凭证建立一个新的权杖。有关这个登录类型的范例,请看Windows 2000提供的RunAs.exe公用程序。 「/NetOnly」参数会使用LOGON32_LOGON_NEW_CREDENTIALS登入类型为新的程序建立一个权杖。这个被产生的权杖是一个主要权杖。 |
LOGON32_LOGON_SERVICE | 传递这个值给LogonUser,使系统检查要求权杖的使用者帐户中,是否存在SE_SERVICE_LOGON_NAME权限。假如使用者没有被指派这个权限,LogonUser将执行失败。如果机器与验证代理程序失去联络时,为了将来还能呼叫LogonUser,这个权杖会被快取。LogonUser会传回一个主要权杖。 |
LOGON32_LOGON_ UNLOCK | 这个登入类型被GINA用来处理未锁定的工作站。假如这个系统的稽核被启动时,可用这个登入类型呼叫LogonUser,以便在事件日志中建立一个项目。所产生的权杖是个主要权杖。 |
作为一个开发人员或网路管理员,您可以绑紧您的安全性,并且使系统更安全来预防建立了只能与LOGON32_LOGON_SERVICE或LOGON32_ LOGON_INTERACTIVE类型一起作用的使用者帐户的误用。例如,您只能分别授予SE_SERVICE_LOGON_NAME或SE_INTERACTIVE_LOGON_NAME权限。
说明
尽管登入类型,如LOGON32_LOGON_SERVICE及LOGON32_LOGON_INTERACTIVE,要求了不同的权限,但是实际上它们却不能彻底地产生不同的权杖(权杖之间主要的不同是权杖群组中存在SID,它指出登录使用什么类型,登录的来源也有不同)。要求不同的权限可让系统提供额外的验证等级,而不用使权杖的结构复杂化。
用LOGON32_LOGON_BATCH或LOGON32_LOGON_SERVICE建立的权杖仍旧可以执行与使用者的互动程序,及建立视窗和其他GUI物件。
然而,当系统建立一个如服务一样执行的处理程序时,它使用的权杖是用LOGON32_LOGON_SERVICE登入类型登入的。而当系统以互动方式让使用者登入系统时,它会使用LOGON32_LOGON_INTERACTIVE登入类型。
您传递给dwLogonProvider的值,决定要使用哪个方法验证传递到LogonUser的凭证。除非您正在使用LOGON32_LOGON_NEW_CREDENTIALS登入类型,否则您就应该传递LOGON32_PROVIDER_DEFAULT给所有使用的LogonUser。在这种情形中,您应该传递LOGON32_PROVIDER_WINNT50的值给dwLogonProvider参数。
您会经由LogonUser的phToken参数收到新权杖的handle。
假如系统在验证凭证时执行成功,LogonUser函数将传回TRUE,假如失败则传回FALSE。呼叫GetLastError将传回失败的原因。当LogonUser执行失败时,会产生叁种常见的错误:即ERROR_LOGON_FAILURE、ERROR_LOGON_ TYPE_NOT_GRANTED及ERROR_PRIVILEGE_NOT_HELD。当GetLastError传回ERROR_LOGON_FAILURE时,表示无法识别传递到LogonUser的凭证。假如GetLastError传回ERROR_LOGON_TYPE_NOT_GRANTED,则表示要求的帐户没有要求登入类型的适当帐户权利。错误程序代码ERROR_PRIVILEGE_NOT_HELD暗示呼叫LogonUser的程序没有被授予SE_TCB_NAME权限。
本机帐户预设为拥有此授予的权限,系统中没有预设其他信任成员拥有这个权限。
当您用完这个权杖后,呼叫属于物件的CloseHandle以关闭它。
当您在呼叫LogonUser时,您会要求系统为您建立一个权杖。您可以呼叫CreateProcessAsUser及模拟来使用这个权杖,这是下一节的主题。
模拟(Impersonation)
当您在Windows中编写服务器软件时,可以选择建立一个代表客户端的处理程序以执行客户端的请求。然而,为每个客户端建立一个处理程序并不是可调整的技巧。模拟是这个问题的解决办法,因为它让个别的线程在您的客户端安全性环境下执行任一时间长度,然而当它完成,就会回复到程序的安全性环境。模拟可以极有效率的使用并且以可调整的方式完成。
我非常喜爱模拟-系统会为您处理琐碎的细节,而且它经由呼叫ImpersonateLoggedOnUser而被简单地实作。这真的是很棒,因为它保证线程在客户端的安全性环境下执行。有什么更好的方法可保证您不会意外地让客户端导致服务经由本机帐户而滥用授予的可怕权利?以下是ImpersonateLoggedOnUser函数的定义:
BOOL ImpersonateLoggedOnUser( HANDLE hToken);
假如传递给hToken参数的权杖是TokenPrimary类型,则ImpersonateLoggedOnUser会建立一个与TokenImpersonation类型完全一样的权杖,并将它分派到呼叫的线程。假如原始的权杖已经是个模拟权杖,则权杖会被直接分派给线程。在第一个情形下,呼叫的线程必须拥有对权杖的TOKEN_QUERY及TOKEN_DUPLICATE存取权。第二个情况中,则只需要TOKEN_QUERY存取权。
假如ImpersonateLoggedOnUser执行成功,它会传回TRUE。否则您的服务应该呼叫GetLastError函数,以找出失败的原因。
传递给ImpersonateLoggedOnUser的权杖可以是经由呼叫LogonUser、DuplicateTokenEx、OpenThreadToken,或是目前为止我们所讨论过的任何其他类似函数收到的权杖。它也可以经由某些其他方法撷取的权杖(本书将扼要地讨论一些方法)。在执行成功后,多数经由模拟线程呼叫的安全函数能识别由权杖代表的新安全性。
说明
假如现在处于模拟状态的线程下,则呼叫CreateThread建立另一个线程,新的线程将不会被模拟。另一种方式是,除非建立的线程明确地模拟一个权杖,否则所有使用CreateThread建立的线程皆会为了安全的行为而使用程序的权杖。
这会导致某些非常难以发现的错误,因为您的程序代码可以模拟使用者,然后呼叫一个函数建立新的线程,以执行工作。这个新的线程不代表建立线程的安全性环境。在这个开发服务的案例中,通常会授予比预期中更多的存取权给线程,因此会在您的服务中产生一个安全性「漏洞」。
当您的线程代表您的客户端完成执行时,线程会经由呼叫RevertToSelf恢复使用程序的权杖:
BOOL RevertToSelf(VOID);
说明
为了改善模拟时的效能,不管您是否呼叫了LogonUser、OpenProcessToken,或使用某些其他函数取得您用来模拟的权杖,最好要避免取得比需要还多的权杖。通常您的服务会撷取或建立一个handle一次,在连接的状态资料中储存权杖的handle。然后服务在需要时可以使用储存的handle呼叫ImpersonateLoggedOnUser及RevertToSelf 函数,只有在连结被结束且不再需要权杖时才结束handle。
现在您的服务可以经由任何通讯机制来代表客户端执行连接,以使客户端能够传递它的凭证给服务。另外,您的服务可以储存一组事先设定给各种客户端帐户使用的凭证。依据您服务的需求,这个方法是非常有效的。然而,在许多的案例中,一个更无接缝的方法是值得向往的。
模拟连接的客户端
Windows提供了一种真正的模拟,它不需要您的服务取得一组凭证。这种模拟是连接导向的,但是除此以外,它与我们所讨论过的模拟技巧很类似。假如一个被授予信任的授权单位已经验证客户端,而且通讯媒介支援模拟时,您的服务可以自动地模拟客户端!
表格11-5列出模拟所支援的连接方式,以及用来初始模拟与回复到程序权杖的函数。
为了讨论每个模拟的种类,我们暂时离开网路通讯的主题,它可以用整本书来讨论。然而,前面章节所提的RoboService范例应用程序使用了命名管道完全地实作模拟的功能,而SSPIChat范例应用程序(在第十二章)则使用Security Support Provider Interface(SSPI)来实作模拟(SSPI也在第十二章讨论)。
表格11-5中谈论到的两个函数可以提出来深入地探讨。它们是ImpersonateSelf及SetThreadToken函数:
BOOL ImpersonateSelf( SECURITY_IMPERSONATION_LEVEL ImpersonationLevel);
ImpersonateSelf函数会复制您程序的权杖,建立一个TokenImpersonation类型的权杖,并且分派这个权杖给呼叫的线程。
在实际的情形下,您通常不会使用ImpersonateSelf调整权杖的模拟等级。您或许宁可用它来建立模拟以调整线程,例如启用及停用权限,或启用及停用权杖中的群组,这只会影响个别的线程而非程序中的每个线程。RevertToSelf函数用来结束模拟。
表格11-5 模拟支援的连接方式 |
连接方式 | 模拟函数 |
---|---|
非连接 |
BOOL ImpersonateLoggedOnUser( HANDLE hToken); 或 BOOL ImpersonateSelf( SECURITY_IMPERSONATION_LEVEL ImpersonationLevel); 和 BOOL RevertToSelf(VOID); 和/或 BOOL SetThreadToken( PHANDLE Thread, HANDLE Token); |
命名管道 |
BOOL ImpersonateNamedPipeClient( HANDLE hNamedPipe); 和 BOOL RevertToSelf(VOID); |
动态资料交换 |
BOOL DdeImpersonateClient( HCONV hConv); 或 BOOL ImpersonateDdeClientWindow( HWND hWndClient, HWND hWndServer); 和 BOOL RevertToSelf(VOID); |
远端程序呼叫 (RPC) |
RPC_STATUS RPC_ENTRY RpcImpersonateClient( RPC_BINDING_HANDLE BindingHandle); 和 RPC_STATUS RPC_ENTRY RpcRevertToSelfEx( RPC_BINDING_HANDLE BindingHandle); |
通讯端或任何其他传输机制(经由SSPI)-在第十二章中讨论 |
SECURITY_STATUS ImpersonateSecurityContext( PCtxtHandle phContext ); 和 SECURITY_STATUS RevertSecurityContext( PCtxtHandle phContext); |
以下列出的SetThreadToken函数,可让您使用线程的handle任意地选择任何线程的模拟权杖。pThread参数是个指向您要调整的线程handle指标,传递NULL以指出当前的线程。hToken参数指出用来模拟的权杖,NULL值将导致线程回复成程序等级的权杖。
BOOL SetThreadToken( PHANDLE pThread, HANDLE hToken);
起先粗略地浏览时,发觉SetThreadToken函数似乎是不必要的。为什么不就使用适合您通讯机制的模拟函数呢?答案是,模拟缺少了一样特色-即模拟堆叠 的概念。让我来解释一下。
假如您用权杖A呼叫ImpersonateLoggedOnUser,然后再用权杖B呼叫一个呼叫到ImpersonateLoggedOnUser的函数,当这个函数在呼叫RevertToSelf时,系统将会使线程的权杖回复到程序权杖。系统没有记忆线程跟权杖A的结合。通常,您会控制您的程序代码并避免像这样的情况。但是以下有两个值得注意的例外情形:
- 在较大的专案里,进入您函数前的状态是不稳定或不一致的。
- 被用来扩充其他软件的DLL专案,例如扩充Microsoft Internet Information Services的ISAPI。
为了伪造模拟堆叠,您的函数必需经由呼叫OpenThreadToken,以撷取其当前权杖的handle(我们已经讨论过),把它储存起来(最有可能在堆叠上),然后再呼叫适合的模拟函数,而非呼叫相符的「回复」函数,您的程序代码需要使用SetThreadToken以恢复被储存的权杖。这个方法使您的函数用它找到线程的方法把线程带回呼叫的程序代码中。
说明
在高效能的服务器环境中,网路上的通讯模拟资讯开销是不受欢迎的。在许多情况下,您可以经由在连结上呼叫适合的模拟函数来达到最好的效能,然后再呼叫OpenThreadToken撷取及储存模拟线程权杖的handle。之后,当服务中的线程执行客户端更进一步的请求时,您可以使用SetThreadToken或ImpersonateLoggedOnUser模拟储存的权杖。
受限的权杖
如同前面所提的,若要您的服务或任何应用程序从头开始建立一个权杖是不可能的-系统必须为您建立权杖。但是Windows可让您以现存的权杖为基础,再加上一些额外的限制以建立一个新的权杖。这个新的权杖即是受限的权杖。受限的权杖可帮助您的软件用非常清楚的解决办法来符合一些复杂的安全性需求。
当您建立一个受限的权杖时,并不是把限制加入现存的权杖中;反而是使用一个范本权杖建立一个新的权杖,虽然这个新的权杖会有额外的限制。以下列出叁种可让您放置在权杖上的限制。每个限制是非必需的,您可以在建立新权杖时选择它们的任何一种组合。有两种限制需依赖对存取控制的认识,它们已经在第十章中讨论。您可以在权杖上执行以下所列之行为的任何一种组合,用来建立一个新的受限权杖:
- 删除权限。
- 停用信任成员帐户的权杖SIDs。
- 增加信任成员帐户的「受限SIDs」。
删除权限
在建立一个受限的权杖时,可以选择一组您不想授予新权杖的权限,它与停用的权限不同。并不是您明确地指定从新权杖中移除权限。您可能会有想使用现存的安全性环境,但是需要删除某些权限的情况。例如,您可能会使用自己的环境,或者经由模拟而收到并移除SE_SHUTDOWN_NAME或SE_TCB_NAME权限的环境。
停用权杖SIDs
在建立一个受限的权杖时,可以选择停用现存权杖中的哪个信任成员。被停用的信任成员可以是权杖使用者及群组成员的任一种组合。当存取检查被执行时,任何被选择加入停用清单的SIDs的信任成员将只被用来拒绝存取。
例如,假设权杖使用者具有TEMP USERS群组中的成员资格,当拒绝使用者存取C:/Permanent目录,而授予使用者存取C:/Temp目录时,假如TEMP USERS群组的SID被选为受限权杖中的停用SID,则群组中的成员资格将不再被允许存取C:/Temp目录,但是它仍旧被拒绝存取C:/Permanent目录。
您可以只停用已经存在于原始权杖的SIDs,而停用权杖中的信任成员不会以任何方法影响权杖的身分识别。例如,停用权杖使用者只会影响物件的存取权。权杖仍旧会识别特定的使用者。
增加受限的SIDs
除了停用现存的SIDs外,您还可以建立一组信任成员或SIDs,即受限的SIDs,并将它动态地加入您的新权杖。您可能会认为动态地加入信任成员到权杖的能力是非常强大的。但是其隐藏的困难是新的SIDs会被用来作为存取安全物件的再确认。不只存取检查必须在存取被授予前即清除权杖「天生的」信任成员,它也必须清除一组新的受限信任成员。
使用受限的SIDs与建立次要的权杖类似,它会确保权杖在物件上执行任可行为前,已经拥有安全物件的存取权。但是使用受限的权杖时,这个次要检查会被系统自动地处理。受限的SIDs稍微改变存取检查,实际上它们必须被执行两次:一次是权杖中天生的SIDs,另一次则是受限的SIDs。只有当两者都执行成功时,才表示允许存取检查(有关存取检查的更详细讨论,请参阅
您现在已经了解了可以应用于新受限权杖副本的限制。让我们看看这个建立 权杖 的函数,CreateRestrictedToken:
BOOL CreateRestrictedToken( HANDLE hExistingTokenHandle, DWORD dwFlags, DWORD dwDisableSidCount, PSID_AND_ATTRIBUTES pSidsToDisable, DWORD dwDeletePrivilegeCount, PLUID_AND_ATTRIBUTES pPrivilegesToDelete, DWORD dwRestrictedSidCount, PSID_AND_ATTRIBUTES pSidsToRestrict, PHANDLE phNewTokenHandle);
hExistingTokenHandle参数是新权杖被建立的原始权杖。CreateRestrictedToken以传递给hExistingTokenHandle参数的权杖handle为基础,建立一个新的权杖。
这个handle必须有TOKEN_DUPLICATE的存取权。任何作为授限权杖的原始权杖皆是合法的,除了已经有授限SIDs清单的权杖外。您可以经由传递它的handle到IsTokenRestricted函数,以查明权杖是否包含了受限SIDs:
BOOL IsTokenRestricted( HANDLE TokenHandle);
假如您想让新的权杖只允许不需要权限的功能时,可以传递DISABLE_MAX_PRIVILEGE给dwFlags参数。假如您只想删除权限的子集合,可以传递0。传递0是较常见的情况。
dwDisableSidCount参数指出您要确保在新的权杖中被停用的信任成员数量。下一个参数为pSidsToDisable,指向一个SID_AND_ATTRIBUTES结构的阵列,它指出新权杖中停用的群组(或许是权杖使用者)。dwDisableSidCount参数涉及传递给这个参数的项目数量。停用SIDs的特点是新的权杖中存在之SID的SE_GROUP_USE_FOR_DENY_ONLY属性资讯。
说明
您可以传递原始权杖中表示的信任成员超集(superset)给pSidsToDisable参数。系统将会忽略不在原始权杖中的任何SID。这样一来,您就可以将停用SIDs的个别清单运用到多于一个拥有不同基本权杖使用者及群组的权杖中。
SID_AND_ATTRIBUTES结构之定义如下:
typedef struct _SID_AND_ATTRIBUTES { PSID Sid; DWORD Attributes; }SID_AND_ATTRIBUTES ;
此结构的Attributes成员被CreateRestrictedToken所忽视。其他函数,例如GetTokenInformation,使用Attributes成员记录SID的属性,例如SE_GROUP_USE_FOR_DENY_ONLY及SE_GROUP_MANDATORY即是。SID_AND_ATTRIBUTES的Sid成员指向一个SID结构,它指出您想在新的权杖中停用的信任成员。
您的服务器应该传递一个SID_AND_ATTRIBUTES结构的阵列给CreateRestrictedToken的pSidsToDisable参数。假如您不想要停用新权杖中的任何信任成员时,应该传递0给dwDisableSidCount及NULL给pSidsToDisable参数。
除了pPrivilegesToDelete指向一个LUID_AND_ATTRIBUTES结构的阵列之外,CreateRestrictedToken的dwDeletePrivilegeCount及pPrivilegesToDelete参数与「停用的SID」参数的运作同样。当在使用停用的SIDs时,如果您不想删除任何权限,则传递0及NULL给dwDeletePrivilegeCount及pPrivilegesToDelete是适当的。有关LUID_AND_ATTRIBUTES结构的详细讨论,请参阅本章前面之〈 调整权杖的权限 〉一节。
dwRestrictedSidCount及pSidsToRestrict参数分别表示您要加到新权杖中,作为「受限SIDs」的信任成员的总数及阵列。您应该使用与dwDisableSidCount及pSidsToDisable相同的规则来建立您的受限信任成员清单。请记得您不需要在新的权杖中包括任何的受限SID。假如不需要受限SIDs的话,可以传递0给dwRestrictedSidCount参数。
您传递给CreateRestrictedToken的停用SIDs阵列与受限SIDs阵列之间的不同处在于受限SIDs,其函数不会忽略不存在于原始权杖的信任成员。事实上,您时常会含括未包含于原始权杖中的信任成员,以更进一步地限制存取检查。
CreateRestrictedToken的最后一个参数,即phNewTokenHandle,指向一个HANDLE变数,它会接收一个新的、受限权杖的handle。
CreateRestrictedToken是一个强大的函数,可让您有弹性的限制现存的权杖。例如,思考以下这个说明弹性的方案。想像您要保护物件群组的安全,以不是很正统的方式明确地允许或拒绝使用者的某些存取权。您只想要在星期二限制某些物件的存取权,而不管这个物件存在的特有存取权。您可以采取以下步骤来实作这个功能,而不用破坏此物件之星期二以外的特有存取权。以下是管理的工作:
- 为了增加限制到安全物件的唯一目的,建立一个名为星期二的使用者帐户。
- 修改物件的DACLs,包括星期二要求的限制,并分派这些拒绝存取ACEs到星期二帐户的SID。
以下为服务器的工作:
- 当使用者连接到您的服务器时,使用GetSystemTime函数决定是否为星期二。
- 假如是星期二,则在执行代表客户端的程序代码之前建立一个受限权杖,而不是采用使用者的模拟权杖。
- 建立符合来源权杖内之群组的受限SIDs清单,并包含权杖的使用者SID。同时包含一个星期二信任成员项目在受限SIDs清单中。
- 使用ImpersonateLoggedOnUser模拟新的受限权杖。
使用这个程序,您可以轻易地执行附加限制的功能,而不用每个星期二就请求改变所有安全物件的存取控制一次。
TokenMaster范例应用程序可让您撷取权杖,然后建立一个权杖的受限版本。您可以使用这个受限版本启动应用程序。这对于您更进一步了解受限权杖,会是个非常有用的工具。
在阅读本章之后,您已经熟悉可让软件维护及与Windows存取控制模组一致的弹性方式,以管理身分识别的工具及技巧。您也已经了解用权杖为系统执行使用者环境的方式对您的软件很有帮助。同时也了解模拟也对您的软件极有好处,它可让您代表您的客户端执行工作(第十二章把这个主题带到下一个层级,SSPI)。受限权杖是另一个强大的特色,您可以在要求大量存取控制弹性的安全专案中使用它。
在您愈了解本章讨论的特色和能力以及最后两章所涵盖的特色后,您就愈能够设计为您安全工作的软件。
第十章 )。 第十章 )。 表格11-2 。 第十章 的内容)。藉由移除应用于程序代码之安全性所建立的每个物件之必要性来修改预设的DACL,可以大大地简化伺服端的程序代码。 第十章 )。 第九章 。)