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