内存中数据覆盖问题

8
我用 Ocaml 编写了一个密码管理器。为了使其尽可能安全,我希望以一种可以被覆盖的方式将字符串(加密密钥)存储在内存中。Ocaml 是值传递的,并且有一个垃圾回收器,这证明了它是困难的。我加密了所有的缓冲区和变量,但我仍然需要一个“会话密钥”来存储这些。为了防止自动密钥搜索程序检测到此情况或将其放入交换文件中,它由缓冲区中的一堆随机数据使用随机增量组成。所以,我只需要一个可以在几秒钟内被覆盖的变量,以便将其传递给 Nocrypto 库......参照工作吗?
根据这个康奈尔“ 参考和数组”页面,参考是可变的,并且类似于 C 中的指针。也就是说,我还发现了一个关于Ocaml 引用的 stack overflow 的答案, 其中的答案提到 “它们像指向新分配的内存的指针”。这是否意味着每次都只是在内存中分配一个新的东西而不是实际突变内存中的内容?如果是这样的话,你不能真正“覆盖”引用。
我遇到的其他可能的解决方案包括 Bigarrays 和“自定义块”。我不确定“自定义块”是否实际上在垃圾回收的作用域之外进行了分配。它们似乎是用于访问外部 C 代码的。它们是否会被垃圾回收器复制?它们能被“覆盖”吗?还有这种在堆栈溢出上关于内存中自定义块的讨论非常有用但也很令人困惑: 在内存中自定义块是否被复制?答案说它们可以被移动。即便如此,它们能被覆盖吗?
最后一种可能的解决方案是使用像 Nocrypto 库那样的 Cstruct 进行存储。他们在这个Github issue 中讨论了这点。发问者说:

“Granted, most key material is Cstruct.t, which is a Bigarray.Array1.t, which is allocated outside of the GC heap”

这是正确的吗?如果是,我似乎找不到实际执行此操作的源文件。我对Ocaml和函数式编程都很陌生。如果您感兴趣,我的程序在github上可用: ocaml-pass


2
顺便提一下,OCaml 是按引用传递而不是按值传递。 - ivg
2个回答

14

简介

在OCaml堆中不要存储任何机密信息。因此,您绝不能将机密复制到任何OCaml堆分配的值中,因此,即使是暂时使用的Bytes、Strings或Arrays也不能使用。

OCaml内存模型介绍

OCaml值统一以标记机器字表示。字的最低有效位用作标记,区分指针(tag = 0)和立即数值(tag = 1)。因此,一个值始终具有固定大小,并且是指针或立即数值。

立即数值将其数据存储在字的最高有效部分,即32位系统中为31位,在64位系统中为63位。指针将其数据存储在块中,这些块位于所谓的OCaml堆中。OCaml堆是 Garbage Collector (GC) 管理的一组块。块是带有头的数据块。头指定数据的大小以及GC使用的其他元信息。块可以包含OCaml值(指针或即时数值)或不透明数据。

总之,所有OCaml值都表示为机器字,可以直接将数据存储在字中,也可以是指向堆分配的块的指针。每个指针指向一个且仅一个块。多个指针可以指向同一个块。这些值被认为是物理相等的。一些块没有被任何指针指向。这些块称为死块,并由GC回收。

OCaml垃圾收集器介绍

GC通过分配、移动和释放它们来管理块。GC本身使用arena,该arena可以从C内存分配器(malloc)或通过memmap系统调用直接从内核中获得(取决于特定系统和运行时)。

GC是分代的,这意味着值首先分配在堆的一个特殊区域中,称为小堆。小堆是一个固定大小的连续内存区域,在运行时用三个指针表示:beg指向小堆的开头,end指向小堆的结尾,cur指向小堆自由区域的开头。当分配一个块时,cur增加块的大小。然后使用数据初始化该块。当小堆没有更多的自由空间(即end-cur小于所需块的大小)时,会触发一个小GC循环。GC分析存储在小堆中的所有块,并将至少被一个指针引用的所有块复制到大堆中。之后,将cur指针设置为beg

在大堆中,一个块也可能在称为压缩的过程中被复制多次。压缩器可以尝试重新排列其区域中的块,以实现更紧凑的堆表示。

安全后果

由于OCaml GC是移动式GC,它可能任意复制堆分配的数据。虽然它被称为移动,但实际上仍然只是复制。即,当一个块从小堆移到大堆时,实际上只是位复制,因此会复制一份。小堆中的虚拟块可能会存活任意长的时间,直到被某个新分配的值覆盖。当对象在压缩过程中移动时,它也会被复制,并且在该过程中可能或可能不会被覆盖。当然,不言而喻,一旦一个块变成死块,它仍然可能在堆中存活任意长的时间,直到被GC重用。

所有这些意味着,如果一个密钥最终存储在OCaml堆中,它将变得不可控,因为GC可以以任意和相对不可预测的方式复制它。因此,我们只能将密钥存储在立即值或不受GC控制的区域中。正如之前所说的,所有指针类型的OCaml值总是指向OCaml堆中的一个块。一个块可能直接包含数据,也可能包含一个指针本身,该指针将指向内存堆之外。所谓的自定义块可能会或可能不会将它们的信息存储在OCaml堆中,这取决于每个自定义块的特定表示。例如,Bigarray库提供了存储负载在OCaml堆之外的自定义块。因此,Bigarray是一个自定义块,有两个字段:指针和大小。它是一个不透明的块,即GC永远不会将这两个值视为OCaml值,并且永远不会跟随大小或指针。指针指向的数据位于OCaml堆之外,并且可以由malloc或memmap分配(实际上,它可以是任意整数,甚至指向堆栈、静态数据,这并不重要,只要我们将bigarrays视为ptr,len配对即可)。
所有这些使得Bigarrays非常适合存储机密信息。我们可以确信它们不会被GC移动,我们可以在它们被释放后覆盖它们以防止信息泄漏。
进一步考虑

我们应该谨慎,绝不能允许将秘密从安全的地方复制到OCaml堆中。这意味着,即使我们的主要存储是一个安全的bigarray,如果我们将其内容复制到OCaml字符串中,信息仍然会泄漏。因此,如果我们首先将信息读入OCaml字符串,然后再将其复制到bigarray中,信息仍然会泄漏。因此,任何使用OCaml堆分配值的接口都是不安全的,不应使用。例如,我们不能使用OCaml通道读取或写入秘密(我们应该依赖Unix模块提供的内存映射或无缓冲IO)。同时,每当你从Bigarray获取string数据类型时,你都会得到复制的数据,带来了所有的影响。


谢谢你写这篇文章,非常有趣!我有一个问题想跟进你的结论。你能指点一下如何在不让数据出现在OCaml堆中的情况下操作数据吗?完全依赖C代码,只使用OCaml来启动整个过程? - Richard-Degenne
2
这基本上意味着类型应该是纯抽象的,并提供操作,如相等、迭代、映射、比较和IO操作。 - ivg
谢谢!看起来最简单的方法是使用ocaml-cstructs,它使用Bigarray。我将不得不重写一堆函数,但由于很多数据可以保持加密状态直到操作,所以应该不会太糟糕。有点棘手。 - JChase2
在pervasives.ml的源代码中,输出函数似乎使用它们自己的绑定。即使一切都在Bigarrays中,但只要需要打印任何内容,它最终还是会泄漏。我想。更不用说我使用的正则表达式库了。如果我想让所有东西都不落到内存中,似乎Ocaml可能不是一个好选择。也许类似于加密进程内存之类的东西会更好。 - JChase2
1
这正是我所说的,你可以使用Unix接口来实现打印,你永远不应该使用通道或任何其他缓冲IO。关于正则表达式...你真的需要在机密信息上应用正则表达式吗? - ivg
我明白你现在提到了哪里。我还在逐渐适应很多这种低级术语。正则表达式不是必需的,只是更方便一些。这个问题比我预期的要难一些。不过我正在学到很多东西,谢谢! - JChase2

0

我会使用类型为bytes的值,它本质上是一个可变的字节数组:

# let buffer = Bytes.make 16 'x';;
val buffer : bytes = "xxxxxxxxxxxxxxxx"
# Bytes.set buffer 0 'T';;
- : unit = ()
# buffer;;
- : bytes = "Txxxxxxxxxxxxxxx"
# Bytes.fill buffer 0 16 ' ';;
- : unit = ()
# buffer;;
- : bytes = "                "

在完成后,您可以使用Bytes.fill进行覆盖。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接