How I Used Python to Steal Money

NERD Summit - March 20, 2020

Michael Lynch (@deliberatecoder)

https://decks.mtlynch.io/nerds-2020/

You're all sworn to secrecy

  • This is a true story*
  • I really did steal money
  • But it was the right thing to do

*Variable names have been changed to protect the innocent.

A brief crash course in cryptocurrency

  • For people who don't care about cryptocurrency

What do banks do?

  1. Confirm your identity
  2. Manage transfers to/from your account

Brute forcing bank logins

  • Brute force can crack most bank passwords.
    • Banks are responsible for stopping this.

How do you manage financial accounts without a bank?

  • Traditional payment processors rely on banks to verify your identity.

Public/private keys

The heart of cryptocurrency

  • Private key
    • Only you know it.
    • Proves that you're you.
  • Public key
    • The world knows it.
    • Confirms that messages using your private key are correct.

Encryption/decryption with a public/private keypair

Public/private key encryption is one way

Transfering cryptocurrency with public/private keys

> encrypt("I hereby give $5 to Rick Hood", private_key)
WW8gZG95ICQ1IGEgS2VsbHkgQWxicmVjaHQ=
> decrypt("WW8gZG95ICQ1IGEgS2VsbHkgQWxicmVjaHQ=", public_key)
I hereby give $5 to Rick Hood
  • In short: private keys are like passwords that the world can verify.

Preventing brute force for cryptocurrency

  • Banks rate limit attempts to guess every password
    • You can't prevent people from guessing every private key.
    • Private keys must be strong

Private key strength

  • 256-bit random value (almost every cryptocurrency)
    • 2256 possible keys
    • 1077 possible keys
  • Atoms on Earth: 1050
  • Atoms in the universe: 1082

Sia

A marketplace for disk storage

  • Like Airbnb, but for disk space
  • Participants pay one another in Siacoin

Private keys on Sia

A seedy reddit post

I spent €2,000 on this Sia thing…

Literally 500 tries

Would publishing my password help?

A seedy reddit post

I'm pretty sure I didn't make a mistake

The race is on!

  • People steal from exposed wallets in minutes
  • Sia's price had increased, attracted popular attention

Finding the mistake

  • Transposed letters?
    • meat -> meta

Finding the mistake

  • Added or subtracted letters?
    • heart -> heat

Finding the mistake

  • Incorrect letter?
    • tagged -> tugged

Where did those words come from

  • How did Sia generate the passphrase?

To the dictionary!

  englishDictionary = Dictionary{
    "abbey",
    "abducts",
    "ability",
    "ablaze",
    "abnormal",
    "abort",
    "abrasive",
    "absorb",
    "abyss",
    "academy",
    "aces",
    "aching",
    "acidic",
    "acoustic",
    "acquire",
    "across",
    "actress",
    ...

Ctrl+F my way to riches

  • Which word is not in the dictionary?

No dice

  • Every word is in dictionary

Finding similar words

  • How do we find words that are one copying error away from one another?

Levenshtein distance

  • Measures “edit distance” between two words
Word A Word B Distance
cat car 1
cat scar 2
baker bread 4

Framing the problem

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

Finding alternate candidates

Checking the word wise

Dictionary word Levenshtein distance
abbey 4
abducts 7
ability 6
wife 1
wildly 4

Finding alternate candidates

Posted Word Alternate Candidates
eluded
logic
wise wife
ascend
tagged jagged, nagged

Finding a Levenshtein implementation

Installying python-Levenshtein

Using Levenshtein library

>>> import Levenshtein
>>> Levenshtein.distance('cat', 'scar')
2

A quick ‘n dirty Levenshtein search

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))

Running the search

$ 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

Checking seed candidates

“wise” -> “wife”

$ siac wallet init-seed
Seed: 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

Could not initialize wallet from seed: error when calling /wallet/init/seed: seed failed
checksum verification

Checking seed candidates

“tonic” -> “ionic”

$ siac wallet init-seed
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 ionic vulture shrugged otter adapt

Wallet initialized and encrypted with seed.

Checking the loot

$ siac wallet
Wallet status:
Encrypted, Unlocked
Confirmed Balance:   594.8 SC

I thought you said €2,000…

  • 594.8 SC ~= €10
  • Where'd the rest of the money go?

Yoinks

About that €2,000…

$ siac wallet transactions
[height]          [transaction id]    [net siacoins]   [net siafunds]
  108589   427b72c98e8ea64fba2...         594.83 SC             0 SF
  109002   2304da26d61bd2cb7fc...        -594.55 SC             0 SF

Transactions stuck in limbo

  • Cryptocurrencies can process a limited number of transactions
    • Usually a few hundred per minute.
  • When the network is overloaded, transactions get stuck in limbo.
    • Sender has sent the money, but the recipient hasn't received it.

Poloniex stuck in limbo

How do you steal money when it's not there yet?

  • Naive approach: Infinitely set the transfer
    • But what if I incorrectly guess the amount?
  • I'm playing The Price is Right.

Draining little by little

for /l %%x in (1, 0, 100) do (
   siac wallet send siacoins 2000SC fff0228f02a01cf8e037047a5ea0db5a88d614913af5f21de209ebf2e58431c68cfc9c27d0e4
)

Informing the owner

  • But what could I buy with the money?

I know you said €2,000, but…

  • I'm not pocketing your money.

Doing the right thing

And then…

Nothing

Mystery solved

Why can't you guess every possible passphrase?

Because you can't

  • 1,62629 ~= 1093 possible passphrases
  • Brute forcing every possible private key: 1077

Did I commit a crime?

Consulting the CFAA

“Accesses a computer”

Physical-world equivalent

You find a wallet on a busy street corner. Do you…

A. Pick it up and track down the owner.

B. Leave it on the ground and contact the owner to say you saw it.

Lessons learned

  • Cryptographic keys are fragile.
  • Avoid human copying for precious data.
  • Keep track of where you announce major financial losses.
  • Quick 'n dirty code works when there's a time crunch.

Thanks!