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

问题描述 投票:6回答:4

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

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

我使用[]byte而不是string的原因是因为,从我迄今为止学到的关于Go的内容来看,我至少可以循环使用纯文本密码并将所有字节归零,甚至可以将指针传递给libsodiumsodium_memzero功能,为了不让它在内存中停留超过它需要的时间。

这对于我能够直接以字节读取输入的应用程序来说很好,但我现在正尝试在小型Web应用程序中使用它,我需要使用POST方法从表单中读取密码。

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

问题在于,因为Go中的strings是不可变的,所以我认为我无法做任何关于将密码的内存归零的形式; POSTed in form;至少,只使用Go。

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

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

我的问题如下:

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

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

string security memory go cgo
4个回答
6
投票

如果您想接受具有多字节字符的密码,我认为您的方案一般不会起作用。

处理具有多字节字符的密码要求您首先对它们进行标准化(有多个不同的字节序列可能是“Å”之类的,并且您输入的内容会因键盘,操作系统以及月亮的相位而异。

因此,除非您想要重写所有Go的Unicode规范化代码以处理字节数组,否则您将遇到问题。

鉴于在这个问题上似乎没有太多活动,我将假设大多数人之前不需要/想要研究这个问题,或者认为它不值得花时间。

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


5
投票

鉴于在这个问题上似乎没有太多活动,我将假设大多数人之前不需要/想要研究这个问题,或者认为它不值得花时间。因此,尽管我对Go的内部运作方式一无所知,但我会发布自己的发现作为答案。

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

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

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

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

我还返回[]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$N05osI8nuTjftzfAYBIcbA$3yb92yt9S9dRmPtlSV/J8jY4DG3reqm+2eV+fi54Its

所以它看起来很成功,但由于我们不能保证在内存中的其他位置没有纯文本密码的副本,我认为这是我们可以使用它。

下面的代码只是将带有unsafe.Pointers数量的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))
}

3
投票

在Go中处理安全值比使用C或C ++更难。那是因为GC,复制和乱搞任何记忆的感觉。

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

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

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

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


2
投票

“无论如何,没有人使用字符串作为安全值。”

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

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

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

与memguard不可变缓冲区相同。

我已经尝试在给定的地址上使用unix.Mprotect但我认为技巧是我必须找到存储字符串缓冲区的实际内存页地址,而不是指向缓冲区起点的指针,以便有效地执行此操作。

暂时找到合适的解决方案对我来说有点太多的工作,但是知道字符串是不可变的并且从这里堆积到副本中的副本存在于记忆中,我认为如果你使用memguard并且有处理密码,在第一时间将密码放入memguard缓冲区,之后只处理其中的数据。

正是出于这样的原因,Qubes被设计出来,才能在应用程序之间建立更强大的界限。如果您的程序装在VM容器内,它根本无法到达该框之外。如果你的程序运行恶意代码,那么只有攻击向量。

由于网络数据包以[]字节到达,因此可以根据需要将其中敏感的任何内容清零。由于键盘输入端由OS控制,因此只需找到(或可能写入)直接转到可变字节切片的控制台文本输入函数,然后应用顶部引用的语句。

考虑到这一点,我现在正在改变我的代码,以便在使用它之后不需要在任何地方使用字符串变量。

© www.soinside.com 2019 - 2024. All rights reserved.