覆盖字符串中的值 char[] 是否更安全

信息安全 爪哇 记忆 反射
2021-08-19 08:31:00

我说的是Java实现。使用反射来访问java.lang.String实例中类型的内部字段并覆盖包含的值(如下例所示)是否更安全?char[]

举个为什么有人可以使用它的例子:有人输入密码,在使用密码(可能是授权)之后,这个确切的字符串将没有用,除了读出内存,他可以覆盖它,密码为清晰可读的字符串将超出内存。

示例实现:

SecureRandom random = SecureRandom.getInstanceStrong();
String source = "1234SecureThingy"

Field charField = String.class.getDeclaredField("value");
charField.setAccessible(true);

char[] noise = new char[source.length()];
for (int i = 0; i < source.length(); i++) {
    noise[i] = (char) random.nextInt(Character.MAX_VALUE + 1);
}

charField.set(source, noise);

TL;DR:使用反射删除字符串的值是否更安全?

2个回答

通过摆弄String实例的内部内容,您将面临严重破坏应用程序的风险。

第一个原因是String实例应该是不可变的,这意味着实例可以被重用;当您修改“您的”字符串时,您实际上可能会修改其他在概念上不同但恰好具有相同内容的字符串。这种重用也可以在内部发生,如果String实例真的引用了一个char[]带有几个索引的底层来分隔该数组中的一个块。有关更多详细信息,请参阅此页面。一般来说,使用String实例的代码依赖于它们的不变性,而打破这种不变性可能会导致影响深远的令人不快的后果。

第二个原因是String实例的内部内容没有记录,并且可能会发生变化事实上,他们已经这样做了好几次了。如果我们只考虑 Sun/Oracle JVM(已经是一个大胆的举措,因为那里还有其他 JVM,例如来自 IBM 的那个),那么 Java 6 版本(从更新 21 开始)可以使用压缩字符串,这意味着它char[]是自动byte[]如果字符恰好都在 0..255 范围内(即所有字符实际上都是Latin-1的一部分),则转换为 a “压缩字符串”旨在在某些基准测试中获得最高分,但后来被丢弃(Java 7 没有它们)。但是,这足以表明内部存储格式可能更改恕不另行通知。他们在 Java 7 update 6 中再次这样做了。

因此,使用备用 JVM,或者只是将 JVM 更新到更高版本(当存在需要修复的安全漏洞时强烈建议这样做),可能会完全破坏您的代码,可能是静默,这意味着您会得到数据损坏而不是干净的异常这只会杀死您的应用程序。这是不可取的,所以不要这样做。您无法可靠地处理String实例的内部组织方式。作为旁注,访问私有字段对于 Java 小程序也不是一个真正可行的选择(例如,您不能使用未签名的小程序来做到这一点)。

第三个原因,也许是三个原因中最引人注目的,是覆盖内存中的敏感值在 Java 中不能(可靠地)工作要知道为什么,你必须了解垃圾收集算法是如何工作的(这篇文章是对基础知识的非常好的介绍)。从程序员的角度来看,事情很简单:分配一个对象,位于 RAM 中,当应用程序代码停止引用它时,GC 回收内存。不过,在内部,情况可能会有所不同。特别是,最有效的 GC 算法倾向于在内存中移动对象,即真正将它们从一个地方复制到另一个地方。这对您的代码是不可见的,因为 GC 会调整引用:由于 Java 是强类型的,因此您不会注意到指针的内部表示发生了变化(例如,您不能将引用转换为整数)。这种复制允许更快的 GC 操作和更好的局部性(关于缓存)。但是,这意味着您的宝贵数据的多个副本可能会在 RAM 的其他地方保存,完全超出您的范围。String 内容,这只会影响该实例的当前存储区域,而不会触及它的幽灵副本。

(在 Sun/Oracle JVM 中,内部复制对象的 GC 出现在 Java 1.3 前后。这可以在他们的库代码设计中看到;旧代码用于char[]密码,以防止可能发生的自动重用String,并促进手动覆盖; 使用较新的代码String是因为库设计者明白这种覆盖无论如何都不可靠。)


这是否意味着 Java 本质上是不安全的?不,因为在内存中覆盖敏感数据的重要性被大大夸大了. 你应该覆盖密码和密钥的想法是这些继承的教条之一:很久以前在特定案例中相关的东西,但现在被许多接受它作为神圣智慧并且不理解它是什么的人应用和强制执行真的差不多。当攻击者不是很能干时,覆盖内存对于在受感染系统上运行的应用程序代码来说是一件好事:场景是一个普通的房主拥有一台充满恶意软件的 PC。该恶意软件可以完全控制机器,但是,作为一个简单的自动化代码,它并没有真正利用这种控制;该恶意软件只是简单地扫描 RAM 中的字符序列,例如信用卡信息。所以我们谈论的是注定要生存的客户端系统,只是因为攻击者更喜欢这种方式,

这些都不适用于服务器应用程序,也不适用于处理具有实际不可忽略值的机密的客户端代码。如果恶意攻击者能够扫描 RAM 中的敏感数据,并且该数据值得人类攻击者 1 或 2 分钟的明确关注,那么再多的覆盖也无法拯救您。因此,在许多安全性很重要的情况下,重写密码和密钥只是浪费精力,给人一种安全感,但实际上并没有改善事情(尽管它可能对敬畏审计员很方便)。

使问题更加复杂的是,当您的敏感数据出现在您的 Java 代码中时,它已经通过了您无法触及的各个层。例如,如果您从文件中读取密码,那么它的副本会保留在 RAM 中,用作内核的缓存,并且可能会保留一两个由 Java 维护的反弹缓冲区,作为本地世界和 Java 提供的抽象之间的中介。如果密码是通过 SSL 从网络接收的,则密码再次通过 SSL 库的内部缓冲,这是您无法控制的。如果我们谈论的是客户端应用程序并且密码只是由用户输入,那么任何可以扫描内存的恶意软件也会运行键盘记录器并在密码到达您的代码之前获取密码。

因此,总结一下:不,使用反射覆盖内存中的密码并不能真正提高安全性。它使您的代码更容易被破坏(即使是对 JVM 进行简单的小更新),但在安全性方面并没有提供实际的切实收益。所以不要这样做。


注意:我们在这里讨论了 Java,但以上所有内容同样适用于大多数其他编程语言和框架,包括 .NET (C#)、PHP、Ruby、Node.js、Python、Go……如果你真的想保留跟踪敏感数据,那么您必须使用一种与裸机(汇编、C、Forth)足够接近的语言,在整个系统中跟踪它,包括基础库、内核和设备驱动程序。如果您只是专注于应用程序代码,那么您肯定会错过重点。

恕我直言,使用SealedObject是在 JVM(Java 虚拟机)内存中存储秘密信息的正确方法。

这使用了 JRE 已经支持的标准安全和加密机制。

请在https://stackoverflow.com/a/58933366/2590615上查看我的答案- 我不想复制内容。还有一个工作示例的链接。