Daxia Blog
Uncategorized | Rust | WebUI | FHIR | CyberSecurity | KB | Javascript

使用Rust进行字符串比对:时序攻击风险与防御实践

本文的编写,源自哔哩哔哩上一个Rust编程小技巧:判断字符串是否全为数字。里面提到了两种方法:

基础遍历法

  fn is_all_digits_ascii(s: &str) -> bool {
    for char in s.chars() {
  	if !char.is_ascii_digit() {
        return false;
      }
    }
    true
  }

优雅迭代法

  fn is_all_digits_ascii(s: &str) -> bool {
    s.chars().all(|c| c.is_ascii_digit())
  }

评论里有人指出:这样写有时序攻击隐患。

我就去了解了下时序攻击,看看如何在安全场景下如何避免风险。

什么是时序攻击

时序攻击(Timing Attack) 是一种通过分析程序执行时间的差异来推断敏感信息的攻击手段。它属于旁路攻击(Side-Channel Attack)的一种,攻击者不直接破解算法,而是利用程序在不同输入下的运行时间差异来推测秘密数据(如密码、加密密钥等)。

时序攻击的原理

程序在处理不同输入时,可能因逻辑分支、缓存命中率、硬件优化等因素导致执行时间存在微小差异。例如:

  • 密码验证:逐字符比较密码时,若发现某一位不匹配立即返回失败,则错误密码在第一位出错和最后一位出错的执行时间不同。
  • 加密算法:RSA等加密操作中,某些数学运算(如模幂运算)的时间可能泄露密钥的二进制位。

攻击者通过多次测量时间差,结合统计学分析,逐步缩小可能性,最终破解秘密信息。

经典案例

密码比较漏洞

早期系统使用类似以下代码逐字符比较密码:

  def compare(password, input):
    for i in range(len(password)):
        if password[i] != input[i]:
            return False  # 立即返回,导致时间差异
    return True

攻击者通过测量不同输入的时间差,逐步猜解正确密码。

网络服务中的用户枚举

某些系统验证用户名和密码时,若先检查用户名是否存在再验证密码,攻击者可通过响应时间差异判断用户名是否有效。

如何避免时序攻击风险

首先,要看看上下文环境:

  • 非安全场景:优先使用基础方法
  • 安全场景:例如认证/授权系统,必须使用恒定时间检查

时序攻击防御的核心是消除时间与敏感数据的关联性,并通过工程手段增加攻击难度。

我们就以最常见的安全场景 - 密码比对为例来阐述如何避免时序攻击风险。

在Rust语言中,常规的字符串比对操作(如 == 运算符)确实存在时序攻击风险:

  fn compare_string(a: &str, b: &str) -> bool {
    a == b // use Eq trait
  }

在比对过程中,有两个逻辑存在风险:

  • 字符串长度不相等,直接返回false
  • 字符串逐个字符比对时,遇到第一个不相同的就直接返回false

针对上面的缺陷,自己编写了一个防止时序攻击的字符串比对函数:

  pub fn constant_time_string_compare(a: &str, b: &str) -> bool {
      let a_bytes = a.as_bytes();
      let b_bytes = b.as_bytes();
      
      // 获取最长字符串的长度
      let max_len = std::cmp::max(a_bytes.len(), b_bytes.len());
      
      // 初始结果:0表示字符串相等,非0表示不等
      let mut result: u8 = 0;
      
      // 遍历所有字节位置
      for i in 0..max_len {
          // 如果超出字符串长度,使用0作为填充
          let a_byte = if i < a_bytes.len() { a_bytes[i] } else { 0 };
          let b_byte = if i < b_bytes.len() { b_bytes[i] } else { 0 };
          
          // 使用按位或(|)操作累积差异
          // 如果所有字节都相同,result保持为0;否则result会变为非0值
          result |= a_byte ^ b_byte;
      }
      
      // 最终检查是否有差异(使用==0避免条件分支)
      result == 0
  }

通过本文的优化,开发者可以更系统地理解时序攻击防御策略,在Rust开发中做出合理的安全决策。

记住:安全永远是功能需求的一部分,而不是事后补充的附加功能。

About Daxia
我是一名独立开发者,国家工信部认证高级系统架构设计师,在健康信息化领域与许多组织合作。具备大型卫生信息化平台产品架构、设计和开发的能力,从事软件研发、服务咨询、解决方案、行业标准编著相关工作。
我对健康信息化非常感兴趣,尤其是与HL7和FHIR标准的健康互操作性。我是HL7中国委员会成员,从事FHIR培训讲师和FHIR测评现场指导。
我还是FHIR Chi的作者,这是一款用于FHIR测评的工具。