How I Stole Your Siacoin

Michael Lynch

あなたのSiacoinをどうやって盗んだか

怪しげなReddit投稿

2017年6月9日の夜だった。いつもどおりの金曜の夜だ。僕はNetflixを見ながらRedditを眺めるイケてるやつらとパーティーをしていた。

ふと、/r/siacoinサブレディットの「新着」タブで、こんな投稿を見つけた。

Redditのスクリーンショット

Siacoinを知らない人のために説明しておくと、これは余ったハードディスクの空き容量を貸し出したり、逆に他人の容量を借りたりできる暗号通貨だ。この技術については以前にも2回記事を書いている(マイニングガイドNASガイド)。

このRedditユーザーは、とても危険なことをしてしまった。Siaウォレットのシードをネット上に投稿したのだ。Siaの「シード」とは、それを持っている人にウォレット内の暗号通貨を完全に支配する権限を与えるパスフレーズのことだ。このユーザーの場合、2,000ユーロ相当のSiacoinを意味する。なぜなら、その額をこのウォレットに送金していたからだ。スクリーンショットを見ると、ユーザーはパスフレーズが機能しないことを確認したので安全だと思い込んでいることがわかる。

惜しいだけではダメだ(蹄鉄投げとSiaシード以外は)

この投稿ですぐに僕の興味を引いたのは、ユーザーがシードを手書きしていたことだった。

i’m pretty sure i didn’t make a mistake writing it down, i always double check.

僕は、絶対に書き写しを間違えていると確信していた。ただ、間違いが一つだけであってほしいと願っていた。もし一文字だけ違っていたり、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本の指のことを「奥の手」と呼んでいる)を出すときが来た。Redditに投稿されたシードから、書き写しミス1文字違いの辞書内の単語をすべて見つけ出す方法が必要だった。

ここでレーベンシュタイン距離が役立つことに気づいた。レーベンシュタイン距離とは、ある単語から別の単語へ変形するのに必要な文字の追加・削除・置換の回数だ。例えば「cat」と「car」は、tをrに置き換えるだけで変わるので距離は1だ。「cat」と「scar」は、tを置き換えて先頭にsを付け加える必要があるので距離は2になる。

可能性のあるシードを見つけるために、間違ったシードに含まれる単語からレーベンシュタイン距離が1の辞書内の単語を探し出すスクリプトを書くことにした。

まずは辞書をローカルにダウンロードし、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に手入力で試すことにした。まずは最初の候補、間違ったシードのwisewifeに置き換えたものを試した。

> siac wallet init-seed
Seed:
Could not initialize wallet from seed: error when calling /wallet/init/seed: seed failed checksum verification

これは想定内だった。すべてが有効なシードであるはずがない。1つずつ試していき、tonicionicに置き換えたシードにたどり着いたとき、ついにこうなった。

> 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ブロックエクスプローラーでトランザクションを確認すると、この入金はユーザーがRedditに投稿する2日前の2017年6月7日に行われていた。

なぜユーザーは2,000ユーロを入金したと主張したのに、実際には10ユーロしか入金されていなかったのだろうか?

宙ぶらりんのトランザクション

僕が大胆な強奪をしていた当時、最大の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のようにパスフレーズを使うものばかりではありませんが、どの通貨も、コインを失いたくなければ秘密にしておかなければならない何らかの秘密鍵を使っています。

原文は Michael Lynch により に公開されました。

この記事は「muse-spark-1.2-contributor」を使用して翻訳されました。