65.9K
CodeProject 正在变化。 阅读更多。
Home

Windows 访问控制模型: 第二部分

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.80/5 (28投票s)

2005 年 4 月 24 日

CPOL

43分钟阅读

viewsIcon

252533

downloadIcon

7243

《访问控制系列》的第二部分将通过基本的访问控制结构进行编程。

MyWhoami output

图 8:Whoami 克隆程序的示例输出

关于系列

本四部分系列将讨论 Windows 访问控制模型及其在 Windows NT 和 2000 中的实现。

在本第二篇文章中,我们将开始使用安全标识符、访问控制列表和安全描述符进行编程。我们将使用 SID 解决微不足道的问题,从访问令牌中获取信息,启用特权,填充访问控制列表,最后检查我们是否有权访问某个资源。提供的演示项目是用 Windows 2000 风格编写的 Whoami 克隆程序。源代码包含使用低级 API、Windows 2000 API 和 Active Template Library 编写的本文代码的等效程序。

目录

目录是为整个系列准备的。

  1. 第一部分 - 背景和核心概念。访问控制结构
    1. 安全标识符 (SID)
    2. 安全描述符 (SD)
    3. 访问控制列表 (ACL)
    4. 选择一个好的自由访问控制列表
    5. Windows 2000 继承模型
    6. 令牌
    7. 关于安全描述符定义语言的说明
    8. 即将推出
  2. 第二部分 - 基本访问控制编程
    1. 选择你的语言
    2. SID 的乐趣
    3. 你是哪些组的成员?
    4. 启用令牌特权
    5. 分析安全描述符
    6. 遍历访问控制列表
    7. 我是否有权访问?
    8. 创建访问控制列表
    9. 创建和编辑安全对象
    10. 让你的类安全
    11. 玩具程序下载
  3. 第三部分 - 使用 .NET v2.0 进行访问控制编程
    1. .NET 安全的历史。
    2. 在 .NET 中读取 SID。
    3. Whoami .NET。
    4. .NET 中的特权处理。
    5. .NET 中以非特权用户身份运行。
    6. 在 .NET 中获取和编辑安全描述符,并将其应用于对象。
    7. .NET 中的访问检查。
    8. NetAccessControl 程序和 AccessToken 类库。
  4. 第四部分 - Windows 2000 风格的访问控制编辑器
    1. ACL 编辑器的功能 (ACLUI)。
    2. 入门。
    3. 实现 ISecurityInformation 接口。
    4. ISecurityInformation::SetSecurity
    5. 可选接口。
    6. 呈现接口。
    7. Filepermsbox - 一个显示 NTFS 文件或文件夹上安全描述符的程序。
    8. 历史

9. 选择你的毒药(选择你的语言)

在第一部分结束时,我请你选择用哪种语言进行编程。我给你提供了四种选择

  1. 低级方法。
  2. Windows 2000 方法。
  3. ATL 方法。
  4. .NET 方法。(如果你选择这个,请跳过此部分直接到第三部分。)

每种方法的详细介绍

  1. 低级方法涉及使用原始安全 API 来创建和读取安全对象(可追溯到 1988 年 Windows NT 诞生之初)。此方法的优点是可以在 Windows NT3.x 和 NT4.x 中工作。它还有一个优点是不依赖任何外部 DLL,也不需要你购买 Visual Studio .NET(此方法在 Win9x 上不起作用,因此如果你关心向后兼容性,则必须依赖动态加载)。

    为了使此方法奏效,你必须调用 Windows SDK 中最令人困惑的一些 API!这些低级 API 对 Windows 2000 的继承模型一无所知(使用这些 API 会导致 Windows 2000 上的安全漏洞)。函数返回 bool、errnos、指针还是 void 方面没有一致性。一个 API 要求你从 LocalAlloc() 堆中管理五个内存缓冲区!使用此方法进行开发很容易出错。

    你唯一想使用此技术的原因是,如果你的目标市场仍然使用 Windows NT3.x 或 4.x(并且你喜欢出现在安全邮件列表中)。新程序根本不应选择此技术。如果你只需要支持一个 Win9x 或 Win2000 客户端,你一定不能使用此方法开发。我很同情你,如果你被迫使用此访问控制方法进行开发。由于你的目标环境受限,我将假定你的开发环境也受限(即你没有可靠的 C++ 编译器)。此方法将用 C 语言编写。

  2. 新程序不应考虑低于此方法。此方法的好处是,你不需要 Visual Studio .NET 就可以进行安全开发,并且对于不喜欢 ATL 的人来说,这是最舒适的编程语言。但是,此方法的缺点是它仅在 Windows 2000 及更高版本上工作。使用此方法进行安全描述符的编码也会有点困难(除非你以前编写过文本解析器)。

    在 Windows NT 4 和 2000 期间,Microsoft 添加了一组新的 API 来简化安全编程。其中最显著的成果可能是添加了安全描述符定义语言 (SDDL)。

    安全描述符定义语言 (SDDL) 将安全描述符呈现为数据驱动结构而不是程序结构,因此开发人员和管理员现在都可以编写安全描述符。乍一看,SDDL 似乎和低级结构一样不显眼,但如果你查看足够的 SDDL 字符串,你会发现编写 SDDL 字符串比编写原始结构要简单得多。ConvertSecurityDescriptorToStringSecurityDescriptor()ConvertStringSecurityDescriptorToSecurityDescriptor() 函数可以轻松地将 SDDL 转换为安全描述符。

    Windows 2000 的另一个重要补充是 DACL 的自动继承(第一部分所述)。支持自动继承需要添加新的 API 来操作它们,你可以在 GetSecurityInfo() 函数(如果你使用自定义类,则为 GetPrivateObjectSecurityEx() 函数)中找到此新功能。

  3. 在可信计算计划期间,Microsoft 在 ATL 中添加了一组类,这些类允许 ATL 项目像调用 COM 服务器一样轻松地操作安全描述符和访问控制列表。此方法的好处是提供了一个完全面向对象的框架来进行访问控制编辑(安全应该从一开始就那样实现)。不再需要管理缓冲区、检查返回代码的类型或创建文本解析器。

    如果你足够小心,你可以让这项技术在 Windows NT 上运行!本文不会告诉你如何做到这一点(你只需要仔细阅读文档),但这是可能的。

    有一个要求,你必须重新分发 ATL DLL(或者期望一个臃肿的应用程序),如果你不拥有 Visual Studio .NET,此方法将不可用。最后,如果你是那些讨厌 ATL 的人之一,你不太可能选择此方法。

  4. (这将在下一部分讨论。)

你很可能基于你之前的访问控制经验或编程背景做出了决定。现在不幸的是,对于本文,我已经为你做了决定:“本文中的所有代码都将使用 ATL 方法”。但别担心!演示项目包含所有四种方法的等效程序。我将讨论所有问题的解决方案,每种方法一个(只有代码将以 ATL 形式呈现)。

10. SID 的乐趣

问:检索 LocalSystem 帐户的 SID。以文本形式打印 SID,并将其转储到 TRUSTEE 结构中。

LocalSystem 帐户是一个特殊的 NT 用户,代表内核和系统服务的用户名。在英语 Windows 中,它的名称是“NT AUTHORITY\SYSTEM”(仅限英语 Windows),通常对你的本地工作站拥有不受限制的访问权限。

  1. 如果你是低级方式,你需要做一些技巧。你会注意到 NT 将 LocalSystem 帐户命名为“NT AUTHORITY\SYSTEM”。通过将此名称传递给 LookupAccountName(),你可以检索 LocalSystem 帐户的 SID。然后,你可以使用此函数 [^] 来打印 SID。TRUSTEE 仅适用于 NT3.x 中不可用的 API,由于你选择此方法的唯一原因是为了支持 NT3.x,因此 TRUSTEE 对你来说几乎是无用的。
  2. 在 Windows 2000 中,你可以通过利用 SDDL 来稍微作弊。LocalSystem 的 SDDL 形式是“SY”。将此字符串传递给 ConvertStringSidToSid() 会得到所需的 SID。然后,你只需使用 BuildTrusteeWithSid() 来创建 TRUSTEE
  3. 你可以从 SID 的命名空间检索系统的 SID。现在,你有了 SID,你可能会想将其包装在 ATL 的 CSid 类中。打印 SID 对 ATL 来说非常容易,只需打印 CSid::Sid() 方法的输出即可。最后一件事是填写 TRUSTEE,这可以通过 BuildTrusteeWithSid() 来完成。
int WellKnownSid2Trustee(void)
{
  /* Wrap up the object in a CSid */
  ATL::CSid SidUser(ATL::Sids::System());

  std::wcout << SidUser.Sid();

  TRUSTEE TrusteeSid = {0};
  ::BuildTrusteeWithSid(&TrusteeSid, 
       const_cast<SID *>(SidUser.GetPSID()));

  return 0;
}

图 9:将一个知名 SID 转换为 TRUSTEE

问:你已从线程令牌中检索到你的用户名。不幸的是,它是 SID 格式。需要一个用户友好的格式。将 SID 转换为用户名。

  1. 如果你在图 8 中没有遇到问题,那么这个练习应该很容易。它基本上是上面练习的逆过程。你提供了一个 SID,所以你只需要调用 LookupAccountSid()。返回的用户名可以根据 SidType 的值格式化为 SAM 格式、别名格式或域格式。
  2. 为了表明“一猫多法”,将使用 WMI 来获取用户。连接到“root\cimv2”命名空间后,你可能需要执行以下查询:SELECT * FROM Win32_UserAccount WHERE Sid = "<SidUser>"(将 <SidUser> 替换为提供的 SID)。此查询应返回一个结果,你可以打开它来获取与该用户关联的 Win32_UserAccount。用户名位于 Name 属性中,因此检索它,最后将 BSTR 转换为普通字符串。呼!你不必做所有这些。如果你不想这样做,你可以简单地复制方法 1。(我包含了 WMI 作为解决方案,让你考虑 WMI 作为 Windows 安全编程的替代 API。)
  3. 强烈建议你将 SID 包装在 CSid 类中。这样,你就可以简单地使用 CSid 类的方法来获取用户名(在本例中为 CSid::AccountName())。
void Sid2UserName(const SID *UserSid)
{
  ATL::CSid UserCSid(UserSid /*, Domain */);
  /** This line would be unnecessary if we passed in a
  * CSid rather than an unwrapped SID
  **/
  std::wcout << UserCSid.AccountName();
}

图 10:从 SID 获取用户名。

11. 你是哪些组的成员?

问:你需要确定当前用户是否是管理员。有一个操作需要执行,如果你的程序不是以管理员身份运行,则无法正常工作。

首先,你为什么需要知道你是否是管理员?如果有一个目录或注册表项你被拒绝访问,为什么不检查该文件夹的安全描述符?如果某个 API 不起作用,可能是因为你没有启用正确的特权。其次,你正在做什么需要管理员特权?你是否正在尝试访问 Program Files(而是写入“Application data”或“My documents”)?还是你试图安装某种恶意软件(这需要管理员权限才能造成破坏)?第三,为什么你必须是管理员?为什么不能是 Power User 或 Domain Account Operator?第四,请注意,你可以通过使用 Net*Info 函数,或调用 WMI,甚至使用 Shell 提供的函数:IsUserAnAdmin() 来完成此操作。

  1. 你可以从线程令牌(或进程令牌)中获取你的整个组 membership。打开你的线程令牌(或者进程令牌,如果失败的话),然后对返回的令牌调用 GetTokenInformation(TokenGroups)。使用返回的组列表,查找组 SID,然后查看它是否是 Administrators 组的 SID。
  2. 现在 Windows 2000 中这更容易了,因为有了 CheckTokenMembership(),而不需要读取整个组列表。这是 IsUserAnAdmin() 用来检查你是否是管理员的方法。
  3. ATL 包装了 CheckTokenMembership()、访问令牌和组到其 CAccessToken 类中。只需提供 Administrators 组的正确 SID。感谢 GetEffectiveToken() 方法,在 ATL 中获取你的令牌更容易。
int IsAdminRunning(void)
{
  bool IsMember = FALSE;
  ATL::CAccessToken ProcToken;
  ATL::CAccessToken ImpersonationToken;
  ATL::CSid UserSid(Sids::Admins());

  ProcToken.GetEffectiveToken(TOKEN_READ | TOKEN_DUPLICATE);
  ProcToken.CreateImpersonationToken(&ImpersonationToken);
  ImpersonationToken.CheckTokenMembership(UserSid, &IsMember);

  return IsMember;
}

图 11:确定用户是否为管理员。

如果你处理的是域环境,你还需要查找域管理员 SID(SDDL 中的“DA”)。Platform SDK 中的一个示例包含另一个示例解决方案。

12. 启用令牌特权

问:你需要读取和编辑对象的 SACL,但只能通过启用“管理审核和安全日志”策略来读取 SACL。如何启用此“SeSecurityPrivilege”?

  1. Platform SDK 中有一个名为 SetPrivilege() 的函数。此示例代码将允许我们启用“SeSecurityPrivilege”(前提是管理员已允许我们),因此可以读取 SACL。此函数将启用特权的任务简化为一行。但在复制它之前,你想为函数添加错误处理,并让函数本身调用 OpenThreadToken()
  2. 使用方法 1。
  3. ATL 团队发现 SetPrivilege() 函数非常有用,他们创建了一个名为 CAccessToken::EnablePrivilege() 的方法,该方法只是 ATL 风格的 SetPrivilege()
void SetPrivilege(
     const ATL::CStringT<TCHAR,
     ATL::StrTraitATL<TCHAR> > &lpszPrivilege,
     bool bEnablePrivilege)
{
  ATL::CAccessToken ProcToken;
  ProcToken.GetEffectiveToken(TOKEN_QUERY |
    TOKEN_ADJUST_PRIVILEGES);

  if(bEnablePrivilege)
  {
    ProcToken.EnablePrivilege(lpszPrivilege);
  }
  else
  {
    ProcToken.DisablePrivilege(lpszPrivilege);
  }
}

图 12:启用组策略特权。

组策略特权默认是关闭的,即使在组策略中已启用。你必须通过更改访问令牌来打开它们。你应该准备好处理特权在组策略中被禁用的情况(并且无论你多么努力都无法启用它)。完成后,不要忘记将其关闭。

问:从令牌打印当前用户、可用特权列表、受限 SID 列表和组列表。

这是一个 Whoami 克隆。为了简化生活,我们不会将属性从数字解密为文本(不像 Whoami 所做的那样)。

  1. 你可以通过调用 GetTokenInformation() 并将 TokenInformation 参数设置为 TokenGroupsAndPrivileges 来获取所需信息。然后,只需打印返回结构的內容即可。
  2. 参见方法 1。
  3. 要获取令牌中的组,请调用 CAccessToken::GetGroups() 方法,它会返回一个 CSidArray 和一个 CAtlArrayDWORDSIDS_AND_ATTRIBUTES 映射)。此列表包含强制 SID 和受限 SID 的合并视图。你可以使用 CAccessToken::GetPrivileges() 来获取特权的类似结果。
void DoWhoAmI(void)
{
  size_t i = 0;
  ATL::CAccessToken ProcToken;
  ATL::CSid SidUser;
  ProcToken.GetEffectiveToken(TOKEN_QUERY);

  /* First print off the user. */
  ProcToken.GetUser(&SidUser);
  std::wcout << _T("Owner: ") << 
        SidUser.AccountName() << _T("\r\n");

  /* Now print the groups */
  ATL::CTokenGroups pGroups;
  ProcToken.GetGroups(&pGroups);
  ATL::CSid::CSidArray pSids;
  ATL::CAtlArray<DWORD> pAttributes;
  pGroups.GetSidsAndAttributes(&pSids, &pAttributes);

  /* Iterate both pSids and pAttributes simultaneously */
  std::wcout << _T("\r\nGroups\r\n");
  for(i = 0; i < pGroups.GetCount() ; i++)
    std::wcout << pSids[i].AccountName() << _T(": ") <<
        pAttributes.GetAt(i) << _T("\r\n");

  /* Get the list of Privileges */
  ATL::CTokenPrivileges pPrivileges;
  ProcToken.GetPrivileges(&pPrivileges);
  ATL::CTokenPrivileges::CNames pNames;
  ATL::CTokenPrivileges::CAttributes pGroupAttributes;
  pPrivileges.GetNamesAndAttributes(&pNames, &pGroupAttributes);

  /* Printing Privileges is very similar to */
  std::wcout << _T("\r\nPrivileges\r\n");
  for(i = 0; i < pGroups.GetCount() ; i++)
  std::wcout << static_cast<LPCTSTR>(pNames.GetAt(i))
  << _T(": ") << pGroupAttributes.GetAt(i) << _T("\r\n");

  /** TODO: the DWORDs are printed out as numbers. Convert these
  * DWORDs into text, the same text that whoami displays.
  **/
}

图 13:从 Whoami 重新生成信息。

问:如何在 Windows XP / Server 2003 中以低权限运行 IE?

此技术仅适用于 Windows XP 和 Server 2003。下一个版本的 Windows *将*更改此技术。

  1. 此方法的一个示例不可用。如果你想实现这一点,你将不得不求助于方法 2 或 3。
  2. 你可以使用 CreateRestrictedToken() 函数来处理必要的任务,或者对于 XP 及以上版本,你可以利用软件限制策略(简称 SAFER)。SAFER 函数基本上是一组预定义的受限令牌,可用于降低进程令牌的权限。
  3. ATL 已将特权列表封装到 CAtlArray 中,这使得迭代和禁用特权变得非常容易。创建受限令牌也同样容易。但是,这些令牌可能过于严格(严格到足以阻止应用程序初始化)。因此,你可能需要考虑使用软件限制策略作为替代方案。

对于方法 2,我将 SAFER 例程包装在一个类中(以抽象对象管理)。

class SaferRaiiWrapper {
public:
  /** Error handling has been added in the
  *downloadable version of this class
  **/
  explicit SaferRaiiWrapper(
    const DWORD dwScopeIdIn = SAFER_LEVELID_NORMALUSER,
    const HANDLE hTokenIn = NULL) : hToken(hTokenIn),
    LevelHandle(NULL), dwScopeId(dwScopeIdIn)
  {
    ::SaferCreateLevel(SAFER_SCOPEID_USER, this->dwScopeId,
                       SAFER_LEVEL_OPEN, &LevelHandle, NULL);
    ::SaferComputeTokenFromLevel(this->get_LevelHandle(),
                       NULL, &hToken, NULL, NULL);
  } ;


  virtual PROCESS_INFORMATION CreateProcessAsUser(const
    const std::basic_string<TCHAR> &lpCommandLine,
    STARTUPINFO *lpStartupInfoIn = NULL,
    DWORD dwCreationFlags = CREATE_NEW_CONSOLE,
    const std::basic_string<TCHAR> &lpApplicationName = _T(""),
    const std::basic_string<TCHAR> &lpCurrentDirectory = _T(""),
    LPVOID lpEnvironment = NULL, BOOL bInheritHandles = FALSE,
    SECURITY_ATTRIBUTES *lpProcessAttributes = NULL,
    SECURITY_ATTRIBUTES *lpThreadAttributes = NULL)
  {
    STARTUPINFO StartupInfoAlt = {0};
    LPSTARTUPINFO lpStartupInfoActual = (lpStartupInfoIn != NULL) ?
    lpStartupInfoIn : &StartupInfoAlt;
    PROCESS_INFORMATION Result = {0};

    TCHAR *lpCmdLineWritable = new TCHAR[sCmdLine.capacity() + 1];
    /** The command line needs to be writable.
    * So make a writable copy of our command line.
    **/
    sCmdLine.copy(lpCmdLineWritable, sCmdLine.size());
    lpCmdLineWritable[sCmdLine.size()] = _T('\0');

    lpStartupInfoActual->cb = sizeof(STARTUPINFO);
    lpStartupInfoActual->lpDesktop = NULL;
    ::CreateProcessAsUser(this->hToken,
      (sAppName.empty() ? NULL : sAppName.c_str()),
      lpCmdLineWritable, lpProcessAttributes,
      lpThreadAttributes, bInheritHandles,
      dwCreationFlags, lpEnvironment,
      (sCurDir.empty() ? NULL : sCurDir.c_str()),
      lpStartupInfoActual, &Result);

    delete [] lpCmdLineWritable;

    return Result;
  } ;

  HANDLE get_hToken(void) const
  {
    return hToken;
  } ;

  virtual ~SaferRaiiWrapper()
  {
    ::CloseHandle(this->hToken);
    ::SaferCloseLevel(this->LevelHandle);
  } ;

protected:

  const SAFER_LEVEL_HANDLE &get_LevelHandle(void) const
  {
    return LevelHandle;
  } ;
  void set_LevelHandle(const SAFER_LEVEL_HANDLE &LevelHandleIn)
  {
    this->LevelHandle = LevelHandleIn;
  } ;

  void set_hToken(const HANDLE hToken)
  {
    this->hToken = hToken;
  } ;

private:
  HANDLE hToken;
  SAFER_LEVEL_HANDLE LevelHandle;
  const DWORD dwScopeId;
};

图 14:使用软件限制策略创建受限令牌。

13. 分析安全描述符

问:如何获取文件夹的安全描述符?

问题没有具体说明安全描述符中要返回哪些特定信息,因此我们假设它希望安全描述符中返回所有信息(Control、SACL、DACL、Group 和 Owner)。

要读取 SACL,你必须首先在令牌中启用 SeSecurityPrivilege(使用图 10 中方便的 SetPrivilege() 函数)。

  1. 如果你在 Windows 2000 或更高版本上,请不要尝试使用此方法(检测 Windows 版本,并分支到单独的代码),否则你将破坏操作系统的安全性。要获取文件的安全描述符,请调用 GetFileSecurity() API(如果你已经有了句柄,则为 GetKernelObjectSecurity())。
  2. 如果你已打开文件,请对打开的文件句柄调用 GetSecurityInfo()。否则,请对文件名调用 GetNamedSecurityInfo()。由于我们只获取安全描述符,因此可以将其他参数设置为 NULL。完成后不要忘记 LocalFree() 安全描述符。
  3. GetNamedSecurityInfo() API 已封装到全局 ATL 函数:AtlGetSecurityDescriptor()。它为我们返回 CSecurityDesc 类型的信息。默认情况下,AtlGetSecurityDescriptor() 会自动启用 SeSecurityPrivilege,因此无需在此处使用 SetPrivilege()
int GetFolderSecDesc(const CStringT<TCHAR, 
       ATL::StrTraitATL<TCHAR> > &FileName)
{
  ATL::CSecurityDesc OutSecDesc;
  ATL::AtlGetSecurityDescriptor(FileName, SE_FILE_OBJECT, &OutSecDesc);
  return 0;
}

图 15:获取文件夹的安全描述符。

GetNamedSecurityInfo() 也可用于读取注册表项、内核对象、窗口站和其他对象上的安全描述符。有关 GetNamedSecurityInfo() 支持的对象列表,请参阅第 17 部分或帮助文档中的SE_OBJECT_TYPE [^]。如果你的对象不受 GetNamedSecurityInfo() 支持,那么请自行打开一个句柄(使用 READ_CONTROL 访问权限),并将其传递给 GetSecurityInfo() 函数。

返回的安全描述符将是自相对形式。如果你要枚举安全描述符,那么如果安全描述符是绝对的,会更容易。

问:将自相对安全描述符转换为绝对安全描述符。

  1. 你必须调用 MakeAbsoluteSD() API 来创建绝对安全描述符。MakeAbsoluteSD() 函数不会为你分配缓冲区,你必须自己分配。你必须管理五个缓冲区,仅仅是为了一个安全描述符!如果你必须将安全描述符传递回操作系统(正如我们在第四部分 [^] 中将要做的),那么内存泄漏的可能性就非常大。你可以维护五个全局变量来跟踪缓冲区,或者你可以分配一个大的内存块,并通过一些有创意的指针修复,让其他缓冲区指向这个大缓冲区(这种技术在C FAQ [^] 中有描述)。现在,分配了缓冲区,并且你的指针指向足够大的内存位置,下一次调用 MakeAbsoluteSD() 应该就可以正常工作了。
  2. 如果你的自相对安全描述符在此任务中将保持在范围内,那么你可以自己构建绝对安全描述符。使用 GetSecurityDescriptorDacl()GetSecurityDescriptorOwner() 等函数可以为你提供所需的指针。
  3. ATL 包含 CSecurityDesc::MakeAbsolute() 方法,使得转换安全描述符更容易。更重要的是,你不再需要担心管理缓冲区;ATL 会为你处理缓冲区。请注意,转换为安全描述符的大部分原因在 ATL 中并不需要。(ATL 安全类可以处理绝对安全描述符以及自相对安全描述符。)
...
OutSecDesc.MakeAbsolute();
...

图 16:将自相对安全描述符转换为绝对安全描述符。

反过来(即,将绝对安全描述符转换为自相对安全描述符)要容易得多。原因是一个绝对安全描述符必须维护五个缓冲区才能工作(或者在我们的例子中,是五个指针的堆),而一个自相对安全描述符只需要维护一个缓冲区。好消息是,除非你需要使用方法 1,否则很少需要转换安全描述符。

你可能一直在问,为什么不分配一个大小与自相对安全描述符相同的缓冲区,将其重新解释为绝对安全描述符,然后将偏移量索引转换为物理指针。问题是你假设索引的大小与指针相同。这在 Win64 上不成立,尝试这样做会导致错误(是的,Microsoft 应该使自相对安全描述符中的 DWORD 索引大小无关紧要,但现在我们只能忍受这个 17 年前的错误)。

问:你已经提供了一个安全描述符。现在需要打印出安全描述符的内容。

虽然没有提到,但这个问题要求安全描述符采用调试器格式或 SDDL 格式。

  1. 首先,确保安全描述符是绝对形式(这样更容易阅读)。有一组函数可用于获取安全描述符的各个部分。它们是 GetSecurityDescriptorLength()GetSecurityDescriptorControl()GetSecurityDescriptorOwner()GetSecurityDescriptorGroup()GetSecurityDescriptorDacl()GetSecurityDescriptorSacl()。这些函数分别返回长度、控制位、所有者、组、DACL 和 SACL(无论安全描述符是自相对还是绝对)。获取组和所有者后,你应该以文本形式打印 SID。稍后将介绍如何打印访问控制列表。
  2. 在这种情况下,你将受益于 SDDL。一旦你有了安全描述符,你就可以调用 ConvertSecurityDescriptorToStringSecurityDescriptor()。这将把安全描述符(绝对或自相对)转换为 SDDL 字符串,你可以将其打印出来。
  3. ATL 可以使用 CSecurityDesc::ToString()CSecurityDesc 转换为 SDDL 字符串。
...
ATL::CString pstr = _T("");
OutSecDesc.ToString(&pstr);
std::wcout << static_cast<LPCTSTR>(pstr);
...

图 17:打印安全描述符的内容。

现在你已经以统一的方式(SDDL)呈现了安全描述符,你已将解析安全描述符的任务简化为文本处理任务。

14. 遍历访问控制列表。

问:你已经提供了一个访问控制列表。将此 ACL 转换为访问控制条目数组。

结果应为一个包含三列的表:SID(deny | allow | audit | alarm) 继承,以及 ACCESS_MASK

  1. 要在 NT3.x 中遍历访问条目列表,你首先必须读取 ACL 头以获取 ACE 的计数(如果没有条目则不要继续)。要获取第 n 个访问控制条目的指针,你需要对 ACL 调用 GetAce()。此函数返回一个 void*。指针处的第一个字节标识结构的精确类型(这让我想起了 RTTI 的原始版本)。一旦你将 void* 转换为正确的结构,你就可以从该结构中获取所需详细信息。SID 位于 SidStart 成员(将此成员强制转换为 SID)。要检查拒绝或允许,请检查你的结构名称(它是 ACCESS_ALLOWED_ACE 结构还是 ACCESS_DENIED_ACE?)。精确的继承类型可以从 AceFlags 获取。最后一项(访问掩码)可以从 Mask 成员获取。对所有 ACE 重复此过程。
  2. 将安全描述符转换为 SDDL 格式是最简单的。然后你可以对返回的安全描述符执行文本处理,并从 SDDL 打印所需的内容。
  3. CDaclCSacl 类派生自 CAcl。然后,你可以通过调用 GetAclEntries() 方法来获取 ACL 列(返回四个数组:SID、访问掩码、类型和继承)。或者,你可以通过调用 GetAclEntry() 来逐行遍历 ACL。我个人宁愿将 ACL 解密为 SDDL 格式并在那里打印。

如果 ACL 是系统访问控制列表,你将得不到允许/拒绝条目。相反,你将在 SACL 中得到审核/警报条目。为了让你的遍历函数既能读取 SACL 也能读取 DACL,请扩展你的遍历函数以处理审核和警报 ACE struct(示例代码中提供的遍历函数可以同样处理 SACL 和 DACL)。

void ReadDacl(const ATL::CDacl &pDacl)
{
  UINT i = 0;
  for(i = 0; i < pDacl.GetAceCount(); i++)
  {
    ATL::CSid pSid;
    ACCESS_MASK pMask = 0;
    BYTE pType = 0, pFlags = 0;
    const_cast<ATL::CDacl &>(pDacl).GetAclEntry
        (i, &pSid, &pMask, &pType, &pFlags);
    std::wcout << pSid.AccountName() << _T(": ");
    switch (pType)
    {
      case ACCESS_ALLOWED_ACE_TYPE:
        std::wcout << _T("allow");
        break;
      case ACCESS_DENIED_ACE_TYPE:
        std::wcout << _T("deny");
        break;
      case SYSTEM_AUDIT_ACE_TYPE:
        std::wcout << _T("audit");
        break;
      case SYSTEM_ALARM_ACE_TYPE:
        std::wcout << _T("alarm");
        break;
      /* ... TODO: Repeat for the other structures */
      default:
        std::wcout << _T("Unknown");
        break;
    }
    std::wcout << _T(": ");
    if(pFlags & INHERITED_ACE)
      std::wcout << _T("Inherited: ");
    std::wcout << std::hex << 
         pMask << std::dec << std::endl;
  }
  std::wcout << std::endl;
}

图 18:读取和打印自由访问控制列表。

15. 我是否有权访问?

问:你需要确定一个特定的安全描述符是否允许你访问一个对象,而不会收到可怕的错误 5(ERROR_ACCESS_DENIED)。如何做到这一点?

一种天真的实现方法是查找安全描述符中的你的用户名,并直接检查授予了哪些访问权限,拒绝了哪些访问权限(GetEffectiveRightsFromAcl() 可以提供帮助)。使用此技术存在两个问题。

  • 你的用户名可能实际上没有出现在安全描述符中——而是你的组出现在其中。
  • 一个单独的条目可能不会授予你所需的访问权限。可能是两个访问控制条目,一个授予你部分所需访问权限,另一个授予剩余访问权限。

唯一可靠的检查方法是实际执行操作(即打开文件并读取)。如果成功,则授予访问权限。如果失败并出现错误 5,则拒绝访问。但是,如果你真的必须……

要检查安全描述符是否授予你访问权限,你需要调用 AccessCheck() API。AccessCheck() API 是 AccessCheckByTypeResultListAndAuditAlarmByHandle() API 的简化形式。AccessCheckByTypeResultListAndAuditAlarmByHandle() 是整个 Windows 访问控制模型的核心。所有安全 API 和对象只是配置此小 API(好吧,也许不是那么小!)行为的方式。但就我们的目的而言,AccessCheck() 应该足够了。AccessCheck() 一开始看起来可能令人生畏,但仔细观察,它只接受三个参数:安全描述符、*你*(你的线程令牌)以及你期望的操作(所需的访问掩码)。AccessCheck() 的其余部分只是 Out 参数。

你可能会认为 Windows 可以通过使这三个参数成为可选参数来让你更容易。为什么 AccessCheck() 不能默认获取当前线程令牌,然后你传入文件名,AccessCheck() 会自己查找其安全描述符。这只是*一个* In 参数。哦,对了,这只是 CreateFile()!这又回到了我们最初的说法。

实际上,AccessCheck() 要求你提供第四个参数,即 GENERIC_MAPPING 结构。此结构将对象特定的 ACL(如 GENERIC_READ)映射到对象特定的权限(如 FILE_GENERIC_READ)。AccessCheck() 需要 GENERIC_MAPPING 的原因是因为它调用了 AreAllAccessesGranted() 函数,而该函数要求你提供 GENERIC_MAPPING 结构。Larry Osterman [^] 提供了更完整的原因,说明了为什么需要 GENERIC_MAPPING

  1. 要检查安全描述符的访问权限,首先创建一个 GENERIC_MAPPING 结构。
  2. 收集此结构、你的安全描述符、所需的访问权限和你的线程令牌。
  3. 进行第一次调用 AccessCheck()。我们期望此调用失败。
  4. 如果调用因 ERROR_INSUFFICIENT_BUFFER 而失败,则为 PRIVILEGE_SET 结构分配缓冲区。
  5. 使用新缓冲区再次调用 AccessCheck()
  6. 检查 AccessStatus 参数中的结果。如果为 true,请检查 GrantedAccess 成员是否等于所需的访问权限。
  7. 如果出现任何问题,则拒绝访问。

正如在第一部分中所讨论的,可以自己进行访问检查。涉及的十个步骤是

  1. 使用 OpenThreadToken() 打开你的令牌(线程或进程)。
  2. 调用 GetTokenInformation(TokenGroups) 来检索组列表(如图 11 所示)。
  3. 从你提供的安全描述符中,访问 DACL。(参见图 15。)如果它是 null,你应该使用 DACL:Everyone(完全控制)。
  4. 获取第 n 个 ACE(如图 18 所示)。
  5. 获取与此 ACE 关联的 SID(如图 18 所示)。
  6. 在步骤 2 中获得的组列表(TOKEN_GROUPS 数组)中查找此 SID。
  7. 返回 ACE 并查找其类型和访问掩码(参见图 18)。
  8. 使用 MapGenericMask() 将任何通用访问权限映射到提供的 GENERIC_MAPPING 结构。
  9. 比较当前访问掩码与所需的访问掩码。

    要比较两个 ACCESS_MASK,只需对其中一个 ACCESS_MASK 使用 NOT,然后将两个变量 AND 在一起。结果应为零(如果你被授予访问权限),否则将被拒绝。或者你可以调用 AreAllAccessesGranted() 来帮助你。(此 API 的优点是可以帮助你修复通用访问权限。)

  10. 如果所需的访问掩码被 ACE 覆盖,则授予访问权限。
  11. 如果 ACE 没有完全允许访问,则清除授予的访问权限并继续搜索。
  12. 如果你已到达末尾,则拒绝访问。

你可以执行上述 11 个步骤,或者可以使用 AccessCheck() 函数。Windows 2000 或 ATL 并没有提供任何特殊的东西来使这项任务更容易;此技术对所有操作系统都相同。

{
  ATL::CAccessToken ProcToken, ImpersonationToken;
  ProcToken.GetEffectiveToken(TOKEN_QUERY |
    TOKEN_DUPLICATE | TOKEN_IMPERSONATE);
  ProcToken.CreateImpersonationToken(&ImpersonationToken);

  {
    BOOL AccessStatus = FALSE;
    DWORD GrantedAccess = 0, PrivilegeSetLength = 0,
    DesiredAccess = FILE_GENERIC_WRITE;
    GENERIC_MAPPING GenericMapping =
      {
        READ_CONTROL | FILE_READ_DATA |
        FILE_READ_ATTRIBUTES | FILE_READ_EA,
        FILE_WRITE_ATTRIBUTES | FILE_WRITE_EA |
        FILE_WRITE_DATA | FILE_APPEND_DATA,
        READ_CONTROL | FILE_READ_ATTRIBUTES |
        FILE_EXECUTE,
        FILE_ALL_ACCESS
      } ;

    ::AccessCheck(const_cast<SECURITY_DESCRIPTOR *>
    (OutSecDesc.GetPSECURITY_DESCRIPTOR()),
      ImpersonationToken.GetHandle(), DesiredAccess, &GenericMapping,
      NULL, &PrivilegeSetLength,
      &GrantedAccess, &AccessStatus);

    ATL::CAutoVectorPtr<BYTE> PrivilegeSet
      (new BYTE[PrivilegeSetLength]);

    ::AccessCheck(const_cast<SECURITY_DESCRIPTOR *>
    (OutSecDesc.GetPSECURITY_DESCRIPTOR()),
      ImpersonationToken.GetHandle(), DesiredAccess,
      &GenericMapping, reinterpret_cast<PPRIVILEGE_SET>
    (static_cast<BYTE *>(PrivilegeSet)), 
      &PrivilegeSetLength, &GrantedAccess,
      &AccessStatus);
    if(AccessStatus == TRUE)
    {
      std::wcout << std::hex <<
      GrantedAccess==DesiredAccess << std::dec;
    }
  }
}

图 19:验证安全描述符是否授予你对对象的访问权限

16. 创建自由访问控制列表。

本部分假设你已经知道你的访问控制列表包含什么(有关选择良好 DACL 的建议,请参阅第一部分 [^])。请注意,每个安全描述符都与它所保护的对象存在紧密耦合关系。原因是每个 ACE 都包含一个 ACCESS_MASK 成员,这是一个依赖于对象的 a value。

问:你现在知道你的自由访问控制列表的内容了。现在要求你构建它。

这是我们将要构建的示例 ACL。这是一个典型的用户配置文件下文件的 DACL

Allow LocalSystem: Full Control (FILE_ALL_ACCESS), and propagate to all children. 
Allow Admins: Full Control (FILE_ALL_ACCESS), and propagate to all children.
Allow CurrentUser: Read Write & Execute (FILE_GENERIC_READ | 
FILE_GENERIC_EXECUTE | FILE_GENERIC_WRITE), and propagate to all children.

图 20a:构建此示例 DACL。

在 SDDL 中是

"(A;OICI;FA;;;SY)(A;OICI;FA;;;BA)(A;OICI;GRGX;;;<CurrentUserSid>)"

图 20b:SDDL 中的示例 DACL

  1. 这可能是访问控制中最难的部分。如果你正在编辑安全描述符而不是从头开始创建,你需要先获取旧的安全描述符。然后,在添加 DACL 的条目时,确保以 ACE 的首选顺序添加 ACE。
    1. 计算 ACL 的总大小(ACL 需要是一个连续的块,可以容纳 ACL 结构、所有简单 ACE 的大小减去 SidStart 成员、所有对象 ACE 的大小减去 SidStart 成员,以及所有 SID 的大小)。
    2. ACL_HEADER 分配一个缓冲区,该缓冲区很可能需要 LocalAlloc()
    3. 如果你是从头开始构建安全描述符,请调用 InitializeAcl() 来初始化 ACL 头。否则,你可以从现有 ACL 复制信息。
    4. 构建一个访问拒绝 ACE 数组。
    5. 重新分配你的 ACL,使其可以在其之后容纳此 ACE 数组。
    6. (可能不需要此步骤)获取指向你的 ACL 之后的空闲空间的指针(可以通过调用 FindFirstFreeAce(),或自己移动指针)。
    7. 通过调用 AddAccessDeniedAce() 将拒绝 ACE 添加到 ACL。
    8. 对允许 ACE 重复步骤 3-7(调用 AddAccessAllowedAce() 而不是 AddAccessDeniedAce())。
    9. 构建好 ACL 后,通过调用 SetSecurityDescriptorDacl() 将绝对安全描述符的 Dacl 成员设置为此成员。

    有趣的是,这是三种方法中唯一一种可以制作无序 DACL 和 NULL DACL 的方法。

  2. 在 Windows 2000 中,编辑 DACL 就像向字符串追加文本一样简单。从 SDDL 字符串构建你的访问控制列表。一旦构建了 SDDL,调用 ConvertStringSecurityDescriptorToSecurityDescriptor() 函数来构建安全描述符。这将给你一个安全描述符。然后只需使用 GetSecurityDescriptorDacl() 提取 DACL。如果你正在编辑现有的安全描述符,你可以从头开始,构建一个全新的 DACL,或者你可以采用现有的 SDDL,并在此基础上进行构建。
  3. 你可以在 ATL 中通过将 SDDL 提供给 CSecurityDesc::FromString() 来构建安全描述符,或者使用 CDacl 类来构建。如果你正在编辑安全描述符,你应该首先获取安全描述符并调用其 GetDacl() 方法。你可以通过调用 AtlGetDacl() 直接从对象获取 Dacl。否则,自己实例化一个新的 Dacl 对象。无论创建方式如何,你都调用 AddDeniedAce() 添加一个访问拒绝条目,然后调用 AddAllowedAce() 添加一个访问允许条目。
...
pDacl.AddAllowedAce(ATL::Sids::LocalSystem(), FILE_ALL_ACCESS,
  CONTAINER_INHERIT_ACE | OBJECT_INHERIT_ACE);
pDacl.AddAllowedAce(ATL::Sids::Admins(), FILE_ALL_ACCESS,
  CONTAINER_INHERIT_ACE | OBJECT_INHERIT_ACE);
pDacl.AddAllowedAce(ATL::CSid(CurrentUser), FILE_GENERIC_READ |
  FILE_GENERIC_WRITE | FILE_GENERIC_WRITE, CONTAINER_INHERIT_ACE |
  OBJECT_INHERIT_ACE);

ATL::AtlSetDacl(FileName, SE_FILE_OBJECT, pDacl);
...

图 20c:创建要应用于文件的访问控制列表。

17. 创建安全对象

问:这对自由访问控制列表来说都很好,但系统访问控制列表呢?

除了管理和/或故障排除目的,你**永远**不需要系统访问控制列表(我还没有遇到过除测试目的以外的 SACL)。无论如何,你只能在启用 SeSecurityPrivilege 时设置 SACL。如果你的对象支持继承,只需从父对象获取 SACL。否则,你的 SACL 应该为 NULL 或为空。请记住,如果你每次访问对象时都生成审核(特别是如果频繁访问对象),你的安全事件日志将充满信息过载。

要读取和写入 SACL,你需要启用 SeSecurityPrivilege。向 SACL 添加条目类似于创建 DACL。

  1. 在低级而不是调用 AddAce() 来添加 ACE,你调用 AddAuditAccessAce() 函数。
  2. 在 SDDL 中,SACL 只是一个前面带 S 的 DACL!审核 ACE 字符串以“AU”开头而不是“A”,警报以“AL”开头。
  3. CSaclCDacl 之间的唯一区别在于它们添加 ACE 的方式。否则,其余方法相同。
...
pSacl.AddAuditAce(ATL::Sids::Users(), WRITE_DAC, true, false);
...

图 21:向 SACL 添加系统访问控制条目。

问:你现在已经创建了 DACL 和 SACL。现在如何处理其他成员?

本节将涵盖仅保护新对象。对于新对象,默认情况下对象没有所有者。你可以通过填充安全描述符的 Owner 成员来指定所有者。建议的新所有者是*你*(*你*来自你的线程令牌)或你的组。但是,任何对对象具有 WRITE_OWNER 访问权限的用户都可以拥有它(从而获得对其的完全控制)。

很少有应用程序读取安全描述符的 Group 部分,但以防万一,这应该设置为你的令牌的主组,从 GetTokenInformation(TokenPrimaryGroup) 获取。

Control 成员是一组转储到 32 位整数的标志。Windows 将使用此参数来确定安全描述符的哪些成员是有效的。如果你正在保护支持继承的对象,那么在安全描述符的 Control 成员中还有额外的标志需要设置。如果你在此成员中出错(例如,你说组有效但实际上不是),你可能会崩溃。

要设置这些成员,请遵循你选择的方法

  1. 在图 17 中,我们讨论了一组可以获取安全描述符部分的函数。这些函数具有相应的 Set 函数,是的,你猜对了,它们设置安全描述符的各个部分。这些函数仅在安全描述符被构建为绝对形式时才有效(看看从一开始创建绝对安全描述符有多么有用?)。SetSecurityDescriptorControl() API 被故意设计得复杂,以防止你意外地将绝对安全描述符转换为自相对安全描述符。你必须同时提供替换值和你打算更改的控制位。
  2. 如果你将所有者和组 SID 转换为用户名,你将不得不将它们转换回来。然后,要构建所有者,在你的 SDDL 前面追加字符“O:”和字符串 SID。对于组,先追加“G:”而不是“O:”。控制位直接在“D:”和“S:”令牌中设置。
  3. 一旦你的成员构建完毕,你就可以通过调用以下方法来设置安全描述符部分:CSecurityDesc::SetControl()CSecurityDesc::SetGroup()CSecurityDesc::SetOwner()CSecurityDesc::SetDacl()CSecurityDesc::SetSacl()
...
OutSecDesc.SetOwner(ATL::Sids::Admins(), false);
OutSecDesc.SetGroup(ATL::Sids::Admins(), false);
...

图 22:为新对象完成安全描述符。

问:继承是如何融入其中的?

安全描述符的继承出现在两个地方。每个 ACE 都有一个继承标志,指定如何将继承应用于子对象/容器。这些标志(IOOICI 等)已在第一部分中描述。相同的标志也决定了一个 ACE 是否来自父 ACL(你可以使用 INHERITED_ACE 标志检测到)。

Control 成员决定 DACL 和 SACL 是否自动继承其父级的 ACE。通过在安全描述符中设置 SE_DACL_AUTO_INHERITED | SE_DACL_AUTO_INHERIT_REQ 控制标志,Windows 将获取父级的 DACL,将其附加到你的 DACL 末尾,并将合并后的 DACL 写入对象。这些继承的 ACL 不能编辑——如果你想更改继承 ACL 中的内容,你必须添加一个拒绝 ACE 来覆盖继承的 ACE,或者停止继承。

要阻止继承,你必须将你的 DACL 设置为受保护(SE_DACL_PROTECTED)。如果你设置了这个标志,那么只有显式条目会保留,这意味着你可能需要将父级的 DACL 复制到对象中才能获取旧的 DACL。

如果你有一个受保护的 DACL,并且你想停止它被保护,你应该清空 DACL,然后设置 SE_DACL_AUTO_INHERIT_REQ 控制位。这将禁用 DACL 保护并启用自动继承。

  1. 低级方法不支持继承。这本身就足以让你考虑其他方法。至少,你必须为不同的 Windows 版本分支到单独的代码路径,实际上创建程序的两个版本。
  2. 你可以从 SDDL ACE 字符串获取继承信息。在 ACE 字符串的第二个标记中,有决定该 ACE 继承的标志(可以是“IO”、“OI”、“CI”、“ID”和“NP”)。对于控制位,你可以通过在 DACL 分隔符后面加上“AI”来设置自动继承,例如:D:AI(A;ID;FA;;;SY)

    D:”后面的“AI”告诉 SDDL 在 Control 位中设置 SE_DACL_AUTO_INHERITED。如果不是“AI”而是“P”,则设置 SE_DACL_PROTECTED。最后一个标志“AR”对应于 SE_DACL_AUTO_INHERIT_REQ

  3. 你可以使用 CSecurityDesc::SetControl() 直接编辑安全描述符控制标志。此函数需要你提供两个参数。一个是 Control 成员的新值,另一个是你希望设置的标志。这是为了防止你意外地将安全描述符从自相对转换为绝对。
...
OutSecDesc.SetControl(SE_DACL_AUTO_INHERITED |
SE_DACL_PROTECTED, SE_DACL_AUTO_INHERITED);
...

图 23:支持安全描述符的继承。

SACL 的继承规则与 DACL 相同。

问:创建一个带有你的安全描述符的 Windows NT 对象。

当你创建 Windows 对象时,你会遇到一个要求 SECURITY_ATTRIBUTES 结构(除非你遇到了资源包装器类/函数)的参数。你将在这里为新对象提供安全描述符。请注意,某些对象不支持安全描述符的所有功能(特别是继承)——在这种情况下,这些额外功能将被忽略(如果你不小心,可能会导致对象无法访问)。一旦你传入了这个参数,就可以了。安全描述符的存储方式是对象的问题,而不是你的问题。但是,为了避免意外,你应该确保对象被创建(而不仅仅是打开)。如果对象已存在,则安全描述符不适用,安全属性将被忽略。

通常,为了访问 SECURITY_ATTRIBUTES,你可能需要直接使用 Win32 API。这是因为大多数包装器类会忽略 SECURITY_ATTRIBUTES 参数,并将 NULL 传递给此类。(CAtlFile 是一个例外,但默认情况下,它也会传递 NULL。)

if(::GetFileAttributes(FileName) == INVALID_FILE_ATTRIBUTES)
{
  SECURITY_ATTRIBUTES lpSecurityAttributes =
    {sizeof(SECURITY_ATTRIBUTES),
    const_cast<SECURITY_DESCRIPTOR *>
  (OutSecDesc.GetPSECURITY_DESCRIPTOR()), FALSE};

  ::SetLastError(ERROR_SUCCESS);
  /** We're going to use the low level Win32 APIs
  * instead of ATL::CAtlFile
  **/
  ATL::CHandle FileHandle (::CreateFile(FileName, GENERIC_ALL |
     READ_CONTROL | WRITE_DAC, 
     FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
     &lpSecurityAttributes,
     CREATE_NEW, FILE_ATTRIBUTE_NORMAL, NULL));
  if( static_cast<HANDLE>(FileHandle) == NULL ||
      static_cast<HANDLE>(FileHandle) == INVALID_HANDLE_VALUE)
  {
    throw ATL::CAtlException(HRESULT_FROM_WIN32(::GetLastError()));
  }
}
/* The else part will be handled in fig. 24. */

图 24:创建具有自定义安全描述符的对象。

如果对象已存在,此技术不起作用。如果你打开一个现有对象,安全描述符将被忽略。

问:对象已存在。需要编辑当前安全描述符,而不是创建。如何做到?

这个问题假设你已知要保护的对象的名称和类型,或者你拥有该对象的句柄。如果你要保护一个现有对象,你可以创建一个不完整的安全描述符(例如,一个没有所有者或组的安全描述符)。你通过传递 SECURITY_INFORMATION 变量来告诉 Windows 安全描述符的哪些部分是有效的。例如,如果你只是设置对象的 DACL,你可以指定 DACL_SECURITY_INFORMATION

  1. 在此方法中,解决方案特定于你要设置的对象。如果你深入研究安全文档,你可能会找到正确的函数。例如,要设置文件的安全描述符,你需要调用 SetFileSecurity()。对于注册表项,它是 RegSetKeySecurity()。较旧的 API 都不支持继承,因此如果你在 Windows 2000 系统上使用这些 API,可能会严重损坏继承。
  2. 设置安全描述符的首选 API 是 SetSecurityInfo()。此外,它支持继承(和多重继承)。虽然此函数最早出现在 Windows NT 3.51 中,但早期版本存在一些错误,导致它只能在有限的情况下应用。在 Windows NT4 SP6 中,所有这些错误都已修复。

    如果你无法获取文件句柄(例如,你还没有 READ_CONTROLWRITE_DAC 访问权限),你可以调用类似的 SetNamedSecurityInfo() 函数来设置文件的安全性。但是,SetSecurityInfo() 函数可以保护比 SetNamedSecurityInfo() 更多的对象。

    SetNamedSecurityInfo() 可以设置以下对象上的安全性

    • SE_FILE_OBJECT:文件系统对象(文件或目录)。
    • SE_SERVICE:NT 服务。
    • SE_PRINTER:本地或远程打印机。
    • SE_REGISTRY_KEY:注册表项。
    • SE_LMSHARE:NetBIOS 共享。
    • SE_KERNEL_OBJECT:信号灯、事件、互斥体、可等待计时器或文件映射。
    • SE_WINDOW_OBJECT:窗口站。
    • SE_DS_OBJECT:目录服务对象的特定属性。
    • SE_DS_OBJECT_ALL:目录服务对象的所有属性。
    • SE_PROVIDER_DEFINED_OBJECT:提供程序定义的。对象。
    • SE_WMIGUID_OBJECT:WMI 对象。
    • SE_REGISTRY_WOW64_32_KEY:WOW32 注册表项(仅在 64 位应用程序内部工作)。

    要使 SetNamedSecurityInfo() 工作,你需要将安全描述符拆分为单独的部分。你可以调用 GetSecurityDescriptorDacl() 等函数来获取这些成员(可移植)。需要注意的一点是,SetSecurityInfo() 似乎不允许你直接设置控制位。相反,你在 SECURITY_INFORMATION 参数中提供控制位。Control 位到 SECURITY_INFORMATION 的映射如下:

    • SE_DACL_PRESENT --> DACL_SECURITY_INFORMATION
    • SE_SACL_PRESENT --> SACL_SECURITY_INFORMATION(确保你有 SeSecurityPrivilege)。
    • SE_DACL_AUTO_INHERITED --> UNPROTECTED_DACL_SECURITY_INFORMATION
    • SE_SACL_AUTO_INHERITED --> UNPROTECTED_SACL_SECURITY_INFORMATION(确保你有 SeSecurityPrivilege)。
    • SE_DACL_PROTECTED --> PROTECTED_DACL_SECURITY_INFORMATION
    • SE_SACL_PROTECTED --> PROTECTED_SACL_SECURITY_INFORMATION(确保你有 SeSecurityPrivilege)。
    • SE_DACL_DEFAULTED --> [无]。SetSecurityInfo() 不关心安全描述符是否为默认值。
    • SE_SACL_DEFAULTED --> [无]。SetSecurityInfo() 不关心安全描述符是否为默认值。
    • SE_GROUP_DEFAULTED --> GROUP_SECURITY_INFORMATION
    • SE_OWNER_DEFAULTED --> OWNER_SECURITY_INFORMATION
    • SE_SELF_RELATIVE --> [无]。SetSecurityInfo() 不关心安全描述符是自相对还是绝对。

    使用此映射将控制位转换为 SECURITY_INFORMATION 标志。这个新的安全描述符将替换旧的安全描述符。如果这不是你想要的(即,你想向 DACL 添加条目而不是替换它),你必须自己合并旧条目和新条目,或者你可以应用自动继承(这些将自动应用于子级)。

    如果你有权限,安全描述符应该会以正确的继承设置和排序的条目应用于对象。Set*SecurityInfo() 不支持 NULL DACL,如果你尝试设置一个,则会失败。

  3. 而不是调用 SetSecurityInfo(),你需要逐个设置安全描述符的部分。要设置所有者,请调用 AtlSetOwnerSid()。类似地,你有 AtlSetGroupSid()AtlSetDacl()AtlSetSacl() 来设置组 DACL 和 SACL。在内部,这些函数调用 SetSecurityInfo(),因此方法 2 的规则与 ATL 相同。如果你在调试版本中,ATL 会在你尝试将 NULL DACL 设置到对象时发出警告。
...
ATL::AtlSetDacl(FileName, SE_FILE_OBJECT, pDacl);
ATL::CSacl pSacl;
/* We've already set the Dacl. Now set the SACL. */
OutSecDesc.GetSacl(&pSacl, &pbPresent);
if(pbPresent)
{
  ATL::AtlSetSacl(FileName, SE_FILE_OBJECT, pSacl);
}
ATL::CSid pOwner, pGroup;
if(OutSecDesc.GetOwner(&pOwner))
{
  ATL::AtlSetOwnerSid(FileName, SE_FILE_OBJECT, pOwner);
}
if(OutSecDesc.GetGroup(&pGroup))
{
  ATL::AtlSetGroupSid(FileName, SE_FILE_OBJECT, pGroup);
}

...

图 25:将安全描述符应用于现有对象。

为了使 SetSecurityInfo() 成功,你必须拥有 WRITE_DAC 权限来设置 DACL 和 WRITE_OWNER 权限来设置所有者。如果你被拒绝 WRITE_OWNER | WRITE_DAC 权限,你可以通过获取文件的所有权来获得这些权限。启用 SeTakeOwnershipPrivilege 将允许你获取文件的所有权(即使 WRITE_OWNER 被禁用)。

虽然 Set*SecurityInfo() 函数适用于几乎所有 Windows 内置对象(甚至一些特殊对象),但你可能希望为自己的类实现安全描述符。

18. 让你的类安全

问:这个安全描述符模型看起来很酷。有没有办法用它来保护我的对象?

可以将类成员类型设置为 PSECURITY_DESCRIPTOR 来保护对你的对象的访问。如果你想让你的安全描述符可写,你必须在你的属性函数中添加特殊逻辑。特别是在更新时,你需要在新安全描述符合并到当前安全描述符。

你可以使用自己的逻辑来管理安全描述符(以 SDDL 形式管理会更容易)。或者你可以使用一个特殊的 API,CreatePrivateObjectSecurity(),来为你完成。首先,你必须决定你想限制你班级的哪些方法(每个安全描述符最多可以限制 16 种方法)。这些方法必须包括在 GENERIC_MAPPING 结构中指定的通用操作(即使你不支持它们——在这种情况下只提供空方法)。

Platform SDK 示例中的类定义了图 26 中的操作作为其私有对象的一部分。该类不仅应该将其自己的方法映射到这些操作,还应该将 GENERIC_MAPPING 结构映射到此结构(没有这个 GENERIC_MAPPING 结构,AccessCheck() 函数将不起作用)。

ACCESS_READ// ==1 <-- GENERIC_MAPPING::GenericRead
ACCESS_MODIFY// ==2 <-- GENERIC_MAPPING::GenericWrite
ACCESS_DELETE // ==4
ACCESS_ALL// ==7 <-- GENERIC_MAPPING::GenericAll

图 26:一套由自定义类执行的操作的示例。

在类的构造函数中,调用 CreatePrivateObjectSecurity() 并将返回的安全描述符存储在你的一名类成员中。现在你已经将安全描述符与你的类关联起来了。如果你需要更新安全描述符(例如,在配置更改期间),请调用 SetPrivateObjectSecurity()。此函数将合并旧的安全描述符和你的安全描述符。

当你准备执行某个操作时(即调用你的某个方法时),你应该调用 AccessCheckAndAuditAlarm()。你在这里调用是因为如果安全描述符中有任何审计条目,你将希望触发一个审计事件。AccessCheckAndAuditAlarm() 要求你提供审计事件日志的信息。调用完成后,调用 ObjectCloseAuditAlarm()

如果你的类具有父子关系模型(如文件夹),你将希望在安全描述符中支持继承。在这种情况下,你将使用 *PrivateSecurity 函数的 Ex 变体。这些函数有两个额外的参数:一个 GUID(以防你的类多重继承),以及 AutoInheritFlagsAutoInheritFlags 控制如何从父对象应用继承,并且还可以减少启用特权的开销。

当你完成使用该类后,请在析构函数中调用 DestroyPrivateObjectSecurity() 来释放资源。ATL 不提供特殊的类来处理私有安全对象。因此,只需直接调用私有安全 API。对于方法 1,私有安全对象仅适用于类(在 C 中不可用)。

class SecureClass
{
private:
  PSECURITY_DESCRIPTOR ppSD;
  double len;
  CAccessToken ProcToken;


  bool CheckClassAccess(DWORD RightsToCheck) const;
  /* This helper is where we do the access check */

public:
  enum Rights { /* The set of rights */
    ReadLen = 1,
    WriteLen = 2,
    SetClassSecurity = 4,
    CopyClass = 8
  };

  ~SecureClass();
  SecureClass(int FullRights, const CHandle &ThreadHandle);

  SecureClass(const SecureClass &OldClass);
  /* Copy constructor needs CopyClass access */

  double get_len(void) const ;
  /* You must have ReadLen rights to access this property */

  void set_len(double Newlen) ;
  /* You must have WriteLen rights to write this property */

  void set_SecDesc(SECURITY_INFORMATION psi,
    PSECURITY_DESCRIPTOR pNewSD);
  /** You need SetClassSecurity rights to access
  * this method
  **/
};

图 27:通过安全描述符安全访问的类的轮廓。

19. 摘要

问:速查表

要构建一个安全描述符,信息量确实很大(尤其是如果你使用方法 1!)。大多数时候你只想检索和编辑对象的安全描述符。我已总结了必要的步骤,因此你不必记住以上所有内容

  1. 步骤
    1. 如果需要启用任何特权,请先执行此操作。
    2. 找出你要保护的对象。
    3. 使用 READ_CONTROL/WRITE_DAC 访问权限获取对象的句柄(或者获取对象名称)。
    4. 调用相应的 Get*Security() API,具体取决于对象类型。如果对象尚不存在,请调用 InitializeSecurityDescriptor()
    5. 将返回的自相对安全描述符转换为绝对安全描述符。
    6. 通过调用 GetSecurityDescriptor*() 及其友元函数将安全描述符拆分为各个部分。
    7. 如果需要,设置所有者 SID,然后使用 SetSecurityDescriptorOwner() 将其重新应用于安全描述符。
    8. 为组 SID 重复步骤 7。
    9. 如果你正在编辑 DACL,请获取当前的 DACL 及其大小。
    10. 根据当前大小和新条目的大小,计算结果 DACL 所需的大小。
    11. 为此大小分配缓冲区。
    12. 除非是从头开始,否则将旧 DACL 复制到此缓冲区(GetAce()AddAce())。
    13. 使用 FindFirstFreeAce()AddAce() 将新条目添加到 ACL,必要时重新分配。
    14. 使用 SetSecurityDescriptorDacl() 将编辑后的 DACL 应用于安全描述符。
    15. 为 SACL 重复步骤 9-12。
    16. 构建好安全描述符后,调用相应的 Set*Security() API,具体取决于对象类型。
    17. 如果对象尚不存在,请创建一个包含安全描述符的 SECURITY_ATTRIBUTES 结构。
    18. 使用相关 API 创建对象。将 SECURITY_ATTRIBUTES 参数设置为你在步骤 17 中创建的参数。
    19. 祈祷有一天你可以使用方法 2 或 3 来重做这一切,因为这只在 Windows NT 3.x 和 4.0 上工作。
  2. 首先启用任何必需的特权。然后构建一个 SDDL 字符串来表示你的所需安全描述符。构建后,通过调用 ConvertStringSecurityDescriptorToSecurityDescriptor() 将 SDDL 转换回安全描述符。将安全描述符拆分为各个部分,然后使用 Set*SecurityInfo() 将其应用于对象。如果你需要从现有安全描述符构建,你可以调用 Get*SecurityInfo() 来检索它。比方法 1 简单得多,无需转换为绝对形式,并且支持自动继承。
  3. 通过调用 AtlGetSecurityDescriptor() 获取安全描述符,然后使用 ToString() 方法将其转换为 SDDL 格式。然后继续方法 2。

在本部分中,我们展示了如何获取 SID,打印它,将其转换为 TRUSTEE,以及将其转换为用户名。你从访问令牌中提取了信息,例如你是谁,你是哪些组的成员,受限组列表,特权列表及其状态。你启用和禁用了一个特权,并创建了一个受限令牌来运行低特权应用程序。

接下来,你从文件中获取了一个安全描述符,将其转换为绝对形式,然后提取了安全描述符的五个部分。你将安全描述符转换为 SDDL 格式,并打印了其内容。对于 DACL,我们向你展示了如何创建和编辑访问控制列表,并打印其内容。然后你编辑了一个安全描述符,对其应用了继承规则,最后用它保护了一个对象(预定义和自定义对象)。

我们向你展示了如何在 NT3.x 样式、2000 样式和 ATL 样式中完成所有这些。所有这些都汇集到 AccessCheck() 函数,该函数检查你是否被允许访问某个资源。

我省略了域问题和多个继承对象,因为它们超出了本系列的范围。要了解有关这些内容,我建议查阅 Windows 资源工具包。演示项目包含本部分的所有代码,以方法 1、2 和 3 编写。目前样本并未设计成可重用,但如果兴趣足够,我可能会改变这一点(我将真正的功能留给第四部分)。要编译方法 2,你需要安装并启用 boost::regex 库和 WMI SDK。要编译方法 3,你需要 ATL 库。

即将推出

第三部分是第二部分的重复。但是,下一部分将使用 C# 和 .NET 2.0 编写。如果你对使用 .NET 编程 Windows 访问控制感兴趣,请继续阅读第三部分。如果你不感兴趣,也请阅读——它可能会诱使你转向 .NET 框架(嗯,可能不会!)。

历史记录现在维护在第四部分 [^] 中。

下一部分... [^]

© . All rights reserved.