
escalada de privilegios en Linux
Presentamos Mempodipper, un exploit para CVE-2012-0056. /proc/pid/mem es una interfaz para leer y escribir directamente en la memoria de un proceso, desplazándose por las mismas direcciones que el espacio de memoria virtual del proceso. En 2.6.39, las protecciones contra el acceso no autorizado a /proc/pid/mem se consideraron suficientes, por lo que el #ifdef anterior que impedía la escritura en la memoria arbitraria de procesos se eliminó. Cualquiera con los permisos adecuados podía escribir en la memoria de un proceso. Resulta, por supuesto, que la comprobación de permisos estaba mal hecha. Esto significa que todos los kernels de Linux >=2.6.39 son vulnerables, hasta el commit que lo corrige de hace un par de días. Repasemos el antiguo código del kernel paso a paso y veamos cuál es el problema.
Cuando se abre /proc/pid/mem, se llama al siguiente código del kernel:
static int mem_open(struct inode* inode, struct file* file)
{
file->private_data = (void*)((long)current->self_exec_id);
/* OK to pass negative loff_t, we can catch out-of-range */
file->f_mode |= FMODE_UNSIGNED_OFFSET;
return 0;
}
No hay restricciones para abrirlo; cualquiera puede abrir el /proc/pid/mem fd de cualquier proceso (sujeto a las restricciones VFS ordinarias). Simplemente registra el self_exec_id del proceso original con el que se abrió y lo guarda para verificarlo después durante las lecturas y escrituras.
Sin embargo, las escrituras (y las lecturas) tienen restricciones de comprobación de permisos. Echemos un vistazo a la función de escritura:
static ssize_t mem_write(struct file * file, const char __user *buf,
size_t count, loff_t *ppos)
{
/* unimportant code removed for blog post */
struct task_struct *task = get_proc_task(file->f_path.dentry->d_inode);
/* unimportant code removed for blog post */
mm = check_mem_permission(task);
copied = PTR_ERR(mm);
if (IS_ERR(mm))
goto out_free;
/* unimportant code removed for blog post */
if (file->private_data != (void *)((long)current->self_exec_id))
goto out_mm;
/* unimportant code removed for blog post
* (the function here goes onto write the buffer into the memory)
*/
Así pues, hay dos comprobaciones relevantes para impedir escrituras no autorizadas: check_mem_permission y self_exec_id. Primero la primera y después la segunda.
El código de check_mem_permission simplemente llama a __check_mem_permission, así que este es su código:
static struct mm_struct *__check_mem_permission(struct task_struct *task)
{
struct mm_struct *mm;
mm = get_task_mm(task);
if (!mm)
return ERR_PTR(-EINVAL);
/*
* A task can always look at itself, in case it chooses
* to use system calls instead of load instructions.
*/
if (task == current)
return mm;
/*
* If current is actively ptrace'ing, and would also be
* permitted to freshly attach with ptrace now, permit it.
*/
if (task_is_stopped_or_traced(task)) {
int match;
rcu_read_lock();
match = (ptrace_parent(task) == current);
rcu_read_unlock();
if (match && ptrace_may_access(task, PTRACE_MODE_ATTACH))
return mm;
}
/*
* No one else is allowed.
*/
mmput(mm);
return ERR_PTR(-EPERM);
}
Hay dos formas de autorizar la escritura en memoria. O bien task == current, es decir, el proceso al que se escribe es el mismo proceso que escribe, o bien current (el proceso que escribe) tiene permisos esotéricos a nivel de ptrace para manipular a task (el proceso al que se escribe). ¿Quizá crees que puedes engañar al código de ptrace? Es tentador. Pero no lo sé. Mejor averigüemos cómo podemos hacer que un proceso escriba memoria arbitraria en sí mismo, de modo que task == current.
Ahora, naturalmente, queremos escribir en la memoria de los procesos suid, porque así podemos obtener root. Miremos esto:
$ su "yeeeee haw I am a cowboy"
Unknown id: yeeeee haw I am a cowboy
su escupirá en stderr el texto que quieras, prefijado por "Unknown id:". Así que podemos abrir un fd a /proc/self/mem, hacer lseek al lugar correcto de la memoria para escribir (más sobre esto luego), usar dup2 para emparejar stderr con el fd de mem, y después exec a su $shellcode para escribir un lanzador de shell en la memoria del proceso, y entonces tenemos root. ¿De verdad? No tan fácil.
Aquí entra en juego la otra restricción. Después de pasar la prueba task == current, comprueba si el self_exec_id actual coincide con el self_exec_id con el que se abrió el fd. ¿Qué demonios es self_exec_id? Solo se referencia en unos pocos sitios del kernel. El más importante está dentro de exec:
void setup_new_exec(struct linux_binprm * bprm)
{
/* massive amounts of code trimmed for the purpose of this blog post */
/* An exec changes our domain. We are no longer part of the thread
group */
current->self_exec_id++;
flush_signal_handlers(current, 0);
flush_old_files(current->files);
}
EXPORT_SYMBOL(setup_new_exec);
self_exec_id se incrementa cada vez que un proceso hace exec. En este caso, funciona para que no puedas abrir el fd en un proceso no suid, hacer dup2 y luego exec a un proceso suid... que es exactamente lo que intentábamos hacer antes. Una forma bastante ingeniosa de frustrar nuestro ataque, ¿eh?
Así se sortea. Hacemos fork de un hijo y, dentro de ese hijo, hacemos exec a un proceso nuevo. El hijo inicial tras el fork tiene un self_exec_id igual al de su padre. Cuando hacemos exec a un proceso nuevo, self_exec_id se incrementa en uno. Mientras tanto, el propio padre está ocupado haciendo exec a nuestro proceso su que escribe el shellcode, de modo que su self_exec_id se incrementa al mismo valor. Así que lo que hacemos es lo siguiente: hacemos fork del hijo y exec a un proceso nuevo y, dentro de ese proceso nuevo, abrimos un fd a /proc/parent-pid/mem usando el pid del proceso padre, no el nuestro (como ocurría antes). Podemos abrir el fd de esta manera porque un simple open no tiene comprobación de permisos. Cuando se abre, su self_exec_id ya se ha incrementado al valor correcto que tendrá el self_exec_id del padre cuando hagamos exec a su. Finalmente, pasamos el fd abierto del proceso hijo de vuelta al proceso padre (usando algo de magia muy oscura de unix domain sockets), hacemos nuestro dup2 y hacemos exec a su con el shellcode.