返回论坛

智能合约安全审计:私有数据的“公开秘密”

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

查找币安全研究院

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

查看研究院 研究报告中心
## 引言 在智能合约开发中,`private` 关键字常被开发者视为数据安全的“保险箱”,认为被标记为私有的状态变量无法被外部读取。然而,区块链的本质决定了链上所有数据都是公开透明的。本文将深入剖析 Solidity 中私有数据的存储机制,揭示其可被读取的技术原理,并给出专业的安全审计建议。 ## 数据存储机制:Storage 的底层逻辑 Solidity 中数据存储分为三种类型:`storage`、`memory` 和 `calldata`。其中 `storage` 是永久存储在区块链上的数据区域,以键值对的形式组织在 2^256 个插槽(slot)中,每个插槽大小为 32 字节。操作 `storage` 的 gas 成本较高:占用新插槽需 20,000 gas,修改值需 5,000 gas,清理插槽(将非零值置零)可退还部分 gas。 状态变量按声明顺序依次存储,从 slot 0 开始。若相邻变量能容纳在单个 32 字节内,则打包到同一插槽;否则启用新插槽。数据从插槽右侧开始填充。 ## 数组的存储方式:定长与变长的差异 ### 定长数组 定长数组的每个元素分配独立插槽。例如,一个包含三个 `uint64` 元素的数组,每个元素占用一个插槽。 ### 变长数组 变长数组的存储方式更为复杂: - 声明位置 `slotA` 存储数组长度 `length` - 实际数据存储在 `slotV = keccak256(slotA) + index` 处 - 通过 `sload(slotV)` 读取对应值 由于编译期间无法确定长度,Solidity 用 `slotA` 存储长度信息,数据则通过哈希计算定位。 **验证示例**: ```solidity pragma solidity ^0.8.0; contract haha { uint[] user; function addUser(uint a) public returns (bytes memory) { user.push(a); return abi.encode(user); } } ``` 部署后调用 `addUser(998)`,debug 显示: - slot 0(`keccak256("0x00")`):存储长度值 `1` - slot 1(`keccak256("0x00") + 0`):存储 `0x3e6`(即 998) 再次调用 `addUser(999)`,新增插槽 `keccak256("0x00") + 1`,存储 `0x3e7`(即 999)。 ## 结构体的存储规则 结构体成员按声明顺序依次存储,优先填充当前插槽的剩余空间。例如: ```solidity struct User { uint128 id; uint256 balance; uint64 age; } ``` `id` 占用 slot 0 的低 128 位,`balance` 需新插槽 slot 1,`age` 占用 slot 2。 ## 映射的存储机制 映射 `mapping(KeyType => ValueType)` 的存储基于 `keccak256(abi.encode(key, slot))`,其中 `slot` 为映射声明位置的插槽编号。例如: ```solidity contract Test { mapping(uint => address) public users; uint[] public ids; // users 声明在 slot 0 } ``` 要读取 `users[1]`,需计算 `keccak256(abi.encode(1, 0))`。 ## 实战:读取私有数据 假设合约: ```solidity contract PrivateData { uint256 public publicData = 12345; uint256 private privateData = 67890; struct User { string name; uint256 id; } User private user; mapping(uint => User) private users; constructor() { user = User("Alice", 1); users[0] = User("Bob", 2); } } ``` ### 步骤一:定位插槽 - `publicData` 在 slot 0 - `privateData` 在 slot 1 - `user` 结构体:`name`(动态数组)在 slot 2,`id` 在 slot 3 - `users` 映射在 slot 4 ### 步骤二:读取数据 使用 web3.py 读取: ```python from web3 import Web3 w3 = Web3(Web3.HTTPProvider('http://localhost:8545')) contract_addr = '0x...' slot_0 = w3.eth.get_storage_at(contract_addr, 0) # 12345 slot_1 = w3.eth.get_storage_at(contract_addr, 1) # 67890 ``` ### 步骤三:读取结构体 - `user.name` 长度:`w3.eth.get_storage_at(contract_addr, 2)` → 5("Alice"长度) - `user.name` 数据:`keccak256(abi.encode(2)) + 0` → "Alice" 的 ASCII 编码 - `user.id`:`w3.eth.get_storage_at(contract_addr, 3)` → 1 ### 步骤四:读取映射 - `users[0].name`:`keccak256(abi.encode(0, 4))` → 长度 3("Bob") - `users[0].id`:`keccak256(abi.encode(0, 4)) + 1` → 2 至此,所有“私有”数据均被完整读取。 ## 安全建议 ### 面向开发者 1. **永不存储敏感数据**:私钥、密码、通关密钥等任何敏感信息都不应写入合约,因为链上数据对全节点公开。 2. **使用链下存储**:敏感数据应存储在链下,通过哈希或零知识证明上链验证。 3. **加密处理**:若必须上链,使用强加密算法(如 AES-256-GCM)加密后再存储,但需注意密钥管理风险。 ### 面向审计者 1. **重点检查敏感变量**:扫描合约中标记为 `private` 或 `internal` 的状态变量,评估其是否包含敏感信息。 2. **验证业务逻辑**:确认合约是否依赖私有数据的不可读性来保证安全(如游戏通关口令、访问控制密钥等)。 3. **建议重构**:若发现敏感数据,立即建议开发者迁移至链下方案或加密存储。 ## 结语 Solidity 的 `private` 修饰符仅控制合约间访问权限,无法阻止链上数据读取。区块链的透明性决定了所有状态变量均可被公开读取。安全审计中,必须将“私有数据可读”作为默认假设,杜绝任何基于数据隐藏的安全设计。 --- 本文由查找币安全团队整理发布
在论坛中查看和回复