Smart contract Overflows Underflows
Đây là phần 2 nằm trong series Smart Contract Security

Bạn có biết Ethereum Virtual Machine sử dụng kích thước cố định (fixed-size) cho dữ liệu integer. Điều này có nghĩa là biến integer trong Solidity chỉ có thể biểu diễn trong một phạm vi giá trị nhất định. Ví dụ, uint8 chỉ có thể chứa giá trị nằm trong khoảng [0,255].

Nếu giá trị của biến uint8 là 255 thì khi tăng lên 1 đơn vị thì giá trị không phải là 256 mà là 1. Hiện tượng này gọi là Overflows.

Nếu giá trị hiện của biến uint8 là 0, bạn giảm đi 1 đơn vị thì giá trị không phải là -1 mà là 255. Hiện tượng này gọi là Underflows.

Lỗ hỗng trong khi tính toán như trên được gọi tên chính xác là Arithmetic Overflows/Underflows: tạm gọi là tràn số trong tính toán số học.

Để rõ hơn thì thì chúng ta cùng xem smart contract trong ví dụ sau. Lưu ý dùng phiên bản Solidity là 0.7.0 trở xuống, phiên bản Solidity từ 0.8.0 đã khắc phục được vấn đề này.

pragma solidity 0.7.0;

contract OverflowUnderflow {
    
    function decrease(uint8 number) public pure returns(uint8) {
        return --number;
    }
  
    function increase(uint8 number) public pure returns(uint8) {
    return ++number;
    }
}

Sau khi compile, bạn có thể thử gọi 2 hàm trên sẽ có kết quả như sau:

Đây chính là lý do chúng ta cần một phương pháp để tính toán an toàn. Một trong những cách để tránh tình trạng trên đó là sử dụng thư viện SafeMath được cung cấp ở OpenZeppelin.

SafeMath:

Thư viện SafeMath của OpenZepplin bạn có thể xem chi tiết tại đây.

Tóm gọn lại thì thư viện SafeMath sẽ viết các function có chức năng cộng, trừ, nhân, chia, chia lấy dư kèm theo là việc kiểm tra overflow và underflow của phép tính.

Chẳng hạn như cách mà thư viện SafeMath xử lý overflow cho phép tính cộng. SafeMath sẽ dùng require kiểm tra xem tổng có lớn hơn số hạng hay không:

function add(uint256 a, uint256 b) internal pure returns (uint256) {
    uint256 c = a + b;
    require(c >= a, "SafeMath: addition overflow");
    return c;
}

Đối với phép tính trừ, SafeMath sẽ kiểm tra: số bị trừ có lớn hơn hoặc bằng số trừ hay không:

function sub(uint256 a, uint256 b) internal pure returns (uint256) {
  require(b <= a, "SafeMath: subtraction overflow");
  return a - b;
}

Phép tính nhân thì kiểm tra điều kiện: tích chia 1 số hạng thì phải bằng số hạng còn lại:

function mul(uint256 a, uint256 b) internal pure returns (uint256) {
  if (a == 0) return 0;
  uint256 c = a * b;
  require(c / a == b, "SafeMath: multiplication overflow");
  return c;
}

Phép tính chia và chia lấy dư thì SafeMath chỉ cần kiểm tra lỗi chia với 0:

function div(uint256 a, uint256 b) internal pure returns (uint256) {
  require(b > 0, "SafeMath: division by zero");
  return a / b;
}
function mod(uint256 a, uint256 b) internal pure returns (uint256) {
  require(b > 0, "SafeMath: modulo by zero");
  return a % b;
} 

Khai thác lỗ hổng Overflows:

Chúng ta sẽ xem qua smart contract sau để phân tích xem thử chúng ta có thể khai thác lỗ hổng Overflows như thế nào.

pragma solidity 0.7.0;
contract TimeLock {
    mapping(address => uint) public balances;
    mapping(address => uint) public lockTime;

    function deposit() external payable {
        balances[msg.sender] += msg.value;
        lockTime[msg.sender] = now + 1 weeks;
    }

    function increaseLockTime(uint _secondsToIncrease) public {
        lockTime[msg.sender] += _secondsToIncrease;
    }

    function withdraw() public {
        require(balances[msg.sender] > 0);
        require(now > lockTime[msg.sender]);
        uint transferValue = balances[msg.sender];
        balances[msg.sender] = 0;
        msg.sender.transfer(transferValue);
    }
}

Smart contract TimeLock trên thiết kế như một vault (tạm dịch là kho chứa): Người dùng nạp ether vào smart contract thông hàm deposit đồng thời thời gian khóa của số ether sẽ là trong 1 tuần. Người dùng có thể hàm increaseLockTime để tăng thời gian khóa lên. Khi hết thời gian khóa thì người dùng mới có thể gọi hàm withdraw để rút ether đã nạp vào.

Vấn đề nằm ở hàm increaseLockTime: do đang dùng giá trị là uint (tương đương với uint256) để lưu trữ lockTime nên giá trị chứa nằm trong khoảng là [0, 2256 – 1]. Chúng ta có thể truyền vào tham số là  (2256 – lockTime), khi đó lockTime của người dùng sẽ bị Overflows và reset về giá trị 0. Do đó, chúng ta có thể rút ether về mà không bị ràng buộc về thời gian khóa.

Khai thác lỗ hổng Underflows:

Ví dụ bên dưới là một smart contract đơn giản cho một token:

pragma solidity ^0.4.18;

contract Token {

  mapping(address => uint) balances;
  uint public totalSupply;

  function Token(uint _initialSupply) {
    balances[msg.sender] = totalSupply = _initialSupply;
  }

  function transfer(address _to, uint _value) public returns (bool) {
    require(balances[msg.sender] - _value >= 0);
    balances[msg.sender] -= _value;
    balances[_to] += _value;
    return true;
  }

  function balanceOf(address _owner) public constant returns (uint balance) {
    return balances[_owner];
  }
}

Bạn có thể thấy rủi ro về Undeflows nằm ở đâu không?

Rủi ro về Underflow nằm ở hàm transfer. Khi tài khoản ban đầu của người dùng là 0, attacker có thể gọi hàm transfer với tham số _value với bất kì giá trị nào khác 0. Phép toán (2256 – _value) sẽ có kết quả là số dương do bị Undeflows.Với việc làm này, attacker sẽ nhận được token miễn phí.

Kết:

Trong bài viết này, chúng ta đã tìm hiểu về Arithmetic Overflows và Underflows, tạm dịch là lỗi tính toán bị tràn số. Ngoài ra, mình cũng đã gợi ý phương án để giải quyết vấn đề trên là dùng thư viện SafeMath của OpenZeppelin.

Trong bài viết theo, chúng ta sẽ cùng tìm hiểu các rủi ro khác trong lập trình smart contract.


Nhận thấy các bài viết tiếng Việt chuyên về lập trình blockchain còn ít nên tôi quyết định chuyển hướng sang chuyên viết về chủ đề blockchain dành riêng cho lập trình viên. Hi vọng những bài viết này sẽ giúp ích cho các bạn đang muốn theo đuổi lĩnh vực còn khá mới này.

Nếu bạn thấy bài viết hữu ích, bạn có thể ủng hộ tôi vài tách cà phê thông qua MoMo tại đây

Bạn cũng có thể nhờ tôi tư vấn về giải pháp công nghệ thông tin nói chung và blockchain nói riêng (có tính phí) thông qua đây

Series Navigation<< Các nguyên tắc đảm bảo security cho smart contract