Reentrancy Atak nedir? - Akıllı Sözleşmeler

Featured image

EVM aynı anda aynı istek sahibi tarafından birden fazla sözleşme çalıştıramazken, farklı bir sözleşme üzerinde çalışan bir kod parçacığı, çağrı dönene kadar hedef sözleşmenin yürütmesini ve bellek durumunu duraklatır, bu noktada kod içerisindeki akış normal şekilde ilerler. Bu duraklama ve yeniden başlatma, Reentrancy olarak bilinen bir güvenlik açığı oluşturabilir. Kısaca özetlemek gerekirse sözleşme içerisindeki bir değişkenin değişmesini engeleyerek aynı metodu tekrar çağrılabilir hale getirip istismar ediyor.

contract Token {
    mapping (address => uint256) public balances;

    function deposit() external payable {
        balances[msg.sender] += msg.value;
    }

    function withdraw() external {
        uint256 amount = balances[msg.sender];
        (bool success, ) = msg.sender.call.value(amount)("");
        require(success);
        balances[msg.sender] = 0;
    }
}
Sıkça karşılaştığımız bir durum üzerinden örnekle;
Bir kullanıcının daha önce sözleşmede sakladığı ETH’yi geri çekmesine izin vermek için süreç bu şekilde işler:
  1. Kullanıcının ne kadar bakiyesi olduğunu okur
  2. ETH bakiyesini talep eden adrese gönderir
  3. Ardından bakiyesini 0’a eşitler, böylece bakiyesini tekrar çekmesini engeller.

Normal bir kullanıcı kendi cüzdanından (örneğin MetaMask hesabınızdan) withdraw() metodunu çağırırsa, sözleşme tam da sözleşmeyi yazanların istediği gibi çalışır: msg.sender.call.value()yalnızca hesabına bakiyesi kadar ETH gönderir.

Ancak, akıllı sözleşmeler başka sözleşmeler tarafından çağrılabilir. withdraw() metodunu çağıran kötü niyetli bir sözleşme ise, msg.sender.call.value() metodu yalnızca ETH miktarını göndermekle kalmaz, aynı zamanda kodu yürütmeye devam etmek için dolaylı olarak için de bulunduğu metodu tekrar çağırır. Bu kötü niyetli sözleşmenin örneği:

contract AttackContract {
    function beginAttack() external payable {
        Token(TOKEN_ADDRESS).deposit.value(1 ether)();
        Token(TOKEN_ADDRESS).withdraw();
    }

    function() external payable {
        if (gasleft() > 40000) {
            Token(TOKEN_ADDRESS).withdraw();
        }
    }
}
Bu sefer ise süreç şu şekilde işler;
  1. Kötü niyetli sözleşme Hedef kontrata 1 ETH gönderir
  2. Kötü niyetli sözleşme withdraw() metodunu çağırır

    1. Hedef sözleşme bakiye durumunu okurbalances[msg.sender]
    2. Kurban, tüm bakiyesini kötü niyetli sözleşmeye iade eder
  3. Kötü niyetli sözleşmeye gönderilen ETH sonrası default metodu çalışır ve withdraw() metodu tekrar çağrılır

    1. Hedef sözleşme bakiye durumunu okurbalances[msg.sender]
    2. Kurban, tüm bakiyesini kötü niyetli sözleşmeye iade eder
  4. Kötü niyetli sözleşmeye gönderilen ETH sonrası default metodu çalışır ve withdraw() metodu tekrar çağrılır

Bu şekilde hedef kontrattaki ETH veya Saldıran kontrattaki gas tükenene kadar süreç devam eder.

reentrancy atak şeması

Reentrancy’den nasıl korunabilirim?

Birçok yöntemin varlığından bahsedilsede çoğunluğu EVM yapısının efektif kullanımını sekteye uğratmaktadır.

Doğru ve basit olan çözüm ise;

Yalnızca bakiye güncellemesi ve eth gönderiminin sırasını değiştirerek, saldırıyı etkinleştiren reentrancy koşulunu önleriz. balances metod çalıştığı anda 0’a eşitleneceğinden, bundan sonra yapılabilecek reentrancy atağı saldırgana fayda sağlamayacaktır .

contract Token {
    function withdraw() external {
        uint256 amount = balances[msg.sender];
        balances[msg.sender] = 0;
        (bool success, ) = msg.sender.call.value(amount)("");
        require(success);
    }
}


Daha önce yaşanmış Reentrancy saldırılarına göz atmak için akalayci.com/reentrancy