Pomóż nam zaktualizować tę stronę

🌏

Dostępna jest nowsza wersja tej strony, ale tylko w języku angielskim. Pomóż nam przetłumaczyć najnowszą wersję.

Ta strona jest niekompletna. Jeśli jesteś ekspertem w temacie, edytuj tę stronę i wzbogać ją swą mądrością.

Ochrona

Ostatnia edycja: , Invalid DateTime
Edit page

Inteligentne kontrakty Ethereum są niezwykle elastyczne, zdolne zarówno do utrzymywania dużych ilości tokenów (często powyżej 1 mld USD), jak i do używania niezmiennej logiki opartej na wcześniej wdrożonym kodzie kontraktów inteligentnych. Stworzyło to wprawdzie dynamiczny i kreatywny ekosystem godny zaufania, ale powiązane ze sobą inteligentne kontrakty są również idealnym ekosystemem, który przyciąga atakujących poszukujących zysków poprzez wykorzystanie słabych punktów w inteligentnych kontraktach i nieoczekiwanych zachowań w Ethereum. Kodu inteligentnego kontraktu zazwyczaj nie można zmienić w celu usunięcia luk w zabezpieczeniach; zasoby, które zostały skradzione z inteligentnych kontraktów, są nie do odzyskania, a skradzione aktywa są niezwykle trudne do śledzenia. Całkowita kwota wartości skradzionej lub utraconej z powodu problemów z inteligentnymi kontraktami z dużym prawdopodobieństwem sięga 1 mld USD. Do poważniejszych strat wynikających z błędów w kodowaniu kontraktów inteligentnych należą:

Warunki wstępne

W tej części zostanie omówione bezpieczeństwo kontraktów inteligentnych, więc wcześniej upewnij się, że dobrze orientujesz się w dotyczących ich kwestiach.

Jak napisać bezpieczniejszy kod inteligentnego kontraktu

Przed uruchomieniem jakiegokolwiek kodu w sieci głównej należy podjąć odpowiednie środki ostrożności, aby chronić wszystko, co ma wartość, którą powierzono Twojemu inteligentnemu kontraktowi. W tym artykule omówimy kilka konkretnych ataków, udostępnimy zasoby, z których można dowiedzieć się więcej na temat innych typów ataków, a także przedstawimy kilka podstawowych narzędzi i najlepszych praktyk, aby zapewnić prawidłowe i bezpieczne funkcjonowanie kontraktów.

Audyty to nie srebrne pociski

Wiele lat wcześniej narzędzia do pisania, kompilowania, testowania i wdrażania inteligentnych kontraktów były bardzo niedojrzałe, co prowadziło do tego, że w wielu projektach kod Solidity był pisany w sposób chaotyczny i szybko przekazywany audytorowi, który go badał, aby upewnić się, że działa on bezpiecznie i zgodnie z oczekiwaniami. W 2020 roku procesy programistyczne i narzędzia wspierające pisanie Solidity są znacznie lepsze; wykorzystanie tych najlepszych praktyk nie tylko zapewnia łatwiejsze zarządzanie projektem, ale jest także istotną częścią bezpieczeństwa projektu. Audyt pod koniec pisania inteligentnego kontraktu nie jest już wystarczający jako jedyny czynnik bezpieczeństwa w projekcie. Bezpieczeństwo zaczyna się przed napisaniem pierwszego wiersza kodu inteligentnego kontraktu, bezpieczeństwo zaczyna się od odpowiednich procesów projektowania i programowania.

Proces tworzenia inteligentnych kontraktów

Co najmniej:

  • Wszystkie kody przechowywane w systemie z kontrolą wersji, takim jak git
  • Wszystkie modyfikacje kodu dokonane za pośrednictwem Pull Request
  • Wszystkie Pull Request mają przynajmniej jednego recenzenta. Jeśli realizujesz projekt jednoosobowo, rozważ znalezienie innego autora pracującego solo i recenzji kodu handlowego!
  • Pojedyncze polecenie kompiluje, wdraża i uruchamia zestaw testów na Twoim kodzie przy użyciu programistycznego środowiska Ethereum (patrz: Truffle)
  • Uruchomiłeś swój kod za pomocą podstawowych narzędzi do analizy kodu, takich jak Mythril i Slither, najlepiej przed scaleniem każdego pull request, porównując różnice w danych wyjściowych
  • Solidity nie emituje ŻADNYCH ostrzeżeń kompilatora
  • Twój kod jest dobrze udokumentowany

Można powiedzieć o wiele więcej na temat procesu rozwoju, ale te elementy są dobre do rozpoczęcia. Aby uzyskać więcej elementów i szczegółowe wyjaśnienia, zobacz listę kontrolną jakości procesu dostarczoną przez DeFiSafety. DefiSafety jest nieoficjalnym publicznym serwisem publikującym recenzje różnych dużych, publicznych aplikacji zdecentralizowanych Ethereum. Część systemu oceny programu DeFiSafety obejmuje zakres, w jakim projekt jest zgodny z tą listą kontrolną jakości procesów. Postępując zgodnie z tymi procesami:

  • Stworzysz bardziej bezpieczny kod, wykonując odtwarzalne, automatyczne testy
  • Audytorzy będą mogli skuteczniej przeglądać Twój projekt
  • Łatwiejsze wdrożenie nowych deweloperów
  • Pozwala deweloperom na szybkie iterowanie, testowanie i uzyskiwanie informacji zwrotnych na temat modyfikacji
  • Mniej prawdopodobne, że Twój projekt doświadcza regresji

Ataki i podatność na zagrożenia

Teraz, gdy piszesz kod Solidity za pomocą wydajnego procesu rozwoju, przyjrzyjmy się pewnym powszechnym słabościom Solidity, aby zobaczyć, co może pójść źle.

Wielobieżność

Wielobieżność jest jedną z największych i najistotniejszych kwestii bezpieczeństwa, które należy rozważyć podczas opracowywania inteligentnych kontraktów. Chociaż EVM nie może uruchamiać wielu kontraktów w tym samym czasie, kontrakt wywołujący inny kontrakt wstrzymuje wykonywanie kontraktu wywołującego i stan pamięci do momentu zwrotu wywołania, w którym to momencie wykonanie przebiega normalnie. Ta przerwa i ponowne uruchomienie może stworzyć podatność znaną jako „wielobieżność”.

Oto prosta wersja kontraktu, która jest podatna na wielobieżność:

1//TEN KONTRAKT JEST CELOWO PODATNY, NIE KOPIOWAĆ
2contract Victim {
3 mapping (address => uint256) public balances;
4
5 function deposit() external payable {
6 balances[msg.sender] += msg.value;
7 }
8
9 function withdraw() external {
10 uint256 amount = balances[msg.sender];
11 (bool success, ) = msg.sender.call.value(amount)("");
12 require(success);
13 balances[msg.sender] = 0;
14 }
15}
16
Pokaż wszystko
📋 Kopiuj

Aby zezwolić użytkownikowi na wycofanie ETH, który wcześniej przechowywał w kontrakcie, funkcja ta

  1. Odczytuje, ile salda ma użytkownik
  2. Wysyła mu kwotę tego salda w ETH
  3. Resetuje saldo do do 0, więc nie może ponownie wypłacić swojego salda.

W przypadku wywołania ze zwykłego konta (takiego jak własne konto Metamask), działa to zgodnie z oczekiwaniami: msg.sender.call.value() po prostu wysyła ETH z twojego konta. Inteligentne kontrakty mogą jednak również wywoływać połączenia. Jeśli niestandardowy, złośliwy kontrakt jest tym, który wywołuje withdraw(), msg.sender.call.value() nie tylko wyśle amount w ETH, ale będzie także niejawnie wywoływać kontrakt, aby rozpocząć wykonywanie kodu. Wyobraź sobie, że ten złośliwy kontrakt:

1contract Attacker {
2 function beginAttack() external payable {
3 Victim(VICTIM_ADDRESS).deposit.value(1 ether)();
4 Victim(VICTIM_ADDRESS).withdraw();
5 }
6
7 function() external payable {
8 if (gasleft() > 40000) {
9 Victim(VICTIM_ADDRESS).withdraw();
10 }
11 }
12}
13
Pokaż wszystko
📋 Kopiuj

Wywołanie Attacker.beginAttack() rozpocznie cykl, który wygląda następująco:

10.) Atakujący EOA wywołuje Attacker.beginAttack() z 1 ETH
20.) Attacker.beginAttack() deponuje 1 ETH na rzecz ofiary
3
4 1.) Atakujący -> Victim.withdraw()
5 1.) Ofiara odczytuje balanceOf[msg.sender]
6 1.) Ofiara wysyła ETH do atakującego (który wykonuje domyślną funkcję)
7 2.) Atakujący -> Victim.withdraw()
8 2.) Ofiara odczytuje balanceOf[msg.sender]
9 2.) Ofiara wysyła ETH do atakującego (który wykonuje domyślną funkcję)
10 3.) Atakujący -> Victim.withdraw()
11 3.) Ofiara odczytuje balanceOf[msg.sender]
12 3.) Ofiara wysyła ETH do atakującego (który wykonuje domyślną funkcję)
13 4.) Atakujący nie ma już wystarczającej ilości gazu, zwraca bez ponownego wywołania
14 3.) balances[msg.sender] = 0;
15 2.) balances[msg.sender] = 0; (już było 0)
16 1.) balances[msg.sender] = 0; (już było 0)
17
Pokaż wszystko

Wywołanie Attacker.beginAttack z 1 ETH spowoduje atak ponownego wejścia na ofiarę, wycofanie więcej ETH niż zostało dostarczone (pobrane z sald innych użytkowników, powodując, że kontrakt ofiary stanie się niewystarczająco zabezpieczony)

Jak radzić sobie z wielobieżnością (niewłaściwy sposób)

Można rozważyć pokonanie wielobieżności po prostu uniemożliwiając jakimkolwiek inteligentnym kontraktom interakcję z Twoim kodem. Wyszukujesz stackoverflow, znajdujesz ten fragment kodu z mnóstwem głosów za:

1function isContract(address addr) internal returns (bool) {
2 uint size;
3 assembly { size := extcodesize(addr) }
4 return size > 0;
5}
6
📋 Kopiuj

Wydaje się to mieć sens: kontrakty mają kod, jeśli wywołujący ma jakiś kod, nie zezwalaj na wpłatę. Dodajmy to:

1// TEN KONTRAKT JEST CELOWO PODATNY, NIE KOPIOWAĆ
2contract ContractCheckVictim {
3 mapping (address => uint256) public balances;
4
5 function isContract(address addr) internal returns (bool) {
6 uint size;
7 assembly { size := extcodesize(addr) }
8 return size > 0;
9 }
10
11 function deposit() external payable {
12 require(!isContract(msg.sender)); // <- NEW LINE
13 balances[msg.sender] += msg.value;
14 }
15
16 function withdraw() external {
17 uint256 amount = balances[msg.sender];
18 (bool success, ) = msg.sender.call.value(amount)("");
19 require(success);
20 balances[msg.sender] = 0;
21 }
22}
23
Pokaż wszystko
📋 Kopiuj

Teraz, aby zdeponować ETH, nie wolno Ci mieć kodu inteligentnego kontraktu pod swoim adresem. Można to jednak łatwo pokonać za pomocą następującego kontraktu atakującego:

1contract ContractCheckAttacker {
2 constructor() public payable {
3 ContractCheckVictim(VICTIM_ADDRESS).deposit(1 ether); // <- New line
4 }
5
6 function beginAttack() external payable {
7 ContractCheckVictim(VICTIM_ADDRESS).withdraw();
8 }
9
10 function() external payable {
11 if (gasleft() > 40000) {
12 Victim(VICTIM_ADDRESS).withdraw();
13 }
14 }
15}
16
Pokaż wszystko
📋 Kopiuj

Podczas gdy pierwszy atak był atakiem na logikę kontraktu, ten jest atakiem atak na zachowanie wdrożenia kontraktu Ethereum. Podczas budowy kontrakt nie zwrócił jeszcze swojego kodu do wdrożenia pod jego adresem, ale zachowuje pełną kontrolę EVM PODCZAS tego procesu.

Z technicznego punktu widzenia możliwe jest zapobieżenie wywołaniu Twojego kodu przez inteligentne kontrakty, używając tego wiersza:

1require(tx.origin == msg.sender)
2
📋 Kopiuj

Nadal jednak nie jest to dobre rozwiązanie. Jednym z najbardziej ekscytujących aspektów Ethereum jest możliwość komponowania, inteligentne kontrakty integrują się ze sobą i nadbudowują się wzajemnie. Korzystając z powyższej linii, ograniczasz użyteczność swojego projektu.

Jak radzić sobie z wielobieżnością (właściwy sposób)

Po prostu zmieniając kolejność aktualizacji pamięci masowej i wywołania zewnętrznego, zapobiegamy warunkowi wielobieżności, który umożliwił atak. Wywołanie zwrotne wycofania jest wprawdzie możliwe, ale atakujący na tym nie skorzysta, ponieważ pamięć balances będzie już ustawiona na 0.

1contract NoLongerAVictim {
2 function withdraw() external {
3 uint256 amount = balances[msg.sender];
4 balances[msg.sender] = 0;
5 (bool success, ) = msg.sender.call.value(amount)("");
6 require(success);
7 }
8}
9
📋 Kopiuj

Powyższy kod jest zgodny z wzorcem projektu „Checks-Effects-Interaction”, który pomaga chronić przed wielobieżnością. Możesz przeczytać więcej na temat interakcji Checks-Effects-Interactions tutaj

Jak radzić sobie z wielobieżnością (opcja nuklearna)

Za każdym razem, gdy wysyłasz ETH na niezaufany adres lub wchodzisz w interakcje z nieznanym kontraktem (np. wywołanie transfer() adresu tokenów dostarczonego przez użytkownika), otwiera się na możliwość wielobieżności. Projektując kontrakty, które nie wysyłają ETH ani nie wywołują niezaufanych kontraktów, zapobiegasz możliwości wielobieżności!

Więcej rodzajów ataków

Powyższe rodzaje ataków obejmują problemy z kodowaniem inteligentnych kontraktów (wielobieżność) i osobliwości Ethereum (działający kod wewnątrz konstruktorów, zanim kod będzie dostępny pod adresem kontraktowym). Istnieje wiele, wiele innych rodzajów ataków, o których należy wiedzieć, takich jak:

  • Front-running
  • Odrzucenie wysyłania ETH
  • Przeładowanie/niedomiar liczby całkowitej

Dalsza lektura:

Narzędzia bezpieczeństwa

Chociaż nic nie zastąpi zrozumienia podstaw bezpieczeństwa Ethereum i zaangażowania profesjonalnej firmy audytorskiej w sprawdzenie kodu, dostępnych jest wiele narzędzi, które pomogą wskazać potencjalne problemy w kodzie.

Bezpieczeństwo kontraktów inteligentnych

Slither — framework analizy statycznej Solidity napisany w Pythonie 3.

MythX — API analizy bezpieczeństwa dla inteligentnych kontraktów Ethereum.

Mythril — narzędzie do analizy bezpieczeństwa dla kodu bajtowego EVM.

SmartContract.Codes — wyszukiwarka sprawdzonych kodów źródłowych Solidity.

Manticore — interfejs wiersza poleceń, który wykorzystuje symboliczne narzędzie do wykonywania inteligentnych kontraktów i plików binarnych.

Securify — skaner bezpieczeństwa dla inteligentnych kontraktów Ethereum.

Weryfikator ERC20 — narzędzie weryfikacji używane do sprawdzenia, czy kontrakt jest zgodny ze standardem ERC20.

Weryfikacja formalna

Informacje na temat weryfikacji formalnej

Korzystanie z narzędzi

Dwa najpopularniejsze narzędzia do analizy bezpieczeństwa inteligentnych kontraktów to:

Oba są użytecznymi narzędziami, które analizują Twój kod i zgłaszają problemy. Każdy ma wersję [commercial] hostowaną, ale są również dostępne za darmo do uruchomienia lokalnie. Poniżej znajduje się szybki przykład jak uruchomić Slither, który jest dostępny w wygodnym obrazie Docker trailofbits/eth-security-toolbox. Będziesz musiał zainstalować Docker, jeśli jeszcze go nie masz.

$ mkdir test-slither
$ curl https://gist.githubusercontent.com/epheph/460e6ff4f02c4ac582794a41e1f103bf/raw/9e761af793d4414c39370f063a46a3f71686b579/gistfile1.txt > bad-contract.sol
$ docker run -v `pwd`:/share -it --rm trailofbits/eth-security-toolbox
docker$ cd /share
docker$ solc-select 0.5.11
docker$ slither bad-contract.sol

Wygeneruje ten wynik:

ethsec@1435b241ca60:/share$ slither bad-contract.sol
INFO:Detectors:
Reentrancy in Victim.withdraw() (bad-contract.sol#11-16):
External calls:
- (success) = msg.sender.call.value(amount)() (bad-contract.sol#13)
State variables written after the call(s):
- balances[msg.sender] = 0 (bad-contract.sol#15)
Reference: https://github.com/crytic/slither/wiki/Detector-Documentation#reentrancy-vulnerabilities
INFO:Detectors:
Low level call in Victim.withdraw() (bad-contract.sol#11-16):
- (success) = msg.sender.call.value(amount)() (bad-contract.sol#13)
Reference: https://github.com/crytic/slither/wiki/Detector-Documentation#low-level-calls
INFO:Slither:bad-contract.sol analyzed (1 contracts with 46 detectors), 2 result(s) found
INFO:Slither:Use https://crytic.io/ to get access to additional detectors and Github integration
Pokaż wszystko

Slither zidentyfikował tutaj możliwość wielobieżności, identyfikując kluczowe linie, w których może wystąpić problem, i podając nam link do dalszych szczegółów na temat problemu:

Piśmiennictwo: https://github.com/crytic/slither/wiki/Detector-Documentation#reentrancy-vulnerabilities

pozwalający szybko dowiedzieć się o potencjalnych problemach z kodem. Podobnie jak wszystkie zautomatyzowane narzędzia testowe, Slither nie jest doskonały i popełnia zbyt wiele błędów, jeśli chodzi o raportowanie. Może ostrzec przed potencjalną wielobieżnością nawet jeśli nie ma podatności na zagrożenia. Często przeglądanie RÓŻNICY w danych wyjściowych Slither między zmianami w kodzie jest niezwykle pouczające, pomagając odkryć luki w zabezpieczeniach, które zostały wprowadzone znacznie wcześniej niż w przypadku czekania na ukończenie projektu.

Dalsza lektura

Przewodnik po najlepszych praktykach bezpieczeństwa dotyczących inteligentnych kontraktów

Standard weryfikacji bezpieczeństwa inteligentnych kontraktów (SCSVS)

Znasz jakieś zasoby społeczności, które Ci pomogły? Wyedytuj tę stronę i dodaj je!