resilience 0.0.2
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 +7 -0
- data/README.md +3 -0
- data/bin/cle.rb +24 -0
- data/bin/clum.py +28 -0
- data/bin/fs.rb +21 -0
- data/bin/ref.rb +49 -0
- data/bin/rels.rb +269 -0
- data/bin/resilience.rb +351 -0
- data/bin/rex.rb +74 -0
- data/lib/resilience.rb +13 -0
- data/lib/resilience/attribute.rb +28 -0
- data/lib/resilience/constants.rb +32 -0
- data/lib/resilience/dirs.rb +5 -0
- data/lib/resilience/dirs/root_dir.rb +15 -0
- data/lib/resilience/fs_dir.rb +6 -0
- data/lib/resilience/fs_dir/dir_base.rb +106 -0
- data/lib/resilience/fs_dir/record.rb +59 -0
- data/lib/resilience/image.rb +37 -0
- data/lib/resilience/mixins.rb +5 -0
- data/lib/resilience/mixins/on_image.rb +25 -0
- data/lib/resilience/tables.rb +6 -0
- data/lib/resilience/tables/object.rb +46 -0
- data/lib/resilience/tables/system.rb +42 -0
- metadata +72 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 4ce546fb9a6dcfd401ccb8adbfa16bb3147f3a3b
|
4
|
+
data.tar.gz: 6c47f4c2c0c3f89d74a30335928ef8bb991dcd0b
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 7d25ff402e660223d44333beab243b7281d8382a8a2f31caeea6670e1adef46fc6fd54d218475ecd2b0f7061148b2f0111b297e45ffcbc412886920dbcf23bee
|
7
|
+
data.tar.gz: 9767ac37ea16d858c02d89a217d33a8959509f03c1e7974d8b37d81db05b50576b8501a69ea1392338afafff7a06fae8c8a58dee3418198af1625d5120a1d199
|
data/README.md
ADDED
data/bin/cle.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
# ReFS 0x4000 Cluster Extractor
|
3
|
+
# By mmorsi - 2014-07-14
|
4
|
+
|
5
|
+
CLUSTERS = [0x1e, 0x20, 0x21, 0x22, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38,
|
6
|
+
0x2c0, 0x2c1, 0x2c2, 0x2c3, 0x2c4, 0x2c5, 0x2c6, 0x2c7, 0x2c8, 0x2cc, 0x2cd, 0x2ce, 0x2cf]
|
7
|
+
|
8
|
+
DISK = 'flat-disk.vmdk'
|
9
|
+
OUTDIR = 'clusters'
|
10
|
+
|
11
|
+
REFS_VOLUME_OFFSET = 0x2100000
|
12
|
+
|
13
|
+
inf = File.open(DISK, 'rb')
|
14
|
+
CLUSTERS.each do |cluster|
|
15
|
+
outf = File.open("#{OUTDIR}/#{cluster.to_s(16)}", 'wb')
|
16
|
+
|
17
|
+
offset = REFS_VOLUME_OFFSET + cluster * 0x4000
|
18
|
+
inf.seek(offset)
|
19
|
+
contents = inf.read(0x4000)
|
20
|
+
outf.write contents
|
21
|
+
outf.close
|
22
|
+
end
|
23
|
+
|
24
|
+
inf.close
|
data/bin/clum.py
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# ReFS 0x4000 Cluster Usage Mapper
|
2
|
+
# By Willi Ballenthin 2012-03-25
|
3
|
+
# With modifications by mmorsi - 2014-07-14
|
4
|
+
import sys, struct
|
5
|
+
|
6
|
+
#REFS_VOLUME_OFFSET = 0x2010000
|
7
|
+
REFS_VOLUME_OFFSET = 0x2100000
|
8
|
+
|
9
|
+
def main(args):
|
10
|
+
with open(args[1], "rb") as f:
|
11
|
+
global REFS_VOLUME_OFFSET
|
12
|
+
offset = REFS_VOLUME_OFFSET
|
13
|
+
cluster = 0
|
14
|
+
while True:
|
15
|
+
f.seek(offset + cluster * 0x4000)
|
16
|
+
buf = f.read(4)
|
17
|
+
if not buf: break
|
18
|
+
magic = struct.unpack("<I", buf)[0]
|
19
|
+
if magic == cluster:
|
20
|
+
print "Metadata cluster %s (%s)" % \
|
21
|
+
(hex(cluster), hex(offset + cluster * 0x4000))
|
22
|
+
elif magic != 0:
|
23
|
+
print "Non-null cluster %s (%s)" % \
|
24
|
+
(hex(cluster), hex(offset + cluster * 0x4000))
|
25
|
+
cluster += 1
|
26
|
+
|
27
|
+
if __name__ == '__main__':
|
28
|
+
main(sys.argv)
|
data/bin/fs.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
# ReFS file searcher
|
3
|
+
# By mmorsi - 2014-09-03
|
4
|
+
|
5
|
+
DISK = 'flat-disk.vmdk'
|
6
|
+
OFFSET = 0x2100000
|
7
|
+
|
8
|
+
SEQUENCE_LENGTH = 8
|
9
|
+
SEQUENCE = 0xe010002800000038 # inverted due to endian ordering
|
10
|
+
|
11
|
+
inf = File.open(DISK, 'rb')
|
12
|
+
inf.seek(OFFSET)
|
13
|
+
|
14
|
+
while check = inf.read(SEQUENCE_LENGTH)
|
15
|
+
check = check.unpack('Q').first
|
16
|
+
if check == SEQUENCE
|
17
|
+
puts 'File at: 0x' + inf.pos.to_s(16) + ' cluster ' + ((inf.pos - OFFSET) / 0x4000).to_s(16)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
inf.close
|
data/bin/ref.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
# ReFS reference analyzer
|
3
|
+
# By mmorsi - 2014-09-03
|
4
|
+
|
5
|
+
require 'graphviz'
|
6
|
+
|
7
|
+
CLUSTERS = [0x1e, 0x2e, 0x37, 0x38,
|
8
|
+
0x2c0, 0x2c1, 0x2c2, 0x2c3, 0x2c4, 0x2c5, 0x2c6, 0x2c7, 0x2c8, 0x2cc, 0x2cd, 0x2ce, 0x2cf]
|
9
|
+
|
10
|
+
DISK = 'flat-disk.vmdk'
|
11
|
+
OFFSET = 0x2100000
|
12
|
+
|
13
|
+
inf = File.open(DISK, 'rb')
|
14
|
+
|
15
|
+
refs = {}
|
16
|
+
|
17
|
+
CLUSTERS.each do |cluster|
|
18
|
+
offset = OFFSET + cluster * 0x4000
|
19
|
+
inf.seek(offset + 2) # skip first 2 bytes containing cluster #
|
20
|
+
while inf.pos < offset + 0x4000
|
21
|
+
checkb = inf.read(2).unpack('S').first
|
22
|
+
CLUSTERS.each do |checkc|
|
23
|
+
if checkb == checkc
|
24
|
+
refs[cluster] ||= {}
|
25
|
+
refs[cluster][checkc] ||= 0
|
26
|
+
refs[cluster][checkc] += 1
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
inf.close
|
33
|
+
|
34
|
+
g = GraphViz.new( :G, :type => :digraph )
|
35
|
+
|
36
|
+
refs.keys.each { |cluster|
|
37
|
+
g.add_nodes cluster.to_s(16)
|
38
|
+
}
|
39
|
+
|
40
|
+
refs.keys.each { |cluster|
|
41
|
+
puts "cluster #{cluster.to_s(16)} references: "
|
42
|
+
refs[cluster].keys.each { |ref|
|
43
|
+
puts ref.to_s(16) + " (#{refs[cluster][ref]})"
|
44
|
+
#g.add_edges(cluster.to_s(16), ref.to_s(16))
|
45
|
+
g.add_edges(ref.to_s(16), cluster.to_s(16))
|
46
|
+
}
|
47
|
+
}
|
48
|
+
|
49
|
+
g.output :png => 'fan-out.png'
|
data/bin/rels.rb
ADDED
@@ -0,0 +1,269 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
# ReFS File Lister
|
3
|
+
# Copyright (C) 2014 Red Hat Inc.
|
4
|
+
|
5
|
+
require 'optparse'
|
6
|
+
require 'colored'
|
7
|
+
|
8
|
+
FIRST_PAGE_ID = 0x1e
|
9
|
+
PAGE_SIZE = 0x4000
|
10
|
+
FIRST_PAGE_ADDRESS = FIRST_PAGE_ID * PAGE_SIZE
|
11
|
+
|
12
|
+
ROOT_DIR_ID = [0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0]
|
13
|
+
DIR_ENTRY = 0x20030
|
14
|
+
FILE_ENTRY = 0x10030
|
15
|
+
|
16
|
+
DIR_TREE = 0x301
|
17
|
+
DIR_LIST = 0x200
|
18
|
+
#DIR_BRANCH = 0x000 ?
|
19
|
+
|
20
|
+
ADDRESSES = {
|
21
|
+
# page
|
22
|
+
:virtual_page_number => 0x18,
|
23
|
+
:first_attr => 0x30,
|
24
|
+
|
25
|
+
# page 0x1e
|
26
|
+
:system_table_page => 0xA0,
|
27
|
+
|
28
|
+
# system table
|
29
|
+
:system_pages => 0x58,
|
30
|
+
|
31
|
+
# generic table
|
32
|
+
:num_objects => 0x20, # referenced from start of first attr
|
33
|
+
|
34
|
+
:table_length => 0x04 # referenced from start of table header
|
35
|
+
}
|
36
|
+
|
37
|
+
def read_attribute(image)
|
38
|
+
pos = image.pos
|
39
|
+
attr_len = image.read(4).unpack('L').first
|
40
|
+
return nil if attr_len == 0
|
41
|
+
image.seek pos
|
42
|
+
|
43
|
+
image.read(attr_len)
|
44
|
+
end
|
45
|
+
|
46
|
+
def record_boundries(attribute)
|
47
|
+
header = attribute.unpack('S*')
|
48
|
+
key_offset = header[2]
|
49
|
+
key_length = header[3]
|
50
|
+
value_offset = header[5]
|
51
|
+
value_length = header[6]
|
52
|
+
[key_offset, key_length, value_offset, value_length]
|
53
|
+
end
|
54
|
+
|
55
|
+
def record_flags(attribute)
|
56
|
+
attribute.unpack('S*')[4]
|
57
|
+
end
|
58
|
+
|
59
|
+
def record_key(attribute)
|
60
|
+
ko, kl, vo, vl = record_boundries(attribute)
|
61
|
+
attribute.unpack('C*')[ko...ko+kl].pack('C*')
|
62
|
+
end
|
63
|
+
|
64
|
+
def record_value(attribute)
|
65
|
+
ko, kl, vo, vl = record_boundries(attribute)
|
66
|
+
attribute.unpack('C*')[vo..-1].pack('C*')
|
67
|
+
end
|
68
|
+
|
69
|
+
def filter_dir_record(dir_entry, opts)
|
70
|
+
image = opts[:image]
|
71
|
+
|
72
|
+
# '4' seems to indicate a historical record or similar,
|
73
|
+
# records w/ flags '0' or '8' are what we want
|
74
|
+
record_flags(dir_entry)== 4 ? filter_dir_record(read_attribute(opts[:image]), opts) : dir_entry
|
75
|
+
end
|
76
|
+
|
77
|
+
def parse_dir_branch(node, prefix, opts)
|
78
|
+
key = record_key(node)
|
79
|
+
value = record_value(node)
|
80
|
+
flags = record_flags(node)
|
81
|
+
|
82
|
+
value_dwords = value.unpack('L*')
|
83
|
+
value_qwords = value.unpack('Q*')
|
84
|
+
|
85
|
+
page_id = value_dwords[0]
|
86
|
+
page_address = page_id * PAGE_SIZE
|
87
|
+
checksum = value_qwords[2]
|
88
|
+
parse_dir_page page_address, prefix, opts unless checksum == 0 || flags == 4
|
89
|
+
end
|
90
|
+
|
91
|
+
def parse_dir_record(dir_entry, prefix, opts)
|
92
|
+
key = record_key(dir_entry)
|
93
|
+
value = record_value(dir_entry)
|
94
|
+
|
95
|
+
key_bytes = key.unpack('C*')
|
96
|
+
key_dwords = key.unpack('L*')
|
97
|
+
entry_type = key_dwords.first
|
98
|
+
if entry_type == DIR_ENTRY
|
99
|
+
dir_name = key_bytes[4..-1].pack('L*')
|
100
|
+
opts[:dirs] << "#{prefix}\\#{dir_name}"
|
101
|
+
|
102
|
+
dir_obj = value.unpack('C*')[0...8]
|
103
|
+
dir_obj = [0, 0, 0, 0, 0, 0, 0, 0].concat(dir_obj)
|
104
|
+
parse_dir_obj(dir_obj, "#{prefix}\\#{dir_name}", opts)
|
105
|
+
|
106
|
+
elsif entry_type == FILE_ENTRY
|
107
|
+
file_name = key_bytes[4..-1].pack('L*')
|
108
|
+
opts[:files] << "#{prefix}\\#{file_name}"
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def parse_dir_obj(object_id, prefix, opts)
|
113
|
+
image = opts[:image]
|
114
|
+
object_pages = opts[:object_pages]
|
115
|
+
opts[:dirs] ||= []
|
116
|
+
opts[:files] ||= []
|
117
|
+
|
118
|
+
page_id = object_pages[object_id]
|
119
|
+
page_address = page_id * PAGE_SIZE
|
120
|
+
parse_dir_page page_address, prefix, opts
|
121
|
+
end
|
122
|
+
|
123
|
+
def parse_dir_page(page_address, prefix, opts)
|
124
|
+
image = opts[:image]
|
125
|
+
image_start = opts[:offset]
|
126
|
+
|
127
|
+
# skip container/placeholder attribute
|
128
|
+
image.seek(image_start + page_address + ADDRESSES[:first_attr])
|
129
|
+
read_attribute(image)
|
130
|
+
|
131
|
+
# start of table attr, pull out table length, type
|
132
|
+
table_header_attr = read_attribute(image)
|
133
|
+
table_header_dwords = table_header_attr.unpack("L*")
|
134
|
+
header_len = table_header_dwords[0]
|
135
|
+
table_len = table_header_dwords[1]
|
136
|
+
remaining_len = table_len - header_len
|
137
|
+
table_type = table_header_dwords[3]
|
138
|
+
|
139
|
+
until remaining_len == 0
|
140
|
+
orig_pos = image.pos
|
141
|
+
record = read_attribute(image)
|
142
|
+
|
143
|
+
# need to keep track of position locally as we
|
144
|
+
# recursively call parse_dir via helpers
|
145
|
+
pos = image.pos
|
146
|
+
|
147
|
+
if table_type == DIR_TREE
|
148
|
+
parse_dir_branch record, prefix, opts
|
149
|
+
else #if table_type == DIR_LIST
|
150
|
+
record = filter_dir_record(record, opts)
|
151
|
+
pos = image.pos
|
152
|
+
parse_dir_record record, prefix, opts
|
153
|
+
|
154
|
+
end
|
155
|
+
|
156
|
+
image.seek pos
|
157
|
+
remaining_len -= (image.pos - orig_pos)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
def parse_object_table(opts)
|
162
|
+
image = opts[:image]
|
163
|
+
image_start = opts[:offset]
|
164
|
+
opts[:object_pages] ||= {}
|
165
|
+
|
166
|
+
# in the images I've seen this has always been the first entry
|
167
|
+
# in the system table, though always has virtual page id = 2
|
168
|
+
# which we could look for if this turns out not to be the case
|
169
|
+
object_page_id = opts[:system_pages].first
|
170
|
+
object_page_address = object_page_id * PAGE_SIZE
|
171
|
+
|
172
|
+
# read number of objects from index header
|
173
|
+
image.seek(image_start + object_page_address + ADDRESSES[:first_attr])
|
174
|
+
first_attr = read_attribute(image)
|
175
|
+
num_objects = first_attr.unpack('L*')[ADDRESSES[:num_objects]/4]
|
176
|
+
|
177
|
+
# start of table attr, skip for now
|
178
|
+
read_attribute(image)
|
179
|
+
|
180
|
+
0.upto(num_objects-1) do
|
181
|
+
object_record = read_attribute(image)
|
182
|
+
object_id = record_key(object_record).unpack('C*')
|
183
|
+
|
184
|
+
# here object page is first qword of record value
|
185
|
+
object_page = record_value(object_record).unpack('Q*').first
|
186
|
+
opts[:object_pages][object_id] = object_page
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
def parse_system_table(opts)
|
191
|
+
image = opts[:image]
|
192
|
+
image_start = opts[:offset]
|
193
|
+
opts[:system_pages] ||= []
|
194
|
+
|
195
|
+
image.seek(image_start + FIRST_PAGE_ADDRESS + ADDRESSES[:system_table_page])
|
196
|
+
system_table_page = image.read(8).unpack('Q').first
|
197
|
+
system_table_address = system_table_page * PAGE_SIZE
|
198
|
+
|
199
|
+
image.seek(image_start + system_table_address + ADDRESSES[:system_pages])
|
200
|
+
num_system_pages = image.read(4).unpack('L').first
|
201
|
+
|
202
|
+
0.upto(num_system_pages-1) do
|
203
|
+
system_page_offset = image.read(4).unpack('L').first
|
204
|
+
pos = image.pos
|
205
|
+
|
206
|
+
image.seek(image_start + system_table_address + system_page_offset)
|
207
|
+
system_page = image.read(8).unpack('Q').first
|
208
|
+
opts[:system_pages] << system_page
|
209
|
+
|
210
|
+
image.seek(pos)
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
def print_results(opts)
|
215
|
+
puts "Dirs found:".bold
|
216
|
+
opts[:dirs].each { |dir|
|
217
|
+
puts "#{dir}".blue
|
218
|
+
}
|
219
|
+
|
220
|
+
puts
|
221
|
+
puts "Files found:".bold
|
222
|
+
opts[:files].sort.each { |file|
|
223
|
+
puts "#{file}".red
|
224
|
+
}
|
225
|
+
end
|
226
|
+
|
227
|
+
def main(opts = {})
|
228
|
+
image = File.open(opts[:image], 'rb')
|
229
|
+
opts[:image] = image
|
230
|
+
|
231
|
+
parse_system_table(opts)
|
232
|
+
parse_object_table(opts)
|
233
|
+
parse_dir_obj(ROOT_DIR_ID, '', opts)
|
234
|
+
print_results(opts)
|
235
|
+
end
|
236
|
+
|
237
|
+
def parse_cli(cli)
|
238
|
+
opts = {}
|
239
|
+
parser = OptionParser.new do |popts|
|
240
|
+
popts.on("-h", "--help", "Print help message") do
|
241
|
+
puts parser
|
242
|
+
exit
|
243
|
+
end
|
244
|
+
|
245
|
+
popts.on("-i", "--image path", "Path to the disk image to parse") do |path|
|
246
|
+
opts[:image] = path
|
247
|
+
end
|
248
|
+
|
249
|
+
popts.on("-o", "--offset bytes", "Start of volume with ReFS filesystem") do |offset|
|
250
|
+
opts[:offset] = offset.to_i
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
begin
|
255
|
+
parser.parse!(cli)
|
256
|
+
rescue OptionParser::InvalidOption
|
257
|
+
puts parser
|
258
|
+
exit
|
259
|
+
end
|
260
|
+
|
261
|
+
if !opts[:image] || !opts[:offset]
|
262
|
+
puts "--image and --offset params are needed at a minimum"
|
263
|
+
exit 1
|
264
|
+
end
|
265
|
+
|
266
|
+
opts
|
267
|
+
end
|
268
|
+
|
269
|
+
main parse_cli(ARGV) if __FILE__ == $0
|
data/bin/resilience.rb
ADDED
@@ -0,0 +1,351 @@
|
|
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
|
data/bin/rex.rb
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
# ReFS File Extractor
|
3
|
+
# Copyright (C) 2015 Red Hat Inc.
|
4
|
+
|
5
|
+
require 'optparse'
|
6
|
+
require 'resilience'
|
7
|
+
|
8
|
+
def main
|
9
|
+
cli_opts = parse_cli(ARGV)
|
10
|
+
results = parse_image cli_opts
|
11
|
+
write_results cli_opts, results
|
12
|
+
end
|
13
|
+
|
14
|
+
def write_results(opts, results)
|
15
|
+
dir = opts[:dir]
|
16
|
+
Dir.mkdir(dir) unless File.directory?(dir)
|
17
|
+
|
18
|
+
results[:files].each do |name, contents|
|
19
|
+
puts "Got #{name}"
|
20
|
+
path = "#{dir}/#{name}".delete("\0")
|
21
|
+
File.write(path, contents)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def parse_image(opts)
|
26
|
+
file = File.open(opts[:image], 'rb')
|
27
|
+
image = Resilience::OnImage.image
|
28
|
+
image.file = file
|
29
|
+
image.offset = opts[:offset]
|
30
|
+
image.opts = opts
|
31
|
+
|
32
|
+
image.parse
|
33
|
+
files = image.root_dir.files
|
34
|
+
dirs = image.root_dir.dirs
|
35
|
+
{:files => files, :dirs => dirs}
|
36
|
+
end
|
37
|
+
|
38
|
+
def parse_cli(cli)
|
39
|
+
opts = {}
|
40
|
+
parser = OptionParser.new do |popts|
|
41
|
+
popts.on("-h", "--help", "Print help message") do
|
42
|
+
puts parser
|
43
|
+
exit
|
44
|
+
end
|
45
|
+
|
46
|
+
popts.on("-i", "--image path", "Path to the disk image to parse") do |path|
|
47
|
+
opts[:image] = path
|
48
|
+
end
|
49
|
+
|
50
|
+
popts.on("-o", "--offset bytes", "Start of volume with ReFS filesystem") do |offset|
|
51
|
+
opts[:offset] = offset.to_i
|
52
|
+
end
|
53
|
+
|
54
|
+
popts.on("-d", "--dir dir", "Output directory") do |dir|
|
55
|
+
opts[:dir] = dir
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
begin
|
60
|
+
parser.parse!(cli)
|
61
|
+
rescue OptionParser::InvalidOption
|
62
|
+
puts parser
|
63
|
+
exit
|
64
|
+
end
|
65
|
+
|
66
|
+
if !opts[:image] || !opts[:offset] || !opts[:dir]
|
67
|
+
puts "--image, --offset, and --dir params are needed"
|
68
|
+
exit 1
|
69
|
+
end
|
70
|
+
|
71
|
+
opts
|
72
|
+
end
|
73
|
+
|
74
|
+
main if __FILE__ == $0
|
data/lib/resilience.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
# ReFS Parser (expiremental)
|
3
|
+
# Copyright (C) 2015 Red Hat Inc.
|
4
|
+
|
5
|
+
require 'resilience/mixins'
|
6
|
+
require 'resilience/constants'
|
7
|
+
|
8
|
+
require 'resilience/fs_dir'
|
9
|
+
require 'resilience/attribute'
|
10
|
+
require 'resilience/image'
|
11
|
+
|
12
|
+
require 'resilience/dirs'
|
13
|
+
require 'resilience/tables'
|
@@ -0,0 +1,28 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
# ReFS Attributes
|
3
|
+
# Copyright (C) 2015 Red Hat Inc.
|
4
|
+
|
5
|
+
module Resilience
|
6
|
+
class Attribute
|
7
|
+
include OnImage
|
8
|
+
|
9
|
+
attr_accessor :bytes
|
10
|
+
|
11
|
+
def initialize(args={})
|
12
|
+
@bytes = args[:bytes] if args.key?(:bytes)
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.read
|
16
|
+
pos = image.pos
|
17
|
+
attr_len = image.read(4).unpack('L').first
|
18
|
+
return new if attr_len == 0
|
19
|
+
|
20
|
+
image.seek pos
|
21
|
+
new(:bytes => image.read(attr_len))
|
22
|
+
end
|
23
|
+
|
24
|
+
def unpack(format)
|
25
|
+
bytes.unpack(format)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end # module Resilience
|
@@ -0,0 +1,32 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
# ReFS Constants
|
3
|
+
# Copyright (C) 2015 Red Hat Inc.
|
4
|
+
|
5
|
+
FIRST_PAGE_ID = 0x1e
|
6
|
+
PAGE_SIZE = 0x4000
|
7
|
+
FIRST_PAGE_ADDRESS = FIRST_PAGE_ID * PAGE_SIZE
|
8
|
+
|
9
|
+
ROOT_DIR_ID = [0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0]
|
10
|
+
DIR_ENTRY = 0x20030
|
11
|
+
FILE_ENTRY = 0x10030
|
12
|
+
|
13
|
+
DIR_TREE = 0x301
|
14
|
+
DIR_LIST = 0x200
|
15
|
+
#DIR_BRANCH = 0x000 ?
|
16
|
+
|
17
|
+
ADDRESSES = {
|
18
|
+
# page
|
19
|
+
:virtual_page_number => 0x18,
|
20
|
+
:first_attr => 0x30,
|
21
|
+
|
22
|
+
# page 0x1e
|
23
|
+
:system_table_page => 0xA0,
|
24
|
+
|
25
|
+
# system table
|
26
|
+
:system_pages => 0x58,
|
27
|
+
|
28
|
+
# generic table
|
29
|
+
:num_objects => 0x20, # referenced from start of first attr
|
30
|
+
|
31
|
+
:table_length => 0x04 # referenced from start of table header
|
32
|
+
}
|
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
# ReFS Root Dir
|
3
|
+
# Copyright (C) 2015 Red Hat Inc.
|
4
|
+
|
5
|
+
module Resilience
|
6
|
+
class RootDir < FSDir::DirBase
|
7
|
+
include OnImage
|
8
|
+
|
9
|
+
def self.parse
|
10
|
+
dir = new
|
11
|
+
dir.parse_dir_obj ROOT_DIR_ID, ''
|
12
|
+
dir
|
13
|
+
end
|
14
|
+
end # class RootDir
|
15
|
+
end # module Resilience
|
@@ -0,0 +1,106 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
# ReFS Directory Handling
|
3
|
+
# Copyright (C) 2014-2015 Red Hat Inc.
|
4
|
+
|
5
|
+
require 'fileutils'
|
6
|
+
|
7
|
+
module Resilience
|
8
|
+
module FSDir
|
9
|
+
class DirBase
|
10
|
+
include OnImage
|
11
|
+
|
12
|
+
attr_accessor :dirs
|
13
|
+
attr_accessor :files
|
14
|
+
|
15
|
+
def parse_dir_obj(object_id, prefix)
|
16
|
+
object_table = image.object_table
|
17
|
+
@dirs ||= {}
|
18
|
+
@files ||= {}
|
19
|
+
|
20
|
+
page_id = object_table.pages[object_id]
|
21
|
+
page_address = page_id * PAGE_SIZE
|
22
|
+
parse_dir_page page_address, prefix
|
23
|
+
end
|
24
|
+
|
25
|
+
def parse_dir_page(page_address, prefix)
|
26
|
+
# skip container/placeholder attribute
|
27
|
+
image.seek(page_address + ADDRESSES[:first_attr])
|
28
|
+
Attribute.read
|
29
|
+
|
30
|
+
# start of table attr, pull out table length, type
|
31
|
+
table_header_attr = Attribute.read
|
32
|
+
table_header_dwords = table_header_attr.unpack("L*")
|
33
|
+
header_len = table_header_dwords[0]
|
34
|
+
table_len = table_header_dwords[1]
|
35
|
+
remaining_len = table_len - header_len
|
36
|
+
table_type = table_header_dwords[3]
|
37
|
+
|
38
|
+
until remaining_len == 0
|
39
|
+
orig_pos = image.pos
|
40
|
+
record = Record.read
|
41
|
+
|
42
|
+
# need to keep track of position locally as we
|
43
|
+
# recursively call parse_dir via helpers
|
44
|
+
pos = image.pos
|
45
|
+
|
46
|
+
if table_type == DIR_TREE
|
47
|
+
parse_dir_branch record, prefix
|
48
|
+
|
49
|
+
else #if table_type == DIR_LIST
|
50
|
+
record = filter_dir_record(record)
|
51
|
+
pos = image.pos
|
52
|
+
parse_dir_record record, prefix
|
53
|
+
|
54
|
+
end
|
55
|
+
|
56
|
+
image.seek pos
|
57
|
+
remaining_len -= (image.pos - orig_pos)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def filter_dir_record(record)
|
62
|
+
# '4' seems to indicate a historical record or similar,
|
63
|
+
# records w/ flags '0' or '8' are what we want
|
64
|
+
record.flags == 4 ? filter_dir_record(Record.read) : record
|
65
|
+
end
|
66
|
+
|
67
|
+
def parse_dir_branch(record, prefix)
|
68
|
+
key = record.key
|
69
|
+
value = record.value
|
70
|
+
flags = record.flags
|
71
|
+
|
72
|
+
value_dwords = value.unpack('L*')
|
73
|
+
value_qwords = value.unpack('Q*')
|
74
|
+
|
75
|
+
page_id = value_dwords[0]
|
76
|
+
page_address = page_id * PAGE_SIZE
|
77
|
+
checksum = value_qwords[2]
|
78
|
+
|
79
|
+
parse_dir_page page_address, prefix unless checksum == 0 || flags == 4
|
80
|
+
end
|
81
|
+
|
82
|
+
def parse_dir_record(record, prefix)
|
83
|
+
key = record.key
|
84
|
+
value = record.value
|
85
|
+
|
86
|
+
key_bytes = key.unpack('C*')
|
87
|
+
key_dwords = key.unpack('L*')
|
88
|
+
entry_type = key_dwords.first
|
89
|
+
|
90
|
+
if entry_type == DIR_ENTRY
|
91
|
+
dir_name = key_bytes[4..-1].pack('L*')
|
92
|
+
dir_obj = value.unpack('C*')[0...8]
|
93
|
+
dirs["#{prefix}\\#{dir_name}"] = dir_obj
|
94
|
+
|
95
|
+
dir_obj = [0, 0, 0, 0, 0, 0, 0, 0].concat(dir_obj)
|
96
|
+
parse_dir_obj(dir_obj, "#{prefix}\\#{dir_name}")
|
97
|
+
|
98
|
+
elsif entry_type == FILE_ENTRY
|
99
|
+
file_name = key_bytes[4..-1].pack('L*')
|
100
|
+
prefixed = "#{prefix}\\#{file_name}"
|
101
|
+
files[prefixed] = value
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end # class DirBase
|
105
|
+
end # module FSDir
|
106
|
+
end # module Resilience
|
@@ -0,0 +1,59 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
# ReFS Records
|
3
|
+
# Copyright (C) 2015 Red Hat Inc.
|
4
|
+
|
5
|
+
module Resilience
|
6
|
+
module FSDir
|
7
|
+
class Record
|
8
|
+
include OnImage
|
9
|
+
|
10
|
+
attr_accessor :attribute
|
11
|
+
|
12
|
+
def initialize(attribute)
|
13
|
+
@attribute = attribute
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.read
|
17
|
+
new(Attribute.read)
|
18
|
+
end
|
19
|
+
|
20
|
+
def calc_boundries
|
21
|
+
return if @boundries_set
|
22
|
+
@boundries_set = true
|
23
|
+
|
24
|
+
header = attribute.unpack('S*')
|
25
|
+
@key_offset = header[2]
|
26
|
+
@key_length = header[3]
|
27
|
+
@value_offset = header[5]
|
28
|
+
@value_length = header[6]
|
29
|
+
end
|
30
|
+
|
31
|
+
def boundries
|
32
|
+
calc_boundries
|
33
|
+
[@key_offset, @key_length, @value_offset, @value_length]
|
34
|
+
end
|
35
|
+
|
36
|
+
def calc_flags
|
37
|
+
return if @flags_set
|
38
|
+
@flags_set = true
|
39
|
+
|
40
|
+
@flags = attribute.unpack('S*')[4]
|
41
|
+
end
|
42
|
+
|
43
|
+
def flags
|
44
|
+
calc_flags
|
45
|
+
@flags
|
46
|
+
end
|
47
|
+
|
48
|
+
def key
|
49
|
+
ko, kl, vo, vl = boundries
|
50
|
+
attribute.unpack('C*')[ko...ko+kl].pack('C*')
|
51
|
+
end
|
52
|
+
|
53
|
+
def value
|
54
|
+
ko, kl, vo, vl = boundries
|
55
|
+
attribute.unpack('C*')[vo..-1].pack('C*')
|
56
|
+
end
|
57
|
+
end # class Record
|
58
|
+
end # module FSDir
|
59
|
+
end # module Resilience
|
@@ -0,0 +1,37 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
# ReFS Image Representation
|
3
|
+
# Copyright (C) 2015 Red Hat Inc.
|
4
|
+
|
5
|
+
module Resilience
|
6
|
+
class Image
|
7
|
+
attr_accessor :file
|
8
|
+
attr_accessor :offset
|
9
|
+
attr_accessor :opts
|
10
|
+
|
11
|
+
attr_accessor :root_dir
|
12
|
+
attr_accessor :system_table
|
13
|
+
attr_accessor :object_table
|
14
|
+
|
15
|
+
def initialize(args={})
|
16
|
+
@file = args[:file] || []
|
17
|
+
end
|
18
|
+
|
19
|
+
def parse
|
20
|
+
@system_table = Resilience::SystemTable.parse
|
21
|
+
@object_table = Resilience::ObjectTable.parse
|
22
|
+
@root_dir = Resilience::RootDir.parse
|
23
|
+
end
|
24
|
+
|
25
|
+
def seek(position)
|
26
|
+
@file.seek offset + position
|
27
|
+
end
|
28
|
+
|
29
|
+
def pos
|
30
|
+
@file.pos - offset
|
31
|
+
end
|
32
|
+
|
33
|
+
def read(len)
|
34
|
+
@file.read(len)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end # module Resilience
|
@@ -0,0 +1,25 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
# ReFS On Image Mixin
|
3
|
+
# Copyright (C) 2015 Red Hat Inc.
|
4
|
+
|
5
|
+
module Resilience
|
6
|
+
module OnImage
|
7
|
+
def self.included(base)
|
8
|
+
base.extend(ClassMethods)
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.image
|
12
|
+
@image ||= Resilience::Image.new
|
13
|
+
end
|
14
|
+
|
15
|
+
def image
|
16
|
+
OnImage.image
|
17
|
+
end
|
18
|
+
|
19
|
+
module ClassMethods
|
20
|
+
def image
|
21
|
+
OnImage.image
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end # module OnImage
|
25
|
+
end # module Resilience
|
@@ -0,0 +1,46 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
# ReFS Object Table
|
3
|
+
# Copyright (C) 2015 Red Hat Inc.
|
4
|
+
|
5
|
+
module Resilience
|
6
|
+
class ObjectTable
|
7
|
+
include OnImage
|
8
|
+
|
9
|
+
attr_accessor :pages
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@pages ||= {}
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.parse
|
16
|
+
table = new
|
17
|
+
table.parse_pages
|
18
|
+
table
|
19
|
+
end
|
20
|
+
|
21
|
+
def parse_pages
|
22
|
+
# in the images I've seen this has always been the first entry
|
23
|
+
# in the system table, though always has virtual page id = 2
|
24
|
+
# which we could look for if this turns out not to be the case
|
25
|
+
object_page_id = image.system_table.pages.first
|
26
|
+
object_page_address = object_page_id * PAGE_SIZE
|
27
|
+
|
28
|
+
# read number of objects from index header
|
29
|
+
image.seek(object_page_address + ADDRESSES[:first_attr])
|
30
|
+
first_attr = Attribute.read
|
31
|
+
num_objects = first_attr.unpack('L*')[ADDRESSES[:num_objects]/4]
|
32
|
+
|
33
|
+
# start of table attr, skip for now
|
34
|
+
Attribute.read
|
35
|
+
|
36
|
+
0.upto(num_objects-1) do
|
37
|
+
object_record = FSDir::Record.read
|
38
|
+
object_id = object_record.key.unpack('C*')
|
39
|
+
|
40
|
+
# here object page is first qword of record value
|
41
|
+
object_page = object_record.value.unpack('Q*').first
|
42
|
+
@pages[object_id] = object_page
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end # class ObjectTable
|
46
|
+
end # module Resilience
|
@@ -0,0 +1,42 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
# ReFS System Table
|
3
|
+
# Copyright (C) 2015 Red Hat Inc.
|
4
|
+
|
5
|
+
module Resilience
|
6
|
+
class SystemTable
|
7
|
+
include OnImage
|
8
|
+
|
9
|
+
attr_accessor :pages
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@pages = []
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.parse
|
16
|
+
table = new
|
17
|
+
table.parse_pages
|
18
|
+
table
|
19
|
+
end
|
20
|
+
|
21
|
+
def parse_pages
|
22
|
+
image.seek(FIRST_PAGE_ADDRESS + ADDRESSES[:system_table_page])
|
23
|
+
system_table_page = image.read(8).unpack('Q').first
|
24
|
+
system_table_address = system_table_page * PAGE_SIZE
|
25
|
+
|
26
|
+
image.seek(system_table_address + ADDRESSES[:system_pages])
|
27
|
+
num_system_pages = image.read(4).unpack('L').first
|
28
|
+
|
29
|
+
0.upto(num_system_pages-1) do
|
30
|
+
system_page_offset = image.read(4).unpack('L').first
|
31
|
+
pos = image.pos
|
32
|
+
|
33
|
+
image.seek(system_table_address + system_page_offset)
|
34
|
+
system_page = image.read(8).unpack('Q').first
|
35
|
+
@pages << system_page
|
36
|
+
|
37
|
+
image.seek(pos)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end # class SystemTable
|
41
|
+
end # module Resilience
|
42
|
+
|
metadata
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: resilience
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Mo Morsi
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-05-23 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: Ruby ReFS utils
|
14
|
+
email: mmorsi@redhat.com
|
15
|
+
executables:
|
16
|
+
- fs.rb
|
17
|
+
- resilience.rb
|
18
|
+
- rex.rb
|
19
|
+
- rels.rb
|
20
|
+
- ref.rb
|
21
|
+
- clum.py
|
22
|
+
- cle.rb
|
23
|
+
extensions: []
|
24
|
+
extra_rdoc_files: []
|
25
|
+
files:
|
26
|
+
- README.md
|
27
|
+
- bin/cle.rb
|
28
|
+
- bin/clum.py
|
29
|
+
- bin/fs.rb
|
30
|
+
- bin/ref.rb
|
31
|
+
- bin/rels.rb
|
32
|
+
- bin/resilience.rb
|
33
|
+
- bin/rex.rb
|
34
|
+
- lib/resilience.rb
|
35
|
+
- lib/resilience/attribute.rb
|
36
|
+
- lib/resilience/constants.rb
|
37
|
+
- lib/resilience/dirs.rb
|
38
|
+
- lib/resilience/dirs/root_dir.rb
|
39
|
+
- lib/resilience/fs_dir.rb
|
40
|
+
- lib/resilience/fs_dir/dir_base.rb
|
41
|
+
- lib/resilience/fs_dir/record.rb
|
42
|
+
- lib/resilience/image.rb
|
43
|
+
- lib/resilience/mixins.rb
|
44
|
+
- lib/resilience/mixins/on_image.rb
|
45
|
+
- lib/resilience/tables.rb
|
46
|
+
- lib/resilience/tables/object.rb
|
47
|
+
- lib/resilience/tables/system.rb
|
48
|
+
homepage: https://gist.github.com/movitto/866de4356f56a3b478ca
|
49
|
+
licenses:
|
50
|
+
- MIT
|
51
|
+
metadata: {}
|
52
|
+
post_install_message:
|
53
|
+
rdoc_options: []
|
54
|
+
require_paths:
|
55
|
+
- lib
|
56
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0'
|
61
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
62
|
+
requirements:
|
63
|
+
- - ">="
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: '0'
|
66
|
+
requirements: []
|
67
|
+
rubyforge_project:
|
68
|
+
rubygems_version: 2.2.2
|
69
|
+
signing_key:
|
70
|
+
specification_version: 4
|
71
|
+
summary: A module/command-line utility to parse a ReFS file system image
|
72
|
+
test_files: []
|