Skip to content

Instantly share code, notes, and snippets.

@TechByTom
Forked from williamknows/Invoke-HideVBAModule.psm1
Created September 3, 2019 14:54
Show Gist options
  • Save TechByTom/89f7d607a15aebe5392f2076c7a9ae2c to your computer and use it in GitHub Desktop.
Save TechByTom/89f7d607a15aebe5392f2076c7a9ae2c to your computer and use it in GitHub Desktop.
PowerShell cmdlet for hiding VBA modules in Microsoft Office documents
<#
.Synopsis
Used to hide VBA modules from the VBA editor in Microsoft Office documents/templates, but still have them contain executable code.
Script created by William Knowles. @william_knows
Technique originally found by Thegrideon Software: https://www.thegrideon.com/vba-internals.html
.Description
This cmdlet facilitates editing the Word/Excel documents/templates to remove references to VBA modules.
For the older compatibility formats you can edit the file directly with a hex editor and remove module references.
For the newer XML formats that use zip files, you need to unzip them, and edit the vbaProject.bin file.
This cmdlet handles the editing automatically for both file formats, and can take an arbitrary module name to remove.
You MUST have at least two modules for this to work, even if "Module1" is empty (e.g., leave Module1 empty, and put your code in Module2).
.Parameter inputFile
The path of the file containing the VBA module to be hidden.
.Parameter outFile
The path of where to store the file with the hidden module.
The path must be in the current working directory or be an absolute path.
.Parameter moduleName
The name of the module you want to remove the reference to (e.g., "Module2").
.Example
# Take a Word input file and store it somewhere after hiding "Module2".
Invoke-HideVBAModule -inputFile TwoModules.docm -outFile TwoModules-Mod.docm -moduleName Module2
#>
function Invoke-HideVBAModule {
param(
[Parameter(Mandatory=$True)]
[string]$inputFile,
[Parameter(Mandatory=$True)]
[string]$outputFile,
[Parameter(Mandatory=$True)]
[string]$moduleName
)
Write-Host -foreground yellow "`n******************** VBA module hider ********************"
Write-Host -foreground yellow "Script by @william_knows, technique by Thegrideon Software.`n"
Write-Host "Removes references to VBA modules within Office documents so they don't show in the VBA editor, but still exist, and contain executable code."
Write-Host "Supports document and template formats for Word and Excel.`n"
if ((Test-Path $inputFile) -eq $false)
{
Write-Host -ForegroundColor Red "ERROR! The input file doesn't exist. Exiting."
return;
}
$inputFile = Resolve-Path $inputFile # convert to absolute path
# if outFile is not an absolute path, make it one (store output file in the current directory)
if (([System.IO.Path]::IsPathRooted($outputFile)) -eq $false)
{
$outputFile = (Resolve-Path .\).Path + "\" + $outputFile
}
if ($inputFile -eq $outputFile)
{
Write-Host -ForegroundColor Red "ERROR: inputFile can not be the same as outFile. Exiting!"
return
}
$inputExtension = $inputFile.split(".")[-1] # get extension
$supportedExcelExtensions = "xls","xlsm","xlt","xltm"
$supportedWordExtensions = "doc","docm","dot","dotm"
$oldFormatExtensions = "xls","xlt","doc","dot"
$newFormatExtensions = "xlsm","xltm","docm","dotm"
# Check if a valid file extension has been used for the input files
# Also used to set a variable to identify the directory for the vbaProject.bin file when the newer zipped XML format is in use (e.g., xlsm and docm).
if ($supportedExcelExtensions -contains $inputExtension){$appSpecificDir = "xl"}
elseif ($supportedWordExtensions -contains $inputExtension){$appSpecificDir = "word"}
else {Write-Host -ForegroundColor Red "ERROR! Unknown application specified. Must be an document or template for Excel or Word. Exiting."; return}
# The following C# code was adapted from: http://stackoverflow.com/questions/283456/byte-array-pattern-search
$csharpfindByteSequenceOffset = @"
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
public class findByteSequenceOffset
{
static public int SearchBytePattern(byte[] bytes, string moduleName)
{
byte[] pattern = Encoding.ASCII.GetBytes(moduleName);
List<int> positions = new List<int>();
int patternLength = pattern.Length;
int totalLength = bytes.Length;
byte firstMatchByte = pattern[0];
for (int i = 0; i < totalLength; i++)
{
if (firstMatchByte == bytes[i] && totalLength - i >= patternLength)
{
byte[] match = new byte[patternLength];
Array.Copy(bytes, i, match, 0, patternLength);
if (match.SequenceEqual<byte>(pattern))
{
positions.Add(i);
i += patternLength - 1;
}
}
}
return positions[0]; // We only need to replace the first occurance of it (and there should be only one)
}
}
"@
# Make C# available for use
Add-Type -TypeDefinition $csharpfindByteSequenceOffset
Add-Type -AssemblyName System.IO.Compression.FileSystem
$moduleString = "Module=" + $moduleName
# Pre module hiding clean up - check if outfile exists
if (Test-Path $outputFile)
{
Write-Host "Existing outfile exists. Delete?"
Remove-Item -Confirm $outputFile
if (Test-Path $outputFile) {Write-Host -ForegroundColor Red "ERROR! You chose not to delete the file. Exiting."; return}
}
# If the newer zipped XML format is used, unzip, modify, rezip.
if ($newFormatExtensions -contains $inputExtension)
{
# create a location to extract the contents to
$randomdir = [guid]::NewGuid().ToString()
$fileUnzippedDir = "$env:TEMP\$randomdir"
if (Test-Path $fileUnzippedDir)
{
Write-Host "Directory for unzipped items exists. Deleting!"
Remove-Item $fileUnzippedDir -Recurse
}
# now extract
[System.IO.Compression.ZipFile]::ExtractToDirectory($inputFile, $fileUnzippedDir)
# check if the VBA project exists, and if so, read, modify and write back the contents
if (Test-Path $fileUnzippedDir\$appSpecificDir\vbaProject.bin)
{
$bytes = [System.IO.File]::ReadAllBytes("$fileUnzippedDir\$appSpecificDir\vbaProject.bin")
$offset = [findByteSequenceOffset]::SearchBytePattern($bytes, $moduleString)
if ($offset -ne $null)
{
foreach ($x in 0..$moduleString.Length) # 14 = length of "Module=Module1"
{
# replace characters at offset with CRLF control characters
# modulo used to switch between CR and LF
if (($x % 2) -eq 0){$bytes[$offset+$x] = 0x0D} else {$bytes[$offset+$x] = 0x0A}
}
[System.IO.File]::WriteAllBytes("$fileUnzippedDir\$appSpecificDir\vbaProject.bin", $bytes)
# rezip to make it a usable Office document
[System.IO.Compression.ZipFile]::CreateFromDirectory($fileUnzippedDir, $outputFile)
} else {Write-Host -ForegroundColor Red "ERROR! This file does not have a module with the specified name. Exiting."; return}
} else {Write-Host -ForegroundColor Red "ERROR! This file does not have a VBA project associated with it. Exiting."; return}
}
# if the older compatibility file format is used, just modify
if ($oldFormatExtensions -contains $inputExtension)
{
$bytes = [System.IO.File]::ReadAllBytes($inputFile)
$offset = [findByteSequenceOffset]::SearchBytePattern($bytes, $moduleString)
if ($offset -ne $null)
{
foreach ($x in 0..$moduleString.Length) # length of "Module=<name>"
{
# replace characters at offset with CRLF control characters
# modulo used to switch between CR and LF
if (($x % 2) -eq 0){$bytes[$offset+$x] = 0x0D} else {$bytes[$offset+$x] = 0x0A}
}
[System.IO.File]::WriteAllBytes($outputFile, $bytes)
} else {Write-Host -ForegroundColor Red "ERROR! This file does not have a module with the specified name. Exiting."; return}
}
Write-Host -foreground Green "Success! Module reference deleted! Modified file stored at: $outputFile`n"
} # closes function
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment