Błędy w kontraktach

Piotr Nazimek, 25 września 2017

Jakie mogą być skutki błędów w kontraktach? Jednym z nich jest utrata środków, o czym niektórzy użytkownicy portfela Parity mieli okazję boleśnie przekonać się w lipcu tego roku. Blockchain nie zapomina, to co w nim zaszło nie może być odwołane. Jest to jego niewątpliwa zaleta, która w pewnych sytuacjach staje się wadą. Kontrakt rządzi się regułami takimi jakie zostały w nim zaimplementowane. Tylko je możemy wykorzystać, aby uratować nasze środki, jeśli zachodzi podejrzenie, że w kontrakcie nie są one już bezpieczne. Nawet jeśli zasady są błędne to nadal w kontrakcie są zasadami.

Dziura w Parity

Parity to jedna z implementacji klienta wraz z kontraktem portfela dla Ethereum. W poprzednim wpisie omówiłem inną implementację portfela jaką jest Ethereum Wallet.

W uproszczeniu implementacja portfela Parity składa się z dwóch kontraktów. Jeden z nich spełnia rolę biblioteki WalletLibrary, która dostarcza implementacji funkcji wykorzystywanych przez kontrakt Wallet. Technikę taką stosuje się, aby nie ponosić wysokich kosztów przy tworzeniu instancji kontraktów, jeśli część z ich kodu może być wspólna. Bibliotekę tworzymy w blockchain wyłącznie raz, instancji portfela będzie wiele.

Biblioteka WalletLibrary dostarcza między innymi funkcję initWallet, która była wywoływana z konstruktora portfela oraz funkcję changeOwner, która służy do zmiany właściciela, a wołana była z metody changeOwner. Poniżej przedstawiłem uproszczoną implementację tej części kontraktu Parity.

contract WalletLibrary {
    address public owner;

    // funkcja wołana przy tworzeniu portfela
    function initWallet(address _owner) {
        owner = _owner;
        // ...
    }

    // zmiana właściciela portfela
    function changeOwner(address _new_owner) external {
        // sprawdzamy czy funkcję wywołuje obecny właściciel
        if (msg.sender == owner) {
            owner = _new_owner;
        }
    }
}

contract Wallet {
    address public _walletLibrary;
    address public owner;

    // konstruktor portfela, woła initWallet
    function Wallet(address _owner) {
        // ...
        _walletLibrary.delegatecall(bytes4(sha3("initWallet(address)")), _owner);
    }

    // zmiana właściciela portfela
    function changeOwner(address _new_owner) {
        _walletLibrary.delegatecall(bytes4(sha3("changeOwner(address)")), _new_owner);
    }

    // funkcja awaryjna, wołana w razie braku możliwości dopasowania innej funkcji
    function () payable {
        _walletLibrary.delegatecall(msg.data);
    }
}

W tym miejscu należy wytłumaczyć jak działa delegatecall. Wywołanie z poziomu kontraktu funkcji innego kontraktu za pomocą delegatecall powoduje wykonanie kodu na rzecz wołającego kontraktu. Oznacza to, że delegatecall do funkcji changeOwner w rzeczywistości zmodyfikuje pole owner konkretnej instancji kontraktu Wallet a nie pole owner w WalletLibrary. Jest to sensowne zachowanie, wszak wszystkie portfele nie mają wspólnego właściciela.

Powyższa implementacja wykonuje delegatecall z konstruktora do funkcji initWallet oraz z changeOwner do odpowiedniej funkcji w bibliotece. Zauważmy, że funkcja initWallet zmienia właściciela kontraktu bezwarunkowo. Wydaje się to poprawne, w konstruktorze dopiero tworzymy kontrakt, więc będzie to pierwszy właściciel. Konstruktor może być wywołany tylko raz. Funkcja changeOwner weryfikuje czy to aktualny właściciel ją wywołuje i tylko wtedy pozwala na ustawienie nowego adresu.

Wewnątrz Ethereum Virtual Machine wywołanie funkcji kontraktu z innego kontraktu polega na wyliczeniu jej sygnatury oraz przekazaniu (doklejeniu do sygnatury) wartości parametrów. Sygnatura funkcji to cztery bajty ze skrótu jej nazwy wraz z typami parametrów. Wyliczana jest ona przez kontrakt z ciągu znaków. W powyższym przykładzie dla initWallet(address) będzie to 9da8be21. Zatem taką sygnaturę można wyliczyć samodzielnie poza kontraktem. Czy w przedstawionym kontrakcie można w jakiś sposób wywołać funkcję initWallet gdy kontrakt jest już utworzony? Przyjrzyjmy się implementacji funkcji bez nazwy, która jest tak zwaną funkcją awaryjną (ang. fallback) wołaną w razie braku możliwości dopasowania innej funkcji. Wykonuje ona delegatecall podając jako parametr dane przekazane w transakcji. Bingo! Wystarczy zatem doprowadzić do wywołania funkcji awaryjnej z sygnaturą initWallet(address) podając jako parametr wybrany przez nas adres nowego właściciela. To działało, ponieważ kontrakt Wallet nie miał implementacji funkcji o sygnaturze initWallet(address), więc wywołanie trafiało do funkcji awaryjnej i było przekazywane do biblioteki WalletLibrary, po czym wykonywało się bez przeszkód na rzecz kontraktu wołającego.

Błąd w kontrakcie Parity polegał na tym, że możliwość wywołania funkcji initWallet nie była ograniczona tylko do przypadku procesu tworzenia nowego portfela. Został on wykorzystany przez atakujących do wyprowadzenia Etherów z istniejących kontraktów. Straty osiągnęły kwotę kilkudziesięciu milionów dolarów, a byłyby jeszcze większe gdyby w porę nie zareagowali dobrzy hakerzy, którzy zabezpieczyli środki z błędnych kontraktów poprzez wykorzystanie tego samego ataku. Różnica polegała na tym, że zwrócili potem wyprowadzone środki prawowitym właścicielom kontraktów.

Poprawka w kontrakcie Parity dodatkowo objęła funkcje, które bezwarunkowo modyfikują adres właściciela. Powinny być funkcjami internal, co oznacza, że nie ma możliwości wywołania ich spoza kontraktu. Trzeba pamiętać, że w języku Solidity domyślnie funkcje mają status public.

Łatkę w portfelu Parity, która eliminowała te błędy można zobaczyć pod tym adresem. Cytując niektóre z komentarzy: internal wart 30 milionów dolarów.

Podsumowanie

Powyższy przykład pokazuje jak dużą ostrożność trzeba zachować implementując kontrakty w Solidity i wykorzystując przygotowany kod jako ich użytkownik. Niuanse języka Solidity oraz maszyny wirtualnej EVM mogą mieć fatalne skutki dla kontraktów i ich właścicieli. Rozwijanych jest wiele narzędzi, które pomagają w analizie kodu kontraktów. Stosowane są też różne techniki, jak na przykład dzienny limit wypłat, które pozwalają zminimalizować skutki wykorzystania potencjalnych dziur. Wiele sprytnych ataków jeszcze się pojawi. Na pewno jednym ze sposobów na ich uniknięcie jest nauka poprzez implementację różnych kontraktów i ich analiza we własnej sieci Ethereum.


Tematy: ochrona informacjikryptografiablockchainrejestrbaza danychethereumportfel
Napisano w: Technologie
Zapisz się przez RSS
comments powered by Disqus