
Analyse des vulnérabilités du compilateur Solidity : défaut de recodage ABI
TechFlow SélectionTechFlow Sélection

Analyse des vulnérabilités du compilateur Solidity : défaut de recodage ABI
Ce processus en lui-même ne présente pas de problème logique majeur, mais lorsqu'il est combiné au mécanisme de nettoyage de Solidity, une négligence dans le code du compilateur Solidity a conduit à l'existence d'une vulnérabilité.
Aperçu
Ce document analyse en détail, au niveau du code source, une vulnérabilité présente dans le compilateur Solidity (version 0.5.8 <= version < 0.8.16), découlant d’un traitement incorrect des tableaux de type uint ou bytes32 à longueur fixe lors du processus ABIReencoding. Des solutions et mesures d’atténuation sont également proposées.
Détails de la vulnérabilité
Le format d'encodage ABI est la méthode standard utilisée pour coder les paramètres lorsqu'un utilisateur ou un contrat appelle une fonction d'un contrat. Pour plus de détails, veuillez consulter la spécification officielle de Solidity concernant l'encodage ABI.
Lors du développement de contrats, il est fréquent d'extraire des données depuis les données calldata provenant d’un utilisateur ou d’un autre contrat, puis de les transférer ou de les émettre via une opération emit. Étant donné que toutes les opérations de la machine virtuelle EVM s'appuient sur la mémoire, la pile et le stockage, tout encodage ABI dans Solidity implique de copier les données calldata selon le format ABI dans la mémoire, suivant un nouvel ordre.
Ce processus ne comporte pas en soi de problème logique majeur. Cependant, combiné au mécanisme de nettoyage (cleanup) de Solidity, une omission dans le code du compilateur Solidity a conduit à l’apparition d’une vulnérabilité.
Selon les règles d'encodage ABI, après suppression du sélecteur de fonction, les données encodées se divisent en deux parties : head (tête) et tail (queue). Lorsque les données sont des tableaux à longueur fixe de type uint ou bytes32, elles sont entièrement stockées dans la partie head. Le mécanisme de nettoyage de Solidity consiste à effacer la mémoire à l’index suivant dès qu’un emplacement mémoire courant est utilisé, afin d’éviter que des données résiduelles n’affectent les futures utilisations. En outre, lorsque Solidity effectue un encodage ABI d’un ensemble de paramètres, il procède **de gauche à droite**.
Pour faciliter l’analyse du principe de cette vulnérabilité, considérons le contrat suivant :
contract Eocene {
event VerifyABI(bytes[], uint[2]);
function verifyABI(bytes[] calldata a, uint[2] calldata b) public {
emit VerifyABI(a, b); // Les données de l'événement seront encodées selon le format ABI avant d'être stockées sur la blockchain
}
}
La fonction verifyABI du contrat Eocene a simplement pour but d’émettre les deux paramètres : le tableau dynamique a de type bytes[] et le tableau statique b de type uint[2].
Notez que les événements (events) déclenchent également un encodage ABI. Ainsi, les paramètres a et b seront encodés selon le format ABI avant d’être enregistrés sur la chaîne.
Nous compilons ce contrat avec la version v0.8.14 de Solidity, le déployons via Remix, puis appelons la fonction avec verifyABI(['0xaaaaaa','0xbbbbbb'], [0x11111, 0x22222]).
Examinons d’abord le format correct d’encodage de verifyABI(['0xaaaaaa','0xbbbbbb'], [0x11111, 0x22222]) :
0x52cd1a9c // bytes4(sha3("verify(bytes[], uint[2])"))
0000000000000000000000000000000000000000000000000000000000000060 // index de a
0000000000000000000000000000000000000000000000000000000000011111 // b[0]
0000000000000000000000000000000000000000000000000000000000022222 // b[1]
0000000000000000000000000000000000000000000000000000000000000002 // longueur de a
0000000000000000000000000000000000000000000000000000000000000040 // index de a[0]
0000000000000000000000000000000000000000000000000000000000000080 // index de a[1]
0000000000000000000000000000000000000000000000000000000000000003 // longueur de a[0]
aaaaaa0000000000000000000000000000000000000000000000000000000000 // a[0]
0000000000000000000000000000000000000000000000000000000000000003 // longueur de a[1]
bbbbbb0000000000000000000000000000000000000000000000000000000000 // a[1]
Si le compilateur Solidity fonctionnait correctement, les données enregistrées par l’événement devraient correspondre exactement aux entrées envoyées. Essayons maintenant d’exécuter l’appel réellement et examinons les logs sur la chaîne. Pour comparaison, vous pouvez consulter cette transaction.
Après exécution réussie, les données enregistrées par l’événement sont les suivantes :
!! Stupéfiant, la valeur juste après b[1], qui représente la longueur du tableau a, a été incorrectement supprimée !!
0000000000000000000000000000000000000000000000000000000000000060 // index de a
0000000000000000000000000000000000000000000000000000000000011111 // b[0]
0000000000000000000000000000000000000000000000000000000000022222 // b[1]
0000000000000000000000000000000000000000000000000000000000000000 // longueur de a ?? pourquoi est-elle devenue 0 ??
0000000000000000000000000000000000000000000000000000000000000040 // index de a[0]
0000000000000000000000000000000000000000000000000000000000000080 // index de a[1]
0000000000000000000000000000000000000000000000000000000000000003 // longueur de a[0]
aaaaaa0000000000000000000000000000000000000000000000000000000000 // a[0]
0000000000000000000000000000000000000000000000000000000000000003 // longueur de a[1]
bbbbbb0000000000000000000000000000000000000000000000000000000000 // a[1]
Pourquoi cela se produit-il ?
Comme mentionné précédemment, lorsque Solidity traite une série de paramètres nécessitant un encodage ABI, l’ordre de génération est de gauche à droite. Le processus d’encodage pour a et b suit donc les étapes suivantes :
- Solidity encode d’abord
aselon le format ABI : l’index deaest placé dans la partie head, tandis que la longueur et les valeurs des éléments sont placées dans la partie tail. - Ensuite, les données
bsont traitées. Puisquebest de typeuint[2], ses valeurs sont stockées dans la partie head. Toutefois, en raison du mécanisme de nettoyage de Solidity, après avoir écritb[1]en mémoire, la case mémoire suivante (utilisée pour stocker la longueur dea) est remise à zéro. - L’encodage ABI se termine, mais les données incorrectes sont désormais stockées sur la blockchain : la vulnérabilité SOL-2022-6 est ainsi exposée.
Au niveau du code source, la logique erronée est claire : chaque fois que des données de type tableau à longueur fixe uint ou bytes32 sont copiées depuis calldata vers la mémoire, Solidity remet systématiquement à zéro la case mémoire suivante. Combiné au fait que l’encodage ABI possède deux parties (head et tail) et que l’encodage se fait de gauche à droite, cette erreur conduit directement à la vulnérabilité.
Le code source fautif dans le compilateur Solidity est le suivant :
Lorsque les données sources proviennent de calldata, et que leur type est ByteArray, String, ou qu’il s’agit d’un tableau dont le type de base est uint ou bytes32, on entre dans la fonction ABIFunctions::abiEncodingFunctionCalldataArrayWithoutCleanup().

À l’intérieur, on vérifie d’abord via fromArrayType.isDynamicallySized() si le tableau source est de longueur fixe. Seuls les tableaux statiques peuvent déclencher la vulnérabilité.
Le résultat de isByteArrayOrString() est transmis à YulUtilFunctions::copyToMemoryFunction(), afin de décider si un nettoyage doit être effectué après l’opération calldatacopy sur l’index mémoire suivant.
Ces conditions combinées signifient que la vulnérabilité ne peut être déclenchée que lors de la copie, depuis calldata, d’un tableau à longueur fixe de type uint ou bytes32 vers la mémoire. C’est là l’origine des contraintes de déclenchement.

Puisque l’encodage ABI se fait toujours de gauche à droite, pour exploiter la vulnérabilité, il faut impérativement qu’un type de données dynamique soit présent avant le tableau à longueur fixe, et que ce dernier se trouve en dernière position parmi les paramètres à encoder.
La raison est simple : si le tableau fixe n’est pas en fin de liste, la mise à zéro de la case suivante n’a aucun effet car elle sera écrasée par le paramètre suivant. Si aucun type dynamique ne précède le tableau fixe, alors la zone mémoire affectée n’est pas utilisée par l’encodage ABI, donc la mise à zéro est sans conséquence.
Par ailleurs, il convient de noter que toutes les opérations ABI implicites ou explicites, ainsi que tous les Tuple (groupes de données) respectant ce format, sont affectés par cette vulnérabilité.
Les opérations concernées incluent notamment :
- event
- error
- abi.encode*
- returns // retour de fonction
- struct // structures définies par l’utilisateur
- all external call
Solutions
- Lorsque le code contient des opérations affectées, veiller à ce que le dernier paramètre ne soit **pas** un tableau à longueur fixe de type
uintoubytes32. - Utiliser une version du compilateur Solidity non affectée par cette vulnérabilité (par exemple >= 0.8.16).
- Faire appel à des experts en sécurité pour auditer professionnellement le contrat.
Bienvenue dans la communauté officielle TechFlow
Groupe Telegram :https://t.me/TechFlowDaily
Compte Twitter officiel :https://x.com/TechFlowPost
Compte Twitter anglais :https://x.com/BlockFlow_News














