resilience 0.0.2 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- 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/lib/resilience/tables.rb
CHANGED
@@ -0,0 +1,89 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
# ReFS Boot Table
|
3
|
+
# Copyright (C) 2015 Red Hat Inc.
|
4
|
+
|
5
|
+
module Resilience
|
6
|
+
class BootTable
|
7
|
+
include OnImage
|
8
|
+
|
9
|
+
SECTOR_SIZE = 0x200
|
10
|
+
|
11
|
+
def refs_offsets
|
12
|
+
offsets = []
|
13
|
+
fs_off = fs_offsets
|
14
|
+
image.offset = 0
|
15
|
+
fs_off.each do |address|
|
16
|
+
image.seek address
|
17
|
+
sig = image.read(FS_SIGNATURE.size).unpack('C*')
|
18
|
+
offsets << address if sig == FS_SIGNATURE
|
19
|
+
end
|
20
|
+
offsets
|
21
|
+
end
|
22
|
+
|
23
|
+
def refs_offset
|
24
|
+
refs_offsets.first
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class MBR < BootTable
|
29
|
+
SIG_ADDRESS = 0x1FE
|
30
|
+
SIG = [0x55, 0xAA]
|
31
|
+
PARTITION_ADDRESSES = [0x1C6, 0x1D6, 0x1E6, 0x1F6]
|
32
|
+
|
33
|
+
def detected?
|
34
|
+
image.seek SIG_ADDRESS
|
35
|
+
sig = image.read(2)
|
36
|
+
sig == SIG
|
37
|
+
end
|
38
|
+
|
39
|
+
def fs_offsets
|
40
|
+
offsets = []
|
41
|
+
PARTITION_ADDRESSES.each do |address|
|
42
|
+
image.seek address
|
43
|
+
address = image.read(4).unpack('V').first
|
44
|
+
offsets << address * SECTOR_SIZE unless address == 0
|
45
|
+
end
|
46
|
+
offsets
|
47
|
+
end
|
48
|
+
|
49
|
+
def gpt_offsets
|
50
|
+
offsets = []
|
51
|
+
fs_offsets.each do |address|
|
52
|
+
image.seek address
|
53
|
+
sig = image.read(GPT::SIG.size).unpack('C*')
|
54
|
+
offsets << address if sig == GPT::SIG
|
55
|
+
end
|
56
|
+
offsets
|
57
|
+
end
|
58
|
+
|
59
|
+
def gpt_offset
|
60
|
+
gpt_offsets.first
|
61
|
+
end
|
62
|
+
end # class MBR
|
63
|
+
|
64
|
+
class GPT < BootTable
|
65
|
+
NUM_ADDRESS = 0x50
|
66
|
+
PARTITIONS_START = 0x200 # from the current image offset, the start of gpt partition
|
67
|
+
PARTITION_SIZE = 0x80
|
68
|
+
OFFSET_ADDRESS = 0x20
|
69
|
+
|
70
|
+
SIG = [0x45, 0x46, 0x49, 0x20, 0x50, 0x41, 0x52, 0x54] # EFI PART
|
71
|
+
|
72
|
+
def num
|
73
|
+
image.seek NUM_ADDRESS
|
74
|
+
image.read(4).unpack('V').first
|
75
|
+
end
|
76
|
+
|
77
|
+
def fs_offsets
|
78
|
+
offsets = []
|
79
|
+
0.upto(num-1) do |i|
|
80
|
+
image.seek PARTITIONS_START + i * PARTITION_SIZE
|
81
|
+
partition = image.read(PARTITION_SIZE).unpack('C*')
|
82
|
+
offset = partition[OFFSET_ADDRESS...OFFSET_ADDRESS+8].pack('C*').unpack('V').first
|
83
|
+
offsets << offset * SECTOR_SIZE
|
84
|
+
break if offset == 0
|
85
|
+
end
|
86
|
+
offsets
|
87
|
+
end
|
88
|
+
end # class GPT
|
89
|
+
end # module Resilience
|
@@ -18,11 +18,15 @@ module Resilience
|
|
18
18
|
table
|
19
19
|
end
|
20
20
|
|
21
|
-
|
21
|
+
# Depends on SystemTable extraction
|
22
|
+
def object_page_id
|
22
23
|
# in the images I've seen this has always been the first entry
|
23
24
|
# in the system table, though always has virtual page id = 2
|
24
25
|
# which we could look for if this turns out not to be the case
|
25
|
-
|
26
|
+
image.system_table.pages.first
|
27
|
+
end
|
28
|
+
|
29
|
+
def parse_pages
|
26
30
|
object_page_address = object_page_id * PAGE_SIZE
|
27
31
|
|
28
32
|
# read number of objects from index header
|
@@ -18,8 +18,12 @@ module Resilience
|
|
18
18
|
table
|
19
19
|
end
|
20
20
|
|
21
|
+
def self.first_page_address
|
22
|
+
PAGES[:first] * PAGE_SIZE
|
23
|
+
end
|
24
|
+
|
21
25
|
def parse_pages
|
22
|
-
image.seek(
|
26
|
+
image.seek(self.class.first_page_address + ADDRESSES[:system_table_page])
|
23
27
|
system_table_page = image.read(8).unpack('Q').first
|
24
28
|
system_table_address = system_table_page * PAGE_SIZE
|
25
29
|
|
@@ -39,4 +43,3 @@ module Resilience
|
|
39
43
|
end
|
40
44
|
end # class SystemTable
|
41
45
|
end # module Resilience
|
42
|
-
|
@@ -0,0 +1,45 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
# ReFS Object Tree
|
3
|
+
# Copyright (C) 2015 Red Hat Inc.
|
4
|
+
|
5
|
+
module Resilience
|
6
|
+
class ObjectTree
|
7
|
+
include OnImage
|
8
|
+
|
9
|
+
attr_accessor :map
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@map ||= {}
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.parse
|
16
|
+
tree = new
|
17
|
+
tree.parse_entries
|
18
|
+
tree
|
19
|
+
end
|
20
|
+
|
21
|
+
# Depends on Image Pages extraction
|
22
|
+
def page
|
23
|
+
image.pages.newest_for PAGES[:object_table]
|
24
|
+
end
|
25
|
+
|
26
|
+
def parse_entries
|
27
|
+
page.attributes.each { |attr|
|
28
|
+
obj1 = obj1_from attr
|
29
|
+
obj2 = obj2_from attr
|
30
|
+
@map[obj1] ||= []
|
31
|
+
@map[obj1] << obj2
|
32
|
+
}
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def obj1_from(attr)
|
38
|
+
attr.bytes[ADDRESSES[:object_tree_start1]..ADDRESSES[:object_tree_end1]]
|
39
|
+
end
|
40
|
+
|
41
|
+
def obj2_from(attr)
|
42
|
+
attr.bytes[ADDRESSES[:object_tree_start2]..ADDRESSES[:object_tree_end2]]
|
43
|
+
end
|
44
|
+
end # class ObjectTree
|
45
|
+
end # module Resilience
|
metadata
CHANGED
@@ -1,50 +1,90 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: resilience
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Mo Morsi
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-
|
12
|
-
dependencies:
|
11
|
+
date: 2015-07-06 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: colored
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
13
27
|
description: Ruby ReFS utils
|
14
28
|
email: mmorsi@redhat.com
|
15
29
|
executables:
|
16
|
-
-
|
17
|
-
-
|
30
|
+
- axe.rb
|
31
|
+
- rarser.rb
|
32
|
+
- reach.rb
|
33
|
+
- pex.rb
|
18
34
|
- rex.rb
|
19
|
-
-
|
20
|
-
-
|
21
|
-
- clum.py
|
22
|
-
- cle.rb
|
35
|
+
- rinfo.rb
|
36
|
+
- fcomp.rb
|
23
37
|
extensions: []
|
24
38
|
extra_rdoc_files: []
|
25
39
|
files:
|
26
40
|
- README.md
|
27
|
-
- bin/
|
28
|
-
- bin/
|
29
|
-
- bin/
|
30
|
-
- bin/
|
31
|
-
- bin/
|
32
|
-
- bin/resilience.rb
|
41
|
+
- bin/axe.rb
|
42
|
+
- bin/fcomp.rb
|
43
|
+
- bin/pex.rb
|
44
|
+
- bin/rarser.rb
|
45
|
+
- bin/reach.rb
|
33
46
|
- bin/rex.rb
|
47
|
+
- bin/rinfo.rb
|
34
48
|
- lib/resilience.rb
|
35
49
|
- lib/resilience/attribute.rb
|
50
|
+
- lib/resilience/cli/all.rb
|
51
|
+
- lib/resilience/cli/bin/axe.rb
|
52
|
+
- lib/resilience/cli/bin/fcomp.rb
|
53
|
+
- lib/resilience/cli/bin/pex.rb
|
54
|
+
- lib/resilience/cli/bin/rarser.rb
|
55
|
+
- lib/resilience/cli/bin/reach.rb
|
56
|
+
- lib/resilience/cli/bin/rex.rb
|
57
|
+
- lib/resilience/cli/bin/rinfo.rb
|
58
|
+
- lib/resilience/cli/conf.rb
|
59
|
+
- lib/resilience/cli/default.rb
|
60
|
+
- lib/resilience/cli/disk.rb
|
61
|
+
- lib/resilience/cli/file.rb
|
62
|
+
- lib/resilience/cli/image.rb
|
63
|
+
- lib/resilience/cli/metadata.rb
|
64
|
+
- lib/resilience/cli/output.rb
|
65
|
+
- lib/resilience/collections/dirs.rb
|
66
|
+
- lib/resilience/collections/files.rb
|
67
|
+
- lib/resilience/collections/pages.rb
|
68
|
+
- lib/resilience/conf.rb
|
36
69
|
- lib/resilience/constants.rb
|
70
|
+
- lib/resilience/core_ext.rb
|
37
71
|
- lib/resilience/dirs.rb
|
38
72
|
- lib/resilience/dirs/root_dir.rb
|
39
73
|
- lib/resilience/fs_dir.rb
|
40
74
|
- lib/resilience/fs_dir/dir_base.rb
|
75
|
+
- lib/resilience/fs_dir/dir_entry.rb
|
76
|
+
- lib/resilience/fs_dir/file_entry.rb
|
41
77
|
- lib/resilience/fs_dir/record.rb
|
42
78
|
- lib/resilience/image.rb
|
43
79
|
- lib/resilience/mixins.rb
|
44
80
|
- lib/resilience/mixins/on_image.rb
|
81
|
+
- lib/resilience/page.rb
|
45
82
|
- lib/resilience/tables.rb
|
83
|
+
- lib/resilience/tables/boot.rb
|
46
84
|
- lib/resilience/tables/object.rb
|
47
85
|
- lib/resilience/tables/system.rb
|
86
|
+
- lib/resilience/trees.rb
|
87
|
+
- lib/resilience/trees/object_tree.rb
|
48
88
|
homepage: https://gist.github.com/movitto/866de4356f56a3b478ca
|
49
89
|
licenses:
|
50
90
|
- MIT
|
data/bin/cle.rb
DELETED
@@ -1,24 +0,0 @@
|
|
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
DELETED
@@ -1,28 +0,0 @@
|
|
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
DELETED
@@ -1,21 +0,0 @@
|
|
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
DELETED
@@ -1,49 +0,0 @@
|
|
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
DELETED
@@ -1,269 +0,0 @@
|
|
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
|