crypto-toolbox 0.1.18 → 0.1.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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:
|