是否可以将 Golang 字符串的内存 "safely" 归零?
Is it possible to zero a Golang string's memory "safely"?
最近我一直在使用 cgo 在我的一个项目中设置 libsodium
,以便使用 crypto_pwhash_str
和 crypto_pwhash_str_verify
函数。
这一切都非常顺利,我现在有一小部分函数以纯文本密码的形式接收 []byte
并对其进行哈希处理,或者将其与另一个 []byte
进行验证。
我使用 []byte
而不是 string
的原因是,根据我目前对 Go 的了解,我至少可以遍历纯文本密码和零所有字节,甚至将指针传递给 libsodium
的 sodium_memzero
函数,以免它在内存中停留的时间超过需要的时间。
这对于我能够直接读取字节输入的应用程序来说很好,但我现在正尝试在一个小型 Web 应用程序中使用它,我需要使用 POST
方法。
根据我在 Go 源代码和文档中看到的内容,在请求处理程序中使用 r.ParseForm
会将所有表单值解析为 map
of string
s。
问题是因为 Go 中的 string
s 是不可变的,我认为我无法做任何事情来将表单中 POST
ed 的密码的内存清零;至少,只使用 Go。
所以我唯一(简单)的选择似乎是将 unsafe.Pointer
连同字节数传递给 C 中的函数,然后让 C 为我清零内存(例如,传递它到前面提到的 sodium_memzero
函数)。
我试过了,毫不奇怪,它确实有效,但是我在 Go 中留下了一个不安全的 string
,如果在像 fmt.Println
这样的函数中使用它会崩溃程序。
我的问题如下:
- 我是否应该接受密码将被
POST
ed 并解析为字符串,我不应该乱搞它而只是等待 GC 启动? (不理想)
- 是否可以使用 cgo 将
string
的内存清零,前提是代码中明确记录了不应再次使用字符串变量?
- 使用 cgo 将
string
的内存归零是否会导致 GC 崩溃?
- 是否值得为
http.Request
编写一种装饰器,添加一个函数来直接将表单值解析为 []byte
,以便我可以完全控制值到达时的值?
编辑: 澄清一下,Web 应用程序和表单 POST
只是一个方便的例子,我可能会因为使用 Go 的标准而收到敏感数据string
形式的库。我更感兴趣的是我的所有问题是否 possible/worthwhile 在某些情况下尽快清理内存中的数据更像是一个安全问题。
鉴于关于这个问题的 activity 似乎不多,我假设大多数人之前没有 needed/wanted 研究过这个问题,或者没有'认为这是值得的时间。因此,尽管我对 Go 的内部工作原理一无所知,但我将 post 我自己的发现作为答案。
我应该以免责声明作为这个答案的开头,因为 Go 是一种垃圾收集语言,我不知道它在内部是如何工作的,以下信息实际上可能无法保证任何内存实际上被清除为零,但是不会阻止我尝试;毕竟,在我看来,内存中的明文密码越少越好。
考虑到这一点,这就是我发现的与 libsodium
一起工作的一切(据我所知);到目前为止 none 它至少让我的任何程序崩溃了。
首先,你可能已经知道 Go 中的 string
s 是不可变的,所以从技术上讲它们的值不应该改变,但是如果我们使用 unsafe.Pointer
到 string
在 Go 中或在 C 中通过 Cgo,我们实际上可以覆盖存储在 string
值中的数据;我们只是不能保证内存中其他地方没有任何其他数据副本。
出于这个原因,我让我的密码相关函数专门处理 []byte
变量,以减少可能在内存中复制的纯文本密码的数量。
我还 return 传递给所有密码函数的纯文本密码的 []byte
参考,因为将 string
转换为 []byte
将分配新的内存并将内容复制过来。这样,至少如果您将 string
就地转换为 []byte
而没有先将其分配给变量,您仍然可以在函数调用完成后访问新的 []byte
并将该内存也归零。
以下是我想出的要点。您可以填写空白,包括 libsodium
C 库并编译它以查看结果。
对我来说,它在调用 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$N05osI8nuTjftzfAYBIcbAyb92yt9S9dRmPtlSV/J8jY4DG3reqm+2eV+fi54Its
所以它看起来像成功了,但由于我们不能保证内存中其他地方没有纯文本密码的副本,我认为这是我们所能做到的 go 用它。
下面的代码简单地将一个unsafe.Pointer
和byte
的数量传递给C中的sodium_memzero
函数来实现这一点。所以内存的实际清零最多留到libsodium
.
如果我在代码中留下任何拼写错误或任何不起作用的内容,我深表歉意,但我不想粘贴太多,只粘贴相关部分。
例如,如果您真的需要,您也可以使用像 mlock
这样的函数,但是由于这个问题的重点是将 string
归零,我将在这里展示它。
package sodium
// Various imports, other functions and <sodium.h> here...
func init() {
if err := sodium.Init(); err != nil {
log.Fatalf("sodium: %s", err)
}
}
func PasswordHash(pwd []byte, opslimit, memlimit int) ([]byte, []byte, error) {
pwdPtr := unsafe.Pointer(&pwd[0])
hashPtr := unsafe.Pointer(&make([]byte, C.crypto_pwhash_STRBYTES)[0])
res := C.crypto_pwhash_str(
(*C.char)(hashPtr),
(*C.char)(pwdPtr),
C.ulonglong(len(pwd)),
C.ulonglong(opslimit),
C.size_t(memlimit),
)
if res != 0 {
return nil, pwd, fmt.Errorf("sodium: passwordhash: out of memory")
}
return C.GoBytes(hashPtr, C.crypto_pwhash_STRBYTES), pwd, nil
}
func MemZero(p unsafe.Pointer, size int) {
if p != nil && size > 0 {
C.sodium_memzero(p, C.size_t(size))
}
}
func MemZeroBytes(bytes []byte) {
if size := len(bytes); size > 0 {
MemZero(unsafe.Pointer(&bytes[0]), size)
}
}
func MemZeroStr(str *string) {
if size := len(*str); size > 0 {
MemZero(unsafe.Pointer(str), size)
}
}
然后全部使用:
package main
// Imports etc here...
func main() {
// Unfortunately there is no guarantee that this won't be
// stored elsewhere in memory, but we will try to remove it anyway
pwd := "Correct Horse Battery Staple"
// I convert the pwd string to a []byte in place here
// Because of this I have no reference to the new memory, with yet
// another copy of the plain password hanging around
// The function always returns the new []byte as the second value
// though, so we can still zero it anyway
hash, pwdBytes, err := sodium.PasswordHash([]byte(pwd), 6, 134217728)
// Byte slice and string before MemZero* functions
fmt.Println("pwd :", pwd)
fmt.Println("pwdBytes:", pwdBytes)
// No need to keep a plain-text password in memory any longer than required
sodium.MemZeroStr(&pwd)
sodium.MemZeroBytes(pwdBytes)
if err != nil {
log.Fatal(err)
}
// Byte slice and string after MemZero* functions
fmt.Println("pwd :", pwd)
fmt.Println("pwdBytes:", pwdBytes)
// We've done our best to make sure we only have the hash in memory now
fmt.Println("Hash:", string(hash))
}
在内存中处理安全值在 Go 中比在 C 或 C++ 中更难。那是因为 GC,它到处复制和弄乱它感觉的任何内存。
因此,第一步是获取一些 GC 不能乱用的内存。为此,我们可以根据需要启动 cgo 和 malloc;或者使用像 mmap 和 VirtualAlloc 这样的系统调用;然后像往常一样传递生成的切片。
下一步是告诉 OS 你不希望这个内存被换出到磁盘,所以你 mlock 或 VirtualLock 它。
在退出之前,使用 libsodium 或通过简单地迭代将切片归零,将每个元素设置为零。这对于字符串是不可能的,而且我不确定我是否会推荐手动擦除字符串的内存。我的意思是,我不能立即发现它有什么问题,但是……就是感觉不对。无论如何,没有人使用字符串作为安全值。
有一个库(我的)是专门为存储安全值而设计的,它可以完成我上面描述的内容以及其他一些事情。您可能会发现它很有用:https://github.com/awnumar/memguard
"No one uses strings for secure values anyway."
KDF中用于解开密文或直接解密的密码除外
如果您尝试改变字符串的底层缓冲区,则字符串分配中使用的内存会触发分段错误:
https://medium.com/kokster/mutable-strings-in-golang-298d422d01bc
与 memguard 不可变缓冲区相同。
我已经尝试在给定的地址上使用 unix.Mprotect 但我认为诀窍是我必须找到存储字符串缓冲区的实际内存页地址,而不是指向缓冲区开头的指针,有效地做到这一点。
暂时找到合适的解决方案对我来说工作量有点大,但是知道字符串是不可变的并且从这里到王国的副本堆积在内存中,我认为这应该是一个规则,如果你使用 memguard 并且必须处理密码,首先将其放入 memguard 缓冲区,然后仅使用该形式的数据。
正是出于这样的原因,Qubes 被设计出来,以在应用程序之间设置更牢固的边界。如果您的程序装在 VM 容器内,则它根本无法到达该盒子之外。如果您的程序运行恶意代码,则只有攻击向量。
由于网络数据包以 [] 字节的形式到达,因此可以根据需要将其中的任何敏感信息清零。由于键盘输入端由 OS 控制,因此只需找到(或编写)一个直接进入可变字节片的控制台文本输入函数,然后应用我在顶部引用的语句。
牢记这一点,我现在正在更改我的代码,以便在使用后需要将数据归零的任何地方不使用字符串变量。
如果您想接受多字节字符的密码,我认为您的方案通常不会奏效。
处理包含多字节字符的密码需要您先将它们规范化(有多个不同的字节序列可能在“Å”之类的基础上,并且您得到的输入会因键盘、操作系统和可能的阶段而异月亮。
因此,除非您想重写 Go 的所有 Unicode 规范化代码以处理您的字节数组,否则您将 运行 遇到问题。
Given that there doesn't seem to be much activity on this question, I'm going to just assume that most people haven't needed/wanted to look into this before, or haven't thought it was worth the time.
其实我今天才注意到这个问题。相信我,我已经考虑过了。
最近我一直在使用 cgo 在我的一个项目中设置 libsodium
,以便使用 crypto_pwhash_str
和 crypto_pwhash_str_verify
函数。
这一切都非常顺利,我现在有一小部分函数以纯文本密码的形式接收 []byte
并对其进行哈希处理,或者将其与另一个 []byte
进行验证。
我使用 []byte
而不是 string
的原因是,根据我目前对 Go 的了解,我至少可以遍历纯文本密码和零所有字节,甚至将指针传递给 libsodium
的 sodium_memzero
函数,以免它在内存中停留的时间超过需要的时间。
这对于我能够直接读取字节输入的应用程序来说很好,但我现在正尝试在一个小型 Web 应用程序中使用它,我需要使用 POST
方法。
根据我在 Go 源代码和文档中看到的内容,在请求处理程序中使用 r.ParseForm
会将所有表单值解析为 map
of string
s。
问题是因为 Go 中的 string
s 是不可变的,我认为我无法做任何事情来将表单中 POST
ed 的密码的内存清零;至少,只使用 Go。
所以我唯一(简单)的选择似乎是将 unsafe.Pointer
连同字节数传递给 C 中的函数,然后让 C 为我清零内存(例如,传递它到前面提到的 sodium_memzero
函数)。
我试过了,毫不奇怪,它确实有效,但是我在 Go 中留下了一个不安全的 string
,如果在像 fmt.Println
这样的函数中使用它会崩溃程序。
我的问题如下:
- 我是否应该接受密码将被
POST
ed 并解析为字符串,我不应该乱搞它而只是等待 GC 启动? (不理想) - 是否可以使用 cgo 将
string
的内存清零,前提是代码中明确记录了不应再次使用字符串变量? - 使用 cgo 将
string
的内存归零是否会导致 GC 崩溃? - 是否值得为
http.Request
编写一种装饰器,添加一个函数来直接将表单值解析为[]byte
,以便我可以完全控制值到达时的值?
编辑: 澄清一下,Web 应用程序和表单 POST
只是一个方便的例子,我可能会因为使用 Go 的标准而收到敏感数据string
形式的库。我更感兴趣的是我的所有问题是否 possible/worthwhile 在某些情况下尽快清理内存中的数据更像是一个安全问题。
鉴于关于这个问题的 activity 似乎不多,我假设大多数人之前没有 needed/wanted 研究过这个问题,或者没有'认为这是值得的时间。因此,尽管我对 Go 的内部工作原理一无所知,但我将 post 我自己的发现作为答案。
我应该以免责声明作为这个答案的开头,因为 Go 是一种垃圾收集语言,我不知道它在内部是如何工作的,以下信息实际上可能无法保证任何内存实际上被清除为零,但是不会阻止我尝试;毕竟,在我看来,内存中的明文密码越少越好。
考虑到这一点,这就是我发现的与 libsodium
一起工作的一切(据我所知);到目前为止 none 它至少让我的任何程序崩溃了。
首先,你可能已经知道 Go 中的 string
s 是不可变的,所以从技术上讲它们的值不应该改变,但是如果我们使用 unsafe.Pointer
到 string
在 Go 中或在 C 中通过 Cgo,我们实际上可以覆盖存储在 string
值中的数据;我们只是不能保证内存中其他地方没有任何其他数据副本。
出于这个原因,我让我的密码相关函数专门处理 []byte
变量,以减少可能在内存中复制的纯文本密码的数量。
我还 return 传递给所有密码函数的纯文本密码的 []byte
参考,因为将 string
转换为 []byte
将分配新的内存并将内容复制过来。这样,至少如果您将 string
就地转换为 []byte
而没有先将其分配给变量,您仍然可以在函数调用完成后访问新的 []byte
并将该内存也归零。
以下是我想出的要点。您可以填写空白,包括 libsodium
C 库并编译它以查看结果。
对我来说,它在调用 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$N05osI8nuTjftzfAYBIcbAyb92yt9S9dRmPtlSV/J8jY4DG3reqm+2eV+fi54Its
所以它看起来像成功了,但由于我们不能保证内存中其他地方没有纯文本密码的副本,我认为这是我们所能做到的 go 用它。
下面的代码简单地将一个unsafe.Pointer
和byte
的数量传递给C中的sodium_memzero
函数来实现这一点。所以内存的实际清零最多留到libsodium
.
如果我在代码中留下任何拼写错误或任何不起作用的内容,我深表歉意,但我不想粘贴太多,只粘贴相关部分。
例如,如果您真的需要,您也可以使用像 mlock
这样的函数,但是由于这个问题的重点是将 string
归零,我将在这里展示它。
package sodium
// Various imports, other functions and <sodium.h> here...
func init() {
if err := sodium.Init(); err != nil {
log.Fatalf("sodium: %s", err)
}
}
func PasswordHash(pwd []byte, opslimit, memlimit int) ([]byte, []byte, error) {
pwdPtr := unsafe.Pointer(&pwd[0])
hashPtr := unsafe.Pointer(&make([]byte, C.crypto_pwhash_STRBYTES)[0])
res := C.crypto_pwhash_str(
(*C.char)(hashPtr),
(*C.char)(pwdPtr),
C.ulonglong(len(pwd)),
C.ulonglong(opslimit),
C.size_t(memlimit),
)
if res != 0 {
return nil, pwd, fmt.Errorf("sodium: passwordhash: out of memory")
}
return C.GoBytes(hashPtr, C.crypto_pwhash_STRBYTES), pwd, nil
}
func MemZero(p unsafe.Pointer, size int) {
if p != nil && size > 0 {
C.sodium_memzero(p, C.size_t(size))
}
}
func MemZeroBytes(bytes []byte) {
if size := len(bytes); size > 0 {
MemZero(unsafe.Pointer(&bytes[0]), size)
}
}
func MemZeroStr(str *string) {
if size := len(*str); size > 0 {
MemZero(unsafe.Pointer(str), size)
}
}
然后全部使用:
package main
// Imports etc here...
func main() {
// Unfortunately there is no guarantee that this won't be
// stored elsewhere in memory, but we will try to remove it anyway
pwd := "Correct Horse Battery Staple"
// I convert the pwd string to a []byte in place here
// Because of this I have no reference to the new memory, with yet
// another copy of the plain password hanging around
// The function always returns the new []byte as the second value
// though, so we can still zero it anyway
hash, pwdBytes, err := sodium.PasswordHash([]byte(pwd), 6, 134217728)
// Byte slice and string before MemZero* functions
fmt.Println("pwd :", pwd)
fmt.Println("pwdBytes:", pwdBytes)
// No need to keep a plain-text password in memory any longer than required
sodium.MemZeroStr(&pwd)
sodium.MemZeroBytes(pwdBytes)
if err != nil {
log.Fatal(err)
}
// Byte slice and string after MemZero* functions
fmt.Println("pwd :", pwd)
fmt.Println("pwdBytes:", pwdBytes)
// We've done our best to make sure we only have the hash in memory now
fmt.Println("Hash:", string(hash))
}
在内存中处理安全值在 Go 中比在 C 或 C++ 中更难。那是因为 GC,它到处复制和弄乱它感觉的任何内存。
因此,第一步是获取一些 GC 不能乱用的内存。为此,我们可以根据需要启动 cgo 和 malloc;或者使用像 mmap 和 VirtualAlloc 这样的系统调用;然后像往常一样传递生成的切片。
下一步是告诉 OS 你不希望这个内存被换出到磁盘,所以你 mlock 或 VirtualLock 它。
在退出之前,使用 libsodium 或通过简单地迭代将切片归零,将每个元素设置为零。这对于字符串是不可能的,而且我不确定我是否会推荐手动擦除字符串的内存。我的意思是,我不能立即发现它有什么问题,但是……就是感觉不对。无论如何,没有人使用字符串作为安全值。
有一个库(我的)是专门为存储安全值而设计的,它可以完成我上面描述的内容以及其他一些事情。您可能会发现它很有用:https://github.com/awnumar/memguard
"No one uses strings for secure values anyway."
KDF中用于解开密文或直接解密的密码除外
如果您尝试改变字符串的底层缓冲区,则字符串分配中使用的内存会触发分段错误:
https://medium.com/kokster/mutable-strings-in-golang-298d422d01bc
与 memguard 不可变缓冲区相同。
我已经尝试在给定的地址上使用 unix.Mprotect 但我认为诀窍是我必须找到存储字符串缓冲区的实际内存页地址,而不是指向缓冲区开头的指针,有效地做到这一点。
暂时找到合适的解决方案对我来说工作量有点大,但是知道字符串是不可变的并且从这里到王国的副本堆积在内存中,我认为这应该是一个规则,如果你使用 memguard 并且必须处理密码,首先将其放入 memguard 缓冲区,然后仅使用该形式的数据。
正是出于这样的原因,Qubes 被设计出来,以在应用程序之间设置更牢固的边界。如果您的程序装在 VM 容器内,则它根本无法到达该盒子之外。如果您的程序运行恶意代码,则只有攻击向量。
由于网络数据包以 [] 字节的形式到达,因此可以根据需要将其中的任何敏感信息清零。由于键盘输入端由 OS 控制,因此只需找到(或编写)一个直接进入可变字节片的控制台文本输入函数,然后应用我在顶部引用的语句。
牢记这一点,我现在正在更改我的代码,以便在使用后需要将数据归零的任何地方不使用字符串变量。
如果您想接受多字节字符的密码,我认为您的方案通常不会奏效。
处理包含多字节字符的密码需要您先将它们规范化(有多个不同的字节序列可能在“Å”之类的基础上,并且您得到的输入会因键盘、操作系统和可能的阶段而异月亮。
因此,除非您想重写 Go 的所有 Unicode 规范化代码以处理您的字节数组,否则您将 运行 遇到问题。
Given that there doesn't seem to be much activity on this question, I'm going to just assume that most people haven't needed/wanted to look into this before, or haven't thought it was worth the time.
其实我今天才注意到这个问题。相信我,我已经考虑过了。