From 4852228f3fbc5dd1ffad83e3f6cf625c04c25b89 Mon Sep 17 00:00:00 2001 From: Tim Allison Date: Thu, 14 Sep 2017 02:22:55 +0000 Subject: [PATCH] 60279 -- back off to brute-force search for macro content if offset information is not correct git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1808301 13f79535-47bb-0310-9956-ffa450edef68 --- .../poi/poifs/macros/VBAMacroReader.java | 474 ++++++++++++++---- .../poi/util/RLEDecompressingInputStream.java | 2 +- .../poi/poifs/macros/TestVBAMacroReader.java | 20 + test-data/document/60279.doc | Bin 0 -> 39424 bytes 4 files changed, 386 insertions(+), 110 deletions(-) create mode 100644 test-data/document/60279.doc diff --git a/src/java/org/apache/poi/poifs/macros/VBAMacroReader.java b/src/java/org/apache/poi/poifs/macros/VBAMacroReader.java index bad012e29..921f749df 100644 --- a/src/java/org/apache/poi/poifs/macros/VBAMacroReader.java +++ b/src/java/org/apache/poi/poifs/macros/VBAMacroReader.java @@ -43,7 +43,9 @@ import org.apache.poi.poifs.filesystem.OfficeXmlFileException; import org.apache.poi.util.CodePageUtil; import org.apache.poi.util.HexDump; import org.apache.poi.util.IOUtils; +import org.apache.poi.util.LittleEndian; import org.apache.poi.util.RLEDecompressingInputStream; +import org.apache.poi.util.StringUtil; /** *

Finds all VBA Macros in an office file (OLE2/POIFS and OOXML/OPC), @@ -61,9 +63,7 @@ import org.apache.poi.util.RLEDecompressingInputStream; public class VBAMacroReader implements Closeable { protected static final String VBA_PROJECT_OOXML = "vbaProject.bin"; protected static final String VBA_PROJECT_POIFS = "VBA"; - // FIXME: When minimum supported version is Java 7, replace with java.nio.charset.StandardCharsets.UTF_16LE - private static final Charset UTF_16LE = Charset.forName("UTF-16LE"); - + private NPOIFSFileSystem fs; public VBAMacroReader(InputStream rstream) throws IOException { @@ -145,7 +145,7 @@ public class VBAMacroReader implements Closeable { } } protected static class ModuleMap extends HashMap { - Charset charset = Charset.forName("Cp1252"); // default charset + Charset charset = StringUtil.WIN_1252; // default charset } /** @@ -172,20 +172,7 @@ public class VBAMacroReader implements Closeable { } } - /** - * Read length bytes of MBCS (multi-byte character set) characters from the stream - * - * @param stream the inputstream to read from - * @param length number of bytes to read from stream - * @param charset the character set encoding of the bytes in the stream - * @return a java String in the supplied character set - * @throws IOException If reading from the stream fails - */ - private static String readString(InputStream stream, int length, Charset charset) throws IOException { - byte[] buffer = new byte[length]; - int count = stream.read(buffer); - return new String(buffer, 0, count, charset); - } + /** * reads module from DIR node in input stream and adds it to the modules map for decompression later @@ -199,7 +186,7 @@ public class VBAMacroReader implements Closeable { * @param modules a map to store the modules * @throws IOException If reading data from the stream or from modules fails */ - private static void readModule(RLEDecompressingInputStream in, String streamName, ModuleMap modules) throws IOException { + private static void readModuleMetadataFromDirStream(RLEDecompressingInputStream in, String streamName, ModuleMap modules) throws IOException { int moduleOffset = in.readInt(); Module module = modules.get(streamName); if (module == null) { @@ -218,27 +205,57 @@ public class VBAMacroReader implements Closeable { } } - private static void readModule(DocumentInputStream dis, String name, ModuleMap modules) throws IOException { + private static void readModuleFromDocumentStream(DocumentNode documentNode, String name, ModuleMap modules) throws IOException { Module module = modules.get(name); // TODO Refactor this to fetch dir then do the rest if (module == null) { // no DIR stream with offsets yet, so store the compressed bytes for later module = new Module(); modules.put(name, module); - module.read(dis); + InputStream dis = new DocumentInputStream(documentNode); + try { + module.read(dis); + } finally { + dis.close(); + } } else if (module.buf == null) { //if we haven't already read the bytes for the module keyed off this name... + if (module.offset == null) { //This should not happen. bug 59858 throw new IOException("Module offset for '" + name + "' was never read."); } - // we know the offset already, so decompress immediately on-the-fly - long skippedBytes = dis.skip(module.offset); - if (skippedBytes != module.offset) { - throw new IOException("tried to skip " + module.offset + " bytes, but actually skipped " + skippedBytes + " bytes"); + + //try the general case, where module.offset is accurate + InputStream decompressed = null; + InputStream compressed = new DocumentInputStream(documentNode); + try { + // we know the offset already, so decompress immediately on-the-fly + long skippedBytes = compressed.skip(module.offset); + if (skippedBytes != module.offset) { + throw new IOException("tried to skip " + module.offset + " bytes, but actually skipped " + skippedBytes + " bytes"); + } + decompressed = new RLEDecompressingInputStream(compressed); + module.read(decompressed); + return; + } catch (IllegalArgumentException e) { + } catch (IllegalStateException e) { + } finally { + IOUtils.closeQuietly(compressed); + IOUtils.closeQuietly(decompressed); + } + + //bad module.offset, try brute force + compressed = new DocumentInputStream(documentNode); + byte[] decompressedBytes = null; + try { + decompressedBytes = findCompressedStreamWBruteForce(compressed); + } finally { + IOUtils.closeQuietly(compressed); + } + + if (decompressedBytes != null) { + module.read(new ByteArrayInputStream(decompressedBytes)); } - InputStream stream = new RLEDecompressingInputStream(dis); - module.read(stream); - stream.close(); } } @@ -249,7 +266,7 @@ public class VBAMacroReader implements Closeable { * @throws IOException If skipping would exceed the available data or skipping did not work. */ private static void trySkip(InputStream in, long n) throws IOException { - long skippedBytes = in.skip(n); + long skippedBytes = IOUtils.skipFully(in, n); if (skippedBytes != n) { if (skippedBytes < 0) { throw new IOException( @@ -258,33 +275,18 @@ public class VBAMacroReader implements Closeable { } else { throw new IOException( "Tried skipping " + n + " bytes, but only " + skippedBytes + " bytes were skipped. " - + "This should never happen."); + + "This should never happen with a non-corrupt file."); } } } // Constants from MS-OVBA: https://msdn.microsoft.com/en-us/library/office/cc313094(v=office.12).aspx - private static final int EOF = -1; - private static final int VERSION_INDEPENDENT_TERMINATOR = 0x0010; - @SuppressWarnings("unused") - private static final int VERSION_DEPENDENT_TERMINATOR = 0x002B; - private static final int PROJECTVERSION = 0x0009; - private static final int PROJECTCODEPAGE = 0x0003; - private static final int STREAMNAME = 0x001A; - private static final int MODULEOFFSET = 0x0031; - @SuppressWarnings("unused") - private static final int MODULETYPE_PROCEDURAL = 0x0021; - @SuppressWarnings("unused") - private static final int MODULETYPE_DOCUMENT_CLASS_OR_DESIGNER = 0x0022; - @SuppressWarnings("unused") - private static final int PROJECTLCID = 0x0002; - @SuppressWarnings("unused") - private static final int MODULE_NAME = 0x0019; - @SuppressWarnings("unused") - private static final int MODULE_NAME_UNICODE = 0x0047; - @SuppressWarnings("unused") - private static final int MODULE_DOC_STRING = 0x001c; private static final int STREAMNAME_RESERVED = 0x0032; + private static final int PROJECT_CONSTANTS_RESERVED = 0x003C; + private static final int HELP_FILE_PATH_RESERVED = 0x003D; + private static final int REFERENCE_NAME_RESERVED = 0x003E; + private static final int DOC_STRING_RESERVED = 0x0040; + private static final int MODULE_DOCSTRING_RESERVED = 0x0048; /** * Reads VBA Project modules from a VBA Project directory located at @@ -293,76 +295,330 @@ public class VBAMacroReader implements Closeable { * @since 3.15-beta2 */ protected void readMacros(DirectoryNode macroDir, ModuleMap modules) throws IOException { + //bug59858 shows that dirstream may not be in this directory (\MBD00082648\_VBA_PROJECT_CUR\VBA ENTRY NAME) + //but may be in another directory (\_VBA_PROJECT_CUR\VBA ENTRY NAME) + //process the dirstream first -- "dir" is case insensitive + for (String entryName : macroDir.getEntryNames()) { + if ("dir".equalsIgnoreCase(entryName)) { + processDirStream(macroDir.getEntry(entryName), modules); + break; + } + } + for (Entry entry : macroDir) { if (! (entry instanceof DocumentNode)) { continue; } String name = entry.getName(); DocumentNode document = (DocumentNode)entry; - DocumentInputStream dis = new DocumentInputStream(document); - try { - if ("dir".equalsIgnoreCase(name)) { - // process DIR - RLEDecompressingInputStream in = new RLEDecompressingInputStream(dis); - String streamName = null; - int recordId = 0; - try { - while (true) { - recordId = in.readShort(); - if (EOF == recordId - || VERSION_INDEPENDENT_TERMINATOR == recordId) { - break; - } - int recordLength = in.readInt(); - switch (recordId) { - case PROJECTVERSION: - trySkip(in, 6); - break; - case PROJECTCODEPAGE: - int codepage = in.readShort(); - modules.charset = Charset.forName(CodePageUtil.codepageToEncoding(codepage, true)); - break; - case STREAMNAME: - streamName = readString(in, recordLength, modules.charset); - int reserved = in.readShort(); - if (reserved != STREAMNAME_RESERVED) { - throw new IOException("Expected x0032 after stream name before Unicode stream name, but found: "+ - Integer.toHexString(reserved)); - } - int unicodeNameRecordLength = in.readInt(); - readUnicodeString(in, unicodeNameRecordLength); - // do something with this at some point - break; - case MODULEOFFSET: - readModule(in, streamName, modules); - break; - default: - trySkip(in, recordLength); - break; - } - } - } catch (final IOException e) { - throw new IOException( - "Error occurred while reading macros at section id " - + recordId + " (" + HexDump.shortToHex(recordId) + ")", e); - } - finally { - in.close(); - } - } else if (!startsWithIgnoreCase(name, "__SRP") + + if (! "dir".equalsIgnoreCase(name) && !startsWithIgnoreCase(name, "__SRP") && !startsWithIgnoreCase(name, "_VBA_PROJECT")) { // process module, skip __SRP and _VBA_PROJECT since these do not contain macros - readModule(dis, name, modules); - } - } - finally { - dis.close(); + readModuleFromDocumentStream(document, name, modules); } } } + private enum RecordType { + // Constants from MS-OVBA: https://msdn.microsoft.com/en-us/library/office/cc313094(v=office.12).aspx + MODULE_OFFSET(0x0031), + PROJECT_SYS_KIND(0x01), + PROJECT_LCID(0x0002), + PROJECT_LCID_INVOKE(0x14), + PROJECT_CODEPAGE(0x0003), + PROJECT_NAME(0x04), + PROJECT_DOC_STRING(0x05), + PROJECT_HELP_FILE_PATH(0x06), + PROJECT_HELP_CONTEXT(0x07, 8), + PROJECT_LIB_FLAGS(0x08), + PROJECT_VERSION(0x09, 10), + PROJECT_CONSTANTS(0x0C), + PROJECT_MODULES(0x0F), + DIR_STREAM_TERMINATOR(0x10), + PROJECT_COOKIE(0x13), + MODULE_NAME(0x19), + MODULE_NAME_UNICODE(0x47), + MODULE_STREAM_NAME(0x1A), + MODULE_DOC_STRING(0x1C), + MODULE_HELP_CONTEXT(0x1E), + MODULE_COOKIE(0x2c), + MODULE_TYPE_PROCEDURAL(0x21, 4), + MODULE_TYPE_OTHER(0x22, 4), + MODULE_PRIVATE(0x28, 4), + REFERENCE_NAME(0x16), + REFERENCE_REGISTERED(0x0D), + REFERENCE_PROJECT(0x0E), + REFERENCE_CONTROL_A(0x2F), + + //according to the spec, REFERENCE_CONTROL_B(0x33) should have the + //same structure as REFERENCE_CONTROL_A(0x2F). + //However, it seems to have the int(length) record structure that most others do. + //See 59830.xls for this record. + REFERENCE_CONTROL_B(0x33), + //REFERENCE_ORIGINAL(0x33), + + + MODULE_TERMINATOR(0x002B), + EOF(-1), + UNKNOWN(-2); + + + private final int VARIABLE_LENGTH = -1; + private final int id; + private final int constantLength; + + RecordType(int id) { + this.id = id; + this.constantLength = VARIABLE_LENGTH; + } + + RecordType(int id, int constantLength) { + this.id = id; + this.constantLength = constantLength; + } + + int getConstantLength() { + return constantLength; + } + + static RecordType lookup(int id) { + for (RecordType type : RecordType.values()) { + if (type.id == id) { + return type; + } + } + return UNKNOWN; + } + } + + + private enum DIR_STATE { + INFORMATION_RECORD, + REFERENCES_RECORD, + MODULES_RECORD + } + + private static class ASCIIUnicodeStringPair { + private final String ascii; + private final String unicode; + + ASCIIUnicodeStringPair(String ascii, String unicode) { + this.ascii = ascii; + this.unicode = unicode; + } + + private String getAscii() { + return ascii; + } + + private String getUnicode() { + return unicode; + } + } + + private void processDirStream(Entry dir, ModuleMap modules) throws IOException { + DocumentNode dirDocumentNode = (DocumentNode)dir; + DocumentInputStream dis = new DocumentInputStream(dirDocumentNode); + DIR_STATE dirState = DIR_STATE.INFORMATION_RECORD; + try { + RLEDecompressingInputStream in = new RLEDecompressingInputStream(dis); + String streamName = null; + int recordId = 0; + boolean inReferenceTwiddled = false; + try { + while (true) { + recordId = in.readShort(); + if (recordId == -1) { + break; + } + RecordType type = RecordType.lookup(recordId); + + if (type.equals(RecordType.EOF) || type.equals(RecordType.DIR_STREAM_TERMINATOR)) { + break; + } + switch (type) { + case PROJECT_VERSION: + trySkip(in, RecordType.PROJECT_VERSION.getConstantLength()); + break; + case PROJECT_CODEPAGE: + in.readInt();//record size must == 4 + int codepage = in.readShort(); + modules.charset = Charset.forName(CodePageUtil.codepageToEncoding(codepage, true)); + break; + case MODULE_STREAM_NAME: + ASCIIUnicodeStringPair pair = readStringPair(in, modules.charset, STREAMNAME_RESERVED); + streamName = pair.getAscii(); + break; + case PROJECT_DOC_STRING: + readStringPair(in, modules.charset, DOC_STRING_RESERVED); + break; + case PROJECT_HELP_FILE_PATH: + readStringPair(in, modules.charset, HELP_FILE_PATH_RESERVED); + break; + case PROJECT_CONSTANTS: + readStringPair(in, modules.charset, PROJECT_CONSTANTS_RESERVED); + break; + case REFERENCE_NAME: + if (dirState.equals(DIR_STATE.INFORMATION_RECORD)) { + dirState = DIR_STATE.REFERENCES_RECORD; + } + readStringPair(in, modules.charset, REFERENCE_NAME_RESERVED); + break; + case MODULE_DOC_STRING : + int modDocStringLength = in.readInt(); + readString(in, modDocStringLength, modules.charset); + int modDocStringReserved = in.readShort(); + if (modDocStringReserved != MODULE_DOCSTRING_RESERVED) { + throw new IOException("Expected x003C after stream name before Unicode stream name, but found: " + + Integer.toHexString(modDocStringReserved)); + } + int unicodeModDocStringLength = in.readInt(); + readUnicodeString(in, unicodeModDocStringLength); + // do something with this at some point + break; + case MODULE_OFFSET: + int modOffsetSz = in.readInt(); + //should be 4 + readModuleMetadataFromDirStream(in, streamName, modules); + break; + case PROJECT_MODULES: + dirState = DIR_STATE.MODULES_RECORD; + in.readInt();//size must == 2 + in.readShort();//number of modules + break; + case REFERENCE_CONTROL_A: + int szTwiddled = in.readInt(); + trySkip(in, szTwiddled); + int nextRecord = in.readShort(); + //reference name is optional! + if (nextRecord == RecordType.REFERENCE_NAME.id) { + readStringPair(in, modules.charset, REFERENCE_NAME_RESERVED); + nextRecord = in.readShort(); + } + if (nextRecord != 0x30) { + throw new IOException("Expected 0x30 as Reserved3 in a ReferenceControl record"); + } + int szExtended = in.readInt(); + trySkip(in, szExtended); + break; + case MODULE_TERMINATOR: + int endOfModulesReserved = in.readInt(); + //must be 0; + break; + default: + if (type.getConstantLength() > -1) { + trySkip(in, type.getConstantLength()); + } else { + int recordLength = in.readInt(); + trySkip(in, recordLength); + } + break; + } + } + } catch (final IOException e) { + throw new IOException( + "Error occurred while reading macros at section id " + + recordId + " (" + HexDump.shortToHex(recordId) + ")", e); + } finally { + in.close(); + } + } finally { + dis.close(); + } + } + + private ASCIIUnicodeStringPair readStringPair(RLEDecompressingInputStream in, Charset charset, int reservedByte) throws IOException { + int nameLength = in.readInt(); + String ascii = readString(in, nameLength, charset); + int reserved = in.readShort(); + if (reserved != reservedByte) { + throw new IOException("Expected "+Integer.toHexString(reservedByte)+ "after name before Unicode name, but found: " + + Integer.toHexString(reserved)); + } + int unicodeNameRecordLength = in.readInt(); + String unicode = readUnicodeString(in, unicodeNameRecordLength); + return new ASCIIUnicodeStringPair(ascii, unicode); + } + + + /** + * Read length bytes of MBCS (multi-byte character set) characters from the stream + * + * @param stream the inputstream to read from + * @param length number of bytes to read from stream + * @param charset the character set encoding of the bytes in the stream + * @return a java String in the supplied character set + * @throws IOException If reading from the stream fails + */ + private static String readString(InputStream stream, int length, Charset charset) throws IOException { + byte[] buffer = IOUtils.safelyAllocate(length, 20000); + int bytesRead = IOUtils.readFully(stream, buffer); + if (bytesRead != length) { + throw new IOException("Tried to read: "+length + + ", but could only read: "+bytesRead); + } + return new String(buffer, 0, length, charset); + } + private String readUnicodeString(RLEDecompressingInputStream in, int unicodeNameRecordLength) throws IOException { - byte[] buffer = new byte[unicodeNameRecordLength]; - IOUtils.readFully(in, buffer); - return new String(buffer, UTF_16LE); + byte[] buffer = IOUtils.safelyAllocate(unicodeNameRecordLength, 20000); + int bytesRead = IOUtils.readFully(in, buffer); + if (bytesRead != unicodeNameRecordLength) { + + } + return new String(buffer, StringUtil.UTF16LE); + } + + /** + * Sometimes the offset record in the dirstream is incorrect, but the macro can still be found. + * This will try to find the the first RLEDecompressing stream that starts with "Attribute". + * This relies on some, er, heuristics, admittedly. + * + * @param is full module inputstream to read + * @return uncompressed bytes if found, null otherwise + * @throws IOException for a true IOException copying the is to a byte array + */ + private static byte[] findCompressedStreamWBruteForce(InputStream is) throws IOException { + //buffer to memory for multiple tries + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + IOUtils.copy(is, bos); + byte[] compressed = bos.toByteArray(); + byte[] decompressed = null; + for (int i = 0; i < compressed.length; i++) { + if (compressed[i] == 0x01 && i < compressed.length-1) { + int w = LittleEndian.getUShort(compressed, i+1); + if (w <= 0 || (w & 0x7000) != 0x3000) { + continue; + } + decompressed = tryToDecompress(new ByteArrayInputStream(compressed, i, compressed.length - i)); + if (decompressed != null) { + if (decompressed.length > 9) { + //this is a complete hack. The challenge is that there + //can be many 0 length or junk streams that are uncompressed + //look in the first 20 characters for "Attribute" + int firstX = Math.min(20, decompressed.length); + String start = new String(decompressed, 0, firstX, StringUtil.WIN_1252); + if (start.contains("Attribute")) { + return decompressed; + } + } + } + } + } + return decompressed; + } + + private static byte[] tryToDecompress(InputStream is) { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + try { + IOUtils.copy(new RLEDecompressingInputStream(is), bos); + } catch (IllegalArgumentException e){ + return null; + } catch (IllegalStateException e) { + return null; + } catch (IOException e) { + return null; + } + return bos.toByteArray(); } } diff --git a/src/java/org/apache/poi/util/RLEDecompressingInputStream.java b/src/java/org/apache/poi/util/RLEDecompressingInputStream.java index 471fdab2c..7cc68483c 100644 --- a/src/java/org/apache/poi/util/RLEDecompressingInputStream.java +++ b/src/java/org/apache/poi/util/RLEDecompressingInputStream.java @@ -156,7 +156,7 @@ public class RLEDecompressingInputStream extends InputStream { private int readChunk() throws IOException { pos = 0; int w = readShort(in); - if (w == -1) { + if (w == -1 || w == 0) { return -1; } int chunkSize = (w & 0x0FFF) + 1; // plus 3 bytes minus 2 for the length diff --git a/src/testcases/org/apache/poi/poifs/macros/TestVBAMacroReader.java b/src/testcases/org/apache/poi/poifs/macros/TestVBAMacroReader.java index eeb43b4d2..12e2f16f0 100644 --- a/src/testcases/org/apache/poi/poifs/macros/TestVBAMacroReader.java +++ b/src/testcases/org/apache/poi/poifs/macros/TestVBAMacroReader.java @@ -33,6 +33,7 @@ import java.util.HashMap; import java.util.Map; import static org.apache.poi.POITestCase.assertContains; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; @@ -251,6 +252,7 @@ public class TestVBAMacroReader { File f = POIDataSamples.getSpreadSheetInstance().getFile("59830.xls"); VBAMacroReader r = new VBAMacroReader(f); Map macros = r.readMacros(); + assertEquals(29, macros.size()); assertNotNull(macros.get("Module20")); assertContains(macros.get("Module20"), "here start of superscripting"); r.close(); @@ -261,6 +263,7 @@ public class TestVBAMacroReader { File f = POIDataSamples.getSpreadSheetInstance().getFile("59858.xls"); VBAMacroReader r = new VBAMacroReader(f); Map macros = r.readMacros(); + assertEquals(11, macros.size()); assertNotNull(macros.get("Sheet4")); assertContains(macros.get("Sheet4"), "intentional constituent"); r.close(); @@ -271,6 +274,7 @@ public class TestVBAMacroReader { File f = POIDataSamples.getDocumentInstance().getFile("60158.docm"); VBAMacroReader r = new VBAMacroReader(f); Map macros = r.readMacros(); + assertEquals(2, macros.size()); assertNotNull(macros.get("NewMacros")); assertContains(macros.get("NewMacros"), "' dirty"); r.close(); @@ -282,8 +286,24 @@ public class TestVBAMacroReader { File f = POIDataSamples.getSpreadSheetInstance().getFile("60273.xls"); VBAMacroReader r = new VBAMacroReader(f); Map macros = r.readMacros(); + assertEquals(2, macros.size()); assertNotNull(macros.get("Module1")); assertContains(macros.get("Module1"), "9/8/2004"); r.close(); } + + + @Test + public void bug60279() throws IOException { + File f = POIDataSamples.getDocumentInstance().getFile("60279.doc"); + VBAMacroReader r = new VBAMacroReader(f); + Map macros = r.readMacros(); + assertEquals(1, macros.size()); + String content = macros.get("ThisDocument"); + assertContains(content, "Attribute VB_Base = \"1Normal.ThisDocument\""); + assertContains(content, "Attribute VB_Customizable = True"); + r.close(); + } + + } diff --git a/test-data/document/60279.doc b/test-data/document/60279.doc new file mode 100644 index 0000000000000000000000000000000000000000..bf864338011ceb658e4ce13da7631fad05cc80c8 GIT binary patch literal 39424 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 BW7Plv literal 0 HcmV?d00001