找回密码
 注册
搜索
热搜: 回贴
微赢网络技术论坛 门户 安全攻防 查看内容

保护内存中的敏感数据

2009-12-14 01:41| 发布者: admin| 查看: 12| 评论: 0|原作者: 云忆

目标很简单,但潜在的困难却很多 — 这里描述了如何避免泄密
具有安全意识的程序员经常需要保护内存中诸如密码和密钥那样的敏感数据。为了有效地做到这一点,程序员应该使敏感数据在内存中保留的时间尽可能地短,并应尝试确保该数据从不写入磁盘。
简介
当要从用户的角度保护敏感数据(例如密码和密钥)时,程序员可能常常会觉得不值得去关注安全。程序员可能觉得没有必要采取安全措施,因为数据只在本地使用。然而,程序员应该考虑由他们的程序使用中的数据的价值。
如果攻击者通过某些方法闯入机器并捕获诸如密码等信息,那么会发生什么呢?在这种情况中,攻击者肯定已获取了对您应用程序中帐户的某种访问权。另外,为了使事情简单些,人们往往会在不同种类的帐户中重用密码。
有人可能会说将相同的密码用于多个帐户的那些人是自作自受。这个理论将过多的安全责任推卸给了可能更需要关心其它事情的用户;应用程序的职责就是在安全方面合理地下些功夫。另外,这种马虎的方法对那些并未重用密码的人是不公平的,因为攻击者仍可以相当容易地危及他所能涉及的帐户的安全。
因此,您应该关注用户可能认为是敏感的任何数据。首要的高级目标应该是要使敏感数据在内存中保留的时间尽可能地短。而且当需要存储这样的数据时,应该尽量防止该数据被未授权方恢复。
这些目标很简单,但实际上通常很难实现。本文中,我们将探究在实际应用程序中保护数据的最佳方法。
要防备什么
假设攻击者闯入了一个正在运行您软件的系统。如果那个攻击者十分坚定和具有充分手段的话,那么您无法阻止他获取您在内存中的数据。然而,如果从运行的程序中抽取密码很困难,那么大多数闯入计算机的人都不太可能花大力气去这样做。因为还有许多成熟的水果悬挂在低得多的地方。
最重要的是要避免将敏感数据放入文件系统的文件中。如果您一定要在文件中将敏感数据存储任意长的时间(即,如果您不能使用密码校验和),那么应该使用加密来保护数据。我们将在下面更详细地讨论这一点。
此外,您应该在程序崩溃时防止您的程序将内存转储遗留在各处。内存转储存储在常规的旧文件中,而且在这样的文件发生崩溃时,很容易获取内存中的 ASCII 字符串。通过使用 setrlimit 调用,您可以禁止核心转储。(rlimit 是“resource limit”的缩写。)
#include
#include
#include
int main(int argc, char **argv){
struct rlimit rlim;
getrlimit(RLIMIT_CORE, &rlim);
rlim.rlim_max = rlim.rlim_cur = 0;
if(setrlimit(RLIMIT_CORE, &rlim)) {
exit(-1);
}
...
return 0;
}
通常,使用 setrlimit 的最佳方法是首先调用 getrlimit 以获取当前的资源限制,更改那些值,然后调用 setrlimit 以设置新值。这样确保所有的资源限制总是具有合理的值。
另一种可以将您的数据存储到磁盘的方法是通过交换调出。操作系统可以决定获取内存中运行的部分程序,并将它们保存到磁盘。在 C 中,您也许能够“锁定”数据以免交换调出;程序通常需要管理特权来成功地做到这一点,但尝试一下也无妨。这里是可能情况下锁定内存的简单方法:
#include
void *locking_alloc(size_t numbytes) {
static short have_warned = 0;
void *mem = malloc(numbytes);
if(mlock(mem, numbytes) && !have_warned) {
/* We probably do not have permission.
* Sometimes, it might not be possible to lock enough memory.
*/
fprintf(stderr, "Warning: Using insecure memory!\n");
have_warned = 1;
}
return mem;
}
mlock() 调用通常锁定比您希望的要多的内存。它以页面为单位执行锁定。内存范围中的所有页面都将在 RAM 中锁定,而且在任何环境下都不会被交换调出,直到进程通过使用 mlock() 来解锁同一个页面中的某些内容为止。
这里会有一些潜在的负面结果。首先,如果进程锁定的碰巧是同一个页面上的两个缓冲区,那么解锁这两个缓冲区中的任一个都将解锁整个页面,从而引起这两个缓冲区都解锁。其次,当在锁定大量数据时,很容易锁定比所需更多的页面(数据一旦已分配好,操作系统就不会到处移动它了),这样会大大降低机器性能。
因此,应该同时分配可能需要包含敏感数据的所有内存,最好是分配在一个大块中。假定所有数据都分配到单个页面上,那么当您需要保护内存时,应该锁定整个块。当您在某一特定时刻不需要保护内存时,就解锁整个块(当没有数据要保护时,没必要冒降低性能的风险)。
除了调用 munlock() 以外,解锁内存块看起来与锁定它完全相同:
munlock(mem, numbytes);
如果您需要大量敏感数据,那么通过使用 mlockall() 调用,锁定地址空间中的所有内存是有可能的。但是由于会对性能有潜在影响,您应该尽量避免这个调用。
在多数情况中,这些调用是不可用的:通常,程序不能在管理权限下运行;在其它情况中,正在使用的语言根本不支持页面锁定。在这样的情况中,最好的办法是使用尽可能小的内存块,并尽快地使用和擦除它,这样易受攻击性的窗口就会降到最小。 如果从数据放入缓冲区到擦除它的期间,您频繁地访问缓冲区,那么风险将降到最低;调页规则总是要(但并不一定)防止交换有问题的页面。
获取将不交换的内存块的另一种方法是使用 RAM 磁盘。也就是说,操作系统将向您提供一个“磁盘驱动器”,它实际上是系统内存的一部分。它比 mlock() 更容易管理,但需要一个非常不标准的环境。如果您认为这对于您可能是个可行的选项,请参阅参考资料以获取关于 RAM 磁盘的链接。其它操作系统可能提供加密的交换空间,或者可以用于存储敏感数据的加密的文件系统(请参阅参考资料)。然而,这些方法都不常用。
另外,也许对从内存上真正擦除数据您还有问题。我们将在下一节讨论这个问题。
从内存擦除数据
如果可能的话,尝试避免将敏感数据保存在一起。例如,当保存一个登录数据库时,许多人选择根本不存储密码。实际上,他们保留一个密码散列,这只是原始密码高质量的校验和。为了验证用户,您只需重新计算输入密码的校验和,并查看它是否与已存储的校验和匹配。这个方法的主要优点是应用程序从来不必存储实际密码 — 这改善了终端用户的安全和私密。
当然,在某些时候您可能别无选择,而只能以其原始格式处理密码。例如,用户不直接输入校验和;这需要从输入文本进行计算。在这样的情况中,您应该在计算校验和后立即擦除密码。
要擦除内存中的数据,应覆盖数据本身。以下代码在 C 程序中不能满足要求:
int validate(char *username) {
char *password;
char *checksum;
password = read_password();
checksum = compute_checksum(password);
password = 0;
return !strcmp(checksum, get_stored_checksum(username));
}
这不能胜任,因为我们尚未擦除存储密码的实际内存;我们只擦除了指向密码的指针。如果我们知道密码是以 null 为结尾的,那么我们可以象这样擦除它:
/* Overwrite a string with 0's, until we get to the null terminator. */
void erase_string(char *s) {
while(*s) { *s = 0; }
}
否则,我们需要知道数据长度:
memset(buf, 0, numbytes); /* set numbytes to 0, starting at buf. */
在其它语言中,擦除敏感数据可能更困难。高级语言经常含有不可变的数据类型。程序在创建时对不可变对象只能写入一次。例如,请考虑以下 Python 代码:
pw = input() # Returns an ASCII string
pw = compute_checksum(pw) # Computes the cryptographic hash, overwriting pw
甚至在这个代码运行后,未加密的密码可能仍旧在内存中存在。这是因为将 pw 指派给 compute_chcksum(pw) 的结果将可能没有覆盖以前存储 pw 的实际内存。取而代之的是,将在内存别处创建一个新字符串。
您可能认为通过直接覆盖这个字符串的每个字符可以修正这个问题。您可以尝试以下代码:
for i in range(len(pw)):
pw[i] = 0
不幸的是,这个方法不起作用,因为 Python 不允许用户覆盖字符串的任何部分。您无法知道这种语言何时决定真正覆盖已存储的内存。Java、Perl、Tcl 和大多数其它高级语言中的字符串都有这一完全相同的问题。
这个问题唯一的解决方案是使用可变的数据结构。也就是说,您必须只使用允许您动态替换元素的数据结构。例如,在 Python 中,可以使用列表来存储字符数组。然而,每次您从列表上添加或除去元素时,根据实现细节,这种语言可能会背着您复制整个列表。为了安全起见,如果您必须动态调整数据结构的大小,那么您应该创建一个新数据结构,复制数据,然后覆盖旧的数据结构。例如:
def paranoid_add_character_to_list(ch, l):
"""Copy l, adding a new character, ch. Erase l. Return the result."""
new_list = []
for i in range(len(l)):
new_list.append(0)
new_list.append(ch)
for i in range(len(l)):
new_list[i] = l[i]
l[i] = 0
return new_list
对敏感数据不能使用不可变的数据类型,这一点十分不便。例如,在大多数高级语言中,不再能够使用标准输入函数来读入字符串;您必须一次读入一个字符。根据所用的语言,这可能是它本身的和其中的一项艰巨的任务。
即使一些支持垃圾收集的语言提供可变的数据类型,但类似问题仍存在于这些语言中。当敏感内存正在使用时,垃圾收集器可能复制它们(出于效率目的)。只支持引用计数的语言(例如 Python)将没有这个问题。然而,即使大多数 Java 实现没有这个问题,但是它们中的一些还是可能有问题。
擦除磁盘
您可能需要将敏感数据存储在硬盘上,而没有保护它们。您的应用程序可能需要查看一个敏感文档,而它实在太大而不能同时全部放在内存中。加密可能是某些环境中保护文档的一个选项,但在其它环境中可能出于性能考虑而禁用加密。最佳的解决方案是当文件在使用时就尝试保护它,并尽快删除它。但当我们删除该文件时,它真的被删除了吗?
通常,“删除”文件意味着只是在文件系统中除去了指向文件的文件系统项。该文件仍存在于某处,至少在它被覆盖以前如此。不幸的是,即使该文件被覆盖,它还将存在。磁盘技术就是这样,如果有合适的设备并告知了工作原理,还是能恢复已覆盖的文件。有人声称如果您希望安全地删除文件,那么您应该对它覆盖七次。第一次,全部用 1 覆盖,第二次全部用 0。接着,用 1 和 0 的交替模式来覆盖。最后,用随机数(例如从 /dev/urandom 或类似的来源生成的随机数)对该文件覆盖四次。
遗憾的是,这种技术可能还是不能满足要求。人们普遍认为美国政府拥有成功对付这种方案的磁盘恢复技术。如果您真的在乎这个问题,那么我们建议实现 Peter Gutmann 的 35-pass 方案作为最低限度的解决方案(请参阅参考资料)。
当然,如果有人告诉您覆盖数据所需的最多次数,那么他一定在误导您。没人知道要多少次才够。如果您根本不想冒险,那么需要确保从未将任何重要内容加密地写入磁盘,并将它们直接解密到锁定的内存。别无其它选择。
结束语
当要处理软件应用程序中的敏感数据时,很容易忽视安全这个问题,特别是因为很难正确地做到这一点。目前的高级语言是存在安全问题最多的地方,它们无法为安全地保存数据提供良好的机制,而且常常使得完全擦除敏感数据变得很困难或不可能。
将来的编程语言可能会有效地处理这个安全要求。到那以前,当处理敏感数据时,开发人员将被迫做出艰难的权衡。
参考资料
请阅读 Peter Gutmann 的论文 Secure Deletion of Data from Magnetic and Solid-State Memory。
在 LinuxSecurity.com 上了解 filesystem encryption 的重要信息。
在 LinuxFocus.org 站点上查找如何在 Linux 下使用 Ramdisk。
有关更多的随机信息,请查看“Make your software behave: Random number generation”,它是 developerWorks 上 Gary McGraw 和 John Viega 合著的三部系列“摆弄数字、消除偏差和软件策略”。
关于作者
John Viega(viega@list.org)是 Building Secure Software(Addison-Wesley,2001)和 Java Enterprise Architecture(O'Reilly 和 Associates,2001)的合著者。John 已经撰写了 50 余篇主要涉及软件安全领域的技术出版物。他还编写了 Mailman(即 GNU Mailing List Manager)和 ITS4(一种在 C 和 C 代码中查找安全薄弱环节的工具)。

最新评论

QQ|小黑屋|最新主题|手机版|微赢网络技术论坛 ( 苏ICP备08020429号 )

GMT+8, 2024-10-1 01:20 , Processed in 0.189280 second(s), 12 queries , Gzip On, MemCache On.

Powered by Discuz! X3.5

© 2001-2023 Discuz! Team.

返回顶部