在 Ruby 4.0.0 于 2025 年圣诞节发布之际,安全研究人员重新审视了 Ruby MRI(Ruby 的规范实现)中的整数处理缺陷,发现了一个被命名为 "Ruby Array Pack Bleed" 的内存泄露漏洞。这一漏洞存在于Array#pack实例方法中,影响 Ruby 4.0.0 及更早版本,可能追溯到 2002 年发布的 Ruby 1.6.7。尽管内存泄露漏洞具有严重的安全影响,但值得注意的是,受影响的方法在实际 Ruby 应用中很少使用,攻击者也很少能够控制该方法的参数。
二进制数据序列化的边界检查缺陷
Array#pack方法是 Ruby 中用于二进制数据序列化的核心工具,它接受一个模板字符串参数,用于确定如何将数组元素转换为二进制字符串。模板字符串由指令组成,每个指令通常是一个字母,如 "H" 表示高位优先的十六进制字符串,"m" 表示 Base64 编码字符串。指令可以跟随一个重复计数,指定指令应消耗多少数据,例如H2会消耗两个十六进制字符。
漏洞的核心在于重复计数的处理逻辑。在pack.c文件的pack_pack函数中,相关代码如下:
static VALUE
pack_pack(rb_execution_context_t *ec, VALUE ary, VALUE fmt, VALUE buffer)
{
long len, idx, plen;
// ...
p = RSTRING_PTR(fmt);
// ...
else if (ISDIGIT(*p)) {
len = STRTOUL(p, (char**)&p, 10);
这段代码获取重复计数并将其存储在len变量中。虽然STRTOUL宏展开为对ruby_strtoul的调用,返回一个unsigned long,但len变量本身是一个有符号的long。这种无符号和有符号类型的不匹配意味着大无符号值在存储到len时会被解释为负值。
类型系统逃逸与内存泄露机制
要利用负重复计数,需要找到一个能够有效利用负值的指令。幸运的是,X指令存在,它被记录为 "回退一个字节",其行为如下:
irb(main):001> ["414243"].pack("H6")
=> "ABC"
irb(main):002> ["414243"].pack("H6X")
=> "AB"
irb(main):003> ["414243"].pack("H6XX")
=> "A"
irb(main):004> ["414243"].pack("H6X2")
=> "A"
X指令的实现同样位于pack.c中:
static VALUE
pack_pack(rb_execution_context_t *ec, VALUE ary, VALUE fmt, VALUE buffer)
{
// ...
switch (type) {
// ...
case 'X': /* back up byte */
shrink:
plen = RSTRING_LEN(res);
if (plen < len)
rb_raise(rb_eArgError, "X outside of string");
rb_str_set_len(res, plen - len);
break;
// ...
X指令的关键在于,如果我们将字符串缩小一个负值,实际上会意外地增长字符串。这就是漏洞利用的核心机制。
漏洞利用条件与限制
通过构造特定的格式字符串,攻击者可以触发内存泄露:
irb(main):001> ["414243"].pack("H6X#{2**64}")
(irb):1:in 'Array#pack': pack length too big (RangeError)
irb(main):002> ["414243"].pack("H6X#{2**64 - 1}")
=> "ABC\x00"
irb(main):003> ["414243"].pack("H6X#{2**64 - 2}")
=> "ABC\x00\x00"
然而,攻击者无法泄露任意数量的内存,因为rb_str_set_len函数中存在防护条件:
void
rb_str_set_len(VALUE str, long len)
{
// ...
if (len > (capa = (long)str_capacity(str, termlen)) || len < 0) {
rb_bug("probable buffer overflow: %ld for %ld", len, capa);
}
}
通过尝试不同长度的字符串,研究人员发现容量被四舍五入到下一个 2 的幂次方。这意味着通过控制正在打包的数组中的字符串,可以选择长度等于 2 的幂次方的字符串,以泄露最多内存,同时避免触发防护条件。
工程修复方案:类型系统强化
修复这一漏洞的关键在于正确处理类型转换和边界检查。在 GitHub 上的 PR #15763 中,开发团队实施了以下修复方案:
-
类型一致性强化:确保重复计数在整个处理流程中保持无符号类型,避免有符号 / 无符号转换导致的数值解释错误。
-
边界检查改进:在
X指令处理逻辑中添加额外的验证,确保重复计数在合理范围内,防止负值导致的缓冲区增长。 -
容量计算优化:改进字符串容量的计算逻辑,减少因四舍五入到 2 的幂次方而可能泄露的内存范围。
修复后的代码需要确保:
- 所有数值转换都经过适当的边界检查
- 类型系统的一致性得到维护
- 内存操作的安全性得到验证
历史漏洞回顾与对比分析
除了新发现的 Ruby Array Pack Bleed 漏洞,Array#pack方法历史上还存在其他安全漏洞:
CVE-2016-2338:Use After Free 漏洞
这是一个存在于pack_pack函数中的 Use After Free 漏洞。攻击者可以通过传递特殊构造的对象作为数组元素,在转换过程中清除数组,导致在后续迭代中访问已释放的内存。漏洞的关键在于:
- 数组长度在循环开始前被读取一次,而不是每次迭代时读取
- 传递具有自定义
to_str方法的非字符串对象可以操作数组内容 - 在转换过程中清除数组会导致后续迭代访问已释放的内存
CVE-2018-16396:污染字符串传播问题
这个漏洞涉及Array#pack和String#unpack中污染字符串未正确传播的问题,可能允许远程用户绕过安全限制。
防护策略与最佳实践
针对Array#pack方法的内存安全漏洞,开发人员应采取以下防护措施:
1. 版本升级与补丁管理
- 及时升级到修复了相关漏洞的 Ruby 版本
- 关注 Ruby 安全公告,及时应用安全补丁
- 对于无法升级的生产环境,考虑使用安全补丁或替代方案
2. 输入验证与净化
- 对传递给
Array#pack的格式字符串进行严格验证 - 限制重复计数的取值范围
- 避免使用用户控制的输入作为格式字符串
3. 安全编码实践
- 避免在安全关键代码中使用
Array#pack方法 - 如果必须使用,确保对输入进行适当的清理和验证
- 考虑使用更安全的替代方案进行二进制数据序列化
4. 监控与检测
- 实施运行时监控,检测异常的内存访问模式
- 使用静态分析工具检测潜在的漏洞模式
- 定期进行安全代码审查
技术影响评估
尽管 Ruby Array Pack Bleed 漏洞允许内存泄露,但其实际影响受到以下因素的限制:
-
使用频率有限:
Array#pack方法在实际应用中很少使用,特别是在 Web 应用等常见场景中。 -
利用条件苛刻:攻击者需要能够控制格式字符串参数,这在大多数应用中不太可能。
-
泄露范围受限:由于
rb_str_set_len中的防护条件,内存泄露被限制在下一个 2 的幂次方容量内。 -
修复可用性:修复已在 PR #15763 中提供,用户可以升级到修复版本或应用补丁。
结论
Ruby Array#pack 方法的内存安全漏洞揭示了二进制数据序列化中类型系统和边界检查的重要性。从类型转换错误到 Use After Free 漏洞,这些安全问题强调了在系统编程中正确处理内存管理的重要性。
对于 Ruby 开发人员来说,理解这些漏洞的技术原理不仅有助于编写更安全的代码,还能提高对类似安全问题的识别能力。通过实施适当的防护策略、遵循安全编码实践,并及时应用安全更新,可以显著降低这些漏洞带来的风险。
随着 Ruby 语言的持续发展,安全研究人员和开发团队需要继续关注内存安全、类型系统和边界检查等核心安全问题,确保 Ruby 生态系统能够为开发人员提供既强大又安全的编程环境。
资料来源
- Luke Jahnke, "Ruby Array Pack Bleed", nastystereo.com, December 28, 2025
- Ruby GitHub Repository, PR #15763: Fix for Array#pack memory disclosure vulnerability
- Cisco Talos Intelligence Group, "Ruby pack_pack Use After Free Vulnerability", TALOS-2016-0033, June 14, 2016
- CVE-2018-16396: Tainted string propagation issue in Array#pack and String#unpack