Forked from williamknows/Invoke-HideVBAModule.psm1
Created
September 3, 2019 14:54
-
-
Save TechByTom/89f7d607a15aebe5392f2076c7a9ae2c to your computer and use it in GitHub Desktop.
PowerShell cmdlet for hiding VBA modules in Microsoft Office documents
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<# | |
.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