提升漏洞披露流程的五大理由与实战案例

本文通过五个真实漏洞案例,深入分析漏洞披露过程中的挑战与解决方案,探讨GitHub私有报告功能如何优化安全漏洞的沟通与修复流程,提升开源软件生态的安全性。

5个追求更好披露流程的理由 - Trail of Bits博客

本博客展示了我们在过去一年中披露的五个真实漏洞案例(此前未公开披露)。我们还分享了在披露这些漏洞时遇到的挫折,以说明建立有效披露流程的必要性。

以下是五个漏洞:

  1. borsh-rs Rust库中的未定义行为
  2. 解析以太坊ABI的Rust库中的拒绝服务(DoS)向量
  3. Expo中认证标签长度缺少限制
  4. num-bigint Rust库中的DoS向量
  5. 通过react-native-mmkv将MMKV数据库加密密钥插入Android系统日志

在开源项目中发现漏洞需要谨慎处理,因为公开报告(也称为完全披露)可能会在修复准备就绪前提醒攻击者。协调漏洞披露(CVD)使用更安全、结构化的报告框架来最小化风险。我们的五个案例展示了缺乏CVD流程如何不必要地复杂化了报告这些漏洞并确保及时修复的过程。

在“要点”部分,我们通过提供可用的基本安全策略,并引导您完成称为GitHub私有报告的简化披露流程,向您展示如何为项目成功做好准备。GitHub的功能有几个好处:

  • 对开发者的谨慎安全警报:无需PGP加密电子邮件
  • 简化流程:无需与公司电子邮件地址玩捉迷藏
  • 简单的CVE发布:无需在MITRE提交CVE表格

行动时间:如果您在GitHub上拥有知名项目,请立即使用私有报告!阅读更多关于为存储库配置私有漏洞报告的信息,或直接跳到本文的“要点”部分。

案例1:borsh-rs Rust库中的未定义行为

第一个案例,也是实施全面安全策略的原因,涉及一个名为borsh-rs的加密序列化库中的漏洞,该漏洞两年未修复。

在审计期间,我发现了不安全的Rust代码,如果与未实现Copy特性的零大小类型一起使用,可能导致未定义行为。尽管之前有人报告过这个漏洞,但由于开发人员不清楚如何避免代码中的未定义行为并保持相同属性(例如,抵抗DoS攻击),因此未修复。在此期间,库的用户未被告知该漏洞。

整个流程本可以通过GitHub的私有报告功能简化。如果项目开发人员在私下报告时无法解决漏洞,他们仍然可以一键通知Dependabot用户。在GitHub上私下报告漏洞时,发布实际修复是可选的。

我联系了borsh-rs开发人员,询问在没有可用修复的情况下通知用户的事宜。开发人员决定最好通知用户,因为只有库的某些使用会导致未定义行为。我们提交了通知RUSTSEC-2023-0033,创建了一个GitHub公告。几个月后,开发人员修复了该漏洞,并发布了主要版本1.0.0。然后我更新了RustSec公告以反映修复情况。

以下代码包含导致未定义行为的漏洞:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
impl<T> BorshDeserialize for Vec<T>
where
    T: BorshDeserialize,
{
    #[inline]
    fn deserialize<R: Read>(reader: &mut R) -> Result<Self, Error> {
        let len = u32::deserialize(reader)?;
        if size_of::<T>() == 0 {
            let mut result = Vec::new();
            result.push(T::deserialize(reader)?);

            let p = result.as_mut_ptr();
            unsafe {
                forget(result);
                let len = len as usize;
                let result = Vec::from_raw_parts(p, len, len);
                Ok(result)
            }
        } else {
            // TODO(16): return capacity allocation when we can safely do that.
            let mut result = Vec::with_capacity(hint::cautious::<T>(len));
            for _ in 0..len {
                result.push(T::deserialize(reader)?);
            }
            Ok(result)
        }
    }
}

图1:使用不安全的Rust代码(borsh-rs/borsh-rs/borsh/src/de/mod.rs#123–150)

图1中的代码将字节反序列化为某种泛型数据类型T的向量。如果类型T是零大小类型,则执行不安全的Rust代码。代码首先将向量的请求长度读取为u32。之后,代码分配一个空的Vec类型。然后将T的单个实例推入其中。随后,通过调用forget函数临时泄漏刚刚分配的内存的Vec,并通过将Vec的长度和容量设置为请求的长度来重建它。因此,不安全的Rust代码假设T是可复制的。

不安全的Rust代码防止了反序列化内存表示显著大于序列化磁盘表示的DoS攻击。该攻击通过将向量长度设置为大数并使用零大小类型来工作。我们博客文章Billion times emptiness中描述了此漏洞的一个实例。

案例2:解析以太坊ABI的Rust库中的DoS向量

7月,我披露了四个以太坊API解析库中的多个DoS漏洞,这些漏洞难以报告,因为我必须联系多个相关方。

该漏洞影响了四个GitHub托管项目。只有Python项目eth_abi启用了GitHub私有报告。对于其他三个项目(ethabi、alloy-rs和ethereumjs-abi),我必须研究谁在维护它们,这可能容易出错。例如,我不得不通过向GitHub提交URL追加后缀.patch来获取维护者的电子邮件地址。以下链接显示了我用于提交的非工作电子邮件地址:

https://github.com/trailofbits/publications/commit/a2ab5a1cab59b52c4fa71b40dae1f597bc063bdf.patch

在博客文章Billion times emptiness中阅读有关此漏洞技术细节的更多信息。

案例3:Expo中认证标签长度缺少限制

2022年底,Trail of Bits的安全工程师Joop van de Pol在expo-secure-store中发现了一个加密漏洞。在这种情况下,供应商Expo未能跟进我们关于他们是否确认或修复了漏洞的信息,这让我们处于黑暗中。更糟糕的是,尝试跟进供应商消耗了大量时间,这些时间本可以用于在开源软件中寻找更多漏洞。

当我们最初通过其GitHub上列出的电子邮件地址secure@expo.io向Expo发送关于该漏洞的电子邮件时,一名Expo员工在一天内回复并确认他们将把报告转发给他们的技术团队。然而,在那次回复之后,尽管在一年内进行了两次温和的提醒,我们再也没有收到Expo的回复。

不幸的是,Expo不允许通过GitHub进行私有报告,因此电子邮件是我们唯一的联系地址。

现在来看漏洞的具体细节:在API级别23以上的Android上,SecureStore使用KeyStore中的AES-GCM密钥来加密存储的值。在加密过程中,标签长度和初始化向量(IV)由底层的Java加密库作为Cipher类的一部分生成,并与密文一起存储:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
/* package */ JSONObject createEncryptedItem(Promise promise, String plaintextValue, Cipher cipher, GCMParameterSpec gcmSpec, PostEncryptionCallback postEncryptionCallback) throws GeneralSecurityException, JSONException {

  byte[] plaintextBytes = plaintextValue.getBytes(StandardCharsets.UTF_8);
  byte[] ciphertextBytes = cipher.doFinal(plaintextBytes);
  String ciphertext = Base64.encodeToString(ciphertextBytes, Base64.NO_WRAP);

  String ivString = Base64.encodeToString(gcmSpec.getIV(), Base64.NO_WRAP);
  int authenticationTagLength = gcmSpec.getTLen();

  JSONObject result = new JSONObject()
    .put(CIPHERTEXT_PROPERTY, ciphertext)
    .put(IV_PROPERTY, ivString)
    .put(GCM_AUTHENTICATION_TAG_LENGTH_PROPERTY, authenticationTagLength);

  postEncryptionCallback.run(promise, result);

  return result;
}

图2:加密存储中项目的代码,其中标签长度与密文一起存储(SecureStoreModule.java)

对于解密,读取密文、标签长度和IV,然后使用KeyStore中的AES-GCM密钥进行解密。

有权访问存储的攻击者可以更改现有的AES-GCM密文以具有更短的认证标签。根据底层Java加密服务提供商的实现,最佳情况下的最小标签长度为32位(这是NIST规范允许的最小值),但在最坏情况下可能更低(例如,8位甚至1位)。因此在最佳情况下,攻击者修改密文后相同标签被接受的概率很小但不可忽略,但在最坏情况下,这个概率可能很大。无论哪种情况,成功概率都取决于密文块的数量。此外,重复的解密失败和成功最终都会泄露认证密钥。有关如何执行此攻击的详细信息,请参阅NIST的GCM中的认证弱点

从加密的角度来看,这是一个问题。然而,由于需要存储访问,在实践中可能难以利用此问题。根据我们的发现,我们建议将标签长度固定为128位,而不是写入存储并从那里读取。

故事本应在这里结束,因为我们在初始交流后没有收到Expo的任何回复。但在我们的第二次电子邮件提醒中,我们提到我们将公开披露此问题。一周后,通过将最小标签长度限制为96位,该漏洞被静默修复。实际上,96位提供了足够的安全性。但是,也没有理由不选择更高的128位。

修复是在我们最后一次提醒整整一周后创建的。我们怀疑我们之前的电子邮件提醒导致了修复,但我们不确定。不幸的是,我们从未得到适当的认可。

案例4:num-bigint Rust库中的DoS向量

2023年7月,Trail of Bits的安全工程师Sam Moelius在著名的num-bigint Rust库中遇到了一个DoS向量。尽管通过电子邮件的披露工作非常顺利,但用户从未通过GitHub公告或CVE等方式被告知此漏洞。

num-bigint项目托管在GitHub上,但未设置GitHub私有报告,因此库作者或我们都没有快速创建公告的方式。Sam通过发送电子邮件向num-bigint的开发人员报告了此漏洞。但查找开发人员的电子邮件容易出错且耗时。您必须首先通过电子邮件确认已联系到正确的人,然后才发送漏洞详细信息。如果使用GitHub私有报告或存储库中的安全策略,发送漏洞的渠道将很明确。

但现在让我们讨论漏洞本身。该库实现了不再适合原始数据类型(如i128)的非常大的整数。除此之外,库还可以序列化和反序列化这些数据类型。Sam发现的漏洞隐藏在该序列化功能中。具体来说,由于内存消耗过大或请求的内存分配过大且失败,库可能会崩溃。

num-bigint类型实现了Serde的特性。这意味着crate中的任何类型都可以使用任意文件格式(如JSON或bincode crate使用的二进制格式)进行序列化和反序列化。以下示例程序展示了如何使用此反序列化功能:

1
2
3
4
5
6
7
8
9
use num_bigint::BigUint;
use std::io::Read;

fn main() -> std::io::Result<()> {
    let mut buf = Vec::new();
    let _ = std::io::stdin().read_to_end(&mut buf)?;
    let _: BigUint = bincode::deserialize(&buf).unwrap_or_default();
    Ok(())
}

图3:示例反序列化格式

事实证明,某些输入会导致上述程序崩溃。这是因为实现Visitor特性使用不受信任的用户输入来分配特定的向量容量。下图显示了可能导致程序崩溃并显示消息“memory allocation of 2893606913523067072 bytes failed”的代码行:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
impl<'de> Visitor<'de> for U32Visitor {
    type Value = BigUint;

    {...为简洁起见省略...}

    #[cfg(not(u64_digit))]
    fn visit_seq<S>(self, mut seq: S) -> Result<Self::Value, S::Error>
    where
        S: SeqAccess<'de>,
    {
        let len = seq.size_hint().unwrap_or(0);
        let mut data = Vec::with_capacity(len);

        {...为简洁起见省略...}
    }

    #[cfg(u64_digit)]
    fn visit_seq<S>(self, mut seq: S) -> Result<Self::Value, S::Error>
    where
        S: SeqAccess<'de>,
    {
        use crate::big_digit::BigDigit;
        use num_integer::Integer;

        let u32_len = seq.size_hint().unwrap_or(0);
        let len = Integer::div_ceil(&u32_len, &2);
        let mut data = Vec::with_capacity(len);

        {...为简洁起见省略...}
    }
}

图4:基于用户输入分配内存的代码(num-bigint/src/biguint/serde.rs#61–108)

我们于2023年7月20日首次联系作者,漏洞于2023年8月22日在提交44c87c1中修复。修复版本于次日发布为0.4.4。

案例5:通过react-native-mmkv将MMKV数据库加密密钥插入Android系统日志

最后一个案例涉及react-native-mmkv库中明文加密密钥的披露,该漏洞于2023年9月修复。在为客户进行安全代码审查期间,我发现了一个修复关键依赖项中未跟踪漏洞的提交。由于没有安全公告或CVE ID,我和客户都未被告知该漏洞。缺乏漏洞管理导致了一种情况,即攻击者知道漏洞,但用户却蒙在鼓里。

在客户参与期间,我想验证加密密钥的使用和处理方式。提交修复:“Don’t leak encryption key in logs”在react-native-mmkv库中引起了我的注意。以下代码显示了有问题的日志语句:

1
2
3
4
5
6
7
MmkvHostObject::MmkvHostObject(const std::string& instanceId, std::string path,
                               std::string cryptKey) {
  __android_log_print(ANDROID_LOG_INFO, "RNMMKV",
                      "Creating MMKV instance \"%s\"... (Path: %s, Encryption-Key: %s)",
                      instanceId.c_str(), path.c_str(), cryptKey.c_str());
  std::string* pathPtr = path.size() > 0 ? &path : nullptr;
  {...为简洁起见省略...}

图5:初始化MMKV并记录加密密钥的代码

在该修复之前,我正在调查的加密密钥以明文形式打印到Android系统日志中。这破坏了威胁模型,因为即使启用了Android调试功能,也不应从设备提取此加密密钥。

经客户同意,我通知了react-native-mmkv的作者,作者和我得出结论,应通知库用户有关漏洞的信息。因此作者启用了私有报告,我们一起发布了GitHub公告。漏洞被分配了ID CVE-2024-21668。该公告现在在运行npm audit或npm install时,如果开发人员使用易受攻击的react-native-mmkv版本,会发出警报。

这个案例强调,在npm包方面,基本上无法绕过GitHub公告。为npm audit命令提供输出的唯一方法是创建GitHub公告。使用私有报告简化了该过程。

要点

GitHub的私有报告功能有助于保护软件生态系统。如果使用正确,该功能可以为漏洞报告者和软件维护者节省时间。私有报告的最大影响是它与GitHub公告数据库相关联——例如,在使用GitLab的机密问题时缺少这种链接。通过GitHub的私有报告功能,现在有一个流程供安全研究人员发布到该数据库(经存储库维护者批准)。

通过GitHub上的私有报告,披露过程也变得更加清晰。使用电子邮件时,不清楚是否应该加密电子邮件以及应该发送给谁。如果您曾经加密过电子邮件,您知道有无尽的陷阱。

但是,您可能仍然希望向开发人员或安全联系人发送电子邮件通知,因为维护者可能会错过GitHub通知。一封包含已创建公告链接的基本电子邮件通常足以提高意识。

步骤1:添加安全策略

发布安全策略是拥有漏洞报告流程的第一步。为避免混淆,良好的策略明确定义了发现漏洞时应采取的措施。

GitHub有两种发布安全策略的方式。您可以在存储库根目录中创建SECURITY.md文件,或者通过创建.github存储库并在其根目录中放置SECURITY.md文件来创建用户或组织范围的策略。

我们建议从使用disclose.io的Policymaker生成的策略开始(参见此示例),但将“官方渠道”部分替换为以下内容:

我们有多

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计