我如何偷走你的 Siacoin
原文由 Michael Lynch 于 發布,訂閱此部落格
一則可疑的 Reddit 貼文
那是 2017 年 6 月 9 日的夜晚。對我來說是個典型的週五夜晚。我當時正在看 Netflix、刷 Reddit跟酷炫的人們一起狂歡。
突然,我在 /r/siacoin 子版的「最新」分頁看到這篇貼文:

如果你不熟悉 Siacoin,它是一種加密貨幣,讓你可以出租自己閒置的硬碟空間,或是向別人租用空間。我之前已經寫過幾篇關於這項技術的文章(挖礦指南、NAS 指南)。
這位 Reddit 使用者剛剛做了件非常危險的事。他在網路上公開了自己的 Sia 錢包助記詞。Sia 的「助記詞」是一組密語,任何拿到它的人都能完全掌控該錢包裡的加密貨幣。對這位使用者來說,這意味著價值 2,000 歐元的 Siacoin 拱手讓人,因為那正是他轉入這個錢包的金額。從截圖中可以看到,使用者認為這樣做是安全的,因為他已經確認過這組密語無法使用。
差一點就不算(除了擲馬蹄鐵和 Sia 助記詞之外)
這篇貼文立刻吸引我注意的是,使用者是手寫下助記詞的:
我很確定我抄寫的時候沒寫錯,我每次都會再三確認。
我倒是相當確定他們的確抄錯了。但我希望他們只抄錯了一個地方。如果使用者只是錯了一個字母,或是把兩個字母寫顛倒了,我或許就能推算出正確的助記詞,並取回那 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 個單字中有哪個不在詞庫裡,那個顯然就是寫錯的字。接著我只要在詞庫裡尋找跟那個錯字相似的字,就能很快推算出正確的助記詞。
可惜的是,那組錯誤助記詞裡的 29 個字全都在 entropy 詞庫裡,所以光用肉眼是找不出問題的。
暴力破解
是時候拿出大傢伙了(我指的是用來打字的兩根手指頭,我稱之為「槍」)。我需要一個方法,找出詞庫裡所有跟 Reddit 上那組助記詞只差一個抄寫錯誤的單字。
我想到 萊文斯坦距離或許能幫上忙。萊文斯坦距離指的是要從一個字變成另一個字,需要新增、刪除或替換幾個字母。舉例來說,「cat」和「car」的萊文斯坦距離是 1,因為只要把「t」換成「r」就能從「cat」變成「car」。而「cat」和「scar」的距離則是 2,因為必須替換「t」並在前面加上一個「s」。
為了找出可能的助記詞,我可以寫一支程式,找出 entropy 詞庫中與那組錯誤助記詞裡的單字萊文斯坦距離為 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 的組合只有 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這在預料之中。並非每一組都會是有效的助記詞。我持續嘗試每一組可能的助記詞,直到試到把 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 偷走了。
Ben Gates:有人也在覬覦這個寶藏。
Riley Poole:當然有人在覬覦,這可是尋寶的公理。
-國家寶藏:古籍秘辛
我迅速 把全部餘額轉到我自己的 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 錢包在自己地址之間轉帳時會產生這種 0.0 的交易。
我感興趣的是清單中的第一筆交易。那一行顯示這個錢包只在區塊高度 108,589 時收到過一筆 594.83 SC 的存款。區塊高度基本上就是 Siacoin 時間單位裡的「時間」。查看 Sia 區塊瀏覽器中的這筆交易,我發現這筆存款是在 2017 年 6 月 7 日,也就是使用者發 Reddit 貼文的兩天前入帳的。
為什麼使用者會聲稱自己在錢包裡放了 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 SC 是因為轉帳金額小一點比較保險。我實際上是在玩The Price is Right 的規則。如果我選擇比方說 125,000 SC,也就是當時相當於 2,000 歐元的金額,但實際只到了 124,000 SC,我的指令就會因為餘額不足而失敗,一毛錢也轉不出來。
猜得太低其實沒什麼懲罰,只不過要多付一點交易手續費而已。2,000 SC 大約是 35 歐元,所以如果真的有一筆約 125,000 SC(2,000 歐元)的存款入帳,我的批次檔會在幾分鐘內就把錢包搬空。
通知受害者
我承認,我確實幻想過如果把那 10 歐元據為己有可以怎麼花:私人飛機、勞力士、還有一棟配有史高治叔叔那種錢幣泳池的豪宅。但最後,我決定還是要做正確的事,把 Siacoin 還給那位貼出助記詞的使用者。

如果我把錢占為己有,這就是我。
不過,我找到的金額和他們聲稱遺失的金額之間的落差,可能會讓場面有點尷尬。就像打電話給某人說:「嘿,你就是那個到處貼海報說掉了裝有 2,000 歐元現金的錢包的人嗎?我找到了,不過裡面只有——呃……10 歐元……」(眼神飄移)。
在使用者發文約兩小時後,我在 Reddit 上私訊了他們。我解釋了我如何復原了他們的助記詞,並把錢先取走,以免被心術不正的人同樣破解拿走。我請他們提供一個與外洩助記詞無關的 Siacoin 地址,好讓我把餘額還給他們。
幾個小時過去,接著幾天過去,我都沒有收到回覆。我注意到他們已經刪除了那篇公開助記詞的 Reddit 貼文。什麼人掉了 2,000 歐元、在網路上發文求救,然後幾個小時後就好像完全忘了這回事?
謎團解開
最後,在週一早上,這位我犯下滔天大罪的受害者回覆了我。他們解釋說,發文後不久,他們就發現錢其實還在交易所,根本從未到達錢包(我就知道!)。他們已經成功把錢轉到另一個助記詞安全的錢包。當他們發現自己其實沒有把錢弄丟時,就沒想到要回 Reddit 查看。
他們很高興我復原了助記詞,因為我解開了他們對於密語哪裡出錯的謎團。他們其實正確地寫下了 ionic,但每次回頭讀的時候都誤看成 tonic,因為那是對他們來說更熟悉的字。這位使用者甚至提出讓我把全額留下,但我覺得這樣在這篇部落格文章裡看起來會比較好看這些幣理應屬於遺失它們的使用者。我堅持歸還,他們最後才讓步,給了我一個地址讓我歸還那 594.8 SC。
啟示
絕對不要在網路上公開你的 Sia 錢包助記詞。正如這個故事所顯示的,即使是錯誤或不完整的助記詞,也可能讓你的錢包完全被攻破。
這不僅適用於 Siacoin,也適用於所有加密貨幣。並非所有幣都像 Sia 一樣使用密語,但它們都使用某種私鑰,如果你不想丟掉手上的幣,就必須妥善保密。
隨機一篇部落格
留言
登入後參與討論