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:

  1. https://bugs.exim.org/attachment.cgi?id=1049
  2. https://bugs.exim.org/attachment.cgi?id=1052

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:

  1. My debug information is different from the one provided by meh in the last part.
  2. 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


Paper 本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1028/