---
# System prepended metadata

title: Как воруют крипту у хакеров?

---

# Как воруют крипту у хакеров?

Авторы: artur.lukianov@cyberok.ru, 

В последнее время блокчейн и Web3 обретают всё большую популярность во всём мире. Это привлекает как исследователей и охотников за баг-баунти, так и злоумышленников. Для одних эта интересная цель для исследователей, для других – возможность законно (или нет) подзаработать - вывести крипту или токены с контракта и обналичить на бирже куда проще и эффективнее чем требовать выкуп.

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

## Поиск уязвимых смартконтрактов

Проводя рутинный обзор смартконтрактов с открытым исходным кодом в рамках методики CyberOK (скоро опубликуем, не беспокойтесь) я проверял последние опубликованные контракты с etherscan.io, автоматически выгружая их с помощью [этой](https://github.com/tintinweb/smart-contract-sanctuary-ethereum) утилиты.

![](https://i.imgur.com/dSJ1788.png)

Многие контракты реализуют ERC20, ERC721, и прочие интерфейсы. Но я нацелился на контракты с нестандартизированными интерфейсами - в них намного проще сделать ошибку.

![](https://i.imgur.com/nLgAQM6.png)

Таких контрактов не так уж и много, их можно просмотреть руками.

## Уязвимый смартконтракт

Один из этих смартконтрактов оказался уязвим к [reentrancy attack](https://habr.com/ru/post/655639/). 

Вот его код:
```solidity=
/**
 *Submitted for verification at Etherscan.io on 2022-08-19
*/

pragma solidity ^0.4.25;

contract PEOPLE_BANK
{
    function Put(uint _unlockTime)
    public
    payable
    {
        var acc = Acc[msg.sender];
        acc.balance += msg.value;
        acc.unlockTime = _unlockTime>now?_unlockTime:now;
        LogFile.AddMessage(msg.sender,msg.value,"Put");
    }

    function Collect(uint _am)
    public
    payable
    {
        var acc = Acc[msg.sender];
        if( acc.balance>=MinSum && acc.balance>=_am && now>acc.unlockTime)
        {
            if(msg.sender.call.value(_am)())
            {
                acc.balance-=_am;
                LogFile.AddMessage(msg.sender,_am,"Collect");
            }
        } 
    }

    function() 
    public 
    payable
    {
        Put(0);
    }

    struct Holder   
    {
        uint unlockTime;
        uint balance;
    }

    mapping (address => Holder) public Acc;

    Log LogFile;

    uint public MinSum = 1 ether;    

    function PEOPLE_BANK(address log) public{
        LogFile = Log(log);
    }
}


contract Log 
{
    struct Message
    {
        address Sender;
        string  Data;
        uint Val;
        uint  Time;
    }

    Message[] public History;

    Message LastMsg;

    function AddMessage(address _adr,uint _val,string _data)
    public
    {
        LastMsg.Sender = _adr;
        LastMsg.Time = now;
        LastMsg.Val = _val;
        LastMsg.Data = _data;
        History.push(LastMsg);
    }
}
```

При вызове `msg.sender.call.value(_am)()` на 26 строчке, если msg.sender - контракт, то он может снова вызвать функцию `Collect()`, который снова вызовет `msg.sender.call.value(_am)()`, который снова... Таким "рекурсивным" образом вывести все деньги с контракта.

Кроме того на этом смартконтракте находилось целых 20 ETH (~1900000 рублей на момент написания статьи): https://etherscan.io/address/0xab2c687ea93662dec2abfe5e5f833e46ec656f8e

Мы быстро сделали PoC и запустили его в testnet. Но что-то пошло не так и вместо ожидаемых эфиров мы увидели скучный revert. 
При дебаге транзакции, изучив вызываемые функции я заметил кое-что подозрительное:
![](https://i.imgur.com/6nz3YJg.png)

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

## Ловушка
В цепочке вызовов обнаружился другой смарт-контракт - Log
Его исходный код так же приводится в файле:
```solidity=
contract Log 
{
    struct Message
    {
        address Sender;
        string  Data;
        uint Val;
        uint  Time;
    }

    Message[] public History;

    Message LastMsg;

    function AddMessage(address _adr,uint _val,string _data)
    public
    {
        LastMsg.Sender = _adr;
        LastMsg.Time = now;
        LastMsg.Val = _val;
        LastMsg.Data = _data;
        History.push(LastMsg);
    }
}
```

А вот и вызов Log:
```solidity
function Collect(uint _am)
    public
    payable
    {
        var acc = Acc[msg.sender];
        if( acc.balance>=MinSum && acc.balance>=_am && now>acc.unlockTime)
        {
            if(msg.sender.call.value(_am)())
            {
                acc.balance-=_am;
                LogFile.AddMessage(msg.sender,_am,"Collect");
            }
        } 
    }
```

Но как этот код может вызвать revert?
Декомпилируем bytecode на etherscan и обнаруживаем странное...
![](https://i.imgur.com/QAQw3cw.png)
```python=
# Palkeoramix decompiler. 

def AddMessage(address _adr, uint256 _val, string _data): # not payable
  mem[128 len _data.length] = _data[all]
  stor1 = _adr
  stor4 = block.timestamp
  stor3 = _val
  uint256(stor2[]) = Array(len=_data.length, data=_data[all])
  stor0.length++
  addr(stor0[stor0.length].field_0) = stor1
  if 31 >= stor2.length:
      stor290D[stor0.length] = stor2.length
      idx = 0
      while stor[(4 * stor0.length) + 0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e564].length + 31 / 32 > idx:
          stor[idx + sha3((4 * stor0.length) + 0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e564)] = 0
          idx = idx + 1
          continue 
  else:
      stor290D[stor0.length] = Mask(255, 1, (256 * not bool(stor2.length)) - 1 and stor2.length) + 1
      if not Mask(255, 1, (256 * not bool(stor2.length)) - 1 and stor2.length):
          idx = 0
          while stor[(4 * stor0.length) + 0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e564].length + 31 / 32 > idx:
              stor[idx + sha3((4 * stor0.length) + 0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e564)] = 0
              idx = idx + 1
              continue 
      else:
          s = 0
          idx = 0
          while stor2.length + 31 / 32 > idx:
              stor[s + sha3((4 * stor0.length) + 0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e564)] = uint256(stor2[idx])
              s = s + 1
              idx = idx + 1
              continue 
          idx = stor2.length + 31 / 32
          while stor[(4 * stor0.length) + 0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e564].length + 31 / 32 > idx:
              stor[idx + sha3((4 * stor0.length) + 0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e564)] = 0
              idx = idx + 1
              continue 
  stor290D[stor0.length] = stor3
  stor290D[stor0.length] = stor4
  if caller == stor5: # !!!!!!
      if stor7 != _adr:
          if stor6 != tx.origin:
              require 0 < _data.length
              if 'C' == Mask(8, 248, mem[128]):
                  require _val <= 0



```

Этой части кода вообще нет в исходнике!
Если мы выводим деньги с контракта (операция начинается с 'C' - то есть 'Collect'), то требуется чтобы выводимых средств было меньше либо равно нулю, за исключением одного адреса (который находится на storage slot 6 - "0x0a494fec232c75df8ba0a781015c2382fef5abc8"). 
Это и мешает проэксплуатировать такую простую уязвимость.
Кроме того, контракт требует перевести на него 1 ETH для попытки эксплуатации. Таким образом, этот контракт выступает как ловушка для незадачливых хакеров, позволяя своему владельцу собирать ETH.

!!!!
Я провёл небольшое расследование - оказалось что мы не первые обнаружили эту ловушку. [Тут (китайский)](https://blog.csdn.net/Fly_hps/article/details/80833798), описали ещё пару подобных контрактов. Мы решили пойти дальше и понять поведение не только контрактов, но и злоумышленников
!!!!

## Расследование
Отследив путь этих 20 ETH, можно заметить что PEOPLE_BANK переехал на новый адрес: https://etherscan.io/address/0xe07e724a96866daae308870d1a5eb41258436b53

![](https://i.imgur.com/H4S5rTq.png)

Взглянем на цепочку транзакций, которая привела ETH на адрес этого контракта, используя утилиту от CyberOK:
![](https://i.imgur.com/OFprdzP.png)

Из этого графа можно сделать следующие выводы:
- Часть средств поступила через миксер
- Существовало две цепочки, которые соеденились некоторое время назад

Подробный анализ этих транзакций раскрывает интересный факт - смартконтракт всё время "переезжает" на новые адреса. Вероятнее всего это было сделано чтобы код контракта отображался в выдаче от etherscan (500 последних verified контрактов). Код контракта не меняется, но его имя образуется как `<случайное слово>_BANK`

![](https://i.imgur.com/kLTGNUS.png)
!!! Две цепочки

Кроме того, существует ещё один контракт, связанный с BANK, находящийся на параллельной цепочке, Game. Его код отличается от BANK и не имеет явных уязвимостей, но схожее поведение (именование, "переезды") и пересечение цепочек транзакций на некоторых адресах, не оставляет сомнений что эти два контракта принадлежат одному злоумышленнику.

Время от времени на эти ханипоты попадается по несколько человек:
https://etherscan.io/address/0xe37b75941d9b8e3139e16a774faa2d9fb1fc9f28
![](https://i.imgur.com/xA9FSTr.png)


Пример контракта Game: https://etherscan.io/address/0xe37b75941d9b8e3139e16a774faa2d9fb1fc9f28#code

Тема ханипотов не нова, и уже была давно описана: https://www.slideshare.net/PolySwarm/smart-contract-honeypots-for-profit-and-fun-bha. Удивительно что эта схема продолжается уже на протяжении более 3 лет.

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


