Skip to content

Commit 3989c86

Browse files
authored
feat(pedm): explicitly support elevating .msi files (#1325)
By inspecting elevated .msi files launched from Explorer, we see that Explorer invokes %systemroot%\system32\msiexec, with the command line `"%systemroot%\system32\msiexec" /i "{path-to-msi}"`. We achieve the same in the PEDM module by using the same command if the file extension is .msi. The .msi extension is already being trapped by the shell extension, but previously we would call `CreateProcess` on the .msi causing "file is not a valid Win32 executable". Additionally, I polished and added @awakecoding's original "ExplorerCommand.ps1" script. This is useful for debugging, where the installed shell extension can't be overwritten as it's in use by Explorer. Install Devolutions Agent with the PEDM feature; then use the PowerShell to unregistered the shell extension. Swap in your development or debug DLL, then re-register the shell extension. If you supply the .pdb file as well it's possible to attach the debugger to Explorer.exe and debug the shell extension code.
1 parent e90f06f commit 3989c86

File tree

5 files changed

+224
-8
lines changed

5 files changed

+224
-8
lines changed

crates/devolutions-pedm-shell-ext/src/lib_win.rs

+37-7
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ use devolutions_pedm_shared::client::{self};
1212
use devolutions_pedm_shared::desktop;
1313
use parking_lot::{Mutex, RwLock};
1414
use tokio::sync::mpsc::{self, Receiver, Sender};
15+
use win_api_wrappers::fs::get_system32_path;
1516
use win_api_wrappers::process::{Module, Process};
1617
use win_api_wrappers::raw::core::{
1718
implement, interface, Error, IUnknown, IUnknown_Vtbl, Interface, Result, GUID, HRESULT, PWSTR,
@@ -218,6 +219,32 @@ fn find_main_explorer(session: u32) -> Option<u32> {
218219
})
219220
}
220221

222+
fn resolve_msi(path: &Path) -> Option<LaunchPayload> {
223+
if !matches!(path.extension().and_then(|e| e.to_str()), Some(ext) if ext.eq_ignore_ascii_case("msi")) {
224+
return None;
225+
}
226+
227+
let system32 = get_system32_path().ok()?;
228+
let msiexec_path = Path::new(&system32).join("msiexec.exe");
229+
230+
let environment = environment_block(None, false).ok()?;
231+
232+
let exe_path = expand_environment_path(&msiexec_path, &environment).ok()?;
233+
234+
// By inspecting elevated .msi files launched from Explorer, we see that Explorer invokes %systemroot%\system32\msiexec,
235+
// with the command line "%systemroot%\system32\msiexec" /i "{path-to-msi}".
236+
// We achieve the same in the PEDM module by using the same command if the file extension is .msi.
237+
// The .msi extension is already being trapped by the shell extension, but previously we would call
238+
// CreateProcess on the .msi causing "file is not a valid Win32 executable".
239+
Some(LaunchPayload {
240+
executable_path: exe_path.as_os_str().to_str().map(str::to_owned),
241+
command_line: Some(format!("\"{}\" /i \"{}\"", exe_path.display(), path.display())),
242+
working_directory: None,
243+
creation_flags: 0,
244+
startup_info: None,
245+
})
246+
}
247+
221248
fn resolve_lnk(path: &Path) -> Option<LaunchPayload> {
222249
let link = Link::new(path);
223250

@@ -253,13 +280,16 @@ fn start_listener() {
253280
match command {
254281
ChannelCommand::Exit => break,
255282
ChannelCommand::Elevate(path) => {
256-
let mut payload = resolve_lnk(&path).unwrap_or_else(|| LaunchPayload {
257-
executable_path: path.as_os_str().to_str().map(str::to_owned),
258-
command_line: None,
259-
working_directory: None,
260-
creation_flags: 0,
261-
startup_info: None,
262-
});
283+
let mut payload =
284+
resolve_lnk(&path)
285+
.or_else(|| resolve_msi(&path))
286+
.unwrap_or_else(|| LaunchPayload {
287+
executable_path: path.as_os_str().to_str().map(str::to_owned),
288+
command_line: None,
289+
working_directory: None,
290+
creation_flags: 0,
291+
startup_info: None,
292+
});
263293

264294
payload.startup_info = Some(StartupInfoDto {
265295
parent_pid: find_main_explorer(

crates/win-api-wrappers/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ features = [
3434
"Win32_System_Environment",
3535
"Win32_System_GroupPolicy",
3636
"Win32_System_IO",
37+
"Win32_System_SystemInformation",
3738
"Win32_System_Kernel",
3839
"Win32_System_LibraryLoader",
3940
"Win32_System_Memory",

crates/win-api-wrappers/src/fs.rs

+25
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1+
use std::ffi::OsString;
2+
use std::os::windows::ffi::OsStringExt;
13
use std::path::Path;
24

35
use anyhow::Context as _;
6+
use windows::Win32::Foundation::MAX_PATH;
47
use windows::Win32::Storage::FileSystem;
8+
use windows::Win32::System::SystemInformation::GetSystemDirectoryW;
59

610
use crate::security::attributes::SecurityAttributes;
711
use crate::str::{U16CStrExt as _, U16CString};
@@ -14,3 +18,24 @@ pub fn create_directory(path: &Path, security_attributes: Option<&SecurityAttrib
1418

1519
Ok(())
1620
}
21+
22+
pub fn get_system32_path() -> anyhow::Result<String> {
23+
let mut buffer = [0u16; MAX_PATH as usize];
24+
25+
// SAFETY:
26+
// - `buffer.as_mut_ptr()` gives a valid writable pointer for MAX_PATH u16s.
27+
// - `GetSystemDirectoryW` expects a valid mutable wide string buffer.
28+
let len = unsafe { GetSystemDirectoryW(Some(std::slice::from_raw_parts_mut(buffer.as_mut_ptr(), buffer.len()))) };
29+
30+
if len == 0 {
31+
anyhow::bail!("GetSystemDirectoryW failed");
32+
}
33+
34+
if len as usize >= buffer.len() {
35+
anyhow::bail!("buffer too small for system directory path");
36+
}
37+
38+
Ok(OsString::from_wide(&buffer[..len as usize])
39+
.to_string_lossy()
40+
.into_owned())
41+
}

crates/win-api-wrappers/src/security/crypt.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ impl CatalogAdminContext {
172172
let mut required_size = 0u32;
173173

174174
// SAFETY: `hFile` must not be NULL and must be a valid file pointer. The `file` is not dropped so it should be valid.
175-
let res = unsafe {
175+
unsafe {
176176
CryptCATAdminCalcHashFromFileHandle2(
177177
self.handle.0 as isize,
178178
HANDLE(file.as_raw_handle().cast()),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
<#
2+
Register or unregister the shell extension from the system context menu.
3+
4+
This is useful for debugging: unregister the shell extension, swap in your debug DLL, and then re-register
5+
6+
Must run as administrator
7+
8+
e.g.
9+
10+
. ./ExplorerCommand.ps1
11+
Register-ExplorerCommand -FilePath "$Env:ProgramFiles\Devolutions\Agent\DevolutionsPedmShellExt.dll" -CLSID "{0ba604fd-4a5a-4abb-92b1-09ac5c3bf356}" -Verb "RunElevated" -MenuText "Run Elevated"
12+
Unregister-ExplorerCommand -CLSID "{0ba604fd-4a5a-4abb-92b1-09ac5c3bf356}" -Verb "RunElevated"
13+
#>
14+
15+
function Register-ExplorerCommand {
16+
[CmdletBinding()]
17+
param (
18+
[Parameter(Mandatory = $true)]
19+
[string]$FilePath,
20+
21+
[Parameter(Mandatory = $true)]
22+
[string]$CLSID,
23+
24+
[Parameter(Mandatory = $true)]
25+
[string]$Verb,
26+
27+
[Parameter(Mandatory = $false)]
28+
[string]$MenuText = "Run Elevated",
29+
30+
[Parameter(Mandatory = $false)]
31+
[string[]]$Extensions = @(".exe", ".msi", ".lnk", ".ps1", ".bat") # Restrict to these
32+
)
33+
34+
# Validate the DLL Path
35+
if (!(Test-Path $FilePath)) {
36+
Write-Error "ERROR: DLL path '$FilePath' does not exist. Exiting."
37+
return
38+
}
39+
40+
Write-Host "✅ DLL Path verified: $FilePath" -ForegroundColor Green
41+
42+
# Register CLSID in HKEY_CLASSES_ROOT\CLSID
43+
$clsidPathHKCR = "Registry::HKEY_CLASSES_ROOT\CLSID\$CLSID"
44+
if (Test-Path $clsidPathHKCR) {
45+
Write-Host "⚠️ CLSID already exists in registry: $CLSID" -ForegroundColor Yellow
46+
} else {
47+
Write-Host "🆕 Registering CLSID: $CLSID" -ForegroundColor Cyan
48+
New-Item -Path $clsidPathHKCR -Force | Out-Null
49+
Set-ItemProperty -Path $clsidPathHKCR -Name "(Default)" -Value "PedmShellExt"
50+
Write-Host "✅ CLSID registered in HKCR" -ForegroundColor Green
51+
}
52+
53+
# Register InprocServer32
54+
$inprocPathHKCR = "$clsidPathHKCR\InprocServer32"
55+
if (!(Test-Path $inprocPathHKCR)) {
56+
Write-Host "🆕 Registering InprocServer32..." -ForegroundColor Cyan
57+
New-Item -Path $inprocPathHKCR -Force | Out-Null
58+
Set-ItemProperty -Path $inprocPathHKCR -Name "(Default)" -Value $FilePath
59+
Set-ItemProperty -Path $inprocPathHKCR -Name "ThreadingModel" -Value "Apartment"
60+
Write-Host "✅ InprocServer32 registered" -ForegroundColor Green
61+
}
62+
63+
# Register Explorer Command for Specific File Extensions
64+
foreach ($ext in $Extensions) {
65+
$extKeyPath = "Registry::HKEY_CLASSES_ROOT\$ext"
66+
67+
# Find the associated file class (e.g., exefile for .exe)
68+
try {
69+
$fileClass = (Get-ItemProperty -Path $extKeyPath -ErrorAction Stop)."(Default)"
70+
} catch {
71+
Write-Host "⚠️ No registry entry found for $ext. Skipping." -ForegroundColor Yellow
72+
continue
73+
}
74+
75+
# If no file class is found, assume the extension itself
76+
if (-not $fileClass) { $fileClass = $ext }
77+
78+
$commandPath = "Registry::HKEY_CLASSES_ROOT\$fileClass\shell\$Verb"
79+
80+
Write-Host "🆕 Registering ExplorerCommand for: $ext -> $fileClass at $commandPath" -ForegroundColor Cyan
81+
82+
# Ensure the shell key exists
83+
if (!(Test-Path "$commandPath")) {
84+
New-Item -Path $commandPath -Force | Out-Null
85+
}
86+
87+
# Set menu text and ExplorerCommandHandler CLSID
88+
Set-ItemProperty -Path $commandPath -Name "(Default)" -Value $MenuText
89+
Set-ItemProperty -Path $commandPath -Name "ExplorerCommandHandler" -Value $CLSID
90+
Set-ItemProperty -Path $commandPath -Name "MUIVerb" -Value "@FilePath,-150"
91+
}
92+
93+
Add-Type -Namespace Win32 -Name NativeMethods -MemberDefinition @"
94+
[System.Runtime.InteropServices.DllImport("shell32.dll")]
95+
public static extern void SHChangeNotify(int wEventId, int uFlags, IntPtr dwItem1, IntPtr dwItem2);
96+
"@
97+
98+
[Win32.NativeMethods]::SHChangeNotify(0x8000000, 0x1000, [IntPtr]::Zero, [IntPtr]::Zero)
99+
100+
Write-Host "✅ ExplorerCommand registered successfully for selected file types!" -ForegroundColor Green
101+
}
102+
103+
function Unregister-ExplorerCommand {
104+
[CmdletBinding()]
105+
param (
106+
[Parameter(Mandatory = $true)]
107+
[string]$CLSID,
108+
109+
[Parameter(Mandatory = $true)]
110+
[string]$Verb,
111+
112+
[Parameter(Mandatory = $false)]
113+
[string[]]$Extensions = @(".exe", ".msi", ".lnk", ".ps1", ".bat") # Restrict to these
114+
)
115+
116+
Write-Host "Unregistering ExplorerCommand with CLSID: $CLSID" -ForegroundColor Yellow
117+
118+
# Remove CLSID registration
119+
$clsidPathHKCR = "Registry::HKEY_CLASSES_ROOT\CLSID\$CLSID"
120+
if (Test-Path $clsidPathHKCR) {
121+
Remove-Item -Path $clsidPathHKCR -Force -Recurse -ErrorAction SilentlyContinue
122+
Write-Host "✅ Removed CLSID from HKCR" -ForegroundColor Green
123+
} else {
124+
Write-Host "⚠️ CLSID not found in HKCR, skipping." -ForegroundColor Yellow
125+
}
126+
127+
# Remove ExplorerCommand registry entry for specific file types
128+
foreach ($ext in $Extensions) {
129+
$extKeyPath = "Registry::HKEY_CLASSES_ROOT\$ext"
130+
131+
# Find the associated file class (e.g., exefile for .exe)
132+
try {
133+
$fileClass = (Get-ItemProperty -Path $extKeyPath -ErrorAction Stop)."(Default)"
134+
} catch {
135+
Write-Host "⚠️ No registry entry found for $ext. Skipping." -ForegroundColor Yellow
136+
continue
137+
}
138+
139+
# If no file class is found, assume the extension itself
140+
if (-not $fileClass) { $fileClass = $ext }
141+
142+
$commandPath = "Registry::HKEY_CLASSES_ROOT\$fileClass\shell\$Verb"
143+
144+
if (Test-Path $commandPath) {
145+
Write-Host "🗑 Removing ExplorerCommand for: $ext -> $fileClass at $commandPath" -ForegroundColor Cyan
146+
Remove-Item -Path $commandPath -Force -Recurse -ErrorAction SilentlyContinue
147+
} else {
148+
Write-Host "⚠️ No registered menu for $ext ($fileClass), skipping." -ForegroundColor Yellow
149+
}
150+
}
151+
152+
Add-Type -Namespace Win32 -Name NativeMethods -MemberDefinition @"
153+
[System.Runtime.InteropServices.DllImport("shell32.dll")]
154+
public static extern void SHChangeNotify(int wEventId, int uFlags, IntPtr dwItem1, IntPtr dwItem2);
155+
"@
156+
157+
[Win32.NativeMethods]::SHChangeNotify(0x8000000, 0x1000, [IntPtr]::Zero, [IntPtr]::Zero)
158+
159+
Write-Host "✅ ExplorerCommand unregistered successfully!" -ForegroundColor Cyan
160+
}

0 commit comments

Comments
 (0)