Michael Howard
Microsoft Corporation
2002 年 6 月 14 日
安全领域存在这样一种观点:执行某项任务只要有刚好足够的权限就可以了。为什么是这样呢?还是让我从一个小故事讲起吧。几年前,我在一家大银行从事安全咨询和编程工作。上班第一天,我惊讶地发现我的帐户竟然是域管理员组的成员。我询问为什么我是域管理员时,负责我工作的主管告诉我,因为我将要开发应用程序,而开发应用程序就要求我是一名域管理员。我立即要求将我的帐户改为普通用户,而不要作为管理员。原因很简单,开发应用程序不需要用户拥有这样的特权帐户。但是为什么不能是域管理员呢?原因同样简单,如果银行遇到任何严重危及安全的情况,域管理员都是第一个受怀疑的对象,因为他们拥有那么大的能力。如果我的帐户只是一个简单用户,而简单用户做不了太多事情,因此也就少有机会危及系统的安全。当然,域管理员应该是值得信任的人,但我宁愿是一个没有什么特权的可信用户。不妨称我为偏执狂!
最小权限和操作系统
使用应用程序时,最小权限的概念同样适用。运行应用程序时,您的权限应当始终以足够完成工作为准,而没有必要拥有更大的权限。阅读邮件时,您无需拥有管理员权限;撰写文档时,也只需要您的帐户是普通用户就可以了。某些任务要求比较大的权限,例如配置计算机和管理用户帐户。但是让人诧异的是,几乎没有什么任务要求用户具有至高无上的权限,因此,大多数用户没有必要拥有管理员权限。
为什么要在软件中设置最小权限呢?
好,现在我们来讨论一下为什么要在软件中设置最小权限。假设攻击者可以利用您的进程做一些危险的事情,再假设您的代码正在以较高的权限运行。猜猜危险的黑客会拥有什么级别的权限?没错,是您的进程的所有权限。因此,如果该进程正在由具有管理员权限的用户使用,危险的黑客将具有相同的权限。一旦危险的黑客具有管理员权限,他就可以利用计算机为所欲为。真正的为所欲为!有一个很好的例子:如果您的代码存在缓冲区溢出漏洞,攻击者注入的代码就可以以管理员身份运行。从这个故事得到的教训是:除非绝对需要管理员权限,否则切勿以管理员身份运行您的应用程序。
为什么应用程序要求高级权限呢?
通常,有三个原因要求以高级权限执行应用程序。这三个原因是:
- 访问控制列表 (ACL) 问题。
- 权限问题。
- 使用 LSA 机密。
下面我们来详细介绍每个原因。
ACL 问题
假设 NTFS 分区上有一个文件夹,该文件夹具有以下 ACL:
- SYSTEM(完全控制)
- Administrators(完全控制)
- Everyone(读取)
除非您是管理员或系统帐户(许多服务都以系统帐户运行)等特权帐户,否则您只能在该文件夹中读取文件。您无法写入、无法删除、也无法做其他任何事情。如果您的应用程序尝试执行读取之外的任何文件输入/输出操作,则将收到访问被拒绝的错误信息。要习惯这一信息——拒绝访问是错误 #5!
这是一个非常常见的问题。将数据写入文件系统的保护区域或操作系统的其他部分(例如注册表)的应用程序要求应用程序用户具有管理员权限,才能正确运行。您知道有多少种游戏可以将排行榜信息写入 C:/Program Files 目录吗?让我替您回答,非常非常多。这就存在一个问题,因为这意味着游戏玩家必须是管理员。由于很多游戏都允许用户通过 Internet 与别人一起玩,这意味着他们必须打开套接字,如果游戏套接字处理代码中出现缓冲区溢出或类似的漏洞,攻击者就可以利用这一漏洞运行代码,并且代码将以管理员身份运行。游戏结束!
ACL 问题略有不同:它是开放的资源,拥有的权限比需要的要多。例如,假设某个文件上存在上面定义的相同 ACL,代码将为 GENERIC_ALL 打开该文件。那么,用户需要使用什么帐户才能使代码运行不会失败呢?Administrator(管理员)或 SYSTEM(系统)帐户。GENERIC_ALL 等同于完全控制。也就是说,您希望打开该文件,并且能够对文件执行任何操作,但是您的代码只需要读取文件。需要为 GENERIC_ALL 打开该文件吗?不,当然不。代码可以为 GENERIC_READ 打开该文件,但想想会发生什么情况?运行此应用程序的任何用户都可以成功地打开该文件,因为在该文件中存在 Everyone(读取)ACE。
记住,在 Windows NT® 或更高版本中,您要么得到所需的权限,要么就会收到拒绝访问的错误。如果您要求得到所有访问权限,而资源上的 ACL 只允许读取,则您将不会被授予读取访问权限,而将被告知需要获取更高权限。
解决 ACL 问题
解决 ACL 问题的方法主要有两种。第一种是使用所需权限打开资源。如果您想读取注册表项,则只需请求只读访问权限即可,无需更多。这非常简单,而且不太可能使应用程序中出现回归错误。
另一种解决方法是不要将用户数据写入操作系统受保护的部分。这包括(但不仅限于)注册表 HKEY_LOCAL_MACHINE 配置单元、C:/Program Files(或 %PROGRAMFILES% 环境变量指向的目录)和 C:/Windows 目录 (%SYSTEMROOT%)。而应当将用户信息存储在 HKEY_CURRENT_USER 中,将用户文件存储在用户的配置文件目录中。您可以使用以下代码段来确定用户的配置文件目录:
#include "shlobj.h" ... TCHAR szPath[MAX_PATH]; ... if (SUCCEEDED(SHGetFolderPath(NULL, CSIDL_PERSONAL NULL, 0, szPath)) { HANDLE hFile = CreateFile(szPath, ...); ... }
请注意,如果您当前的应用程序将用户数据存储在操作系统受保护的部分,而您决定将数据移到用户不需要管理员权限就可以安全存储他们自己的数据的区域,则您需要提供迁移工具来迁移现有的全部数据。否则,您会遇到向后兼容的问题,因为用户将无法访问他们现有的数据。
权限问题
在基于 Windows NT 代码的所有 Windows® 版本中,当而且仅当某个帐户具有适当的权限时,才能执行某些操作。例如,对于不是在开发人员自己的帐户下运行的应用程序,调试该应用程序将要求开发人员帐户具有调试权限。(如果进程在您自己的帐户下运行,则无需此权限就可以进行调试。)对安全性非常敏感的操作具有很多其他权限限制,例如备份和恢复文件(绕过 ACL 检查)的能力、加载设备驱动程序(将代码加载到内核中)的能力等等。
坦率地说,目前还没有找到解决权限问题的简易方法。如果您的帐户需要特定的权限才能完成某项工作,那么事情很简单,您需要该权限。但是,请防止管理员给您的帐户添加太多具有潜在危险的权限或要求您的用户拥有过多不必要的权限。
使用 LSA 机密
本地安全授权 (LSA) 可以为应用程序存储机密数据。控制 LSA 机密的 API 包括 LsaStorePrivateData 和 LsaRetrievePrivateData。这里就出现了一个问题:要使用 LSA 机密,执行这些任务的进程必须是本地管理员组的成员。请注意平台 SDK 中有关 LsaStorePrivateData 的说明:“数据在存储之前被加密,密钥具有 DACL,只允许创建者和管理员读取数据。”而事实上,只有管理员才能使用这些 LSA 功能,因此,如果应用程序采用最小权限目标,而您想做的只是为用户存储一些机密数据,这就会成为一个问题。
解决 LSA 机密问题
在 Windows 2000 或更高版本中,您可以找到一种称为数据保护 API 或 DPAPI 的解决方案。使用 DPAPI 有四大理由。
- 用户访问机密数据时不要求是管理员,数据使用绑定到用户的密钥进行保护,因此数据的所有者有权访问数据。
- 您只需要留意 CryptProtectData 和 CryptUnprotectData 这两个 API。使用 LSA 机密的代码(事实上涉及 LSA 的所有代码)很快就会变得非常复杂。
- 向数据中添加了一种消息验证代码 (MAC),以便验证数据的完整性。
- DPAPI 不会为您存储数据,但是会为您提供您所保留的二进制大对象。这非常有用,因为您可以将数据存储在文件系统或注册表中,并可以将这些数据与所有其他用户数据一起备份。
下面的简单代码显示了如何调用 DPAPI。
#include <stdio.h> #include "windows.h" #include "wincrypt.h" #include "stdlib.h" int main(int argc, char *argv[]) { if (argc != 2) { printf("请提供一些机密数据。"); return -1; } // 要保护的数据 DATA_BLOB blobIn; blobIn.pbData = reinterpret_cast<BYTE *>(argv[argc-1]); blobIn.cbData = lstrlen(reinterpret_cast<char *>(blobIn.pbData))+1; // 可选熵 DATA_BLOB blobEntropy; blobEntropy.pbData = reinterpret_cast<BYTE *>("*71hdm2%b/x12w9B"); blobEntropy.cbData = lstrlen(reinterpret_cast<char *>(blobEntropy.pbData)); // 记录所有操作 DWORD dwFlags = CRYPTPROTECT_AUDIT; // 加密数据 DATA_BLOB blobOut; if(CryptProtectData( &blobIn, L"写入安全代码示例", &blobEntropy, NULL, NULL, dwFlags, &blobOut)) { printf("保护已生效。/n"); } else { printf("CryptProtectData() 中出错 -> %x", GetLastError()); return -1; } // 解密数据 DATA_BLOB blobVerify; if (CryptUnprotectData( &blobOut, NULL, &blobEntropy, NULL, NULL, 0, &blobVerify)) { printf("解密的数据为: %s/n", blobVerify.pbData); } else { printf("CryptUnprotectData() 中出错 -> %x", GetLastError()); } if (blobOut.pbData) LocalFree(blobOut.pbData); if (blobVerify.pbData) LocalFree(blobVerify.pbData); return 0; }
Windows 9x 的遗留问题
我们遇到的最大问题(也就是本文所起标题的原因)是:旧的应用程序在 Windows 95、Windows 98 和 Windows Me 上运行良好,但是在 Windows 2000 或 Windows XP 上却因为新的操作系统提供了额外保护而无法正常运行。记住,在 Windows 9x 中,没有 ACL 和权限的概念。任何用户都可以对操作系统的任何部分执行写入操作,而根本没有拒绝访问的概念。除非用户是管理员,否则许多应用程序在 Windows 2000 和 Windows XP 上都无法正常运行;并且根据我的经验,这些问题 90% 以上都是由于 ACL 引起的。下一代应用程序要符合最小权限原理尚需时日。您的应用程序在这方面表现如何?
找出弱点
你们中的一些人找出了上篇文章中的错误。答案是 g_wszComputerName
和 szComputerName
的长度看起来似乎都是 INTERNET_MAX_HOST_NAME_LENGTH+1
,但事实上它们不一样。g_wsComputerName
是 WCHAR
或 Unicode 字符的一个数组,其中每个 WCHAR
都是 16 位。因此,调用 GetServerVaraible
会尝试将两倍数量的字节复制到 szComputerName
,从而产生缓冲区溢出漏洞。
现在,让我们来看看,您能发现以下 ASP 代码中的安全漏洞吗?
Hello, <% response.write(request.querystring("Name")) %>
答案是……下次再告诉您吧!