あなたのSiacoinをどうやって盗んだか
原文は Michael Lynch により に公開されました。 このブログを購読する
怪しげなReddit投稿
2017年6月9日の夜のことだった。いつも通りの金曜の夜だった。私はNetflixを見ながらRedditをチェックしていたイケてる連中とパーティーをしていた。
突然、/r/siacoinサブレディットの「新着」タブでこんな投稿を見つけた。

Siacoinを知らない人のために説明すると、これは余ったハードディスクの空き容量を貸し出したり、逆に他人の空き容量を借りたりできる暗号通貨だ。この技術については以前にも何度か記事を書いている(マイニングガイド、NASガイド)。
このRedditユーザーは非常に危険なことをしてしまった。Siaウォレットのシードをネット上に投稿したのだ。Siaの「シード」とは、それを持っている人にウォレット内の暗号通貨を完全に支配する権限を与えるパスフレーズだ。このユーザーにとっては、2,000ユーロ相当のSiacoinを支配できることを意味する。なぜなら、その額をウォレットに送金していたからだ。スクリーンショットを見ればわかるように、ユーザーはこのパスフレーズが機能しないことを確認したため、安全だと思い込んでいる。
「惜しい」では済まされない(馬蹄投げとSiaシードを除いては)
この投稿で私がすぐに注目したのは、ユーザーがシードを手書きで書き写していたことだった。
書き写しを間違えたとは思えない、いつも二重にチェックしてるし。
私は、きっと書き写しを間違えていると確信した。ただ、間違いが一つだけであってほしいと願った。もし1文字だけ違っていたり、2文字が入れ替わっただけなら、正しいシードを突き止めて2,000ユーロを取り戻せるかもしれない。
急ぐ必要があった。漏洩したシードを見てそれと気づけるのは自分だけではない。誰よりも早くシードを解読して資金を確保しなければならなかった。
手作業ハッキング
まずは投稿された間違ったシードに含まれる単語を調べてみた。
eluded logic wise ascend tagged acoustic situated stylishly younger aptitude inroads avidly hefty also godfather unrest avatar push because brunt viking gone august public tonic vulture shrugged otter adapt
Siaがどのようにシードを生成しているのかは知らなかったが、Siaは完全にオープンソースなので、調べるのはそう難しくないだろうと思った。
案の定、そうだった。Siaのwallet.goファイルを見ていくと、SeedToString関数が見つかった。そこからentropy-mnemonicsというGitHubプロジェクトにたどり着き、そこにはこの辞書というシードに使われうる単語の辞書があった。
englishDictionary = Dictionary{
"abbey",
"abducts",
"ability",
"ablaze",
"abnormal",
"abort",
"abrasive",
"absorb",
"abyss",
"academy",
"aces",
"aching",
"acidic",
"acoustic",
"acquire",
"across",
"actress",
...entropy辞書に収録されている単語は約1,600語しかなかった。私が期待したのは、ユーザーがシードを書き写す際に、辞書に存在しない単語を誤って書き込んでしまった可能性だ。もし投稿された29語のうち1つが辞書に存在しなければ、それが間違った単語だと一目でわかる。そうなれば、辞書内でその単語に似たものを探すだけで、すぐに正しいシードを特定できる。
しかし残念ながら、間違ったシードに含まれる29語はすべてentropy辞書に存在しており、目視で見つける方法は使えなかった。
総当たり攻撃
大砲を持ち出す時が来た(といっても、コードを打つときに使う2本の指を「大砲」と呼んでいるのだが)。投稿されたシードから写し間違い1つ分のズレにある辞書内の単語をすべて見つけ出す方法が必要だった。
レーベンシュタイン距離がここで役立つことに気づいた。レーベンシュタイン距離とは、ある単語から別の単語に変換するために追加・削除・置換しなければならない文字数のことだ。例えば「cat」と「car」はレーベンシュタイン距離が1だ。なぜなら「t」を「r」に置き換えれば「cat」から「car」になるからだ。「cat」と「scar」の距離は2で、「t」を置き換え、さらに先頭に「s」を追加する必要がある。
候補となるシードを見つけるために、間違ったシード内の各単語からレーベンシュタイン距離が1の単語をentropy辞書から探し出すスクリプトを書けばよい。
まず辞書をローカルにダウンロードし、aからz以外の文字を取り除いた。
$ wget -qO- https://raw.githubusercontent.com/NebulousLabs/entropy-mnemonics/master/english.go \
| egrep "^\s+\"(.+)\"," \
| egrep -o [a-z]+ \
> dictionary.txt次にpython-Levenshteinライブラリをインストールし、候補のシードを吐き出す適当なPythonスクリプトを書いた。
import Levenshtein
seed = raw_input('enter your wallet seed: ')
for seed_word in seed.split():
for dict_word in open('dictionary.txt'):
dict_word = dict_word.strip()
distance = Levenshtein.distance(seed_word, dict_word)
if distance != 1:
continue
print '"%s" -> "%s"\n%s\n' % (seed_word, dict_word,
seed.replace(seed_word, dict_word))白状すると、実際にはもっと適当なスクリプトで、辞書の1,600行を直接Pythonスクリプトにコピペしていた。ここに示すコードは説明用に整えたものだ。
金庫を開ける
何百通りもの候補が出てきて、1つずつ試す処理も自動化しなければならないかと心配していた。幸い、スクリプトの結果、間違ったシードからレーベンシュタイン距離が1の候補は12通りしかないことがわかった。
$ python recover.py
enter your wallet seed: eluded logic wise ascend tagged acoustic situated stylishly younger aptitude inroads avidly hefty also godfather unrest avatar push because brunt viking gone august public tonic vulture shrugged otter adapt
"wise" -> "wife"
eluded logic wife ascend tagged acoustic situated stylishly younger aptitude inroads avidly hefty also godfather unrest avatar push because brunt viking gone august public tonic vulture shrugged otter adapt
"tagged" -> "jagged"
eluded logic wise ascend jagged acoustic situated stylishly younger aptitude inroads avidly hefty also godfather unrest avatar push because brunt viking gone august public tonic vulture shrugged otter adapt
"tagged" -> "nagged"
eluded logic wise ascend nagged acoustic situated stylishly younger aptitude inroads avidly hefty also godfather unrest avatar push because brunt viking gone august public tonic vulture shrugged otter adapt
"aptitude" -> "altitude"
eluded logic wise ascend tagged acoustic situated stylishly younger altitude inroads avidly hefty also godfather unrest avatar push because brunt viking gone august public tonic vulture shrugged otter adapt
"push" -> "lush"
eluded logic wise ascend tagged acoustic situated stylishly younger aptitude inroads avidly hefty also godfather unrest avatar lush because brunt viking gone august public tonic vulture shrugged otter adapt
"brunt" -> "grunt"
eluded logic wise ascend tagged acoustic situated stylishly younger aptitude inroads avidly hefty also godfather unrest avatar push because grunt viking gone august public tonic vulture shrugged otter adapt
"tonic" -> "ionic"
eluded logic wise ascend tagged acoustic situated stylishly younger aptitude inroads avidly hefty also godfather unrest avatar push because brunt viking gone august public ionic vulture shrugged otter adapt
"tonic" -> "sonic"
eluded logic wise ascend tagged acoustic situated stylishly younger aptitude inroads avidly hefty also godfather unrest avatar push because brunt viking gone august public sonic vulture shrugged otter adapt
"tonic" -> "topic"
eluded logic wise ascend tagged acoustic situated stylishly younger aptitude inroads avidly hefty also godfather unrest avatar push because brunt viking gone august public topic vulture shrugged otter adapt
"tonic" -> "toxic"
eluded logic wise ascend tagged acoustic situated stylishly younger aptitude inroads avidly hefty also godfather unrest avatar push because brunt viking gone august public toxic vulture shrugged otter adapt
"adapt" -> "adept"
eluded logic wise ascend tagged acoustic situated stylishly younger aptitude inroads avidly hefty also godfather unrest avatar push because brunt viking gone august public tonic vulture shrugged otter adept
"adapt" -> "adopt"
eluded logic wise ascend tagged acoustic situated stylishly younger aptitude inroads avidly hefty also godfather unrest avatar push because brunt viking gone august public tonic vulture shrugged otter adopt候補が少なかったので、手作業でSiaに打ち込んで試すことにした。まずは最初の候補、間違ったシード内のwiseをwifeに置き換えたものを試した。
> siac wallet init-seed
Seed:
Could not initialize wallet from seed: error when calling /wallet/init/seed: seed failed checksum verificationこれは想定内だった。すべてが有効なシードになるわけではない。候補を1つずつ試していき、tonicをionicに置き換えたシードにたどり着いた。
> siac wallet init-seed
Seed:
Wallet initialized and encrypted with seed.大当たりだ!
中身を確認してみよう。
> siac wallet unlock
Wallet password:
Wallet unlocked
> siac wallet
Wallet status:
Encrypted, Unlocked
Confirmed Balance: 594.8 SCおかしい。当時の594.8 SC(Siacoin)は約10ユーロ相当で、ユーザーがウォレットに入っていると主張していた2,000ユーロとは程遠い。
逆に自分が騙されているのだろうか?ユーザーは10ユーロしかないことを知りながら、誰かに助けてもらうためにわざと高い金額を主張したのか?それとも、もっと腕のいい暗号泥棒が先にウォレットに到達し、からかうように10ユーロだけ残していったのか?
戦利品の確保
奇妙な残高について座って考え込みたい気持ちはあったが、一刻を争う状況だった。あの投稿を他に誰が見ていて、今まさに自分と同じようにウォレットをアンロックしようとしているかわからない。Siacoinを盗む時だった。
ベン・ゲイツ: 他にもお宝を狙っている奴がいる。
ライリー・プール: 当たり前だろ。お宝探しの公理さ。
-ナショナル・トレジャー リンカーン暗殺者の日記
私はすぐに全残高を自分のSiaウォレットに送金した。こうしておけば、たとえ後から誰かが正しいシードを発見しても、資金を取り戻すことはできない。
謎に戻る
コインを確保したところで、一体何が起きているのかを突き止める時が来た。ウォレットの取引履歴を確認した。
> siac wallet transactions
[height] [transaction id] [net siacoins] [net siafunds]
108589 427b72c98e8ea64fba234ca2a00288f7a750003a243e6b3e967f5c6d426c2f9f 594.83 SC 0 SF
109002 32ad2729fe6b487aedc1b70d0dff0843404ff1cef69223d5f03699dcd1dbe568 0.00 SC 0 SF
109002 2304da26d61bd2cb7fcac5c7b38a553d788d8dfc386ae4eb47772e36e4a9269d -594.55 SC 0 SF
リストの最後のトランザクションは出金だ。これは私がお金を盗んだものなので気にしなくていい。0.00 SCのトランザクションはノイズで、Siaウォレットが自身のアドレス間で資金を移動する際に生成するものだ。
興味があったのはリストの最初のトランザクションだ。その行は、このウォレットがこれまでに一度だけ、ブロック高108,589で594.83 SCの入金を受け取ったことを示していた。ブロック高は、Siacoinにおける「時間」のようなものだ。Siaブロックエクスプローラーでそのトランザクションを確認すると、この入金が2017年6月7日、ユーザーのReddit投稿の2日前に行われたことがわかった。
なぜユーザーは10ユーロしか入金していないのに、2,000ユーロをウォレットに入れたと主張したのだろうか?
宙ぶらりんのトランザクション
私の大胆な強奪の当時、最大のSiacoin取引所であるPoloniexは、Siacoinをユーザーのウォレットへ送金する処理に問題を抱えていた。ユーザーの資金が失われたわけではないが、ユーザーが取引所のアカウントから個人のSiaウォレットへ送金しても、Poloniex側の処理が滞って数日から数週間宙ぶらりんになることがよくあった。
もしかすると、このユーザーは2,000ユーロをウォレットに送金したが、そのお金がPoloniexの宙ぶらりん状態に捕まっていたのかもしれない。つまり、その2,000ユーロは最終的にはウォレットに届くはずなので、まだ誰の手にも渡る可能性があるということだ。
これは新しくて面白い問題だった。まだウォレットに届いておらず、いつ届くかもわからないお金をどうやって盗めばいいのか?私は、漏洩したウォレットから自分のウォレットへ資金を送り続けるバッチスクリプトを書くことにした。というより、バッチスクリプトの書き方を学ぶことにした。手軽に使えるSia環境がWindows仮想マシンしかなく、Windowsでバッチスクリプトを書いたことがなかったからだ。あれこれやって、最終的にこんな見事なバッチスクリプトが出来上がった。
for /l %%x in (1, 0, 100) do (
siac wallet send siacoins 2000SC fff0228f02a01cf8e037047a5ea0db5a88d614913af5f21de209ebf2e58431c68cfc9c27d0e4
)このスクリプトは、Redditユーザーの漏洩したウォレットから自分のウォレットアドレスへ2,000 SCを繰り返し送金しようとする。1から100まで0刻みでカウントするので、永遠にループする。
ウォレットの残高がゼロの間は、このコマンドは失敗するだけで何も起きない。しかし、もし期待していた2,000ユーロがウォレットに届けば、2,000 SCずつ私のウォレットへ吸い上げてくれる。
比較的少額の送金にした方が安全だと考え、2,000 SCを選んだ。いわば『ザ・プライス・イズ・ライト』のルールでプレイしていたのだ。もし当時の2,000ユーロに相当する125,000 SCを選んでいたとして、実際にウォレットに届いたのが124,000 SCだけだったら、コマンドは残高不足エラーで失敗し、何も送金されないことになる。
少なめに見積もっても、取引手数料が少し増える以外に大きなデメリットはなかった。2,000 SCは約35ユーロ相当なので、もし約125,000 SC(2,000ユーロ)の入金があれば、私のバッチスクリプトは数分でウォレットを空にできるはずだった。
被害者に知らせる
白状すると、もし10ユーロを自分のものにしたら何に使おうかと、かなりいろいろな妄想を膨らませた。プライベートジェット、ロレックス、スクルージ・マクダックのようなお金のプールがある豪邸などだ。でも結局、正しいことをして、シードを投稿したユーザーにSiacoinを返すことにした。

もしお金を自分のものにしていたら、の私。
とはいえ、見つけた金額と相手が失ったと主張する金額の食い違いは、気まずい状況を生みかねなかった。「もしもし、現金2,000ユーロ入りの財布を落としたという張り紙を出した方ですか?見つけたんですが、中には——えっと……10ユーロしか入ってなくて……」(目を泳がせながら)と言うようなものだ。
ユーザーが最初に投稿してから約2時間後、私はRedditでダイレクトメッセージを送った。どのようにシードを復元し、悪意のある他のユーザーに奪われる前に資金を安全な場所に移したかを説明した。そして、漏洩したシードとは紐づいていないSiacoinアドレスを教えてくれれば、残高を返却すると伝えた。
数時間が経ち、やがて数日が経ったが、返事はなかった。気づくと、シードを晒していたRedditの投稿は削除されていた。2,000ユーロを失ってネットに助けを求めておきながら、数時間後にはすっかり忘れたかのように振る舞う人がいるだろうか?
謎が解けた
ついに月曜の朝、私の凶悪犯罪の被害者から返信が来た。投稿してすぐに、お金はまだ取引所に残っておりウォレットには一度も届いていなかったことに気づいたという(やっぱり!)。そのお金はシードが安全な別のウォレットに移すことができた。実際にはお金を失っていなかったとわかったので、わざわざRedditをチェックしに戻る気にならなかったのだそうだ。
彼らは私がシードを復元したことを喜んでくれた。パスフレーズで何が間違っていたのかという謎が解けたからだ。彼らは正しくionicと書き記していたのに、読み返すたびにより馴染みのあるtonicと誤読していたのだ。ユーザーは全額をそのまま受け取ってくれて構わないとさえ言ってくれたが、このブログ記事での印象が良くなるからコインは本来失くしたユーザーのものだと感じた。私は固辞し、最終的に相手は折れて、594.8 SCを返却するためのアドレスを送ってくれた。
教訓
Siaウォレットのシードは絶対にオンラインに投稿しないでください。この話からもわかるように、たとえ間違った、あるいは部分的なシードであっても、ウォレット全体が危険にさらされる可能性がある。
これはSiacoinだけでなく、暗号通貨全般に当てはまることだ。すべてがSiaのようにパスフレーズを使っているわけではないが、コインを失いたくなければ秘密にしておかなければならない何らかの秘密鍵が必ず存在する。
記事をランダムに読む
コメント
ログインしてコメントする