最小权限的挑战

安全领域存在这样一种观点:执行某项任务只要有刚好足够的权
限就可以了。为什么是这样呢?还是让我从一个小故事讲起吧。
几年前,我在一家大银行从事安全咨询和编程工作。上班第一
天,我惊讶地发现我的帐户竟然是域管理员组的成员。我询问为
什么我是域管理员时,负责我工作的主管告诉我,因为我将要开
发应用程序,而开发应用程序就要求我是一名域管理员。我立即
要求将我的帐户改为普通用户,而不要作为管理员。原因很简
单,开发应用程序不需要用户拥有这样的特权帐户。但是为什么
不能是域管理员呢?原因同样简单,如果银行遇到任何严重危及
安全的情况,域管理员都是第一个受怀疑的对象,因为他们拥有
那么大的能力。如果我的帐户只是一个简单用户,而简单用户做
不了太多事情,因此也就少有机会危及系统的安全。当然,域管
理员应该是值得信任的人,但我宁愿是一个没有什么特权的可信
用户。不妨称我为偏执狂!

最小权限和操作系统

使用应用程序时,最小权限的概念同样适用。运行应用程序时,
您的权限应当始终以足够完成工作为准,而没有必要拥有更大的
权限。阅读邮件时,您无需拥有管理员权限;撰写文档时,也只
需要您的帐户是普通用户就可以了。某些任务要求比较大的权
限,例如配置计算机和管理用户帐户。但是让人诧异的是,几乎
没有什么任务要求用户具有至高无上的权限,因此,大多数用户
没有必要拥有管理员权限。

为什么要在软件中设置最小权限呢?

好,现在我们来讨论一下为什么要在软件中设置最小权限。假设
攻击者可以利用您的进程做一些危险的事情,再假设您的代码正
在以较高的权限运行。猜猜危险的黑客会拥有什么级别的权限?
没错,是您的进程的所有权限。因此,如果该进程正在由具有管
理员权限的用户使用,危险的黑客将具有相同的权限。一旦危险
的黑客具有管理员权限,他就可以利用计算机为所欲为。真正的
为所欲为!有一个很好的例子:如果您的代码存在缓冲区溢出漏
洞,攻击者注入的代码就可以以管理员身份运行。从这个故事得
到的教训是:除非绝对需要管理员权限,否则切勿以管理员身份
运行您的应用程序。

为什么应用程序要求高级权限呢?

通常,有三个原因要求以高级权限执行应用程序。这三个原因
是:

访问控制列表 (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
#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(argv[argc-1]);
blobIn.cbData = lstrlen(reinterpret_cast(blobIn.pbData))+1;

// 可选熵
DATA_BLOB blobEntropy;
blobEntropy.pbData = reinterpret_cast(“*71hdm2%b\x12w9B”);
blobEntropy.cbData =
lstrlen(reinterpret_cast(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,

答案是……下次再告诉您吧!

——————————————————————————–

Michael Howard 是 Microsoft Secure Windows Initiative 组
的安全程序经理,是 Writing Secure Code(英文)的作者之
一,也是 Designing Secure Web-based for Applications
for Windows 2000(英文)的主要作者。他的主要工作就是确保
人们设计、构建、测试和记录无缺陷的安全系统。他最喜欢的话
是“尺有所短,寸有所长”。

发表评论

电子邮件地址不会被公开。