The Ethernaut #4 i #5 – telephone & token

Przemek/ 21 marca, 2023

W dzisiejszym wpisie wracamy na platformę Ethernaut(https://ethernaut.openzeppelin.com/). Skupimy się na dwóch kolejnych wyzwaniach, które pomogą poznać kolejne “smaczki” języka Solidity. Dlatego bez zbędnego gadania bierzemy się do roboty! 

Zadanie #4 – telephone

Pierwszym wyzwaniem, za które się dzisiaj zabieramy jest to o nazwie Telephone. Jego celem ponownie będzie przejęcie kontroli nad kontraktem. Spójrzmy,  jak wygląda kod.

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

contract Telephone {

 address public owner;

 constructor() {
   owner = msg.sender;
 }

 function changeOwner(address _owner) public {
   if (tx.origin != msg.sender) {
     owner = _owner;
   }
 }
}

Kontrakt jest bardzo krótki i nie zawiera skomplikowanej logiki. Przechowuje jedną wartość, mówiącą, kto obecnie jest właścicielem kontraktu. Jest ona inicjalizowana podczas wgrywania kontraktu na blockchain, a dzieje się to dzięki konstruktorowi. Posiada on również jedną funkcję changeOwner”, która służy do tego, do czego nawiązuje sama nazwa:)

Jej logika jest bardzo prosta. Jeżeli zostanie spełniony odpowiedni warunek, to nowy właściciel zostanie przypisany do kontraktu. Co zatem musi się stać, aby był on prawdziwy? Wartości przechowywane w globalnych zmiennych “tx.origin” oraz “msg.sender” muszą się od siebie różnić. Spełnienie tego warunku za chwilę będzie banalnie proste, ale najpierw musisz zrozumieć, jak działają te zmienne, a pomoże w tym poniższy rysunek.

W przedstawionym przykładzie mamy użytkownika oznaczonego jako EOA oraz dwa oddzielne smart kontrakty. EOA, czyli Externally Owned Account, to nic innego jak konto, które zarządzane jest poprzez klucz prywatny. Innymi słowy, to po prostu Twój adres blockchainowy. Więcej informacji na ten temat znajdziesz w tym poście.

W przedstawionym przykładzie użytkownik wywołuje najpierw jakąkolwiek funkcję z kontraktu A, po czym ten woła funkcję z kontraktu B. Jak możesz zauważyć podczas pierwszego wywołania zarówno “msg.sender” oraz “tx.origin” mają tę samą wartość, którą jest adres użytkownika. W momencie drugiego zapytania “msg.sender” zmienia się na adres kontraktu A, natomiast “tx.origin” pozostaje bez zmian. 

Jaki wniosek możemy wyciągnąć z tego prostego przykładu? Globalna zmienna “tx.origin” przechowuje zawsze informacje o adresie, który zainicjalizował cały łańcuch wywołań. Dodatkowo będzie to zawsze adres typu EOA, ponieważ tylko taki rodzaj kont może rozpocząć akcję na blockchainie. Natomiast wartość przechowywana w “msg.sender” będzie przechowywać adres, który odpowiada za bezpośrednie wywołanie funkcji. Jeżeli jest to równocześnie pierwsza akcja w całym łańcuchu, to obie zmienne będą sobie równe.

Mając tę wiedzę, możemy wrócić do naszego wyzwania. Dla przypomnienia musimy ustawić swój adres jako właściciela kontraktu, a żeby to zrobić, wystarczy wywołać funkcję “changeOwner” i spełnić warunek mówiący, że “msg.sender” musi się różnić od “tx.origin”. Wiemy już, że te dwie zmienne są sobie równe tylko w sytuacji, kiedy wywołanie jest pierwszych w całym łańcuchu, dlatego wystarczy, że utworzymy drugi kontrakt, który odwoła się do pierwszego.

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

import "./Telephone.sol";

contract Hacker {
   Telephone immutable telephone;

   constructor(address _target) {
       telephone = Telephone(_target);
   }

   function attack() external {
       telephone.changeOwner(msg.sender);
   }
}

Wystarczy teraz wgrać powyższy kontrakt na blockchain i wywołać funkcję “attack”. Jako parametr funkcji “changeOwner” możemy na sztywno podać adres naszego portfela albo zrobić to, jak w powyższym kodzie wykorzystująć wartość przechowywaną przez “msg.sender”, ponieważ ta będzie równa naszemu adresowi, dlatego że to my odwołujemy się do kontraktu “Hacker”.

Jak zwykle zostało nam jeszcze zatwierdzić rozwiązanie na stronie wyzwania i możemy przejść dalej.

Zadanie #5 – token

Kolejne zadanie posiada minimalnie więcej kodu do analizy, ale jak się zaraz okaże, nie jest wcale bardziej skomplikowane. Celem tego wyzwanie nie będzie jak poprzednio przejęcie władzy, ale zwiększenie swoich środków. Co ważne, na samym początku posiadamy 20 tokenów. Przejdźmy do kodu.

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

import "./Telephone.sol";

contract Hacker {
   Telephone immutable telephone;

   constructor(address _target) {
       telephone = Telephone(_target);
   }

   function attack() external {
       telephone.changeOwner(msg.sender);
   }
}

Powyższy kontrakt reprezentuje okrojoną wersję tokena ERC20. Posiada pola przechowujące aktualny saldo dla danego adresu oraz maksymalną liczbę wszystkich tokenów, która ustawiana jest w konstruktorze.

W kontrakcie znajdziemy dwie funkcje “transfer” oraz “balanceOf”. Druga z nich służy do sprawdzenia salda dla podanego konta, pierwsza do przetransferowania środków i to właśnie ona będzie nas interesować.

Na jej początku znajduje się sprawdzenie, czy obecne saldo wysyłającego pomniejszone o ilość środków do wysłania jest większe lub równe zero. Jeśli nie, to funkcja kończy wywołania. Jeżeli warunek przejdzie, to zmniejszane jest saldo nadawcy i zwiększane odbiorcy. Na końcu zwracana jest wartość ”true” mówiąca o powodzeniu operacji.

Jak tu spowodować, żebyśmy stali się posiadaczami większej liczby tokenów, bez proszenia innych o ich wysłanie na nasz adres? Kluczem do rozwiązania tej zagadki jest wersja Solidity, która została użyta do napisania tego kontraktu i zachowanie zmiennych liczbowych, jakie się z tym wiąże.

W starszym Solidity występował problem zwany “Integer Overflow/Underflow”. Sytuacja ta powodowała możliwość “przekręcenia” się liczby w momencie przekroczenia granicznych wartości dla danego typu. Uint, który przechowuje liczby całkowite, ma ustawioną dolną granicę na 0, a górną na 2^256-1 (bardzo duża liczba). Innymi słowy, jeżeli za dużo dodasz albo odejmiesz, to otrzymasz wynik z drugiego końca zakresu. Dodam, że od wersji Solidity 0.8 problemem tym nie musimy się przejmować, ponieważ domyślnie we wspomnianej sytuacji poleci błąd. Wcześniej natomiast należało to sprawdzać ręcznie lub można też było użyć gotowej biblioteki SafeMath dostarczonej przez OpenZeppelin.

Z ciekawostek kiedyś podobna sytuacja przydarzyła się na platformie YouTube, kiedy to teledysk do piosenki Gangam Style odniósł ogromny sukces i liczba jego wyświetleń przekroczyła wcześniej założony zakres:)

Wykorzystując tę wiedzę, możemy oszukać zasady rządzące powyższym kontraktem. Wystarczy, że przy pomocy naszego konta wywołamy funkcję transfer, podając do niej dowolny inny adres i liczbę środków do wysłania równą 21. Takie dane spowodują, że w pierwszej linijce warunek zostanie spełniony, ponieważ po odjęciu od naszego salda liczby 21 wynik się przekręci i wynosić będzie górną granicę typu “uint”, co jest więcej niż wymagane 0. Dalej zostanie nam przypisana dokładnie ta wartość i wykona się reszta kodu. Tym sposobem staliśmy się posiadaczami ogromnej liczby… bezwartościowych tokenów:)

Co warto jeszcze zauważyć, pomimo zdefiniowania w konstruktorze całkowitej podaży tokenów nic nie stało na przeszkodzie, abyśmy mieli ich więcej, niż wynosi ta liczba.

Dobrnęliśmy do końca dzisiejszego wpisu. Mam nadzieję, że zapamiętasz z niego, że używanie zmiennej globalnej “tx.origin” w warunkach jest bardzo niebezpieczne i należy się tego wystrzegać, jak również problemów wynikających z “przekręcenia” się liczby całkowitej. Wierz mi lub nie, ale przez takie błędy ludzie potracili dużo pieniędzy. Wyzwań na platformie Ethernaut jest jeszcze dużo, dlatego za jakiś czas powrócę do tego tematu:)

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