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(tx.origin), "GatekeeperOne: invalid gateThree part three"); _; }
function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) { entrant = tx.origin; return true; } }
合约分析
gateOne,通过另一个合约调用即可
gateTwo,需要满足 gasleft() % 8191 == 0
先对合约进行debug,将燃料限制调到999999
attack后,去etherscan中看一下debug trace
因为第二个条件执行了gasleft(),我们需要找一下Gas操作:获取剩余可执行燃料数
由于Gas本身的操作也是消耗燃气的,所以958082才是gas操作获得的剩余可执行燃气数
958082%8191=7926
999999-7926=992073
再将燃料限制调到992073
950280%8191=124
992073-124=991949
再将燃料限制调到991949
950158%8191=2
991949-2=991947
再将燃料限制调到991947
此时已经没有revert
且950156%8191=0
gateThree,先了解一下 Solidity 的类型转换规则
转换成更小的类型,会丢失高位。
1 2
uint32 a = 0x12345678; uint16 b = uint16(a); // b = 0x5678
转换成更大的类型,将向左侧添加填充位。
1 2
uint16 a = 0x1234; uint32 b = uint32(a); // b = 0x00001234
转换到更小的字节类型,会丢失后面数据。
1 2
bytes2 a = 0x1234; bytes1 b = bytes1(a); // b = 0x12
转换为更大的字节类型时,向右添加填充位。
1 2
bytes2 a = 0x1234; bytes4 b = bytes4(a); // b = 0x12340000
只有当字节类型和int类型大小相同时,才可以进行转换。
1 2 3 4 5
bytes2 a = 0x1234; uint32 b = uint16(a); // b = 0x00001234 uint32 c = uint32(bytes4(a)); // c = 0x12340000 uint8 d = uint8(uint16(a)); // d = 0x34 uint8 e = uint8(bytes1(a)); // e = 0x12
把整数赋值给整型时,不能超出范围,发生截断,否则会报错。
1 2 3
uint8 a = 12; // no error uint32 b = 1234; // no error uint16 c = 0x123456; // error, 有截断,变为 0x3456
// string public constant name = 'NaughtCoin'; // string public constant symbol = '0x0'; // uint public constant decimals = 18; uint public timeLock = now + 10 * 365 days; uint256 public INITIAL_SUPPLY; address public player;
function transferFrom( address from, address to, uint256 amount ) public virtual override returns (bool) { address spender = _msgSender(); _spendAllowance(from, spender, amount); _transfer(from, to, amount); return true; } ...
function _transfer( address from, address to, uint256 amount ) internal virtual { require(from != address(0), "ERC20: transfer from the zero address"); require(to != address(0), "ERC20: transfer to the zero address");
function _approve( address owner, address spender, uint256 amount ) internal virtual { require(owner != address(0), "ERC20: approve from the zero address"); require(spender != address(0), "ERC20: approve to the zero address");
// SPDX-License-Identifier: MIT pragma solidity ^0.6.0;
contract Preservation {
// public library contracts address public timeZone1Library; address public timeZone2Library; address public owner; uint storedTime; // Sets the function signature for delegatecall bytes4 constant setTimeSignature = bytes4(keccak256("setTime(uint256)"));
// set the time for timezone 1 function setFirstTime(uint _timeStamp) public { timeZone1Library.delegatecall(abi.encodePacked(setTimeSignature, _timeStamp)); }
// set the time for timezone 2 function setSecondTime(uint _timeStamp) public { timeZone2Library.delegatecall(abi.encodePacked(setTimeSignature, _timeStamp)); } }
// Simple library contract to set the time contract LibraryContract {
// stores a timestamp uint storedTime;
function setTime(uint _time) public { storedTime = _time; } }
// public library contracts address public timeZone1Library; address public timeZone2Library; address public owner; uint storedTime; // Sets the function signature for delegatecall bytes4 constant setTimeSignature = bytes4(keccak256("setTime(uint256)"));
// set the time for timezone 1 function setFirstTime(uint _timeStamp) public { timeZone1Library.delegatecall(abi.encodePacked(setTimeSignature, _timeStamp)); }
// set the time for timezone 2 function setSecondTime(uint _timeStamp) public { timeZone2Library.delegatecall(abi.encodePacked(setTimeSignature, _timeStamp)); } }
// Simple library contract to set the time contract LibraryContract {
// stores a timestamp uint storedTime;
function setTime(uint _time) public { storedTime = _time; } }
contract exploit { address public timeZone1Library; address public timeZone2Library; address public owner;
.code PUSH 80 contract C {\r\n} PUSH 40 contract C {\r\n} MSTOREcontractC {\r\n} CALLVALUEcontractC {\r\n} DUP1olidity ^ ISZERO a PUSH [tag] 1 a JUMPI a PUSH 0 r DUP1 o REVERT .11;\r\ncontra tag 1 a JUMPDEST a POP contract C {\r\n} PUSH #[$] 0000000000000000000000000000000000000000000000000000000000000000 contract C {\r\n} DUP1contractC {\r\n} PUSH [$] 0000000000000000000000000000000000000000000000000000000000000000 contract C {\r\n} PUSH 0 contract C {\r\n} CODECOPYcontractC {\r\n} PUSH 0 contract C {\r\n} RETURNcontractC {\r\n} .data 0: .code PUSH 80 contract C {\r\n} PUSH 40 contract C {\r\n} MSTOREcontractC {\r\n} PUSH 0 contract C {\r\n} DUP1contractC {\r\n} REVERTcontractC {\r\n} .data
using SafeMath for uint256; address public partner; // withdrawal partner - pay the gas, split the withdraw address payable public constant owner = address(0xA9E); uint timeLastWithdrawn; mapping(address => uint) withdrawPartnerBalances; // keep track of partners balances
function setWithdrawPartner(address _partner) public { partner = _partner; }
// withdraw 1% to recipient and 1% to owner function withdraw() public { uint amountToSend = address(this).balance.div(100); // perform a call without checking return // The recipient can revert, the owner will still get their share partner.call{value:amountToSend}(""); owner.transfer(amountToSend); // keep track of last withdrawal time timeLastWithdrawn = now; withdrawPartnerBalances[partner] = withdrawPartnerBalances[partner].add(amountToSend); }
// allow deposit of funds receive() external payable {}
// convenience function function contractBalance() public view returns (uint) { return address(this).balance; } }
合约分析
可以使 transfer 失败,也就是把 gas 耗光
使用 assert 失败的话,将会 spend all gas ,这样的话 owner.transfer(amountToSend) 将执行失败
重入漏洞 partner.call.value(amountToSend)() ,利用重入漏洞把 gas 消耗完
using SafeMath for uint256; address public partner; // withdrawal partner - pay the gas, split the withdraw address payable public constant owner = address(0xA9E); uint timeLastWithdrawn; mapping(address => uint) withdrawPartnerBalances; // keep track of partners balances
function setWithdrawPartner(address _partner) public { partner = _partner; }
// withdraw 1% to recipient and 1% to owner function withdraw() public { uint amountToSend = address(this).balance.div(100); // perform a call without checking return // The recipient can revert, the owner will still get their share partner.call{value:amountToSend}(""); owner.transfer(amountToSend); // keep track of last withdrawal time timeLastWithdrawn = now; withdrawPartnerBalances[partner] = withdrawPartnerBalances[partner].add(amountToSend); }
// allow deposit of funds receive() external payable {}
// convenience function function contractBalance() public view returns (uint) { return address(this).balance; } }
function attack() public { instance_add.call(abi.encodeWithSignature("setWithdrawPartner(address)",this)); instance_add.call(abi.encodeWithSignature("withdraw()")); }
contract Dex { using SafeMath for uint; address public token1; address public token2; constructor(address _token1, address _token2) public { token1 = _token1; token2 = _token2; }
function swap(address from, address to, uint amount) public { require((from == token1 && to == token2) || (from == token2 && to == token1), "Invalid tokens"); require(IERC20(from).balanceOf(msg.sender) >= amount, "Not enough to swap"); uint swap_amount = get_swap_price(from, to, amount); IERC20(from).transferFrom(msg.sender, address(this), amount); IERC20(to).approve(address(this), swap_amount); IERC20(to).transferFrom(address(this), msg.sender, swap_amount); }
function add_liquidity(address token_address, uint amount) public{ IERC20(token_address).transferFrom(msg.sender, address(this), amount); }
function get_swap_price(address from, address to, uint amount) public view returns(uint){ return((amount * IERC20(to).balanceOf(address(this)))/IERC20(from).balanceOf(address(this))); }
function approve(address spender, uint amount) public { SwappableToken(token1).approve(spender, amount); SwappableToken(token2).approve(spender, amount); }
function balanceOf(address token, address account) public view returns (uint){ return IERC20(token).balanceOf(account); } }
contract DexTwo { using SafeMath for uint; address public token1; address public token2; constructor(address _token1, address _token2) public { token1 = _token1; token2 = _token2; }
function swap(address from, address to, uint amount) public { require(IERC20(from).balanceOf(msg.sender) >= amount, "Not enough to swap"); uint swap_amount = get_swap_amount(from, to, amount); IERC20(from).transferFrom(msg.sender, address(this), amount); IERC20(to).approve(address(this), swap_amount); IERC20(to).transferFrom(address(this), msg.sender, swap_amount); }
function add_liquidity(address token_address, uint amount) public{ IERC20(token_address).transferFrom(msg.sender, address(this), amount); }
function get_swap_amount(address from, address to, uint amount) public view returns(uint){ return((amount * IERC20(to).balanceOf(address(this)))/IERC20(from).balanceOf(address(this))); }
function approve(address spender, uint amount) public { SwappableTokenTwo(token1).approve(spender, amount); SwappableTokenTwo(token2).approve(spender, amount); }
function balanceOf(address token, address account) public view returns (uint){ return IERC20(token).balanceOf(account); } }
modifier onlyAdmin { require(msg.sender == admin, "Caller is not the admin"); _; }
function proposeNewAdmin(address _newAdmin) external { pendingAdmin = _newAdmin; }
function approveNewAdmin(address _expectedAdmin) external onlyAdmin { require(pendingAdmin == _expectedAdmin, "Expected new admin by the current admin is not the pending admin"); admin = pendingAdmin; }
function upgradeTo(address _newImplementation) external onlyAdmin { _upgradeTo(_newImplementation); } }
contract PuzzleWallet { using SafeMath for uint256; address public owner; uint256 public maxBalance; mapping(address => bool) public whitelisted; mapping(address => uint256) public balances;
function init(uint256 _maxBalance) public { require(maxBalance == 0, "Already initialized"); maxBalance = _maxBalance; owner = msg.sender; }
contract Motorbike { // keccak-256 hash of "eip1967.proxy.implementation" subtracted by 1 bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
struct AddressSlot { address value; }
// Initializes the upgradeable proxy with an initial implementation specified by `_logic`. constructor(address _logic) public { require(Address.isContract(_logic), "ERC1967: new implementation is not a contract"); _getAddressSlot(_IMPLEMENTATION_SLOT).value = _logic; (bool success,) = _logic.delegatecall( abi.encodeWithSignature("initialize()") ); require(success, "Call failed"); }
// Delegates the current call to `implementation`. function _delegate(address implementation) internal virtual { // solhint-disable-next-line no-inline-assembly assembly { calldatacopy(0, 0, calldatasize()) let result := delegatecall(gas(), implementation, 0, calldatasize(), 0, 0) returndatacopy(0, 0, returndatasize()) switch result case 0 { revert(0, returndatasize()) } default { return(0, returndatasize()) } } }
// Fallback function that delegates calls to the address returned by `_implementation()`. // Will run if no other function in the contract matches the call data fallback () external payable virtual { _delegate(_getAddressSlot(_IMPLEMENTATION_SLOT).value); }
// Returns an `AddressSlot` with member `value` located at `slot`. function _getAddressSlot(bytes32 slot) internal pure returns (AddressSlot storage r) { assembly { r_slot := slot } } }
contract Engine is Initializable { // keccak-256 hash of "eip1967.proxy.implementation" subtracted by 1 bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
address public upgrader; uint256 public horsePower;
// Upgrade the implementation of the proxy to `newImplementation` // subsequently execute the function call function upgradeToAndCall(address newImplementation, bytes memory data) external payable { _authorizeUpgrade(); _upgradeToAndCall(newImplementation, data); }
// Restrict to upgrader role function _authorizeUpgrade() internal view { require(msg.sender == upgrader, "Can't upgrade"); }
// Perform implementation upgrade with security checks for UUPS proxies, and additional setup call. function _upgradeToAndCall( address newImplementation, bytes memory data ) internal { // Initial upgrade and setup call _setImplementation(newImplementation); if (data.length > 0) { (bool success,) = newImplementation.delegatecall(data); require(success, "Call failed"); } }
// Stores a new address in the EIP1967 implementation slot. function _setImplementation(address newImplementation) private { require(Address.isContract(newImplementation), "ERC1967: new implementation is not a contract");
// Initializable.sol bool private _initialized; bool private _initializing; modifier initializer() { // If the contract is initializing we ignore whether _initialized is set in order to support multiple // inheritance patterns, but we only do this in the context of a constructor, because in other contexts the // contract may have been reentered. require(_initializing ? _isConstructor() : !_initialized, "Initializable: contract is already initialized");