I'm writing Powershell script for dismounting connected USB disks, after that it would run some PS code and then it should mount the USB drives again.
The Powershell script is for Windows 7 above and for MS Server 2012 above. I think that the best way for mounting ejected USB disks will be through disable/enable USB Mass Storage Device in Device Manager because after that an USB disk can be mounted again. When an USB disk is dismounted from Main Panel then appropriated USB Mass Storage Device in Device Manager will change its state to safe removal (error 47), so according to this "error" the USB disk is easily identifiable.
https://www.thewindowsclub.com/remount-ejected-usb-drive-windows
#Identify connected USB disk/s and dismounting
$usbDrives = #(Get-CimInstance -Class Win32_DiskDrive -Filter 'InterfaceType = "USB"' -KeyOnly | Get-CimAssociatedInstance - ResultClassName Win32_DiskPartition -KeyOnly | Get-CimAssociatedInstance - ResultClassName Win32_LogicalDisk | ForEach-Object{$_.deviceid})
for ($i = 0; $i -le ($usbDrives.length - 1); $i += 1) {
$Eject = New-Object -comObject Shell.Application
$Eject.NameSpace(17).ParseName($usbDrives[$i]).InvokeVerb("Eject")
}
#Some code...
#Getting USB Mass Storage Devices with error 47 and disable/enable
Get-WmiObject Win32_PNPEntity | Where-Object{$_.ConfigManagerErrorCode -eq 47} | Select-Object Name, DeviceID | ForEach-Object {
$_ | Disable-PnpDevice -Confirm:$false;
$_ | Enable-PnpDevice -Confirm:$false;
}
If I use my script for ejecting connected an USB disk, then the USB disk will disconnect, but the USB Mass Storage Device in Device Manager won't change its state to error 47, its state will stay as connected and I can't mount connected USB disks.
Please know somebody, how to write or repair this PS code for dismounting and mounting connected USB disk(s)?
Thank you
To change the state of a USB Mass Storage Device to error 47 programmatically, we need to call the CM_Request_Device_Eject function for a device with the CM_DEVCAP_REMOVABLE bit set in its Capabilities property. We can verify that it's parent of the USB drive itself (e.g. using the Config manager).
To do that from PowerShell in Windows, define and call a native Windows API (see e.g. Example 4 in the Add-Type cmdlet documentation).
First, define the [Usb.Api] type with the following basic functions:
CM_Locate_DevNode - to get the root node of the device tree.
CM_Get_Parent - to get the device ID with the CM_DEVCAP_REMOVABLE bit set (important!).
CM_Request_Device_Eject - the key function allows you to safely remove the device.
Then, use it as follows (in 56496356a.ps1 script I omit the disable/enable part as it requires elevated session, see also 56496356b.ps1 and 56496356c.ps1 scripts in the addendum below)
# 56496356a.ps1
Function Remove-USBSafely {
[CmdletBinding()]
param (
[Parameter(Mandatory,ValueFromPipeline)]
[PSObject]$usbDrive # necessary: a `PNPDeviceID` property
)
Begin {
try { $null = [Usb.Api] }
catch { . D:\PShell\tests\Set-UsbApi.ps1 } # change path to match your circumstances
}
Process {
$DevInst = $DevInstParent = 0
# get the root node of the device tree
$DevNodeA = [Usb.Api]::CM_Locate_DevNode(
[ref]$devinst,$usbDrive.PNPDeviceID,0)
# get the device ID; Capabilities bit CM_DEVCAP_REMOVABLE (Important!)
$DevNodeP = [Usb.Api]::CM_Get_Parent(
[ref]$DevInstParent,$devinst,0)
# safely remove the device
try {
$aux = [Usb.Api]::Eject($DevInstParent)
switch ( $aux ) {
"OK" { Write-Host -ForegroundColor Yellow "Success $aux`: $usbDrive";
return 0 } # device removed
default { Write-Host -ForegroundColor Cyan "Fail $aux`: $usbDrive";
return -1 } # failed: maybe some locked files?
}
}
catch {
Write-Host -ForegroundColor Red "Blocked $usbDrive";
return -5 } # failed: maybe some locked files?
}
}
# Identify connected USB disk/s
$usbDrives = #(
Get-CimInstance Win32_DiskDrive -Filter 'InterfaceType = "USB"' -OutVariable DiskDrive -PipelineVariable Disk |
Get-CimAssociatedInstance -ResultClassName Win32_DiskPartition -OutVariable DiskPartition -PipelineVariable Part |
Get-CimAssociatedInstance -ResultClassName Win32_LogicalDisk -OutVariable DiskLogical |
Select-Object `
#{n='DriveLetter'; e={$_.DeviceID}},
#{n='PNPDeviceID'; e={$Disk.PNPDeviceID}},
# collect the following properties merely for debugging purposes
#{n='VolumeName' ; e={$_.VolumeName}},
#{n='DiskModel' ; e={$Disk.model}},
#{n='Disk' ; e={$Disk.deviceid}},
#{n='Partition' ; e={$Part.name}}
)
$usbDrives | ForEach-Object {
$auxResult = $_ | Remove-USBSafely
if ( $auxResult -ne 0 ) {
### eject the drive anyway ?
# $Eject = New-Object -comObject Shell.Application
# $Eject.NameSpace(17).ParseName($_.DriveLetter).InvokeVerb("Eject")
}
}
#Some code...
The [Usb.Api] type is defined in Set-UsbApi.ps1 script as follows:
Set-StrictMode -Version latest
# D:\PShell\tests\Set-UsbApi.ps1
try { $null = [Usb.Api] }
catch { # C# signature (pinvoke)
$script:UsbApiCode = #"
using System;
using System.Text;
using System.ComponentModel;
using System.Runtime.InteropServices;
namespace Usb
{
public class Api
{
public enum PNP_VETO_TYPE
{
Ok,
TypeUnknown,
LegacyDevice,
PendingClose,
WindowsApp,
WindowsService,
OutstandingOpen,
Device,
Driver,
IllegalDeviceRequest,
InsufficientPower,
NonDisableable,
LegacyDriver,
InsufficientRights
}
[DllImport("setupapi.dll", CharSet = CharSet.Auto)]
static extern int CM_Request_Device_Eject(
IntPtr devinst,
out PNP_VETO_TYPE pVetoType,
System.Text.StringBuilder pszVetoName,
int ulNameLength,
int ulFlags); // 0 (not used)
[DllImport("setupapi.dll", SetLastError=true)]
public static extern int CM_Locate_DevNode(
ref int pdnDevInst,
string pDeviceID,
int ulFlags); // 0..4, 7
[DllImport("setupapi.dll")]
public static extern int CM_Get_Parent(
out UInt32 pdnDevInst,
UInt32 dnDevInst,
int ulFlags); // Not used, must be zero
public static string Eject(int devinst)
{
StringBuilder sb = new StringBuilder(255);
PNP_VETO_TYPE veto;
IntPtr dev = new IntPtr(devinst);
int hr = CM_Request_Device_Eject(dev, out veto, sb, sb.Capacity, 0);
if (hr != 0)
throw new Win32Exception(hr);
return veto.ToString();
}
// JosefZ 2019-06-10
[DllImport("setupapi.dll")]
public static extern int CM_Disable_DevNode(
UInt32 dnDevInst,
int ulFlags); // 0..4, 8, 0xF
[DllImport("setupapi.dll")]
public static extern int CM_Enable_DevNode(
UInt32 dnDevInst,
int ulFlags); // must be 0
/*
The CM_Setup_DevNode function restarts a device instance that is
not running because there is a problem with the device configuration.
*/
[DllImport("setupapi.dll")]
public static extern int CM_Setup_DevNode(
UInt32 dnDevInst,
int ulFlags); // 0 or 4
}
public enum CONFIGRET
{
CR_SUCCESS , // 0x00
CR_DEFAULT , // 0x01
CR_OUT_OF_MEMORY , // 0x02
CR_INVALID_POINTER , // 0x03
CR_INVALID_FLAG , // 0x04
CR_INVALID_DEVNODE , // 0x05 // CR_INVALID_DEVINST
CR_INVALID_RES_DES , // 0x06
CR_INVALID_LOG_CONF , // 0x07
CR_INVALID_ARBITRATOR , // 0x08
CR_INVALID_NODELIST , // 0x09
CR_DEVNODE_HAS_REQS , // 0x0A // CR_DEVINST_HAS_REQS
CR_INVALID_RESOURCEID , // 0x0B
CR_DLVXD_NOT_FOUND , // 0x0C // WIN 95 ONLY
CR_NO_SUCH_DEVNODE , // 0x0D // CR_NO_SUCH_DEVINST
CR_NO_MORE_LOG_CONF , // 0x0E
CR_NO_MORE_RES_DES , // 0x0F
CR_ALREADY_SUCH_DEVNODE , // 0x10 // CR_ALREADY_SUCH_DEVINST
CR_INVALID_RANGE_LIST , // 0x11
CR_INVALID_RANGE , // 0x12
CR_FAILURE , // 0x13
CR_NO_SUCH_LOGICAL_DEV , // 0x14
CR_CREATE_BLOCKED , // 0x15
CR_NOT_SYSTEM_VM , // 0x16 // WIN 95 ONLY
CR_REMOVE_VETOED , // 0x17
CR_APM_VETOED , // 0x18
CR_INVALID_LOAD_TYPE , // 0x19
CR_BUFFER_SMALL , // 0x1A
CR_NO_ARBITRATOR , // 0x1B
CR_NO_REGISTRY_HANDLE , // 0x1C
CR_REGISTRY_ERROR , // 0x1D
CR_INVALID_DEVICE_ID , // 0x1E
CR_INVALID_DATA , // 0x1F
CR_INVALID_API , // 0x20
CR_DEVLOADER_NOT_READY , // 0x21
CR_NEED_RESTART , // 0x22
CR_NO_MORE_HW_PROFILES , // 0x23
CR_DEVICE_NOT_THERE , // 0x24
CR_NO_SUCH_VALUE , // 0x25
CR_WRONG_TYPE , // 0x26
CR_INVALID_PRIORITY , // 0x27
CR_NOT_DISABLEABLE , // 0x28
CR_FREE_RESOURCES , // 0x29
CR_QUERY_VETOED , // 0x2A
CR_CANT_SHARE_IRQ , // 0x2B
CR_NO_DEPENDENT , // 0x2C
CR_SAME_RESOURCES , // 0x2D
CR_NO_SUCH_REGISTRY_KEY , // 0x2E
CR_INVALID_MACHINENAME , // 0x2F // NT ONLY
CR_REMOTE_COMM_FAILURE , // 0x30 // NT ONLY
CR_MACHINE_UNAVAILABLE , // 0x31 // NT ONLY
CR_NO_CM_SERVICES , // 0x32 // NT ONLY
CR_ACCESS_DENIED , // 0x33 // NT ONLY
CR_CALL_NOT_IMPLEMENTED , // 0x34
CR_INVALID_PROPERTY , // 0x35
CR_DEVICE_INTERFACE_ACTIVE , // 0x36
CR_NO_SUCH_DEVICE_INTERFACE , // 0x37
CR_INVALID_REFERENCE_STRING , // 0x38
CR_INVALID_CONFLICT_LIST , // 0x39
CR_INVALID_INDEX , // 0x3A
CR_INVALID_STRUCTURE_SIZE , // 0x3B
NUM_CR_RESULTS , // 0x3C
}
}
"#
Add-Type -TypeDefinition $script:UsbApiCode
Remove-Variable -Name UsbApiCode -Scope script
}
Addendum - unsuccessful tries to get the USB device back to its operational state: the commented scripts:
56496356b.ps1 using the Disable-PnpDevice and Enable-PnpDevice cmdlets
#Requires -RunAsAdministrator
Begin
{
Import-Module PnpDevice
}
Process
{
#Getting USB Mass Storage Devices with error 47 and disable/enable
Get-WmiObject Win32_PNPEntity | Where-Object{$_.ConfigManagerErrorCode -eq 47} |
ForEach-Object {
Write-Host $_.Caption -ForegroundColor Cyan
$_ | Disable-PnpDevice -Confirm:$false; ### #Requires -RunAsAdministrator
Write-Host $_.DeviceID -ForegroundColor Yellow
pause # Device manager shows `Enable Device` in the right click menu
# this means that the device was succesfully disabled
$_ | Enable-PnpDevice -Confirm:$false;
# Device manager shows `Disable Device` in the right click menu
}
}
56496356c.ps1 using the CM_Disable_DevNode, CM_Enable_DevNode and CM_Setup_DevNode functions of the [Usb.Api] class:
#Requires -RunAsAdministrator
begin {
try { $null = [Usb.Api] }
catch { . D:\PShell\tests\Set-UsbApi.ps1 }
Import-Module PnpDevice
}
Process {
#Getting USB Mass Storage Devices with error 47 and disable/enable
Get-WmiObject Win32_PNPEntity | Where-Object{$_.ConfigManagerErrorCode -eq 47} |
ForEach-Object {
Write-Host $_.Caption -ForegroundColor Cyan
$DevInst = 0
[Usb.CONFIGRET]([Usb.Api]::CM_Locate_DevNode( [ref]$DevInst, $_.PNPDeviceID, 0))
[Usb.CONFIGRET]([Usb.Api]::CM_Disable_DevNode( $DevInst, 0)) # using any supported flag
Write-Host $_.DeviceID -ForegroundColor Yellow
# Device manager shows `Disable Device` constantly in the right click menu
# this means that the device was NOT disabled keeping its `Enable` state
# pause
[Usb.CONFIGRET]([Usb.Api]::CM_Enable_DevNode( $DevInst, 0))
# pause
[Usb.CONFIGRET]([Usb.Api]::CM_Setup_DevNode( $DevInst, 8)) # using any supported flag
}
}
Acknowledgment. Great thanks to Александр AKA Kazun for his operational complex script in the Safely remove USB flash (Безопасное извлечение USB Flash in Russian original) article.
Related
I am trying to use Powershell (auditpol) to query the security setting values of the Audit Policy items. So far with all the auditpol commands, I only able to get the subcategories value instead.
auditpol /get /category:*
So far I could only get the list of the 9 items without the success/failure/no auditing values using:
auditpol /list/category
Could there be a command/flag that I might have left out for auditpol or is there any other command for me to retrieve the policies and its relevant security setting values?
Policy and values that I would like to query.
As you've found, auditpol only manages the settings that are in effect when the "Advanced Audit Policy Configuration" feature is enabled.
To query the "classic" audit policy, you will need to use the LSA Policy Win32 API to:
Open the local security policy using LsaOpenPolicy()
Query the audit settings using LsaQueryPolicyInformation()
Translate the results to something readable.
The following example uses Add-Type to compile a C# type that in turn does all of the above:
$AuditPolicyReader = Add-Type -TypeDefinition #'
using System;
using System.Runtime.InteropServices;
using System.Text;
using System.Linq;
using System.Collections.Generic;
public class AuditPolicyReader
{
[Flags()]
public enum AuditPolicySetting
{
Unknown = -1,
None = 0x0,
Success = 0x1,
Failure = 0x2
}
[StructLayout(LayoutKind.Sequential)]
private struct LSA_UNICODE_STRING
{
public UInt16 Length;
public UInt16 MaximumLength;
public IntPtr Buffer;
}
[StructLayout(LayoutKind.Sequential)]
private struct LSA_OBJECT_ATTRIBUTES
{
public int Length;
public IntPtr RootDirectory;
public LSA_UNICODE_STRING ObjectName;
public UInt32 Attributes;
public IntPtr SecurityDescriptor;
public IntPtr SecurityQualityOfService;
}
public struct POLICY_AUDIT_EVENTS_INFO
{
public bool AuditingMode;
public IntPtr EventAuditingOptions;
public Int32 MaximumAuditEventCount;
}
[DllImport("advapi32.dll")]
static extern uint LsaQueryInformationPolicy(IntPtr PolicyHandle, uint InformationClass, out IntPtr Buffer);
[DllImport("advapi32.dll", SetLastError = true, PreserveSig = true)]
static extern uint LsaOpenPolicy(ref LSA_UNICODE_STRING SystemName, ref LSA_OBJECT_ATTRIBUTES ObjectAttributes, uint DesiredAccess, out IntPtr PolicyHandle);
[DllImport("advapi32.dll", SetLastError = true)]
static extern uint LsaClose(IntPtr ObjectHandle);
public static Dictionary<string, AuditPolicySetting> GetClassicAuditPolicy()
{
// Create dictionary to hold the audit policy settings (the key order here is important!!!)
var settings = new Dictionary<string, AuditPolicySetting>
{
{"System", AuditPolicySetting.Unknown},
{"Logon", AuditPolicySetting.Unknown},
{"Object Access", AuditPolicySetting.Unknown},
{"Privilige Use", AuditPolicySetting.Unknown},
{"Detailed Tracking", AuditPolicySetting.Unknown},
{"Policy Change", AuditPolicySetting.Unknown},
{"Account Management", AuditPolicySetting.Unknown},
{"Directory Service Access", AuditPolicySetting.Unknown},
{"Account Logon", AuditPolicySetting.Unknown},
};
// Open local machine security policy
IntPtr polHandle;
LSA_OBJECT_ATTRIBUTES aObjectAttributes = new LSA_OBJECT_ATTRIBUTES();
aObjectAttributes.Length = 0;
aObjectAttributes.RootDirectory = IntPtr.Zero;
aObjectAttributes.Attributes = 0;
aObjectAttributes.SecurityDescriptor = IntPtr.Zero;
aObjectAttributes.SecurityQualityOfService = IntPtr.Zero;
var systemName = new LSA_UNICODE_STRING();
uint desiredAccess = 2; // we only need the audit policy, no need to request anything else
var res = LsaOpenPolicy(ref systemName, ref aObjectAttributes, desiredAccess, out polHandle);
if (res != 0)
{
if(res == 0xC0000022)
{
// Access denied, needs to run as admin
throw new UnauthorizedAccessException("Failed to open LSA policy because of insufficient access rights");
}
throw new Exception(string.Format("Failed to open LSA policy with return code '0x{0:X8}'", res));
}
try
{
// now that we have a valid policy handle, we can query the settings of the audit policy
IntPtr outBuffer;
uint policyType = 2; // this will return information about the audit settings
res = LsaQueryInformationPolicy(polHandle, policyType, out outBuffer);
if (res != 0)
{
throw new Exception(string.Format("Failed to query LSA policy information with '0x{0:X8}'", res));
}
// copy the raw values returned by LsaQueryPolicyInformation() to a local array;
var auditEventsInfo = Marshal.PtrToStructure<POLICY_AUDIT_EVENTS_INFO>(outBuffer);
var values = new int[auditEventsInfo.MaximumAuditEventCount];
Marshal.Copy(auditEventsInfo.EventAuditingOptions, values, 0, auditEventsInfo.MaximumAuditEventCount);
// now we just need to translate the provided values into our settings dictionary
var categoryIndex = settings.Keys.ToArray();
for (int i = 0; i < values.Length; i++)
{
settings[categoryIndex[i]] = (AuditPolicySetting)values[i];
}
return settings;
}
finally
{
// remember to release policy handle
LsaClose(polHandle);
}
}
}
'# -PassThru |Where-Object Name -eq AuditPolicyReader
Now we can call GetClassicAuditPolicy() (remember to run this from an elevated prompt):
PS ~> $AuditPolicyReader::GetClassicAuditPolicy()
Key Value
--- -----
System None
Logon Success, Failure
Object Access None
Privilige Use None
Detailed Tracking None
Policy Change Success
Account Management Success, Failure
Directory Service Access None
Account Logon None
auditpol only returns the Advanced audit policy configuration. These settings can be found in the UI under Security Settings > Advanced Audit Policy Configuration > System Audit Policies
The legacy audit policy your screenshot shows were mostly done away with after Windows Server 2003/Windows Vista. Note the warnings in the policy properties or on the MS compatibility page:
For advanced policies, you can use /r to get a csv-formatted table:
auditpol /get /category:'Account Logon' /r | ConvertFrom-Csv |
Format-Table 'Policy Target',Subcategory,'Inclusion Setting'
Policy Target Subcategory Inclusion Setting
------------- ----------- -----------------
System Kerberos Service Ticket Operations No Auditing
System Other Account Logon Events No Auditing
System Kerberos Authentication Service No Auditing
System Credential Validation No Auditing
For legacy audit policies:
secedit.exe /export /areas SECURITYPOLICY /cfg filename.txt
[Event Audit]
AuditSystemEvents = 0
AuditLogonEvents = 0
AuditObjectAccess = 0
AuditPrivilegeUse = 0
AuditPolicyChange = 0
AuditAccountManage = 0
AuditProcessTracking = 0
AuditDSAccess = 0
AuditAccountLogon = 0
Requires that it hasn't been disabled. Check in the registry:
Get-ItemProperty HKLM:\System\CurrentControlSet\Control\Lsa -Name SCENoApplyLegacyAuditPolicy
Here is a code that gives you a list of all categories and subcategories with their current audit-status. I made it a bit longer than really needed to add the local names of each object. Also see some usage-samples at the end of the code.
# getting the audit policy settings for each subcategory
# works for any OS language
cls
Remove-Variable * -ea 0
$ErrorActionPreference = 'stop'
#requires -runasadmin
$dll = [string]::Join("`r`n", '[DllImport("advapi32.dll")]', 'public static extern bool')
$auditpol = Add-Type -Name 'AuditPol' -Namespace 'Win32' -PassThru -MemberDefinition "
$dll AuditEnumerateCategories(out IntPtr catList, out uint count);
$dll AuditLookupCategoryName(Guid catGuid, out string catName);
$dll AuditEnumerateSubCategories(Guid catGuid, bool all, out IntPtr subList, out uint count);
$dll AuditLookupSubCategoryName(Guid subGuid, out String subName);
$dll AuditQuerySystemPolicy(Guid subGuid, uint count, out IntPtr policy);
$dll AuditFree(IntPtr buffer);"
Add-Type -TypeDefinition "
using System;
public struct AUDIT_POLICY_INFORMATION {
public Guid AuditSubCategoryGuid;
public UInt32 AuditingInformation;
public Guid AuditCategoryGuid;
}"
function getPolicyInfo($sub) {
# get policy info for one subcategory:
$pol = new-object AUDIT_POLICY_INFORMATION
$size = $ms::SizeOf($pol)
$ptr = $ms::AllocHGlobal($size)
$null = $ms::StructureToPtr($pol, $ptr, $false)
$null = $auditpol::AuditQuerySystemPolicy($sub, 1, [ref]$ptr)
$pol = $ms::PtrToStructure($ptr, [type][AUDIT_POLICY_INFORMATION])
$null = $ms::FreeHGlobal($ptr)
[PsCustomObject]#{
category = $pol.AuditCategoryGuid
success = [bool]($pol.AuditingInformation -band 1)
failure = [bool]($pol.AuditingInformation -band 2)
}
}
# (optional) get GUID and local name of all categories:
$ms = [System.Runtime.InteropServices.Marshal]
$count = [uint32]0
$buffer = [IntPtr]::Zero
$size = $ms::SizeOf([type][guid])
$null = $auditpol::AuditEnumerateCategories([ref]$buffer,[ref]$count)
$ptr = [int64]$buffer
$name = [System.Text.StringBuilder]::new()
$catList = #{}
foreach($id in 1..$count) {
$guid = $ms::PtrToStructure([IntPtr]$ptr,[type][guid])
$null = $auditpol::AuditLookupCategoryName($guid,[ref]$name)
$catList[$guid] = $name
$ptr += $size
}
$null = $auditpol::AuditFree($buffer)
# get all subcategories (with optional name):
$guid = [guid]::Empty
$null = $auditpol::AuditEnumerateSubCategories($guid, $true, [ref]$buffer, [ref]$count)
$ptr = [int64]$buffer
$subList = #{}
foreach($id in 1..$count) {
$guid = $ms::PtrToStructure([IntPtr]$ptr,[type][guid])
$null = $auditpol::AuditLookupSubCategoryName($guid,[ref]$name)
$pol = getPolicyInfo $guid
$data = [psCustomObject]#{
category = $catList[$pol.category]
subcategory = $name
success = $pol.success
failure = $pol.failure
}
$subList[$guid.guid] = $data
$ptr += $size
}
$null = $auditpol::AuditFree($buffer)
# listing all subCategories and their audit settings:
$subList.Values | sort category, subcategory | ft -AutoSize
# gettings the audit-settings for a given subcategory-GUID (without '{}'):
$process_creation_guid = '0CCE922B-69AE-11D9-BED3-505054503030'
$subList[$process_creation_guid]
I want a powershell script running as SYSTEM user to display a Windowsform on another users session and have interaction with the controls of it.
I am trying to automate the installation/repair of Symantec Endpoint Protection with Solarwinds N-Able. This platform uses agent software which is installed on clients to monitor and execute tasks on them.
The agent uses the NT AUTHORITY\SYSTEM user to execute tasks on the machine. The installation of SEP works fine so far, but the reboots in between the deinstall/install phases are still uncontrollable as a regular user on the machine. I want the currently active user be able to control this reboot cycles. Something like the Windows update reboot prompt.
My idea is to display a windowsform on logged on user's desktop with controls on it to execute or delay the reboot. My question now is how do I display a windowsform defined in powershell on another user's session, and how am I going to get the actions of the controls back in the script that is running on the SYSTEM user.
I've already tried the msg command to send a message to all the users on the system. But this is only one-way communication and isn't really meant to be used in situations like this is guess.
I found the solution for my problem. I used the WTSSendMessage function which boxdog suggested in the comments. I combined this with a script that gets the sessionID's of the logged on users. I minimized this script to only get the "Active" user's sessionID. This is then used to send the message to user. I tested it in Solarwinds and so far this works flawless.
My coding skills are pretty basic, but this is the end result.
function Send-MessageBox
{
[CmdletBinding()]
[OutputType([string])]
Param
(
[Parameter(Mandatory=$true, Position=0)]
[string]$title,
[Parameter(Mandatory=$true, Position=1)]
[string]$message,
[Parameter(Mandatory=$true, Position=2)]
[int]$duration,
[Parameter(Mandatory=$true, Position=3)]
[int]$style
)
Begin
{
$typeDefinition = #"
using System;
using System.Runtime.InteropServices;
public class WTSMessage {
[DllImport("wtsapi32.dll", SetLastError = true)]
public static extern bool WTSSendMessage(
IntPtr hServer,
[MarshalAs(UnmanagedType.I4)] int SessionId,
String pTitle,
[MarshalAs(UnmanagedType.U4)] int TitleLength,
String pMessage,
[MarshalAs(UnmanagedType.U4)] int MessageLength,
[MarshalAs(UnmanagedType.U4)] int Style,
[MarshalAs(UnmanagedType.U4)] int Timeout,
[MarshalAs(UnmanagedType.U4)] out int pResponse,
bool bWait
);
static int response = 0;
public static int SendMessage(int SessionID, String Title, String Message, int Timeout, int MessageBoxType) {
WTSSendMessage(IntPtr.Zero, SessionID, Title, Title.Length, Message, Message.Length, MessageBoxType, Timeout, out response, true);
return response;
}
}
"#
}
Process
{
if (-not ([System.Management.Automation.PSTypeName]'WTSMessage').Type)
{
Add-Type -TypeDefinition $typeDefinition
}
$RawOuput = (quser) -replace '\s{2,}', ',' | ConvertFrom-Csv
$sessionID = $null
Foreach ($session in $RawOuput) {
if(($session.sessionname -notlike "console") -AND ($session.sessionname -notlike "rdp-tcp*")) {
if($session.ID -eq "Active"){
$sessionID = $session.SESSIONNAME
}
}else{
if($session.STATE -eq "Active"){
$sessionID = $session.ID
}
}
}
$response = [WTSMessage]::SendMessage($sessionID, $title, $message, $duration, $style )
}
End
{
Return $response
}
}
Send-MessageBox -title "Title" -message "Message" -duration 60 -style 0x00001034L
I am trying to change the Windows Console Mode for output (CONOUT$) using the Windows API and SetConsoleMode calls. So I modified a PowerShell script, based on ConinMode.ps1 (and which works for input), to do this. Reading works fine with both the ConoutMode.exe and my script, and returns:
# .\ConoutMode.exe
mode: 0x3
ENABLE_PROCESSED_OUTPUT 0x0001 ON
ENABLE_WRAP_AT_EOL_OUTPUT 0x0002 ON
ENABLE_VIRTUAL_TERMINAL_PROCESSING 0x0004 off <====
DISABLE_NEWLINE_AUTO_RETURN 0x0008 off
ENABLE_LVB_GRID_WORLDWIDE 0x0010 off
# .\ConoutMode.ps1
OK! GetConsoleWindow handle : 0x1F06D6
Console Input Mode (CONIN) : 0x21f
Console Output Mode (CONOUT) : 0x3
However, both my script and the exe fails in writing the mode. (Possibly because it thinks that my output handle is not pointing to a console?)
In PowerShell:
# .\ConoutMode.exe set 0x000f
error: SetConsoleMode failed (is stdout a console?)
# .\ConoutMode.ps1 -Mode 7
OK! GetConsoleWindow handle : 0x1F06D6
old (out) mode: 0x3
SetConsoleMode (out) failed (is stdout a console?)
At C:\cygwin64\home\emix\win_esc_test\ConoutMode.ps1:112 char:9
+ throw "SetConsoleMode (out) failed (is stdout a console?)"
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : OperationStopped: (SetConsoleMode ...out a console?):String) [], RuntimeException
+ FullyQualifiedErrorId : SetConsoleMode (out) failed (is stdout a console?)
Here is my ConoutMode.ps1 script in it's entirety:
param (
[int] $Mode
)
$signature = #'
[DllImport("kernel32.dll", SetLastError=true)]
public static extern IntPtr GetConsoleWindow();
[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr GetStdHandle(int nStdHandle);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern uint GetConsoleMode(IntPtr hConsoleHandle, out uint lpMode);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern uint SetConsoleMode(IntPtr hConsoleHandle, uint dwMode);
public const int STD_INPUT_HANDLE = -10;
public const int STD_OUTPUT_HANDLE = -11;
public const int ENABLE_PROCESSED_OUTPUT = 0x0001;
public const int ENABLE_WRAP_AT_EOL_OUTPUT = 0x0002;
public const int ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004;
public const int DISABLE_NEWLINE_AUTO_RETURN = 0x0008;
public const int ENABLE_LVB_GRID_WORLDWIDE = 0x0010;
'#
#----------------------------------------------------------
$WinAPI = Add-Type -MemberDefinition $signature -Name WinAPI -Namespace ConoutMode -PassThru
#$WinAPI = Add-Type -MemberDefinition $signature -Name WinAPI -Namespace IdentifyConsoleWindow -PassThru
$hwnd = $WinAPI::GetConsoleWindow()
if ($hwnd -eq 0) {
throw "Error: GetConsoleWindow returned NULL."
}
echo "OK! GetConsoleWindow handle : 0x$($hwnd.ToString("X"))"
function GetConIn {
$ret = $WinAPI::GetStdHandle($WinAPI::STD_INPUT_HANDLE)
if ($ret -eq -1) {
throw "Error: cannot get stdin handle"
}
return $ret
}
function GetConOut {
$ret = $WinAPI::GetStdHandle($WinAPI::STD_OUTPUT_HANDLE)
if ($ret -eq -1) {
throw "Error: cannot get stdout handle"
}
return $ret
}
function GetConInMode { #GetCOnsoleMode
$conin = GetConIn
$mode = 0
$ret = $WinAPI::GetConsoleMode($conin, [ref]$mode)
if ($ret -eq 0) {
throw "GetConsoleMode (in) failed (is stdin a console?)"
}
return $mode
}
function GetConOutMode {
$conout = GetConOut
$mode = 0
$ret = $WinAPI::GetConsoleMode($conout, [ref]$mode)
if ($ret -eq 0) {
throw "GetConsoleMode (out) failed (is stdout a console?)"
}
return $mode
}
function SetConInMode($mode) {
$conin = GetConIn
$ret = $WinAPI::SetConsoleMode($conin, $mode)
if ($ret -eq 0) {
throw "SetConsoleMode (in) failed (is stdin a console?)"
}
}
function SetConOutMode($mode) {
#$conout = GetConOut
# Different tack:
$conout = $hwnd
$ret = $WinAPI::SetConsoleMode($conout, $mode)
if ($ret -eq 0) {
throw "SetConsoleMode (out) failed (is stdout a console?)"
}
}
#----------------------------------------------------------
# MAIN
#----------------------------------------------------------
$oldInMode = GetConInMode
$oldOutMode = GetConOutMode
$newMode = $oldOutMode
$doit = $false
if ($PSBoundParameters.ContainsKey("Mode")) {
$newMode = $Mode
$doit = $true
}
if ($doit) {
echo "old (out) mode: 0x$($oldOutMode.ToString("x"))"
SetConOutMode $newMode
$newMode = GetConOutMode
echo "new (out) mode: 0x$($newMode.ToString("x"))"
} else {
echo "Console Input Mode (CONIN) : 0x$($oldInMode.ToString("x"))"
echo "Console Output Mode (CONOUT) : 0x$($oldOutMode.ToString("x"))"
}
So at the end of the day, I want to enable ENABLE_VIRTUAL_TERMINAL_PROCESSING for the Console.
The system I am using for this is:
---------------------------------------------------------
PowerShell Version : 6.1.1
OS Name : Microsoft Windows 8.1 (64-bit)
OS Version : 6.3.9600 [2018-11-16 00:50:01]
OS BuildLabEx : 9600.19179
OS HAL : 6.3.9600.18969
OS Kernel : 6.3.9600.18217
OS UBR : 19182
-------------------------------------------------------
with Privilege : Administrator
-------------------------------------------------------
ExecutionPolicy :
MachinePolicy : Undefined
UserPolicy : Undefined
Process : Undefined
CurrentUser : Undefined
LocalMachine : Bypass
Console Settings:
Type : ConsoleHost
OutputEncoding : Unicode (UTF-8)
Color Capability : 23
Registry VT Level : 1
CodePage (input) : 437
CodePage (output) : 437
I have also enabled the registry item: HKCU:\Console\VirtualTerminalLevel, as instructed elsewhere.
Q: How can I enable this bit/flag using PowerShell?
Some things I can think of that may be wrong:
I surely have the wrong understanding of how this works...
I might have the wrong permissions to write to console? (How to set?)
I might not write to the correct output buffer? (How to find?)
Note that setting the output mode of one screen buffer does not affect the output mode of other screen buffers.
When executing a script, perhaps a new output buffer is created? (What to do?)
I might have to create a new buffer?
I might have to create a new Console?
Related Questions and Links:
ENABLE_VIRTUAL_TERMINAL_PROCESSING and DISABLE_NEWLINE_AUTO_RETURN failing
MSDN: Inside the Windows Console
MSDN: Introducing the Windows Pseudo Console - ConPty
The code is using "GetStdHandle" to get stdin or stdout. And those might be console handles - but it's dependent on how the process is created. If you're launched from a console, and the stdin/stdout aren't redirected, then you'd probably get a console handle. But it's not guaranteed.
Rather than asking for stdin/stdout, just open the console handles directly - you can "CreateFile" for CONIN$ and CONOUT$. The documentation for CreateFile has a whole section on Consoles.
Update: I made a Gist to demonstrate how to use CreateFile to open CONIN$ and CONOUT$ from PowerShell.
I have been experimenting with this myself for a few days and have come to realize a few things. Apparently PowerShell enables Virtual Terminal Processing when it starts up. It disables it again, however, when it launches an executable and then reenables it when the executable exits. This means that whatever executable needs Virtual Terminal Processing must enable it itself or be called through a wrapper program that enables it and then pipes stdin/stdout/stderr through to the terminal. As for how would even go about writing such a program, I don't know.
The following code does not return "Y" as expected. Only in the next session (another new window) it works? I would expect it to be available immediately?
[Environment]::SetEnvironmentVariable("X", "Y", "Machine")
Write-Host $env:X
You must do this since the process gets env vars on start, not while running (i.e. you would have to restart shell for this to work your way):
[Environment]::SetEnvironmentVariable("X", "Y", "Machine")
$Env:X = "Y"
There is also a way to broadcast this to other windows using WM_SETTINGCHANGE
To effect a change in the environment variables for the system or the
user, broadcast this message with lParam set to the string
"Environment".)
# Notify system of change via WM_SETTINGCHANGE
if (! ("Win32.NativeMethods" -as [Type]))
{
Add-Type -Namespace Win32 -Name NativeMethods -MemberDefinition #"
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
public static extern IntPtr SendMessageTimeout( IntPtr hWnd, uint Msg, UIntPtr wParam, string lParam, uint fuFlags, uint uTimeout, out UIntPtr lpdwResult);
"#
}
$HWND_BROADCAST = [IntPtr] 0xffff; $WM_SETTINGCHANGE = 0x1a; $result = [UIntPtr]::Zero
[Win32.Nativemethods]::SendMessageTimeout($HWND_BROADCAST, $WM_SETTINGCHANGE, [UIntPtr]::Zero, "Environment", 2, 5000, [ref] $result) | out-null
}
As far as I know, a process loads the environment variables only once (at start). But you can change it using:
[Environment]::SetEnvironmentVariable("X", "Y", "Process") # for the current session
Note: You probably want to set both:
[Environment]::SetEnvironmentVariable("X", "Y", "Machine")
[Environment]::SetEnvironmentVariable("X", "Y", "Process")
Any ideas and suggestions on why this works when run from w/in PS, but not when run from a shortcut defined as:
%SystemRoot%\system32\WindowsPowerShell\v1.0\powershell.exe -File "C:\Users\bin\ChangeDesktop.ps1"
Contents of ChangeDesktop.ps1:
set-itemproperty -path "HKCU:Control Panel\Desktop" -name WallPaper -value ""
rundll32.exe user32.dll, UpdatePerUserSystemParameters
If I am in the PS "command prompt" environment the desktop background is automatically removed and refreshed, outside of that I have to manually refresh the desktop to effect the change.
System is Windows Server 2008 R2 - fresh install. Script executionpolicy is set to RemoteSigned, and I don't see any PS errors. I Just don't see the desktop refresh automatically when running from a desktop shortcut.
scratches head
rundll32.exe user32.dll, UpdatePerUserSystemParameters didn't actually change the wallpaper for me on a 2008 x64 box. This does did though... It calls the Win32 API to invoke changing the wallpaper. If you save this as your ChangeDesktop.ps1 script it should work. As it is below it will remove any desktop wallpaper. However if you do want to set one you can edit the last line with the path of a supported image file like this:
[Wallpaper.Setter]::SetWallpaper( 'C:\Wallpaper.bmp', 0 )
The second argument is for the styling:
0: Tile
1: Center
2: Stretch
3: No Change
The script:
Add-Type #"
using System;
using System.Runtime.InteropServices;
using Microsoft.Win32;
namespace Wallpaper
{
public enum Style : int
{
Tile, Center, Stretch, NoChange
}
public class Setter {
public const int SetDesktopWallpaper = 20;
public const int UpdateIniFile = 0x01;
public const int SendWinIniChange = 0x02;
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
private static extern int SystemParametersInfo (int uAction, int uParam, string lpvParam, int fuWinIni);
public static void SetWallpaper ( string path, Wallpaper.Style style ) {
SystemParametersInfo( SetDesktopWallpaper, 0, path, UpdateIniFile | SendWinIniChange );
RegistryKey key = Registry.CurrentUser.OpenSubKey("Control Panel\\Desktop", true);
switch( style )
{
case Style.Stretch :
key.SetValue(#"WallpaperStyle", "2") ;
key.SetValue(#"TileWallpaper", "0") ;
break;
case Style.Center :
key.SetValue(#"WallpaperStyle", "1") ;
key.SetValue(#"TileWallpaper", "0") ;
break;
case Style.Tile :
key.SetValue(#"WallpaperStyle", "1") ;
key.SetValue(#"TileWallpaper", "1") ;
break;
case Style.NoChange :
break;
}
key.Close();
}
}
}
"#
[Wallpaper.Setter]::SetWallpaper( '', 0 )
Originally from PoshCode: http://poshcode.org/491
This might sounf weird, but what worked for me was using single quotes instead of double. So it would look like this:
Set-ItemProperty -path "HKCU:Control Panel\Desktop" -name 'wallpaper' -value 'some value'
rundll32.exe user32.dll, UpdatePerUserSystemParameters
This script works wonders. For a domain deployment, we didn't want it to continuously change the background each time a user logs in.
I made the following changes so that it checks to see if the background exists on the computer in the desired location, if it does exist then to exit, if it does not to go ahead with the file copy and set the background.
it first maps the hidden share, copies the file to the desired directory, sets the wallpaper and then disconnects the hidden share. if "X" is already used by your company insert another drive letter. :D
$strFileName="C:\Users\Public\Pictures\background.jpg"
If (Test-Path $strFileName){
# // File exists
Exit-PSSession
}Else{
# // File does not exist
New-PSDrive -Name X -PSProvider Filesystem -Root \\hiddenfileshare\wallpapers
Copy-Item X:\background.jpg C:\Users\Public\Pictures
[Wallpaper.Setter]::SetWallpaper( 'C:\Users\Public\Pictures\background.jpg', 0 )
Remove-PSDrive X
}
The script provided by Andy Arismendi is awesome!
I've used it to make a fun project - set a random wallpaper scraped from the net.
I'm posting it here for anyone interested. Before using it you need to change a few constants at the top of the script source. You also need to download the HtmlAgilityPack.dll library (there are instructions in the script comments).
Enjoy!
P.S. If the wallpaper site I'm using goes down or changes its layout the scraping in the script will go to hell, but nevertheless with my script as an example I bet you'll be able to build another wallpaper scraper.
############## CONSTANTS ##############
# add the library for parsing html - HtmlAgilityPack - download it with nuget from https://www.nuget.org/packages/HtmlAgilityPack
# download nuget command line from https://dist.nuget.org/index.html and install HtmlAgilityPack with "nuget install HtmlAgilityPack" from the cmd
# enter the path to HtmlAgilityPack.dll library used for html parsing
$html_parser_path = "C:\Users\username\Documents\htmlagilitypack\HtmlAgilityPack.1.4.9.5\lib\Net20\HtmlAgilityPack.dll"
# choose where your wallpapers will be downloaded
$wallpaper_dir_path = "C:\Users\username\Pictures\"
# get random wallpaper category from wallpaperscraft.com - the ones below are my favourite categories, edit it if you want to get other categories
<#
you can choose your favorite wallpaper categories from the list below
3D
Abstract
Animals
Anime
Brands
Cars
City
Fantasy
Flowers
Food
Games
Girls
Hi-Tech
Holidays
Macro
Men
Movies
Music
Nature
Other
Space
Sport
Textures
TV Series
Vector
#>
$categories = #("animals","flowers","macro","nature","space")
# I download my wallpapers from the site below - real quality wallpapers
# don't forget to change your resolution - I'm using a 1920x1080 monitor
<#
A list of resolutions to choose from:
1600x1200
1400x1050
1280x1024
1280x960
1152x864
1024x768
3840x2400
3840x2160
3840x1200
2560x1600
2560x1440
2560x1080
2560x1024
2048x1152
1920x1200
1920x1080
1680x1050
1600x900
1440x900
1280x800
1280x720
2160x3840
1440x2560
1366x768
1080x1920
1024x600
960x544
800x1280
800x600
720x1280
540x960
480x854
480x800
400x480
360x640
320x480
320x240
240x400
240x320
2732x2732
2048x2048
1080x1920
1024x1024
750x1334
640x1136
640x960
320x480
1366x768
1920x1080
360x640
1024x768
1600x900
1280x900
1440x900
1280x1024
800x600
1680x1050
2560x1440
320x480
1920x1200
480x800
720x1280
#>
$resolution = "1920x1080" # default resolution
$url = "https://wallpaperscraft.com/catalog/category/$resolution" # category is a placeholder
############## END OF CONSTANT DECLARATIONS ##############
Add-Type #"
using System;
using System.Runtime.InteropServices;
using Microsoft.Win32;
namespace Wallpaper
{
public enum Style : int
{
Tile, Center, Stretch, NoChange
}
public class Setter {
public const int SetDesktopWallpaper = 20;
public const int UpdateIniFile = 0x01;
public const int SendWinIniChange = 0x02;
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
private static extern int SystemParametersInfo (int uAction, int uParam, string lpvParam, int fuWinIni);
public static void SetWallpaper ( string path, Wallpaper.Style style ) {
SystemParametersInfo( SetDesktopWallpaper, 0, path, UpdateIniFile | SendWinIniChange );
RegistryKey key = Registry.CurrentUser.OpenSubKey("Control Panel\\Desktop", true);
switch( style )
{
case Style.Stretch :
key.SetValue(#"WallpaperStyle", "2") ;
key.SetValue(#"TileWallpaper", "0") ;
break;
case Style.Center :
key.SetValue(#"WallpaperStyle", "1") ;
key.SetValue(#"TileWallpaper", "0") ;
break;
case Style.Tile :
key.SetValue(#"WallpaperStyle", "1") ;
key.SetValue(#"TileWallpaper", "1") ;
break;
case Style.NoChange :
break;
}
key.Close();
}
}
}
"#
Add-Type -Path $html_parser_path
$rand_index = Get-Random -minimum 0 -maximum $categories.Length
$random_category = $categories[$rand_index]
# replace the placeholder "category" with the random category chosen above
$url = $url -replace "category", $random_category
$doc = New-Object HtmlAgilityPack.HtmlDocument
$doc.LoadHtml((New-Object System.Net.WebClient).DownloadString($url))
# NOTE: the html parser I'm using locates elements by XPath only
$page_links = $doc.DocumentNode.SelectSingleNode("//div[contains(#class, 'pages')]").SelectNodes("a")
# get last page link
$last_page_link = $page_links[$page_links.Count - 1].GetAttributeValue("href", "")
# get last page number
$last_page_number = [regex]::match($last_page_link,'.*page(\d+)').Groups[1].Value
$random_page_number = Get-Random -minimum 0 -maximum $last_page_number
$random_page_addr = ""
# page 1 doesn't add anything to the url
if ($random_page_number -gt 0){
$random_page_addr = "/page$random_page_number"
}
$doc.LoadHtml((New-Object System.Net.WebClient).DownloadString("$url$random_page_addr"))
# get wallpaper divs
$wallpaper_divs = $doc.DocumentNode.SelectNodes("//div[contains(#class, 'wallpaper_pre')]")
$random_wallpaper_div = Get-Random -minimum 0 -maximum 15 # there are 15 wallpapers on a page
# get a sample wallpaper link which has to be substituted later
$sample_wallpaper_link = $wallpaper_divs[$random_wallpaper_div].SelectNodes("a")[0].GetAttributeValue("href", "")
# substitute the above link to get the image link itself
$sample_wallpaper_link = $sample_wallpaper_link -replace "download", "image"
$sample_wallpaper_link = $sample_wallpaper_link -replace "/$resolution", "_$resolution.jpg"
$sample_wallpaper_link = $sample_wallpaper_link -replace "//", "https://"
$wallpaper_image_name = [regex]::match($sample_wallpaper_link,'.*image/(\w+)').Groups[1].Value
$wallpaper_image_name = "$wallpaper_image_name.jpg"
$wc = New-Object System.Net.WebClient
$save_location = "$wallpaper_dir_path$wallpaper_image_name"
$wc.DownloadFile($sample_wallpaper_link, "$save_location")
[Wallpaper.Setter]::SetWallpaper($save_location, 1 )