是否可以“安全地”将Golang字符串的内存归零?

时间:2016-10-10 23:07:55

标签: string security memory go cgo

最近,我一直在使用cgo在我的一个项目中设置libsodium,以便使用crypto_pwhash_strcrypto_pwhash_str_verify函数。

这一切都非常顺利,我现在有一小部分函数,​​它们以纯文本密码的形式接收[]byte并将其哈希,或者将其与另一个[]byte进行比较验证它。

我使用[]byte而不是string的原因是因为,就我迄今为止所学到的Go而言,我至少可以循环使用纯文本密码并将其全部归零字节,甚至将指针传递给libsodium的{​​{1}}函数,以免在内存中停留超过需要的时间。

这适用于我能够直接以字节形式读取输入的应用程序,但我现在正尝试在小型Web应用程序中使用它,我需要使用sodium_memzero从表单中读取密码方法

从我在Go源代码和文档中看到的内容,在请求处理程序中使用POST会将所有表单值解析为r.ParseForm map s。

问题在于,因为Go中的string是不可变的,所以我认为我无法对表单中string密码的内存进行归零;至少,只使用Go。

所以看起来我的唯一(简单)选项是将POST传递给C中的函数以及字节数,让C为我归零内存(例如,将其传递给前面提到的unsafe.Pointer函数。)

我已经尝试了这一点,并且毫不奇怪它当然有效,但是我在Go中留下了一个不安全的sodium_memzero,如果在string这样的函数中使用它会导致程序崩溃

我的问题如下:

  • 我是否应该接受密码fmt.Println并将其解析为字符串,我不应该把它搞乱,只是等待GC启动? (不理想)
  • 使用cgo ok将POST的内存清零,前提是代码中明显记录了字符串变量不应该再次使用吗?
  • 使用cgo将string的内存清零会不会像崩溃GC一样?
  • 是否值得为string编写一种装饰器,添加一个函数来直接将表单值解析为http.Request,这样我们就可以在值到达时完全控制它们了?

修改:为了澄清一下,网络应用和表单[]byte只是一个简单的例子,我可能只是以使用Go的标准库形式传递敏感数据。一个POST。我更感兴趣的是我的所有问题是否可能/值得在某些情况下尽快清理内存中的数据更多是安全问题。

4 个答案:

答案 0 :(得分:6)

如果您要接受多字节字符的密码,我认为您的方案通常不会奏效。

处理具有多字节字符的密码时,需要先对其进行规范化(存在多个不同的字节序列,这些序列可能会像“Å”之类,并且您输入的内容会因键盘,操作系统和输入密码的阶段而异。月亮。

因此,除非您要重写Go的所有Unicode规范化代码以在您的字节数组上工作,否则都会遇到问题。

  

鉴于在这个问题上似乎没有太多的活动,我只是假设大多数人之前并不需要/不想研究这个问题,或者不认为这是值得的

实际上,直到今天我才注意到这个问题。相信我,我已经考虑过了。

答案 1 :(得分:5)

鉴于在这个问题上似乎没有多少活动,我只是假设大多数人还没有/想要在此之前调查过这个问题,或者避免&#39认为值得花时间。因此,尽管我对Go的内部运作方式一无所知,但我会将自己的发现作为答案发布。

我应该在这个答案前加上一个免责声明,因为Go是一种垃圾收集语言而且我不知道它是如何在内部工作的,以下信息实际上可能无法保证任何内存实际上都被清除为零,但是赢了# 39;阻止我尝试;毕竟,在我看来,内存中的纯文本密码越少越好。

考虑到这一点,这就是我与libsodium一起找到的所有工作(据我所知);到目前为止,至少没有任何一个程序崩溃过。

首先,你可能已经知道Go中的string是不可变的,所以从技术上来说它们的价值不应该被改变,但是如果我们使用unsafe.Pointer来{{1}在Go或C via Cgo中,我们实际上可以覆盖存储在string值中的数据;我们无法保证在内存中的任何其他地方都没有任何其他数据副本。

出于这个原因,我让我的密码相关函数专门处理string个变量,以减少在内存中复制的可能的纯文本密码的数量。

我还返回[]byte引用的纯文本密码,该密码被传递到所有密码函数,因为将[]byte转换为string将分配新内存并复制内容。这样,至少如果您将[]byte原地转换为string而未将其分配给变量,则在函数调用完成后仍可以访问新的[]byte并将该记忆归零。

以下是我想出的要点。您可以填写空白,包括[]byte C库并编译它以查看自己的结果。

对我来说,它在调用libsodium函数之前输出:

MemZero*

然后在调用pwd : Correct Horse Battery Staple pwdBytes: [67 111 114 114 101 99 116 32 72 111 114 115 101 32 66 97 116 116 101 114 121 32 83 116 97 112 108 101] 函数后执行此操作:

MemZero*

所以它看起来很成功,但由于我们无法保证内存中其他地方没有纯文本密码的副本,我认为这是我们可以

下面的代码只需将pwd : pwdBytes: [0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0] Hash: $argon2i$v=19$m=131072,t=6,p=1$N05osI8nuTjftzfAYBIcbA$3yb92yt9S9dRmPtlSV/J8jY4DG3reqm+2eV+fi54Its 的{​​{1}}个数传递给C中的unsafe.Pointer函数即可实现此目的。因此,内存的实际归零最多只能保留byte

如果我在代码中遗漏了任何拼写错误或任何无法正常工作的内容,我会道歉,但我并不想粘贴太多,只有相关部分。

例如,如果你真的需要,你也可以使用像sodium_memzero这样的函数,但由于这个问题集中在归零libsodium,我只会在这里显示。

mlock

然后全部使用:

string

答案 2 :(得分:3)

在Go中处理安全值比使用C或C ++更难。这是因为GC,它会复制和弄乱任何记忆。

所以,第一步是获得一些GC不能搞砸的内存。为此,我们要么根据需要调整cgo和malloc;或者使用像mmap和VirtualAlloc这样的系统调用;然后照常传递生成的切片。

下一步是告诉操作系统你不希望这个内存被换成磁盘,所以你要mlock或VirtualLock它。

在退出之前,使用libsodium将切片归零或简单地迭代切片,将每个元素设置为零。使用字符串是不可能的,我不确定我是否建议手动擦除字符串的内存。我的意思是,我无法立即发现它的任何问题,但......它感觉不对。无论如何,没有人使用字符串作为安全值。

有一个专门用于存储安全价值的图书馆(我的),它完成了我上面描述的以及其他一些事情。您可能会发现它很有用:https://github.com/awnumar/memguard

答案 3 :(得分:2)

“反正没人会使用字符串作为安全值。”

KDF中用于解锁密文或直接解密的密码除外。

如果您尝试使字符串的基础缓冲区发生突变,则字符串分配中使用的内存会触发分段错误:

https://medium.com/kokster/mutable-strings-in-golang-298d422d01bc

与memguard不可变缓冲区相同。

我尝试在给定的地址上使用unix.Mprotect,但是我想诀窍是我必须找到存储字符串缓冲区的实际内存页面地址,而不是指向缓冲区开始的指针,才能有效地做到这一点

暂时找不到合适的解决方案对我来说是一件太多的工作,但是知道字符串是不可变的,并且从这里到王国的大量副本都存储在内存中,我认为如果您使用的是规则memguard,必须处理密码,首先将其放入memguard缓冲区中,然后再使用该格式的数据。

正是出于这样的原因,Qubes才被设计出来,以便在应用程序之间建立更强的边界。如果您的程序装在VM容器中,则根本无法到达该框之外。如果您的程序运行恶意代码,则只有攻击媒介。

由于网络数据包以[] byte的形式到达,因此可以根据需要将其中的任何敏感数据清零。由于键盘输入端是由操作系统控制的,因此只需要查找(或编写)直接用于可变字节片的控制台文本输入功能,然后我在上面引用的语句就适用。

考虑到这一点,我现在正在更改代码,以在使用完后需要将数据归零的任何地方都不使用字符串变量。