方案的具体讲解来自《智能合约安全审计入门篇 —— 访问私有数据》,本片文章将在QEasyWeb3测试链上进行数据验证
测试合约
contract Vault {
uint256 public count = 123;
address public owner = msg.sender;
bool public isTrue = true;
uint16 public u16 = 31;
bytes32 private password;
bytes32[3] public data;
struct User {
uint256 id;
bytes32 password;
}
User[] private users;
mapping(uint256 => User) private idToUser;
constructor(bytes32 _password) {
password = _password;
}
function addUser(bytes32 _password) public {
User memory user = User({id: users.length, password: _password});
users.push(user);
idToUser[user.id] = user;
}
}
部署合约
通过https://remix.ethereum.org/ 进行部署,具体操作不做详细讲解
网络配置
QEasyWeb3的网络配置,可以查看《QEasyChain 测试链信息》
类型 | RPC |
---|---|
https | https://qeasyweb3.com |
http | http://qeasyweb3.com |
chain id: 9528
合约操作
合约部署时,构造函数参数bytes32 _password
,需要传入初始化参数,用于写入测试状态变量password
例如:0x4141414242424343430000000000000000000000000000000000000000000001
然后通过addUser
再添加两个测试数据
0x4141414242424343430000000000000000000000000000000000000000000002
0x4141414242424343430000000000000000000000000000000000000000000003
执行完成后,开始slot的读取测试。
读取私有slot数据
为了简化操作,我们直接使用python3 + web3py进行测试脚本的编写, 也可以选用其他web3库,大同小异
我们上面合约部署完的合约地址是0x4398BdBD9eF8bcACc2A41Abc671BF8f428BB4904
测试脚本
import time
from web3 import Web3
w3 = Web3(Web3.HTTPProvider('http://qeasyweb3.com'))
if __name__ == "__main__":
for i in range(0,7):
print("slot"+str(i)+" = " + w3.toHex(w3.eth.getStorageAt("0x4398BdBD9eF8bcACc2A41Abc671BF8f428BB4904",hex(i))))
执行后返回数据如下
slot0 = 0x000000000000000000000000000000000000000000000000000000000000007b
slot1 = 0x000000000000000000001f016bc0e9c6a939f8f6d3413091738665ad1d7d2776
slot2 = 0x4141414242424343430000000000000000000000000000000000000000000001
slot3 = 0x0000000000000000000000000000000000000000000000000000000000000000
slot4 = 0x0000000000000000000000000000000000000000000000000000000000000000
slot5 = 0x0000000000000000000000000000000000000000000000000000000000000000
slot6 = 0x0000000000000000000000000000000000000000000000000000000000000002
- slot0
存储的为状态变量count
的值,等于123
的十六进制 - slot1
由于存储紧凑原则,当前slot存储了3部署数据,从右往左依次是- 状态变量
owner
的值,等于用于部署合约的msg.sender
=0x6BC0E9C6a939f8f6d3413091738665aD1D7d2776
- 状态变量
isTrue
= 01 = true - 状态变量
u16
= 1f = 31
- 状态变量
- slot2
存储的为状态变量password
- slot3
存储的为状态变量data第1个存储值 = data[0] - slot4
存储的为状态变量data第2个存储值 = data[1] - slot5
存储的为状态变量data第3个存储值 = data[2] - slot6
存储的为变长数组状态变量users的长度,当前已执行两次addUser
查看私有变长数组数据
上面我们从slot6中查询到了变长数组users的长度,下面我们继续查看users中存储的数据。
我们先再次回顾下变长数组的存储原则
对于变长数组,会先启用一个新的插槽 slotA 用来存储数组的长度,其数据存储在另外的编号为 slotV 的插槽中。slotA 表示变长数组声明的位置,用 length 表示变长数组的长度,用 slotV 表示变长数组数据存储的位置,用 value 表示变长数组某个数据的值
对应的代码逻辑
length = sload(slotA)
slotV = keccak256(slotA) + index// 索引下标
value = sload(slotV)
依据变长数组的存储以及紧凑打包的原则,所以对于前面两次addUser
的数据存储槽位置为
- user1.id == keccak256(slotA) + 0
- user1.password == keccak256(slotA) + 1
- user2.id == keccak256(slotA) + 2
- user2.password == keccak256(slotA) + 3
我们继续使用python3 + web3py进行验证
先计算keccak256(slotA)
print("slot6-keccak = " +w3.toHex(Web3.keccak(hexstr="0x0000000000000000000000000000000000000000000000000000000000000006")))
得到
0xf652222313e28459528d920b65115c16c04f3efc82aaedc97be59f3f377c0d3f
所以user1和user2的存储槽位置为
- user1.id ==
0xf652222313e28459528d920b65115c16c04f3efc82aaedc97be59f3f377c0d3f
- user1.password ==
0xf652222313e28459528d920b65115c16c04f3efc82aaedc97be59f3f377c0d40
- user2.id ==
0xf652222313e28459528d920b65115c16c04f3efc82aaedc97be59f3f377c0d41
- user2.password ==
0xf652222313e28459528d920b65115c16c04f3efc82aaedc97be59f3f377c0d42
通过测试脚本,获取上面存储槽位置的值
print("user1.id = " + w3.toHex(w3.eth.getStorageAt("0x4398BdBD9eF8bcACc2A41Abc671BF8f428BB4904", "0xf652222313e28459528d920b65115c16c04f3efc82aaedc97be59f3f377c0d3f")))
print("user1.password = " + w3.toHex(w3.eth.getStorageAt("0x4398BdBD9eF8bcACc2A41Abc671BF8f428BB4904", "0xf652222313e28459528d920b65115c16c04f3efc82aaedc97be59f3f377c0d40")))
print("user2.id = " + w3.toHex(w3.eth.getStorageAt("0x4398BdBD9eF8bcACc2A41Abc671BF8f428BB4904", "0xf652222313e28459528d920b65115c16c04f3efc82aaedc97be59f3f377c0d41")))
print("user2.password = " + w3.toHex(w3.eth.getStorageAt("0x4398BdBD9eF8bcACc2A41Abc671BF8f428BB4904", "0xf652222313e28459528d920b65115c16c04f3efc82aaedc97be59f3f377c0d42")))
得到数据如下
user1.id = 0x0000000000000000000000000000000000000000000000000000000000000000
user1.password = 0x4141414242424343430000000000000000000000000000000000000000000002
user2.id = 0x0000000000000000000000000000000000000000000000000000000000000001
user2.password = 0x4141414242424343430000000000000000000000000000000000000000000003
经测试与前面两次addUser
的数据一致
总结
对于定长数组data,每个元素单独一个存储槽,所以data状态变量依次为slot3-5
对于变长数组users,会先启用一个新的插槽 slotA 用来存储数组的长度,其数据存储在另外的编号为 slotV 的插槽中。slotA 表示变长数组声明的位置,用 length 表示变长数组的长度,用 slotV 表示变长数组数据存储的位置,用 value 表示变长数组某个数据的值