Öncelikle reentrancy saldırısının ne olduğunu ve bunu nasıl önleyebileceğinizi basit bir şekilde anlamanıza yardımcı olacağım, ardından güvenlik açıklarının nerede olduğunu, saldırgan kodunun ne olacağını göstermek için kod örneklerine daha derinlemesine dalacağım. ve en önemlisi projenizdeki akıllı sözleşmelerin yalnızca birini değil tümünü korumak için size en son doğrulanmış yöntemleri göstereceğim.

Spoiler: NonReentrant() değiştiricisini zaten duyduysanız okumaya devam edin çünkü globalNonReentrant() değiştiricisinin ve checks-fects-interactions modelinin birkaç satır altında keşfetmek üzeresiniz.



Yukarıdaki resimde SözleşmeA ve SözleşmeB var. Artık bildiğiniz gibi bir akıllı sözleşme başka bir akıllı sözleşmeyle etkileşime girebilir, bu durumda olduğu gibi SözleşmeA, SözleşmeB'yi çağırabilir. Dolayısıyla, yeniden girişin en temel fikri, Sözleşme B'nin Sözleşme A yürütülürken Sözleşme A'yı geri arayabilmesidir.

Peki saldırgan bunu nasıl kullanabilir?

Yukarıda 10 Ether içeren Sözleşme A'mız var ve Sözleşme B'nin Sözleşme A'da 1 Ether depoladığını görüyoruz. Bu durumda SözleşmeB, SözleşmeA'dan para çekme işlevini kullanabilecek ve bakiyesinin 0'dan büyük olduğu kontrolü geçerken Ether'i kendisine geri gönderebilecek ve daha sonra toplam bakiyesini 0 olarak değiştirebilecektir.



Şimdi ContractB'nin geri çekme fonksiyonundan yararlanmak ve ContractA'dan tüm Ether'leri çalmak için yeniden girişi nasıl kullanabileceğini görelim. Temel olarak saldırganın iki işleve ihtiyacı olacak: attack() ve fallback().

Solidity'de bir geri dönüş işlevi, adı, parametreleri veya dönüş değerleri olmayan harici bir işlevdir. Herkes bir geri dönüş işlevini şu şekilde çağırabilir: Sözleşmede bulunmayan bir işlevi çağırmak; Gerekli verileri iletmeden bir fonksiyonun çağrılması; Ether'in sözleşmeye herhangi bir veri olmadan gönderilmesi.

Yeniden girişin çalışma şekli (okları adım adım takip edelim), saldırganın, içeride ContractA'dan çekilme() işlevini çağıran saldırı() işlevini çağırmasıdır. Fonksiyonun içinde, SözleşmeB'nin bakiyesinin 0'dan büyük olup olmadığını doğrulayacak ve eğer öyleyse uygulamaya devam edecektir.



ContractB'nin bakiyesi 0'dan büyük olduğu için o 1 Ether'i geri gönderir ve geri dönüş fonksiyonunu tetikler. Şu anda Sözleşme A'nın 9 Ether'e ve Sözleşme B'nin zaten 1 Ether'e sahip olduğuna dikkat edin.



Daha sonra, geri dönüş işlevi yürütüldüğünde, Sözleşme A'nın para çekme işlevini yeniden tetikler ve Sözleşme B'nin bakiyesinin 0'dan büyük olup olmadığını tekrar kontrol eder. Yukarıdaki görüntüyü tekrar kontrol ederseniz, bakiyesinin hala 1 Ether olduğunu fark edeceksiniz.



Bu, kontrolün başarılı olduğu ve ContractB'ye geri dönüş işlevini tetikleyen başka bir Ether gönderdiği anlamına gelir. “Bakiye=0”a sahip olduğumuz satır hiçbir zaman yürütülmediği için, bunun Sözleşme A'daki tüm Ether bitene kadar devam edeceğine dikkat edin.

___________

Şimdi yeniden girişi Solidity koduyla tanımlayabileceğimiz akıllı bir sözleşmeye bakalım.



EtherStore sözleşmesinde, gönderenin bakiyelerini saklayan ve güncelleyen deposit() fonksiyonuna ve ardından depolanan tüm bakiyeleri aynı anda alacak olan pullAll() fonksiyonuna sahibiz. Lütfen, bakiyenin 0'dan büyük olup olmadığını kontrol eden ve hemen ardından tekrar Ether'i gönderip gönderenin bakiyesinin güncellenmesini 0'a bırakan, ilk önce pullAll() uygulamasının uygulandığına dikkat edin.



Burada, EtherStore sözleşmesini boşaltmak için yeniden girişi kullanacak olan Saldırı sözleşmesine sahibiz. Kodunu analiz edelim:

  • Saldırgan, oluşturucusunda bir örnek oluşturmak ve böylece işlevlerini kullanabilmek için EtherStore adresini iletecektir.

  • Burada EtherStore, Ether'i bu sözleşmeye gönderdiğinde çağrılacak olan fallback() işlevini görüyoruz. Bakiye 1'e eşit veya 1'den büyük olduğu sürece içeride EtherStore'dan para çekme çağrısı olacaktır.

  • Attack() fonksiyonunun içinde EtherStore'dan yararlanacak mantığa sahibiz. Görüldüğü gibi öncelikle yeterli eterimiz olduğundan emin olarak atağı başlatacağız, ardından EtherStore'da bakiyenin 0'dan büyük olması için 1 ether yatıracağız ve dolayısıyla çekilmeye başlamadan önce kontrolleri geçeceğiz.

Yukarıda SözleşmeA ve SözleşmeB örneğinde kodun nasıl çalışacağını adım adım anlattım, o halde şimdi nasıl olacağının bir özetini yapalım. Her şeyden önce saldırgan, EtherStore'dan Attack()'ı çağıracak, bu da içeriden DrawAll()'ı çağıracak ve daha sonra Ether'i Attack kontratının geri dönüş fonksiyonuna gönderecektir. Ve orada yeniden girişi başlatacak ve EtherStore'un bakiyesini boşaltacak.

Peki sözleşmelerimizi yeniden giriş saldırılarından nasıl koruyabiliriz?

Bunları tam olarak korumak için size üç önleme tekniği göstereceğim. Tek bir işlevde yeniden girişin, yeniden giriş çapraz işlevin ve yeniden giriş çapraz sözleşmesinin nasıl önleneceğini ele alacağım.



Tek bir işlevi korumaya yönelik ilk teknik, noReentrant adı verilen bir değiştiricinin kullanılmasıdır.

Değiştirici, diğer işlevlerin davranışını değiştirmek için kullandığınız özel bir işlev türüdür. Değiştiriciler, bir fonksiyonun tamamını yeniden yazmaya gerek kalmadan, bir fonksiyona ekstra koşullar veya işlevsellik eklemenizi sağlar.

Burada yaptığımız şey, işlev yürütülürken sözleşmeyi kilitlemektir. Bu şekilde, gereksinimde yapılan kontrolü tekrar geçmek için işlevin kodunu gözden geçirmesi ve ardından kilitli durum değişkenini false olarak değiştirmesi gerekeceğinden tek işleve yeniden giremeyecektir.

___________



İkinci teknik, sözleşmelerimizi işlevler arası yeniden girişten koruyacak Kontroller-Etkiler-Etkileşimler modelini kullanmaktır. Yukarıda güncellenen EtherStore sözleşmesinde nelerin değiştiğini görebiliyor musunuz?

Check-Effects-Etkileşim modelini derinlemesine incelemek için https://fravoll.github.io/solidity-patterns/checks_fects_interactions.html adresini okumanızı öneririm. 





Yukarıda, yukarıda görüldüğü gibi potansiyel olarak hiçbir zaman ulaşılamayan Ether gönderildikten sonra bakiyenin güncellendiği soldaki resimdeki savunmasız kod ile sağda yapılan şey bakiyeleri taşımaktır[ msg.sender] = 0 (veya efekt) require(bal > 0) (kontrol) işleminden hemen sonra, ancak eter (etkileşim) göndermeden önce.

Bu şekilde, başka bir işlev pullAll()'a erişiyor olsa bile, bakiye Ether gönderilmeden önce her zaman güncelleneceği için bu sözleşmenin saldırgandan korunacağından emin olacağız.

https://twitter.com/GMX_IO tarafından oluşturulan desen

Size göstereceğim üçüncü teknik, sözleşmeler arası yeniden girişten korunmak için GlobalReentrancyGuard sözleşmesini oluşturmaktır. Bunun birbiriyle etkileşim halinde olan birden fazla sözleşmeye sahip projeler için geçerli olduğunu anlamak önemlidir.

Buradaki fikir, ilk teknikte anlattığım noReentrant değiştiricidekiyle aynı, değiştiriciye giriyor, sözleşmeyi kilitlemek için bir değişkeni güncelliyor ve kodu bitirene kadar kilidini açmıyor. Buradaki en büyük fark, fonksiyonun girilip girilmediğini kontrol etmek için kullanılan, ayrı bir sözleşmede saklanan bir değişkeni kullanıyor olmamızdır.







Burada, fikri anlamak için referans olarak gerçek kod olmadan ve yalnızca işlev adlarıyla bir örnek oluşturdum; deneyimlerime göre, durumu sadece kelimelerle yazmaktan daha fazlasını görselleştirmeye yardımcı olabilir.

Burada saldırgan, ScheduledTransfer sözleşmesindeki işlevi çağırıyor ve koşulları karşıladıktan sonra belirtilen Ether'i AttackTransfer sözleşmesine gönderiyor, böylece geri dönüş işlevine giriyor ve dolayısıyla ScheduledTransfer sözleşmesinin başlangıç ​​noktasından işlemi "iptal ediyor". Ether'i görüntüleyin ve yine de alın. Ve bu şekilde, ScheduledTransfer'deki tüm Ether'ler boşaltılana kadar bir bakış başlatılmış olacaktır.

Yukarıda bahsettiğim GlobalReentrancyGuard'ı kullanmak bu tür bir saldırı senaryosunun önüne geçecektir.

__________________

Akıllı Sözleşmeler, Web3 Güvenliği, Sağlamlık, Akıllı sözleşmelerin denetlenmesi ve daha fazlası hakkında daha fazla günlük güncelleme bulmak için Twitter @TheBlockChainer.

__________________