resilience 0.0.2 → 0.1.1

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.
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