Password Recovery – Part 2 – Practice

In previous part I’ve given you an overview of strong and weak methods of storing passwords. I’ve told what are the gains of using slow hashing methods and salting as basic methods of protecting passwords. Today, I’m going to perform a simulated attack on passwords to compare efficiency of password cracking. As always all examples are available on gitlab, however, most are my notes only I am supposed to understand.

Tests are carried on my Macbook Pro on its graphic card (I also wanted to compare it with CPU but hashcat refused to work in some cases no matter if I used Homebrew’s 4.2.1 version or compiled newest one myself and I didn’t have time to debug the issue). It’s integrated Intel’s Iris graphics with 48 cores. Disclaimer: I know that I am using business laptop which is far from perfect for this task, but nonetheless there is some interesting outcome.

I have chosen 3 passwords from the list of 10 million most popular passwords (1st, last and middle one – they are in “chosen_passwords.txt” file), hashed them with md5 and PBKDF2 using built-in Django’s hasher and performed brute force and dictionary attacks. Example of hashing with Django’s hashers:

django:
In [1]: from django.contrib.auth.hashers import make_password

In [2]: make_password("123456")
Out[2]: 'pbkdf2_sha256$36000$wzi7098ecXQO$5StEa0bsyZUgX+g8B8yN05KdQkH26D6xykUST9Nm4iM='

The second method, although computing output in far less than 0,5 seconds which was suggested in one of the articles I linked in previous parts and using shorter salt, gave tremendous improvement when it comes to securing users’ passwords. Brute force was conducted using hashcat’s default settings (I assumed I know nothing about passwords I intended to crack).

Cracking md5

1st I run a dictionary attack on md5-hashed passwords. Here’s the partial output:

time hashcat -D 2 -m 0 -a 0 md5_passwords.txt passwords.txt -O
hashcat (v4.2.1) starting...

# skipped

Approaching final keyspace - workload adjusted.

e10adc3949ba59abbe56e057f20f883e:123456
610ef919a73003ec8b73e434fb3116d4:lp0520
3a3a5c3f10e14cc9d5e92127a0ee0880:vjht008

Session..........: hashcat
Status...........: Cracked
Hash.Type........: MD5
Hash.Target......: md5_passwords.txt
Time.Started.....: Sat Aug  3 21:04:35 2019 (0 secs)
Time.Estimated...: Sat Aug  3 21:04:35 2019 (0 secs)
Guess.Base.......: File (passwords.txt)
Guess.Queue......: 1/1 (100.00%)
Speed.Dev.#2.....:  8168.5 kH/s (8.40ms) @ Accel:64 Loops:1 Thr:256 Vec:1
Recovered........: 3/3 (100.00%) Digests, 1/1 (100.00%) Salts
Progress.........: 999999/999999 (100.00%)
Rejected.........: 9/999999 (0.00%)
Restore.Point....: 786433/999999 (78.64%)
Candidates.#2....: zhansuluzhanel -> vjht008

Started: Sat Aug  3 21:04:25 2019
Stopped: Sat Aug  3 21:04:35 2019

real	0m10.280s
user	0m0.307s
sys	0m0.291s

At the bottom you can see that the entire operation took about 10 seconds, most of which was warmup, since in this case I was able to run the attack on CPU and it took about 2 seconds. The speed was over 8 million combinations checked in a single second. Now the brute force – checking every single combination fro length 1 to 7 (longest password’s length) with upper case letter, lower case letters, digits, special characters etc.

time hashcat -D 2 -m 0 -a 3 md5_passwords.txt
hashcat (v4.2.1) starting...

# skipped

Session..........: hashcat
Status...........: Exhausted
Hash.Type........: MD5
Hash.Target......: md5_passwords.txt
Time.Started.....: Sun Aug  4 08:44:03 2019 (0 secs)
Time.Estimated...: Sun Aug  4 08:44:03 2019 (0 secs)
Guess.Mask.......: ?1 [1]
Guess.Charset....: -1 ?l?d?u, -2 ?l?d, -3 ?l?d*!$@_, -4 Undefined
Guess.Queue......: 1/15 (6.67%)
Speed.Dev.#2.....:     3649 H/s (0.10ms) @ Accel:16 Loops:7 Thr:256 Vec:1
Recovered........: 0/3 (0.00%) Digests, 0/1 (0.00%) Salts
Progress.........: 62/62 (100.00%)
Rejected.........: 0/62 (0.00%)
Restore.Point....: 1/1 (100.00%)
Candidates.#2....: O -> X

The wordlist or mask that you are using is too small.
This means that hashcat cannot use the full parallel power of your device(s).
# skipped

Approaching final keyspace - workload adjusted.

Session..........: hashcat
Status...........: Exhausted
Hash.Type........: MD5
Hash.Target......: md5_passwords.txt
Time.Started.....: Sun Aug  4 08:44:03 2019 (0 secs)
Time.Estimated...: Sun Aug  4 08:44:03 2019 (0 secs)
Guess.Mask.......: ?1?2 [2]
Guess.Charset....: -1 ?l?d?u, -2 ?l?d, -3 ?l?d*!$@_, -4 Undefined
Guess.Queue......: 2/15 (13.33%)
Speed.Dev.#2.....:   175.2 kH/s (0.09ms) @ Accel:16 Loops:7 Thr:256 Vec:1
Recovered........: 0/3 (0.00%) Digests, 0/1 (0.00%) Salts
Progress.........: 2232/2232 (100.00%)
Rejected.........: 0/2232 (0.00%)
Restore.Point....: 36/36 (100.00%)
Candidates.#2....: Oa -> Xq

# skipped

Approaching final keyspace - workload adjusted.

Session..........: hashcat
Status...........: Exhausted
Hash.Type........: MD5
Hash.Target......: md5_passwords.txt
Time.Started.....: Sun Aug  4 08:44:04 2019 (0 secs)
Time.Estimated...: Sun Aug  4 08:44:04 2019 (0 secs)
Guess.Mask.......: ?1?2?2 [3]
Guess.Charset....: -1 ?l?d?u, -2 ?l?d, -3 ?l?d*!$@_, -4 Undefined
Guess.Queue......: 3/15 (20.00%)
Speed.Dev.#2.....:  6378.7 kH/s (0.12ms) @ Accel:16 Loops:7 Thr:256 Vec:1
Recovered........: 0/3 (0.00%) Digests, 0/1 (0.00%) Salts
Progress.........: 80352/80352 (100.00%)
Rejected.........: 0/80352 (0.00%)
Restore.Point....: 1296/1296 (100.00%)
Candidates.#2....: Oar -> Xqx

# skipped

Approaching final keyspace - workload adjusted.

Session..........: hashcat
Status...........: Exhausted
Hash.Type........: MD5
Hash.Target......: md5_passwords.txt
Time.Started.....: Sun Aug  4 08:44:04 2019 (0 secs)
Time.Estimated...: Sun Aug  4 08:44:04 2019 (0 secs)
Guess.Mask.......: ?1?2?2?2 [4]
Guess.Charset....: -1 ?l?d?u, -2 ?l?d, -3 ?l?d*!$@_, -4 Undefined
Guess.Queue......: 4/15 (26.67%)
Speed.Dev.#2.....:   130.1 MH/s (2.15ms) @ Accel:16 Loops:15 Thr:256 Vec:1
Recovered........: 0/3 (0.00%) Digests, 0/1 (0.00%) Salts
Progress.........: 2892672/2892672 (100.00%)
Rejected.........: 0/2892672 (0.00%)
Restore.Point....: 46656/46656 (100.00%)
Candidates.#2....: Uari -> Xqxv

Approaching final keyspace - workload adjusted.

Session..........: hashcat
Status...........: Exhausted
Hash.Type........: MD5
Hash.Target......: md5_passwords.txt
Time.Started.....: Sun Aug  4 08:44:04 2019 (1 sec)
Time.Estimated...: Sun Aug  4 08:44:05 2019 (0 secs)
Guess.Mask.......: ?1?2?2?2?2 [5]
Guess.Charset....: -1 ?l?d?u, -2 ?l?d, -3 ?l?d*!$@_, -4 Undefined
Guess.Queue......: 5/15 (33.33%)
Speed.Dev.#2.....:   197.6 MH/s (7.22ms) @ Accel:16 Loops:15 Thr:256 Vec:1
Recovered........: 0/3 (0.00%) Digests, 0/1 (0.00%) Salts
Progress.........: 104136192/104136192 (100.00%)
Rejected.........: 0/104136192 (0.00%)
Restore.Point....: 1679616/1679616 (100.00%)
Candidates.#2....: Upf5u -> Xqxvq

e10adc3949ba59abbe56e057f20f883e:123456
610ef919a73003ec8b73e434fb3116d4:lp0520
Approaching final keyspace - workload adjusted.


Session..........: hashcat
Status...........: Exhausted
Hash.Type........: MD5
Hash.Target......: md5_passwords.txt
Time.Started.....: Sun Aug  4 08:44:05 2019 (14 secs)
Time.Estimated...: Sun Aug  4 08:44:19 2019 (0 secs)
Guess.Mask.......: ?1?2?2?2?2?2 [6]
Guess.Charset....: -1 ?l?d?u, -2 ?l?d, -3 ?l?d*!$@_, -4 Undefined
Guess.Queue......: 6/15 (40.00%)
Speed.Dev.#2.....:   267.3 MH/s (2.96ms) @ Accel:32 Loops:8 Thr:256 Vec:1
Recovered........: 2/3 (66.67%) Digests, 0/1 (0.00%) Salts
Progress.........: 3748902912/3748902912 (100.00%)
Rejected.........: 0/3748902912 (0.00%)
Restore.Point....: 1679616/1679616 (100.00%)
Candidates.#2....: Wq4tpv -> Xqqfqx

3a3a5c3f10e14cc9d5e92127a0ee0880:vjht008

Session..........: hashcat
Status...........: Cracked
Hash.Type........: MD5
Hash.Target......: md5_passwords.txt
Time.Started.....: Sun Aug  4 08:44:19 2019 (53 secs)
Time.Estimated...: Sun Aug  4 08:45:12 2019 (0 secs)
Guess.Mask.......: ?1?2?2?2?2?2?2 [7]
Guess.Charset....: -1 ?l?d?u, -2 ?l?d, -3 ?l?d*!$@_, -4 Undefined
Guess.Queue......: 7/15 (46.67%)
Speed.Dev.#2.....:   245.0 MH/s (11.60ms) @ Accel:32 Loops:8 Thr:256 Vec:1
Recovered........: 3/3 (100.00%) Digests, 1/1 (100.00%) Salts
Progress.........: 13517193216/134960504832 (10.02%)
Rejected.........: 0/13517193216 (0.00%)
Restore.Point....: 0/1679616 (0.00%)
Candidates.#2....: ebmeran -> oowb3ye

Started: Sun Aug  4 08:44:00 2019
Stopped: Sun Aug  4 08:45:12 2019

real	1m12.052s
user	0m2.602s
sys	0m4.428s

This finished in a little bit over a minute. Notice how cracking speed rises with longer combinations that allowed to utilize the GPU in full. At peek this was 267 million combinations per second. and this is achieved using non-sophisticated integrated graphic card from business laptop.

PBKDF2 cracking

This is where I did not finish the attacks. If I wanted to wait for the brute force I would need to wait over 230 days for it to finish, so after some time I just hit CTRL+C.

$ time hashcat -D 2 -m 10000 -a 3 pbkdf2_passwords.txt
hashcat (v4.2.1) starting...

# skipped

Session..........: hashcat
Status...........: Running
Hash.Type........: Django (PBKDF2-SHA256)
Hash.Target......: pbkdf2_passwords.txt
Time.Started.....: Sun Aug  4 10:53:02 2019 (2 hours, 44 mins)
Time.Estimated...: Tue Mar 24 06:58:25 2020 (232 days, 18 hours)
Guess.Mask.......: ?1?2?2?2?2?2 [6]
Guess.Charset....: -1 ?l?d?u, -2 ?l?d, -3 ?l?d*!$@_, -4 Undefined
Guess.Queue......: 6/15 (40.00%)
Speed.Dev.#2.....:      559 H/s (10.06ms) @ Accel:8 Loops:2 Thr:256 Vec:1
Recovered........: 0/3 (0.00%) Digests, 0/3 (0.00%) Salts
Progress.........: 5505024/11246708736 (0.05%)
Rejected.........: 0/5505024 (0.00%)
Restore.Point....: 0/60466176 (0.00%)
Candidates.#2....: Oarier -> Oc7223

[s]tatus [p]ause [b]ypass [c]heckpoint [q]uit => ^C

real	270m51.501s
user	16m24.450s
sys	38m28.882s

I did manage to accomplish the dictionary attack on the other hand.

$ time hashcat -D 2 -m 10000 -a 0 pbkdf2_passwords.txt  passwords.txt
hashcat (v4.2.1) starting...

# skipped

pbkdf2_sha256$36000$wzi7098ecXQO$5StEa0bsyZUgX+g8B8yN05KdQkH26D6xykUST9Nm4iM=:123456
pbkdf2_sha256$36000$5ZI2h8VP2VrZ$zhlEVEYdPthCpdlZPNIgoTbxqJTOFbiJMjCkHyEkDcA=:lp0520
Approaching final keyspace - workload adjusted.

pbkdf2_sha256$36000$Ax8dKgCTBmyo$IIj8oIM5HemI0qLwZq52FaYLzr4DmAH1LJwsLz4gplc=:vjht008

Session..........: hashcat
Status...........: Cracked
Hash.Type........: Django (PBKDF2-SHA256)
Hash.Target......: pbkdf2_passwords.txt
Time.Started.....: Sun Aug  4 13:39:41 2019 (49 mins, 22 secs)
Time.Estimated...: Sun Aug  4 14:29:03 2019 (0 secs)
Guess.Base.......: File (passwords.txt)
Guess.Queue......: 1/1 (100.00%)
Speed.Dev.#2.....:      570 H/s (1.64ms) @ Accel:8 Loops:2 Thr:256 Vec:1
Recovered........: 3/3 (100.00%) Digests, 3/3 (100.00%) Salts
Progress.........: 2999997/2999997 (100.00%)
Rejected.........: 0/2999997 (0.00%)
Restore.Point....: 983040/999999 (98.30%)
Candidates.#2....: vrs4983 -> vjht008

Started: Sun Aug  4 13:39:36 2019
Stopped: Sun Aug  4 14:29:04 2019

real	49m28.392s
user	1m0.759s
sys	1m53.776s

This took nearly 1 hour to finish. As you can see it managed to check 570 combinations per second.

Comment on the results

OK, so first things first: on my machine PBKDF2 lowered the speed as much as to 0,0002% as compared to md5 cracking (570 checks compared to 267 millions). This was achieved both by salting and using slower hashing algorithm. The difference could be made even greater if I introduced changes to the default hasher. Here you can find a description how it can be done.

Second: using passwords that are simple words, or can be found on lists like n most popular passwords is asking for trouble. Nothing is going to save you if you have such a password. Remember that I use low-end graphic card and not a dedicated cluster. The longer the password, the better. Long enough passwords that cannot be guessed from a dictionary can withstand even the shittiest hashing algorithm.

Comparison of hashing algorithms reluctancy to cracking
Source: https://twitter.com/TerahashCorp/status/1155112559206383616/photo/1

13 and longer passwords will survive every attack we know today. Yes, eventually it can be cracked, but sometimes it’s enough not to be the slowest rather than being the fastest that will save you from danger.

So, if I was to give you an advice it would be:

  1. if you are an average user – take advantage of password managers. Can be built in browser if that’s OK for your threat model, can be LastPass, can be anything (I use KeePassXC and Safari’s built in manager for non-important tasks like subscribing to exercises on a gym, where I do not leave any personal data and I do not make payments). Set it to give you long random passwords (32 characters with lowercase, uppercase and special characters and digits will do).
  2. If you are responsible for designing password storing system at least go through this article first and check what smarter people write about it when the technology you are using is considered. I have given you an example of docs in framework I work with, I am sure you’ll find some decent sources when it comes to Spring, Rails and whatever you want it to be.

Last but not least, I am curious how this experiment would look like if I used real equipment and not toys like the 48-core graphic card. If you can download the repo and repeat the attacks using something decent and share the outcome in comments, I’ll be grateful. And I encourage you to leave comments anyway 😉