Rainbow table
Rainbow table
해시는 포렌식에서 무결성을 검증하는 중요한 도구로 사용된다. 해시 함수의 주요 특징은 다음과 같다.
- 입력이 한 비트만 바뀌어도 결과 전체가 완전하게 바뀐다.(눈사태 효과)
- 두 해시 값이 다르다면 원래의 데이 터도 다르다.
- 같은 해시 값을 갖더라도 원래의 입력값이 같다는 것을 보장하지 않는다.(단사함수가 아님)
특히 첫 번째 특징 때문에 해시는 데이터의 무결성을 검증하는 도구로 사용되고, 결과만 보고 입력을 유추하는 것은 불가능해진다. 그런데, 입력의 경우의 수가 유한한 경우 모든 입력에 대해 해시값을 구한 후 유지하고 있으면, 향후 해시값을 가지고 이 값을 만들어낸 원본 데이터를 찾아낼 수 있는데 이런 자료구조를 레인보우 테이블이라고 한다.
안드로이드의 패턴락 및 카카오톡의 패런락은 사용자의 입력을 내부적으로 해시를 통해 저장하는데, 패턴락을 입력할 수 있는 경우의 수가 유한하기 때문에, 모든 경우의 수에 대한 패턴락을 미리 계산해 놓으면 추후 증거물에서 저장된 해시값을 데이터베이스의 키로 사용하여 원래의 패턴을 찾아낼 수 있다.
아래 화면은 카카오톡의 패턴락 입력화면인데 좌측 상단부터 1, 2, 3, …, 7, 8, 9의 값을 가진다.
표 1. 카카오톡 패턴락
1에서 다음으로 이동할 수 있는 경우는 { 2, 4, 5 }. 2에서는 { 1, 3, 4, 5, 6 } 이런 식으로 나누어 볼 수 있는데 이것을 정리해 보면 다음과 같은 자료구조로 표현이 가능하다.
Ruby: ranbow_table.rb
1
2
3
4
5
6
7
8
9
10
11
12
@candidate_hash = {
0 => [ 1, 2, 3, 4, 5, 6, 7, 8, 9 ],
1 => [ 2, 4, 5 ],
2 => [ 1, 3, 4, 5, 6 ],
3 => [ 2, 5, 6 ],
4 => [ 1, 2, 5, 7, 8 ],
5 => [ 1, 2, 3, 4, 5, 6, 7, 8, 9],
6 => [ 2, 3, 5, 8, 9],
7 => [ 4, 5, 8 ],
8 => [ 4, 5, 6, 7, 9 ],
9 => [ 5, 6, 8 ]
}
위의 코드에서 키 ‘0’은 사용자가 패턴을 시작하는 첫 지점, 즉 아홉 개의 점에서 어떤 점에서도 선택을 할 수 있다는 것을 표시하고 나머지 1 ~ 9의 경우는 각 점에서 이동할 수 있는 인접한 점(8-neighbor)을 표시한다.
이 데이터를 기반으로 ‘backtracking’을 해서 사용자가 입력가능한 모든 패턴의 경우를 나열하고 이 데이터를 기반으로 레인보우 테이블을 구성하여, 실제 증거물에서 발견된 해시를 기반으로 원래 패턴을 찾아낸다. 레인보우 테이블 구성을 위한 ‘backtracking’ 코드는 다음과 같다.
Ruby: ranbow_table.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#!/usr/bin/env ruby
require "deep_clone"
require "digest/sha1"
class Passcode
def initialize
@sha1 = Digest::SHA1.new
@passcode = {}
@candidates_hash = {
0 => [1, 2, 3, 4, 5, 6, 7, 8, 9], # for start
1 => [2, 4, 5],
2 => [1, 3, 4, 5, 6],
3 => [2, 5, 6],
4 => [1, 2, 5, 7, 8],
5 => [1, 2, 3, 4, 6, 7, 8, 9],
6 => [2, 3, 5, 8, 9],
7 => [4, 5, 8],
8 => [4, 5, 6, 7, 9],
9 => [5, 6, 8]
}
prepare_rainbow_table
end
def passcode_of hash
@passcode[hash]
end
private
def prepare_rainbow_table
4.upto(9) { |i| backtrack([], 0, i) }
end
def process arr
@sha1.reset
arr.each { |e| @sha1 << (e-1).chr }
s = arr.map(&:to_s).reduce(:+) # [1, 2, 3, 4] => "1234"
e = @sha1.hexdigest.upcase
@passcode[e] = s
printf("%s => %s\n",e, s)
end
def backtrack(arr, beg, depth)
process(arr) && return if arr.length == depth
@candidates_hash[beg].each do |c|
unless arr.include?(c)
backtrack(DeepClone.clone(arr).push(c), c, depth)
end
end
end
end
decryptor = Passcode.new
p decryptor.passcode_of("59A3556203C8F6C908D6C6BCE4C5E03F6BF343E3")
Leave a comment