
Une preuve de concept pour CVE-2025-40019
CVE-2025-40019 concerne le mécanisme de vecteur d'initialisation encrypté salt-sector (crypto/essiv.c) qui présente une validation insuffisante de la longueur des données d'authentification associées (AAD) par rapport à la taille du vecteur d'initialisation (IV), entraînant un accès mémoire hors limites. Le code fourni dans ce dépôt est simplement un déclencheur de bogue, mais ce bogue est facilement exploitable.
Je suis tombé sur ce bogue dans le tableur kernelctf ici. Comme il n'y avait ni billet de blog ni PoC associé à ce bogue, j'ai décidé d'explorer le sous-système crypto du noyau Linux pour comprendre ce qui causait le problème.

Pour comprendre la nécessité d'ESSIV, nous devons examiner comment les données sont stockées sur le disque. Le chiffrement de disque (comme LUKS ou dm-crypt) fonctionne généralement sur des secteurs. Chaque secteur doit être chiffré indépendamment afin que la lecture d'un secteur n'exige pas la lecture de tout le disque.
En mode CBC (Cipher Block Chaining) standard, un vecteur d'initialisation (IV) est requis pour chaque opération de chiffrement. Les premières implémentations de chiffrement de disque utilisaient le numéro de secteur comme IV.
Cependant, parce que les numéros de secteur sont prévisibles, un attaquant peut effectuer une "attaque par tatouage". En écrivant des données spécialement conçues sur un secteur connu, l'attaquant peut observer des motifs dans le texte chiffré qui révèlent la présence de fichiers spécifiques, contournant ainsi efficacement la confidentialité du chiffrement.
ESSIV (Encrypted Salt-Sector Initialization Vector) a été conçu pour rendre l'IV imprévisible. Il fonctionne en :
Cela garantit que même si un attaquant connaît le numéro de secteur, il ne peut pas prédire l'IV sans connaître la clé secrète.
Chaque algorithme, qu'il s'agisse d'un chiffrement de base comme AES ou d'un wrapper comme ESSIV, implémente une structure que le noyau utilise pour acheminer les appels :
struct skcipher_alg {
int (*setkey)(struct crypto_skcipher *tfm, const u8 *key, unsigned int keylen);
int (*encrypt)(struct skcipher_request *req);
int (*decrypt)(struct skcipher_request *req);
// ...
struct skcipher_alg_common co; // Contient ivsize, chunksize, etc.
};
La vulnérabilité est un cas classique où l'on suppose que les métadonnées fournies par l'utilisateur (la longueur AAD) satisferont toujours les exigences internes de la transformation cryptographique. Dans le chemin "sur place" ou de déchiffrement (!enc), le code calcule un décalage : req->assoclen - crypto_aead_ivsize(tfm). Mais il ne vérifie jamais si req->assoclen < ivsize. ce qui signifie que ce décalage peut être négatif.
static int essiv_aead_crypt(struct aead_request *req, bool enc)
{
struct crypto_aead *tfm = crypto_aead_reqtfm(req);
const struct essiv_tfm_ctx *tctx = crypto_aead_ctx(tfm);
struct essiv_aead_request_ctx *rctx = aead_request_ctx(req);
struct aead_request *subreq = &rctx->aead_req;
struct scatterlist *src = req->src;
int err;
crypto_cipher_encrypt_one(tctx->essiv_cipher, req->iv, req->iv);
/*
* dm-crypt intègre le numéro de secteur et l'IV dans la région AAD, donc
* nous devons copier l'IV converti dans le bon scatterlist avant de le transmettre.
*/
rctx->assoc = NULL;
if (req->src == req->dst || !enc) {
scatterwalk_map_and_copy(req->iv, req->dst,
req->assoclen - crypto_aead_ivsize(tfm), // <------- bogue !
crypto_aead_ivsize(tfm), 1);
} else {
u8 *iv = (u8 *)aead_request_ctx(req) + tctx->ivoffset;
int ivsize = crypto_aead_ivsize(tfm);
int ssize = req->assoclen - ivsize;
struct scatterlist *sg;
int nents;
.
.
.
En regardant la fonction scatterwalk_map_and_copy, on voit qu'elle copie simplement en mémoire vers le scatterlist sg :
static inline void scatterwalk_map_and_copy(void *buf, struct scatterlist *sg,
unsigned int start,
unsigned int nbytes, int out)
{
if (out)
memcpy_to_sglist(sg, start, buf, nbytes);
else
memcpy_from_sglist(buf, sg, start, nbytes);
}
Le correctif pour ce bogue était très simple : il vérifie simplement si assoclen est inférieur à ivsize.
diff --git a/crypto/essiv.c b/crypto/essiv.c
index d003b78fcd855a..a47a3eab693519 100644
--- a/crypto/essiv.c
+++ b/crypto/essiv.c
@@ -186,9 +186,14 @@ static int essiv_aead_crypt(struct aead_request *req, bool enc)
const struct essiv_tfm_ctx *tctx = crypto_aead_ctx(tfm);
struct essiv_aead_request_ctx *rctx = aead_request_ctx(req);
struct aead_request *subreq = &rctx->aead_req;
+ int ivsize = crypto_aead_ivsize(tfm);
+ int ssize = req->assoclen - ivsize;
struct scatterlist *src = req->src;
int err;
+ if (ssize < 0)
+ return -EINVAL;
+
crypto_cipher_encrypt_one(tctx->essiv_cipher, req->iv, req->iv);
/*
@@ -198,19 +203,12 @@ static int essiv_aead_crypt(struct aead_request *req, bool enc)
*/
rctx->assoc = NULL;
if (req->src == req->dst || !enc) {
- scatterwalk_map_and_copy(req->iv, req->dst,
- req->assoclen - crypto_aead_ivsize(tfm),
- crypto_aead_ivsize(tfm), 1);
+ scatterwalk_map_and_copy(req->iv, req->dst, ssize, ivsize, 1);
} else {
u8 *iv = (u8 *)aead_request_ctx(req) + tctx->ivoffset;
- int ivsize = crypto_aead_ivsize(tfm);
- int ssize = req->assoclen - ivsize;
struct scatterlist *sg;
int nents;
- if (ssize < 0)
- return -EINVAL;
-
nents = sg_nents_for_len(req->src, ssize);
if (nents < 0)
return -EINVAL;