memorandom 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'credit_card_validator'
4
+
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2014 Rapid7
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,41 @@
1
+ Memorandom
2
+ ==
3
+
4
+ Memorandom provides a command-line utility and class library from extracting secrets from binary files. Common use cases include extracting encryption keys from memory dumps and identifying sensitive data stored in block devices.
5
+
6
+
7
+ Installation
8
+ --
9
+ $ git clone https://github.com/rapid7/memorandom
10
+ $ cd memorandom
11
+ $ bundle install
12
+
13
+ Usage
14
+ --
15
+ $ bin/memorandom.rb --help
16
+
17
+ Usage: bin/memorandom.rb [options] file1 file2 ... fileN
18
+ Extracts interesting data from binary files
19
+
20
+ Options
21
+ -p [plugin1,plugin2,plugin3...], Specify a list of plugin names to use, otherwise use all plugins
22
+ --plugins
23
+ -o, --output [directory] Specify the directory in which to store found data
24
+ -w, --window [number] Specify the number of kilobytes to scan at once (default: 1024).
25
+ -x, --overlap [number] Specify the number of kilobytes to overlap between windows (default: 4).
26
+ -l, --list-plugins List all of the available plugins
27
+ -h, --help Show this message.
28
+
29
+
30
+ Memorandom can search files, block devices, and standard input for interesting things and automatically extract and save these into the specified output directory (--output). This makes it useful for processing memory dumps, network traffic logs, hard disk images, or entire filesystems.
31
+
32
+
33
+ Memorandom uses plugins to scan for specific types of data within the target files. Ny default, all plugins are enabled, which can lead to noisy and slow output. For small files, all plugins are usually fine, but when processing large amounts of data, it is better to limit the search to specific plugins.
34
+
35
+ The example below will only scan for PEM-encoded data in the target files
36
+
37
+ $ bin/memorandom.rb -p pem ~/.ssh/*
38
+ [+] file:/home/dev/.ssh/ec2.pem PEM@0 ("-----BEGIN RSA PRIVATE KEY-----\r"...)
39
+ [+] file:/home/dev/.ssh/id_dsa PEM@0 ("-----BEGIN DSA PRIVATE KEY-----\n"...)
40
+ [+] file:/home/dev/.ssh/id_rsa PEM@0 ("-----BEGIN RSA PRIVATE KEY-----\n"...)
41
+
data/bin/memorandom.rb ADDED
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $:.unshift(File.expand_path(File.join(File.dirname(__FILE__), "..", "lib")))
4
+ require 'optparse'
5
+ require 'ostruct'
6
+ require 'memorandom'
7
+
8
+ options = {}
9
+
10
+ option_parser = OptionParser.new do |opts|
11
+ opts.banner = "Usage: #{$0} [options] file1 file2 ... fileN"
12
+ opts.separator "Extracts interesting data from binary files"
13
+ opts.separator ""
14
+ opts.separator "Options"
15
+
16
+ opts.on("-p", "--plugins [plugin1,plugin2,plugin3...]",
17
+ "Specify a list of plugin names to use, otherwise use all plugins"
18
+ ) do |plugins|
19
+ options[:plugins] = plugins.split(",").map{ |x| x.downcase.strip }
20
+ end
21
+
22
+ opts.on("-o", "--output [directory]",
23
+ "Specify the directory in which to store found data") do |out|
24
+ options[:output] = out
25
+ end
26
+
27
+ opts.on("-w", "--window [number]",
28
+ "Specify the number of kilobytes to scan at once (default: 1024).") do |num|
29
+ options[:window] = (num.to_i == 0) ? (1024*1024) : (num.to_i * 1024)
30
+ end
31
+
32
+ opts.on("-x", "--overlap [number]",
33
+ "Specify the number of kilobytes to overlap between windows (default: 4).") do |num|
34
+ options[:overlap] = (num.to_i == 0) ? (4*1024) : (num.to_i * 1024)
35
+ end
36
+
37
+ opts.on("-l", "--list-plugins",
38
+ "List all of the available plugins") do
39
+ puts ""
40
+ puts "Memorandom Plugins"
41
+ puts "==============="
42
+
43
+ Memorandom::PluginManager.plugins.each_pair do |name, klass|
44
+ puts " * #{name.ljust(24)} #{klass.description}"
45
+ end
46
+ puts ""
47
+ exit
48
+ end
49
+
50
+ opts.on("-h", "--help", "Show this message.") do
51
+ puts opts
52
+ exit
53
+ end
54
+ end
55
+ option_parser.parse!(ARGV)
56
+
57
+ if ARGV.count < 1
58
+ puts option_parser
59
+ exit
60
+ end
61
+
62
+ scanner = Memorandom::Scanner.new(options)
63
+ if scanner.plugins.length == 0
64
+ $stderr.puts "Error: No valid plugins have been selected"
65
+ exit(1)
66
+ end
67
+
68
+ ARGV.each do |target|
69
+ scanner.scan(target)
70
+ end
data/lib/memorandom.rb ADDED
@@ -0,0 +1,5 @@
1
+ require 'memorandom/version'
2
+ require 'memorandom/plugins'
3
+ require 'memorandom/scanner'
4
+
5
+ Memorandom::PluginManager.load_plugins
@@ -0,0 +1,31 @@
1
+ module Memorandom
2
+ class PluginManager
3
+ @@plugins = {}
4
+
5
+ def self.register(name, klass)
6
+ @@plugins[name.downcase.gsub(/\s+/, '_')] = klass
7
+ end
8
+
9
+ def self.plugins
10
+ @@plugins
11
+ end
12
+
13
+ def self.load_plugins
14
+ Memorandom::Plugins.constants.each do |c|
15
+ register(c.to_s, Memorandom::Plugins.const_get(c))
16
+ end
17
+ end
18
+
19
+ end
20
+ end
21
+
22
+ require 'memorandom/plugins/template'
23
+
24
+ require 'memorandom/plugins/pem'
25
+ require 'memorandom/plugins/der'
26
+ require 'memorandom/plugins/rsa'
27
+ require 'memorandom/plugins/aes'
28
+ require 'memorandom/plugins/capi'
29
+ require 'memorandom/plugins/url_params'
30
+ require 'memorandom/plugins/hashes'
31
+ require 'memorandom/plugins/cc'
@@ -0,0 +1,188 @@
1
+ module Memorandom
2
+ module Plugins
3
+ class AES < PluginTemplate
4
+
5
+ @@description = "This plugin looks for AES encryption keys (128/256)"
6
+ @@confidence = 0.50
7
+
8
+ #
9
+ # This code is a ruby implementation of AESKeyFind (C) 2008-07-18 Nadia Heninger and Ariel Feldman
10
+ # It is approximately 360 times slower than the C implementation :(
11
+ #
12
+
13
+ AES_KEYBLOCK_MIN = 176
14
+ AES_XOR_LIMIT = 10
15
+ AES_SBOX = [
16
+ # 0 1 2 3 4 5 6 7 8 9 A B C D E F
17
+ 0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76, #0
18
+ 0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0, #1
19
+ 0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15, #2
20
+ 0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75, #3
21
+ 0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84, #4
22
+ 0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf, #5
23
+ 0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8, #6
24
+ 0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2, #7
25
+ 0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73, #8
26
+ 0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb, #9
27
+ 0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79, #A
28
+ 0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08, #B
29
+ 0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a, #C
30
+ 0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e, #D
31
+ 0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf, #E
32
+ 0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16 #F
33
+ ]
34
+
35
+ AES_RCON = [
36
+ 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a,
37
+ 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39,
38
+ 0x72, 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a,
39
+ 0x74, 0xe8, 0xcb, 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8,
40
+ 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef,
41
+ 0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc,
42
+ 0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb, 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b,
43
+ 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3,
44
+ 0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94,
45
+ 0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb, 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20,
46
+ 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35,
47
+ 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f,
48
+ 0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb, 0x8d, 0x01, 0x02, 0x04,
49
+ 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63,
50
+ 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd,
51
+ 0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb
52
+ ]
53
+
54
+ BYTE_VALUES = Array.new([0]*256)
55
+ ENTROPIC_MIN = 8
56
+
57
+ M1 = 0x55555555
58
+ M2 = 0x33333333
59
+ M4 = 0x0f0f0f0f
60
+
61
+ # Perform the AES key core operation on a word.
62
+ # (Assumes the standard byte order.)
63
+ def aes_key_core(k, i)
64
+ t = 0
65
+ 0.upto(3) do |j|
66
+ t = aes_set_byte(t, (j-1) % 4, AES_SBOX[aes_get_byte(k, j)])
67
+ end
68
+ aes_set_byte(t, 0, aes_get_byte(t,0) ^ AES_RCON[i])
69
+ end
70
+
71
+ # Run each byte of a word through the sbox separately for word 4 of 256-bit AES.
72
+ def aes_sbox_bytes(k)
73
+ r = 0
74
+ 0.upto(3) do |j|
75
+ r = aes_set_byte(r, j, AES_SBOX[aes_get_byte(k,j)])
76
+ end
77
+ r
78
+ end
79
+
80
+ # Return bit n of vector.
81
+ def aes_bit(vector, n)
82
+ (vector >> n) & 1
83
+ end
84
+
85
+ # Set byte n of vector to val.
86
+ def aes_set_byte(vector, n, val)
87
+ (vector & ~(0xff << (8*n))) | (val << (8*n))
88
+ end
89
+
90
+ # Return byte n of vector.
91
+ def aes_get_byte(vector, n)
92
+ (vector >> (8*n)) & 0xff
93
+ end
94
+
95
+ # Return the number of bits in x that are 1.
96
+ def aes_popcount(x)
97
+ x -= (x >> 1) & M1
98
+ x = (x & M2) + ((x >> 2) & M2)
99
+ x = (x + (x >> 4)) & M4
100
+ x += x >> 8
101
+ (x + (x >> 16)) & 0x3f
102
+ end
103
+
104
+ # Identifies byte windows that are "entropic" (< limit repeats of any byte)
105
+ # Algorithm adapted from aeskeyfind.c
106
+ def entropic(buff, window, limit)
107
+ bytes = buff.unpack("C*")
108
+ index = 0
109
+
110
+ while index < (bytes.length - window)
111
+ counts = BYTE_VALUES.dup
112
+ index.upto(index+window) {|offset| counts[ bytes[offset] ] += 1 }
113
+ mvalue = counts.max
114
+
115
+ if mvalue < limit
116
+ yield(index)
117
+ # Key-hunting time, skip ahead by one byte only
118
+ index += 1
119
+ else
120
+ # Not entropic, skip ahead by at least max - limit
121
+ index += ((mvalue + 1) - limit)
122
+ end
123
+ end
124
+ end
125
+
126
+ # Scan takes a buffer and an offset of where this buffer starts in the source
127
+ def scan(buffer, source_offset)
128
+
129
+ blen = buffer.length
130
+ entropic(buffer, AES_KEYBLOCK_MIN, ENTROPIC_MIN) do |i|
131
+
132
+ # Exit if we dont have at least 240 bytes left
133
+ return if (i + 240) > blen
134
+
135
+ # Create a byte map to work in both little and big endian formats
136
+ #["V", "N"].each do |endian|
137
+
138
+ ["V"].each do |endian|
139
+ bmap = buffer[i, 240].unpack("#{endian}*")
140
+
141
+ # Check distance from 256-bit AES key
142
+ xor_count_256 = 0
143
+ 1.upto(7) do |row|
144
+ 0.upto(7) do |column|
145
+ break if (row == 7 and column == 4)
146
+ case column
147
+ when 0
148
+ xor_count_256 += aes_popcount( aes_key_core( bmap[8*row-1], row) ^ bmap[8*(row-1)] ^ bmap[8*row] )
149
+ when 4
150
+ xor_count_256 += aes_popcount( aes_sbox_bytes( bmap[8*row+3]) ^ bmap[8*(row-1)+4] ^ bmap[8*row+4])
151
+ else
152
+ xor_count_256 += aes_popcount( bmap[8*row+column-1] ^ bmap[8*(row-1)+column] ^ bmap[8*row + column])
153
+ end
154
+ end
155
+ end
156
+
157
+ # We found a possible AES-256 key
158
+ unless xor_count_256 > AES_XOR_LIMIT
159
+ report_hit(:type => "Key(AES-256)", :data => bmap[0, 256/32].pack("#{endian}*").unpack("C*").map{|x| "%.2x" % x }.join, :offset => source_offset + i)
160
+ next
161
+ end
162
+
163
+ # Check distance from 128-bit AES key
164
+ xor_count_128 = 0
165
+ 1.upto(10) do |row|
166
+ 0.upto(3) do |column|
167
+ before_count = xor_count_128
168
+ case column
169
+ when 0
170
+ xor_count_128 += aes_popcount( aes_key_core( bmap[4*row-1],row) ^ bmap[4*(row-1)] ^ bmap[4*row])
171
+ else
172
+ xor_count_128 += aes_popcount( (bmap[4*row + column-1] ^ bmap[4*(row-1)+column]) ^ bmap[4*row + column])
173
+ end
174
+ end
175
+ end
176
+
177
+ # We found a possible AES-128 key
178
+ unless xor_count_128 > AES_XOR_LIMIT
179
+ report_hit(:type => "Key(AES-128)", :data => bmap[0, 128/32].pack("#{endian}*").unpack("C*").map{|x| "%.2x" % x }.join, :offset => source_offset + i)
180
+ end
181
+ end
182
+ end
183
+
184
+ end
185
+
186
+ end
187
+ end
188
+ end
@@ -0,0 +1,62 @@
1
+ module Memorandom
2
+ module Plugins
3
+ class CAPI < PluginTemplate
4
+
5
+ require 'openssl'
6
+
7
+ @@description = "This plugin looks for Microsoft CryptoAPI encryption keys in memory (PRIVATEBLOB)"
8
+ @@confidence = 0.90
9
+
10
+ # Scan takes a buffer and an offset of where this buffer starts in the source
11
+ def scan(buffer, source_offset)
12
+
13
+ buffer.scan(
14
+ /[\x06\x07]\x02.{6}(?:RSA1|RSA2|DSS1|DSS2).{20}/
15
+ ).each do |m|
16
+ # This may hit an earlier identical match, but thats ok
17
+ last_offset = buffer.index(m)
18
+ next unless last_offset
19
+
20
+ # Attempt to parse the key at the specified offset
21
+ key_candidate = buffer[last_offset, 20000]
22
+ key_type = ""
23
+ key = nil
24
+
25
+ bits = key_candidate.unpack("CCA6A4V")
26
+ key_type << ( bits[3] + "-" + bits[4].to_s + "-" )
27
+ key_type << ( (bits[0] == 0x07) ? "Private" : "Public" )
28
+
29
+ key_length = 0
30
+ nbyte = (bits[4] + 7) >> 3
31
+ hnbyte = (bits[4] + 15) >> 4
32
+
33
+ # DSA
34
+ if bits[3].index('DSS')
35
+ if bits[0] == 0x06
36
+ # Expected length: 20 for q + 3 components bitlen each + 24 for seed structure.
37
+ key_length = 44 + (3 * nbyte)
38
+ else
39
+ # Expected length: 20 for q, priv, 2 bitlen components + 24 for seed structure.
40
+ key_length = 64 + (2 * nbyte)
41
+ end
42
+ # RSA
43
+ else
44
+ if bits[0] == 0x06
45
+ # Expected length: 4 for 'e' + 'n'
46
+ key_length = 4 + nbyte
47
+ else
48
+ # Expected length: 4 for 'e' and 7 other components. 2 components are bitlen size, 5 are bitlen/2
49
+ key_length = 4 + (2 * nbyte) + (5 * hnbyte)
50
+ end
51
+ end
52
+
53
+ key = buffer[last_offset, key_length + 16]
54
+ next unless key.length == (key_length+16)
55
+
56
+ report_hit(:type => "CAPI-#{key_type}", :data => key, :offset => source_offset + last_offset)
57
+ end
58
+ end
59
+
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,36 @@
1
+ module Memorandom
2
+ module Plugins
3
+ class CC < PluginTemplate
4
+
5
+ # Use the credit_card_validator gem for luhn checks and card verification
6
+ require 'credit_card_validator'
7
+
8
+ @@description = "This plugin looks for credit card numbers"
9
+ @@confidence = 0.10
10
+
11
+ # Scan takes a buffer and an offset of where this buffer starts in the source
12
+ def scan(buffer, source_offset)
13
+
14
+ buffer.scan(
15
+ # Look for credit card numbers in various formats
16
+ /(?:^|\D)([\d \-]{12,32})(?:$|\D)/
17
+ ).each do |m|
18
+ matched = m.first
19
+
20
+ # This may hit an earlier identical match, but thats ok
21
+ last_offset = buffer.index(matched)
22
+ next unless last_offset
23
+
24
+ # Clean out any non-digits and validate the card
25
+ cleaned = matched.gsub(/[^\d]+/, '')
26
+ next unless CreditCardValidator::Validator.valid?(cleaned)
27
+ cc_type = CreditCardValidator::Validator.card_type(cleaned)
28
+ cc_type = cc_type.split('_').map{|x| x.capitalize }.join
29
+
30
+ report_hit(:type => "CreditCard(#{cc_type})", :data => cleaned, :offset => source_offset + last_offset)
31
+ end
32
+ end
33
+
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,43 @@
1
+ module Memorandom
2
+ module Plugins
3
+ class DER < PluginTemplate
4
+
5
+ require 'openssl'
6
+
7
+ @@description = "This plugin looks for DER-encoded encryption keys (RSA/DSA/EC)"
8
+ @@confidence = 0.90
9
+
10
+ # Scan takes a buffer and an offset of where this buffer starts in the source
11
+ def scan(buffer, source_offset)
12
+
13
+ buffer.scan(
14
+ # Look for a DER record start (0x30), a length value, and a version marker.
15
+ # This identifies RSA, DSA, and EC keys
16
+ /\x30.{1,5}\x02\x01(?:\x00\x02|\x01\x04)/m
17
+ ).each do |m|
18
+ # This may hit an earlier identical match, but thats ok
19
+ last_offset = buffer.index(m)
20
+ next unless last_offset
21
+
22
+ # Attempt to parse the key at the specified offset
23
+ key_candidate = buffer[last_offset, 20000]
24
+ key_type = nil
25
+ key = nil
26
+
27
+ [:RSA, :DSA, :EC ].each do |ktype|
28
+ next unless OpenSSL::PKey.const_defined?(ktype)
29
+ key_type = ktype
30
+ key = OpenSSL::PKey.const_get(ktype).new(key_candidate) rescue nil
31
+ break if key
32
+ end
33
+
34
+ # Ignore this if OpenSSL could not parse out a valid key
35
+ next unless key
36
+
37
+ report_hit(:type => "#{key_type}", :data => key.to_pem, :offset => source_offset + last_offset)
38
+ end
39
+ end
40
+
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,36 @@
1
+ module Memorandom
2
+ module Plugins
3
+ class Hashes < PluginTemplate
4
+
5
+ @@description = "This plugin looks for common hash formats"
6
+ @@confidence = 0.10
7
+
8
+ # Scan takes a buffer and an offset of where this buffer starts in the source
9
+ def scan(buffer, source_offset)
10
+
11
+ # Unix password hash formats
12
+ buffer.scan(
13
+ /[a-z0-9_]+:\$\d+\$[$a-z0-9\.\/]+:\d+:\d+:\d+[a-z0-9 :]*/mi
14
+ ).each do |m|
15
+ # This may hit an earlier identical match, but thats ok
16
+ last_offset = buffer.index(m)
17
+ report_hit(:type => 'UnixHash', :data => m, :offset => source_offset + last_offset)
18
+ last_offset += m.length
19
+ end
20
+
21
+ # Hexadecimal password hashes
22
+ buffer.scan(
23
+ /[a-f0-9]{16,128}/mi
24
+ ).each do |m|
25
+ next unless m.length % 2 == 0
26
+ # This may hit an earlier identical match, but thats ok
27
+ last_offset = buffer.index(m)
28
+ report_hit(:type => 'CommonHash', :data => m, :offset => source_offset + last_offset)
29
+ last_offset += m.length
30
+ end
31
+
32
+ end
33
+
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,22 @@
1
+ module Memorandom
2
+ module Plugins
3
+ class PEM < PluginTemplate
4
+
5
+ @@description = "This plugin looks for PEM-encoded data (keys, certificates, crls, etc)"
6
+ @@confidence = 0.90
7
+
8
+ # Scan takes a buffer and an offset of where this buffer starts in the source
9
+ def scan(buffer, source_offset)
10
+ buffer.scan(
11
+ /-----BEGIN\s*[^\-]+-----+\r?\n[^\-]*-----END\s*[^\-]+-----\r?\n?/m
12
+ ).each do |m|
13
+ # This may hit an earlier identical match, but thats ok
14
+ last_offset = buffer.index(m)
15
+ report_hit(:plugin => self, :type => 'PEM', :data => m, :offset => source_offset + last_offset)
16
+ last_offset += m.length
17
+ end
18
+ end
19
+
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,71 @@
1
+ module Memorandom
2
+ module Plugins
3
+ class RSA < PluginTemplate
4
+
5
+ require 'openssl'
6
+
7
+ @@description = "This plugin looks for RSA keys by finding Bignum-encoded p-values"
8
+ @@confidence = 0.90
9
+
10
+ # Scan takes a buffer and an offset of where this buffer starts in the source
11
+ def scan(buffer, source_offset)
12
+
13
+ key_lengths = [1024, 2048]
14
+
15
+ key_lengths.each do |bits|
16
+ p_size = bits / 16
17
+ n_size = bits / 8
18
+
19
+ found = []
20
+ 0.upto(buffer.length - (p_size + n_size)) do |p_offset|
21
+
22
+ # Look for a prime of the right size (p or q)
23
+ found_p = OpenSSL::BN.new( buffer[p_offset, p_size].unpack("H*").first.to_i(16).to_s )
24
+ next unless found_p.prime?
25
+ next unless found_p > 0x1000000000000000000000
26
+
27
+ # Look for a modulus that matches the found p/q value
28
+ 0.upto(buffer.length - (p_size + n_size) ) do |n_offset|
29
+
30
+ next if (n_offset < p_offset and (n_offset + n_size) > p_offset)
31
+ next if (p_offset < n_offset and (p_offset + p_size) > n_offset)
32
+
33
+ found_n = OpenSSL::BN.new(buffer[n_offset, n_size].unpack("H*").first.to_i(16).to_s )
34
+
35
+ next if found_n == 0
36
+ next if found_n == found_p
37
+ next unless found_n > 0x1000000000000000000000
38
+ next unless (found_n % found_p == 0)
39
+
40
+ found << [found_p, found_n, p_offset]
41
+ end
42
+ end
43
+
44
+ found = found.uniq
45
+
46
+ next unless found.length > 0
47
+
48
+ mods = {}
49
+
50
+ # Track the last unique p/q value for a potential modulus
51
+ found.each do |info|
52
+ mods[ info[1] ] ||= {}
53
+ mods[ info[1] ][ info[0] ] = info[2]
54
+ end
55
+ p mods
56
+
57
+ next
58
+
59
+ mods.keys.each do |n|
60
+ uniq_pees = mods[n].keys.select do |k|
61
+ mods[n].keys.reject {|x| x == k}.select{|x| n == (x * k) }
62
+ end
63
+ end
64
+
65
+
66
+ end
67
+ end
68
+
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,46 @@
1
+ module Memorandom
2
+ class PluginTemplate
3
+
4
+ @@description = "This is an unconfigured plugin using the base template"
5
+ @@confidence = 0.80
6
+
7
+ attr_accessor :scanner, :hits
8
+
9
+ def initialize(scanner)
10
+ self.scanner = scanner
11
+ self.hits = {}
12
+ end
13
+
14
+ def report_hit(info = {})
15
+ unless info[:offset] and info[:type]
16
+ raise RuntimeError, "No offset or type supplied in the hit: #{info.inspect}"
17
+ end
18
+
19
+ scanner.report_hit(info.merge({:plugin => self }))
20
+
21
+ offset = info.delete(:offset)
22
+ self.hits[offset] = info
23
+ end
24
+
25
+ def reset
26
+ self.hits = {}
27
+ end
28
+
29
+ def self.description
30
+ @@description
31
+ end
32
+
33
+ def self.confidence
34
+ @@confidence
35
+ end
36
+
37
+ def description
38
+ self.class.description
39
+ end
40
+
41
+ def confidence
42
+ self.class.confidence
43
+ end
44
+
45
+ end
46
+ end
@@ -0,0 +1,22 @@
1
+ module Memorandom
2
+ module Plugins
3
+ class URLParams < PluginTemplate
4
+
5
+ @@description = "This plugin looks for interesting URL parameters and POST data"
6
+ @@confidence = 0.50
7
+
8
+ # Scan takes a buffer and an offset of where this buffer starts in the source
9
+ def scan(buffer, source_offset)
10
+ buffer.scan(
11
+ /[%a-z0-9_\-=\&]*(?:sid|session|sess|user|usr|login|pass|secret|token)[%a-z0-9_\-=\&]*=[%a-z0-9_\-=&]+/mi
12
+ ).each do |m|
13
+ # This may hit an earlier identical match, but thats ok
14
+ last_offset = buffer.index(m)
15
+ report_hit(:type => 'URLParams', :data => m, :offset => source_offset + last_offset)
16
+ last_offset += m.length
17
+ end
18
+ end
19
+
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,152 @@
1
+ module Memorandom
2
+ class Scanner
3
+
4
+ require 'fileutils'
5
+ require 'openssl'
6
+ require 'yaml'
7
+
8
+ attr_accessor :plugins, :source
9
+ attr_accessor :window, :overlap, :output
10
+
11
+ def initialize(opts={})
12
+ plugin_names = Memorandom::PluginManager.plugins.keys
13
+
14
+ # Allow only a subset of plugins to be selected
15
+ if opts[:plugins]
16
+ plugin_names = Memorandom::PluginManager.plugins.keys.select{ |name|
17
+ opts[:plugins].include?(name)
18
+ }
19
+ end
20
+
21
+ # Load the selected plugins
22
+ self.plugins = plugin_names.map { |name|
23
+ Memorandom::PluginManager.plugins[name].new(self)
24
+ }
25
+
26
+ self.window = opts[:window] || 1024*1024
27
+ self.overlap = opts[:overlap] || 1024*4
28
+ self.output = opts[:output]
29
+
30
+ FileUtils.mkdir_p(self.output) if self.output
31
+
32
+ end
33
+
34
+ def scan(target, source_name = nil)
35
+ fd = nil
36
+
37
+ if target.respond_to?(:read)
38
+ self.source = source_name || target.to_s
39
+ else
40
+ case target
41
+ when '-'
42
+ fd = $stdin
43
+ self.source = source_name || '<stdin>'
44
+ else
45
+ unless ( File.file?(target) or File.blockdev?(target) or File.chardev?(target) )
46
+ display("[-] Skipping #{target}: not a file")
47
+ return
48
+ end
49
+ begin
50
+ fd = File.open(target, "rb")
51
+ self.source = source_name ||"file:#{target.dup}"
52
+ rescue ::Interrupt
53
+ raise $!
54
+ rescue ::Exception
55
+ display("[-] Skipping #{target}: #{$!.class} #{$!}")
56
+ return
57
+ end
58
+ end
59
+ end
60
+
61
+ # Reset the plugin state between each target
62
+ self.plugins.each { |plugin| plugin.reset }
63
+
64
+ buffer = fd.read(self.window)
65
+ offset = 0
66
+
67
+ # Skip empty sources (an empty first read)
68
+ return unless buffer
69
+
70
+ while buffer.length > 0
71
+
72
+ self.plugins.each do |plugin|
73
+ # display("[*] Scanning #{buffer.length} bytes at offset #{offset}")
74
+
75
+ # Track a temporary per-plugin buffer and offset
76
+ scan_buffer = buffer
77
+ scan_offset = offset
78
+
79
+ # Adjust the buffer and offset if any hits have been found that are
80
+ # greater than offset. This is required because of overlap between
81
+ # search windows. It is rare, but should be handled here so that
82
+ # plugins don't have to worry about it.
83
+
84
+ adjust = plugin.hits.keys.select{|hit| hit > offset }.last
85
+ if adjust
86
+ start_index = ( adjust - offset + 1 )
87
+ scan_buffer = buffer[start_index, buffer.length-start_index]
88
+ scan_offset += start_index
89
+ end
90
+
91
+ # Scan the buffer with the plugin
92
+ plugin.scan(scan_buffer, scan_offset)
93
+ end
94
+
95
+ # Calculate the next sliding window
96
+ seeked = self.window - self.overlap
97
+ offset += seeked
98
+
99
+ nbytes = fd.read(seeked)
100
+ break if not nbytes
101
+
102
+ # Append new data to the end of the buffer
103
+ buffer << nbytes
104
+
105
+ # Delete most of the previous data from the beginning
106
+ buffer[0, seeked] = ''
107
+ end
108
+
109
+ # Close the file descriptor if we opened it
110
+ fd.close if source =~ /^file:/
111
+ end
112
+
113
+ def report_hit(info)
114
+ unless self.output
115
+ display("[+] #{source} #{info[:type]}@#{info[:offset]} (#{info[:data][0,32].inspect}...)")
116
+ return
117
+ end
118
+
119
+ fname = source.split("/").last[0,128] + "_"
120
+ fname << info[:data][0,16].unpack("H*").first
121
+ fname << "_#{info[:offset]}.#{info[:type]}"
122
+ yname = fname + ".yml"
123
+
124
+ fname = clean_filename(fname)
125
+ yname = clean_filename(yname)
126
+
127
+ fpath = ::File.join(self.output, fname)
128
+ ::File.open(fpath, "wb") { |fd| fd.write(info[:data]) }
129
+
130
+ display("[+] #{source} #{info[:type]}@#{info[:offset]} (#{info[:data][0,32].inspect}) stored in #{fpath}")
131
+
132
+ ypath = ::File.join(self.output, yname)
133
+ yhash = {
134
+ :source => source,
135
+ :type => info[:type],
136
+ :offset => info[:offset],
137
+ :timestamp => Time.now,
138
+ :length => info[:data].length
139
+ }
140
+ ::File.open(ypath, "wb") { |fd| fd.write(yhash.to_yaml) }
141
+
142
+ end
143
+
144
+ def clean_filename(name)
145
+ name.gsub(/[^a-zA-Z0-9_\.\-]/, '_').gsub(/_+/, '_')
146
+ end
147
+
148
+ def display(str)
149
+ $stdout.puts(str)
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,3 @@
1
+ module Memorandom
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,34 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $LOAD_PATH.push File.expand_path('../lib', __FILE__)
3
+ require 'memorandom/version'
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = 'memorandom'
7
+ s.version = Memorandom::VERSION
8
+ s.authors = [
9
+ 'Rapid7 Research'
10
+ ]
11
+ s.email = [
12
+ 'research@rapid7.com'
13
+ ]
14
+ s.homepage = "https://www.github.com/rapid7/memorandom"
15
+ s.summary = %q{Utility and library for extracting secrets from binary blobs}
16
+ s.description = %q{
17
+ Memorandom provides a command-line utility and class library from extracting secrets
18
+ from binary files. Common use cases include extracting encryption keys from memory dumps
19
+ and identifying sensitive data stored in block devices.
20
+ }.gsub(/\s+/, ' ').strip
21
+
22
+ s.files = `git ls-files`.split("\n")
23
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
24
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
25
+ s.require_paths = ['lib']
26
+
27
+ # ---- Dependencies ----
28
+
29
+ s.add_development_dependency 'rspec'
30
+ s.add_development_dependency 'cucumber'
31
+ s.add_development_dependency 'aruba'
32
+
33
+ s.add_runtime_dependency 'credit_card_validator'
34
+ end
metadata ADDED
@@ -0,0 +1,130 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: memorandom
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Rapid7 Research
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2014-04-23 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rspec
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: cucumber
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: aruba
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ - !ruby/object:Gem::Dependency
63
+ name: credit_card_validator
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :runtime
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ description: Memorandom provides a command-line utility and class library from extracting
79
+ secrets from binary files. Common use cases include extracting encryption keys from
80
+ memory dumps and identifying sensitive data stored in block devices.
81
+ email:
82
+ - research@rapid7.com
83
+ executables:
84
+ - memorandom.rb
85
+ extensions: []
86
+ extra_rdoc_files: []
87
+ files:
88
+ - Gemfile
89
+ - LICENSE
90
+ - README.md
91
+ - bin/memorandom.rb
92
+ - lib/memorandom.rb
93
+ - lib/memorandom/plugins.rb
94
+ - lib/memorandom/plugins/aes.rb
95
+ - lib/memorandom/plugins/capi.rb
96
+ - lib/memorandom/plugins/cc.rb
97
+ - lib/memorandom/plugins/der.rb
98
+ - lib/memorandom/plugins/hashes.rb
99
+ - lib/memorandom/plugins/pem.rb
100
+ - lib/memorandom/plugins/rsa.rb
101
+ - lib/memorandom/plugins/template.rb
102
+ - lib/memorandom/plugins/url_params.rb
103
+ - lib/memorandom/scanner.rb
104
+ - lib/memorandom/version.rb
105
+ - memorandom.gemspec
106
+ homepage: https://www.github.com/rapid7/memorandom
107
+ licenses: []
108
+ post_install_message:
109
+ rdoc_options: []
110
+ require_paths:
111
+ - lib
112
+ required_ruby_version: !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ! '>='
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ required_rubygems_version: !ruby/object:Gem::Requirement
119
+ none: false
120
+ requirements:
121
+ - - ! '>='
122
+ - !ruby/object:Gem::Version
123
+ version: '0'
124
+ requirements: []
125
+ rubyforge_project:
126
+ rubygems_version: 1.8.23
127
+ signing_key:
128
+ specification_version: 3
129
+ summary: Utility and library for extracting secrets from binary blobs
130
+ test_files: []