漏洞原理

以太坊虚拟机 (EVM) 为整数指定固定大小的数据类型。这意味着一个整数变量,只能表示一定范围的数字。例如uint8 只能存储 [0,255] 范围内的数字。尝试将 256 存储到 uint8 将导致 0。如果不小心,用户输入未被检查,并且执行的计算结果超出了存储它们的数据类型的范围,那么 Solidity 中的变量可能会被利用。整数溢出漏洞有上溢和下溢两种情形。

上溢

整数上溢是指数字的增量超过其能存储的最大值。如对于 uint256 类型的变量,Solidity 可以处理多达 256 个比特位的数值 (最大值是 2256 - 1),所以如果在最大数上增加 1 会导致 0。如下所示:

	0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
+ 	0x000000000000000000000000000000000001
------------------------------------------
= 	0x000000000000000000000000000000000000

下溢

同样,在相反的情况下,当数字是无符号的时,递减将会下溢该数字,从而得到可能的最大值。如下所示:

	0x000000000000000000000000000000000000
- 	0x000000000000000000000000000000000001
------------------------------------------
= 	0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF

安全隐患

当执行一个操作需要一个固定大小的变量来存储变量的数据类型范围之外的数字时,就会发生上溢或下溢。

例如,从一个存储值 为 0 的 uint8 (8 位无符号整数) 变量中减去 1 时,将得到数字 255,这是一个下溢。我们指定了一个低于 uint8 范围的数字,结果是给出了一个 uint8 可以存储的最大数字。同样的,从一个存储值 为 255 的 uint8 变量中增加 1 时,将得到数字 0,这是一个上溢。我们指定了一个高于 uint8 范围的数字,结果是给出了一个 uint8 可以存储的最小数字。虽然这两种情况都很危险,但在现实场景中下溢情况更有可能发生。

让我们来看下面这个合约例子:

// SPDX-License-Identifier: MIT
pragma solidity >=0.4.18;

// contract to test uint8 integer underflows and overflows
contract OverFlowUnderFlow {
    uint8 public a = 0;
    uint8 public b = 2**8-1;

    // a will end up at 2**8-1
    function underflow() public {
        a -= 1;
    }

    // b will end up at 0
    function overflow() public {
        b += 1;
    }
}

我们利用 remix 工具来编译和部署这个合约,然后看看发生了什么。部署好合约后,a 的默认值是 0,b 的默认值是 255。当我们执行 overflow 函数后,b 的值变成了 0,执行 underflow 函数后,a 的值变成了 255。如下图所示:
在这里插入图片描述
这个结果正如我们预料的那样。当执行 overflow 函数时发生了整数上溢,当执行 underflow 函数时发生了整数下溢。这个演示是在 Solidity 0.4.18 编译器版本上进行的。实际上,如果不指定编译器版本,Remix 将使用最新的 Solidity 编译器(也就是 0.8.7 版本)来编译合约。那么当我们执行 overflowunderflow 时编译器会报错且不会出现溢出的结果,也就是说,目前新的 Solidity 编译器版本已更新了这个漏洞。

解决方法

我们建议使用 OpenZeppelin 的 SafeMath 库来解决整数溢出问题。OppenZepplin 在构建和审计安全库方面做得很好,特别是他们的安全数学库是一个用来避免溢出漏洞的参考或库,且已称为一个标准。

下面是 SafeMath 合约安全库的源码:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

library SafeMath {
  function mul(uint256 a, uint256 b) internal pure returns (uint256) {
    if (a == 0) {
      return 0;
    }
    uint256 c = a * b;
    assert(c / a == b);
    return c;
  }

  function div(uint256 a, uint256 b) internal pure returns (uint256) {
    // assert(b > 0); // Solidity automatically throws when dividing by 0
    uint256 c = a / b;
    // assert(a == b * c + a % b); // There is no case in which this doesn't hold
    return c;
  }

  function sub(uint256 a, uint256 b) internal pure returns (uint256) {
    assert(b <= a);
    return a - b;
  }

  function add(uint256 a, uint256 b) internal pure returns (uint256) {
    uint256 c = a + b;
    assert(c >= a);
    return c;
  }
}

下面是经我们修复后的合约例子:

// SPDX-License-Identifier: MIT
pragma solidity >=0.4.18;
import "./SafeMath.sol";

// contract to test uint8 integer underflows and overflows
// fixed by using SafeMath
contract OverFlowUnderFlowFixed {
    using SafeMath for uint;
    uint public a = 0;
    uint public b = 2**256-1;

    // will throw
    function underflow() public {
        a = a.sub(1);
    }

    // will throw
    function overflow() public {
        b = b.add(1);
    }
}

同样的,我们还是利用 remix 工具来编译和部署这个合约,然后看看会发生什么。实际上,经过安全修复后的合约,当我们执行 overflow 函数和 underflow 函数时,Solidity 编译器会抛出错误但不会改变 ab 的值。

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐