Class: Rex::Parser::NTFS

Inherits:
Object
  • Object
show all
Defined in:
lib/rex/parser/fs/ntfs.rb

Overview

This class parses the contents of an NTFS partition file. Author : Danil Bazin <danil.bazinhsc.fr> @danilbaz

Constant Summary collapse

DATA_ATTRIBUTE_ID =

Initialize the NTFS class with an already open file handler

128
INDEX_ROOT_ID =
144
INDEX_ALLOCATION_ID =
160

Instance Method Summary collapse

Constructor Details

#initialize(file_handler) ⇒ NTFS

Returns a new instance of NTFS.


17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# File 'lib/rex/parser/fs/ntfs.rb', line 17

def initialize(file_handler)
  @file_handler = file_handler
  data = @file_handler.read(4096)
  # Boot sector reading
  @bytes_per_sector = data[11, 2].unpack('v')[0]
  @sector_per_cluster = data[13].unpack('C')[0]
  @cluster_per_mft_record = data[64].unpack('c')[0]
  if @cluster_per_mft_record < 0
    @bytes_per_mft_record = 2**(-@cluster_per_mft_record)
    @cluster_per_mft_record = @bytes_per_mft_record.to_f / @bytes_per_sector / @sector_per_cluster
  else
    @bytes_per_mft_record = @bytes_per_sector * @sector_per_cluster * @cluster_per_mft_record
  end
  @bytes_per_cluster = @sector_per_cluster * @bytes_per_sector
  @mft_logical_cluster_number = data[48, 8].unpack('Q<')[0]
  @mft_offset = @mft_logical_cluster_number * @sector_per_cluster * @bytes_per_sector
  @file_handler.seek(@mft_offset)
  @mft = @file_handler.read(@bytes_per_mft_record)
end

Instance Method Details

#cluster_from_attribute_non_resident(attribute, cluster_num = 0, size_max = ((2**31) - 1)) ⇒ Object


148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
# File 'lib/rex/parser/fs/ntfs.rb', line 148

def cluster_from_attribute_non_resident(attribute, cluster_num = 0, size_max = ((2**31) - 1))
  lowvcn = attribute[16, 8].unpack('Q<')[0]
  highvcn = attribute[24, 8].unpack('Q<')[0]
  offset = attribute[32, 2].unpack('v')[0]
  real_size = attribute[48, 8].unpack('Q<')[0]
  attribut = ''
  run_list_num = lowvcn
  old_offset = 0
  while run_list_num <= highvcn
    first_runlist_byte = attribute[offset].ord
    run_offset_size = first_runlist_byte >> 4
    run_length_size = first_runlist_byte & 15
    run_length = attribute[offset + 1, run_length_size]
    run_length += "\x00" * (8 - run_length_size)
    run_length = run_length.unpack('Q<')[0]

    offset_run_offset = offset + 1 + run_length_size
    run_offset = attribute[offset_run_offset, run_offset_size]
    if run_offset[-1].ord & 128 == 128
      run_offset += "\xFF" * (8 - run_offset_size)
    else
      run_offset += "\x00" * (8 - run_offset_size)
    end
    run_offset = run_offset.unpack('q<')[0]
    #offset relative to previous offset
    run_offset += old_offset

    size_wanted = [run_length * @bytes_per_cluster, size_max - attribut.length].min
    if cluster_num + (size_max / @bytes_per_cluster) >= run_list_num && (cluster_num < run_length + run_list_num)
      run_list_offset_in_cluster = run_offset + [cluster_num - run_list_num, 0].max
      run_list_offset = (run_list_offset_in_cluster) * @bytes_per_cluster
      run_list_offset = run_list_offset.to_i
      @file_handler.seek(run_list_offset)

      data = ''
      while data.length < size_wanted
        # Use a 4Mb block size to avoid target memory consumption
        data << @file_handler.read([size_wanted - data.length, 2**22].min)
      end
      attribut << data
    end
    offset += run_offset_size + run_length_size + 1
    run_list_num += run_length
    old_offset = run_offset
  end
  attribut = attribut[0, real_size]
  attribut
end

#file(path) ⇒ Object

return the file path in the NTFS partition


241
242
243
244
245
246
247
248
249
250
251
252
253
254
# File 'lib/rex/parser/fs/ntfs.rb', line 241

def file(path)
  repertory = mft_record_from_mft_num(5)
  index_entry = {}
  path.split('\\').each do |r|
    attributes = mft_record_attribute(repertory)
    index = index_list_from_attributes(attributes)
    unless index.key?(r)
      fail ArgumentError, 'File path does not exist', caller
    end
    index_entry = index[r]
    repertory = mft_record_from_mft_num(index_entry['mft_offset'])
  end
  file_content_from_mft_num(index_entry['mft_offset'], index_entry['file_size'])
end

#file_content_from_mft_num(mft_num, size) ⇒ Object

Get the file from the MFT number The size must be gived because the $FILENAME attribute in the MFT entry does not contain it The file is in $DATA (128) Attribute


71
72
73
74
75
76
77
78
79
80
# File 'lib/rex/parser/fs/ntfs.rb', line 71

def file_content_from_mft_num(mft_num, size)
  mft_record = mft_record_from_mft_num(mft_num)
  attribute_list = mft_record_attribute(mft_record)
  if attribute_list[DATA_ATTRIBUTE_ID]['resident']
    return attribute_list[DATA_ATTRIBUTE_ID]['data']
  else
    data_attribute = attribute_list[DATA_ATTRIBUTE_ID]['data']
    return cluster_from_attribute_non_resident(data_attribute)[0, size]
  end
end

#filename_from_filenameattribute(attribute) ⇒ Object

Gather the name of the file from the $FILENAME (64) attribute


57
58
59
60
61
62
63
# File 'lib/rex/parser/fs/ntfs.rb', line 57

def filename_from_filenameattribute(attribute)
  filename_attribute = attribute
  length_of_name = filename_attribute[64].ord
  # uft16 *2
  d = ::Encoding::Converter.new('UTF-16LE', 'UTF-8')
  d.convert(filename_attribute[66, (length_of_name * 2)])
end

#index_list_from_attributes(attributes) ⇒ Object

return the list of files in attribute directory and their MFT number and size


138
139
140
141
142
143
144
145
146
# File 'lib/rex/parser/fs/ntfs.rb', line 138

def index_list_from_attributes(attributes)
  index_root_attribute = attributes[INDEX_ROOT_ID]
  index_record = index_root_attribute[16, index_root_attribute.length - 16]
  if attributes.key?(INDEX_ALLOCATION_ID)
    return parse_index_list(index_record, attributes[INDEX_ALLOCATION_ID])
  else
    return parse_index_list(index_record, '')
  end
end

#mft_record_attribute(mft_record, lazy = true) ⇒ Object

return the attribute list from the MFT record deal with resident and non resident attributes (but not $DATA due to performance issue) if lazy = True, this function only gather essential non resident attributes (INDEX_ALLOCATION). Non resident attributes can still be gathered later with cluster_from_attribute_non_resident function.


204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
# File 'lib/rex/parser/fs/ntfs.rb', line 204

def mft_record_attribute(mft_record, lazy=true)
  attribute_list_offset = mft_record[20, 2].unpack('C')[0]
  curs = attribute_list_offset
  attribute_identifier = mft_record[curs, 4].unpack('V')[0]
  res = {}
  while attribute_identifier != 0xFFFFFFFF
    # attribute_size=mft_record[curs + 4, 4].unpack('V')[0]
    # should be on 4 bytes but doesnt work
    attribute_size = mft_record[curs + 4, 2].unpack('v')[0]
    # resident
    if mft_record[curs + 8] == "\x00"
      content_size = mft_record[curs + 16, 4].unpack('V')[0]
      content_offset = mft_record[curs + 20, 2].unpack('v')[0]
      res[attribute_identifier] = mft_record[curs + content_offset, content_size]
    else
      # non resident
      if attribute_identifier == INDEX_ALLOCATION_ID or
        (!lazy and attribute_identifier != DATA_ATTRIBUTE_ID)
        res[attribute_identifier] = cluster_from_attribute_non_resident(mft_record[curs, attribute_size])
      else
        res[attribute_identifier] = mft_record[curs, attribute_size]
      end
    end
    if attribute_identifier == DATA_ATTRIBUTE_ID
      res[attribute_identifier] = {
        'data' => res[attribute_identifier],
        'resident' => mft_record[curs + 8] == "\x00" }
    end
    curs += attribute_size
    attribute_identifier = mft_record[curs, 4].unpack('V')[0]
  end
  res
end

#mft_record_from_mft_num(mft_num) ⇒ Object

Gather the MFT entry corresponding to his number


40
41
42
43
44
# File 'lib/rex/parser/fs/ntfs.rb', line 40

def mft_record_from_mft_num(mft_num)
  mft_num_offset = mft_num * @cluster_per_mft_record
  mft_data_attribute = mft_record_attribute(@mft)[DATA_ATTRIBUTE_ID]['data']
  cluster_from_attribute_non_resident(mft_data_attribute, mft_num_offset, @bytes_per_mft_record)
end

#parse_index(index_entry) ⇒ Object

parse one index record and return the name, MFT number and size of the file


85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/rex/parser/fs/ntfs.rb', line 85

def parse_index(index_entry)
  res = {}
  filename_size = index_entry[10, 2].unpack('v')[0]
  filename_attribute = index_entry[16, filename_size]
  # Should be 8 bytes but it doesn't work
  # mft_offset =  index_entry[0.unpack('Q<',:8])[0]
  # work with 4 bytes
  mft_offset =  index_entry[0, 4].unpack('V')[0]
  res[filename_from_filenameattribute(filename_attribute)] = {
    'mft_offset' => mft_offset,
    'file_size' => real_size_from_filenameattribute(filename_attribute) }
  res
end

#parse_index_list(index_record, index_allocation_attribute) ⇒ Object

parse index_record in $INDEX_ROOT and recursively index_record in INDEX_ALLOCATION


103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
# File 'lib/rex/parser/fs/ntfs.rb', line 103

def parse_index_list(index_record, index_allocation_attribute)
  offset_index_entry_list = index_record[0, 4].unpack('V')[0]
  index_size =  index_record[offset_index_entry_list + 8, 2].unpack('v')[0]
  index_size_in_bytes = index_size * @bytes_per_cluster
  index_entry = index_record[offset_index_entry_list, index_size]
  res = {}
  while index_entry[12, 4].unpack('V')[0] & 2 != 2
    res.update(parse_index(index_entry))
    # if son
    if index_entry[12, 4].unpack('V')[0] & 1 == 1
      # should be 8 bytes length
      vcn =  index_entry[-8, 4].unpack('V')[0]
      vcn_in_bytes = vcn * @bytes_per_cluster
      res_son = parse_index_list(index_allocation_attribute[vcn_in_bytes + 24, index_size_in_bytes], index_allocation_attribute)
      res.update(res_son)
    end
    offset_index_entry_list += index_size
    index_size =  index_record[offset_index_entry_list + 8, 2].unpack('v')[0]
    index_size_in_bytes = index_size * @bytes_per_cluster
    index_entry = index_record [offset_index_entry_list, index_size]
  end
  # if son on the last
  if index_entry[12, 4].unpack('V')[0] & 1 == 1
    # should be 8 bytes length
    vcn =  index_entry[-8, 4].unpack('V')[0]
    vcn_in_bytes = vcn * @bytes_per_cluster
    res_son = parse_index_list(index_allocation_attribute[vcn_in_bytes + 24, index_size_in_bytes], index_allocation_attribute)
    res.update(res_son)
  end
  res
end

#real_size_from_filenameattribute(attribute) ⇒ Object

Get the size of the file in the $FILENAME (64) attribute


49
50
51
52
# File 'lib/rex/parser/fs/ntfs.rb', line 49

def real_size_from_filenameattribute(attribute)
  filename_attribute = attribute
  filename_attribute[48, 8].unpack('Q<')[0]
end