1. Anasayfa
  2. Akıllı Sözleşme

Solidity Akıllı Sözleşmelerinde Güvenlik Açıklarını Bulma

Solidity Akıllı Sözleşmelerinde Güvenlik Açıklarını Bulma
Solidity Akıllı Sözleşmelerinde Güvenlik Açıklarını Bulma
1

Solidity Akıllı Sözleşmelerinde Güvenlik Açıklarını Bulma

Akıllı sözleşmeler, herkese açık kod ve depolamaya sahip merkezi olmayan programlardır. Her işlem üçüncü bir tarafça gözden geçirilebilir ve verilen girdilerin sonucu tahmin edilebilir. Merkezi olmayan uygulamalarda, güvenlik açıklarının ana kaynağı kod mantığıdır.

Programlama uygulamalarının güvenli bir kod için kolay bir kuralı vardır – onu temiz ve basit tutun. Her talimatın sonucunu belirlemek kolaysa, davranışın ne zaman yanlış olduğunu bulmak kolaydır.

Mantık doğrulaması için birim testleri gereklidir. Kod, tüm girdiler, koşullar ve en önemlisi olumsuz senaryolar için kontroller de dahil olmak üzere tamamen testlerle kapsanırsa birçok sorun bulunabilir.

Ortak güvenlik açıkları

Kodu satır satır okuyarak bulunabilecek birçok basit güvenlik açığı vardır.

Bir “Dummy Token” sözleşme örneği yazarak bazı güvenlik açıklarını keşfedelim.

pragma solidity >=0.8.0;
 
contract DummyToken {
    uint256 public price;
    mapping(address => uint256) public balances;
    constructor(uint256 _price) {
        price = _price;
    }
    function buy() external payable {
        require(msg.value == price, "INVALID_VALUE");
        balances[msg.sender] += 1;
    }
}

Henüz çok fazla işlevsellik yok. Sözleşme dağıtıldığında fiyat belirlenir ve yeterli ETH sağlanırsa herhangi bir kullanıcı bir token satın alabilir.

Fiyat, büyük ölçüde dağıtım sırasında kullanılan değere bağlıdır. Fiyatı değiştiremedik, bu yüzden yeni bir fonksiyon ekleyerek bunu düzeltelim:

function setPrice(uint256 _price) external {
        price = _price;
    }

Artık fiyatı istediğimiz zaman güncelleyebiliriz, ancak bu işlev birkaç soruna neden oldu:

  • Eksik gönderen doğrulaması — herkes fiyatı değiştirebilir
  • Eksik girdi doğrulaması – fiyat 0 olarak ayarlanırsa jetonlar ücretsiz olarak satın alınabilir

Göndereni doğrulamak için yönetici işlemlerini gerçekleştirmesine izin verilen adresi saklamamız gerekiyor.

address owner;
    constructor(uint256 _price) {
        require(_price > 0, "INVALID_PRICE");
        price = _price;
        owner = msg.sender;
    }
    modifier onlyOwner() {
        require(msg.sender == owner, "NOT_OWNER");
        _;
    }
    function setPrice(uint256 _price) external onlyOwner {
        require(_price > 0, "INVALID_PRICE");
        price = _price;
    }

Artık mal sahibi, sözleşmeyi dağıtan adrese ayarlanacak ve yalnızca bu adres fiyatı değiştirebilecekti. Ayrıca, hem yapıcı hem de ayarlayıcı işlevinde fiyatın 0’dan büyük olup olmadığını kontrol ettik. Yapıcıda da önemli değerlerin akıl sağlığı kontrolünün yapılması önerilir.

Bu sözleşmeye ekleyebileceğimiz bir sonraki mantık ücretlerdir – kullanıcı her jeton satın aldığında, gelirin %5’i özel bir adrese gitmelidir.

function processFee(uint256 amount) private {
        uint256 fee = (amount / 100) * 5; // 5%
        (bool success, ) = feeCollector.call{value: fee}("");
        require(success, "SEND_FEE_ERROR");
    }
    function setFeeCollector(address _feeCollector) external onlyOwner {
        feeCollector = _feeCollector;
    }

Matematik doğru görünebilir – %1’in ne kadar olduğunu hesaplar ve sonra %5’i elde etmek için çarparız. Ne yazık ki, tam sayıların bölünmesi yalnızca bir bölüm döndürür ve bir hatırlatıcı kaybolur.

Örneğimizde miktar 40 ise (40/100)*5 0 döndürür, buna rağmen doğru sonuç 2’dir. Bölmeden önce daima çarpma işlemi yapmalısınız.

Ayrıca, FeeCollector adresini ayarlamak için bir fonksiyon ekledik, ancak kurucu değişmedi. Başlatılmamış değişkenler varsayılan olarak 0’a ayarlanmıştır. Bu, varsayılan olarak feeCollector‘ın address(0) olacağı anlamına gelir ve eksik doğrulama nedeniyle bu değeri manuel olarak ayarlayabiliriz. Address (0) adresine gönderilen ETH kaybolur.

Artık kullanıcılar jeton satın alabilir, ancak kullanamazlar. Onlara bir jetonu birine transfer etme yeteneği verelim.

function transfer(address receiver) external {
        balances[msg.sender] -= 1;
        balances[receiver] += 1;
    }

İşlev çok basittir – sadece gönderenin bakiyesini düşürür ve jetonu alıcı adresine ekleriz. Gönderenin bakiyesinde jeton olup olmadığına dair herhangi bir kontrol olmadığını fark edebilirsiniz. Bu örnekte derleyici sürümü >= 0.8.0 olarak ayarlanmıştır, burada solidity otomatik olarak yetersiz/taşma durumunu kontrol eder. Sıfırdan çıkarmaya çalışırsak, hata üretilecektir. Solidity 0.8.0’dan önce, yetersiz/taşma olup olmadığını manuel olarak kontrol etmek veya SafeMath gibi özel bir kitaplık kullanmak gerekiyordu.

Tokenleri 2 alıcıya aktarabilen bir fonksiyon da ekleyelim.

function transfer(address receiver) external {
        transferFrom(msg.sender, receiver);
    }
    function transferMany(address receiver1, address receiver2)
        external
    {
        transferFrom(msg.sender, receiver1);
        transferFrom(msg.sender, receiver2);
    }
    function transferFrom(address from, address receiver) {
        balances[from] -= 1;
        balances[receiver] += 1;
    }

Kod tekrarını önlemek için transfer ve transferMany tarafından kullanılması gereken transferFrom yardımcı fonksiyonunu ekledik. TransferFrom görünürlüğünü belirtmeyi “unuttuğumuz” için, işlevin varsayılan görünürlüğü public olarak ayarlanmıştır. Bu, bir parametre olarak kabul edildiğinden, herhangi bir adresten belirteçlerin aktarılmasına izin verecektir. Kamusal işlevlere dışarıdan erişilebilir olmaları gerekip gerekmediği değerlendirilmelidir.

Şimdi tüm düzeltmeleri uygulama zamanı:

pragma solidity >=0.8.0;
contract DummyToken {
    uint256 public price;
    mapping(address => uint256) public balances;
    address public owner;
    address public feeCollector;
    constructor(uint256 _price, address _feeCollector) {
        owner = msg.sender;
        setPrice(_price);
        setFeeCollector(_feeCollector);
    }
    modifier onlyOwner() {
        require(msg.sender == owner, "NOT_OWNER");
        _;
    }
    function buy() external payable {
        require(msg.value == price, "INVALID_VALUE");
        balances[msg.sender] += 1;
    }
    function setPrice(uint256 _price) public onlyOwner {
        require(_price > 0, "INVALID_PRICE");
        price = _price;
    }
    function processFee(uint256 amount) private {
        uint256 fee = amount / 20; // 5%
        (bool success, ) = feeCollector.call{value: fee}("");
        require(success, "SEND_FEE_ERROR");
    }
    function setFeeCollector(address _feeCollector) public onlyOwner {
        require(_feeCollector != address(0), "INVALID_FEE_ADDRESS");
        feeCollector = _feeCollector;
    }
    function transfer(address receiver) external {
        transferFrom(msg.sender, receiver);
    }
    function transferMany(address receiver1, address receiver2) 
        external 
    {
        transferFrom(msg.sender, receiver1);
        transferFrom(msg.sender, receiver2);
    }
    function transferFrom(address from, address receiver) internal {
        balances[from] -= 1;
        balances[receiver] += 1;
    }
}

Sözleşme tamamlanmış görünüyor, ancak hala önemli bir kısım eksik. Token satın alındığında, fiyatın %95’i sözleşme bakiyesinde kalacaktır, ancak onu geri çekmenin bir yolu yoktur. Ödenebilir işlevlere sahipse, sözleşmenin ETH çekme işlevine sahip olduğundan emin olmak önemlidir.

function withdrawEth() external {
        (bool success, ) = owner.call{value: address(this).balance}("");
        require(success, "WITHDRAW_ERROR");
    }

Mirasla ilgili sorunlar

Bazı problemler çok açık değildir. Kalıtım kullanan birkaç sözleşme örneğine bakalım.

contract Base1 {
    function getValue() public returns(uint) {
        return 1;
    }
}
contract Base2 {
    function getValue() public returns(uint) {
        return 2;
    }
}
contract Derived is Base1, Base2 {
}

Solidity, çoklu kalıtımı destekler. Türetilmiş sözleşmeden getValue() çağrıldığında sonuç ne olacak? Solidity, devralınan son sözleşmedeki uygulamayı kullanacaktır. Birden çok kalıtımla çalışırken sözleşmeleri doğru sırada belirtin.

Temel ve Türetilmiş sözleşmelerin aynı ada sahip değişkenlere sahip olduğu duruma değişken gölgeleme denir.

contract Base {
    uint private x = 1;
    function getValueBase() public returns(uint) {
        return x;
    }
}
contract Derived is Base {
    uint private x = 2;
    function getValue() public returns(uint) {
        return x;
    }
}

Değişken gölgeleme meydana geldiğinde, her iki değişken de depoda oluşturulacak ve bunlara karşılık gelen sözleşmeden erişilebilir. Örnek sözleşmede Derived::getValueBase() 1 değerini döndürür.

Bir işlevi geçersiz kılarken, özellikle temel sözleşme başka biri tarafından uygulanıyorsa (örneğin, openzeppelin tarafından) bazı sorunlar ortaya çıkabilir.

contract Base {
    function pay() external payable {
        afterPayment();
    }
    function afterPayment() internal virtual {
        // do nothing
    }
}
contract Derived is Base {
    uint public payments;
    function afterPayment() internal override {
        ++payments;
    }
    function getAvaragePayment() external returns(uint) {
        return address(this).balance / payments;
    }
}

Temel sözleşmede dahili olarak çağrılan bir işlevi geçersiz kılarken, bu işlevin nasıl kullanıldığını kontrol etmek önemlidir. Örnek sözleşmede, ortalama ödeme tutarını kontrol etmek için afterPayment() işlevini geçersiz kılıyoruz, ancak Temel sözleşmede sıfır ödeme kontrolü yok.

Sonuç Bağlamı : Solidity Akıllı Sözleşmelerinde Güvenlik Açıklarını Bulma

Bu yazıda bazı sorunların nasıl belirleneceğini araştırdık:

  • Dağıtımdan sonra bir değerin değiştirilmesi gerekip gerekmediğini kontrol edin;
  • Gönderen doğrulamasını kontrol edin;
  • Giriş doğrulamasını kontrol edin;
  • Bölmeden önce çarpma işlemini gerçekleştirin;
  • Başlatılmamış değişkenlerin varsayılan olarak 0 olduğunu unutmayın;
  • ETH’nin address(0)‘a gönderilmediğini kontrol edin;
  • Eksik/taşma işlendiğini kontrol edin;
  • Her fonksiyonun görünürlük seviyesini belirttiğini kontrol edin;
  • Kamu işlevine dışarıdan erişilebilir olması gerektiğini doğrulayın;
  • Sözleşmenin ödenebilir işlevleri varsa, ETH’yi geri çekme işlevine sahip olduğundan emin olun;
  • Miras sırasını kontrol edin;
  • Değişken gölgelemeyi kontrol edin;
  • Temel sözleşmede geçersiz kılınan işlevlerin nasıl kullanıldığını kontrol edin.
Akıllı Sözleşme Hackleme Yol Haritası 2023
Akıllı Sözleşme Hackleme Yol Haritası 2023

Akıllı sözleşme geliştirme yolculuğunuz hakkında daha iyi rehberlik almak için Solidity nedir? Ethereum Akıllı Sözleşmelerinin Dili Rehberi içeirğimize göz atın. Dilerseniz Yeni Başlayanlar için Solidity – Akıllı Sözleşme Geliştirme Hızlandırılmış Kursuna katılın.

Çalışmaya nereden başlayacğaım diyenler için Blockchain ​​Developer Olmak İçin Yol Haritası içeriğine de muhakkak bakın.

Bu makaleyi okuduğunuz için teşekkürler! Bana destek olmak isterseniz;

Beni TwitterLinkedin ve YouTube‘da takip edin.

Kısa bir yorum bırakmayı UNUTMAYIN!

Hasan YILDIZ, Girişimci. Doktora Öğrencisi. Yazmayan YAZILIMCI. Veri Şeysi. Eğitmen...

Yazarın Profili
İlginizi Çekebilir

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir

Yorumlar (1)

  1. 17 Ekim 2022

    Hocam Solidity Eğitiminize nasıl katılabiliriz?

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir