Author: Hcamael@Knownsec 404 Team
Chinese Version: https://paper.seebug.org/469/
On Thanksgiving Day, meh submitted an Exim UAF Vulnerability on Bugzilla: https://bugs.exim.org/show_bug.cgi?id=2199. But I could not use his PoC to make a crash.
Vulnerability Recurrence
First is the recurrence.
Environment Construction
Recurrence environment: ubuntu 16.04 server
# Get source code from github
$ git clone https://github.com/Exim/exim.git# The UAF vulnerability was patched in the 4e6ae62 branch, so switch the branch to the previous 178ecb:
$ git checkout ef9da2ee969c27824fcd5aed6a59ac4cd217587b
# Install related dependencies
$ apt install libdb-dev libpcre3-dev
# Get the Makefile provided by meh, put it in the Local directory.
$ cd src
$ mkdir Local
$ cd Local
$ wget "https://bugs.exim.org/attachment.cgi?id=1051" -O Makefile
$ cd ..
# In line 134, modify the user to the present user on the server, then compile and install
$ make && make install
Then modify the 364th line of the configuration file /etc/exim/configure
. Modify
accept hosts = :
to accept hosts = *
.
PoC Test
Get the debug information of meh from https://bugs.exim.org/attachment.cgi?id=1050 :
$ /usr/exim/bin/exim -bdf -d+all
There are two PoCs:
First is to install pwntools with pip. The difference between the two PoC is that they have different length of padding.
Then I used PoC to test and found several problems:
- My debug information is different from the one provided by meh in the last part.
- Although crash is triggered, it is not caused by UAF
Here are the differences:
# My debug information
12:15:09 8215 SMTP>> 500 unrecognized command
12:15:09 8215 SMTP<< BDAT 1
12:15:09 8215 chunking state 1, 1 bytes
12:15:09 8215 search_tidyup called
12:15:09 8215 SMTP>> 250 1 byte chunk received
12:15:09 8215 chunking state 0
12:15:09 8215 SMTP<< BDAT
12:15:09 8215 LOG: smtp_protocol_error MAIN
12:15:09 8215 SMTP protocol error in "BDAT \177" H=(test) [10.0.6.18] missing size for BDAT command
12:15:09 8215 SMTP>> 501 missing size for BDAT command
12:15:09 8215 host in ignore_fromline_hosts? no (option unset)
12:15:09 8215 >>Headers received:
12:15:09 8215 :
...Undisplayable characters
**** debug string too long - truncated ****
12:15:09 8215
12:15:09 8215 search_tidyup called
12:15:09 8215 >>Headers after rewriting and local additions:
12:15:09 8215 :
......Undisplayable characters
**** debug string too long - truncated ****
12:15:09 8215
12:15:09 8215 Data file name: /var/spool/exim//input//1eKcjF-00028V-5Y-D
12:15:29 8215 LOG: MAIN
12:15:29 8215 SMTP connection from (test) [10.0.6.18] lost while reading message data
12:15:29 8215 SMTP>> 421 Lost incoming connection
12:15:29 8215 LOG: MAIN PANIC DIE
12:15:29 8215 internal error: store_reset(0x2443048) failed: pool=0 smtp_in.c 841
12:15:29 8215 SMTP>> 421 Unexpected failure, please try later
12:15:29 8215 LOG: MAIN PANIC DIE
12:15:29 8215 internal error: store_reset(0x2443068) failed: pool=0 smtp_in.c 841
12:15:29 8215 SMTP>> 421 Unexpected failure, please try later
12:15:29 8215 LOG: MAIN PANIC DIE
12:15:29 8215 internal error: store_reset(0x2443098) failed: pool=0 smtp_in.c 841
12:15:29 8215 SMTP>> 421 Unexpected failure, please try later
12:15:29 8215 LOG: MAIN PANIC DIE
12:15:29 8215 internal error: store_reset(0x24430c8) failed: pool=0 smtp_in.c 841
12:15:29 8215 SMTP>> 421 Unexpected failure, please try later
12:15:29 8215 LOG: MAIN PANIC DIE
12:15:29 8215 internal error: store_reset(0x24430f8) failed: pool=0 smtp_in.c 841
12:15:29 8215 SMTP>> 421 Unexpected failure, please try later
12:15:29 8215 LOG: MAIN PANIC DIE
12:15:29 8215 internal error: store_reset(0x2443128) failed: pool=0 smtp_in.c 841
12:15:29 8215 SMTP>> 421 Unexpected failure, please try later
12:15:29 8215 LOG: MAIN PANIC DIE
12:15:29 8215 internal error: store_reset(0x2443158) failed: pool=0 smtp_in.c 841
12:15:29 8215 SMTP>> 421 Unexpected failure, please try later
12:15:29 8215 LOG: MAIN PANIC DIE
12:15:29 8215 internal error: store_reset(0x2443188) failed: pool=0 smtp_in.c 841
12:16:20 8213 child 8215 ended: status=0x8b
12:16:20 8213 signal exit, signal 11 (core dumped)
12:16:20 8213 0 SMTP accept processes now running
12:16:20 8213 Listening...
--------------------------------------------
# meh's debug information
10:31:59 21724 SMTP>> 500 unrecognized command
10:31:59 21724 SMTP<< BDAT 1
10:31:59 21724 chunking state 1, 1 bytes
10:31:59 21724 search_tidyup called
10:31:59 21724 SMTP>> 250 1 byte chunk received
10:31:59 21724 chunking state 0
10:31:59 21724 SMTP<< BDAT
10:31:59 21724 LOG: smtp_protocol_error MAIN
10:31:59 21724 SMTP protocol error in "BDAT \177" H=(test) [127.0.0.1] missing size for BDAT command
10:31:59 21724 SMTP>> 501 missing size for BDAT command
10:31:59 21719 child 21724 ended: status=0x8b
10:31:59 21719 signal exit, signal 11 (core dumped)
10:31:59 21719 0 SMTP accept processes now running
10:31:59 21719 Listening...
It indeed threw an exception, but my debug information is different from meh's. I used gdb to debug and found:
RAX 0xfbad240c
*RBX 0x30
*RCX 0xffffffffffffffd4
RDX 0x2000
*RDI 0x2b
*RSI 0x4b7e8e ?— jae 0x4b7f04 /* 'string.c' */
*R8 0x0
*R9 0x24
*R10 0x24
*R11 0x4a69e8 ?— push rbp
*R12 0x4b7e8e ?— jae 0x4b7f04 /* 'string.c' */
*R13 0x1a9
*R14 0x24431b8 ?— 0x0
*R15 0x5e
*RBP 0x2000
*RSP 0x7ffd75b862c0 —? 0x7ffd75b862d0 ?— 0xffffffffffffffff
*RIP 0x46cf1b (store_get_3+117) ?— cmp qword ptr [rax + 8], rdx
--------------
> 0x46cf1b <store_get_3+117> cmp qword ptr [rax + 8], rdx
------------
Program received signal SIGSEGV (fault address 0xfbad2414)
The crash was not caused by UAF. If the option -d+all
of debug all is replaced with the option -dd
which only displays simple debug information, then no exception will be thrown.
$ sudo ./build-Linux-x86_64/exim -bdf -dd
......
8266 Listening...
8268 Process 8268 is handling incoming connection from [10.0.6.18]
8266 child 8268 ended: status=0x0
8266 normal exit, 0
8266 0 SMTP accept processes now running
8266 Listening...
I again read meh's description on Bugzilla carefully. Maybe it was because of the size of the padding? So I wrote the code to blast the padding at the length from 0-0x4000 but did not find the lenth to cause a crash.
This PoC is affected by the block layout(yield_length), so this line:
r.sendline('a'*0x1250+'\x7f')
should be adjusted according to the program state.
Therefore, it can be ruled out that the PoC test fails because of the padding length.
And I have also found that the author of Exim also tried to test the vulnerability, but he failed ,too. He posted his debug information which is almost the same as mine. (I don't know if he made it after getting meh's Makefile and log).
Study
The full name of UAF is use after free, so I patched a printf before it is freed:
# src/store.c
......
448 void
449 store_release_3(void *block, const char *filename, int linenumber)
450 {
......
481 printf("--------free: %8p-------\n", (void *)bb);
482 free(bb);
483 return;
484 }
Recompile and run again, and it was triggered:
And the gdb debugging information also proves that uaf vulnerability can cause a crash:
*RAX 0xdeadbeef
*RBX 0x1e2e5d0 ?— 0x0
*RCX 0x1e29341 ?— 0xadbeef000000000a /* '\n' */
*RDX 0x7df
*RDI 0x1e2e5d0 ?— 0x0
*RSI 0x46cedd (store_free_3+70) ?— pop rbx
*R8 0x0
R9 0x7f054f32b700 ?— 0x7f054f32b700
*R10 0xffff80fab41c4748
*R11 0x203
*R12 0x7f054dc69993 (state+3) ?— 0x0
*R13 0x4ad5b6 ?— jb 0x4ad61d /* 'receive.c' */
*R14 0x7df
*R15 0x1e1d8f0 ?— 0x0
*RBP 0x0
*RSP 0x7ffe169262b8 —? 0x7f054d9275e7 (free+247) ?— add rsp, 0x28
*RIP 0xdeadbeef
------------------------------------------
Invalid address 0xdeadbeef
PS: Here is the ./build-Linux-x86_64/exim
binary does not patch printf, /usr/exim/bin/exim
patches printf.
It is very strange that adding a printf can trigger the vulnerability, and delete it can not. And i also used puts
and write
instead of printf
to test, and I found that puts
can also trigger the vulnerability, but write
can't. Probably it is because of stdio's buffer mechanism.
In-depth study
Take a look at meh's description of the vulnerability on Bugzilla:
Hi, we found a use-after-free vulnerability which is exploitable to RCE in the SMTP server.
According to receive.c:1783,
1783 if (!store_extend(next->text, oldsize, header_size))
1784 {
1785 uschar *newtext = store_get(header_size);
1786 memcpy(newtext, next->text, ptr);
1787 store_release(next->text);
1788 next->text = newtext;
1789 }
when the buffer used to parse header is not big enough, exim tries to extend the next->text with store_extend function. If there is any other allocation between the allocation and extension of this buffer, store_extend fails.
store.c
276 if ((char *)ptr + rounded_oldsize != (char *)(next_yield[store_pool]) ||
277 inc yield_length[store_pool] + rounded_oldsize - oldsize)
278 return FALSE;
Then exim calls store_get, and store_get cut the current_block directly.
store.c
208 next_yield[store_pool] = (void *)((char *)next_yield[store_pool] + size);
209 yield_length[store_pool] -= size;
210
211 return store_last_get[store_pool];
However, in receive.c:1787, store_release frees the whole block, leaving the new pointer points to a freed location. Any further usage of this buffer leads to a use-after-free vulnerability.
To trigger this bug, BDAT command is necessary to perform an allocation by raising an error. Through our research, we confirm that this vulnerability can be exploited to remote code execution if the binary is not compiled with PIE.
An RIP controlling PoC is in attachment poc.py. The following is the gdb result of this PoC:
Program received signal SIGSEGV, Segmentation fault.
0x00000000deadbeef in ?? ()
(gdb)
-------------------------------------------------------------
In receive.c, exim used receive_getc to get message.
1831 ch = (receive_getc)(GETC_BUFFER_UNLIMITED);
When exim is handling BDAT command, receive_getc is bdat_getc.
In bdat_getc, after the length of BDAT is reached, bdat_getc tries to read the next command.
smtp_in.c
536 next_cmd:
537 switch(smtp_read_command(TRUE, 1))
538 {
539 default:
540 (void) synprot_error(L_smtp_protocol_error, 503, NULL,
541 US"only BDAT permissible after non-LAST BDAT");
synprot_error may call store_get if any non-printable character exists because synprot_error uses string_printing.
string.c
304 /* Get a new block of store guaranteed big enough to hold the
305 expanded string. */
306
307 ss = store_get(length + nonprintcount * 3 + 1);
------------------------------------------------------------------
receive_getc becomes bdat_getc when handling BDAT data.
Oh, I was talking about the source code of 4.89. In the current master, it is here:
https://github.com/Exim/exim/blob/master/src/src/receive.c#L1790
What this PoC does is:
1. send unrecognized command to adjust yield_length and make it less than 0x100
2. send BDAT 1
3. send one character to reach the length of BDAT
3. send an BDAT command without size and with non-printable character -trigger synprot_error and therefore call store_get
// back to receive_msg and exim keeps trying to read header
4. send a huge message until store_extend called
5. uaf
This PoC is affected by the block layout(yield_length), so this line: `r.sendline('a'*0x1250+'\x7f')` should be adjusted according to the program state. I tested on my ubuntu 16.04, compiled with the attached Local/Makefile (simply make -j8). I also attach the updated PoC for current master and the debug report.
There is a simple heap management in Exim. In src/store.c:
void *
store_get_3(int size, const char *filename, int linenumber)
{
/* Round up the size to a multiple of the alignment. Although this looks a
messy statement, because "alignment" is a constant expression, the compiler can
do a reasonable job of optimizing, especially if the value of "alignment" is a
power of two. I checked this with -O2, and gcc did very well, compiling it to 4
instructions on a Sparc (alignment = 8). */
if (size % alignment != 0) size += alignment - (size % alignment);
/* If there isn't room in the current block, get a new one. The minimum
size is STORE_BLOCK_SIZE, and we would expect this to be the norm, since
these functions are mostly called for small amounts of store. */
if (size > yield_length[store_pool])
{
int length = (size <= STORE_BLOCK_SIZE)? STORE_BLOCK_SIZE : size;
int mlength = length + ALIGNED_SIZEOF_STOREBLOCK;
storeblock * newblock = NULL;
/* Sometimes store_reset() may leave a block for us; check if we can use it */
if ( (newblock = current_block[store_pool])
&& (newblock = newblock->next)
&& newblock->length < length
)
{
/* Give up on this block, because it's too small */
store_free(newblock);
newblock = NULL;
}
/* If there was no free block, get a new one */
if (!newblock)
{
pool_malloc += mlength; /* Used in pools */
nonpool_malloc -= mlength; /* Exclude from overall total */
newblock = store_malloc(mlength);
newblock->next = NULL;
newblock->length = length;
if (!chainbase[store_pool])
chainbase[store_pool] = newblock;
else
current_block[store_pool]->next = newblock;
}
current_block[store_pool] = newblock;
yield_length[store_pool] = newblock->length;
next_yield[store_pool] =
(void *)(CS current_block[store_pool] + ALIGNED_SIZEOF_STOREBLOCK);
(void) VALGRIND_MAKE_MEM_NOACCESS(next_yield[store_pool], yield_length[store_pool]);
}
/* There's (now) enough room in the current block; the yield is the next
pointer. */
store_last_get[store_pool] = next_yield[store_pool];
/* Cut out the debugging stuff for utilities, but stop picky compilers from
giving warnings. */
#ifdef COMPILE_UTILITY
filename = filename;
linenumber = linenumber;
#else
DEBUG(D_memory)
{
if (running_in_test_harness)
debug_printf("---%d Get %5d\n", store_pool, size);
else
debug_printf("---%d Get %6p %5d %-14s %4d\n", store_pool,
store_last_get[store_pool], size, filename, linenumber);
}
#endif /* COMPILE_UTILITY */
(void) VALGRIND_MAKE_MEM_UNDEFINED(store_last_get[store_pool], size);
/* Update next pointer and number of bytes left in the current block. */
next_yield[store_pool] = (void *)(CS next_yield[store_pool] + size);
yield_length[store_pool] -= size;
return store_last_get[store_pool];
}
BOOL
store_extend_3(void *ptr, int oldsize, int newsize, const char *filename,
int linenumber)
{
int inc = newsize - oldsize;
int rounded_oldsize = oldsize;
if (rounded_oldsize % alignment != 0)
rounded_oldsize += alignment - (rounded_oldsize % alignment);
if (CS ptr + rounded_oldsize != CS (next_yield[store_pool]) ||
inc > yield_length[store_pool] + rounded_oldsize - oldsize)
return FALSE;
/* Cut out the debugging stuff for utilities, but stop picky compilers from
giving warnings. */
#ifdef COMPILE_UTILITY
filename = filename;
linenumber = linenumber;
#else
DEBUG(D_memory)
{
if (running_in_test_harness)
debug_printf("---%d Ext %5d\n", store_pool, newsize);
else
debug_printf("---%d Ext %6p %5d %-14s %4d\n", store_pool, ptr, newsize,
filename, linenumber);
}
#endif /* COMPILE_UTILITY */
if (newsize % alignment != 0) newsize += alignment - (newsize % alignment);
next_yield[store_pool] = CS ptr + newsize;
yield_length[store_pool] -= newsize - rounded_oldsize;
(void) VALGRIND_MAKE_MEM_UNDEFINED(ptr + oldsize, inc);
return TRUE;
}
void
store_release_3(void *block, const char *filename, int linenumber)
{
storeblock *b;
/* It will never be the first block, so no need to check that. */
for (b = chainbase[store_pool]; b != NULL; b = b->next)
{
storeblock *bb = b->next;
if (bb != NULL && CS block == CS bb + ALIGNED_SIZEOF_STOREBLOCK)
{
b->next = bb->next;
pool_malloc -= bb->length + ALIGNED_SIZEOF_STOREBLOCK;
/* Cut out the debugging stuff for utilities, but stop picky compilers
from giving warnings. */
#ifdef COMPILE_UTILITY
filename = filename;
linenumber = linenumber;
#else
DEBUG(D_memory)
{
if (running_in_test_harness)
debug_printf("-Release %d\n", pool_malloc);
else
debug_printf("-Release %6p %-20s %4d %d\n", (void *)bb, filename,
linenumber, pool_malloc);
}
if (running_in_test_harness)
memset(bb, 0xF0, bb->length+ALIGNED_SIZEOF_STOREBLOCK);
#endif /* COMPILE_UTILITY */
free(bb);
return;
}
}
}
Key functions involved in UAF vulnerabilities:
- store_get_3 heap allocation
- store_extend_3 heap extension
- store_release_3 heap release
There are also 4 important global variables:
- chainbase
- next_yield
- current_block
- yield_length
First step
Send a bunch of unknown commands to adjust the value of yield_length
to less than 0x100.
yield_length
indicates the remaining length of the heap. Use [src/receive.c] (https://github.com/Exim/exim/blob/ef9da2ee969c27824fcd5aed6a59ac4cd217587b/src/src/receive. c#L1617) receive_msg
function to process the command.
When the function processes the command, use next->text
to store the input and initialize it on line 1709:
1625 int header_size = 256;
......
1709 next->text = store_get(header_size);
At line 1709, if 0x100 > yield_length
then the program will excute newblock = store_malloc(mlength);
. Use glibc's malloc to apply for a block of memory clled heap1.
According to the code in store_get_3
, this time:
- current_block->next = heap1 (because block==chainbase,and this means hainbase->next = heap1)
- current_block = heap1
- yield_length = 0x2000
- next_yield = heap1+0x10
- return next_yield
- next_yield = next_yield+0x100 = heap1+0x110
- yield_length = yield_length - 0x100 = 0x1f00
Second Step
Send BDAT 1
, enter the receive_msg
function, and make receive_getc
become bdat_getc
.
Third Step
Send BDAT \x7f
.
The bdat_getc
function in [src/smtp_in.c] (https://github.com/Exim/exim/blob/b488395f4d99d44a950073a64b35ec8729102782/src/src/smtp_in.c):
int
bdat_getc(unsigned lim)
{
uschar * user_msg = NULL;
uschar * log_msg;
for(;;)
{
#ifndef DISABLE_DKIM
BOOL dkim_save;
#endif
if (chunking_data_left > 0)
return lwr_receive_getc(chunking_data_left--);
receive_getc = lwr_receive_getc;
receive_getbuf = lwr_receive_getbuf;
receive_ungetc = lwr_receive_ungetc;
#ifndef DISABLE_DKIM
dkim_save = dkim_collect_input;
dkim_collect_input = FALSE;
#endif
/* Unless PIPELINING was offered, there should be no next command
until after we ack that chunk */
if (!pipelining_advertised && !check_sync())
{
unsigned n = smtp_inend - smtp_inptr;
if (n > 32) n = 32;
incomplete_transaction_log(US"sync failure");
log_write(0, LOG_MAIN|LOG_REJECT, "SMTP protocol synchronization error "
"(next input sent too soon: pipelining was not advertised): "
"rejected \"%s\" %s next input=\"%s\"%s",
smtp_cmd_buffer, host_and_ident(TRUE),
string_printing(string_copyn(smtp_inptr, n)),
smtp_inend - smtp_inptr > n ? "..." : "");
(void) synprot_error(L_smtp_protocol_error, 554, NULL,
US"SMTP synchronization error");
goto repeat_until_rset;
}
/* If not the last, ack the received chunk. The last response is delayed
until after the data ACL decides on it */
if (chunking_state == CHUNKING_LAST)
{
#ifndef DISABLE_DKIM
dkim_exim_verify_feed(NULL, 0); /* notify EOD */
#endif
return EOD;
}
smtp_printf("250 %u byte chunk received\r\n", FALSE, chunking_datasize);
chunking_state = CHUNKING_OFFERED;
DEBUG(D_receive) debug_printf("chunking state %d\n", (int)chunking_state);
/* Expect another BDAT cmd from input. RFC 3030 says nothing about
QUIT, RSET or NOOP but handling them seems obvious */
next_cmd:
switch(smtp_read_command(TRUE, 1))
{
default:
(void) synprot_error(L_smtp_protocol_error, 503, NULL,
US"only BDAT permissible after non-LAST BDAT");
repeat_until_rset:
switch(smtp_read_command(TRUE, 1))
{
case QUIT_CMD: smtp_quit_handler(&user_msg, &log_msg); /*FALLTHROUGH */
case EOF_CMD: return EOF;
case RSET_CMD: smtp_rset_handler(); return ERR;
default: if (synprot_error(L_smtp_protocol_error, 503, NULL,
US"only RSET accepted now") > 0)
return EOF;
goto repeat_until_rset;
}
case QUIT_CMD:
smtp_quit_handler(&user_msg, &log_msg);
/*FALLTHROUGH*/
case EOF_CMD:
return EOF;
case RSET_CMD:
smtp_rset_handler();
return ERR;
case NOOP_CMD:
HAD(SCH_NOOP);
smtp_printf("250 OK\r\n", FALSE);
goto next_cmd;
case BDAT_CMD:
{
int n;
if (sscanf(CS smtp_cmd_data, "%u %n", &chunking_datasize, &n) < 1)
{
(void) synprot_error(L_smtp_protocol_error, 501, NULL,
US"missing size for BDAT command");
return ERR;
}
chunking_state = strcmpic(smtp_cmd_data+n, US"LAST") == 0
? CHUNKING_LAST : CHUNKING_ACTIVE;
chunking_data_left = chunking_datasize;
DEBUG(D_receive) debug_printf("chunking state %d, %d bytes\n",
(int)chunking_state, chunking_data_left);
if (chunking_datasize == 0)
if (chunking_state == CHUNKING_LAST)
return EOD;
else
{
(void) synprot_error(L_smtp_protocol_error, 504, NULL,
US"zero size for BDAT command");
goto repeat_until_rset;
}
receive_getc = bdat_getc;
receive_getbuf = bdat_getbuf;
receive_ungetc = bdat_ungetc;
#ifndef DISABLE_DKIM
dkim_collect_input = dkim_save;
#endif
break; /* to top of main loop */
}
}
}
}
The BDAT command enters the following branch:
f (sscanf(CS smtp_cmd_data, "%u %n", &chunking_datasize, &n) < 1)
{
(void) synprot_error(L_smtp_protocol_error, 501, NULL,
US"missing size for BDAT command");
return ERR;
}
Because of \x7F
, sscanf fails to get the length. Enter the synprot_error
function, which is also located in the smtp_in.c
file:
static int
synprot_error(int type, int code, uschar *data, uschar *errmess)
{
int yield = -1;
log_write(type, LOG_MAIN, "SMTP %s error in \"%s\" %s %s",
(type == L_smtp_syntax_error)? "syntax" : "protocol",
string_printing(smtp_cmd_buffer), host_and_ident(TRUE), errmess);
if (++synprot_error_count > smtp_max_synprot_errors)
{
yield = 1;
log_write(0, LOG_MAIN|LOG_REJECT, "SMTP call from %s dropped: too many "
"syntax or protocol errors (last command was \"%s\")",
host_and_ident(FALSE), string_printing(smtp_cmd_buffer));
}
if (code > 0)
{
smtp_printf("%d%c%s%s%s\r\n", FALSE, code, yield == 1 ? '-' : ' ',
data ? data : US"", data ? US": " : US"", errmess);
if (yield == 1)
smtp_printf("%d Too many syntax or protocol errors\r\n", FALSE, code);
}
return yield;
}
In src/string.c, there is a string_printing
function in the synprot_error
function :
const uschar *
string_printing2(const uschar *s, BOOL allow_tab)
{
int nonprintcount = 0;
int length = 0;
const uschar *t = s;
uschar *ss, *tt;
while (*t != 0)
{
int c = *t++;
if (!mac_isprint(c) || (!allow_tab && c == '\t')) nonprintcount++;
length++;
}
if (nonprintcount == 0) return s;
/* Get a new block of store guaranteed big enough to hold the
expanded string. */
ss = store_get(length + nonprintcount * 3 + 1);
/* Copy everything, escaping non printers. */
t = s;
tt = ss;
while (*t != 0)
{
int c = *t;
if (mac_isprint(c) && (allow_tab || c != '\t')) *tt++ = *t++; else
{
*tt++ = '\\';
switch (*t)
{
case '\n': *tt++ = 'n'; break;
case '\r': *tt++ = 'r'; break;
case '\b': *tt++ = 'b'; break;
case '\v': *tt++ = 'v'; break;
case '\f': *tt++ = 'f'; break;
case '\t': *tt++ = 't'; break;
default: sprintf(CS tt, "%03o", *t); tt += 3; break;
}
t++;
}
}
*tt = 0;
return ss;
}
We will use store_get
in the string_printing2
function. Its length is length + nonprintcount * 3 + 1
. For example, the length of BDAT \x7F
command is 6+1*3+1 => 0x0a
. And because 0xa < yield_length
, it uses Exim's heap allocation directly, and only when the last malloc 0x2000 memory is used up or not enough will we uses malloc.
- 0xa aligns -> 0x10
- return next_yield = heap1+0x110
- next_yield = heap1+0x120
- yield_length = 0x1f00 - 0x10 = 0x1ef0
The final step is to send a large amount of data in the PoC to trigger the UAF:
s = 'a'*6 + p64(0xdeadbeef)*(0x1e00/8)
R.send(s+ ':\r\n')
Back to the receive.c
file. The 1788-line loop read the input. According to meh, the following lines of code is the trigger of UAF:
if (ptr >= header_size - 4)
{
int oldsize = header_size;
/* header_size += 256; */
header_size *= 2;
if (!store_extend(next->text, oldsize, header_size))
{
uschar *newtext = store_get(header_size);
memcpy(newtext, next->text, ptr);
store_release(next->text);
next->text = newtext;
}
}
When the input data is greater than or equal to 0x100-4
, the store_extend
function will be triggered. The value of next->text
is heap1+0x10
, oldsize=0x100, header_size = 0x100*2 = 0x200
.
Then in store_extend
, there are several lines of judgments:
if (CS ptr + rounded_oldsize != CS (next_yield[store_pool]) ||
inc > yield_length[store_pool] + rounded_oldsize - oldsize)
return FALSE;
Where next_yield = heap1+0x120
, ptr + 0x100 = heap1+0x110
The result of the judgment is true, so store_extend
returns False
This is because a memory is allocated in the string_printing
function, so the heap is unbalanced in receive_msg
.
Subsequent entry into the branch will fix this imbalance and execute store_get(0x200)
- return next_yield = heap1+0x120
- next_yield = heap1+0x320
- yield_length = 0x1ef0 - 0x200 = 0x1cf0
Then it will copy the entered data into the new heap.
The problem is at the store_release
function. The previously applied 0x2000 heap has 0x1cf0 left, but we perform glibc free operation on it. This is what we know about UAF, reusing the vulnerability after release.
for (b = chainbase[store_pool]; b != NULL; b = b->next)
{
storeblock *bb = b->next;
if (bb != NULL && CS block == CS bb + ALIGNED_SIZEOF_STOREBLOCK)
{
b->next = bb->next;
.......
free(bb);
return;
}
Here bb = chainbase->next = heap1
, and next->text == bb + 0x10
, and we can execute free(bb)
.
Because a lot of data is entered, the below will also be excuted:
- store_extend(next->text, 0x200, 0x400)
- store_extend(next->text, 0x400, 0x800)
- store_extend(next->text, 0x800, 0x1000)
But when it comes to the judment:
if (CS ptr + rounded_oldsize != CS (next_yield[store_pool]) || inc > yield_length[store_pool] + rounded_oldsize - oldsize)
It will return true and will not enter the following branch.
However, when it comes to store_extend(next->text, 0x1000, 0x2000)
, it returns False again because the second judgment 0x2000-0x1000 > yield_length[store_pool]
was satusfied.
So it will enter the branch again and call store_get(0x2000)
Because 0x2000 > yield_length
so the program will enter the branch:
if (size > yield_length[store_pool])
{
int length = (size <= STORE_BLOCK_SIZE)? STORE_BLOCK_SIZE : size;
int mlength = length + ALIGNED_SIZEOF_STOREBLOCK;
storeblock * newblock = NULL;
if ( (newblock = current_block[store_pool])
&& (newblock = newblock->next)
&& newblock->length < length
)
{
/* Give up on this block, because it's too small */
store_free(newblock);
newblock = NULL;
}
if (!newblock)
{
pool_malloc += mlength; /* Used in pools */
nonpool_malloc -= mlength; /* Exclude from overall total */
newblock = store_malloc(mlength);
newblock->next = NULL;
newblock->length = length;
if (!chainbase[store_pool])
chainbase[store_pool] = newblock;
else
current_block[store_pool]->next = newblock;
}
current_block[store_pool] = newblock;
yield_length[store_pool] = newblock->length;
next_yield[store_pool] =
(void *)(CS current_block[store_pool] + ALIGNED_SIZEOF_STOREBLOCK);
(void) VALGRIND_MAKE_MEM_NOACCESS(next_yield[store_pool], yield_length[store_pool]);
}
Here is the key of exploiting this vulnerability.
First: newblock = current_block = heap1
.
Second: newblock = newblock->next
.
I guess the case of meh is the same as the case where I added printf
to test. In printf
, we need to malloc a heap as a buffer, so there is another heap under heap1. After heap1 is freed, it will be placed in unsortbin, and fd and bk point to arena.
So at this time, heap1->next = fd = arena_top
.
The following process is:
- current_block = arena_top
- next_yield = arena_top+0x10
- return next_yield = arena_top+0x10
- next_yield = arena_top+0x2010
After executing store_get
, execute memcpy
:
Memcpy(newtext, next->text, ptr);
The newtext
above is arena_top+0x10
returned by store_get
.
Copy the entered data into the arena, and finally we can control RIP=0xdeadbeef
to cause a crash.
But the actual situation is different. Because there is no printf, so heap1 is the last heap, and it will be merged into top_chunk after it is freed. Fd and bk fields will not be modified as they are used to store the next
and length
of the storeblock structure.
Summary
CVE-2017-16943 is indeed a UAF vulnerability, but I can't use the PoC provided by meh to cause a crash.
I have tried other methods, but did not find a suitable use chain.
Since Exim implements a heap management, it is not possible to use store_get
to malloc a heap after heap1, because current_block will be modified to point to the latest heap. So only we malloc a heap without using store_get
, can we control RIP.
Besides this, exim also uses store_get
to get memory, so we can only find printf
which has its own function using malloc. But these functions will exit the loop of the receive_msg
function after they are used, so I could not construct a chain.
Reference
1.Exim Source Code
2.Bugzilla-2199
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1028/