Вот адрес контракта для тех, кто хочет посмотреть как он устроен в блокчейне. А вот его исходный код:
pragma solidity ^0.4.19;
contract NEW_YEARS_GIFT {
string message;
bool passHasBeenSet = false;
address sender;
bytes32 public hashPass;
function () public payable {}
function GetHash(bytes pass) public constant returns(bytes32) {
return sha3(pass);
}
function SetPass(bytes32 hash) public payable {
if ((!passHasBeenSet && (msg.value > 1 ether)) || hashPass == 0x0) {
hashPass = hash;
sender = msg.sender;
}
}
function SetMessage(string _message) public {
if (msg.sender == sender) {
message = _message;
}
}
function GetGift(bytes pass) external payable returns(string) {
if (hashPass == sha3(pass)) {
msg.sender.transfer(this.balance);
return message;
}
}
function Revoce() public payable {
if (msg.sender == sender) {
sender.transfer(this.balance);
message = "";
}
}
function PassHasBeenSet(bytes32 hash) public {
if (msg.sender == sender && hash == hashPass) {
passHasBeenSet = true;
}
}
}
Автор контракта как бы намекает, что он хотел сделать поздравительную открытку с деньгами, но программист он никудышный. Алгоритм данного контракта следующий:
- Вы кладете деньги на контракт, с помощью метода SetPass, при этом задавая хэш SHA-3 вашего пароля, который доступен получателю (как романтично).
- Вы отправляете сообщение получателю с помощью метода SetMessage
- Также можете отказаться от подарка методом Revoce
- А получатель получает деньги и сообщением методом GetGift
Ну не красота ли? Плюс эту картину дополняют три транзакции:
Первые две из них, это:
- Публикация контракта
- Вызов функции SetPass с неким хэшем и пополнением баланса контракта на 1 Эфир.
Заметим, что только одна функция была вызвана.
Третья транзакция это «неудачная» попытка взломать контракт с вызовом метода GetGift и случайным набором данных.
А теперь собственно ловушка:
Давайте внимательно рассмотрим проверку в методе SetPass:
function SetPass(bytes32 hash) public payable {
if ((!passHasBeenSet && (msg.value > 1 ether)) || hashPass == 0x0) {
hashPass = hash;
sender = msg.sender;
}
}
Как видите, она основана на переменной passHasBeenSet, которую необходимо устанавливать отдельным одноименным методом PassHasBeenSet. Этот метод вызван не был, следовательно переменная до сих пор имеет значение false. Причем, данный метод может вызвать только тот, кто сначала вызвал метод SetPass.
То есть, теоретически, любой кто вызовет метод SetPass, с пополнением баланса больше чем 1 Эфир, станет sender'ом. Причем, чтобы уже никто другой не стал им, нужно просто сразу вызвать метод PassHasBeenSet или просто один из методов Revoce/GetGift для вывода денег.
И вроде бы все логично — просто вызываешь эти два метода и 1 Эфир твой. Но, как мы знаем, в Эфириуме есть атака опережением (front-running). Смысл ее в следующем: злоумышленник наблюдает пул ожидающих транзакций и ждёт вашу транзакцию. Как только транзакция, связанная с контрактом, появляется в пуле транзакций, злоумышленник выполняет транзакцию с большей ценой газа. Транзакция злоумышленника пришла последней в текущем раунде, но благодаря наивысшей цене газа она в реальности будет исполнена раньше, чем ваша транзакция.
Как думаете, какой метод исполнит злоумышленник? Конечно же PassHasBeenSet.
Это даст ему возможность избежать смены sender'а и кроме того, все ваши деньги, отосланные методом SetPass, благополучно осядут в контракте. Ну а дальше он просто их выведет.
Будьте внимательны при работе с блокчейнами!
Комментариев нет:
Отправить комментарий