区块链安全常见的攻击分析——存储冲突漏洞(Storage Collision Vulnerability)【16】

  • XUYU
  • 发布于 3天前
  • 阅读 566

1、存储冲突漏洞(StorageCollisionVulnerability)在代理合约模式中,代理合约和逻辑合约共享同一存储空间。当代理合约将调用委托给逻辑合约时,逻辑合约中的变量实际写入的是代理合约的存储槽位。如果代理合约和逻辑合约都在槽位0存储关键数据(例如代理合约存储实现地址,而逻

1、存储冲突漏洞(Storage Collision Vulnerability)

在代理合约模式中,代理合约和逻辑合约共享同一存储空间。当代理合约将调用委托给逻辑合约时,逻辑合约中的变量实际写入的是代理合约的存储槽位。如果代理合约和逻辑合约都在槽位 0 存储关键数据(例如代理合约存储实现地址,而逻辑合约存储访客地址),那么逻辑合约中的写操作会覆盖代理合约中的实现地址。攻击者可以利用这一点,通过调用逻辑合约中的函数(如 foo),将代理合约中的实现地址替换为攻击者控制的地址,从而接管代理合约的控制权。

知识点

代理合约

  1. 代理合约 (Proxy Contract)

    • 像一个「空壳」,存储数据(比如计数器数值),但没有业务逻辑
    • 它的核心功能是:将所有的函数调用转发到逻辑合约
    contract Proxy {
        address public logicContract; // 指向 LogicV1 的地址
        uint public number;          // 存储实际数据
    
        // 将所有函数调用转发到 LogicV1
        fallback() external {
            (bool success, ) = logicContract.delegatecall(msg.data);
            require(success);
        }
    }
  2. 逻辑合约 (Logic Contract)

    • 包含实际的业务逻辑(比如如何修改计数器),但不存储数据
    • 当需要升级时,可以部署一个新的逻辑合约,然后让代理合约指向它。
    contract LogicV1 {
        function getNumber() public view returns (uint) { /* ... */ }
        function addNumber() public { /* ... */ }
    }

delegatecallcall 的主要区别

  1. delegatecall
    • 在调用目标合约时,使用调用者的上下文msg.sender 和存储)执行目标合约的代码。
    • 调用者的存储被修改,目标合约的存储不会改变。
  2. call
    • 调用目标合约时,使用目标合约的上下文,即目标合约的 msg.sender 是调用者。
    • 目标合约的存储被修改,调用者的存储不受影响。

      1.1 漏洞分析

      代理合约(Proxy)和逻辑合约(Logic)都在相同的存储槽(槽位 0)中存储重要变量, 即代理合约中的实现地址(implementation address)和逻辑合约中的访客地址(GuestAddress)。

      1.2 漏洞合约

contract Proxy {
    address public implementation; //slot0

    constructor(address _implementation) {
        implementation = _implementation;
    }

    function testcollision() public {
        bool success;
        (success, ) = implementation.delegatecall(
            abi.encodeWithSignature("foo(address)", address(this))
        );
    }

    function getSlot0() external view returns (bytes32 data) {
        assembly {
            data := sload(0) // 读取 slot 0
        }
    }
}

contract Logic {
    address public GuestAddress; //slot0

    constructor() {
        GuestAddress = address(0x0);
    }

    function foo(address _addr) public {
        GuestAddress = _addr;
    }
}

1.3 攻击分析

1、 初始化代理合约,slot0 位置存储逻辑合约的 implementation 地址。

image.png 2、 slot0 用于保存逻辑合约的地址信息,作为代理合约与逻辑合约的关键连接点。 3、 调用 testCollision 函数,通过代理合约的 delegatecall 调用逻辑合约中的 foo(address) 函数。

image.png 4、 foo 函数将传入的地址赋值给 GuestAddress 变量,而该变量也存储在 slot0 位置。

image.png

image.png 5、 由于代理合约和逻辑合约共享 slot0foo 函数会覆盖 implementation 地址,导致代理合约逻辑被篡改,完成攻击并接管合约。

image.png

1.4 攻击合约

pragma solidity ^0.8.18;

import "forge-std/Test.sol";
import "./Storage-collision.sol";

contract ContractTest is Test {
    Proxy ProxyContract;
    Logic LogicContract;
    address Koko;
    address Aquarius;

    function setUp() public {
        LogicContract = new Logic();
        ProxyContract = new Proxy(address(LogicContract));

        console.log("address:");
        console.log("address(this):", address(this));
        console.log("ProxyContract:", address(ProxyContract));
        console.log("LogicContract:", address(LogicContract));
        console.log("-------------------------------");
    }

    function test() public {
        // 获取proxy的 slot 0 数据
        bytes32 slotdata = ProxyContract.getSlot0();
        console.log(
            "slot0 contract address:",
            address(uint160(uint256(slotdata)))
        );
        ProxyContract.testcollision();
        slotdata = ProxyContract.getSlot0();
        console.log(
            "overwritten slot0 implementation contract address:",
            address(uint160(uint256(slotdata)))
        );
    }
}

1.5 解决方法

1、通过使用 initializer 修饰符,确保逻辑合约只能被正确初始化一次,防止攻击者在未初始化期间利用存储冲突漏洞。

2、采用 EIP-1967 存储布局标准,此标准使用 keccak256("eip1967.proxy.implementation") - 1 作为实现地址的存储槽,确保该槽位不会与逻辑合约中的其他变量发生冲突。OpenZeppelin 的升级合约实现已经基于此标准。

  • 原创
  • 学分: 25
  • 分类: 安全
  • 标签:
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
XUYU
XUYU
0xe409...2a81
江湖只有他的大名,没有他的介绍。