如果你的应用程序必须处理密码或非公开密钥(例如会话密钥、私钥或密钥),请尝试隐藏它们并在使用后立即覆盖它们,以最大限度地减少暴露。
像 Linux 这样的系统支持 mlock() 和 mlockall() 调用,以防止内存被分页到磁盘(因为有人可能会稍后从交换文件中获取密钥)。请注意,在 Linux 上,这是一个特权系统调用,这会引发其自身的问题(我是否应该授予程序超级用户权限以便它可以调用 mlock,即使它在其他情况下不需要这些权限?)
此外,如果你的程序处理此类秘密值,请务必禁用创建核心转储(通过 ulimit)。否则,攻击者可能能够停止程序并在数据转储中找到秘密值。
注意 - 通常,进程可以通过调试器的调用(例如,通过 ptrace(2) 和 /proc 伪文件系统)来监视其他进程 [Venema 1996]。如果进程是 setuid 或 setgid,内核通常会防止这些监视例程(在少数旧内核上,如果没有,除了升级之外,真的没有好的方法来保护自己)。因此,如果你的进程管理秘密值,你可能应该将其设置为 setgid 或 setuid(到不同的非特权组或用户),以强制禁止此类监视。除非你需要它是 setuid,否则使用 setgid(因为这授予的权限更少)。
然后是实际覆盖值的问题,这通常变得特定于语言和编译器。在许多语言中,你需要确保将此类信息存储在可变位置,然后覆盖这些位置。例如,在 Java 中,不要使用 String 类型来存储密码,因为 String 是不可变的(它们不会被覆盖,直到垃圾回收然后重用,可能在遥远的将来)。相反,在 Java 中使用 char[] 来存储密码,这样可以立即覆盖它。在 Ada 中,使用 String 类型(字符数组),而不是 Unbounded_String 类型,以确保你对内容有控制权。
在许多语言(包括 C 和 C++)中,请注意编译器不要优化掉用于覆盖值的“死代码” - 因为在这种情况下它不是死代码。许多编译器,包括许多 C/C++ 编译器,会删除对不再使用的存储的写入 - 这通常被称为“死存储消除”。不幸的是,如果写入真的是为了覆盖秘密的值,这意味着看起来正确的代码将被默默地丢弃。Ada 提供了 pragma Inspection_Point;将其放在擦除内存的代码之后,这样你就可以确定包含秘密的对象将被真正擦除(并且覆盖不会被优化掉)。
Andy Polyakov 在 Bugtraq 上的一篇文章(2002 年 11 月 7 日)报告说,C/C++ 编译器 gcc 版本 3 或更高版本、SGI MIPSpro 和 Microsoft 编译器消除了旨在覆盖秘密的简单内联调用 memset。C 和 C++ 标准允许这样做。其他 C/C++ 编译器(例如 gcc 版本低于 3)在所有优化级别都保留了 memset 的内联调用,表明该问题是特定于编译器的。简单地声明目标数据是 volatile 对所有编译器都没有帮助;MIPSpro 和 Microsoft 编译器都忽略了简单的“volatile 化”。简单地“触摸”秘密数据的第一个字节也没有帮助;他发现 MIPSpro 和 GCC>=3 巧妙地仅将第一个字节置零,而保持其余部分完整(这实际上非常聪明 - 问题是编译器的聪明才智正在干扰我们的目标)。一种似乎在所有平台上都有效的方法是编写你自己的 memset 实现,其中对第一个参数进行内部“volatile 化”(此代码基于 Michael Howard 提出的解决方法)
void *guaranteed_memset(void *v,int c,size_t n) { volatile char *p=v; while (n--) *p++=c; return v; } |