diff --git a/lib/rex/ole.rb b/lib/rex/ole.rb new file mode 100644 index 0000000000..47d6cdaec7 --- /dev/null +++ b/lib/rex/ole.rb @@ -0,0 +1,128 @@ +## +# $Id$ +# Version: $Revision$ +## + +## +# Rex::OLE - an OLE implementation +# written in 2010 by Joshua J. Drake +# +# License: MSF_LICENSE +# +# +# This module implements Object-Linking-and-Embedding otherwise known as +# Compound File Binary File Format or Windows Compound Binary File Format. +# OLE is the container format for modern Excel, Word, PowerPoint, and many +# other file formats. +# +# NOTE: This implementation is almost fully compliant with [MS-CFB] v1.1 +# +# +# SUPPORTS: +# +# 1. R/W v3 OLE files (v4 may work, but wasn't tested) +# 2. RO double-indirect fat sectors +# 3. RO fat sectors (including those in double-indirect parts) +# 4. WO support for less than 109 fat sectors :) +# 5. R/W minifat sectors +# 6. R/W ministream +# 7. R/W normal streams +# 8. R/W substorages (including nesting) +# 9. full directory support (hierarchal and flattened access) +# 10. big and little endian files (although only little endian was tested) +# +# +# TODO (in order of priority): +# +# 1. support deleting storages/streams +# 2. create copyto and other typical interface functions +# 3. support writing DIF sectors > 109 +# - may lead to allocating more fat sectors :-/ +# 4. properly support mode params for open_stream/open_storage/etc +# 5. optimize to prevent unecessary loading/writing +# 6. support for auxillary streams (DocumentSummaryInformation and SummaryInformation) +# 7. support non-committal editing (open, change, close w/o save) +# 8. support timestamps +# 9. provide interface to change paramters (endian, etc) +# +# +# TO INVESTIGATE: +# +# 1. moving storage interface functions into something used by both +# the main storage and substorages (unifying the code) (mixin?) +# 2. eliminating flattening the directory prior to writing it out +# +## + +require 'rex' + +module Rex +module OLE + +# misc util +# NOTE: the v1.1 spec says that everything "MUST be stored in little-endian byte order" +BIG_ENDIAN = 0xfeff +LITTLE_ENDIAN = 0xfffe +# defines Util class +require 'rex/ole/util' +require 'rex/ole/clsid' + + +# constants for dealing with the header +HDR_SZ = 512 +# signatures +SIG = "\xd0\xcf\x11\xe0\xa1\xb1\x1a\xe1" +SIG_BETA = "\x0e\x11\xfc\x0d\xd0\xcf\x11\xe0" +# defines Header class +require 'rex/ole/header' + + +# sector types +SECT_MAX = 0xfffffffa +SECT_DIF = 0xfffffffc +SECT_FAT = 0xfffffffd +SECT_END = 0xfffffffe +SECT_FREE = 0xffffffff +# defines DIFAT class +require 'rex/ole/difat' +# defines FAT class +require 'rex/ole/fat' +# defines MiniFAT class +require 'rex/ole/minifat' + + +# directory entries +DIRENTRY_SZ = 128 +DIR_NOSTREAM = 0xffffffff +DIR_MAXREGSID = 0xfffffffa +# defines Directory class +require 'rex/ole/directory' + +# types +STGTY_INVALID = 0 +STGTY_STORAGE = 1 +STGTY_STREAM = 2 +STGTY_LOCKBYTES = 3 +STGTY_PROPERTY = 4 +STGTY_ROOT = 5 +# for red/black tree +COLOR_RED = 0 +COLOR_BLACK = 1 +# defines DirEntry base class +require 'rex/ole/direntry' + + +# constants for storages +STGM_READ = 0 +STGM_WRITE = 1 +STGM_READWRITE = 2 +# defines Storage class +require 'rex/ole/storage' +# defines SubStorage class +require 'rex/ole/substorage' +# defines Stream class +require 'rex/ole/stream' + + +end +end diff --git a/lib/rex/ole/clsid.rb b/lib/rex/ole/clsid.rb new file mode 100644 index 0000000000..4158205467 --- /dev/null +++ b/lib/rex/ole/clsid.rb @@ -0,0 +1,47 @@ +## +# $Id$ +# Version: $Revision$ +## + +## +# Rex::OLE - an OLE implementation +# written in 2010 by Joshua J. Drake +## + + +module Rex +module OLE + +class CLSID + + def initialize(buf=nil) + @buf = buf + @buf ||= "\x00" * 16 + end + + def pack + @buf + end + + def to_s + ret = "" + ret << "%08x" % Util.get32(@buf, 0) + ret << "-" + ret << "%04x" % Util.get16(@buf, 4) + ret << "-" + ret << "%04x" % Util.get16(@buf, 6) + ret << "-" + idx = 0 + last8 = @buf[8,8] + last8.unpack('C*').each { |byte| + ret << [byte].pack('C').unpack('H*')[0] + ret << "-" if (idx == 1) + idx += 1 + } + ret + end + +end + +end +end diff --git a/lib/rex/ole/difat.rb b/lib/rex/ole/difat.rb new file mode 100644 index 0000000000..ac48e0102e --- /dev/null +++ b/lib/rex/ole/difat.rb @@ -0,0 +1,141 @@ +## +# $Id$ +# Version: $Revision$ +## + +## +# Rex::OLE - an OLE implementation +# written in 2010 by Joshua J. Drake +## + +module Rex +module OLE + +class DIFAT + + def initialize stg + @stg = stg + @entries = [] + end + + # + # convenience access to entries + # + def []=(idx,expr) + @entries[idx] = expr + end + + def [](idx) + @entries[idx] + end + + def +(expr) + @entries += expr + self + end + + def <<(expr) + @entries << expr + end + + def length + @entries.length + end + + def slice!(start,stop) + @entries.slice!(start,stop) + end + + def reset + @entries = [] + end + + def each + @entries.each { |el| + yield el + } + end + + # + # woop + # + def to_s + ret = "{ " + @entries.each { |el| + ret << ", " if (ret.length > 2) + case el + when SECT_END + ret << "END" + when SECT_DIF + ret << "DIF" + when SECT_FAT + ret << "FAT" + when SECT_FREE + ret << "FREE" + else + ret << "0x%x" % el + end + } + ret << " }" + ret + end + + # + # low-level functions + # + def read + @entries = [] + + # start with the header part + @entries += @stg.header._sectFat + + # double indirect fat + sect = @stg.header._sectDifStart + while (sect != SECT_END) + if (@entries.include?(sect)) + raise RuntimeError, 'Sector chain loop detected (0x%08x)' % sect + end + + @entries << sect + buf = @stg.read_sector(sect, @stg.header.sector_size) + + # the last sect ptr in the block becomes the next entry + sect = Util.get32(buf, ((@stg.header.idx_per_sect)-1) * 4) + end + + # don't need these free ones, but it doesn't hurt to keep them. + #@difat.delete(SECT_FREE) + end + + def write + len = @entries.length + first109 = @entries.dup + + rest = nil + if (len > 109) + rest = first109.slice!(109,len) + end + + @stg.header._sectFat = [] + @stg.header._sectFat += first109 + if (len < 109) + need = 109 - len + need.times { + @stg.header._sectFat << SECT_FREE + } + end + + if (rest and rest.length > 0) + raise RuntimeError, 'TODO: support writing DIF properly!' + # may require adding more fat sectors :-/ + #@stg.header._csectDif = rest.length + #@stg.header._sectDifStart = idx + end + + @stg.header._csectFat = len + end + +end + +end +end diff --git a/lib/rex/ole/directory.rb b/lib/rex/ole/directory.rb new file mode 100644 index 0000000000..e1b954c419 --- /dev/null +++ b/lib/rex/ole/directory.rb @@ -0,0 +1,230 @@ +## +# $Id$ +# Version: $Revision$ +## + +## +# Rex::OLE - an OLE implementation +# written in 2010 by Joshua J. Drake +## + +module Rex +module OLE + +require 'rex/ole/direntry' + +# +# This class serves as the root directory entry in addition to +# an abstraction around the concept of a directory as a whole. +# +class Directory < DirEntry + + # XXX: num_entries is not maintained once a stream/storage is added! + attr_accessor :num_entries + + def initialize(stg) + super + + @num_entries = 1 + end + + + # woop, recursive each + def yield_entries(de, &block) + block.call(de) + de.each { |el| + yield_entries(el, &block) + } + end + def each_entry(&block) + yield_entries(self, &block) + end + + + def set_ministream_params(start, size) + @_sectStart = start + @_ulSize = size + end + + def link_item(parent, child) + # set sid, advance count + child.sid = @num_entries + @num_entries += 1 + + + # link item to siblings and/or parent + if (parent._sidChild == DIR_NOSTREAM) + parent._sidChild = child.sid + dlog("Linking #{child.name} as THE child of #{parent.name} as sid #{child.sid}", 'rex', LEV_3) + else + sib = nil + parent.each { |el| + if (el._sidLeftSib == DIR_NOSTREAM) + sib = el + el._sidLeftSib = child.sid + dlog("Linking #{child.name} as the LEFT sibling of #{sib.name} as sid #{child.sid}", 'rex', LEV_3) + break + end + if (el._sidRightSib == DIR_NOSTREAM) + sib = el + el._sidRightSib = child.sid + dlog("Linking #{child.name} as the RIGHT sibling of #{sib.name} as sid #{child.sid}", 'rex', LEV_3) + break + end + } + raise RuntimeError, 'Unable to find a sibling to link to in the directory' + end + parent << child + end + + + # + # low-level functions + # + def from_s(sid, buf) + super + + if (@_sidRightSib != DIR_NOSTREAM) + raise RuntimeError, 'Root Entry is invalid! (has right sibling)' + end + if (@_sidLeftSib != DIR_NOSTREAM) + raise RuntimeError, 'Root Entry is invalid! (has left sibling)' + end + end + + def read + @children = [] + visited = [] + entries = [] + root_node = nil + sect = @stg.header._sectDirStart + while (sect != SECT_END) + + if (visited.include?(sect)) + raise RuntimeError, 'Sector chain loop detected (0x%08x)' % sect + end + visited << sect + + sbuf = @stg.read_sector(sect, @stg.header.sector_size) + while (sbuf.length >= DIRENTRY_SZ) + debuf = sbuf.slice!(0, DIRENTRY_SZ) + + type = Util.get8(debuf, 0x42) + case type + when STGTY_ROOT + if (entries.length != 0) + raise RuntimeError, 'Root Entry found, but not first encountered!' + end + if (root_node) + raise RuntimeError, 'Multiple root directory sectors detected (0x%08x)' % sect + end + de = self + root_node = de + + when STGTY_STORAGE + de = SubStorage.new @stg + + when STGTY_STREAM + de = Stream.new @stg + + when STGTY_INVALID + # skip invalid entries + next + + else + raise RuntimeError, 'Unsupported directory entry type (0x%02x)' % type + end + + # read content + de.from_s(entries.length, debuf) + entries << de + end + sect = @stg.next_sector(sect) + end + + @num_entries = entries.length + + # sort out the tree structure, starting with the root + if (@_sidChild != DIR_NOSTREAM) + populate_children(entries, root_node, @_sidChild) + end + end + + + # recursively add entries to their proper parents :) + def populate_children(entries, parent, sid) + node = entries[sid] + dlog("populate_children(entries, \"#{parent.name}\", #{sid}) - node: #{node.name}", 'rex', LEV_3) + parent << node + if (node.type == STGTY_STORAGE) and (node._sidChild != DIR_NOSTREAM) + populate_children(entries, node, node._sidChild) + end + if (node._sidLeftSib != DIR_NOSTREAM) + populate_children(entries, parent, node._sidLeftSib) + end + if (node._sidRightSib != DIR_NOSTREAM) + populate_children(entries, parent, node._sidRightSib) + end + end + + # NOTE: this may not be necessary if we were to use each_entry + def flatten_tree(entries, parent) + entries << parent + parent.each { |el| + flatten_tree(entries, el) + } + end + + + def write + # flatten the directory again + entries = [] + flatten_tree(entries, self) + dlog("flattened tree has #{entries.length} entries...", 'rex', LEV_3) + + # count directory sectors + ds_count = entries.length / 4 + if ((entries.length % 4) > 0) + # one more sector to hold the rest + ds_count += 1 + end + + # put the root entry first + sbuf = self.pack + + # add the rest + prev_sect = nil + dir_start = nil + entries.each { |de| + # we already got the root entry, no more! + next if (de.type == STGTY_ROOT) + + dir = de.pack + dlog("writing dir entry #{de.name}", 'rex', LEV_3) + sbuf << dir + + if (sbuf.length == @stg.header.sector_size) + # we have a full sector, add it! + sect = @stg.write_sector(sbuf, nil, prev_sect) + prev_sect = sect + dir_start ||= sect + # reset.. + sbuf = "" + end + } + + # still a partial sector left? + if (sbuf.length > 0) + # add it! (NOTE: it will get padded with nul bytes if its not sector sized) + sect = @stg.write_sector(sbuf, nil, prev_sect) + prev_sect = sect + dir_start ||= sect + end + + @stg.header._sectDirStart = dir_start + end + +end + +end +end diff --git a/lib/rex/ole/direntry.rb b/lib/rex/ole/direntry.rb new file mode 100644 index 0000000000..6f2e3aa339 --- /dev/null +++ b/lib/rex/ole/direntry.rb @@ -0,0 +1,240 @@ +## +# $Id$ +# Version: $Revision$ +## + +## +# Rex::OLE - an OLE implementation +# written in 2010 by Joshua J. Drake +## + +module Rex +module OLE + +# +# This class serves as the base class for SubStorage, Stream, and Directory head +# +class DirEntry + + attr_accessor :sid + attr_accessor :_sidChild, :_sidLeftSib, :_sidRightSib + + def initialize(stg) + @stg = stg + + # default to a root entry :) + @sid = 0 + @_ab = "Root Entry" + @_cb = nil # NOTE: this is not used until pack + @_mse = STGTY_ROOT + @_bflags = 0 + @_sidLeftSib = SECT_FREE + @_sidRightSib = SECT_FREE + @_sidChild = SECT_FREE + @_clsId = CLSID.new + @_dwUserFlags = 0 + @_ctime = "\x00" * 8 + @_mtime = "\x00" * 8 + @_sectStart = SECT_END + @_ulSize = 0 + + # keep track of logical children (in a tree) + @children = [] + end + + + def length + @_ulSize + end + + def <<(expr) + @children << expr + end + + def each + @children.each { |de| + yield de + } + end + + + def type + @_mse + end + def type=(arg) + @_mse = arg + end + + def name + @_ab + end + def name=(arg) + # XXX: validate? + @_ab = arg + end + + def start_sector + @_sectStart + end + def start_sector=(expr) + @_sectStart = expr + end + + + # NOTE: this will not look at children + def find_stream_by_name_and_type(name, type) + @children.each { |de| + next if (de.type != type) + + if (de.name == name) + return de + end + } + nil + end + + + def find_by_sid(sid, de=self) + if (de.sid == sid) + return de + end + @children.each { |cde| + ret = find_by_sid(cde, sid) + if (ret) + return ret + end + } + nil + end + + + # + # low-level functions + # + def from_s(sid, buf) + @sid = sid + @_ab = Util.getUnicodeString(buf[0x00,64]) + @_cb = Util.get16(buf, 0x40) + + # too big? + if (@_cb > 0x40) + raise RuntimeError, 'Invalid directory entry name length %#x' % @_cb + end + + # mismatch? + if (@_ab.length > 0) + declen = ((@_cb) / 2) - 1 + if (declen != @_ab.length) + raise RuntimeError, 'Directory entry name and length mismatch (%d != %d)' % [declen, @_ab.length] + end + end + + @_mse = Util.get8(buf, 0x42) + @_bflags = Util.get8(buf, 0x43) + @_sidLeftSib = Util.get32(buf, 0x44) + @_sidRightSib = Util.get32(buf, 0x48) + @_sidChild = Util.get32(buf, 0x4c) + + # only used for storages.. + @_clsId = CLSID.new(buf[0x50,16]) + @_dwUserFlags = Util.get32(buf, 0x60) + @_ctime = buf[0x64,8] + @_mtime = buf[0x6c,8] + + # only used for streams... + @_sectStart = Util.get32(buf, 0x74) + if (@stg.header._uMajorVersion == 4) + @_ulSize = Util.get64(buf, 0x78) + else + @_ulSize = Util.get32(buf, 0x78) + end + + # ignore _dptPropType and pad + end + + + def pack + @_sectStart ||= SECT_END + @_cb = (@_ab.length + 1) * 2 + + data = "" + data << Util.putUnicodeString(@_ab) # gets padded/truncated to 0x40 bytes + data << Util.pack16(@_cb) + data << Util.pack8(@_mse) + data << Util.pack8(@_bflags) + data << Util.pack32(@_sidLeftSib) + data << Util.pack32(@_sidRightSib) + data << Util.pack32(@_sidChild) + data << @_clsId.pack + data << Util.pack32(@_dwUserFlags) + data << @_ctime + data << @_mtime + data << Util.pack32(@_sectStart) + data << Util.pack64(@_ulSize) + data + end + + + def to_s(extra_spaces=0) + @_sectStart ||= SECT_END + @_cb = (@_ab.length + 1) * 2 + + spstr = " " * extra_spaces + + ret = "%s{\n" % spstr + ret << "%s sid => 0x%x" % [spstr, @sid] + ret << ",\n" + ret << "%s _ab => \"%s\"" % [spstr, Util.Printable(@_ab)] + ret << ",\n" + ret << "%s _cb => 0x%04x" % [spstr, @_cb] + ret << ",\n" + ret << "%s _mse => 0x%02x" % [spstr, @_mse] + ret << ",\n" + ret << "%s _bflags => 0x%02x" % [spstr, @_bflags] + ret << ",\n" + ret << "%s _sidLeftSib => 0x%08x" % [spstr, @_sidLeftSib] + ret << ",\n" + ret << "%s _sidRightSib => 0x%08x" % [spstr, @_sidRightSib] + ret << ",\n" + ret << "%s _sidChild => 0x%08x" % [spstr, @_sidChild] + ret << ",\n" + ret << "%s _clsId => %s" % [spstr, @_clsId.to_s] + ret << ",\n" + ret << "%s _dwUserFlags => 0x%08x" % [spstr, @_dwUserFlags] + ret << ",\n" + ret << "%s _ctime => %s" % [spstr, Rex::Text.to_hex_dump(@_ctime).strip] + ret << "\n" + ret << "%s _mtime => %s" % [spstr, Rex::Text.to_hex_dump(@_mtime).strip] + ret << "\n" + ret << "%s _sectStart => 0x%08x" % [spstr, @_sectStart] + ret << ",\n" + ret << "%s _ulSize => 0x%016x" % [spstr, @_ulSize] + if (@_mse == STGTY_STREAM) + ret << ",\n" + ret << "%s data =>\n" % spstr + if (@data) + #ret << Util.Printable(@data) + ret << Rex::Text.to_hex_dump(@data).strip + else + if (@_ulSize > 0) + ret << "--NOT OPENED YET--" + end + end + elsif (@_mse == STGTY_STORAGE) or (@_mse == STGTY_ROOT) + if (@children.length > 0) + ret << ",\n" + ret << "%s *children* =>\n" % spstr + @children.each { |de| + ret << de.to_s(extra_spaces+2) + ret << "\n" + } + end + end + ret << "\n" + ret << "%s}" % spstr + end + +end + +end +end diff --git a/lib/rex/ole/fat.rb b/lib/rex/ole/fat.rb new file mode 100644 index 0000000000..827ce17465 --- /dev/null +++ b/lib/rex/ole/fat.rb @@ -0,0 +1,99 @@ +## +# $Id$ +# Version: $Revision$ +## + +## +# Rex::OLE - an OLE implementation +# written in 2010 by Joshua J. Drake +## + +module Rex +module OLE + +class FAT < DIFAT + + # + # low-level functions + # + def read(difat) + @entries = [] + cnt = left = @stg.header._csectFat + difat.each { |fs| + break if (left == 0) + + if (fs != SECT_FREE) + buf = @stg.read_sector(fs, @stg.header.sector_size) + arr = Util.get32array(buf) + + # hax! + if (@entries[fs] == SECT_DIF) + # chop the next ptr + @entries += arr.slice!(0, arr.length - 1) + else + @entries += arr + end + left -= 1 + end + } + + if (left != 0) + raise RuntimeError, 'Only found %u of %u sectors' % [(cnt - left), cnt] + end + end + + def allocate_sector(type=nil) + idx = @entries.index(SECT_FREE) + if (not idx) + # add a sector worth + idx = @entries.length + @stg.header.idx_per_sect.times { + @entries << SECT_FREE + } + end + + # mark the sector as in use + if (type) + @entries[idx] = type + else + # default normal sectors to end of chain + @entries[idx] = SECT_END + end + idx + end + + def write(difat) + # we build the difat as we write these.. + difat.reset + + # allocate the sectors + fat_sects = [] + left = @entries.length + while (left > 0) + if (left > @stg.header.idx_per_sect) + left -= @stg.header.idx_per_sect + else + left = 0 + end + fat_sects << allocate_sector(SECT_FAT) + end + + # write the fat into the difat/allocated sectors + copy = @entries.dup + fat_sects.each { |fs| + part = copy.slice!(0, @stg.header.idx_per_sect) + sbuf = Util.pack32array(part) + + if (sbuf.length != @stg.header.sector_size) + raise RuntimeError, 'Unsupported number of fat sectors (not multiple of idx per sect)' + end + + @stg.write_sector_raw(fs, sbuf) + difat << fs + } + end + +end + +end +end diff --git a/lib/rex/ole/header.rb b/lib/rex/ole/header.rb new file mode 100644 index 0000000000..59bfcbc1ab --- /dev/null +++ b/lib/rex/ole/header.rb @@ -0,0 +1,204 @@ +## +# $Id$ +# Version: $Revision$ +## + +## +# Rex::OLE - an OLE implementation +# written in 2010 by Joshua J. Drake +## + +# +# Should we support major == 4 && sectorshift == 0xc ? +# + +module Rex +module OLE + +require 'rex/ole/util' + +class Header + + attr_accessor :_csectFat, :_sectFat + attr_accessor :_csectMiniFat, :_sectMiniFatStart + attr_accessor :_ulMiniSectorCutoff, :_uMiniSectorShift + attr_accessor :_csectDif, :_sectDifStart + attr_accessor :_sectDirStart + attr_accessor :_uMajorVersion + + attr_accessor :sector_size, :idx_per_sect + attr_accessor :mini_sector_size + + def initialize + set_defaults + + # calculate some numbers (save a little math) + @sector_size = 1 << @_uSectorShift + @mini_sector_size = 1 << @_uMiniSectorShift + @idx_per_sect = @sector_size / 4 + end + + def set_defaults + @_abSig = SIG + @_clid = CLSID.new + @_uByteOrder = LITTLE_ENDIAN + + @_uMinorVersion = 0x3e + @_uMajorVersion = 0x03 + + @_uSectorShift = 9 # 512 byte sectors + @_uMiniSectorShift = 6 # 64 byte mini-sectors + + @_csectDir = nil # TBD (v4 only, 1 required) + + @_csectFat = nil # TBD (one required) + @_sectDirStart = nil # TBD (one required) + + @_signature = 0 # no transactions support + + @_ulMiniSectorCutoff = 0x1000 # 4k + @_sectMiniFatStart = SECT_END # TBD + @_csectMiniFat = 0 # TBD + + @_sectDifStart = SECT_END # TBD (default to none) + @_csectDif = 0 # TBD (default to none) + + @_sectFat = [] # TBD + end + + def to_s + ret = "{\n" + ret << " _abSig => \"%s\"" % Util.Printable(@_abSig) + ret << ",\n" + ret << " _clid => %s" % @_clid.to_s + ret << ",\n" + ret << " _uMinorVersion => 0x%04x" % @_uMinorVersion + ret << ",\n" + ret << " _uMajorVersion => 0x%04x" % @_uMajorVersion + ret << ",\n" + ret << " _uByteOrder => 0x%04x" % @_uByteOrder + ret << ",\n" + ret << " _uSectorShift => 0x%04x" % @_uSectorShift + ret << ",\n" + ret << " _uMiniSectorShift => 0x%04x" % @_uMiniSectorShift + ret << ",\n" + + if (@_csectDir) + ret << " _csectDir => 0x%08x" % @_csectDir + else + ret << " _csectDir => UNALLOCATED" % @_csectDir + end + ret << ",\n" + + if (@_csectFat) + ret << " _csectFat => 0x%08x" % @_csectFat + else + ret << " _csectFat => UNALLOCATED" + end + ret << ",\n" + + if (@_sectDirStart) + ret << " _sectDirStart => 0x%08x" % @_sectDirStart + else + ret << " _sectDirStart => UNALLOCATED" + end + ret << ",\n" + + ret << " _signature => 0x%08x" % @_signature + ret << ",\n" + ret << " _uMiniSectorCutoff => 0x%08x" % @_ulMiniSectorCutoff + ret << ",\n" + ret << " _sectMiniFatStart => 0x%08x" % @_sectMiniFatStart + ret << ",\n" + ret << " _csectMiniFat => 0x%08x" % @_csectMiniFat + ret << ",\n" + ret << " _sectDifStart => 0x%08x" % @_sectDifStart + ret << ",\n" + ret << " _csectDif => 0x%08x" % @_csectDif + #ret << ",\n" + #ret << " _sectFat => " + #ret << Rex::Text.to_hex_dump32array(@_sectFat) + ret << "\n}" + ret + end + + # + # low-level functions + # + def read(fd) + buf = fd.read(HDR_SZ) + + @_abSig = buf[0x00,8] + if (@_abSig != SIG) and (@_abSig != SIG_BETA) + raise RuntimeError, 'Invalid signature for OLE file' + end + @_clid = CLSID.new(buf[0x08,16]) + + @_uByteOrder = Util.get16(buf, 0x1c) + Util.set_endian(@_uByteOrder) + + @_uMinorVersion = Util.get16(buf, 0x18) + @_uMajorVersion = Util.get16(buf, 0x1a) + + @_uSectorShift = Util.get16(buf, 0x1e) + @_uMiniSectorShift = Util.get16(buf, 0x20) + + # ignore reserved bytes + + @_csectDir = Util.get32(buf, 0x28) # NOTE: only for v4 files + + @_csectFat = Util.get32(buf, 0x2c) + @_sectDirStart = Util.get32(buf, 0x30) + + @_signature = Util.get32(buf, 0x34) + + @_ulMiniSectorCutoff = Util.get32(buf, 0x38) + @_sectMiniFatStart = Util.get32(buf, 0x3c) + @_csectMiniFat = Util.get32(buf, 0x40) + + @_sectDifStart = Util.get32(buf, 0x44) + @_csectDif = Util.get32(buf, 0x48) + + @_sectFat = Util.get32array(buf[0x4c, (109 * 4)]) + end + + def write(fd) + hdr = "" + hdr << @_abSig + hdr << @_clid.pack + hdr << Util.pack16(@_uMinorVersion) + hdr << Util.pack16(@_uMajorVersion) + hdr << Util.pack16(@_uByteOrder) + hdr << Util.pack16(@_uSectorShift) + hdr << Util.pack16(@_uMiniSectorShift) + if (@_uMajorVersion == 0x04) + hdr << "\x00" * 6 # reserved bytes + hdr << Util.pack32(@_csectDir) + else + hdr << "\x00" * 10 # reserved bytes + end + + fs_count = @_csectFat + fs_count ||= 0 + hdr << Util.pack32(fs_count) + + dir_start = @_sectDirStart + dir_start ||= SECT_END + hdr << Util.pack32(dir_start) + + hdr << Util.pack32(@_signature) + hdr << Util.pack32(@_ulMiniSectorCutoff) + hdr << Util.pack32(@_sectMiniFatStart) + hdr << Util.pack32(@_csectMiniFat) + hdr << Util.pack32(@_sectDifStart) + hdr << Util.pack32(@_csectDif) + hdr << Util.pack32array(@_sectFat) + + fd.seek(0, ::IO::SEEK_SET) + fd.write(hdr) + end + +end + +end +end diff --git a/lib/rex/ole/minifat.rb b/lib/rex/ole/minifat.rb new file mode 100644 index 0000000000..a5251771ca --- /dev/null +++ b/lib/rex/ole/minifat.rb @@ -0,0 +1,77 @@ +## +# $Id$ +# Version: $Revision$ +## + +## +# Rex::OLE - an OLE implementation +# written in 2010 by Joshua J. Drake +## + +module Rex +module OLE + +class MiniFAT < DIFAT + + # + # low-level functions + # + def read + @entries = [] + + visited = [] + sect = @stg.header._sectMiniFatStart + @stg.header._csectMiniFat.times { |idx| + break if sect == SECT_END + + if (visited.include?(sect)) + raise RuntimeError, 'Sector chain loop detected (0x%08x)' % sect + end + visited << sect + + buf = @stg.read_sector(sect, @stg.header.sector_size) + @stg.header.idx_per_sect.times { |idx| + @entries << Util.get32(buf, (idx*4)) + } + sect = @stg.next_sector(sect) + } + end + + def allocate_sector + idx = @entries.index(SECT_FREE) + + if (not idx) + # add a sector worth + idx = @entries.length + @stg.header.idx_per_sect.times { + @entries << SECT_FREE + } + end + + # default mini-sectors to end of chain + @entries[idx] = SECT_END + idx + end + + def write + return if (@entries.length < 1) + + mf_start = nil + mfs_count = 0 + prev_sect = nil + copy = @entries.dup + while (copy.length > 0) + part = copy.slice!(0, @stg.header.idx_per_sect) + sbuf = Util.pack32array(part) + idx = @stg.write_sector(sbuf, nil, prev_sect) + mfs_count += 1 + mf_start ||= idx + end + @stg.header._sectMiniFatStart = mf_start + @stg.header._csectMiniFat = mfs_count + end + +end + +end +end diff --git a/lib/rex/ole/samples/create_ole.rb b/lib/rex/ole/samples/create_ole.rb new file mode 100755 index 0000000000..f85784020b --- /dev/null +++ b/lib/rex/ole/samples/create_ole.rb @@ -0,0 +1,27 @@ +#!/usr/bin/env ruby + +msfbase = __FILE__ +while File.symlink?(msfbase) + msfbase = File.expand_path(File.readlink(msfbase), File.dirname(msfbase)) +end +path = File.expand_path(File.dirname(msfbase)) +path += "/../../../" +$:.unshift(path) + + +require 'rex/ole' + +if (ARGV.length < 1) + $stderr.puts "usage: make_ole " + exit(1) +end + +document = ARGV.shift + +if (stg = Rex::OLE::Storage.new(document, Rex::OLE::STGM_WRITE)) + if (stm = stg.create_stream("testing")) + stm << "A" * 1024 + stm.close + end + stg.close +end diff --git a/lib/rex/ole/samples/dir.rb b/lib/rex/ole/samples/dir.rb new file mode 100755 index 0000000000..8cd7408a01 --- /dev/null +++ b/lib/rex/ole/samples/dir.rb @@ -0,0 +1,35 @@ +#!/usr/bin/env ruby + +msfbase = __FILE__ +while File.symlink?(msfbase) + msfbase = File.expand_path(File.readlink(msfbase), File.dirname(msfbase)) +end +path = File.expand_path(File.dirname(msfbase)) +path += "/../../../" +$:.unshift(path) + + +require 'rex/ole' + +if (ARGV.length < 1) + $stderr.puts "usage: dir " + exit(1) +end + +document = ARGV.shift + + +# recursive printer :) +def show_entries(ent, spaces=0) + spstr = " " * spaces + + puts "%s + #{ent.name}" % spstr + ent.each { |el| + show_entries(el, spaces+2) + } +end + +if (stg = Rex::OLE::Storage.new(document)) + show_entries(stg) + stg.close +end diff --git a/lib/rex/ole/samples/dump_stream.rb b/lib/rex/ole/samples/dump_stream.rb new file mode 100755 index 0000000000..d4b8b7a755 --- /dev/null +++ b/lib/rex/ole/samples/dump_stream.rb @@ -0,0 +1,34 @@ +#!/usr/bin/env ruby + +msfbase = __FILE__ +while File.symlink?(msfbase) + msfbase = File.expand_path(File.readlink(msfbase), File.dirname(msfbase)) +end +path = File.expand_path(File.dirname(msfbase)) +path += "/../../../" +$:.unshift(path) + +require 'rex/ole' + +if (ARGV.length < 2) + $stderr.puts "usage: dump_stream " + exit(1) +end + +document = ARGV.shift +stream = ARGV.shift + +if (stg = Rex::OLE::Storage.new(document)) + if (stm = stg.open_stream(stream)) + data = stm.read(stm.length) + data ||= "" + $stderr.puts "Successfully opened the \"%s\" stream (%u bytes)" % [stream, data.length] + $stdout.puts data + stm.close + else + $stderr.puts "Unable to open stream: #{stream}" + end + stg.close +else + $stderr.puts "Unable to open storage: #{document}" +end diff --git a/lib/rex/ole/samples/ole_info.rb b/lib/rex/ole/samples/ole_info.rb new file mode 100755 index 0000000000..f221f71390 --- /dev/null +++ b/lib/rex/ole/samples/ole_info.rb @@ -0,0 +1,23 @@ +#!/usr/bin/env ruby + +msfbase = __FILE__ +while File.symlink?(msfbase) + msfbase = File.expand_path(File.readlink(msfbase), File.dirname(msfbase)) +end +path = File.expand_path(File.dirname(msfbase)) +path += "/../../../" +$:.unshift(path) + +require 'rex/ole' + +if (ARGV.length < 1) + $stderr.puts "usage: ole_info " + exit(1) +end + +document = ARGV.shift + +if (stg = Rex::OLE::Storage.new(document)) + puts stg.inspect + stg.close +end diff --git a/lib/rex/ole/storage.rb b/lib/rex/ole/storage.rb new file mode 100644 index 0000000000..57c2cf5874 --- /dev/null +++ b/lib/rex/ole/storage.rb @@ -0,0 +1,395 @@ +## +# $Id$ +# Version: $Revision$ +## + +## +# Rex::OLE - an OLE implementation +# written in 2010 by Joshua J. Drake +## + +module Rex +module OLE + +class Storage + + attr_accessor :header + + def initialize(filename=nil, mode=STGM_READ) + @mode = mode + @modified = nil + + @fd = nil + @filename = nil + @header = Header.new + @difat = DIFAT.new self + @fat = FAT.new self + @minifat = MiniFAT.new self + @directory = Directory.new self + @ministream = Stream.new self + + if (filename) + @filename = filename + open(filename, mode) + return + end + end + + + def each + @directory.each { |el| + yield el + } + end + + + def name + @filename + end + + + def open(filename, mode) + if (mode == STGM_READWRITE) + fmode = 'r+b' + elsif (mode == STGM_WRITE) + fmode = 'w+b' + else + fmode = 'rb' + end + + @fd = File.new(filename, fmode) + + # don't read for new files + if (mode == STGM_WRITE) + # ensure there is a root + write_to_disk + return + end + + # parse the header + @header.read @fd + @difat.read + @fat.read @difat + @minifat.read + @directory.read + # NOTE: we can't use read_stream_data here (must read using regular FAT, regardless of size) + # read data using the root node's start/length + @ministream << read_data(@directory) + end + + def close + if (@modified) and (@mode != STGM_READ) + write_to_disk + end + @fd.close + end + + def inspect + ret = "" + ret << "header = %s\n" % @header.to_s + + ret << "*** %u DIFAT sectors\n" % @difat.length + ret << @difat.to_s << "\n" + + ret << "*** %u FAT sectors\n" % @fat.length + ret << @fat.to_s << "\n" + + ret << "*** %u MiniFAT sectors:\n" % @minifat.length + if (@minifat.length > 0) + ret << @minifat.to_s << "\n" + end + + ret << "*** ministream (%u bytes):\n" % @ministream.length + if (@ministream.length > 0) + ret << @ministream.to_s << "\n" + end + + ret << "*** %u directory entries\n" % @directory.num_entries + ret << @directory.to_s << "\n" + end + + + # + # stream manipulation functions + # + def create_stream(name, mode=STGM_WRITE, parent_stg=nil) + if (stm = open_stream(name, mode, parent_stg)) + stm.close + return nil + end + + # eek, don't check the name for now + # if we do, we cant create alot of streams (summary info for example) +=begin + if (not Util.name_is_valid(name)) + return nil + end +=end + + stm = Stream.new self + stm.name = name + parent_stg ||= @directory + dlog("Adding stream #{name} to storage #{parent_stg.name}", 'rex', LEV_3) + @directory.link_item(parent_stg, stm) + @modified = true + stm + end + + def open_stream(name, mode=STGM_READ, parent_stg=nil) + parent_stg ||= @directory + stm = parent_stg.find_stream_by_name_and_type(name, STGTY_STREAM) + if (stm) + # TODO: optimize out the need to read all of the data up-front + stm << read_stream_data(stm) + end + stm + end + + + # + # storage manipulation functions + # + def create_storage(name, mode=STGM_READ, parent_stg=nil) + stg = SubStorage.new self + stg.name = name + parent_stg ||= @directory + dlog("Adding storage #{name} to storage #{parent_stg.name}", 'rex', LEV_3) + @directory.link_item(parent_stg, stg) + stg + end + + def open_storage(name, mode=STGM_READ, parent_stg=nil) + @directory.find_stream_by_name_and_type(name, STGTY_STORAGE) + end + + + # + # low-level functions + # + def write_to_disk + # reset FAT/DIFAT + @difat = DIFAT.new self + @fat = FAT.new self + + @header.write @fd + write_user_data + + # NOTE: we call write_stream here since we MUST write this to + # the regular stream (regardless of size) + ms_start = write_stream(@ministream) + @directory.set_ministream_params(ms_start, @ministream.length) + + @minifat.write + @directory.write + @fat.write(@difat) + @difat.write + + # write it again, now that its complete + @header.write @fd + @fd.flush + end + + def write_sector(sbuf, type=nil, prev_sect=nil) + len = sbuf.length + if (len != @header.sector_size) + # pad it if less + if (len < @header.sector_size) + sbuf = sbuf.dup + sbuf << "\x00" * (@header.sector_size - len) + else + raise RuntimeError, 'not sector sized!' + end + end + + # write the data + idx = @fat.allocate_sector(type) + # point previous sector to here + if (prev_sect) + @fat[prev_sect] = idx + end + write_sector_raw(idx, sbuf) + return idx + end + + def write_sector_raw(sect, sbuf) + dlog("Writing sector 0x%02x" % sect, 'rex', LEV_3) + @fd.seek((sect + 1) * @header.sector_size, ::IO::SEEK_SET) + @fd.write(sbuf) + end + + + def write_mini_sector(sbuf, prev_sect=nil) + len = sbuf.length + if (len != @header.mini_sector_size) + if (len < @header.mini_sector_size) + sbuf = sbuf.dup + sbuf << "\x00" * (@header.mini_sector_size - len) + else + raise RuntimeError, 'not mini sector sized!' + end + end + + idx = @minifat.allocate_sector + # point the previous mini sector to here + if (prev_sect) + @minifat[prev_sect] = idx + end + write_mini_sector_raw(idx, sbuf) + idx + end + + def write_mini_sector_raw(sect, sbuf) + dlog("Writing mini sector 0x%02x" % sect, 'rex', LEV_3) + @ministream << sbuf + end + + + + def write_user_data + @directory.each_entry { |stm| + # only regular streams this pass + next if (stm.type != STGTY_STREAM) + + if (stm.length >= @header._ulMiniSectorCutoff) + stm.start_sector = write_stream(stm) + else + # NOTE: stm_start is a minifat value + stm.start_sector = write_mini_stream(stm) + end + } + end + + def write_stream(stm) + dlog("Writing \"%s\" to regular stream" % stm.name, 'rex', LEV_3) + stm_start = nil + prev_sect = nil + stm.seek(0) + while (sbuf = stm.read(@header.sector_size)) + sect = write_sector(sbuf, nil, prev_sect) + stm_start ||= sect + prev_sect = sect + end + stm_start + end + + def write_mini_stream(stm) + dlog("Writing \"%s\" to mini stream" % stm.name, 'rex', LEV_3) + prev_sect = nil + stm.seek(0) + while (sbuf = stm.read(@header.mini_sector_size)) + sect = write_mini_sector(sbuf, prev_sect) + stm_start ||= sect + prev_sect = sect + end + stm_start + end + + + def read_stream_data(direntry) + if (direntry.length < @header._ulMiniSectorCutoff) + return read_data_mini(direntry) + end + + read_data(direntry) + end + + def read_data(direntry) + ret = "" + visited = [] + left = direntry.length + sect = direntry.start_sector + while (sect != SECT_END) + if (visited.include?(sect)) + raise RuntimeError, 'Sector chain loop detected (0x%08x)' % sect + end + visited << sect + + # how much to read? + block = @header.sector_size + block = left if (block > left) + + # read it. + dlog("read_data - reading 0x%x bytes" % block, 'rex', LEV_3) + buf = read_sector(sect, block) + ret << buf + left -= buf.length + + # done? + break if (left == 0) + + sect = next_sector(sect) + end + ret + end + + def read_data_mini(direntry) + ret = "" + visited = [] + left = direntry.length + sect = direntry.start_sector + while (sect != SECT_END) + if (visited.include?(sect)) + raise RuntimeError, 'Sector chain loop detected (0x%08x mini)' % sect + end + visited << sect + + # how much to read? + block = @header.mini_sector_size + block = left if (block > left) + + # read it. + dlog("read_data_mini - reading 0x%x bytes" % block, 'rex', LEV_3) + buf = read_mini_sector(sect, block) + ret << buf + left -= buf.length + + # done? + break if (left == 0) + + sect = next_mini_sector(sect) + end + ret + end + + + def read_sector(sect, len) + off = ((sect + 1) * @header.sector_size) + @fd.seek(off, ::IO::SEEK_SET) + buf = @fd.read(len) + if (not buf) + if (@fd.eof?) + raise RuntimeError, 'EOF while reading sector data (0x%08x)' % sect + else + raise RuntimeError, 'Unknown error while reading sector data (0x%08x)' % sect + end + end + if (buf.length != len) + raise RuntimeError, 'Insufficient data for sector (0x%08x): got %u of %u' % [sect, buf.length, len] + end + buf + end + + def next_sector(sect) + return SECT_END if (sect >= @fat.length) + @fat[sect] + end + + + def read_mini_sector(sect, len) + dlog("Reading mini sector 0x%x" % sect, 'rex', LEV_3) + off = (@header.mini_sector_size * sect) + dlog("Reading from offset 0x%x of ministream" % off, 'rex', LEV_3) + @ministream.seek(off) + data = @ministream.read(len) + data + end + + def next_mini_sector(sect) + return SECT_END if (sect >= @minifat.length) + @minifat[sect] + end + +end + +end +end diff --git a/lib/rex/ole/stream.rb b/lib/rex/ole/stream.rb new file mode 100644 index 0000000000..34d887d83f --- /dev/null +++ b/lib/rex/ole/stream.rb @@ -0,0 +1,53 @@ +## +# $Id$ +# Version: $Revision$ +## + +## +# Rex::OLE - an OLE implementation +# written in 2010 by Joshua J. Drake +## + +module Rex +module OLE + +class Stream < DirEntry + + def initialize(stg) + super + + # for reading/writing from this + @offset = 0 + @_mse = STGTY_STREAM + end + + def close + @mode = nil + @offset = nil + end + + def seek(offset) + @offset = offset + end + + def read(len) + return nil if (not @data) + + ret = @data[@offset, len] + @offset += len + ret + end + + def <<(expr) + if (not @data) + @data = expr.dup + else + @data << expr + end + @_ulSize = @data.length + end + +end + +end +end diff --git a/lib/rex/ole/substorage.rb b/lib/rex/ole/substorage.rb new file mode 100644 index 0000000000..de3c3bc62b --- /dev/null +++ b/lib/rex/ole/substorage.rb @@ -0,0 +1,49 @@ +## +# $Id$ +# Version: $Revision$ +## + +## +# Rex::OLE - an OLE implementation +# written in 2010 by Joshua J. Drake +## + +module Rex +module OLE + +class SubStorage < DirEntry + + def initialize(stg) + super + + @_mse = STGTY_STORAGE + end + + + def close + end + + + # stream handling stuff + def create_stream(name, mode=STGM_WRITE) + @stg.create_stream(name, mode, self) + end + + def open_stream(name, mode=STGM_READ) + @stg.open_stream(name, mode, self) + end + + + # storage handling stuff + def create_storage(name, mode=STGM_WRITE) + @stg.create_storage(name, mode, self) + end + + def open_storage(name, mode=STGM_WRITE) + @stg.open_storage(name, mode, self) + end + +end + +end +end diff --git a/lib/rex/ole/util.rb b/lib/rex/ole/util.rb new file mode 100644 index 0000000000..890b88cb33 --- /dev/null +++ b/lib/rex/ole/util.rb @@ -0,0 +1,157 @@ +## +# $Id$ +# Version: $Revision$ +## + +## +# Rex::OLE - an OLE implementation +# written in 2010 by Joshua J. Drake +## + +module Rex +module OLE + +class Util + + def self.Hexify32array(arr) + ret = "" + arr.each { |dw| + ret << " " if ret.length > 0 + ret << "0x%08x" % dw + } + ret + end + + def self.Printable(buf) + ret = "" + buf.unpack('C*').each { |byte| + ch = byte.chr + if (byte < 0x20 || byte > 0x7e) + ret << "\\x" + ch.unpack('H*')[0] + else + ret << ch + end + } + ret + end + + + def self.set_endian(endian) + @endian = endian + end + + def self.get64(buf, offset) + @endian = LITTLE_ENDIAN if not @endian + if (@endian == LITTLE_ENDIAN) + arr = buf[offset,8].unpack('VV') + return (arr[0] + (arr[1] << 32)) + else + arr = buf[offset,8].unpack('NN') + return ((arr[0] << 32) + arr[1]) + end + end + + def self.pack64(value) + @endian = LITTLE_ENDIAN if not @endian + arr = [] + arr << (value & 0xffffffff) + arr << (value >> 32) + if (@endian == LITTLE_ENDIAN) + arr.pack('VV') + else + arr.reverse.pack('NN') + end + end + + def self.get32(buf, offset) + @endian = LITTLE_ENDIAN if not @endian + if (@endian == LITTLE_ENDIAN) + buf[offset,4].unpack('V')[0] + else + buf[offset,4].unpack('N')[0] + end + end + + def self.pack32(value) + @endian = LITTLE_ENDIAN if not @endian + if (@endian == LITTLE_ENDIAN) + [value].pack('V') + else + [value].pack('N') + end + end + + def self.get32array(buf) + @endian = LITTLE_ENDIAN if not @endian + if (@endian == LITTLE_ENDIAN) + buf.unpack('V*') + else + buf.unpack('N*') + end + end + + def self.pack32array(arr) + @endian = LITTLE_ENDIAN if not @endian + if (@endian == LITTLE_ENDIAN) + arr.pack('V*') + else + arr.pack('N*') + end + end + + def self.get16(buf, offset) + @endian = LITTLE_ENDIAN if not @endian + if (@endian == LITTLE_ENDIAN) + buf[offset,2].unpack('v')[0] + else + buf[offset,2].unpack('n')[0] + end + end + + def self.pack16(value) + @endian = LITTLE_ENDIAN if not @endian + if (@endian == LITTLE_ENDIAN) + [value].pack('v') + else + [value].pack('n') + end + end + + def self.get8(buf, offset) + buf[offset,1].unpack('C')[0] + end + + def self.pack8(value) + [value].pack('C') + end + + + def self.getUnicodeString(buf) + buf = buf.unpack('S*').pack('C*') + if (idx = buf.index(0x00.chr)) + buf.slice!(idx, buf.length) + end + buf + end + + def self.putUnicodeString(buf) + buf = buf.unpack('C*').pack('S*') + if (buf.length < 0x40) + buf << "\x00" * (0x40 - buf.length) + end + buf + end + + + def self.name_is_valid(name) + return nil if (name.length > 31) + (0..0x1f).to_a.each { |x| + return nil if (name.include?(x.chr)) + } + return true + end + +end + +end +end