合约安全之-变量隐藏安全问题分析

  • 小驹
  • 更新于 2022-06-09 15:22
  • 阅读 2057

在计算机编程中,当在特定范围(代码块、方法或内部类)中声明的变量与在外部范围中声明的变量具有相同的名称时,就会发生变量隐藏。变量隐藏在多种计算机语言中都存在,并不仅仅是Solidity语言独有的特性。

1. 原理

在计算机编程中,当在特定范围(代码块、方法或内部类)中声明的变量与在外部范围中声明的变量具有相同的名称时,就会发生变量隐藏。变量隐藏在多种计算机语言中都存在,并不仅仅是Solidity语言独有的特性。

我们把这种同一个合约可能存在多个具有相同名称的变量,这种变量称为影子变量。

  1. 在更复杂的合约系统中,这种情况可能不会引起注意,并且随后可能引起某些安全问题。
  2. 当在合约和函数层级存在多个定义时,影子变量也可能在单个合约内发生。

Solidity支持继承,继承的引入可能会给Solidity 的状态变量进行影子变量的问题。如想像一下这种场景:

带有变量 x 的合约 A 可以继承同样定义了状态变量 x 的合约 B,这将导致 x变量 的两个不同版本。其中一个从合约 A 访问,另一个从合约 B 访问。

这种场景下极容易出现变量隐藏的安全问题。

在Solidity编码中,变量隐藏常出现的场景包括:

  1. 同一个合约中,不同特定作用范围的变量。
  2. 继承关系的多个合约中,不同合约中具有相同名称的变量。

在编写更复杂的合约系统时,要时刻注意上面的两种场景,可能会由于忽视并随后导致变量隐藏安全问题。 对于这两种场景分别用下面的代码进行演示。

2. 代码演示

演示1:不同特定作用范围的变量

pragma solidity 0.4.24;

contract ShadowingInFunctions {
    uint n = 2;
    uint x = 3;

    function test1() constant returns (uint n) {
        return n; // Will return 0
    }

    function test2() constant returns (uint n) {
        n = 1;
        return n; // Will return 1
    }

    function test3() constant returns (uint x) {
        uint n = 4;
        return n+x; // Will return 4
    }
}

上面的三个函数的输出内容为:

instance deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3
test1:0
test2:1
test3:4

在这段代码中,涉及到变量n的地方有多个,总结起来代码中变量n有三种变量类型,分别是:

  • 状态变量n 。这个变量的值为2。
  • 返回值类型的n 。在test1函数和test2函数中,n都是返回值类型的n.
  • 局部变量的n 。在test3函数中,定义了n的局部变量,这个局部变量的值为n。

在test1和test2函数中,返回值类型的n覆盖了合约的状态变量n。所以函数返回的内容都是返回值类型的n的值。

在test3函数中,位于函数内的局部变量n覆盖了合约状态变量n。所以函数返回的内容是局部变量计算出来的n的值。

演示2:继承合约中状态变量的隐藏

可以分析下面的代码,看能否看出哪里有问题?

演示代码Children.sol:

代码的本意是:Children合约继承自Base合约,同时Children合约的withdraw取款操作具有onlyOwner修饰符,期望withdraw取款操作只能由Children合约的部署者才能调用。

//SPDX-License-Identifier: Unlicense
pragma solidity ^0.4.0;

import "hardhat/console.sol";

contract Base {
    address public owner;
    modifier onlyOwner() {
        require(msg.sender==owner,"Only Owner can call the function");
        _;
    }
}
contract Children is Base {
    uint public totalSupply = 100;
    address public owner;

    constructor() public {
        owner = msg.sender;
        console.log("\\nChildren constructor:%s",owner);

    }

    function withdraw(uint _amount) public onlyOwner   {
        totalSupply = totalSupply - _amount;
        // console.log(“aaa”);
    }
}

下面是attack.ts模拟的测试过程,通过npx hardhat run script/attack.ts 进行测试。模拟的操作为:

  1. 使用user1部署Children合约,并输出合约部署的地址,合约的owner,totalSupply信息。
  2. 使用user1调用合约的withdraw方法
  3. 查看totalSupply的值,验证user1的withdraw方法调用成功。
import { ethers, waffle } from "hardhat";

async function main() {
    let user1, user2;
    [user1, user2] = await ethers.getSigners();
    const Children = await ethers.getContractFactory("Children",user1);
    const children = await Children.deploy();

    await children.deployed();
    console.log("deploy at:%s", children.address);
    console.log("contract owner:%s", await children.owner());
    console.log("totalSupply:%s", await children.totalSupply());

    await children.connect(user1).withdraw(10);

    console.log("totalSupply:%s", await children.totalSupply());

}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

在user1调用withdraw方法时,却失败了。错误提示如下: 31.png

0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 地址是Children合约的部署者,却无法调用withdraw方法。

返回源代码分析,Base合约和Children合约都具有owner状态变量,却只有Base合约有onlyOwner修饰符。

  • 对Base合约:没有对owner进行赋值(owner默认为0x0000000000000000000000000000000000000000),却直接定义了onlyOwner的修饰符。
  • 对Children合约:对owner进行赋值(owner默认为0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266),却没有直接定义onlyOwner的修饰符(直接继承使用了Base合约的onlyOwner的修饰符)。

这样的结果就是导致调用Children合约的withdraw方法时,使用的是Base合约的onlyOwner的修饰符,而Base合约的onlyOwner的修饰符检查的自然是Base合约的owner,也就是0x0000000000000000000000000000000000000000。因此就导致了Children合约无法调用withdraw()方法。

所以说,能调用Children合约withdraw()方法的地址只能是0x0000000000000000000000000000000000000000黑洞地址。

本质上还是因为父子合约中都使用了owner变量,使owner合约变成了影子变量,程序员在实现功能时混淆了owner变量的指代。

理解了上面的原因,很简单的两种修改方法:

  1. 给Base合约加上owner的赋值。
    constructor() public {
           owner = msg.sender;
           console.log("\\nBase constructor:%s",owner);
       }
  2. 给Children合约加上OnlyOwner修饰符。
    modifier onlyOwner() {
           require(msg.sender==owner,"Only Owner can call the function");
           _;
       }

3. 安全建议

变量隐藏常出现的两类场景:

  1. 同一个合约中,不同特定作用范围的变量
  2. 继承关系的多个合约中,不同合约中具有相同名称的变量。

对于场景1, 在开发环境中,编辑器会提示如下的影子变量的风险。如在代码开发过程中,遇到下面的提示,建议移除影子变量或重命名影子变量。 32.png

对于 场景2,编译器不会有影子变量的提示,更需要依赖于仔细检查您的合约代码的存储变量的定义,尽量消除任何歧义。

4.参考

Smart Contract Weakness Classification and Test Cases中对变量隐藏的解释

https://swcregistry.io/docs/SWC-119

点赞 1
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
小驹
小驹
0xcD46...3461
weixin: xiaoju521区块链安全分析,欢迎私信沟通交流