合约安全

合约安全:访问私有数据

2022-12-01  本文已影响0人  梁帆

private关键词定义的函数和状态变量只对定义它的合约可见,该合约派生的合约都不能调用和访问该函数及状态变量。那么,我们能访问被private限定的变量吗?
首先我们详解一下storage存储。

一、storage

1.普通变量

storage存储方式图

2.数组

(1)定长数组(长度固定):

定长数组中的每个元素都会有一个独立的插槽来存储。以一个含有三个 uint64 元素的定长数组为例,下图可以清楚的看出其存储方式:


定长数组存储方式

(2)变长数组(长度随元素的数量而改变):

变长数组的存储方式就很奇特,在遇到变长数组时,会先启用一个新的插槽 slotA 用来存储数组的长度,其数据存储在另外的编号为 slotV 的插槽中。slotA 表示变长数组声明的位置,用 length 表示变长数组的长度,用 slotV 表示变长数组数据存储的位置,用 value 表示变长数组某个数据的值,用 index 表示 value 对应的索引下标,则

length = sload(slotA)
slotV = keccak256(slotA) + index
value = sload(slotV)

变长数组在编译期间无法知道数组的长度,没办法提前预留存储空间,所以 Solidity 就用 slotA 位置存储了变长数组的长度。

我们写一个简单的例子来验证上面描述的变长数组的存储方式:

pragma solidity ^0.8.0;

contract haha{
  
  uint[] user;

  function addUser(uint a) public returns (bytes memory){
    user.push(a);
    return abi.encode(user);
  }
}
0x
0000000000000000000000000000000000000000000000000000000000000020
0000000000000000000000000000000000000000000000000000000000000001
0000000000000000000000000000000000000000000000000000000000000001
然后在Remix的debugger页面, storage
0x
0000000000000000000000000000000000000000000000000000000000000020
0000000000000000000000000000000000000000000000000000000000000002
0000000000000000000000000000000000000000000000000000000000000001
0000000000000000000000000000000000000000000000000000000000000002

然后在Remix的debugger页面,

storage
前面两个插槽的值跟上面是一样的,这里我们可以看到新的插槽为:
0x6c13d8c1c5df666ea9ca2a428504a3776c8ca01021c3a1524ca7d765f600979a
这个值等于:
sha3("0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e564")
插槽编号为: key=0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e564
这个值等于:
sha3("0x0000000000000000000000000000000000000000000000000000000000000000")+1
插槽中存储的数据为:
value=0x0000000000000000000000000000000000000000000000000000000000000002
也就是 16 进制表示的 2 ,也就是我们传入的值。
0x
0000000000000000000000000000000000000000000000000000000000000020
0000000000000000000000000000000000000000000000000000000000000003
0000000000000000000000000000000000000000000000000000000000000001
0000000000000000000000000000000000000000000000000000000000000002
0000000000000000000000000000000000000000000000000000000000000005

然后在Remix的debugger页面,

storage
最新的插槽为:
0x63d75db57ae45c3799740c3cd8dcee96a498324843d79ae390adc81d74b52f13
这个值等于:
sha3("0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e565")
插槽编号为: key=0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e565
这个值等于:
sha3("0x0000000000000000000000000000000000000000000000000000000000000000")+2
插槽中存储的数据为:
value=0x0000000000000000000000000000000000000000000000000000000000000005
也就是 16 进制表示的 5 ,也就是我们传入的值。

二、漏洞

有这样的一个合约:

contract Vault {
    uint public count = 123;
    address public owner = msg.sender;
    bool public isTrue = true;
    uint16 public u16 = 31;
    bytes32 private password;
    uint public constant someConst = 123;
    bytes32[3] public data;

    struct User {
        uint id;
        bytes32 password;
    }
    User[] private users;
    mapping(uint => 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;
    }

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

    function getMapLocation(uint slot, uint key) public pure returns (uint) {
        return uint(keccak256(abi.encodePacked(key, slot)));
    }
}

由上面的合约代码我们可以看到,Vault 合约将用户的用户名和密码这样的敏感数据记录在了合约中,由前置知识中我们可以了解到,合约中修饰变量的关键字仅限制其调用范围,这也就间接证明了合约中的数据均是公开的,可任意读取的,将敏感数据记录在合约中是不安全的。
下面我们就带大家来读取这个合约中的数据。
首先我们使用账户0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266来部署合约,部署合约的时候,我们输入了下面的password:

  const password = ethers.utils.formatBytes32String("share123");
  const test = await Test.deploy(password);

用hardhat写了如下的task:

task("test-transaction", "This task is broken")
    .setAction(async () => {
        const contractAddress = "0x5FbDB2315678afecb367f032d93F642f64180aa3";

        const provider = await ethers.getDefaultProvider("http://127.0.0.1:8545");
        const slot0 = await provider.getStorageAt(contractAddress, "0x0");
        console.log("slot0: ", slot0);
    });

我们可以用getStorageAt来读取slot0的数据,最后输出的结果是:

这个16进制的7b换算成10进制,就是123,也就是我们合约里的count变量值,它是uint256类型,总共256位,16进制需要64位数字。
我们再往下读取slot1:

从后往前看,首先是owner即0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266,它是address类型的,占了160位,16进制需要40位数字;
其次是isTrue,它是bool类型的,占8位,16进制需要2位数字,这里即01
再就是u16,它是uint16类型的,占16位,16进制需要4位数字,即001f,换算成10进制就是31,前面剩余位数都补零。

按照合约中写的,再往下就是bytes32类型的password,它是private类型的,占了32字节,所以下一个slot2就是这个password,输出:
-slot2:0x7368617265313233000000000000000000000000000000000000000000000000

我们把它转成string:

console.log("password: ", ethers.utils.parseBytes32String(slot2));

输出:

password:  share123

可以看到,我们成功得到了隐私变量password,它的值和传入的password是一样的,都是share123

三、预防手段

不要将任何敏感数据存放在合约中,因为合约中的任何数据都可被读取。常见的敏感数据比如秘钥,游戏通关口令等。

上一篇 下一篇

猜你喜欢

热点阅读