resilience 0.0.2 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/bin/axe.rb +18 -0
  3. data/bin/fcomp.rb +15 -0
  4. data/bin/pex.rb +16 -0
  5. data/bin/rarser.rb +15 -0
  6. data/bin/reach.rb +16 -0
  7. data/bin/rex.rb +7 -66
  8. data/bin/rinfo.rb +19 -0
  9. data/lib/resilience.rb +5 -0
  10. data/lib/resilience/attribute.rb +21 -4
  11. data/lib/resilience/cli/all.rb +14 -0
  12. data/lib/resilience/cli/bin/axe.rb +34 -0
  13. data/lib/resilience/cli/bin/fcomp.rb +28 -0
  14. data/lib/resilience/cli/bin/pex.rb +43 -0
  15. data/lib/resilience/cli/bin/rarser.rb +72 -0
  16. data/lib/resilience/cli/bin/reach.rb +34 -0
  17. data/lib/resilience/cli/bin/rex.rb +33 -0
  18. data/lib/resilience/cli/bin/rinfo.rb +19 -0
  19. data/lib/resilience/cli/conf.rb +14 -0
  20. data/lib/resilience/cli/default.rb +17 -0
  21. data/lib/resilience/cli/disk.rb +40 -0
  22. data/lib/resilience/cli/file.rb +23 -0
  23. data/lib/resilience/cli/image.rb +45 -0
  24. data/lib/resilience/cli/metadata.rb +24 -0
  25. data/lib/resilience/cli/output.rb +84 -0
  26. data/lib/resilience/collections/dirs.rb +8 -0
  27. data/lib/resilience/collections/files.rb +41 -0
  28. data/lib/resilience/collections/pages.rb +16 -0
  29. data/lib/resilience/conf.rb +28 -0
  30. data/lib/resilience/constants.rb +31 -7
  31. data/lib/resilience/core_ext.rb +39 -0
  32. data/lib/resilience/fs_dir.rb +3 -0
  33. data/lib/resilience/fs_dir/dir_base.rb +16 -6
  34. data/lib/resilience/fs_dir/dir_entry.rb +33 -0
  35. data/lib/resilience/fs_dir/file_entry.rb +53 -0
  36. data/lib/resilience/image.rb +33 -0
  37. data/lib/resilience/mixins/on_image.rb +25 -0
  38. data/lib/resilience/page.rb +104 -0
  39. data/lib/resilience/tables.rb +1 -0
  40. data/lib/resilience/tables/boot.rb +89 -0
  41. data/lib/resilience/tables/object.rb +6 -2
  42. data/lib/resilience/tables/system.rb +5 -2
  43. data/lib/resilience/trees.rb +5 -0
  44. data/lib/resilience/trees/object_tree.rb +45 -0
  45. metadata +55 -15
  46. data/bin/cle.rb +0 -24
  47. data/bin/clum.py +0 -28
  48. data/bin/fs.rb +0 -21
  49. data/bin/ref.rb +0 -49
  50. data/bin/rels.rb +0 -269
  51. data/bin/resilience.rb +0 -351
@@ -1,351 +0,0 @@
1
- #!/usr/bin/ruby
2
- # resilience.rb - Ruby ReFS Parser
3
-
4
- require 'optparse'
5
- require 'colored'
6
-
7
- FIRST_PAGE_ID = 0x1e
8
- PAGE_SIZE = 0x4000
9
-
10
- # note I believe we can also use the object-id's
11
- # of these entities
12
- ROOT_PAGE_NUMBER = 0x00
13
- OBJECT_TABLE_PAGE_NUMBER = 0x02
14
- OBJECT_TREE_PAGE_NUMBER = 0x03
15
-
16
- ADDRESSES = {
17
- # vbr
18
- :bytes_per_sector => 0x20,
19
- :sectors_per_cluster => 0x24,
20
-
21
- # page
22
- :page_sequence => 0x08, # shadow pages share the same virtual page number
23
- :virtual_page_number => 0x18, # but will have higher sequences
24
- :first_attr => 0x30,
25
-
26
- # index header
27
- :object_id => 0xC, # possibly index type of similar
28
- :entries_count => 0x20
29
- }
30
-
31
- def unpack_attribute(image)
32
- pos = image.pos
33
- attr_len = image.read(4).unpack('L').first
34
- return nil if attr_len == 0
35
- image.seek pos
36
-
37
- image.read(attr_len).unpack('C*')
38
- end
39
-
40
- def process_attributes(image, start)
41
- attributes = []
42
- image.seek(start)
43
- while true
44
- attribute = unpack_attribute(image)
45
- break if attribute.nil?
46
- attributes << attribute
47
- end
48
-
49
- attributes
50
- end
51
-
52
- # Large object table seems to have a special edge case w/
53
- # an extra 0x40 data block, haven't deduced meaning of this yet
54
- def process_object_table_attributes(image, start)
55
- image.seek(start)
56
-
57
- attributes = []
58
-
59
- # unpack first three attributes as normal
60
- attributes << unpack_attribute(image)
61
- attributes << unpack_attribute(image)
62
- attributes << unpack_attribute(image)
63
-
64
- # XXX hacky edge case detection, if next two bytes are 0,
65
- # handling as extended block, skipping for now
66
- if image.read(2).unpack('S').first == 0
67
- image.seek(38, IO::SEEK_CUR)
68
- else
69
- image.seek(-2, IO::SEEK_CUR)
70
- end
71
-
72
- # process rest of attributes as normal
73
- attributes + process_attributes(image, image.pos)
74
- end
75
-
76
- # Extract additional metadata from attributes
77
- def inspect_attributes(attributes)
78
- return {} if attributes.empty?
79
-
80
- object_id = attributes.first[ADDRESSES[:object_id]]
81
- entries = attributes.first[ADDRESSES[:entries_count]]
82
- {:object_id => object_id, :entries => entries}
83
- end
84
-
85
- def parse_pages(data, opts)
86
- image = data[:image]
87
- image_start = opts[:offset]
88
-
89
- data[:pages].keys.each { |page|
90
- page_offset = page * PAGE_SIZE
91
-
92
- image.seek(image_start + page_offset + ADDRESSES[:page_sequence])
93
- page_sequence = image.read(4).unpack('L').first
94
-
95
- image.seek(image_start + page_offset + ADDRESSES[:virtual_page_number])
96
- virtual_page_number = image.read(4).unpack('L').first
97
-
98
- attributes_start = image_start + page_offset + ADDRESSES[:first_attr]
99
-
100
- if virtual_page_number == ROOT_PAGE_NUMBER
101
- # skipping root page analysis until it is further understood
102
- is_root = true
103
-
104
- elsif virtual_page_number == OBJECT_TABLE_PAGE_NUMBER
105
- attributes = process_object_table_attributes(image, attributes_start)
106
-
107
- else
108
- attributes = process_attributes image, attributes_start
109
- end
110
-
111
- data[:pages][page][:sequence] = page_sequence
112
- data[:pages][page][:virtual_page_number] = virtual_page_number
113
-
114
- unless is_root
115
- data[:pages][page][:attributes] = attributes
116
- data[:pages][page].merge! inspect_attributes(attributes)
117
- end
118
- }
119
- end
120
-
121
- def volume_metadata(data, opts)
122
- image = data[:image]
123
- image_start = opts[:offset]
124
-
125
- image.seek(image_start + ADDRESSES[:bytes_per_sector])
126
- bytes_per_sector = image.read(4).unpack('L').first
127
-
128
- image.seek(image_start + ADDRESSES[:sectors_per_cluster])
129
- sectors_per_cluster = image.read(4).unpack('L').first
130
-
131
- cluster_size = bytes_per_sector * sectors_per_cluster
132
-
133
- {:bytes_per_sector => bytes_per_sector,
134
- :sectors_per_cluster => sectors_per_cluster,
135
- :cluster_size => cluster_size }
136
- end
137
-
138
- def pages(data, opts)
139
- image = data[:image]
140
- image_start = opts[:offset]
141
- page = FIRST_PAGE_ID
142
- pages = {}
143
-
144
- image.seek(image_start + page * PAGE_SIZE)
145
- while contents = image.read(PAGE_SIZE)
146
- # only pull out metadata pages currently
147
- is_metadata = contents.unpack('S').first == page
148
- pages[page] = {:contents => contents} if is_metadata
149
-
150
- page += 1
151
- end
152
-
153
- pages
154
- end
155
-
156
- # Convert an array of bytes in little endian order to human friendly string
157
- def little_endian_str(bytes)
158
- str = '0x'
159
- value = false
160
- bytes.reverse_each { |b|
161
- next if b == 0 && !value
162
- value = true
163
- str += b.to_s(16)
164
- }
165
- str
166
- end
167
-
168
- def object_table_page_id(data)
169
- # find shadow page w/ highest sequence
170
- data[:pages].keys.select { |p| data[:pages][p][:virtual_page_number] == OBJECT_TABLE_PAGE_NUMBER }
171
- .sort { |p1, p2| data[:pages][p2][:sequence] <=> data[:pages][p1][:sequence] }.first
172
- end
173
-
174
- def object_table(data, opts)
175
- table = {}
176
- page = data[:pages][object_table_page_id(data)]
177
-
178
- # XXX this could start from the 2nd attribute if the exception in
179
- # process_table_attributes does _not_ apply, need to investigate furthur / fix
180
- page[:attributes][3...-1].each { |bytes|
181
- # bytes 4-7 give us the key offset & length and
182
- key_offset = bytes[4..5].pack('C*').unpack('S').first.to_i
183
- key_length = bytes[6..7].pack('C*').unpack('S').first.to_i
184
-
185
- # bytes A-D give us the value offset & length
186
- value_offset = bytes[0xA..0xB].pack('C*').unpack('S').first.to_i
187
- value_length = bytes[0xC..0xD].pack('C*').unpack('S').first.to_i
188
-
189
- key = bytes[key_offset...key_offset+key_length]
190
- value = bytes[value_offset...value_offset+value_length]
191
-
192
- cluster_bytes = value[0..7]
193
- # TODO extract 'type' from value[3a..3d]a (?)
194
-
195
- object_id = key.pack('C*')
196
- cluster = cluster_bytes.pack('C*')
197
-
198
- object_str = little_endian_str(key)
199
- cluster_str = little_endian_str(cluster_bytes)
200
-
201
- table[object_id] = {:object_str => object_str,
202
- :cluster => cluster,
203
- :cluster_str => cluster_str}
204
- }
205
-
206
- table
207
- end
208
-
209
- def object_tree_page_id(data)
210
- # find shadow page w/ highest sequence
211
- data[:pages].keys.select { |p| data[:pages][p][:virtual_page_number] == OBJECT_TREE_PAGE_NUMBER }
212
- .sort { |p1, p2| data[:pages][p2][:sequence] <=> data[:pages][p1][:sequence] }.first
213
- end
214
-
215
- def object_tree(data, opts)
216
- tree = {}
217
- page = data[:pages][object_tree_page_id(data)]
218
-
219
- page[:attributes][2...-1].each { |bytes|
220
- obj1_bytes = bytes[0x10..0x1F]
221
- obj2_bytes = bytes[0x20..0x2F]
222
-
223
- obj1 = little_endian_str(obj1_bytes)
224
- obj2 = little_endian_str(obj2_bytes)
225
-
226
- tree[obj1] ||= []
227
- tree[obj1] << obj2
228
- }
229
-
230
- tree
231
- end
232
-
233
- def data_str(data, str_opts = {})
234
- places = str_opts[:places] || 1
235
-
236
- return '0x'+ ('0' * places) if data.nil?
237
- '0x'+data.to_s(16).rjust(places, '0').upcase
238
- end
239
-
240
- def print_results(data, opts)
241
- out = "Analyzed ReFS filesystem on #{opts[:image].green.bold} "\
242
- "starting at #{opts[:offset].to_s.green.bold}\n" \
243
- "VBR: #{data_str(data[:bytes_per_sector]).to_s.yellow.bold} (bytes per sector) * " \
244
- "#{data_str(data[:sectors_per_cluster]).to_s.yellow.bold} (sectors per cluster) = " \
245
- "#{data_str(data[:cluster_size]).to_s.yellow.bold} (bytes per cluster)\n"
246
-
247
- data[:pages].keys.each { |page_id|
248
- page = data[:pages][page_id]
249
-
250
- page_out = "Page #{data_str(page_id, :places => 4).blue.bold}: "\
251
- "number #{data_str(page[:virtual_page_number], :places => 3).blue.bold} - " \
252
- "sequence #{data_str(page[:sequence], :places => 2).blue.bold} - " \
253
- "object id #{data_str(page[:object_id], :places => 2).blue.bold} - " \
254
- "records #{data_str(page[:entries], :places => 2).blue.bold}\n"
255
-
256
- if opts[:attributes] && page[:attributes]
257
- page_out += " Attributes:\n"
258
- page[:attributes].each { |attr_values|
259
- attr_out = attr_values.collect { |a| a.to_s(16) }.join(' ')[0...10] +'...'
260
- page_out += ' ' + attr_out + "\n"
261
- }
262
- end
263
-
264
- out += page_out
265
- }
266
-
267
- if opts[:object_table]
268
- out += "\nObject table:\n"
269
- out += "Obj | Cluster\n"
270
- out += "-------------\n"
271
- data[:object_table].keys.each { |obj_id|
272
- object_str = data[:object_table][obj_id][:object_str]
273
- cluster = data[:object_table][obj_id][:cluster_str]
274
- out += "#{object_str[0..4]} | #{cluster}\n"
275
- }
276
- end
277
-
278
- if opts[:object_tree]
279
- out += "\nObject tree:\n"
280
- out += "-------------\n"
281
- data[:object_tree].keys.each { |obj_id|
282
- references = data[:object_tree][obj_id].collect { |obj| obj[0..4] }.join(', ')
283
- out += "#{obj_id[0..4]} -> #{references}\n"
284
- }
285
- end
286
-
287
- puts out
288
- end
289
-
290
- def main(opts = {})
291
- image = File.open(opts[:image], 'rb')
292
-
293
- data = {}
294
- data[:image] = image
295
-
296
- data.merge! volume_metadata(data, opts)
297
- data.merge! :pages => pages(data, opts)
298
-
299
- parse_pages data, opts
300
-
301
- data.merge! :object_table => object_table(data, opts)
302
- data.merge! :object_tree => object_tree(data, opts)
303
-
304
- print_results data, opts
305
- end
306
-
307
- def parse_cli(cli)
308
- opts = {}
309
- parser = OptionParser.new do |popts|
310
- popts.on("-h", "--help", "Print help message") do
311
- puts parser
312
- exit
313
- end
314
-
315
- popts.on("-i", "--image path", "Path to the disk image to parse") do |path|
316
- opts[:image] = path
317
- end
318
-
319
- popts.on("-o", "--offset bytes", "Start of volume with ReFS filesystem") do |offset|
320
- opts[:offset] = offset.to_i
321
- end
322
-
323
- popts.on("-a", "--attributes", "Include attribute analysis in output") do
324
- opts[:attributes] = true
325
- end
326
-
327
- popts.on("--table", "Include object table analysis in output") do
328
- opts[:object_table] = true
329
- end
330
-
331
- popts.on("--tree", "Include object tree analysis in output") do
332
- opts[:object_tree] = true
333
- end
334
- end
335
-
336
- begin
337
- parser.parse!(cli)
338
- rescue OptionParser::InvalidOption
339
- puts parser
340
- exit
341
- end
342
-
343
- if !opts[:image] || !opts[:offset]
344
- puts "--image and --offset params are needed at a minimum"
345
- exit 1
346
- end
347
-
348
- opts
349
- end
350
-
351
- main parse_cli(ARGV) if __FILE__ == $0