Imported msfmachscan from Darren Kemp

git-svn-id: file:///home/svn/framework3/trunk@5682 4d416f70-5f16-0410-b530-b9f4589650da
This commit is contained in:
HD Moore 2008-09-24 22:14:33 +00:00
parent b3c3b29ed1
commit 1310e0e94c
7 changed files with 978 additions and 0 deletions

9
lib/rex/machparsey.rb Normal file
View File

@ -0,0 +1,9 @@
#!/usr/bin/env ruby
module Rex
module MachParsey
end
end
require 'rex/machparsey/mach'

View File

@ -0,0 +1,35 @@
#!/usr/bin/env ruby
module Rex
module MachParsey
class MachError < ::RuntimeError
end
class MachParseError < MachError
end
class MachHeaderError < MachParseError
end
class ProgramHeaderError < MachParseError
end
class BoundsError < MachError
end
#class WtfError < MachError
#end
class FatError < ::RuntimeError
end
class FatParseError < FatError
end
class FatHeaderError < FatParseError
end
end
end

209
lib/rex/machparsey/mach.rb Normal file
View File

@ -0,0 +1,209 @@
#!/usr/bin/env ruby
require 'rex/machparsey/machbase'
require 'rex/machparsey/exceptions'
require 'rex/image_source'
module Rex
module MachParsey
class Mach < MachBase
attr_accessor :mach_header, :segments, :isource, :bits, :endian, :arch, :fat_offset
def initialize(isource, offset = 0, fat = false)
_parse_mach_header(isource, offset)
if fat == true
self.fat_offset = offset
else
self.fat_offset = 0
end
self.isource = isource
end
def _parse_mach_header(isource, offset)
self.mach_header = MachHeader.new(isource.read(offset, MACH_HEADER_SIZE_64))
bits = mach_header.bits
endian = mach_header.endian
ncmds = mach_header.ncmds
if bits == BITS_32
offset += MACH_HEADER_SIZE
else
offset += MACH_HEADER_SIZE_64
end
segments = []
ncmds.times do
load_command = LoadCommand.new(isource.read(offset, LOAD_COMMAND_SIZE), endian)
case load_command.cmd
when LC_SEGMENT
segments << Segment.new(isource.read(offset, SEGMENT_COMMAND_SIZE), bits, endian)
when LC_SEGMENT_64
segments << Segment.new(isource.read(offset, SEGMENT_COMMAND_SIZE_64), bits, endian)
end
offset += load_command.cmdsize
end
self.mach_header = mach_header
self.segments = segments
self.isource = isource
self.bits = bits
self.endian = endian
return segments
end
def self.new_from_file(filename, disk_backed = false)
file = ::File.open(filename, "rb")
if disk_backed
return self.new(ImageSource::Disk.new(file))
else
obj = new_from_string(file.read)
file.close
return obj
end
end
def self.new_from_string(data)
return self.new(ImageSource::Memory.new(data))
end
def ptr_64?
mach_header.bits == BITS_64
end
def ptr_32?
ptr_64? == false
end
def ptr_s(vaddr)
(ptr_32?) ? ("0x%.8x" % vaddr) : ("0x%.16x" % vaddr)
end
def read(offset, len)
isource.read(offset, len)
end
def index(*args)
isource.index(*args)
end
def close
isource.close
end
end
class Fat < FatBase
attr_accessor :fat_header, :fat_archs, :machos, :isource
def initialize(isource, offset = 0)
self.fat_archs = []
self.machos = []
self.isource = isource
self.fat_header = FatHeader.new(isource.read(offset, FAT_HEADER_SIZE))
if !self.fat_header
raise FatHeaderError, "Could not parse FAT header"
end
print "Detected " + self.fat_header.nfat_arch.to_s + " archs in binary.\n"
offset += FAT_HEADER_SIZE
self.fat_header.nfat_arch.times do
fat_arch = FatArch.new(isource.read(offset, FAT_ARCH_SIZE), self.fat_header.endian)
self.fat_archs << fat_arch
self.machos << Mach.new(isource, fat_arch.offset, true)
offset += FAT_ARCH_SIZE
end
end
#this is useful for debugging but we don't use it for anything.
def _parse_fat_header(isource, offset)
archs = []
nfat_arch = self.fat_header.nfat_arch
print "Number of archs in binary: " + nfat_arch.to_s + "\n"
nfat_arch.times do
arch = FatArch.new(isource.read(offset, FAT_ARCH_SIZE), self.endian)
case arch.cpu_type
when CPU_TYPE_I386
print "i386\n"
when CPU_TYPE_X86_64
print "x86_64\n"
when CPU_TYPE_ARM
print "Arm\n"
when CPU_TYPE_POWERPC
print "Power PC\n"
when CPU_TYPE_POWERPC64
print "Power PC 64\n"
end
offset += FAT_ARCH_SIZE
end
end
def self.new_from_file(filename, disk_backed = false)
file = ::File.open(filename, "rb")
if disk_backed
return self.new(ImageSource::Disk.new(file))
else
obj = new_from_string(file.read)
file.close
return obj
end
end
def self.new_from_string(data)
return self.new(ImageSource::Memory.new(data))
end
def ptr_64?
mach_header.bits == BITS_64
end
def ptr_32?
ptr_64? == false
end
def ptr_s(vaddr)
(ptr_32?) ? ("0x%.8x" % vaddr) : ("0x%.16x" % vaddr)
end
def read(offset, len)
isource.read(offset, len)
end
def index(*args)
isource.index(*args)
end
def close
isource.close
end
end
end
end

View File

@ -0,0 +1,408 @@
#!/usr/bin/env ruby
require 'rex/struct2'
module Rex
module MachParsey
require 'rex/machparsey/exceptions'
require 'rex/struct2'
class GenericStruct
attr_accessor :struct
def initialize(_struct)
self.struct = _struct
end
# Access a value
def v
struct.v
end
# Access a value by array
def [](*args)
struct[*args]
end
# Obtain an array of all fields
def keys
struct.keys
end
def method_missing(meth, *args)
v[meth.to_s] || (raise NoMethodError.new, meth)
end
end
class GenericHeader < GenericStruct
end
BITS_32 = 0
BITS_64 = 1
ENDIAN_LSB = 0
ENDIAN_MSB = 1
class MachBase
MH_MAGIC = 0xfeedface
MH_MAGIC_64 = 0xfeedfacf
MH_CIGAM = 0xcefaedfe
MH_CIGAM_64 = 0xcffaedfe
MACH_HEADER_SIZE = 28
MACH_HEADER_SIZE_64 = 32
MACH_HEADER_LSB = Rex::Struct2::CStructTemplate.new(
['uint32v', 'magic', 0],
['uint32v', 'cputype', 0],
['uint32v', 'cpusubtype',0],
['uint32v', 'filetype', 0],
['uint32v', 'ncmds', 0],
['uint32v', 'sizeofcmds',0],
['uint32v', 'flags', 0]
)
MACH_HEADER_MSB = Rex::Struct2::CStructTemplate.new(
['uint32n', 'magic', 0],
['uint32n', 'cputype', 0],
['uint32n', 'cpusubtype',0],
['uint32n', 'filetype', 0],
['uint32n', 'ncmds', 0],
['uint32n', 'sizeofcmds',0],
['uint32n', 'flags', 0]
)
MACH_HEADER_64_LSB = Rex::Struct2::CStructTemplate.new(
['uint32v', 'magic', 0],
['uint32v', 'cputype', 0],
['uint32v', 'cpusubtype',0],
['uint32v', 'filetype', 0],
['uint32v', 'ncmds', 0],
['uint32v', 'sizeofcmds',0],
['uint32v', 'flags', 0],
['uint32v', 'reserved', 0]
)
MACH_HEADER_64_MSB = Rex::Struct2::CStructTemplate.new(
['uint32n', 'magic', 0],
['uint32n', 'cputype', 0],
['uint32n', 'cpusubtype',0],
['uint32n', 'filetype', 0],
['uint32n', 'ncmds', 0],
['uint32n', 'sizeofcmds',0],
['uint32n', 'flags', 0],
['uint32n', 'reserved', 0]
)
#cpu types for Mach-O binaries
CPU_TYPE_I386 = 0x7
CPU_TYPE_X86_64 = 0x01000007
CPU_TYPE_ARM = 0xC
CPU_TYPE_POWERPC = 0x12
CPU_TYPE_POWERPC64 = 0x01000012
CPU_SUBTYPE_LITTLE_ENDIAN = 0
CPU_SUBTYPE_BIG_ENDIAN = 1
LC_SEGMENT = 0x1 #/* segment of this file to be mapped */
LC_SYMTAB = 0x2 #/* link-edit stab symbol table info */
LC_SYMSEG = 0x3 #/* link-edit gdb symbol table info (obsolete) */
LC_THREAD = 0x4 #/* thread */
LC_UNIXTHREAD = 0x5 #/* unix thread (includes a stack) */
LC_LOADFVMLIB = 0x6 #/* load a specified fixed VM shared library */
LC_IDFVMLIB = 0x7 #/* fixed VM shared library identification */
LC_IDENT = 0x8 #/* object identification info (obsolete) */
LC_FVMFILE = 0x9 #/* fixed VM file inclusion (internal use) */
LC_PREPAGE = 0xa #/* prepage command (internal use) */
LC_DYSYMTAB = 0xb #/* dynamic link-edit symbol table info */
LC_LOAD_DYLIB = 0xc #/* load a dynamicly linked shared library */
LC_ID_DYLIB = 0xd #/* dynamicly linked shared lib identification */
LC_LOAD_DYLINKER = 0xe #/* load a dynamic linker */
LC_ID_DYLINKER = 0xf #/* dynamic linker identification */
LC_PREBOUND_DYLIB = 0x10 #/* modules prebound for a dynamicly */
LC_SEGMENT_64 = 0x19 #/* segment of this file to be mapped */
class MachHeader < GenericHeader
attr_accessor :bits, :endian
def initialize(rawdata)
mach_header = MACH_HEADER_LSB.make_struct
if !mach_header.from_s(rawdata)
raise MachHeaderError, "Could't access Mach-O Magic", caller
end
if mach_header.v['magic'] == MH_MAGIC
endian = ENDIAN_LSB
bits = BITS_32
mach_header = MACH_HEADER_LSB.make_struct
elsif mach_header.v['magic'] == MH_CIGAM
bits = BITS_32
endian = ENDIAN_MSB
mach_header = MACH_HEADER_MSB.make_struct
elsif mach_header.v['magic'] == MH_MAGIC_64
endian = ENDIAN_LSB
bits = BITS_64
mach_header = MACH_HEADER_LSB.make_struct
elsif mach_header.v['magic'] == MH_CIGAM_64
endian = ENDIAN_MSB
bits = BITS_64
mach_header = MACH_HEADER_MSB.make_struct
else
raise MachHeaderError, "Couldn't find Mach Magic", caller
end
if !mach_header.from_s(rawdata)
raise MachHeaderError, "Could't process Mach-O Header", caller
end
self.struct = mach_header
self.endian = endian
self.bits = bits
end
end
LOAD_COMMAND_SIZE = 8
LOAD_COMMAND_LSB = Rex::Struct2::CStructTemplate.new(
['uint32v','cmd',0],
['uint32v','cmdsize',0]
)
LOAD_COMMAND_MSB = Rex::Struct2::CStructTemplate.new(
['uint32n','cmd',0],
['uint32n','cmdsize',0]
)
class LoadCommand < GenericHeader
def initialize(rawdata, endian)
if endian == ENDIAN_MSB
load_command = LOAD_COMMAND_MSB.make_struct
else
load_command = LOAD_COMMAND_LSB.make_struct
end
if !load_command.from_s(rawdata)
raise MachParseError, "Couldn't parse load command"
end
self.struct = load_command
end
end
SEGMENT_COMMAND_SIZE = 56
SEGMENT_COMMAND_LSB = Rex::Struct2::CStructTemplate.new(
['uint32v', 'cmd', 0],
['uint32v', 'cmdsize', 0],
['string', 'segname', 16, ''],
['uint32v', 'vmaddr', 0],
['uint32v', 'vmsize', 0],
['uint32v', 'fileoff', 0],
['uint32v', 'filesize', 0],
['uint32v', 'maxprot', 0],
['uint32v', 'initprot', 0],
['uint32v', 'nsects', 0],
['uint32v', 'flags', 0]
)
SEGMENT_COMMAND_MSB = Rex::Struct2::CStructTemplate.new(
['uint32n', 'cmd', 0],
['uint32n', 'cmdsize', 0],
['string', 'segname', 16, ''],
['uint32n', 'vmaddr', 0],
['uint32n', 'vmsize', 0],
['uint32n', 'fileoff', 0],
['uint32n', 'filesize', 0],
['uint32n', 'maxprot', 0],
['uint32n', 'initprot', 0],
['uint32n', 'nsects', 0],
['uint32n', 'flags', 0]
)
SEGMENT_COMMAND_SIZE_64 = 72
SEGMENT_COMMAND_64_LSB = Rex::Struct2::CStructTemplate.new(
['uint32v', 'cmd', 0],
['uint32v', 'cmdsize', 0],
['string', 'segname', 16, ''],
['uint64v', 'vmaddr', 0],
['uint64v', 'vmsize', 0],
['uint64v', 'fileoff', 0],
['uint64v', 'filesize', 0],
['uint32v', 'maxprot', 0],
['uint32v', 'initprot', 0],
['uint32v', 'nsects', 0],
['uint32v', 'flags', 0]
)
SEGMENT_COMMAND_64_MSB = Rex::Struct2::CStructTemplate.new(
['uint32n', 'cmd', 0],
['uint32n', 'cmdsize', 0],
['string', 'segname', 16, ''],
['uint64n', 'vmaddr', 0],
['uint64n', 'vmsize', 0],
['uint64n', 'fileoff', 0],
['uint64n', 'filesize', 0],
['uint32n', 'maxprot', 0],
['uint32n', 'initprot', 0],
['uint32n', 'nsects', 0],
['uint32n', 'flags', 0]
)
class Segment < GenericHeader
attr_accessor :_bits, :_endian
def initialize(rawdata, bits, endian)
self._bits = bits
if bits == BITS_64
if endian == ENDIAN_MSB
segment_command = SEGMENT_COMMAND_64_MSB.make_struct
else
segment_command = SEGMENT_COMMAND_64_LSB.make_struct
end
else
if endian == ENDIAN_MSB
segment_command = SEGMENT_COMMAND_MSB.make_struct
else
segment_command = SEGMENT_COMMAND_LSB.make_struct
end
end
if !segment_command.from_s(rawdata)
raise MachParseError, "Couldn't parse segment command"
end
self.struct = segment_command
end
def Segname
v['segname']
end
def Vmaddr
v['vmaddr']
end
def Vmsize
v['vmsize']
end
def FileOff
v['fileoff']
end
def FileSize
v['filesize']
end
end
class Thread < GenericHeader
def initialize(rawdata)
end
end
end
FAT_MAGIC = 0xcafebabe
FAT_CIGAM = 0xbebafeca
FAT_HEADER_SIZE = 8
FAT_HEADER_LSB = Rex::Struct2::CStructTemplate.new(
['uint32v', 'magic', 0],
['uint32v', 'nfat_arch',0]
)
FAT_HEADER_MSB = Rex::Struct2::CStructTemplate.new(
['uint32n', 'magic', 0],
['uint32n', 'nfat_arch',0]
)
FAT_ARCH_SIZE = 20
FAT_ARCH_LSB = Rex::Struct2::CStructTemplate.new(
['uint32v', 'cpu_type', 0],
['uint32v', 'cpu_subtype',0],
['uint32v', 'offset', 0],
['uint32v', 'size', 0],
['uint32v', 'align', 0]
)
FAT_ARCH_MSB = Rex::Struct2::CStructTemplate.new(
['uint32n', 'cpu_type', 0],
['uint32n', 'cpu_subtype',0],
['uint32n', 'offset', 0],
['uint32n', 'size', 0],
['uint32n', 'align', 0]
)
class FatBase
class FatHeader < GenericHeader
attr_accessor :nfat_arch, :endian, :exists
def initialize(rawdata)
fat_header = FAT_HEADER_LSB.make_struct
if !fat_header.from_s(rawdata)
#raise something
end
magic = fat_header.v['magic']
if magic == FAT_MAGIC
endian = ENDIAN_LSB
elsif magic == FAT_CIGAM
endian = ENDIAN_MSB
fat_header = FAT_HEADER_MSB.make_struct
if !fat_header.from_s(rawdata)
raise FatHeaderError, "Could not parse FAT header"
end
else
self.exists = 0
return
end
self.nfat_arch = fat_header.v['nfat_arch']
self.struct = fat_header
self.endian = endian
end
end
class FatArch < GenericHeader
attr_accessor :cpu_type, :cpu_subtype, :offset, :size
def initialize(rawdata, endian)
if endian == ENDIAN_LSB
fat_arch = FAT_ARCH_LSB.make_struct
else
fat_arch = FAT_ARCH_MSB.make_struct
end
if !fat_arch.from_s(rawdata)
raise FatHeaderError, "Could not parse arch from FAT header"
end
self.cpu_type = fat_arch.v['cpu_type']
self.cpu_subtype = fat_arch.v['cpu_subtype']
self.offset = fat_arch.v['offset']
self.size = fat_arch.v['size']
self.struct = fat_arch
end
end
class Thread < GenericHeader
def initialize(rawdata)
end
end
end
end
end

9
lib/rex/machscan.rb Normal file
View File

@ -0,0 +1,9 @@
#!/usr/bin/env ruby
module Rex
module MachScan
end
end
require 'rex/machscan/scanner'

216
lib/rex/machscan/scanner.rb Normal file
View File

@ -0,0 +1,216 @@
#!/usr/bin/env ruby
module Rex
module MachScan
module Scanner
class Generic
attr_accessor :mach, :fat, :regex
def initialize(binary)
if binary.class == Rex::MachParsey::Mach
self.mach = binary
else
self.fat = binary
end
end
def config(param)
end
def scan(param)
config(param)
$stdout.puts "[#{param['file']}]"
if !self.mach
for mach in fat.machos
if mach.mach_header.cputype == 0x7 #since we only support intel for the time being its all we process
self.mach = mach
end
end
end
self.mach.segments.each do |segment|
if segment.segname.include? "__TEXT"
scan_segment(segment, param).each do |hit|
vaddr = hit[0]
message = hit[1].is_a?(Array) ? hit[1].join(" ") : hit[1]
$stdout.puts self.mach.ptr_s(vaddr - self.mach.fat_offset) + " " + message
end
end
end
end
def scan_segment(segment, param={})
[]
end
end
class JmpRegScanner < Generic
def config(param)
regnums = param['args']
# build a list of the call bytes
calls = _build_byte_list(0xd0, regnums - [4]) # note call esp's don't work..
jmps = _build_byte_list(0xe0, regnums)
pushs1 = _build_byte_list(0x50, regnums)
pushs2 = _build_byte_list(0xf0, regnums)
regexstr = '('
if !calls.empty?
regexstr += "\xff[#{calls}]|"
end
regexstr += "\xff[#{jmps}]|([#{pushs1}]|\xff[#{pushs2}])(\xc3|\xc2..))"
self.regex = Regexp.new(regexstr)
end
# build a list for regex of the possible bytes, based on a base
# byte and a list of register numbers..
def _build_byte_list(base, regnums)
regnums.collect { |regnum| Regexp.escape((base | regnum).chr) }.join('')
end
def _ret_size(offset)
case mach.read(offset, 1)
when "\xc3"
return 1
when "\xc2"
return 3
end
$stderr.puts("Invalid return instruction")
end
def _parse_ret(data)
if data.length == 1
return "ret"
else
return "retn 0x%04x" % data[1, 2].unpack('v')[0]
end
end
def scan_segment(segment, param={})
base_addr = segment.vmaddr
segment_offset = segment.fileoff
offset = segment_offset
hits = []
while (offset = mach.index(regex, offset)) != nil
vaddr = base_addr + (offset - segment_offset)
message = ''
parse_ret = false
byte1 = mach.read(offset, 1)[0]
if byte1 == 0xff
byte2 = mach.read(offset+1, 1)[0]
regname = Rex::Arch::X86.reg_name32(byte2 & 0x7)
case byte2 & 0xf8
when 0xd0
message = "call #{regname}"
offset += 2
when 0xe0
message = "jmp #{regname}"
offset += 2
when 0xf0
retsize = _ret_size(offset+2)
message = "push #{regname}; " + _parse_ret(mach.read(offset+2, retsize))
offset += 2 + retsize
else
raise "wtf"
end
else
regname = Rex::Arch::X86.reg_name32(byte1 & 0x7)
retsize = _ret_size(offset+1)
message = "push #{regname}; " + _parse_ret(mach.read(offset+1, retsize))
offset += 1 + retsize
end
hits << [ vaddr, message ]
end
return hits
end
end
class PopPopRetScanner < JmpRegScanner
def config(param)
pops = _build_byte_list(0x58, (0 .. 7).to_a - [4]) # we don't want pop esp's...
self.regex = Regexp.new("[#{pops}][#{pops}](\xc3|\xc2..)")
end
def scan_segment(segment, param={})
base_addr = segment.vmaddr
segment_offset = segment.fileoff
offset = segment_offset
hits = []
while offset < segment.fileoff + segment.filesize && (offset = mach.index(regex, offset)) != nil
vaddr = base_addr + (offset - segment_offset)
message = ''
pops = mach.read(offset, 2)
reg1 = Rex::Arch::X86.reg_name32(pops[0] & 0x7)
reg2 = Rex::Arch::X86.reg_name32(pops[1] & 0x7)
message = "pop #{reg1}; pop #{reg2}; "
retsize = _ret_size(offset+2)
message += _parse_ret(mach.read(offset+2, retsize))
offset += 2 + retsize
hits << [ vaddr, message ]
end
return hits
end
end
class RegexScanner < JmpRegScanner
def config(param)
self.regex = Regexp.new(param['args'])
end
def scan_segment(segment, param={})
base_addr = segment.vmaddr
segment_offset = segment.fileoff
offset = segment_offset
hits = []
while offset < segment.fileoff + segment.filesize && (offset = mach.index(regex, offset)) != nil
idx = offset
buf = ''
mat = nil
while (! (mat = buf.match(regex)))
buf << mach.read(idx, 1)
idx += 1
end
vaddr = base_addr + (offset - segment_offset)
hits << [ vaddr, buf.unpack("H*") ]
offset += buf.length
end
return hits
end
end
end
end
end

92
msfmachscan.rb Executable file
View File

@ -0,0 +1,92 @@
#!/usr/bin/env ruby
msfbase = File.symlink?(__FILE__) ? File.readlink(__FILE__) : __FILE__
$:.unshift(File.join(File.dirname(msfbase), 'lib'))
require 'rex/machparsey'
require 'rex/machscan'
require 'rex/arch/x86'
require 'optparse'
def opt2i(o)
o.index("0x")==0 ? o.hex : o.to_i
end
opt = OptionParser.new
opt.banner = "Usage: #{$PROGRAM_NAME} [mode] <options> [targets]"
opt.separator('')
opt.separator('Modes:')
worker = nil
param = {}
opt.on('-j', '--jump [regA,regB,regC]', 'Search for jump equivalent instructions') do |t|
# take csv of register names (like eax,ebx) and convert
# them to an array of register numbers
regnums = t.split(',').collect { |o| Rex::Arch::X86.reg_number(o) }
worker = Rex::MachScan::Scanner::JmpRegScanner
param['args'] = regnums
end
opt.on('-p', '--poppopret', 'Search for pop+pop+ret combinations') do |t|
worker = Rex::MachScan::Scanner::PopPopRetScanner
param['args'] = t
end
opt.on('-r', '--regex [regex]', 'Search for regex match') do |t|
worker = Rex::MachScan::Scanner::RegexScanner
param['args'] = t
end
opt.separator('')
opt.separator('Options:')
opt.on('-A', '--after [bytes]', 'Number of bytes to show after match (-a/-b)') do |t|
param['after'] = opt2i(t)
end
opt.on('-B', '--before [bytes]', 'Number of bytes to show before match (-a/-b)') do |t|
param['before'] = opt2i(t)
end
opt.on('-I', '--image-base [address]', 'Specify an alternate ImageBase') do |t|
param['imagebase'] = opt2i(t)
end
opt.on_tail("-h", "--help", "Show this message") do
puts opt
exit(0)
end
opt.parse!
if (! worker)
puts opt
exit(0)
end
ARGV.each do |file|
param['file'] = file
begin
mach = Rex::MachParsey::Mach.new_from_file(file, true)
o = worker.new(mach)
o.scan(param)
mach.close
rescue Rex::MachParsey::MachHeaderError
$stderr.puts("File is not a Mach-O binary, trying Fat..\n")
fat = Rex::MachParsey::Fat.new_from_file(file, true)
o = worker.new(fat)
o.scan(param)
fat.close
rescue Errno::ENOENT
$stderr.puts("File does not exist: #{file}")
next
end
end
#end