美文网首页合约安全
合约安全:访问私有数据

合约安全:访问私有数据

作者: 梁帆 | 来源:发表于2022-12-01 23:06 被阅读0次

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

一、storage

1.普通变量

  • storage 中的数据被永久存储。其以键值对的形式存储在 slot 插槽中。

  • storage 中的数据会被写在区块链中(因此它们会更改状态),这就是为什么使用存储非常昂贵的原因。

  • 占用 256 位插槽的 gas 成本为 20,000 gas。

  • 修改 storage 的值将花费 5,000 gas 。

  • 清理存储插槽时(即将非零字节设置为零),将退还一定量的 gas 。

  • storage 共有 2^256 个插槽,每个插槽 32 个字节数据按声明顺序依次存储,数据将会从每个插槽的右边开始存储,如果相邻变量适合单个 32 字节,然后它们被打包到同一个插槽中否则将会启用新的插槽来存储。

storage存储方式图

2.数组

  • storage 中的数组的存储方式就比较独特了,首先,solidity 中的数组分为两种:

(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);
  }
}
  • 我们输入1,输出:
0x
0000000000000000000000000000000000000000000000000000000000000020
0000000000000000000000000000000000000000000000000000000000000001
0000000000000000000000000000000000000000000000000000000000000001
然后在Remix的debugger页面, storage
  • 其中第一个插槽为(这里存储的是变长数组的长度):
    0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563
    这个值等于:
    sha3("0x0000000000000000000000000000000000000000000000000000000000000000")
    这是一个固定的值,不是随机生成的。
    key = 0 这是当前插槽的编号;
    value = 1 这说明变长数组 user[] 中只有一条数据也就是数组长度为 1 ;

  • 第二个插槽为(这里存储的是变长数组中的数据):
    0x510e4e770828ddbf7f7b00ab00a9f6adaf81c0dc9cc85f1f8249c256942d61d9
    这个值等于:
    sha3("0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563")
    插槽编号为:
    key=0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563
    这个值等于:
    sha3("0x0000000000000000000000000000000000000000000000000000000000000000")+0
    插槽中存储的数据为:
    value=0x0000000000000000000000000000000000000000000000000000000000000001
    也就是 16 进制表示的 1 ,也就是我们传入的值。

  • 我们输入2,输出:

0x
0000000000000000000000000000000000000000000000000000000000000020
0000000000000000000000000000000000000000000000000000000000000002
0000000000000000000000000000000000000000000000000000000000000001
0000000000000000000000000000000000000000000000000000000000000002

然后在Remix的debugger页面,

storage
前面两个插槽的值跟上面是一样的,这里我们可以看到新的插槽为:
0x6c13d8c1c5df666ea9ca2a428504a3776c8ca01021c3a1524ca7d765f600979a
这个值等于:
sha3("0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e564")
插槽编号为: key=0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e564
这个值等于:
sha3("0x0000000000000000000000000000000000000000000000000000000000000000")+1
插槽中存储的数据为:
value=0x0000000000000000000000000000000000000000000000000000000000000002
也就是 16 进制表示的 2 ,也就是我们传入的值。
  • 我们输入5,输出:
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的数据,最后输出的结果是:

  • slot0:0x000000000000000000000000000000000000000000000000000000000000007b

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

  • slot1:0x000000000000000000001f01f39fd6e51aad88f6f4ce6ab8827279cfffb92266

从后往前看,首先是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

三、预防手段

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

相关文章

  • 合约安全:访问私有数据

    private关键词定义的函数和状态变量只对定义它的合约可见,该合约派生的合约都不能调用和访问该函数及状态变量。那...

  • 面向对象(七)

    私有属性 概念:将一些原本公开的属性设置权限, 只能 小范围访问 , 其他地方访问不了1. 保证数据安全 例如...

  • 如何访问智能合约中的私有数据(private 数据)

    不要将任何敏感数据存放在合约中,因为合约中的任何数据都可被读取,包括private 定义私有数据。 在 solid...

  • Android-N(7.0)应用文件限制访问

    android 7.0 新增私有目录访问权限 Google 官方说明: 为了提高私有文件的安全性,面向 Andro...

  • besu合约测试

    验证三个点:共有合约:是否可以进行私有交易私有合约:是否可以进行共有交易私有合约:是否可以部分方进行交易,类似qu...

  • 访问 Android 私有数据

    简评:在开发调试过程中,总会免不了需要访问到应用的私有数据,这里就介绍下如何访问非 root 设备的应用私有数据。...

  • 阿里云构建混合云架构的3种方案

    【背景】 金融证券行业的个别客户,对数据的安全性要求较高,数据库是部署在他们自己的私有云中,通过IP白名单授权访问...

  • 编程语言-java-重要概念

    1、面向对象封装:核心思想就是“隐藏细节”、“数据安全”,将对象不需要让外界访问的成员变量和方法私有化,只提供符合...

  • fabric1.2.1新特效-私有数据

    私有数据 私有数据的应用 在同一通道内某一组织持有私有数据,只要被认证的组织才可以访问。如果为了保持数据的隐私性而...

  • Android复习之旅--ContentProvider

    数据库文件一般是私有的,别的应用程序是没办法访问私有的数据库。而有些应用的需求是需要把自己私有的数据暴露给别的应用...

网友评论

    本文标题:合约安全:访问私有数据

    本文链接:https://www.haomeiwen.com/subject/rgvlfdtx.html