The Ethernaut #8 i #9 – vault & king

Przemek/ 5 lipca, 2023

Dzisiaj powracamy do rozwiązywania wyzwań z platformy Ethernaut. Jeśli spotykasz tę serię po raz pierwszy, zachęcam Cię do zapoznania się z poprzednimi wpisami, które przedstawiają interesujące aspekty związane z bezpieczeństwem. Teraz, bez dłuższego wstępu, przejdźmy do dwóch kolejnych zadań.

Zadanie #8 – vault

Celem pierwszego wyzwanie będzie odblokowanie skarbca. Jak zwykle rozpocznijmy od analizy otrzymanego kodu.

pragma solidity ^0.8.0;

contract Vault {
  bool public locked;
  bytes32 private password;

  constructor(bytes32 _password) {
    locked = true;
    password = _password;
  }

  function unlock(bytes32 _password) public {
    if (password == _password) {
      locked = false;
    }
  }
}

Smart kontrakt zawiera dwa pola. Pierwsze przechowuje wartość odpowiedzialną za blokadę skarbca, a drugie zawiera hasło dostępu. Zauważ, że zmienne mają różne modyfikatory dostępu. Modyfikator “public” automatycznie tworzy funkcję, która umożliwia odczytanie wartości “locked” z blockchaina. Natomiast w przypadku modyfikatora “private” taka funkcja nie jest generowana.

Następnie znajduje się konstruktor, który ustawia hasło i blokuje skarbiec. Poniżej niego znajduje się jedyna funkcja, której celem jest odblokowanie sejfu. Może to zrobić dowolny użytkownik, ale musi znać hasło. Jeśli odkryjemy tajemniczy ciąg znaków, będziemy w stanie odblokować skarbiec.

Próba odgadnięcia hasła brute force (na siłę) nie jest możliwa. Musimy znaleźć sposób, aby uzyskać dostęp do wartości przechowywanej w zmiennej “password“. Jednak jak to zrobić, skoro pole to zostało oznaczona jako prywatne?

Jedna informacja może być zaskakująca, szczególnie dla osób, które miały do czynienia z programowaniem w bardziej tradycyjnych technologiach. Na blockchainie nic nie jest prywatne, nawet jeśli w kodzie smart kontraktu oznaczymy to w ten sposób. Niemożliwe jest jednak odczytanie tej wartości bezpośrednio poprzez wywołanie metody z tego lub innego kontraktu. Ponieważ wszystko na blockchainie jest publiczne, możemy próbować uzyskać dostęp do tej wartości, “uderzając” bezpośrednio w odpowiednie miejsce w pamięci. Na szczęście nie musimy zaczynać od zera, ponieważ istnieją gotowe narzędzia, które mogą nam w tym pomóc.

Najpopularniejszymi bibliotekami do integracji z blockchainem są web3.js oraz ethers.js. Ponieważ pierwsza z nich jest dostępna od razu w konsoli przeglądarki na stronie wyzwania, skorzystamy z niej.

Do pobrania dowolnej wartości z dowolnego miejsca w pamięci smart kontraktu służy metoda:

web3.eth.getStorageAt(address, slot)

Funkcja jest bardzo prosta w użyciu. Wymaga podania adresu kontraktu, który nas interesuje, oraz pozycji w pamięci, z której chcemy odczytać dane. Na blogu jeszcze nie zagłębialiśmy się w szczegóły dotyczące pamięci smart kontraktów, ale teraz nie jest to aż tak istotne. Wystarczy wiedzieć, że każdy kontrakt ma określoną liczbę miejsc (slotów) na dane, od 0 do 2^256 – 1, czyli całkiem sporo. W przyszłości zagłębimy się w ten temat, ale teraz skupmy się na wyzwaniu.

Mamy już sposób, który możemy wykorzystać do uzyskania niezbędnego hasła. Pozostaje nam jedynie odgadnięcie miejsca w pamięci. Pola kontraktu są zapisywane w pamięci zgodnie z kolejnością ich deklaracji, a w tym przypadku każde pole trafia do nowego “slotu”. Pamiętaj, że numeracja slotów zaczyna się od 0. W naszym przypadku wywołanie metody będzie wyglądać następująco:

await web3.eth.getStorageAt("0xBe0220e1AA661037465221876eB7F0fEc4EAfFE1", 1)

Zamień adres na adres swojego kontraktu. Jeśli nie znasz JavaScriptu, nie martw się o słowo “await” na początku. Po prostu załóż, że musi tam być.

Otwórz konsolę przeglądarki i wykonaj przygotowaną komendę. Otrzymasz wartość, która znajduje się pod polem “password“. Teraz wystarczy wywołać funkcję “unlock” z otrzymaną odpowiedzią i gotowe! Wyzwanie zostanie rozwiązane. Do wywołania funkcji możesz użyć Remixa, konsoli przeglądarki lub innego preferowanego narzędzia. Wybór należy do Ciebie.

Pozostaje jeszcze zatwierdzenie rozwiązania wyzwania na stronie i możemy przejść dalej.

Zadanie #9 – king

Rysunek 2. Królewska para
(źródło: https://www.bbc.com/news/uk-56201331)

W kolejnym wyzwaniu weźmiemy udział w grze, a dokładniej mówiąc, będziemy próbować ją zepsuć dla innych graczy. Spójrzmy, o co dokładnie chodzi.

contract King {

  address king;
  uint public prize;
  address public owner;

  constructor() payable {
    owner = msg.sender;  
    king = msg.sender;
    prize = msg.value;
  }

  receive() external payable {
    require(msg.value >= prize || msg.sender == owner);
    payable(king).transfer(msg.value);
    king = msg.sender;
    prize = msg.value;
  }

  function _king() public view returns (address) {
    return king;
  }
}

Zasady gry są proste: gracz, który wyśle największą ilość etheru lub innej natywnej kryptowaluty, zależnie od blockchaina, staje się królem gry, aż do momentu, gdy ktoś go przebije lub wyrówna.

Aby zaimplementować ten mechanizm, twórcy stworzyli kontrakt, który przechowuje trzy wartości: obecnego króla, ostatnią wpłatę, która dała mu berło władzy, oraz adres właściciela smart kontraktu. Konstruktor ustawia domyślne wartości.

W kontrakcie zaimplementowano metodę “receive“, która umożliwia bezpośrednie wysyłanie etheru na adres kontraktu i jest to jedyny sposób, aby przejąć władzę. W pierwszej linii tej metody sprawdzany jest warunek, czy wysłana kwota jest większa lub równa poprzedniej wpłacie. Jeśli tak, poprzedni król otrzymuje nowe środki i schodzi z tronu.

Dodatkowo zauważamy, że właściciel smart kontraktu zawsze może stać się królem, niezależnie od wpłaconej kwoty. Trzeba przyznać, że w prawdziwej grze takie zachowanie byłoby niesprawiedliwe. Na szczęście w tym przypadku zaimplementowano to w celu ułatwienia weryfikacji.

Na dole kontraktu znajduje się również funkcja, która pozwala odczytać adres obecnego króla. Jak możemy więc popsuć zabawę innym?

Rozwiązanie tego wyzwania będzie wymagało pewnej wiedzy, którą zdobyliśmy w przeszłości. Zanim jednak przejdziemy do tego, zastanówmy się, w którym miejscu możemy zrobić psikusa.

Jedyna linia, w której coś może pójść nie tak i spowodować zablokowanie gry, to ta, w której przesyłane są środki do poprzedniego króla. Jeśli udałoby się nam sprawić, że bez względu na wysłaną kwotę i adres nadawcy, posypie się tam błąd, osiągniemy nasz cel. Na szczęście rozwiązanie w tym przypadku jest dość proste.

Ponieważ nie da się faktycznie zablokować możliwości wysłania środków na standardowy adres portfela (EOA), musimy skorzystać ze smart kontraktu. Innymi słowy, napiszmy kod, który zostanie królem, a następnie odrzuci każdą transakcję skierowaną do niego. Możesz pamiętać z poprzednich wpisów, że jeśli kontrakt nie ma metody “receive“, nie można do niego bezpośrednio wysyłać środków, i właśnie to wykorzystamy. Poniżej znajduje się przykładowy kod:

contract PartyPooper {
    King king;

    constructor(address payable _target) {
        king = King(_target);
    }
    
    function attack() external payable {
        uint prize = king.prize();
        (bool success, )= address(king).call{value:prize}("");
        require(success);
    }
}

W funkcji, która posłuży nam do ataku, odczytujemy aktualną wysokość wymaganej wpłaty, a następnie wysyłamy taką ilość środków do gry, co uczyni nas jej królem. Pamiętaj, że podczas wywoływania tej metody musisz jednocześnie przekazać środki na swój kontrakt, inaczej nie będzie miał on z czego zapłacić. Alternatywnie, możesz wysłać ether w osobnej transakcji, poprzedzającej atak.

W ten sposób zablokowaliśmy grę, a jedynym sposobem, aby gracze mogli znowu się bawić, jest wgranie nowego smart kontraktu. Teraz, standardowo, możemy zatwierdzić rozwiązanie na stronie, i voilà, kolejne zadanie za nami.

Zanim zakończę dzisiejszy wpis, warto krótko wspomnieć, jak powinno się postępować w przypadku takich sytuacji.

Środki ze smart kontraktu możemy wysyłać do użytkowników na dwa sposoby, które noszą nazwę płatności push i pull. Pierwszy z nich odpowiada temu, co zaimplementowano w grze. Najprościej mówiąc, w tym przypadku łączymy logikę jakiejś akcji z wysłaniem środków na podany adres. Innymi słowy, oprócz aktualizowania obecnego króla i ostatniej najwyższej kwoty, dodatkowo przesyłaliśmy monety na adres poprzedniego monarchy.

Metoda pull rozdziela te operacje na dwie części. W przypadku dzisiejszej gry polegałoby to na stworzeniu dodatkowej funkcji, która służyłaby jedynie do odebrania należnych środków, a pierwotna metoda używana byłaby jedynie do przejęcia władzy i zapisania odpowiednich danych w kontrakcie. Zaktualizowana wersja mogłaby wyglądać np. tak:

contract BetterKing {

  address king;
  uint public prize;
  address public owner;
  mapping(address => uint256) balance;

  constructor() payable {
    owner = msg.sender;  
    king = msg.sender;
    prize = msg.value;
  }

  function withdraw() external {
    uint256 amount = balance[msg.sender];
    balance[msg.sender] = 0;
   
    (bool success, ) = msg.sender.call{value: amount}("");

    require(success, "Transfer failed");
  }

  receive() external payable {
    require(msg.value >= prize || msg.sender == owner);
    
    balance[msg.sender] += msg.value;
    king = msg.sender;
    prize = msg.value;
  }

  function _king() public view returns (address) {
    return king;
  }
}

Co prawda doszła nam tutaj jeszcze logika związana ze śledzeniem sald poszczególnych kont, ponieważ nie wiemy, kiedy środki zostaną odebrane, ale w ten sposób zabezpieczyliśmy się na wypadek psikusa. Z tego względu podejście pull jest zdecydowanie lepszą opcją, chociaż jego minusem jest zmuszenie użytkownika do wykonania kolejnej transakcji i pokrycia jej kosztów.

Podsumowując dzisiejsze tematy, należy pamiętać, że wszystko, co umieścimy na blockchainie, będzie dostępne publicznie. Niezależnie od tego, czy z perspektywy kodu coś jest ukryte, jeśli ktoś będzie chciał, to i tak dostanie się do tej informacji. Druga istotna sprawa do zapamiętania jest taka, że w większości przypadków lepiej rozdzielić odbieranie środków przez użytkownika od logiki aplikacji.

Mam nadzieję, że dzisiaj pomogłem zrozumieć kolejne aspekty związane z blockchainem. Do zobaczenia wkrótce! 🙂

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