LINE CTF 2021 - Reversing - Sakura
TL;DR
複数アカウントの状態を上手く遷移させてフラグを出力する
※ ここでいう状態はコントラクトに定義されたStateではない
問題
Sakura is beautiful
nc 34.84.178.140 13000
ファイル構成
code:txt
.
├── contracts
│ ├── Contract.sol
│ └── openzeppelin
│ └── contracts
│ ├── access
│ │ └── Ownable.sol
│ └── utils
│ └── Context.sol
├── (flag)
└── index.js
解法
Contract.solがアプリ本体のコードで、index.jsがそのコントラクトを操作するCLIアプリケーション(コントラクトのバイトコードが埋め込まれている)。openzeppelinはコントラクトにおける標準ライブラリにあたるものなので読まなくていい。
ncでアクセスする or ローカルで実行すると次のような結果が得られ、選択肢を選ぶ形。
code:sh
❯ nc 34.84.178.140 13000
Loading...
Account: 0x66ab6d9362d4f35596279692f0251db635165871
Account: 0x33a4622b82d4c04a53e170c638b944ce27cffce3
Account: 0x0063046686e46dc6f15918b61ae2b121458534a5
Set player account a balance of 100 ETH
Compiling...
Deploying the contract...
Contract address: 0xe7cb1c67752cbb975a56815af242ce2ce63d3113
--------------------------------------
Welcome to Timeless Sakura Prediction Game
- You can get ETHs if you predict the future.
- Oracle system that go beyond powerful time will judge.
- We have GOD level BFT consensus model, Ethereum based single node blockchain.
(Yeah, We've solved the bloody byzantine general problem)
- We use a smart contract engine based on a powerful EVM, the World computer.
--------------------------------------
Today's question is
What will be the weather tomorrow?
1) Sunny
2) Rainy
--------------------------------------
1) Bet
2) Cancel
3) Get Player's Balance
4) Finalize
まず、色々実験すると次のことがわかる。
Finalizeで得られるOracle responseがBetの選択の逆になる。これが同じになれば良さそう。
Betすると2つのアカウントの残高が減る。キャンセルすると戻る。
5回までしか選択肢を入力できない。
適切な入力及び適切な順序を踏まないとトランザクションのrevertが起こる。
例: Tx Reverted: [ 'Invalid state' ] { error: 'revert', errorType: 'VmError' }
トランザクションのrevertの個数や内容が変わる。おそらく複数アカウントのトランザクションが発生している。
などなど(省略)
次にflagがどう出力されるのか探すと、index.jsに出力箇所がある。
case 1: return 1 == I.sent().balance.gt(new Y.BN(G.convert("1000", "eth", "wei"))) && (console.log("win!"), console.log(k.readFileSync("/flag", "utf8"))), [2]
おそらく自分のアカウントの残高が1000 ETH超えたらフラグが出力されるのだろうとわかる。
この1000の数値を弄るとFinalizeしたタイミングで実際にフラグが表示される。
長さが5以下の選択肢の列の中にフラグが出力されるようなものがあると考えられる。これをfuzzingすれば良い。
コード↓。Dronex氏が書いたもの。
code:py
import subprocess
import itertools
from concurrent.futures import ProcessPoolExecutor
def work(tup):
print(tup)
stdin=subprocess.PIPE, stdout=subprocess.PIPE)
s = "\n".join(str(x) for x in itertools.chain(*tup))+"\n"
stdout, stderr = proc.communicate(s.encode())
stdout = stdout.decode()
if "win!" in stdout or "LINECTF" in stdout:
print("!" * 1000, tup)
open("!!!.txt", "w").write(str(tup))
exit(0)
with ProcessPoolExecutor(max_workers=4) as executor:
executor.map(work, zenbu)
(1,1),(1,3),(4,)の列を入力するとフラグが出てくる(シンプルだから適当に触ってたらフラグを得たチームもいそう)。
LINECTF{S4kura_hira_hira_come_to_spring}
その他メモ
enum State { Created, Cancelled, WaitingWinnerSelect, WaitingAdminApproval, Closed }
選択肢を選ぶとStateが変化する
選択肢とコントラクトの関数の対応
1) Bet
lock
2) Cancel
cancelByUser
3) Get Player's Balance
4) Finalize
userJudge
コントラクトの関数一覧
getLockedList
index.jsで呼ばれてない
getLocked
index.jsで呼ばれてない
close
private
adminJudge
index.jsで呼ばれてない
userJudge
is_unanimous: 満場一致かどうか
index.jsで呼ばれてる
revoke
index.jsで呼ばれてる
cancelByAdmin
index.jsで呼ばれてない
cancelByUser
index.jsで呼ばれてる
lock
index.jsで呼ばれてる
userJudge, revoke, cancelByUser, lockの4つしか外部から呼び出されない
Stateの遷移
Created --(1)--> WaitingWinnerSelect --(4)--> WaitingAdminApproval
--(cancel)--> Cancelled --(revoke)--> Cancelled
--(adminJudge --> close)--> Closed
回答入力部分
case 5: return 1 != (P = D.sent()) ? [3, 8] : (W = parseInt(w("answer> ")), s = 1 == W ? 2 : 1, [4, M(N, F, I, new Y.BN(W))]);