In connection with another blog post I’m writing in a series about App Volumes Direct Mode, I wanted to find out exactly how the MSI we create during capture
works and what it contains.
In this post I have chosen to use a couple of App Volumes MSI-wrapped
packages I made myself using their capture tool.
After capture, you get a wrapped MSI that contains the App Volumes
package (VHD + JSON), either embedded directly or stored inside an internal CAB
stream. This also explains why you encounter a CAB file when the package
exceeds 2GB.
To get the most out of what I'm writing, you should have some
familiarity with how MSI files work. In short, an MSI is a database with tables
and logic that specifies where and how things should end up on the system.
Custom Actions are the logic that performs custom operations within an MSI.
This may not be rocket science, but I believe you need the right tools and a decent understanding of MSI to follow the flow. This was previously a
gap in my knowledge, but after attending a packaging course with the
MasterPackager team, I have filled in those gaps.
Tools needed to extract all files from the MSI
1.
MSI editor - I
recommend MasterPackager,
but there are other alternatives (worse imo)
2.
LessMsi - "This
is a utility with a graphical user interface and a command line interface that
can be used to view and extract the contents of an MSI file". Had to use
the command line to extract content from the Binary table. Covered later in
this document: lessmsi x <msiFileName> [<outputDir>]
3.
Dependencies - a rewrite of
the legacy Dependency Walker
tool shipped with Windows SDKs, but whose development stopped around 2006.
Dependencies can help Windows developers troubleshoot DLL load dependency
issues.
4.
ILSpy - the open-source .NET
assembly browser and decompiler.
5.
7-Zip - because it can often
open and extract parts of an MSI file.
Easiest way to extract VHD and JSON files
If you only need to get the raw files from the MSI, you only need 7-Zip.
If the package is under 2GB, these files are located directly in the
root of the MSI and can easily be extracted with 7-Zip:
[ Picture 1 ]
For packages larger than 2GB, extraction is a bit trickier. Here is an
example of such a package I created.
[ Picture 2 ]
The .VHD and .JSON files in this package are stored inside an internal
.CAB file, which in this case has been given Chinese characters as the
filename. Since the file is so large, it is easy to identify the .CAB file by
its size. I then extract this file to disk using 7-Zip.
[ Picture 3 ]
Then I open "{㭄㫂㩆㢂-㭉㥊-㮄㢎-㥉㠉-㢊㪈㦊㤎㥁㥊}䆾䅤" again in 7-Zip, where I find the files and
extract them to disk.
Analysing the MSI file
When running msiexec /i "name.msi" /qn and msiexec /x
"name.msi" /qn, it is evident that the MSI runs App Volumes-specific
commands against the agent, which publishes and unpublishes the application via
the App Volumes Agent.
To get a clearer picture of what is actually happening, I opened the MSI
in Master Packager:
[ Picture 4 ]
We can see it contains:
•
0
files in the File table
•
1
registry value
•
1
Feature
•
No
shortcuts
•
UpgradeCode
defined
•
24
properties
•
5
Custom Actions
This indicates that this is not a classic wrapper MSI that typically
wraps an EXE installer inside MSI format for standardized deployment. The App
Volumes package is somewhat different - there is no EXE here, but rather a VHD
and some agent calls. It is therefore an unusual variant of the wrapper
concept, but the principle is the same: the actual payload is handled outside
Windows Installer's File table.
Looking at the Feature in the MSI
[ Picture 5 ]
Here I can see the registry area where App Volumes stores the product
code when the package is installed locally:
Software\Omnissa\AppVolumes\OfflineStore\MsiPackages\
[ProductCode]
I already knew this from working extensively with App Volumes Direct
Mode.
We can also see that it links to the registry entry. MSI features can be
conditionally installed or excluded using ADDLOCAL / REMOVE or feature
conditions, so I am unsure whether they have used that approach here. Perhaps
it is easier to script it this way since it is a Property that can be fetched
dynamically.
[ Picture 6 ]
Here we can clearly see that "installappPackage",
"uninstallappPackage", "upgradeappPackage" and
"getoldinstallerproperties" are run via custom action.
[ Picture 7 ]
The App Volumes-related custom actions are scheduled very early in the InstallExecuteSequence.
In this package, GetOldInstallerProperties, UpgradeAppPackage, and UninstallAppPackage
run before InstallInitialize (around sequence 1401–1404), while InstallAppPackage
runs immediately after InstallInitialize at sequence 1501.
InstallInitialize (seq 1500) marks the beginning of the transactional
execution phase. It is worth noting that actions scheduled before InstallInitialize
in the Execute sequence still run as immediate actions and are typically used
for detection, property setup, and general flow control before the
transactional phase begins.
Standard file-copying actions such as InstallFiles occur much later in
the sequence (typically around sequence ~4000). However, this MSI does not
contain any payload in the File table. Instead, the early custom actions
orchestrate the deployment by communicating with the App Volumes Agent, which
performs the actual attach and mount of the VHD package.
[Picture 8 ]
This is not a classic "file-copying" MSI.
These are the relevant properties:
•
DiskFileName
= ILSpy_Installer_9.1.0.7988-x64.vhd
•
AppPackageId
•
AppVolumeId
•
MetadataFileName
•
DELIVERYTYPE
= OnDemand
What does DELIVERYTYPE = OnDemand mean?
As previously explained, this is a "wrapper" MSI that installs
the App Volumes VHD package, not a traditional MSI with a File table payload.
The MSI functions as a delivery mechanism with Custom Actions that call the App
Volumes agent - it does not contain any application files directly.
DELIVERYTYPE is a public property (uppercase = public), which means it
can be controlled from the command line during installation. It is also listed
in SecureCustomProperties, which ensures the property is propagated from the
client process to the elevated Windows Installer service during execution. This
is required whenever an installation runs elevated — the Windows Installer
client process and the elevated system process are separate, and public
properties do not automatically cross that boundary.
In practice, this means you can control the attach mode (Classic vs
OnDemand) directly from the command line. If no value is supplied, the package
installs using the default value OnDemand, based on my testing and vendor
documentation:
msiexec /i
"package.msi" /qn DELIVERYTYPE=Classic
[ Picture 9 ]
Here we can see that InstallAppPackage runs as Immediate (right away).
To understand what is actually being executed, look at the following MSI
tables:
[ Picture 10 ]
1.
CustomAction
table: find the row where Source = CustomActionBinary (or Target points to the
entry point).
2.
The Type column:
determines whether it is a DLL CA, Immediate/Deferred, etc.
3.
InstallExecuteSequence:
when it is triggered.
Extracting content from the Binary table in the MSI file
Custom Action binary files are stored in the MSI's Binary table. That is
why we need to dig into that table to retrieve CustomActionBinary.dll - it is
not stored as a regular file in the package, but as a raw binary stream in the
database.
There are certainly many tools and methods available, but in my example
I used the previously mentioned "Lessmsi-v2.12.5":
$msi =
".\lessmsi-v2.12.5\ILSpy_Installer_9.1.0.7988-x64.msi"
$outputDir =
"C:\temp\extracted\"
New-Item
-ItemType Directory -Force -Path $outputDir | Out-Null
$installer =
New-Object -ComObject WindowsInstaller.Installer
$db =
$installer.OpenDatabase($msi, 0)
$db.Export("Binary",
$outputDir, "Binary.idt")
The files are written to the folder C:\temp\extracted
I get two .IBD files. This is not a real file format - the extension was
simply invented when I dumped the files. I could have called them anything at
all; the extension has nothing to do with the file contents. That is exactly
why we need to check the magic bytes to find out what we actually have.
To determine the actual file type, I wrote the following small
PowerShell script:
$path =
"C:\Temp\extracted\1\Binary\CustomActionBinary.ibd"
$bytes =
[System.IO.File]::ReadAllBytes($path)
"{0:X2}
{1:X2}" -f $bytes[0], $bytes[1]
The result was: 4D 5A
If you get 4D 5A → rename to .dll first, because Custom Actions are most
often .DLL files.
Inspecting the DLL with Dependencies
In the "Dependencies" program I can see what
CustomActionBinary.dll does:
[ Picture 11 ]
Under Functions we can see all the functions listed - this is where the
"installer magic" happens.
Since a .DLL, like .EXE files, is compiled code, we would need a
decompiler to see further details. However, I stop here since I already know
which commands are executed from my own experience with App Volumes command
lines.
In essence, the MSI is not an
installer in the traditional sense — it is purely a deployment trigger. The
actual work follows a simple chain:
MSI → Custom
Action DLL → App Volumes Agent → mount VHD → app available
The MSI just pulls the lever. The App Volumes Agent does the rest.