crypto-toolbox 0.1.18 → 0.1.19
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/bin/break-vigenere-xor +6 -1
- data/lib/crypto-toolbox.rb +6 -0
- data/lib/crypto-toolbox/analyzers/padding_oracle/analyzer.rb +65 -39
- data/lib/crypto-toolbox/analyzers/utils/ascii_language_detector.rb +72 -0
- data/lib/crypto-toolbox/analyzers/utils/human_language_detector.rb +37 -0
- data/lib/crypto-toolbox/analyzers/utils/key_candidate_map.rb +19 -8
- data/lib/crypto-toolbox/analyzers/utils/letter_frequency.rb +36 -0
- data/lib/crypto-toolbox/analyzers/utils/spell_checker.rb +28 -12
- data/lib/crypto-toolbox/analyzers/vigenere_xor.rb +97 -46
- data/lib/crypto-toolbox/crypt_buffer.rb +4 -0
- data/lib/crypto-toolbox/crypt_buffer/concerns/arithmetic.rb +12 -0
- data/lib/crypto-toolbox/crypt_buffer/concerns/array.rb +2 -2
- data/lib/crypto-toolbox/crypt_buffer/concerns/convertable.rb +5 -0
- data/lib/crypto-toolbox/crypt_buffer_input_converter.rb +5 -0
- data/lib/crypto-toolbox/crypto_challanges/solver.rb +56 -0
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5e7c5e78c91895522590c24250dced966fcfb2f6
|
4
|
+
data.tar.gz: 55532ae19e4091a32318dc423c59df4e3dc06ad4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8ea2cb29c172ff16ba8f48e7d66393e41caa78a539fb48089038ff38ae8dbed5c130a061f2102522f3fcbfe0e3955a743f65e85ba53aa72142b3e20640965671
|
7
|
+
data.tar.gz: b250861b903d5bbaec6511a0bb8e734b8225916bf369fdd10e7b3d7b09d9ae7d297b97065953facafb80154a835872c58cb7d1c786bf073c00d6b395a379372b
|
data/bin/break-vigenere-xor
CHANGED
@@ -7,5 +7,10 @@ if ARGV[0].nil?
|
|
7
7
|
else
|
8
8
|
ciphertext = ARGV[0]
|
9
9
|
|
10
|
-
Analyzers::VigenereXor.new.analyze(ciphertext)
|
10
|
+
results = Analyzers::VigenereXor.new.analyze(ciphertext)
|
11
|
+
unless results.empty?
|
12
|
+
puts "[Success] Found valid result(s):"
|
13
|
+
puts results.map(&:str)
|
14
|
+
end
|
15
|
+
|
11
16
|
end
|
data/lib/crypto-toolbox.rb
CHANGED
@@ -4,7 +4,11 @@ require 'crypto-toolbox/crypt_buffer_input_converter.rb'
|
|
4
4
|
require 'crypto-toolbox/crypt_buffer.rb'
|
5
5
|
|
6
6
|
require 'crypto-toolbox/analyzers/utils/key_filter.rb'
|
7
|
+
|
8
|
+
require 'crypto-toolbox/analyzers/utils/ascii_language_detector.rb'
|
7
9
|
require 'crypto-toolbox/analyzers/utils/spell_checker.rb'
|
10
|
+
require 'crypto-toolbox/analyzers/utils/human_language_detector.rb'
|
11
|
+
|
8
12
|
require 'crypto-toolbox/analyzers/padding_oracle.rb'
|
9
13
|
require 'crypto-toolbox/analyzers/cbc_mac.rb'
|
10
14
|
require 'crypto-toolbox/analyzers/vigenere_xor.rb'
|
@@ -12,3 +16,5 @@ require 'crypto-toolbox/analyzers/vigenere_xor.rb'
|
|
12
16
|
|
13
17
|
require 'crypto-toolbox/ciphers/caesar.rb'
|
14
18
|
require 'crypto-toolbox/ciphers/rot13.rb'
|
19
|
+
|
20
|
+
require 'crypto-toolbox/crypto_challanges/solver.rb'
|
@@ -8,11 +8,11 @@ module Analyzers
|
|
8
8
|
class Analyzer
|
9
9
|
class FailedAnalysis < RuntimeError; end
|
10
10
|
attr_reader :result
|
11
|
-
|
11
|
+
include ::Utils::Reporting::Console
|
12
12
|
|
13
|
-
def initialize(
|
13
|
+
def initialize(oracle = ::Analyzers::PaddingOracle::Oracles::TcpOracle.new)
|
14
14
|
@result = [ ]
|
15
|
-
@oracle =
|
15
|
+
@oracle = oracle
|
16
16
|
end
|
17
17
|
|
18
18
|
# start with the second to last block to manipulate the final block ( cbc xor behaviour )
|
@@ -24,34 +24,45 @@ module Analyzers
|
|
24
24
|
# changing this byte ^- will change ^- this byte at decryption
|
25
25
|
def analyze(cipher)
|
26
26
|
blocks = CryptBuffer.from_hex(cipher).chunks_of(16)
|
27
|
+
|
28
|
+
# for whatever reason ranges cant be from high to low
|
29
|
+
(1..(blocks.length() -1)).reverse_each do |block_index|
|
30
|
+
result.unshift analyse_block(blocks,block_index)
|
31
|
+
end
|
27
32
|
|
28
|
-
(
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
33
|
+
report_result(result)
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def analyse_block(blocks,block_index)
|
41
|
+
block_result = []
|
42
|
+
|
43
|
+
# manipulate each byte of the 16 byte block
|
44
|
+
1.upto(blocks[block_index -1].length) do |pad_index|
|
45
|
+
with_oracle_connection do
|
46
|
+
jot("processing byte #{pad_index} in block: #{block_index -1} => #{block_index}",debug: true)
|
47
|
+
byte = read_byte(pad_index,block_result,blocks,block_index)
|
48
|
+
block_result.unshift byte
|
39
49
|
end
|
40
|
-
result.unshift result_part
|
41
50
|
end
|
51
|
+
block_result
|
52
|
+
end
|
53
|
+
|
54
|
+
def report_result(result)
|
42
55
|
jot(CryptBuffer(result.flatten).chars.inspect,debug: false)
|
43
56
|
jot("stripping padding!",debug: true)
|
44
57
|
jot(CryptBuffer(result.flatten).strip_padding.str,debug: false)
|
45
58
|
end
|
46
59
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
puts message
|
52
|
-
end
|
60
|
+
def with_oracle_connection
|
61
|
+
@oracle.connect
|
62
|
+
yield
|
63
|
+
@oracle.disconnect
|
53
64
|
end
|
54
|
-
|
65
|
+
|
55
66
|
def apply_found_bytes(buf,cur_result,pad_index)
|
56
67
|
# first we have to apply all the already found bytes
|
57
68
|
|
@@ -62,38 +73,53 @@ module Analyzers
|
|
62
73
|
buf.xor(other)
|
63
74
|
end
|
64
75
|
|
65
|
-
|
76
|
+
# the blocks are:
|
77
|
+
# xxxxxxxx xxxxxxxx xxxxxxxx [..]
|
78
|
+
# ^- IV ^- first ^- second ...
|
66
79
|
def read_byte(pad_index,cur_result,blocks,block_index)
|
67
|
-
#iv, first, second, last
|
68
80
|
jot(cur_result.inspect,debug: true)
|
69
81
|
|
70
82
|
# apply all the current-result bytes to the block corresponding to <block_index>
|
71
83
|
# and store the result in a buffer we will mess with
|
72
|
-
#
|
73
84
|
forge_buf = apply_found_bytes(blocks[block_index - 1],cur_result,pad_index)
|
74
85
|
|
75
86
|
1.upto 256 do |guess|
|
76
|
-
|
77
|
-
subset = blocks[0,block_index+1]
|
78
|
-
subset[block_index -1 ] = forge_buf.xor_at([guess,pad_index], -1 * pad_index)
|
79
|
-
|
80
|
-
input = subset.map(&:bytes).flatten
|
87
|
+
input = assemble_oracle_input(forge_buf,blocks,block_index,pad_index,guess)
|
81
88
|
|
82
|
-
|
83
|
-
# otherwise the resulting ciphertext would eq the original input
|
84
|
-
#next if input == blocks.map(&:bytes).flatten
|
85
|
-
next if guess == pad_index && guess == 1 && block_index == 2
|
86
|
-
|
87
|
-
block_amount = block_index + 1
|
88
|
-
if @oracle.valid_padding?(input,block_amount)
|
89
|
-
return guess
|
90
|
-
end
|
89
|
+
next if skip?(pad_index,block_index,guess,cur_result)
|
91
90
|
|
91
|
+
return guess if@oracle.valid_padding?(input,block_amount(block_index))
|
92
92
|
end
|
93
93
|
|
94
94
|
raise FailedAnalysis, "No padding found... this should neve happen..."
|
95
95
|
end
|
96
|
+
private
|
96
97
|
|
98
|
+
# include the block after the index, since this
|
99
|
+
# is the one effected by our manipulation. ( due to cbc mode )
|
100
|
+
def block_amount(index)
|
101
|
+
index +1
|
102
|
+
end
|
103
|
+
|
104
|
+
# Create a subset to only send the blocks we still need to decrypt.
|
105
|
+
# manipulate the byte with a padding-index and a guess
|
106
|
+
# map the crypt buffer array to a flat array of integers ( representing bytes )
|
107
|
+
def assemble_oracle_input(buffer,blocks,block_index,pad_index,guess)
|
108
|
+
# the bytes from the subset we will send to the padding oracle
|
109
|
+
subset = blocks[0,block_index+1]
|
110
|
+
subset[block_index -1 ] = buffer.xor_at([guess,pad_index], -1 * pad_index)
|
111
|
+
subset.map(&:bytes).flatten
|
112
|
+
end
|
113
|
+
|
114
|
+
# In case of the first iteration there is a special case to skip:
|
115
|
+
# 1) No other blocks have been decrypted yet ( result.empty? )
|
116
|
+
# 2) No bytes of the current block have been processed yet ( block_result_empty? )
|
117
|
+
# 3) guess xor pad-index does not modify anything ( eq zero )
|
118
|
+
# => This would leed to the original ciphertext without any modification beeing sent
|
119
|
+
def skip?(pad_index,block_index,guess,block_result)
|
120
|
+
result.empty? && block_result.empty? && (guess ^ pad_index).zero?
|
121
|
+
end
|
122
|
+
|
97
123
|
end
|
98
124
|
end
|
99
125
|
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module Analyzers
|
2
|
+
module Utils
|
3
|
+
class AsciiLanguageDetector
|
4
|
+
ASCII_BASE_RANGE=(32..127).freeze
|
5
|
+
ASCII_BLACKLIST = [40,41,42,43,47,60,61,62,91,92,93,94,95,96,35,59].freeze
|
6
|
+
ASCII_WHITELIST = [10]
|
7
|
+
ASCII_CHARACTERS = ( ASCII_BASE_RANGE.to_a + ASCII_WHITELIST - ASCII_BLACKLIST ).to_ary.freeze # 10 == \n is now allowed!
|
8
|
+
=begin
|
9
|
+
NOTE: This is the output of the benchmark script contained in this gem
|
10
|
+
see: benchmarks/language_detection.rb
|
11
|
+
It compares many ways of filtering bytes to check if only "plain" language
|
12
|
+
characters are contained. Result:
|
13
|
+
|
14
|
+
Comparison:
|
15
|
+
ascii_range_check: 1773.5 i/s <- use range.cover? and then blacklist.include
|
16
|
+
ascii_lingual_byte?: 1494.8 i/s - 1.19x slower <- now uses range.cover? internally
|
17
|
+
ascii_lingual_bytes?: 1459.2 i/s - 1.22x slower <- see prev. but get the entire byte array
|
18
|
+
ascii_lingual?: 1420.1 i/s - 1.25x slower <- see prev. but works on crypt buffers
|
19
|
+
ascii_lingual_and_human_language: 1413.6 i/s - 1.25x slower <- use human_languge?, but apply 0 < byte < 127 first
|
20
|
+
ascii_shift_check: 634.4 i/s - 2.80x slower <- uses & (1 << 5).zero? but has to do slow additional checks
|
21
|
+
ascii_whitelist.bsearch?: 483.8 i/s - 3.50x slower <- whitelist lookup using bsearch
|
22
|
+
hunspell.human_language?: 212.3 i/s - 8.35x slower <- use human_languge?
|
23
|
+
ascii_whitelist.include?: 90.2 i/s - 19.67x slower <- use (whitelist - blacklist).include?
|
24
|
+
hunspell_human_language_without_dict: 0.2 i/s - 10013.62x slower <- instanciating the dict seems to be very very slow...
|
25
|
+
|
26
|
+
NOTE:
|
27
|
+
Normally the shift solution would be the fastes, but we have to convert back and forth,
|
28
|
+
thus the range.cover? check still seems to be the best soution. It is also more readable
|
29
|
+
|
30
|
+
(We need the chr.downcase.ord conversion to support upper case letters)
|
31
|
+
byte < 127 && !(byte.chr.downcase.ord & (1 << 5)).zero?
|
32
|
+
=end
|
33
|
+
def ascii_lingual_byte?(byte)
|
34
|
+
# check how fast bsearch is, if range.cover is no longer needed we can nicely add 10 to the array
|
35
|
+
(ascii_base_range.cover?(byte) && !ascii_blacklist.include?(byte)) || ( ascii_whitelist.bsearch{|i| i == byte} )
|
36
|
+
end
|
37
|
+
|
38
|
+
def ascii_lingual_bytes?(bytes)
|
39
|
+
bytes.all?{|b| ascii_lingual_byte?(b) }
|
40
|
+
end
|
41
|
+
|
42
|
+
def ascii_lingual_chars
|
43
|
+
ASCII_CHARACTERS
|
44
|
+
end
|
45
|
+
|
46
|
+
def ascii_lingual?(buf)
|
47
|
+
ascii_lingual_bytes?(buf.bytes)
|
48
|
+
end
|
49
|
+
|
50
|
+
def ascii_lingual_bytes
|
51
|
+
ascii_whitelist.to_ary
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
# building up the range is too slow, thus we cache
|
57
|
+
def ascii_base_range
|
58
|
+
ASCII_BASE_RANGE
|
59
|
+
end
|
60
|
+
|
61
|
+
def ascii_whitelist
|
62
|
+
ASCII_WHITELIST
|
63
|
+
end
|
64
|
+
|
65
|
+
def ascii_blacklist
|
66
|
+
ASCII_BLACKLIST
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Analyzers
|
2
|
+
module Utils
|
3
|
+
# NOTE the implementation decisions are based on the result of
|
4
|
+
# benchmarks/language_detector.rb
|
5
|
+
class HumanLanguageDetector
|
6
|
+
def initialize
|
7
|
+
@spell_checker = ::Analyzers::Utils::SpellChecker.new
|
8
|
+
@ascii_checker = ::Analyzers::Utils::AsciiLanguageDetector.new
|
9
|
+
end
|
10
|
+
|
11
|
+
# NOTE: we dont use the human_language? method
|
12
|
+
# to be faster at processing and more idiomatic
|
13
|
+
def human_language_entries(buffers,spellcheck: true )
|
14
|
+
filtered = buffers.select{|b| ascii_valid?(b) }
|
15
|
+
if spellcheck
|
16
|
+
buffers.select{|b| spell_valid?(b) }
|
17
|
+
else
|
18
|
+
filtered
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def human_language?(buffer)
|
23
|
+
ascii_valid?(buffer) && spell_valid?(buffer)
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def ascii_valid?(buf)
|
29
|
+
@ascii_checker.ascii_lingual?(buf)
|
30
|
+
end
|
31
|
+
|
32
|
+
def spell_valid?(buf)
|
33
|
+
@spell_checker.human_language?(buf.str)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -11,6 +11,9 @@ module Analyzers
|
|
11
11
|
# }
|
12
12
|
include ::Utils::Reporting::Console
|
13
13
|
|
14
|
+
def initialize
|
15
|
+
@lang_detector = Analyzers::Utils::AsciiLanguageDetector.new
|
16
|
+
end
|
14
17
|
# factory method for easy use
|
15
18
|
def self.create(input_buf,keylen)
|
16
19
|
new.run(input_buf,keylen)
|
@@ -22,9 +25,19 @@ module Analyzers
|
|
22
25
|
# 3) xor any possible byte value (guess) with all nth's bytes
|
23
26
|
# 4) select those guesses that decipher the nth-byte stream to only english plain ascii chars
|
24
27
|
def run(input_buf,keylen)
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
+
#return run2(input_buf,keylen)
|
29
|
+
detector = Analyzers::Utils::HumanLanguageDetector.new
|
30
|
+
|
31
|
+
candidate_map = (0..(keylen-1)).each_with_object({}) do |key_byte_pos,hsh|
|
32
|
+
=begin
|
33
|
+
# Letter frquency testing
|
34
|
+
freqs = letter_freq(nth_byte_stream.xor_all_with(guess).str)
|
35
|
+
diff = FREQUENCIES.keys - freqs.keys
|
36
|
+
binding.pry if nth_byte_stream.xor_all_with(guess).bytes.all?{|byte| acceptable_char?(byte) } &&
|
37
|
+
((diff.map{|c| FREQUENCIES[c]}.reduce(&:+)||0) > 16)
|
38
|
+
=end
|
39
|
+
|
40
|
+
|
28
41
|
# create an array of every nth byte of the input. ( thus a pseudo stream of the nth bytes )
|
29
42
|
# 1) create an enumerator of the nth positions. e.g for iteration 0: [0,7,14,...]
|
30
43
|
# 2) Next: Map the positions to bytes of the input buffer
|
@@ -34,9 +47,9 @@ module Analyzers
|
|
34
47
|
# nth_byte_stream2 = CryptBuffer.new(nth_stream)
|
35
48
|
|
36
49
|
nth_byte_stream = input_buf.nth_bytes(keylen,offset: key_byte_pos)
|
37
|
-
|
50
|
+
hsh[key_byte_pos] = 0.upto(255).select{|guess| nth_byte_stream.xor_all_with(guess).bytes.all?{|byte| acceptable_char?(byte) } }
|
38
51
|
|
39
|
-
jot("found #{
|
52
|
+
jot("found #{hsh[key_byte_pos].inspect} bytes for position: #{key_byte_pos}",debug: true)
|
40
53
|
end
|
41
54
|
candidate_map
|
42
55
|
end
|
@@ -45,10 +58,8 @@ module Analyzers
|
|
45
58
|
|
46
59
|
# Checks if a given byte maps to a reasonable english language character
|
47
60
|
def acceptable_char?(byte)
|
48
|
-
(byte
|
61
|
+
@lang_detector.ascii_lingual_byte?(byte)
|
49
62
|
end
|
50
|
-
|
51
|
-
|
52
63
|
end
|
53
64
|
end
|
54
65
|
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Analyzers
|
2
|
+
module Utils
|
3
|
+
class LetterFrequency
|
4
|
+
|
5
|
+
FREQUENCIES={
|
6
|
+
' ' => 20, # ??
|
7
|
+
'e' => 12.02,
|
8
|
+
't' => 9.10,
|
9
|
+
'a' => 8.12,
|
10
|
+
'o' => 7.68,
|
11
|
+
'i' => 7.31,
|
12
|
+
'n' => 6.95,
|
13
|
+
's' => 6.28,
|
14
|
+
'r' => 6.02,
|
15
|
+
'h' => 5.92,
|
16
|
+
'd' => 4.32,
|
17
|
+
'l' => 3.98,
|
18
|
+
'u' => 2.88,
|
19
|
+
'c' => 2.71
|
20
|
+
}
|
21
|
+
def letter_count(str)
|
22
|
+
str.downcase.each_char.with_object({}) do |c,h|
|
23
|
+
h[c] = (h.fetch(c,0) + 1) if c =~ /[A-Za-z ]/
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def letter_freq(str)
|
28
|
+
counts = letter_count(str)
|
29
|
+
quotient = counts.values.reduce(&:+).to_f
|
30
|
+
counts.sort_by{|k,v| v}.reverse.to_h.each_with_object({}){|(k,v),hsh| hsh[k] = (v/quotient) }
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -1,10 +1,8 @@
|
|
1
1
|
require 'ffi/hunspell'
|
2
2
|
|
3
|
-
|
4
3
|
module Analyzers
|
5
4
|
module Utils
|
6
5
|
class SpellChecker
|
7
|
-
|
8
6
|
|
9
7
|
def initialize(dict_lang="en_GB")
|
10
8
|
@dict = FFI::Hunspell.dict(dict_lang)
|
@@ -23,15 +21,18 @@ Some statistics about it:
|
|
23
21
|
8 13 9 534 1319 2809
|
24
22
|
(0.84,0.88] (0.88,0.92] (0.92,0.96] (0.96,1]
|
25
23
|
10581 46598 198477 1440651
|
24
|
+
|
25
|
+
NOTE: There is ony caveat: Short messages with < 5 words may have 33 or 50% error rates
|
26
|
+
if numbers or single char words are taken into account
|
26
27
|
=end
|
27
28
|
def known_words(str)
|
28
|
-
words = str.split(" ").select{|w|
|
29
|
+
words = str.split(" ").select{|w| check?(w) }
|
29
30
|
end
|
30
31
|
|
31
32
|
def human_word?(str)
|
32
|
-
|
33
|
+
check?(str)
|
33
34
|
end
|
34
|
-
|
35
|
+
|
35
36
|
def human_phrase?(string)
|
36
37
|
string.split(" ").all?{|part| human_word?(part)}
|
37
38
|
end
|
@@ -46,19 +47,34 @@ Some statistics about it:
|
|
46
47
|
# Using shell instead of hunspell ffi causes lots of escaping errors, even with shellwords.escape
|
47
48
|
# errors = Float(`echo '#{Shellwords.escape(str)}' |hunspell -l |wc -l `.split.first)
|
48
49
|
def human_language?(str)
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
$stderr.puts error_rate.round(4) if ENV["CRYPTO_TOOBOX_PRINT_ERROR_RATES"]
|
50
|
+
#NOTE should be reject 1char numbers or all 1 char symbols
|
51
|
+
words = str.split(" ").reject{|w| (w.length < 2 || w =~ /^[0-9]+$/) }
|
52
|
+
word_amount = words.length
|
53
|
+
errors = words.map{|e| check?(e) }.count{|e| e == false}
|
55
54
|
|
55
|
+
error_rate = errors.to_f/word_amount
|
56
|
+
|
57
|
+
report_error_rate(str,error_rate) if ENV["DEBUG_ANALYSIS"]
|
58
|
+
|
56
59
|
error_rate_sufficient?(error_rate)
|
57
60
|
end
|
58
61
|
|
59
62
|
private
|
63
|
+
|
64
|
+
def report_error_rate(str,error_rate)
|
65
|
+
if ENV["DEBUG_ANALYSIS"]
|
66
|
+
$stderr.puts "=================================================="
|
67
|
+
$stderr.puts "str: #{str} has error rate: #{error_rate.round(4)}"
|
68
|
+
$stderr.puts "=================================================="
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def check?(input)
|
73
|
+
@dict.check?(input) rescue false
|
74
|
+
end
|
75
|
+
|
60
76
|
def error_rate_sufficient?(rate)
|
61
|
-
rate < 0.
|
77
|
+
rate < 0.20
|
62
78
|
end
|
63
79
|
end
|
64
80
|
end
|
@@ -27,77 +27,128 @@ module Analyzers
|
|
27
27
|
# the error rate of the candidate plaintext using hunspell
|
28
28
|
|
29
29
|
include ::Utils::Reporting::Console
|
30
|
+
|
31
|
+
class HammingDistanceKeyLengthFinder
|
32
|
+
def keylen_for(buffer)
|
33
|
+
offset = 2
|
34
|
+
distances = ((0+offset)..64).map do |keysize|
|
35
|
+
# take the first 4 blocks of keysize length, generate all combinations (6),
|
36
|
+
# map than to normalized hamming distance and take mean
|
37
|
+
buffer.chunks_of(keysize)[0,4].combination(2).map{|a,b| a.hdist(b,normalize: true)}.reduce(&:+) / 6.0
|
38
|
+
end
|
39
|
+
# get the min distance, find its index, convert the keylen
|
40
|
+
distances.min(4).map{|m| distances.index(m)}.map{|i| i + offset }.uniq
|
41
|
+
end
|
42
|
+
end
|
30
43
|
|
31
|
-
|
32
|
-
|
44
|
+
class EightBitPatternFinder
|
45
|
+
include ::Utils::Reporting::Console
|
46
|
+
def keylen_for(buf)
|
47
|
+
# Example: "100100" || nil
|
48
|
+
key_pattern = find_pattern(buf)
|
49
|
+
|
50
|
+
assert_key_pattern!(key_pattern)
|
51
|
+
|
52
|
+
report_pattern_info(key_pattern)
|
53
|
+
|
54
|
+
[key_pattern.length]
|
55
|
+
end
|
33
56
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
57
|
+
private
|
58
|
+
|
59
|
+
def assert_key_pattern!(key_pattern)
|
60
|
+
if key_pattern.nil?
|
61
|
+
$stderr.puts "failed to find keylength by ASCII-8-Bit anlysis"
|
62
|
+
exit(1)
|
40
63
|
end
|
41
|
-
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def report_pattern_info(key_pattern)
|
67
|
+
jot "Found recurring key pattern: #{key_pattern}"
|
68
|
+
jot "Detected key length: #{key_pattern.length}"
|
69
|
+
end
|
70
|
+
|
71
|
+
def find_pattern(buf)
|
72
|
+
bitstring = buf.nth_bits(7).join("")
|
73
|
+
|
74
|
+
1.upto(buf.bytes.length).map do |ksize|
|
75
|
+
parts = bitstring.scan(/.{#{ksize}}/)
|
76
|
+
if parts.uniq.length == 1
|
77
|
+
parts.first
|
78
|
+
else
|
79
|
+
nil
|
80
|
+
end
|
81
|
+
end.compact.first
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
class StaticKeylength
|
86
|
+
def initialize(keylength)
|
87
|
+
@keylength = keylength
|
88
|
+
end
|
89
|
+
def keylen_for(dummy)
|
90
|
+
[@keylength]
|
91
|
+
end
|
42
92
|
end
|
93
|
+
|
94
|
+
|
43
95
|
|
44
|
-
def analyze(input)
|
96
|
+
def analyze(input, keylength_strategy=EightBitPatternFinder.new)
|
45
97
|
buf = CryptBuffer.from_hex(input)
|
46
|
-
## === Should this be extracted into a dedicated class ?
|
47
|
-
# Example: "100100" || nil
|
48
|
-
key_pattern = find_pattern(buf)
|
49
98
|
|
50
|
-
|
51
|
-
|
52
|
-
|
99
|
+
keylength_strategy.keylen_for(buf).map do |keylen|
|
100
|
+
analyse_single(buf,keylen)
|
101
|
+
end.flatten
|
102
|
+
end
|
103
|
+
|
104
|
+
|
105
|
+
|
106
|
+
def analyse_single(buf,key_length)
|
107
|
+
candidate_map = Analyzers::Utils::KeyCandidateMap.create(buf,key_length)
|
108
|
+
|
109
|
+
|
110
|
+
candidate_amount = candidate_map.map{|k,v| v.length}.reduce(&:*)
|
111
|
+
if candidate_amount.zero?
|
112
|
+
jot("no combinations for keylen #{key_length} (at least one byte has no candidates)",debug: true)
|
113
|
+
return []
|
114
|
+
end
|
115
|
+
jot "Amount of candidate keys: #{candidate_map.map{|k,v| v.length}.reduce(&:*)}. Starting Permutation (RAM intensive)",debug: true
|
53
116
|
|
54
|
-
##====
|
55
|
-
|
56
|
-
candidate_map = Analyzers::Utils::KeyCandidateMap.create(buf,key_pattern.length)
|
57
|
-
jot "Amount of candidate keys: #{candidate_map.map{|k,v| v.length}.reduce(&:*)}. Starting Permutation (RAM intensive)"
|
58
117
|
|
59
118
|
# split the candidate map into head and*tail to create the prduct of all combinations
|
60
119
|
head,*tail = candidate_map.map{|k,v|v}
|
61
|
-
|
120
|
+
begin
|
121
|
+
combinations = head.product(*tail)
|
122
|
+
# we simply skip too big products
|
123
|
+
rescue RangeError => ex
|
124
|
+
jot "keylen: #{key_length}: #{ex}"
|
125
|
+
return []
|
126
|
+
end
|
62
127
|
|
128
|
+
|
129
|
+
|
63
130
|
if ENV["DEBUG_ANALYSIS"]
|
64
131
|
ensure_consistent_result!(combinations,candidate_map)
|
65
|
-
print_candidate_decryptions(candidate_map,
|
132
|
+
print_candidate_decryptions(candidate_map,key_length,buf)
|
66
133
|
end
|
134
|
+
|
67
135
|
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
if key_pattern.nil?
|
74
|
-
$stderr.puts "failed to find keylength by ASCII-8-Bit anlysis"
|
75
|
-
exit(1)
|
136
|
+
keys = Analyzers::Utils::KeyFilter::AsciiPlain.new(combinations,buf).filter.reject(&:empty?)
|
137
|
+
|
138
|
+
# return the result, not the key
|
139
|
+
keys.map do|key|
|
140
|
+
key.xor(buf)
|
76
141
|
end
|
77
142
|
end
|
143
|
+
private
|
78
144
|
|
79
|
-
def ensure_consistent_result!(combinations,
|
145
|
+
def ensure_consistent_result!(combinations,candidate_map)
|
80
146
|
# NOTE Consistency check ( enable if you dont trust the generation anymore )
|
81
147
|
# make sure all permutations are still according to the bytes per position map
|
82
148
|
combinations.select do |arr|
|
83
149
|
raise "Inconsistent key candidate combinations" unless arr.map.with_index{|e,i| candidate_map[i].include?(e) }.all?{|e| e ==true}
|
84
150
|
end
|
85
151
|
end
|
86
|
-
|
87
|
-
def report_pattern_info(key_pattern)
|
88
|
-
jot "Found recurring key pattern: #{key_pattern}"
|
89
|
-
jot "Detected key length: #{key_pattern.length}"
|
90
|
-
end
|
91
|
-
|
92
|
-
|
93
|
-
def report_result(results,buf)
|
94
|
-
unless results.empty?
|
95
|
-
jot "[Success] Found valid result(s):"
|
96
|
-
results.each do |r|
|
97
|
-
jot r.xor(buf).str
|
98
|
-
end
|
99
|
-
end
|
100
|
-
end
|
101
152
|
|
102
153
|
def print_candidate_decryptions(candidate_map,keylen,buf)
|
103
154
|
# printout for debugging. (Manual analysis of the characters)
|
@@ -27,7 +27,19 @@ module CryptBufferConcern
|
|
27
27
|
CryptBuffer(tmp)
|
28
28
|
end
|
29
29
|
|
30
|
+
def hdist(other,normalize: false)
|
31
|
+
if normalize
|
32
|
+
hamming_distance(other) / length.to_f
|
33
|
+
else
|
34
|
+
hamming_distance(other)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
30
38
|
private
|
39
|
+
|
40
|
+
def hamming_distance(other)
|
41
|
+
(self ^ other).bits.join.count("1")
|
42
|
+
end
|
31
43
|
|
32
44
|
def sanitize_modulus(mod)
|
33
45
|
(mod > 0) ? mod : 256
|
@@ -23,6 +23,11 @@ module CryptBufferConcern
|
|
23
23
|
def to_s
|
24
24
|
str
|
25
25
|
end
|
26
|
+
|
27
|
+
def base64(strict: true)
|
28
|
+
strict ? Base64.strict_encode64(str) : Base64.encode64(str)
|
29
|
+
end
|
30
|
+
|
26
31
|
private
|
27
32
|
def bytes2hex(bytes)
|
28
33
|
bytes.map{|b| b.to_s(16)}.map{|hs| hs.length == 1 ? "0#{hs}" : hs }.join
|
@@ -0,0 +1,56 @@
|
|
1
|
+
|
2
|
+
module CryptoChallanges
|
3
|
+
class Solver
|
4
|
+
def solve1(input)
|
5
|
+
#CryptoChallanges::Set1::Challange1::Solver.run(input)
|
6
|
+
CryptBuffer.from_hex(input).base64
|
7
|
+
end
|
8
|
+
def solve2(c1,c2)
|
9
|
+
(CryptBuffer.from_hex(c1) ^ CryptBuffer.from_hex(c2)).hex.downcase
|
10
|
+
end
|
11
|
+
|
12
|
+
def letter_count(str)
|
13
|
+
str.downcase.each_char.with_object({}) do |c,h|
|
14
|
+
h[c] = (h.fetch(c,0) + 1) if c =~ /[A-Za-z ]/
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def letter_freq(str)
|
19
|
+
counts = letter_count(str)
|
20
|
+
quotient = counts.values.reduce(&:+).to_f
|
21
|
+
counts.sort_by{|k,v| v}.reverse.to_h.each_with_object({}){|(k,v),hsh| hsh[k] = (v/quotient) }
|
22
|
+
end
|
23
|
+
|
24
|
+
def solve3(input)
|
25
|
+
candidates = (1..256).map{ |guess| CryptBuffer.from_hex(input).xor_all_with(guess) }
|
26
|
+
detector = Analyzers::Utils::HumanLanguageDetector.new
|
27
|
+
|
28
|
+
detector.human_language_entries(candidates).first.to_s
|
29
|
+
end
|
30
|
+
|
31
|
+
def solve4(hexstrings)
|
32
|
+
detector = Analyzers::Utils::HumanLanguageDetector.new
|
33
|
+
result = hexstrings.map{|h| CryptBuffer.from_hex(h)}.map.with_index do |c,i|
|
34
|
+
candidates = (1..256).map{ |guess| c.xor_all_with(guess) }
|
35
|
+
matches = detector.human_language_entries(candidates)
|
36
|
+
|
37
|
+
matches.empty? ? nil : matches
|
38
|
+
end
|
39
|
+
result.flatten.compact.map(&:str).first
|
40
|
+
end
|
41
|
+
|
42
|
+
def solve5(input,key)
|
43
|
+
CryptBuffer(input).xor(key,expand_input: true).hex
|
44
|
+
end
|
45
|
+
|
46
|
+
def solve6(input)
|
47
|
+
buffer = CryptBuffer.from_base64(input)
|
48
|
+
Analyzers::VigenereXor.new.analyze(buffer.hex,Analyzers::VigenereXor::HammingDistanceKeyLengthFinder.new)
|
49
|
+
end
|
50
|
+
|
51
|
+
def solve7(input,key)
|
52
|
+
end
|
53
|
+
|
54
|
+
|
55
|
+
end
|
56
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: crypto-toolbox
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.19
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Dennis Sivia
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-05-
|
11
|
+
date: 2015-05-13 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: aes
|
@@ -59,8 +59,11 @@ files:
|
|
59
59
|
- lib/crypto-toolbox/analyzers/padding_oracle/analyzer.rb
|
60
60
|
- lib/crypto-toolbox/analyzers/padding_oracle/oracles/http_oracle.rb
|
61
61
|
- lib/crypto-toolbox/analyzers/padding_oracle/oracles/tcp_oracle.rb
|
62
|
+
- lib/crypto-toolbox/analyzers/utils/ascii_language_detector.rb
|
63
|
+
- lib/crypto-toolbox/analyzers/utils/human_language_detector.rb
|
62
64
|
- lib/crypto-toolbox/analyzers/utils/key_candidate_map.rb
|
63
65
|
- lib/crypto-toolbox/analyzers/utils/key_filter.rb
|
66
|
+
- lib/crypto-toolbox/analyzers/utils/letter_frequency.rb
|
64
67
|
- lib/crypto-toolbox/analyzers/utils/spell_checker.rb
|
65
68
|
- lib/crypto-toolbox/analyzers/vigenere_xor.rb
|
66
69
|
- lib/crypto-toolbox/ciphers/caesar.rb
|
@@ -76,6 +79,7 @@ files:
|
|
76
79
|
- lib/crypto-toolbox/crypt_buffer/concerns/random.rb
|
77
80
|
- lib/crypto-toolbox/crypt_buffer/concerns/xor.rb
|
78
81
|
- lib/crypto-toolbox/crypt_buffer_input_converter.rb
|
82
|
+
- lib/crypto-toolbox/crypto_challanges/solver.rb
|
79
83
|
- lib/crypto-toolbox/utils/reporting/console.rb
|
80
84
|
homepage: https://github.com/scepticulous/crypto-toolbox
|
81
85
|
licenses:
|