#DirtyC0w, una condición de carrera desde hace 11 años que permite hacerse root

Dirty Cow es un bug que lleva presente 11 años en Linux y permite a cualquiera elevar privilegios como root. El fallo se encuentra desde la versión 2.6.22 (de 2007) y ha sido corregida el 18 de Oct, 2016.

Su nombre es considerado un BWAIN (Bug With An Impressive Name) y COW viene de Copy On Write (copiar al escribir), un truco que los sistemas operativos más modernos utilizan para ahorrar tiempo cuando se copia algo (por ejemplo un archivo o un bloque de memoria), que en realidad consiste simple y llanamente en que por debajo no realizan la copia de inmediato. Es decir, cuando accedes a un elemento copiado sigues accediendo al elemento original, y realmente sólo se copia si se modifica porque sólo entonces habrá dos versiones distintas que necesitan divergir.

Por supuesto, si hay un fallo en el mecanismo COW y la copia no sucede cuando debe, o sucede cuando debería no ser así, los cambios se pueden perder, o realizarse en lugar equivocado.

El bug DirtyCOW, conocido oficialmente como CVE-2016-5195, es un fallo del segundo tipo: los datos se escriben en una ubicación de memoria incorrecta.

Concretamente es una condición de carrera que se ha encontrado en la forma en la que el subsistema de memoria del kernel de Linux maneja las asignaciones del acceso de escritura de las operaciones COW. Un usuario local sin privilegios podría usar este fallo para obtener acceso de escritura a mapas de memoria de sólo lectura y aumentar así sus privilegios en el sistema.

Ya se han publicado varias PoCs que demuestran cómo funciona y puede explotarse esta vulnerabilidad. Los exploit hacen más o menos lo siguiente:

- Primero mapean su propio proceso en lo que es conocido como un mapeo privado de escritura. Como decimos, con COW no se copiará inmediatamente por podremos modificar ese mapeo privado sin afectar al programa en ejecución.

- Segundo mapean en memoria un archivo con acceso de sólo lectura. En teoría, COW debería ser irrelevante aquí: no se tiene acceso de escritura al archivo por lo que tampoco se tiene acceso de escritura en el mapa de memoria y no se debería poder cambiarlo en absoluto.

- Y tercero, se ejecutan dos threads o hilos en paralelo. Uno que escribe los cambios en el mapa de memoria escribible y privado y el otro que le dice al kernel que puede liberar temporalmente cualquier memoria utilizada para el archivo asignado. Ambos hilos "colisionarán" y los datos que se están intentando escribir en el memory map privado "accidentalmente" se volcarán en el memory map del archivo de sólo lectura.

El resultado es que un usuario normal puede alterar de forma permanente los archivos del sistema que normalmente requieren un login de root para modificarlos. Eso podría incluir archivos de configuración importantes como ssh_config, las claves de cifrado privadas, o incluso el software del sistema, como el /bin/login.

Irónicamente, este error se abordó por primera vez hace once años por el propio Linus Torvalds, pero la solución entonces fue descartada debido a que causaba problemas en la versión mainframe de IBM de Linux.

Linus lo describe como un error meramente "teórico" en ese entonces pero que ha terminado por ser perfectamente práctico:

"Se trata de un antiguo error que en realidad se trató de corregir una vez (mal) por mí hace once años [...], pero que entonces se deshechó debido a problemas en el s390 ... Lo que era una condición de carrera puramente teórica en ese entonces se ha convertido en más fácil de desencadenar".

Por ahora ya está el parche para el kernel, pero hay que esperar a que las principales distribuciones de Linux lo publiquen o bien aplicar el parche y recompilar el kernel por uno mismo...

Detalles de la vulnerabilidad

https://github.com/dirtycow/dirtycow.github.io/wiki/VulnerabilityDetails

Listado de PoCs (https://github.com/dirtycow/dirtycow.github.io/wiki/PoCs):
Análisis del PoC:

faultin_page
  handle_mm_fault
    __handle_mm_fault
      handle_pte_fault
        do_fault <- pte is not present
      do_cow_fault <- FAULT_FLAG_WRITE
        alloc_set_pte
          maybe_mkwrite(pte_mkdirty(entry), vma) <- mark the page dirty
                                but keep it RO 
# Returns with 0 and retry
follow_page_mask
  follow_page_pte
    (flags & FOLL_WRITE) && !pte_write(pte) <- retry fault

faultin_page
  handle_mm_fault
    __handle_mm_fault
      handle_pte_fault
        FAULT_FLAG_WRITE && !pte_write
      do_wp_page
        PageAnon() <- this is CoWed page already
        reuse_swap_page <- page is exclusively ours
        wp_page_reuse
          maybe_mkwrite <- dirty but RO again
          ret = VM_FAULT_WRITE
((ret & VM_FAULT_WRITE) && !(vma->vm_flags & VM_WRITE)) <- we drop FOLL_WRITE

# Returns with 0 and retry as a read fault
cond_resched -> different thread will now unmap via madvise
follow_page_mask
  !pte_present && pte_none
faultin_page
  handle_mm_fault
    __handle_mm_fault
      handle_pte_fault
        do_fault <- pte is not present
      do_read_fault <- this is a read fault and we will get pagecache
               page!

- El exploit se basa en en escribir a /proc/self/mem en una única condición de carrera.
- ptrace(PTRACE_POKEDATA) puede escribir en mapeos de sólo lectura.
- El ataque se basa en lanzar en paralelo la llamada al sistema con madvise(MADV_DONTNEED) a la vez que se tiene la página del ejecurable mapeada en memoria.

Referencias:

- https://nakedsecurity.sophos.com/2016/10/21/linux-kernel-bug-dirtycow-easyroot-hole-and-what-you-need-to-know/
- https://bugzilla.redhat.com/show_bug.cgi?id=1384344
- https://access.redhat.com/security/vulnerabilities/2706661
- https://plus.google.com/+KeesCook/posts/UUaXm3PcQ4n
- https://twitter.com/nelhage/status/789196293629370368
- https://bugzilla.suse.com/show_bug.cgi?id=1004418#c14

Comentarios