
Élévation de privilèges Linux
Présentation de Mempodipper, un exploit pour CVE-2012-0056. /proc/pid/mem est une interface pour lire et écrire, directement, la mémoire des processus en se déplaçant avec les mêmes adresses que l'espace mémoire virtuel du processus. Dans la version 2.6.39, les protections contre l'accès non autorisé à /proc/pid/mem ont été jugées suffisantes, et ainsi le #ifdef précédent qui empêchait l'écriture dans la mémoire arbitraire du processus a été supprimé. Toute personne disposant des bonnes autorisations pouvait écrire dans la mémoire du processus. Il s'est avéré, bien sûr, que la vérification des autorisations était mal faite. Cela signifie que tous les noyaux Linux >=2.6.39 sont vulnérables, jusqu'au commit de correction il y a quelques jours. Prenons l'ancien code du noyau étape par étape et découvrons ce qui cloche.
Lorsque /proc/pid/mem est ouvert, ce code du noyau est appelé :
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;
}
Il n'y a aucune restriction à l'ouverture ; n'importe qui peut ouvrir le fd /proc/pid/mem pour n'importe quel processus (sous réserve des restrictions VFS ordinaires). Il enregistre simplement le self_exec_id du processus d'origine avec lequel il a été ouvert et le conserve pour une vérification ultérieure lors des lectures et écritures.
Les écritures (et lectures), cependant, ont des restrictions de vérification d'autorisations. Examinons la fonction d'écriture :
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)
*/
Il y a donc deux vérifications pertinentes en place pour empêcher les écritures non autorisées : check_mem_permission et self_exec_id. Commençons par la première, puis la seconde.
Le code de check_mem_permission appelle simplement __check_mem_permission, voici donc le code de celle-ci :
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);
}
Il y a deux manières dont l'écriture mémoire est autorisée. Soit task == current, ce qui signifie que le processus en cours d'écriture est celui qui écrit, soit current (le processus qui écrit) dispose d'autorisations esotériques de niveau ptrace pour jouer avec task (le processus en cours d'écriture). Peut-être pensez-vous pouvoir tromper le code ptrace ? C'est tentant. Mais je ne sais pas. Essayons plutôt de comprendre comment faire en sorte qu'un processus écrive de la mémoire arbitraire sur lui-même, de sorte que task == current.
Naturellement, nous voulons écrire dans la mémoire des processus suid, car nous pouvons alors obtenir root. Regardez ceci :
$ su "yeeeee haw I am a cowboy"
Unknown id: yeeeee haw I am a cowboy
su affichera n'importe quel texte que vous voulez sur stderr, précédé de "Unknown id:". Ainsi, nous pouvons ouvrir un fd vers /proc/self/mem, lseek au bon endroit en mémoire pour l'écriture (plus d'informations plus tard), utiliser dup2 pour coupler stderr et le fd mem, puis exec vers su $shellcode pour écrire un générateur de shell dans la mémoire du processus, et nous avons alors root. Vraiment ? Pas si facile.
C'est là qu'intervient l'autre restriction. Après avoir passé le test task == current, il vérifie ensuite si le self_exec_id actuel correspond au self_exec_id avec lequel le fd a été ouvert. Qu'est-ce que self_exec_id ? Il n'est référencé qu'à quelques endroits dans le noyau. Le plus important se trouve justement à l'intérieur 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 est incrémenté à chaque fois qu'un processus exécute un exec. Dans ce cas, il fonctionne de manière à ce que vous ne puissiez pas ouvrir le fd dans un processus non-suid, dup2, puis exec vers un processus suid... c'est exactement ce que nous essayions de faire ci-dessus. Une façon assez astucieuse de contrer notre attaque, n'est-ce pas ?