Chào các bạn, hôm nay chúng ta sẽ đến với phần 2 của chuỗi bài Blockchain - hacking smart contract with Ethernaut CTF

Ở bài trước, chúng ta đã được tiếp cận với những lỗ hổng cơ bản và dễ dàng nhất, ở phần này chúng ta sẽ tiếp cận 4 lỗ hổng khác với độ khó cao hơn, mình sẽ giải thích từng bước cụ thể kèm phân tích để các bạn có thể nắm được trực quan và đơn giản nhất.

Hi vọng sẽ mang lại nhiều điều thú vị cho các bạn.

The Ethernaut: https://ethernaut.zeppelin.solutions/

Một vài recommend:

  • Sẽ tốt hơn nếu bạn có kiến thức về Blockchain và Smart Contract
  • Sẽ tốt hơn nếu bạn có kiến thức về Solidity và Web3js
  • Sẽ là tốt hơn nếu bạn biết cách sử dụng Remix IDE hoặc Truffle

5. Token  ★★★☆☆☆

Nhiệm vụ: Có 20 token trong tay, ta cần chôm ở đâu đó vài token nữa (càng nhiều càng tốt)

pragma solidity ^0.4.18;

contract Token {

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

  function Token(uint _initialSupply) public {
    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 view returns (uint balance) {
    return balances[_owner];
  }
}

Phân tích

  • Trong các ngôn ngữ lập trình static type như C/C++/C#/Java, có lẽ chúng ta hầu như không xa lạ gì với khái niệm overflow - hiện tượng tràn số khi tính toán số lớn hơn giá trị MAX của kiểu dữ liệu đã khao báo. Nhưng có một khái niệm nữa ít được để ý hơn nhưng cũng vô cùng quan trọng, đó là underflow - hiện tượng mà khi số nhỏ dưới giá trị MIN của kiểu dữ liệu đã khai báo thì số đó sẽ được quay vòng trở lại từ MAX, thật tai hại nếu không handle trường hợp này.
  • Trong bài này, kiểu dữ liệu đang dùng là uint256, giới hạn từ 0 cho tới 2256 2^{256}
  • Ở đây ta có đoạn
require(balances[msg.sender] - _value >= 0);

những tưởng rằng điều kiện này chỉ đạt được khi balance của msg.sender lớn hơn giá trị value; nhưng không, điều kiện này sẽ trở thành auto true. Thật vậy, nếu như balance >= value thì hiển nhiên sẽ là true, còn nếu như balance < value thì khi balance - value sẽ xảy ra hiện tượng underflow và trở nên vô cùng lớn, theo đó điệu kiện cũng sẽ là true. Tóm lại, ta sẽ luôn luôn pass.

Solution

  • Trên Chrome console để kiểm tra balance hiện tại:
await contract.balanceOf(player).then(x => x.toNumber())
20
  • Do điều kiện auto pass, ta sẽ transfer cho một địa chỉ nào đó một giá trị lớn hơn 20 - là số token hiện tại của ta, khi đó phép toán balances[msg.sender] -= value sẽ xảy ra hiện tượng underflow và ta sẽ sở hữu một lượng vô cùng lớn token:
contract.transfer(player, 21)
  • Kiểm tra lại balance một lần nữa, wow !! !
await contract.balanceOf(player).then(x =&