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.
- checksums.yaml +4 -4
- data/bin/axe.rb +18 -0
- data/bin/fcomp.rb +15 -0
- data/bin/pex.rb +16 -0
- data/bin/rarser.rb +15 -0
- data/bin/reach.rb +16 -0
- data/bin/rex.rb +7 -66
- data/bin/rinfo.rb +19 -0
- data/lib/resilience.rb +5 -0
- data/lib/resilience/attribute.rb +21 -4
- data/lib/resilience/cli/all.rb +14 -0
- data/lib/resilience/cli/bin/axe.rb +34 -0
- data/lib/resilience/cli/bin/fcomp.rb +28 -0
- data/lib/resilience/cli/bin/pex.rb +43 -0
- data/lib/resilience/cli/bin/rarser.rb +72 -0
- data/lib/resilience/cli/bin/reach.rb +34 -0
- data/lib/resilience/cli/bin/rex.rb +33 -0
- data/lib/resilience/cli/bin/rinfo.rb +19 -0
- data/lib/resilience/cli/conf.rb +14 -0
- data/lib/resilience/cli/default.rb +17 -0
- data/lib/resilience/cli/disk.rb +40 -0
- data/lib/resilience/cli/file.rb +23 -0
- data/lib/resilience/cli/image.rb +45 -0
- data/lib/resilience/cli/metadata.rb +24 -0
- data/lib/resilience/cli/output.rb +84 -0
- data/lib/resilience/collections/dirs.rb +8 -0
- data/lib/resilience/collections/files.rb +41 -0
- data/lib/resilience/collections/pages.rb +16 -0
- data/lib/resilience/conf.rb +28 -0
- data/lib/resilience/constants.rb +31 -7
- data/lib/resilience/core_ext.rb +39 -0
- data/lib/resilience/fs_dir.rb +3 -0
- data/lib/resilience/fs_dir/dir_base.rb +16 -6
- data/lib/resilience/fs_dir/dir_entry.rb +33 -0
- data/lib/resilience/fs_dir/file_entry.rb +53 -0
- data/lib/resilience/image.rb +33 -0
- data/lib/resilience/mixins/on_image.rb +25 -0
- data/lib/resilience/page.rb +104 -0
- data/lib/resilience/tables.rb +1 -0
- data/lib/resilience/tables/boot.rb +89 -0
- data/lib/resilience/tables/object.rb +6 -2
- data/lib/resilience/tables/system.rb +5 -2
- data/lib/resilience/trees.rb +5 -0
- data/lib/resilience/trees/object_tree.rb +45 -0
- metadata +55 -15
- data/bin/cle.rb +0 -24
- data/bin/clum.py +0 -28
- data/bin/fs.rb +0 -21
- data/bin/ref.rb +0 -49
- data/bin/rels.rb +0 -269
- data/bin/resilience.rb +0 -351
data/bin/resilience.rb
DELETED
@@ -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
|