MSIX.OfflineRegistry.ps1
|
# ============================================================================= # MSIX Offline Registry — offreg.dll wrapper # ----------------------------------------------------------------------------- # Parses Registry.dat hive files (from inside an MSIX package) without # requiring elevation. reg.exe load / RegLoadKey demand SeBackupPrivilege + # SeRestorePrivilege (admins only); offreg.dll's OR* APIs parse the hive # directly from disk and never mount it into the live registry, so any user # can call them. # # offreg.dll ships in C:\Windows\System32 on Windows 10/11 by default. # # On Windows 10/11 (10.0.26100 confirmed) the named exports are: # ORCloseHive, ORCloseKey, ORCreateHive, ORCreateHiveEx, ORCreateKey, # ORDeleteKey, ORDeleteValue, OREnumKey, OREnumValue, ORFlushHive, # OROpenHiveByHandle, OROpenKey, ORQueryInfoKey(Ex|ValueEx), # ORRenameKey, ORSaveHive(Ex|ToHandle), ORSetKeySecurity, ORSetValue, # ORSetVirtualFlags # # There is intentionally no ORLoadHive / OROpenHive (those existed in older # SDKs); callers must open the file themselves via CreateFile and pass the # HANDLE to OROpenHiveByHandle. This module wraps that for you. # # Reference: https://learn.microsoft.com/windows/win32/devnotes/offline-registry-library # ============================================================================= if (-not ([System.Management.Automation.PSTypeName]'MsixOffReg').Type) { Add-Type -TypeDefinition @" using System; using System.Runtime.InteropServices; using System.Text; using Microsoft.Win32.SafeHandles; public static class MsixOffReg { // Win32 constants public const uint GENERIC_READ = 0x80000000; public const uint GENERIC_WRITE = 0x40000000; public const uint FILE_SHARE_READ = 0x00000001; public const uint OPEN_EXISTING = 3; // OR error codes (subset) public const int ERROR_SUCCESS = 0; public const int ERROR_FILE_NOT_FOUND = 2; public const int ERROR_MORE_DATA = 234; public const int ERROR_NO_MORE_ITEMS = 259; // REG_* value types public const uint REG_NONE = 0; public const uint REG_SZ = 1; public const uint REG_EXPAND_SZ = 2; public const uint REG_BINARY = 3; public const uint REG_DWORD = 4; public const uint REG_MULTI_SZ = 7; public const uint REG_QWORD = 11; [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] public static extern SafeFileHandle CreateFileW( string lpFileName, uint dwDesiredAccess, uint dwShareMode, IntPtr lpSecurityAttributes, uint dwCreationDisposition, uint dwFlagsAndAttributes, IntPtr hTemplateFile); [DllImport("offreg.dll", ExactSpelling = true, SetLastError = true)] public static extern int OROpenHiveByHandle( SafeFileHandle FileHandle, out IntPtr phkResult); [DllImport("offreg.dll", ExactSpelling = true)] public static extern int ORCloseHive(IntPtr Handle); [DllImport("offreg.dll", CharSet = CharSet.Unicode, ExactSpelling = true)] public static extern int OROpenKey( IntPtr Handle, string lpSubKey, out IntPtr phkResult); [DllImport("offreg.dll", ExactSpelling = true)] public static extern int ORCloseKey(IntPtr Handle); [DllImport("offreg.dll", CharSet = CharSet.Unicode, ExactSpelling = true)] public static extern int OREnumKey( IntPtr Handle, uint dwIndex, StringBuilder lpName, ref uint lpcName, IntPtr lpClass, IntPtr lpcClass, IntPtr lpftLastWriteTime); [DllImport("offreg.dll", CharSet = CharSet.Unicode, ExactSpelling = true)] public static extern int ORGetValue( IntPtr Handle, string lpSubKey, string lpValue, out uint pdwType, byte[] pvData, ref uint pcbData); [DllImport("offreg.dll", CharSet = CharSet.Unicode, ExactSpelling = true)] public static extern int OREnumValue( IntPtr Handle, uint dwIndex, StringBuilder lpValueName, ref uint lpcValueName, out uint lpType, byte[] lpData, ref uint lpcbData); [DllImport("offreg.dll", CharSet = CharSet.Unicode, ExactSpelling = true)] public static extern int ORDeleteKey(IntPtr Handle, string lpSubKey); [DllImport("offreg.dll", CharSet = CharSet.Unicode, ExactSpelling = true)] public static extern int ORSaveHive( IntPtr Handle, string lpHivePath, uint dwOsMajorVersion, uint dwOsMinorVersion); // --- Hive / key creation (used by tests and Remove-MsixUninstallerArtifact) [DllImport("offreg.dll", ExactSpelling = true)] public static extern int ORCreateHive(out IntPtr phkResult); [DllImport("offreg.dll", CharSet = CharSet.Unicode, ExactSpelling = true)] public static extern int ORCreateKey( IntPtr Handle, string lpSubKey, string lpClass, uint dwOptions, IntPtr pSecurityDescriptor, out IntPtr phkResult, out uint pdwDisposition); [DllImport("offreg.dll", CharSet = CharSet.Unicode, ExactSpelling = true)] public static extern int ORSetValue( IntPtr Handle, string lpValueName, uint dwType, byte[] lpData, uint cbData); } "@ } function _MsixOpenOfflineHive { <# .SYNOPSIS Loads a registry hive file (Registry.dat from an MSIX, etc.) via offreg.dll and returns the hive root key handle. .DESCRIPTION Opens the file with CreateFile (GENERIC_READ, share-read, OPEN_EXISTING) and passes the HANDLE to OROpenHiveByHandle. Returns the hive root key as an [IntPtr]. Throws on failure. IMPORTANT: the caller MUST call _MsixCloseOfflineHive on the returned handle to release the in-memory hive — the easiest way is to use the _MsixWithOfflineHive scriptblock wrapper. .PARAMETER Path Absolute path to the hive file. .OUTPUTS [IntPtr] hive root key handle. #> [CmdletBinding()] [OutputType([IntPtr])] param([Parameter(Mandatory)][string]$Path) if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { throw "Offline hive not found: $Path" } $fh = [MsixOffReg]::CreateFileW( $Path, [MsixOffReg]::GENERIC_READ, [MsixOffReg]::FILE_SHARE_READ, [IntPtr]::Zero, [MsixOffReg]::OPEN_EXISTING, 0, [IntPtr]::Zero) if ($fh.IsInvalid) { $code = [Runtime.InteropServices.Marshal]::GetLastWin32Error() throw "CreateFile failed for '$Path' (Win32 error $code)" } try { $hive = [IntPtr]::Zero $rc = [MsixOffReg]::OROpenHiveByHandle($fh, [ref]$hive) if ($rc -ne 0) { throw "OROpenHiveByHandle failed (error $rc) for '$Path'" } return $hive } finally { $fh.Close() } } function _MsixCloseOfflineHive { [CmdletBinding()] param([IntPtr]$Hive) if ($Hive -ne [IntPtr]::Zero) { $null = [MsixOffReg]::ORCloseHive($Hive) } } function _MsixOfflineOpenKey { <# .SYNOPSIS Opens a subkey under the given hive/key handle. Returns the new key handle, or [IntPtr]::Zero if the key does not exist. Caller is responsible for closing the returned handle via _MsixOfflineCloseKey. #> [CmdletBinding()] [OutputType([IntPtr])] param( [Parameter(Mandatory)][IntPtr]$Parent, [Parameter(Mandatory)][string]$SubKey ) if ($Parent -eq [IntPtr]::Zero) { return [IntPtr]::Zero } $key = [IntPtr]::Zero $rc = [MsixOffReg]::OROpenKey($Parent, $SubKey, [ref]$key) if ($rc -ne 0) { return [IntPtr]::Zero } return $key } function _MsixOfflineCloseKey { [CmdletBinding()] param([IntPtr]$Key) if ($Key -ne [IntPtr]::Zero) { $null = [MsixOffReg]::ORCloseKey($Key) } } function _MsixOfflineEnumSubKeys { <# .SYNOPSIS Returns the names (string[]) of all subkeys under the given key handle. #> [CmdletBinding()] [OutputType([string[]])] param([Parameter(Mandatory)][IntPtr]$Key) if ($Key -eq [IntPtr]::Zero) { return [string[]]@() } $names = [System.Collections.Generic.List[string]]::new() $i = [uint32]0 while ($true) { $sb = New-Object System.Text.StringBuilder 512 $cName = [uint32]512 $rc = [MsixOffReg]::OREnumKey($Key, $i, $sb, [ref]$cName, [IntPtr]::Zero, [IntPtr]::Zero, [IntPtr]::Zero) if ($rc -ne 0) { break } $names.Add($sb.ToString()) $i++ } return [string[]]$names.ToArray() } function _MsixOfflineGetValue { <# .SYNOPSIS Reads a named value from the given subkey (relative to the hive root or an open key handle). Pass an empty -Name to read the default value. Decodes REG_SZ / REG_EXPAND_SZ to [string], REG_DWORD to [int], REG_QWORD to [long], REG_MULTI_SZ to [string[]]; everything else returns raw [byte[]]. Returns $null if the value does not exist. .PARAMETER Parent Either the hive root handle (for -SubKey-relative lookups) or an open key handle (use -SubKey '' to read values directly on that key). .PARAMETER SubKey Subkey path relative to -Parent. Use '' to read on -Parent directly. .PARAMETER Name Value name. Use '' (default) to read the default (unnamed) value. #> [CmdletBinding()] [OutputType([string], [int], [long], [byte[]], [string[]])] param( [Parameter(Mandatory)][IntPtr]$Parent, [Parameter(Mandatory)][AllowEmptyString()][string]$SubKey, [AllowEmptyString()][string]$Name = '' ) if ($Parent -eq [IntPtr]::Zero) { return $null } $type = [uint32]0 $size = [uint32]0 # ORGetValue signature requires a non-null subkey string; '' is acceptable. $rc = [MsixOffReg]::ORGetValue($Parent, $SubKey, $Name, [ref]$type, $null, [ref]$size) if ($rc -ne 0 -and $rc -ne 234) { return $null } if ($size -eq 0) { # Zero-length value: still emit appropriate empty result by type. switch ($type) { 1 { return '' } 2 { return '' } 7 { return [string[]]@() } default { return $null } } } $buf = New-Object byte[] $size $rc = [MsixOffReg]::ORGetValue($Parent, $SubKey, $Name, [ref]$type, $buf, [ref]$size) if ($rc -ne 0) { return $null } switch ($type) { 1 { # REG_SZ return [System.Text.Encoding]::Unicode.GetString($buf, 0, [int]$size).TrimEnd("`0") } 2 { # REG_EXPAND_SZ — return raw, callers can ExpandEnvironmentStrings if needed return [System.Text.Encoding]::Unicode.GetString($buf, 0, [int]$size).TrimEnd("`0") } 4 { # REG_DWORD if ($size -lt 4) { return $null } return [BitConverter]::ToInt32($buf, 0) } 7 { # REG_MULTI_SZ $s = [System.Text.Encoding]::Unicode.GetString($buf, 0, [int]$size) return [string[]]@($s -split "`0" | Where-Object { $_ }) } 11 { # REG_QWORD if ($size -lt 8) { return $null } return [BitConverter]::ToInt64($buf, 0) } default { return $buf } } } function _MsixOfflineDeleteKey { <# .SYNOPSIS Deletes a subkey (and all of its descendants) under the given parent handle. Wraps ORDeleteKey. After mutating the hive, callers must call _MsixOfflineSaveHive (to a NEW path) to persist the change. #> [CmdletBinding()] [OutputType([bool])] param( [Parameter(Mandatory)][IntPtr]$Parent, [Parameter(Mandatory)][string]$SubKey ) if ($Parent -eq [IntPtr]::Zero) { return $false } $rc = [MsixOffReg]::ORDeleteKey($Parent, $SubKey) return ($rc -eq 0) } function _MsixCreateOfflineHive { <# .SYNOPSIS Creates a new in-memory empty hive via ORCreateHive. Returns the hive root handle. Caller must release with _MsixCloseOfflineHive. #> [CmdletBinding()] [OutputType([IntPtr])] param() $hive = [IntPtr]::Zero $rc = [MsixOffReg]::ORCreateHive([ref]$hive) if ($rc -ne 0) { throw "ORCreateHive failed (error $rc)" } return $hive } function _MsixOfflineCreateKey { <# .SYNOPSIS Creates a subkey under the given parent. Returns the new key handle. #> [CmdletBinding()] [OutputType([IntPtr])] param( [Parameter(Mandatory)][IntPtr]$Parent, [Parameter(Mandatory)][string]$SubKey ) $key = [IntPtr]::Zero $disp = [uint32]0 $rc = [MsixOffReg]::ORCreateKey($Parent, $SubKey, $null, 0, [IntPtr]::Zero, [ref]$key, [ref]$disp) if ($rc -ne 0) { throw "ORCreateKey failed (error $rc) for '$SubKey'" } return $key } function _MsixOfflineSetValueString { <# .SYNOPSIS Sets a REG_SZ string value on the given key. Use -Type to set REG_EXPAND_SZ instead. #> [CmdletBinding()] param( [Parameter(Mandatory)][IntPtr]$Key, [Parameter(Mandatory)][AllowEmptyString()][string]$Name, [Parameter(Mandatory)][AllowEmptyString()][string]$Value, [ValidateSet('REG_SZ','REG_EXPAND_SZ')][string]$Type = 'REG_SZ' ) # Encode as null-terminated UTF-16LE $bytes = [System.Text.Encoding]::Unicode.GetBytes($Value + "`0") $typeNum = if ($Type -eq 'REG_EXPAND_SZ') { [MsixOffReg]::REG_EXPAND_SZ } else { [MsixOffReg]::REG_SZ } $rc = [MsixOffReg]::ORSetValue($Key, $Name, $typeNum, $bytes, [uint32]$bytes.Length) if ($rc -ne 0) { throw "ORSetValue failed (error $rc) for '$Name'" } } function _MsixOfflineSetValueDword { <# .SYNOPSIS Sets a REG_DWORD value on the given key. #> [CmdletBinding()] param( [Parameter(Mandatory)][IntPtr]$Key, [Parameter(Mandatory)][AllowEmptyString()][string]$Name, [Parameter(Mandatory)][int]$Value ) $bytes = [BitConverter]::GetBytes($Value) $rc = [MsixOffReg]::ORSetValue($Key, $Name, [MsixOffReg]::REG_DWORD, $bytes, [uint32]$bytes.Length) if ($rc -ne 0) { throw "ORSetValue (DWORD) failed (error $rc) for '$Name'" } } function _MsixOfflineSaveHive { <# .SYNOPSIS Persists an in-memory hive to disk via ORSaveHive. ORSaveHive will NOT overwrite an existing file — pass a new path and rename afterwards. #> [CmdletBinding()] [OutputType([bool])] param( [Parameter(Mandatory)][IntPtr]$Hive, [Parameter(Mandatory)][string]$Path, [uint32]$OsMajor = 6, [uint32]$OsMinor = 1 ) if ($Hive -eq [IntPtr]::Zero) { return $false } if (Test-Path -LiteralPath $Path) { throw "ORSaveHive will not overwrite an existing file: $Path" } $rc = [MsixOffReg]::ORSaveHive($Hive, $Path, $OsMajor, $OsMinor) return ($rc -eq 0) } |