レインボーテーブルを実装してみた
どうも。duckです。
名前がダサいことで有名なレインボーテーブルを実装してみました。ハッシュを調べるとよく出てくるやつです。
ハッシュ概要
ハッシュ関数をとする。あるデータに対して、そのハッシュ値を
と表す。
この時、ハッシュ値からは元のデータを計算できない。これがハッシュ関数の満たすべき性質である。
この性質から、認証によく使われるが詳細は割愛。
アタッカーの目線から考えるとハッシュから元のデータを逆算したい。だができない。
そこで、事前にあらゆるデータを計算し、そのハッシュ値をデータベースに保存して逆引きすればよいという発想になる。
しかし、この方法には膨大な記憶領域を必要とするという弱点がある。
そこで考え出されたのがレインボーテーブルである。
理論編
レインボーテーブルとは、計算コストがかかる代わりに、データベースの保存領域を減らすものである。例えば以下のレインボーテーブルの例ではデータベースに(最初と最後)のみ保存すれば、のすべてのハッシュを逆算できる。
:リダクション関数、ハッシュ値からデータを作れれば何でもよい。
例としての逆算を考える。
手順は以下の通り
①がデータベースと一致するか調べる。一致すると復元できる。
②を計算する。
③を計算する。
①に戻る。
は上記の手順を2周したときにデータベースと一致する。レインボーテーブル全体は4周(chainとよぶ)しているため、以下の手順でからを復元できる。
chainを増やせばハッシュのヒット率が上がり復元できる確率が高まる。しかし計算コストは増大する。
そこで並列計算を考える。同じ仕組みの列を増やす。
例えば、
とし
データベースにはを保存する。これで手順の最大周回数を保ったまま、ヒット率を上げられた。
の決め方にほとんど言及していないがそれは実践編を参考にしてほしい。
ぶっちゃけなんでもいい。
実践編
まずはコード。
from hashlib import md5 from random import randint class ReinbowTable(): def __init__(self,line,chain): self.line = line self.chain = chain self.rainbow_table = [] self.seed = 0 def generate_line(self): rainbow_line=[] self.seed = format(randint(0,2**16 - 1),'x') rainbow_line.append(self.seed) for _ in range(self.chain): hash = md5(self.seed.encode()).hexdigest() rainbow_line.append(hash) self.seed = format(self.reduction(hash),'x') return [rainbow_line[0],rainbow_line[-1]] def create_rainbow_table(self): for _ in range(self.line): self.rainbow_table.append(self.generate_line()) def reduction(self,hash): return int(hash,16) & 0xffff def check_rainbow_table(self,check_hash): init_check_hash = check_hash chainline = [] for chain in range(self.chain): line = 0 flag = False for rainbow_hash in self.rainbow_table: if check_hash == rainbow_hash[-1]: chainline.append([chain,line]) line += 1 check = format(self.reduction(check_hash),'x') check_hash = md5(check.encode()).hexdigest() for c,l in chainline: seed = self.rainbow_table[l][0] for _ in range(self.chain - c - 1): hash = md5(seed.encode()).hexdigest() seed = format(self.reduction(hash),'x') if md5(seed.encode()).hexdigest() == init_check_hash: return seed return "not found"
今回の例
:乱数を使って生成
ハッシュ:md5を使用
:ハッシュ値の下四桁をとってくるだけ
一般にはほしいデータから決める。
例えば英数字8桁(パスワードにありがち)のデータのハッシュを復元したいなら、はハッシュ値から英数字8桁を生成する関数を考えるし、
数字のみ6桁~10桁のデータのハッシュ値を復元したいなら、はハッシュ値から数字のみ6桁~10桁を生成する関数を考える。
今回は数字とa~fのみ使う四桁のデータを復元できるようにを決めた。
実行例
r=ReinbowTable(1000,1000) r.create_rainbow_table() print(r.check_rainbow_table('ad7ed5d47b9baceb12045a929e7e2f66'))
結果
6387
やったぜ。復元できた!
ただ、今回の方法では、a~fのみ使う四桁のデータのハッシュを使っても復元できるハッシュが限られてしまうことがわかった。
r=ReinbowTable(1000,1000) r.create_rainbow_table() print(r.check_rainbow_table('81dc9bdb52d04dc20036dbd8313ed055'))
結果
not found
81dc9bdb52d04dc20036dbd8313ed055は1234のハッシュ値なのだが何回やっても復元できなかった。
は乱数を使っているので、そのうち復元できそうなのに。
おそらく、の決め方が悪く、偏ってしまっているのだと考えられる。
の際に現れるに偏りを生じ、となりにくい(もしくはならない)ということかと。
こういった偏りをなくすようには決定しないといけないということか。md5の中身まで理解しないとそこまでは無理。
は何でもいいといったけれど、意外にも職人芸だった。
実装してみると気が付くこともある。