Hemen hemen hepimiz bazı hesaplamalar için veri girmek amacıyla Google E-Tablolar veya Microsoft Excel kullanmışızdır. Diyelim ki çalışanlarınızın isimlerini, telefon numaralarını, unvanlarını ve aldıkları maaşları girmek istiyorsunuz.
En basit haliyle, E-Tablolar'da veya Excel'de bir kayıt veya vaka şu şekilde görünür:
Gördüğünüz gibi hem çalışan adı hem de unvan metinden, Telefon Numarası ve Maaş ise bir dizi rakamdan oluşuyor.
Yani anlamsal açıdan bakıldığında biz insanlar bu alanların gerçek dünyada ne anlama geldiğini anlıyoruz ve aralarında ayrım yapabiliyoruz.
Açıkçası, farkı anlamak için Bilgisayar Bilimleri diplomasına ihtiyacınız olmasa da, bir derleyici veya yorumlayıcı bu verileri nasıl işler?
Veri türlerinin devreye girdiği yer burasıdır ve bu, kodladıkları programlama diline bağlı olarak programcıların belirtmeye zaman ayırıp ayırmadıkları bir şeydir.
Yani çalışan adı ve unvanı altındaki veri noktalarına string adı verilmektedir. Elbette maaş, ondalık noktasının olmaması nedeniyle açıkça bir tam sayıdır. Basitçe söylemek gerekirse bunlar, kod yazarken bu şekilde bildirilmesi gereken veri türleridir, böylece yalnızca o veri türüyle ilişkili doğru işlemler gerçekleştirilebilir.
Solidity'de bir tamsayı veri türünü şu şekilde bildiririz:
Bununla birlikte, yukarıdaki e-tablodaki Telefon Numarası alanı benzersiz bir dize olarak kullanılacak bir veri noktası içermektedir, ancak bu tartışma başka bir güne kaldı. Şimdilik odak noktamız hepimizin temel aritmetik işlemlerini gerçekleştirdiğimiz ilkel veri türü üzerinde olacak.
Evet, temel aritmetik işlemler için önemli olmasına rağmen her türlü hesaplama için sınırlı bir aralığa sahip olan tamsayı veri türünden bahsediyoruz.
Tamsayı taşmasının gerçek dünyadaki en popüler örneği muhtemelen araçlarda yaşanıyor. Kilometre sayacı olarak da bilinen bu cihazlar genellikle bir aracın kaç kilometre kat ettiğini takip eder.
Peki, kat edilen mil değeri altı basamaklı bir kilometre sayacında işaretsiz 999999 tamsayı değerine ulaştığında ne olur?
İdeal olarak, bir mil daha eklendiğinde bu değerin 1000000'e ulaşması gerekir, değil mi? Ancak yedinci rakam için hüküm olduğu için bu gerçekleşmiyor.
Bunun yerine katedilen mil değeri aşağıda gösterildiği gibi 000000 olarak sıfırlanır:
Tanım gereği, yedinci basamak mevcut olmadığından, doğru değer temsil edilmediğinden bu durum 'taşma' ile sonuçlanır.
Resmi anladınız, değil mi?
Tam tersi çok sık olmasa da tam tersi durumlar da yaşanabilmektedir. Başka bir deyişle, kaydedilen değer aralıkta mevcut olan en düşük değerden daha düşük olduğunda ve buna "düşük akış" adı verilir.
Hepimizin bildiği gibi bilgisayarlar tamsayıları ikili eşdeğerleri olarak bellekte saklarlar. Şimdi basitlik adına 8 bitlik bir yazmaç kullandığınızı varsayalım.
= 2⁸*1 + 2⁷*1 + 2⁶*1 + 2⁵*1 + 2⁴*1 + 2³*1 + 2²*1 + 2¹*1 + 2⁰*1
= 256 + 128 + 64 + 32 + 16 + 8 + 4 + 2 + 1
= 111111111
Her bitin 1 olduğu ve sizin de görebileceğiniz gibi daha yüksek bir değeri saklayamazsınız.
Öte yandan, 0 sayısını 8 bitlik kayıtta saklamak istiyorsanız şu şekilde görünecektir:
= 2⁸*0 + 2⁷*0 + 2⁶*0 + 2⁵*0 + 2⁴*0 + 2³*0 + 2²*0 + 2¹*0 + 2⁰*0
= 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0
= 000000000
Her bitin 0 olduğu yer, bu size daha düşük bir değeri saklayamayacağınızı söylemelidir.
Başka bir deyişle, böyle bir 8 bitlik kayıt için izin verilen tamsayı aralığı 0-511'dir. Peki 512 veya -1 tamsayısını böyle bir kayıt defterinde saklamak mümkün müdür?
Tabii ki değil. Sonuç olarak, kilometre sayacı örneğinde katedilen kilometrenin sıfırlama değerine benzeyen bir değeri ikili değerler olarak depolayacaksınız.
Açıkçası, böyle bir sayıyı rahatça barındırabilmek için birkaç bit daha içeren kayıtlara ihtiyacınız olacaktır. Aksi takdirde taşma durumunu bir kez daha riske atabilirsiniz.
İmzalı tamsayılar durumunda, negatif tamsayıları da saklarız. Dolayısıyla, yukarıda gösterildiği gibi kabul edilen aralıktan daha küçük veya sıfırdan daha küçük bir sayıyı depolamaya çalıştığımızda, yetersiz akış meydana gelir.
Yine, herhangi bir hesaplama yapmanın amacı deterministik sonuçlar elde etmek olduğundan, bu en iyi ihtimalle can sıkıcı olabilir, en kötü ihtimalle ise milyonların kaybına neden olabilir. Özellikle akıllı sözleşmelerde bu tam sayı taşması veya yetersiz akış hataları meydana geldiğinde.
Tamsayı taşması ve az akışı onlarca yıldır ortalıkta olsa da, akıllı sözleşmelerdeki bir hata olarak bunların varlığı riskleri artırdı. Saldırganlar bu tür hatalardan yararlandıklarında akıllı sözleşmeyi büyük miktarlarda token tüketebilirler.
Muhtemelen bu tür bir hata ilk kez üç adres için milyarlarca Bitcoin oluşturan 74638 numaralı blokta meydana geldi. Bu hatayı soft fork ile çözmek saatler alacak ve blok atılacak ve işlem geçersiz hale gelecektir.
Birincisi, değeri 21 milyon Bitcoin'den büyük olan işlemler reddedildi. Yukarıda adı geçen üç hesaba çok fazla para gönderilen işlemlerde olduğu gibi, taşma işlemlerinde de durum farklı değildi.
Bununla birlikte, Ethereum akıllı sözleşmeleri de tamsayı taşması ve az akışı yaşadı; BeautyChain de buna önemli bir örnek.
Bu durumda akıllı sözleşme hatalı bir kod satırı içeriyordu:
Sonuç olarak, saldırganlar teorik olarak sınırsız miktarda BEC tokeni alabildiler ve bu da teorik olarak (2²⁵⁶)-1 değerine karşılık gelebilir.
Şimdi tam sayı taşması/eksikliğinin meydana geldiği başka bir akıllı sözleşme örneğine bakalım.
İlk bakışta, bu örnekte etkileşim halinde olan ve tamsayı taşması durumunda ne olacağını gösteren iki sözleşme vardır.
Aşağıda görebileceğiniz gibi, TimeLock sözleşmesi para yatırmanıza ve çekmenize olanak tanır, ancak bir farkla: ikincisini ancak belirli bir süre sonra gerçekleştirebilirsiniz. Bu durumda paranızı ancak bir hafta içinde çekebilirsiniz.
Ancak, Saldırı sözleşmesinde saldırı fonksiyonunu çağırdığınızda, uygulanan zaman kilidi artık etkili değildir ve bu nedenle saldırgan, bakiye tutarını anında geri çekebilir.
Yani type(uint).max+1-timeLock.locktime(address(this)) deyimiyle tamsayı taşmasına neden olacağından zaman kilidi ortadan kalkar.
Örneğin, yukarıdaki kodu kullanarak her iki akıllı sözleşmeyi de dağıttığınızda, aşağıda gösterildiği gibi TimeLock sözleşmesindeki para yatırma ve çekme işlevlerini çağırarak zaman kilidinin geçerli olup olmadığını test edebilirsiniz:
Gördüğünüz gibi 2 Ether miktarını seçerek yukarıda gösterilen 2 Ether akıllı sözleşme bakiyesini elde ediyoruz:
Spesifik olarak, 2 Ether bakiyesini tutan belirli adres, adresi bakiye fonksiyonu alanına ekleyerek ve bakiyeler düğmesine tıklayarak kontrol edilebilir:
Ancak yukarıda da belirtildiği gibi, zaman kilidi nedeniyle bu fonları henüz çekemezsiniz. Para çekme fonksiyonuna bastıktan sonra konsola baktığınızda kırmızı 'x' simgesiyle gösterilen bir hata göreceksiniz. Aşağıda görebileceğiniz gibi bu hatanın nedeni sözleşmede “Kilitleme süresi dolmadı” olarak belirtiliyor:
Şimdi aşağıda gösterildiği gibi konuşlandırılan Saldırı sözleşmesine bakalım:
Şimdi saldırı fonksiyonunu başlatmak için 1 Ether veya daha fazla değer yatırmanız gerekiyor. Bu örnekte aşağıda gösterildiği gibi 2 Ether seçtik:
Bundan sonra 'saldırı'ya basın. Yatırdığınız 2 Ether'in hemen geri çekileceğini ve aşağıdaki 2 Ether bakiyesinin de gösterdiği gibi Saldırı sözleşmesine ekleneceğini göreceksiniz:
Açıkçası, uzun süre kilidinin para yatırdığınız anda devreye girmesi gerektiği için bunun gerçekleşmesi beklenmiyor. Elbette bildiğimiz gibi type(uint).max+1-timeLock.locktime(address(this)) ifadesi, boostLockTime fonksiyonunu kullanarak kilit süresini azaltır. İşte tam da bu yüzden Ether bakiyesini anında çekebiliyoruz.
Bu da bizi şu soruya getiriyor: Tamsayı taşması ve yetersiz akış güvenlik açığını düzeltmenin yolları var mı?
Tamsayı taşması/eksikliği güvenlik açığının yıkıcı olabileceği kabul edilerek, bu hataya yönelik birkaç düzeltme kullanıma sunuldu. Hem bu düzeltmelere hem de bu tür bir hatayı nasıl çözdüklerine bakalım:
Open Zeppelin bir kuruluş olarak siber güvenlik teknolojisi ve hizmetleri söz konusu olduğunda çok şey sunuyor; SafeMath kütüphanesi akıllı sözleşme geliştirme deposunun bir parçası. Bu depo, akıllı sözleşme kodunuza aktarılabilecek sözleşmeleri içerir; SafeMath kütüphanesi de bunlardan biridir.
SafeMath.sol içindeki işlevlerden birinin tam sayı taşmasını nasıl kontrol ettiğini görelim:
Şimdi, a+b hesaplaması yapıldıktan sonra c<a'nın gerçekleşip gerçekleşmediğini kontrol etmek gerekir. Elbette bu yalnızca tamsayı taşması durumunda geçerli olacaktır.
Solidity'nin derleyici sürümünün 0.8.0 ve üstüne ulaşmasıyla, tamsayı taşması ve yetersiz akış kontrolleri artık yerleşiktir. Dolayısıyla, hem dili hem de bu kitaplığı kullanırken bu güvenlik açığını kontrol etmek için bu kitaplığı kullanmaya devam edebilirsiniz. Elbette akıllı sözleşmeniz 0.8.+'dan daha düşük bir derleyici sürümü gerektiriyorsa taşma veya yetersiz akışı önlemek için bu kütüphaneyi kullanmanız gerekir.
Şimdi, daha önce de belirttiğimiz gibi, akıllı sözleşmeniz için 0.8.0 ve üzeri bir derleyici sürümü kullanıyorsanız, bu sürümde bu tür bir güvenlik açığına karşı yerleşik bir denetleyici bulunmaktadır.
Hatta yukarıdaki akıllı sözleşmeyle çalışıp çalışmadığını doğrulamak için derleyici sürümünü “^0.8.0” olarak değiştirip yeniden dağıtırken aşağıdaki 'geri alma' hatası alınıyor:
Tabii ki, zaman kilidi değerinin taşmasının kontrolü nedeniyle 2 Ether yatırma işlemi gerçekleştirilmiyor. Sonuç olarak, ilk etapta para yatırılmadığından para çekme işlemi mümkün değildir.
Hiç şüphe yok ki, Attack.attack() işlev çağrısı burada işe yaramadı, yani her şey yolunda!
Bu uzun blog yazısından anlamanız gereken bir şey varsa o da BEC saldırısında olduğu gibi bu güvenlik açığını göz ardı etmenin maliyetli olabileceğidir. Sizin de görebileceğiniz gibi, eğer işaretlenmezse, kötü amaçlı olmayan hataların meydana gelmesi kolaydır. Veya bilgisayar korsanlarının bu güvenlik açığından yararlanması da aynı derecede basittir.
Bundan bahsetmişken ve BEC saldırısının nasıl gerçekleştiğine dair anlayışımızı kullanarak, bu güvenlik açığını tanımak, sunulan düzeltmeler sayesinde akıllı sözleşmelerinizi yazarken herhangi bir saldırıyı önlemede uzun bir yol kat edebilir. Sizi tuzağa düşürmek için bekleyen başka akıllı sözleşme güvenlik açıkları olsa bile.