【问题标题】:Is it possible to zero a Golang string's memory "safely"?是否可以“安全地”将 Golang 字符串的内存归零?
【发布时间】:2017-02-19 10:53:56
【问题描述】:

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

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

我使用[]byte 而不是string 的原因是,根据我目前所学到的关于 Go 的知识,我至少可以遍历纯文本密码并将所有字节归零,或者甚至将指针传递给libsodiumsodium_memzero 函数,以免它在内存中停留的时间超过它需要的时间。

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

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

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

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

我已经尝试过了,不出所料,它当然可以工作,但是我在 Go 中留下了一个不安全的 string,如果在像 fmt.Println 这样的函数中使用它会导致程序崩溃。

我的问题如下:

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

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

【问题讨论】:

  • 当您说将内存归零时,您的意思是调用 realloc 并将其大小设为 0?因为如果您的意思是将块中的所有位设置为 0,我不确定重点是什么。我的直觉说你应该依赖 GC,我认为你可以安排你的代码,以便地图很快离开范围(比如将字符串传递给你在 goroutine 中调用的函数并移动一个),所以我看不到内存被认为确实是一个问题。我的意思是,这里的内存问题是什么?密码不是很重要。
  • 如果密码是通过 HTTP POST 输入的,那么将特定字符串归零没有多大意义,因为您自己没有直接从网络上读取它,也不知道那里有多少副本一直在记忆中。
  • @JimB 这是非常正确的,也许我应该更好地表达我的问题,因为我似乎已经给出了我想在我的 Web 应用程序中执行此操作的印象。确实,Web 应用程序示例只是触发了我对这个主题的想法,我更感兴趣的是简单地将 Go strings 内存归零是否真的可能/“安全”足以在 Go 中执行,如果安全性这么高是必需的。
  • @JakeLucas:不,没有“安全”的方法可以做到这一点,并且该语言不保证字符串的内部处理。
  • @JimB 这就是我认为的答案,但只是想在这里发帖以检查我的理解。我所说的“安全”是指,如果您小心不要再次使用字符串变量。当然,由于无法保证字符串的内部处理,所以不管你多么小心,最好不要管它,但有时我忍不住想要更直接地使用内存:)跨度>

标签: string security memory go cgo


【解决方案1】:

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

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

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

首先,你可能已经知道 Go 中的 strings 是不可变的,所以从技术上讲,它们的值不应该改变,但是如果我们在 Go 或 C 中使用 unsafe.Pointerstring cgo,我们实际上可以覆盖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

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

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

【讨论】:

  • 为什么不干脆放弃所有字符串?
  • @Awn 我确实在我编写的包中丢弃了字符串,但我试图解决的问题是当你从其他地方传递一个字符串时如何将字符串的内存归零。不幸的是,在纯 Go 中没有任何保证的方法可以做到这一点,但这并不会阻止我至少尝试:)
  • 如果您要接受多字节字符的密码,那么您将需要执行某种 unicode 规范化。并且所有这些工具都需要字符串。所以你需要编写一个低级的 unicode 规范化系统。
【解决方案2】:

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

使用多字节字符处理密码要求您首先对其进行规范化(有多个不同的字节序列可能是“Å”之类的基础,并且您作为输入获得的输入会因键盘、操作系统以及可能的相位而异月亮。

所以除非你想重写所有 Go 的 Unicode 规范化代码来处理你的字节数组,否则你会遇到问题。

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

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

【讨论】:

    【解决方案3】:

    在 Go 中处理内存中的安全值比在 C 或 C++ 中更难。那是因为 GC,它会复制和弄乱任何感觉的内存。

    因此,第一步是获取一些 GC 无法处理的内存。为此,我们可以随心所欲地启动 cgo 和 malloc;或者使用像 mmap 和 VirtualAlloc 这样的系统调用;然后像往常一样传递生成的切片。

    下一步是告诉操作系统您不希望将此内存换出到磁盘,因此您可以对其进行 mlock 或 VirtualLock。

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

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

    【讨论】:

    • 感谢Awn提供使用cgo的链接和建议。那个 memguard 包看起来确实很有趣。我已经有一段时间没有在我的问题的上下文中考虑内存了,但我想下次有机会我会开始使用那个包。
    【解决方案4】:

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

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

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

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

    与 memguard 不可变缓冲区相同。

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

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

    正是出于这样的原因,Qubes 被设计出来,以便在应用程序之间设置更强大的边界。如果你的程序被装在一个 VM 容器内,它根本无法到达那个盒子之外。唯一的攻击向量是如果您的程序运行恶意代码。

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

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

    【讨论】:

      猜你喜欢
      • 2012-12-27
      • 2012-04-22
      • 2011-05-08
      • 1970-01-01
      • 2012-04-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-01-29
      相关资源
      最近更新 更多