返回论坛

深入解析delegatecall:从变量覆盖到权限劫持

查找币 学术研究 安全研究 Web3安全 区块链安全

查找币安全研究院

钱包恢复评估 | 链上取证分析 | Web3 事件响应
以合法授权、证据保全、隐私保护和可复核流程为前提,不要求用户在线提交完整私钥或助记词。

查看研究院 研究报告中心
## 背景回顾 在上一篇文章《智能合约安全审计入门篇——delegatecall(1)》中,我们探讨了`delegatecall`的基础原理及其引发的一个典型漏洞。今天,我们将进一步挖掘这一函数的危险性,通过一个进阶案例演示攻击者如何利用存储布局的差异实现权限劫持。这不仅是对知识的巩固,更是一次实战演练。 ## 前置知识速览 `delegatecall`的核心特性在于:它允许目标合约的代码在调用者的存储环境中执行,但变量的修改完全取决于被调用合约的存储布局。这意味着,如果两个合约的存储变量声明顺序不一致,攻击者可以轻易覆盖关键数据。 如果您对`delegatecall`的基础概念已模糊,建议先回顾前文。 ## 漏洞合约示例 我们来看一个精心设计的漏洞合约,攻击目标依然是获取合约的`owner`权限。 ```solidity // Lib.sol contract Lib { uint public someNumber; function doSomething(uint _num) public { someNumber = _num; } } // HackMe.sol contract HackMe { address public lib; address public owner; uint public someNumber; constructor(address _lib) { lib = _lib; owner = msg.sender; } function doSomething(uint _num) public { lib.delegatecall(abi.encodeWithSignature("doSomething(uint256)", _num)); } } ``` 观察`HackMe`合约,只有构造函数能设置`owner`,其他地方并无修改权限的函数。那么,攻击者如何利用`delegatecall`实现权限劫持?这里需要一点巧思。 ## 攻击思路与实现 **攻击流程:** 1. Alice部署`Lib`合约。 2. Alice部署`HackMe`合约,构造函数传入`Lib`地址。 3. Eve部署`Attack`合约,构造函数传入`HackMe`地址。 4. Eve调用`Attack.attack()`,最终将`HackMe`的`owner`修改为自己的地址。 **攻击合约代码:** ```solidity // Attack.sol contract Attack { // 存储布局必须与HackMe一致 address public lib; address public owner; uint public someNumber; HackMe public hackMe; constructor(HackMe _hackMe) { hackMe = HackMe(_hackMe); } function attack() public { // 第一次调用:覆盖lib地址 hackMe.doSomething(uint(uint160(address(this)))); // 第二次调用:触发owner修改 hackMe.doSomething(1); } // 函数签名需与HackMe.doSomething()匹配 function doSomething(uint _num) public { owner = msg.sender; } } ``` ### 攻击原理深度剖析 `delegatecall`的关键特性是:**它根据被调用合约的存储插槽位置修改调用者合约的存储变量**。在`HackMe`中,存储布局为: - slot0: `lib` - slot1: `owner` - slot2: `someNumber` 而`Lib`合约的存储布局仅为: - slot0: `someNumber` 当`HackMe.doSomething()`通过`delegatecall`调用`Lib.doSomething()`时,`Lib`的代码会在`HackMe`的存储环境中执行。`Lib.doSomething()`修改的是`slot0`,即`HackMe`的`lib`变量。攻击者利用这一特性,分两步完成攻击: 1. **第一次调用**:`Attack.attack()`将`Attack`合约地址转换为`uint256`,调用`hackMe.doSomething(uint(uint160(address(this))))`。`HackMe`通过`delegatecall`执行`Lib.doSomething()`,将传入的地址写入`slot0`,即`HackMe`的`lib`变量。至此,`HackMe`的`lib`被替换为`Attack`合约地址。 2. **第二次调用**:`Attack.attack()`再调用`hackMe.doSomething(1)`。此时`HackMe`的`lib`已是`Attack`合约,因此`delegatecall`会执行`Attack.doSomething()`函数。`Attack.doSomething()`修改的是`slot1`(即`owner`变量),因此`HackMe`的`owner`被修改为`msg.sender`,即Eve的地址。攻击完成。 ### 关键点解析 - **存储布局一致性**:攻击合约的存储布局必须与目标合约完全一致,否则可能导致变量覆盖错误。 - **函数签名匹配**:攻击合约中的`doSomething()`函数签名需与`HackMe`的调用一致,否则`delegatecall`会失败。 - **地址转换**:将`address`类型转换为`uint256`是为了兼容`Lib.doSomething()`的参数类型。 ## 修复建议 ### 对开发者的建议 1. **避免可控的delegatecall目标**:确保被调用的合约地址不可被外部篡改,可使用`immutable`或硬编码地址。 2. **统一存储布局**:如果使用`delegatecall`,应确保调用者与被调用合约的存储变量声明顺序一致,或使用显式的`slot`映射。 3. **最小权限原则**:限制`delegatecall`可调用的函数范围,避免暴露敏感状态修改逻辑。 ### 对审计者的建议 1. **重点审查delegatecall目标**:检查合约中是否允许用户控制`delegatecall`的地址,这是高危信号。 2. **验证存储插槽一致性**:当被调用合约存在状态变量修改时,需逐项核对调用者与被调用者的存储布局,防止变量覆盖。 3. **关注函数签名匹配**:确保`delegatecall`调用的函数签名与实际执行逻辑一致,避免意外跳转到恶意函数。 ## 结语 `delegatecall`是Solidity中功能强大但风险极高的机制。通过本次案例,我们展示了攻击者如何利用存储布局差异实现精准的变量覆盖,进而完成权限劫持。理解这一攻击模式,对于编写安全的智能合约至关重要。 **本文由查找币安全团队整理发布**
在论坛中查看和回复