win: improve soft file/app delete security #260

This commit improves soft file delete logic:

- Unify logic for soft deleting single files and system apps.
- Rename `RenameSystemFile` templating function to `SoftDeleteFiles` so
  new name gives clarity to:
   - It's not necessarily single file being renamed but can be multiple
     files.
   - It's not necessarily system files being renamed, but can also work
     without granting extra permissions.
- Grant permissions for only files that will be backed up, skipping
  unnecessarily granting permissions to folders/other files. Both
  `SeRestorePrivilege` and `SeTakeownershipPrivileges` are claimed and
  revoked as necessary.
- Make granting permissions optional through `grantPermissions`
  parameter. Do not take permissions if not needed.
- Restore permissions to system default after file is renamed. Before
  both deletion of system apps and renaming system files did not restore
  their original permissions. This might leave user computers
  vulnerable, which is fixed in this commit. It ensures that the
  system's original security posture is preserved.
- Deleting system apps is now independent of `Get-AppxPackage`,
  improving its robustness and enabling their execution once system apps
  are hard-deleted (#260)
- Introduce common way to share glob iteration logic of how the
  directories are being cleaned up. It reuses most of the logic from
  former `DeleteGlob` with some improvements:
  - Simplify call to `Get-ChildItem` by avoiding `-Filter` parameter.
  - Improve reliability of getting parent directory in `DeleteGlob`
    sanity check to use .NET's `[System.IO.Path]` methods.
This commit is contained in:
undergroundwires
2023-10-26 18:35:39 +02:00
parent 80821fca07
commit f4a74f058d

View File

@@ -414,7 +414,7 @@ actions:
function: ClearDirectoryContents
parameters:
directoryGlob: '%USERPROFILE%\Local Settings\Temporary Internet Files'
grantPermissions: true # 🔒️ On Windows 10, this folder (Local Settings) is protected 🔓️ On Windows 11 it's not
grantPermissions: true # 🔒️ Protected on Windows 10 since 22H2 | 📂 Unprotected on Windows 11 since 22H2
-
function: ClearDirectoryContents
parameters:
@@ -426,7 +426,7 @@ actions:
# - C:\Users\undergroundwires\AppData\Local\Microsoft\Windows\Temporary Internet Files\Virtualized
# Since Windows 10 22H2 and Windows 11 22H2, data files are observed in this subdirectories but not on the parent.
# Especially in `IE` folder includes many files. These folders are protected and hidden by default.
grantPermissions: true # 🔒️ This folder is protected on both on Windows 10 and 11
grantPermissions: true # 🔒️ Protected on Windows 10 since 22H2 | 🔒️ Protected on Windows 11 since 22H2
-
function: ClearDirectoryContents
parameters:
@@ -435,7 +435,7 @@ actions:
function: ClearDirectoryContents
parameters:
directoryGlob: '%LOCALAPPDATA%\Temporary Internet Files'
grantPermissions: true # 🔒️ This folder is protected on both on Windows 10 and 11
grantPermissions: true # 🔒️ Protected on Windows 10 since 22H2 | 🔒️ Protected on Windows 11 since 22H2
-
name: Clear Internet Explorer feeds cache
recommend: standard
@@ -4017,9 +4017,10 @@ actions:
serviceName: mpsdrv # Check: (Get-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Services\mpsdrv").Start
defaultStartupMode: Manual # Alowed values: Boot | System | Automatic | Manual
-
function: RenameSystemFile
function: SoftDeleteFiles
parameters:
filePath: '%SystemRoot%\System32\drivers\mpsdrv.sys'
fileGlob: '%SYSTEMROOT%\System32\drivers\mpsdrv.sys'
grantPermissions: true # 🔒️ Protected on Windows 10 since 22H2 | 🔒️ Protected on Windows 11 since 22H2
-
name: Disable "Windows Defender Firewall" service
docs:
@@ -4054,9 +4055,10 @@ actions:
serviceName: MpsSvc # Check: (Get-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Services\MpsSvc").Start
defaultStartupMode: Automatic # Alowed values: Boot | System | Automatic | Manual
-
function: RenameSystemFile
function: SoftDeleteFiles
parameters:
filePath: '%WinDir%\system32\mpssvc.dll'
fileGlob: '%WINDIR%\System32\mpssvc.dll'
grantPermissions: true # 🔒️ Protected on Windows 10 since 22H2 | 🔒️ Protected on Windows 11 since 22H2
-
name: Disable firewall via command-line utility
# ❗️ Following must be enabled and in running state:
@@ -5634,10 +5636,11 @@ actions:
parameters:
code: sc stop "WinDefend" >nul 2>&1 & reg add "HKLM\SYSTEM\CurrentControlSet\Services\WinDefend" /v "Start" /t REG_DWORD /d "4" /f
revertCode: reg add "HKLM\SYSTEM\CurrentControlSet\Services\WinDefend" /v "Start" /t REG_DWORD /d "2" /f & sc start "WinDefend" >nul 2>&1
# - # "Access is denied" when renaming file
# function: RenameSystemFile
# - # "Access is denied" when renaming file, cannot grant permissions (Attempted to perform an unauthorized operation) since Windows 10 22H2 and Windows 11 22H2
# function: SoftDeleteFiles
# parameters:
# filePath: '%ProgramFiles%\Windows Defender\MsMpEng.exe' # Found also in C:\ProgramData\Microsoft\Windows Defender\Platform\4.18.2107.4-0 and \4.18.2103.7-0 ...
# fileGlob: '%PROGRAMFILES%\Windows Defender\MsMpEng.exe' # Found also in C:\ProgramData\Microsoft\Windows Defender\Platform\4.18.2107.4-0 and \4.18.2103.7-0 ...
# grantPermissions: true # 🔒️ Protected on Windows 10 since 22H2 | 🔒️ Protected on Windows 11 since 22H2
-
category: Disable Defender kernel-level drivers
children:
@@ -5646,6 +5649,8 @@ actions:
name: Disable "Microsoft Defender Antivirus Network Inspection System Driver" service
docs: http://batcmd.com/windows/10/services/wdnisdrv/
call:
# Excluding:
# - `%SYSTEMROOT%\System32\drivers\wd\WdNisDrv.sys`: Missing on Windows since Windows 10 22H2 and Windows 11 22H2
-
function: RunInlineCodeAsTrustedInstaller
parameters:
@@ -5653,49 +5658,44 @@ actions:
code: net stop "WdNisDrv" /yes >nul & reg add "HKLM\SYSTEM\CurrentControlSet\Services\WdNisDrv" /v "Start" /t REG_DWORD /d "4" /f
revertCode: reg add "HKLM\SYSTEM\CurrentControlSet\Services\WdNisDrv" /v "Start" /t REG_DWORD /d "3" /f & sc start "WdNisDrv" >nul
-
function: RenameSystemFile
function: SoftDeleteFiles
parameters:
filePath: '%SystemRoot%\System32\drivers\WdNisDrv.sys'
# - # "Access is denied" when renaming file
# function: RenameSystemFile
# parameters:
# filePath: '%SystemRoot%\System32\drivers\wd\WdNisDrv.sys'
fileGlob: '%SYSTEMROOT%\System32\drivers\WdNisDrv.sys'
grantPermissions: true # 🔒️ Protected on Windows 10 since 22H2 | 🔒️ Protected on Windows 11 since 22H2
-
name: Disable "Microsoft Defender Antivirus Mini-Filter Driver" service
docs:
- https://www.n4r1b.com/posts/2020/01/dissecting-the-windows-defender-driver-wdfilter-part-1/
- http://batcmd.com/windows/10/services/wdfilter/
call:
# Excluding:
# - `%SYSTEMROOT%\System32\drivers\wd\WdFilter.sys`: Missing on Windows since Windows 10 22H2 and Windows 11 22H2
-
function: RunInlineCodeAsTrustedInstaller
parameters:
code: sc stop "WdFilter" >nul & reg add "HKLM\SYSTEM\CurrentControlSet\Services\WdFilter" /v "Start" /t REG_DWORD /d "4" /f
revertCode: reg add "HKLM\SYSTEM\CurrentControlSet\Services\WdFilter" /v "Start" /t REG_DWORD /d "0" /f & sc start "WdFilter" >nul
-
function: RenameSystemFile
function: SoftDeleteFiles
parameters:
filePath: '%SystemRoot%\System32\drivers\WdFilter.sys'
# - # "Access is denied" when renaming file
# function: RenameSystemFile
# parameters:
# filePath: '%SystemRoot%\System32\drivers\wd\WdFilter.sys'
fileGlob: '%SYSTEMROOT%\System32\drivers\WdFilter.sys'
grantPermissions: true # 🔒️ Protected on Windows 10 since 22H2 | 🔒️ Protected on Windows 11 since 22H2
-
name: Disable "Microsoft Defender Antivirus Boot Driver" service
docs: http://batcmd.com/windows/10/services/wdboot/
call:
# Excluding:
# - `%SYSTEMROOT%\System32\drivers\wd\WdBoot.sys`: Missing on Windows since Windows 10 22H2 and Windows 11 22H2
-
function: RunInlineCodeAsTrustedInstaller
parameters:
code: sc stop "WdBoot" >nul 2>&1 & reg add "HKLM\SYSTEM\CurrentControlSet\Services\WdBoot" /v "Start" /t REG_DWORD /d "4" /f
revertCode: reg add "HKLM\SYSTEM\CurrentControlSet\Services\WdBoot" /v "Start" /t REG_DWORD /d "0" /f & sc start "WdBoot" >nul 2>&1
-
function: RenameSystemFile
function: SoftDeleteFiles
parameters:
filePath: '%SystemRoot%\System32\drivers\WdBoot.sys'
# - # "Access is denied" when renaming file
# function: RenameSystemFile
# parameters:
# filePath: '%SystemRoot%\System32\drivers\wd\WdBoot.sys'
fileGlob: '%SYSTEMROOT%\System32\drivers\WdBoot.sys'
grantPermissions: true # 🔒️ Protected on Windows 10 since 22H2 | 🔒️ Protected on Windows 11 since 22H2
-
name: Disable "Microsoft Defender Antivirus Network Inspection" service
docs:
@@ -5707,10 +5707,11 @@ actions:
parameters:
code: sc stop "WdNisSvc" >nul 2>&1 & reg add "HKLM\SYSTEM\CurrentControlSet\Services\WdNisSvc" /v "Start" /t REG_DWORD /d "4" /f
revertCode: reg add "HKLM\SYSTEM\CurrentControlSet\Services\WdNisSvc" /v "Start" /t REG_DWORD /d "2" /f & sc start "WdNisSvc" >nul 2>&1
# - # "Access is denied" when renaming file
# function: RenameSystemFile
# - # "Access is denied" when renaming file, cannot grant permissions (Attempted to perform an unauthorized operation) since Windows 10 22H2 and Windows 11 22H2
# function: SoftDeleteFiles
# parameters:
# filePath: '%ProgramFiles%\Windows Defender\NisSrv.exe' # Found also in C:\ProgramData\Microsoft\Windows Defender\Platform\4.18.2107.4-0 and \4.18.2103.7-0 ...
# fileGlob: '%PROGRAMFILES%\Windows Defender\NisSrv.exe' # Found also in C:\ProgramData\Microsoft\Windows Defender\Platform\4.18.2107.4-0 and \4.18.2103.7-0 ...
# grantPermissions: true # 🔒️ Protected on Windows 10 since 22H2 | 🔒️ Protected on Windows 11 since 22H2
-
name: Disable "Windows Defender Advanced Threat Protection Service" service
docs: http://batcmd.com/windows/10/services/sense/
@@ -5721,9 +5722,10 @@ actions:
code: sc stop "Sense" >nul 2>&1 & reg add "HKLM\SYSTEM\CurrentControlSet\Services\Sense" /v "Start" /t REG_DWORD /d "4" /f
revertCode: reg add "HKLM\SYSTEM\CurrentControlSet\Services\Sense" /v "Start" /t REG_DWORD /d "3" /f & sc start "Sense" >nul 2>&1 # Alowed values: Boot | System | Automatic | Manual
-
function: RenameSystemFile
function: SoftDeleteFiles
parameters:
filePath: '%ProgramFiles%\Windows Defender Advanced Threat Protection\MsSense.exe'
fileGlob: '%PROGRAMFILES%\Windows Defender Advanced Threat Protection\MsSense.exe'
grantPermissions: true # 🔒️ Protected on Windows 10 since 22H2 | 🔒️ Protected on Windows 11 since 22H2
-
name: Disable "Windows Security Service" service
docs: |-
@@ -5755,9 +5757,10 @@ actions:
code: sc stop "SecurityHealthService" >nul 2>&1 & reg add "HKLM\SYSTEM\CurrentControlSet\Services\SecurityHealthService" /v Start /t REG_DWORD /d 4 /f
revertCode: reg add "HKLM\SYSTEM\CurrentControlSet\Services\SecurityHealthService" /v Start /t REG_DWORD /d 3 /f & sc start "SecurityHealthService" >nul 2>&1
-
function: RenameSystemFile
function: SoftDeleteFiles
parameters:
filePath: '%WinDir%\system32\SecurityHealthService.exe'
fileGlob: '%WINDIR%\System32\SecurityHealthService.exe'
grantPermissions: true # 🔒️ Protected on Windows 10 since 22H2 | 🔒️ Protected on Windows 11 since 22H2
-
category: Disable SmartScreen
docs:
@@ -10045,122 +10048,45 @@ functions:
# - Check :
# - `(Get-AppxPackage -AllUsers 'Windows.CBSPreview').InstallLocation` or `(Get-AppxPackage -AllUsers 'Windows.PrintDialog').InstallLocation`
# - `Get-AppxPackage -PackageTypeFilter Main | ? { $_.SignatureKind -eq "System" } | Sort Name | Format-Table Name, InstallLocation`
# 2. User-specific data
# - Parent : %LOCALAPPDATA%\Packages\
call:
-
# User-specific data
# - Parent : %LOCALAPPDATA%\Packages\{PackageFamilyName}
# - Example : C:\Users\undergroundwires\AppData\Local\Packages\Windows.CBSPreview_cw5n1h2txyewy
# - Check : "$env:LOCALAPPDATA\Packages\$((Get-AppxPackage -AllUsers 'Windows.CBSPreview').PackageFamilyName)"
# 3. Metadata
# - Parent : `%PROGRAMDATA\Microsoft\Windows\AppRepository\Packages\${PackageFullName}`
function: SoftDeleteFiles
parameters:
fileGlob: '%LOCALAPPDATA%\Packages\{{ $packageName }}_{{ $publisherId }}\*'
-
# Metadata
# - Parent : %PROGRAMDATA%\Microsoft\Windows\AppRepository\Packages\{PackageFullName}
# - Example : C:\ProgramData\Microsoft\Windows\AppRepository\Packages\Windows.CBSPreview_10.0.19580.1000_neutral_neutral_cw5n1h2txyewy
# - Check : "$env:PROGRAMDATA\Microsoft\Windows\AppRepository\Packages\$((Get-AppxPackage -AllUsers 'Windows.CBSPreview').PackageFullName)"
call:
function: RunPowerShell
function: SoftDeleteFiles
parameters:
code: |-
$packageName = '{{ $packageName }}'
$publisherId='{{ $publisherId }}'
Write-Host "Soft-deleting `"$packageName`" folders."
$directories = @(
@{ Name = 'User-specific data'; Path = "$env:LOCALAPPDATA\Packages\$($package.PackageFamilyName)"; }
@{ Name = 'Metadata'; Path = "$env:PROGRAMDATA\Microsoft\Windows\AppRepository\Packages\$($package.PackageFullName)"; }
)
$package = Get-AppxPackage -AllUsers $packageName
if ($package -and $package.InstallLocation) {
$directories += @{ Name = 'Installation'; Path = $package.InstallLocation; }
} else {
Write-Host "The package `"$packageName`" could not be found, residual files will still be handled."
$packageFamilyName = "$($packageName)_$($publisherId)"
$appShortName = ($packageName -Split '\.')[-1]
$directories +=@(
@{ Name = 'Installation (SystemApps)'; Path = "$env:WINDIR\SystemApps\$packageFamilyName"; }
@{ Name = 'Installation (Root)'; Path = "$env:WINDIR\$appShortName"; }
)
}
foreach($directory in $directories) {
Write-Host "Processing folder: `"$($directory.Name)`"..."
if (!$directory.Path) {
Write-Host 'Skipping, path not found.'
continue
}
if (!(Test-Path $directory.Path)) {
Write-Host "Skipping, directory `"$($directory.Path)`" does not exist."
continue
}
cmd /c ("takeown /f `"$($directory.Path)`" /r /d y 1> nul")
if ($LASTEXITCODE) {
Write-Error "Failed to obtain ownership for `"$($directory.Path)`"."
continue
}
cmd /c ("icacls `"$($directory.Path))`" /grant administrators:F /t 1> nul")
if ($LASTEXITCODE) {
Write-Error "Failed to assign permissions for `"$($directory.Path)`"."
continue
}
$files = Get-ChildItem -File -Path $directory.Path -Recurse -Force
foreach ($file in $files) {
if($file.Name.EndsWith('.OLD')) {
continue
}
$newName = "$($file.FullName).OLD"
try {
Move-Item -LiteralPath "$($file.FullName)" -Destination "$newName" -Force -ErrorAction Stop
Write-Host "Successfully renamed `"$($file.FullName)`"."
} catch {
Write-Error "Failed to rename `"$($file.FullName)`" to `"$newName`": $($_.Exception.Message)"
}
}
}
revertCode: |-
$packageName = '{{ $packageName }}'
$publisherId='{{ $publisherId }}'
Write-Host "Restoring `"$packageName`" folders."
$directories = @(
@{ Name = 'User-specific data'; Path = "$env:LOCALAPPDATA\Packages\$($package.PackageFamilyName)"; }
@{ Name = 'Metadata'; Path = "$env:PROGRAMDATA\Microsoft\Windows\AppRepository\Packages\$($package.PackageFullName)"; }
)
$package = Get-AppxPackage -AllUsers $packageName
if ($package -and $package.InstallLocation) {
$directories += @{ Name = 'Installation'; Path = $package.InstallLocation; }
} else {
Write-Warning "The package `"$packageName`" could not be found, its files will still be handled."
$packageFamilyName = "$($packageName)_$($publisherId)"
$appShortName = ($packageName -Split '\.')[-1]
$directories +=@(
@{ Name = 'Installation (SystemApps)'; Path = "$env:WINDIR\SystemApps\$packageFamilyName"; }
@{ Name = 'Installation (Root)'; Path = "$env:WINDIR\$appShortName"; }
)
}
foreach ($directory in $directories) {
Write-Host "Processing folder: `"$($directory.Name)`" directory..."
if (!$directory.Path) {
Write-Host "Skipping `"$($directory.Name)`" directory, path not found."
continue
}
if (!(Test-Path $directory.Path)) {
Write-Host "Skipping, directory `"$($directory.Path)`" does not exist."
continue
}
cmd /c ("takeown /f `"$($directory.Path)`" /r /d y 1> nul")
if ($LASTEXITCODE) {
Write-Error "Failed to obtain ownership for `"$($directory.Path)`"."
continue
}
cmd /c ("icacls `"$($directory.Path)`" /grant administrators:F /t 1> nul")
if ($LASTEXITCODE) {
Write-Error "Failed to assign permissions for `"$($directory.Path)`"."
continue
}
$files = Get-ChildItem -File -Path "$($directory.Path)\*.OLD" -Recurse -Force
foreach ($file in $files) {
$newName = $file.FullName.Substring(0, $file.FullName.Length - 4)
try {
Move-Item -LiteralPath "$($file.FullName)" -Destination "$newName" -Force -ErrorAction Stop
Write-Host "Successfully renamed `"$($file.FullName)`" back to original."
} catch {
Write-Error "Failed to rename `"$($file.FullName)`" back to original `"$newName`": $($_.Exception.Message)"
}
}
}
fileGlob: '%PROGRAMDATA%\Microsoft\Windows\AppRepository\Packages\{{ $packageName }}_*_{{ $publisherId }}\*'
grantPermissions: 'true' # 🔒️ Protected on Windows 10 since 22H2 | 🔒️ Protected on Windows 11 since 22H2
-
# Installation (SystemApps)
# - Parent : %WINDIR%\SystemApps\{PackageFamilyName}
# - Example : C:\Windows\SystemApps\Windows.CBSPreview_cw5n1h2txyewy
# - Check : (Get-AppxPackage -AllUsers 'Windows.CBSPreview').InstallLocation
# - Check all : Get-AppxPackage -PackageTypeFilter Main | ? { $_.SignatureKind -eq "System" } | Sort Name | Format-Table Name, InstallLocation
function: SoftDeleteFiles
parameters:
fileGlob: '%WINDIR%\SystemApps\{{ $packageName }}_{{ $publisherId }}\*'
grantPermissions: 'true' # 🔒️ Protected on Windows 10 since 22H2 | 🔒️ Protected on Windows 11 since 22H2
-
# Installation (Root)
# - Parent : %WINDIR%\{ShortAppName}
# - Example : C:\Windows\PrintDialog
# - Check : (Get-AppxPackage -AllUsers 'Windows.PrintDialog').InstallLocation
# - Check all : Get-AppxPackage -PackageTypeFilter Main | ? { $_.SignatureKind -eq "System" } | Sort Name | Format-Table Name, InstallLocation
function: SoftDeleteFiles
parameters:
fileGlob: >-
%WINDIR%\$(("{{ $packageName }}" -Split '\.')[-1])\*
grantPermissions: 'true' # 🔒️ Protected on Windows 10 since 22H2 | 🔒️ Protected on Windows 11 since 22H2
-
name: UninstallCapability
parameters:
@@ -10173,33 +10099,200 @@ functions:
$capability = Get-WindowsCapability -Online -Name '{{ $capabilityName }}*'
Add-WindowsCapability -Name "$capability.Name" -Online
-
name: RenameSystemFile
name: SoftDeleteFiles
# 💡 Purpose:
# Renames files matching a given glob pattern by appending a `.OLD` extension, effectively "soft deleting" them.
# This allows for easier restoration and less immediate disruption compared to permanent deletion.
# 🤓 Implementation:
# - Utilizes the `IterateGlob` function to match and iterate over files.
# - Optionally elevates script permissions to modify file privileges if required.
# - Renames matched files and handles permission restoration after renaming.
# - Provides detailed logs of actions taken and any issues encountered.
parameters:
- name: filePath
code: |-
if exist "{{ $filePath }}" (
takeown /f "{{ $filePath }}"
icacls "{{ $filePath }}" /grant administrators:F
move "{{ $filePath }}" "{{ $filePath }}.OLD" && (
echo Moved "{{ $filePath }}" to "{{ $filePath }}.OLD"
) || (
echo Could not move {{ $filePath }} 1>&2
)
) else (
echo No action required: {{ $filePath }} is not found.
)
revertCode: |-
if exist "{{ $filePath }}.OLD" (
takeown /f "{{ $filePath }}.OLD"
icacls "{{ $filePath }}.OLD" /grant administrators:F
move "{{ $filePath }}.OLD" "{{ $filePath }}" && (
echo Moved "{{ $filePath }}.OLD" to "{{ $filePath }}"
) || (
echo Could restore from backup file {{ $filePath }}.OLD 1>&2
)
) else (
echo Could not find backup file "{{ $filePath }}.OLD" 1>&2
- name: fileGlob
- name: grantPermissions
optional: true
call:
-
function: CommentCode
parameters:
comment: >-
Soft deleting files matching pattern
{{ with $grantPermissions }}(with additional permissions){{ end }}
: "{{ $fileGlob }}"
revertComment: >-
Restoring files matching pattern
{{ with $grantPermissions }}(with additional permissions){{ end }}
: "{{ $fileGlob }}"
-
function: IterateGlob
parameters:
pathGlob: '{{ $fileGlob }}'
revertPathGlob: '{{ $fileGlob }}.OLD'
# Search logic:
# It uses `.PSIsContainer` instead of `-File` otherwise wildcards in directories do not match i.e. pattern
# `C:\ProgramData\Microsoft\Windows\AppRepository\Packages\Microsoft.Windows.SecHealthUI_*_cw5n1h2txyewy` does not match any files.
# Elevating privileges:
# Another (simpler) implementation would be:
# $setPrivilegeFunction = [System.Diagnostics.Process].GetMethods(42) | Where-Object { $_.Name -eq 'SetPrivilege' }
# $privileges = @('SeRestorePrivilege', 'SeTakeOwnershipPrivilege')
# foreach ($privilege in $privileges) {
# $setPrivilegeFunction.Invoke($null, @($privilege, 2))
# }
beforeIteration: |-
$renamedCount = 0
$skippedCount = 0
$failedCount = 0
{{ with $grantPermissions }}
Add-Type -TypeDefinition @"
using System;
using System.Runtime.InteropServices;
public class Privileges {
[DllImport("advapi32.dll", ExactSpelling = true, SetLastError = true)]
internal static extern bool AdjustTokenPrivileges(IntPtr htok, bool disall,
ref TokPriv1Luid newst, int len, IntPtr prev, IntPtr relen);
[DllImport("advapi32.dll", ExactSpelling = true, SetLastError = true)]
internal static extern bool OpenProcessToken(IntPtr h, int acc, ref IntPtr phtok);
[DllImport("advapi32.dll", SetLastError = true)]
internal static extern bool LookupPrivilegeValue(string host, string name, ref long pluid);
[StructLayout(LayoutKind.Sequential, Pack = 1)]
internal struct TokPriv1Luid {
public int Count;
public long Luid;
public int Attr;
}
internal const int SE_PRIVILEGE_ENABLED = 0x00000002;
internal const int TOKEN_QUERY = 0x00000008;
internal const int TOKEN_ADJUST_PRIVILEGES = 0x00000020;
public static bool AddPrivilege(string privilege) {
try {
bool retVal;
TokPriv1Luid tp;
IntPtr hproc = GetCurrentProcess();
IntPtr htok = IntPtr.Zero;
retVal = OpenProcessToken(hproc, TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, ref htok);
tp.Count = 1;
tp.Luid = 0;
tp.Attr = SE_PRIVILEGE_ENABLED;
retVal = LookupPrivilegeValue(null, privilege, ref tp.Luid);
retVal = AdjustTokenPrivileges(htok, false, ref tp, 0, IntPtr.Zero, IntPtr.Zero);
return retVal;
} catch (Exception ex) {
throw new Exception("Failed to adjust token privileges", ex);
}
}
public static bool RemovePrivilege(string privilege) {
try {
bool retVal;
TokPriv1Luid tp;
IntPtr hproc = GetCurrentProcess();
IntPtr htok = IntPtr.Zero;
retVal = OpenProcessToken(hproc, TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, ref htok);
tp.Count = 1;
tp.Luid = 0;
tp.Attr = 0; // This line is changed to revoke the privilege
retVal = LookupPrivilegeValue(null, privilege, ref tp.Luid);
retVal = AdjustTokenPrivileges(htok, false, ref tp, 0, IntPtr.Zero, IntPtr.Zero);
return retVal;
} catch (Exception ex) {
throw new Exception("Failed to adjust token privileges", ex);
}
}
[DllImport("kernel32.dll", CharSet = CharSet.Auto)]
public static extern IntPtr GetCurrentProcess();
}
"@
[Privileges]::AddPrivilege('SeRestorePrivilege') | Out-Null
[Privileges]::AddPrivilege('SeTakeOwnershipPrivilege') | Out-Null
$adminSid = New-Object System.Security.Principal.SecurityIdentifier 'S-1-5-32-544'
$adminAccount = $adminSid.Translate([System.Security.Principal.NTAccount])
$adminFullControlAccessRule = New-Object System.Security.AccessControl.FileSystemAccessRule( `
$adminAccount, `
[System.Security.AccessControl.FileSystemRights]::FullControl, `
[System.Security.AccessControl.AccessControlType]::Allow `
)
{{ end }}
duringIteration: |-
if (Test-Path -Path $path -PathType Container) {
Write-Host "Skipping folder (not its contents): `"$path`"."
$skippedCount++
continue
}
if($revert -eq $true) {
if (-not $path.EndsWith('.OLD')) {
Write-Host "Skipping non-backup file: `"$path`"."
$skippedCount++
continue
}
} else {
if ($path.EndsWith('.OLD')) {
Write-Host "Skipping backup file: `"$path`"."
$skippedCount++
continue
}
}
$originalFilePath = $path
Write-Host "Processing file: `"$originalFilePath`"."
if (-Not (Test-Path $originalFilePath)) {
Write-Host "Skipping, file `"$originalFilePath`" not found."
$skippedCount++
exit 0
}
{{ with $grantPermissions }}
$originalAcl = Get-Acl -Path "$originalFilePath"
$accessGranted = $false
try {
$acl = Get-Acl -Path "$originalFilePath"
$acl.SetOwner($adminAccount) # Take Ownership (because file is owned by TrustedInstaller)
$acl.AddAccessRule($adminFullControlAccessRule) # Grant rights to be able to move the file
Set-Acl -Path $originalFilePath -AclObject $acl -ErrorAction Stop
$accessGranted = $true
} catch {
Write-Warning "Failed to grant access to `"$originalFilePath`": $($_.Exception.Message)"
}
{{ end }}
if ($revert -eq $true) {
$newFilePath = $backupFilePath.Substring(0, $backupFilePath.Length - 4)
} else {
$newFilePath = "$($originalFilePath).OLD"
}
try {
Move-Item -LiteralPath "$($originalFilePath)" -Destination "$newFilePath" -Force -ErrorAction Stop
Write-Host "Successfully processed `"$originalFilePath`"."
$renamedCount++
{{ with $grantPermissions }}
if ($accessGranted) {
try {
Set-Acl -Path $newFilePath -AclObject $originalAcl -ErrorAction Stop
} catch {
Write-Warning "Failed to restore access on `"$newFilePath`": $($_.Exception.Message)"
}
}
{{ end }}
} catch {
Write-Error "Failed to rename `"$originalFilePath`" to `"$newFilePath`": $($_.Exception.Message)"
$failedCount++
{{ with $grantPermissions }}
if ($accessGranted) {
try {
Set-Acl -Path $originalFilePath -AclObject $originalAcl -ErrorAction Stop
} catch {
Write-Warning "Failed to restore access on `"$originalFilePath`": $($_.Exception.Message)"
}
}
{{ end }}
}
afterIteration: |-
if (($renamedCount -gt 0) -or ($skippedCount -gt 0)) {
Write-Host "Successfully processed $renamedCount items and skipped $skippedCount items."
}
if ($failedCount -gt 0) {
Write-Warning "Failed to processed $($failedCount) items."
}
{{ with $grantPermissions }}
[Privileges]::RemovePrivilege('SeRestorePrivilege') | Out-Null
[Privileges]::RemovePrivilege('SeTakeOwnershipPrivilege') | Out-Null
{{ end }}
-
name: SetVsCodeSetting
parameters:
@@ -11053,21 +11146,31 @@ functions:
# This function does not affect the execution flow but helps in understanding the purpose of subsequent code.
parameters:
- name: comment
- name: revertComment
optional: true
call:
function: RunInlineCode
parameters:
code: ':: {{ $comment }}'
revertCode: '{{ with $revertComment }}:: {{ . }}{{ end }}'
-
name: DeleteGlob
# Behavior:
# Deletes files and directories on Windows using Unix-style glob patterns.
# Searches for files and directories based on a Unix-style glob pattern and iterates over them.
# Primarily supports the `*` wildcard; compatibility with other patterns is not tested.
# 💡 Usage:
# This is a low-level function. Favor higher-level functions like `ClearDirectoryContents` and `DeleteDirectory`
# for clearer intent and enhanced security when applicable.
# This is a low-level function. Favor using other functions in script calls.
# It provides following variables for the code in argument value:
# - `$expandedPath` : Expanded path glob pattern.
# - `$path` : Current iterated path (only available for `duringIteration`)
name: IterateGlob
parameters:
- name: pathGlob
- name: grantPermissions
- name: beforeIteration
optional: true
- name: duringIteration
- name: afterIteration
optional: true
- name: revertPathGlob
optional: true
call:
function: RunPowerShell
@@ -11076,9 +11179,100 @@ functions:
$pathGlobPattern = "{{ $pathGlob }}"
$expandedPath = [System.Environment]::ExpandEnvironmentVariables($pathGlobPattern)
Write-Host "Searching for items matching pattern: `"$($expandedPath)`"."
$parentDirectory = Split-Path -Path $expandedPath -Parent
{{ with $grantPermissions }} # Not using `Get-Acl`/`Set-Acl` to avoid adjusting token privileges
$grantPermissions=$true
{{ with $beforeIteration }}
{{ . }}
{{ end }}
$getChildItemParams = @{ Force = $true; }
if ($expandedPath -like '*[*?]*') {
# Recurse only on parent if the path contains glob pattern, otherwise it will unnecessarily try to match
# every folder/file in parent, potentially leading to permission errors.
# Without recursion `Get-ChildItem` does not find subdirectories.
$getChildItemParams['Recurse'] = $true
}
$getChildItemParams['Path'] = $expandedPath
try {
$foundItems = @(Get-ChildItem @getChildItemParams -ErrorAction Stop)
} catch [System.Management.Automation.ItemNotFoundException] { # Do not run `Test-Path` before, it's unreliable for globs requiring extra permissions
$foundItems = @()
}
if (!$foundItems) {
$formattedParams = ($getChildItemParams.GetEnumerator() | ForEach-Object { "$($_.Key): `"$($_.Value)`"" }) -Join ', '
Write-Host "Skipping, no items available with search parameters: $($formattedParams)."
exit 0
}
Write-Host "Initiating processing of $($foundItems.Count) items from `"$expandedPath`"."
foreach ($item in $foundItems) {
$path = $item.FullName
{{ $duringIteration }}
}
{{ with $afterIteration }}
{{ . }}
{{ end }}
# Marked: refactor-with-variables
# Unfortunately a lot of duplication here as privacy.sexy compiler does not support better way for now.
# The difference from this script and `code` is that:
# - It sets `$revert` variable to `$true`.
# - It uses `$revertPathGlob` instead of `$pathGlob`
revertCode: |-
{{ with $revertPathGlob }}
$revert = true
$pathGlobPattern = "{{ . }}"
$expandedPath = [System.Environment]::ExpandEnvironmentVariables($pathGlobPattern)
Write-Host "Searching for items matching pattern: `"$($expandedPath)`"."
{{ with $beforeIteration }}
{{ . }}
{{ end }}
$getChildItemParams = @{ Force = $true; }
if ($expandedPath -like '*[*?]*') {
# Recurse only on parent if the path contains glob pattern, otherwise it will unnecessarily try to match
# every folder/file in parent, potentially leading to permission errors.
# Without recursion `Get-ChildItem` does not find subdirectories.
$getChildItemParams['Recurse'] = $true
}
$getChildItemParams['Path'] = $expandedPath
try {
$foundItems = @(Get-ChildItem @getChildItemParams -ErrorAction Stop)
} catch [System.Management.Automation.ItemNotFoundException] { # Do not run `Test-Path` before, it's unreliable for globs requiring extra permissions
$foundItems = @()
}
if (!$foundItems) {
$formattedParams = ($getChildItemParams.GetEnumerator() | ForEach-Object { "$($_.Key): `"$($_.Value)`"" }) -Join ', '
Write-Host "Skipping, no items available with search parameters: $($formattedParams)."
exit 0
}
Write-Host "Initiating processing of $($foundItems.Count) items from `"$expandedPath`"."
foreach ($item in $foundItems) {
$path = $item.FullName
{{ $duringIteration }}
}
{{ with $afterIteration }}
{{ . }}
{{ end }}
{{ end }}
-
name: DeleteGlob
# Behavior:
# Deletes files and directories based on a Unix-style glob pattern.
# Optionally, it can grant full permissions to the items before deletion.
# 💡 Usage:
# This is a low-level function. Favor higher-level functions like `ClearDirectoryContents` and `DeleteDirectory`
# for clearer intent and enhanced security when applicable.
# 🚫 **Limitations**:
# The function might not perform as expected if the current user lacks read permissions on the parent directory.
# This specific use case is not addressed in the implementation because it has not been deemed necessary for the function's intended
# applications.
parameters:
- name: pathGlob
- name: grantPermissions
optional: true
call:
function: IterateGlob
parameters:
pathGlob: '{{ $pathGlob }}'
beforeIteration: |-
{{ with $grantPermissions }}
# Not using `Get-Acl`/`Set-Acl` to avoid adjusting token privileges
$parentDirectory = [System.IO.Path]::GetDirectoryName($parentDirectory)
if ($parentDirectory -like '*[*?]*') {
throw "Unable to grant permissions to glob paths: `"$parentDirectory`", not supported by ``takeown`` and ``icacls``."
} else {
@@ -11115,50 +11309,23 @@ functions:
}
}
{{ end }}
$getChildItemParams = @{ Force = $true; }
$filter = Split-Path -Path $expandedPath -Leaf
$getChildItemParams['Filter'] = $filter
if ($filter -like '*[*?]*') {
# Recurse only on parent if filter contains glob pattern, otherwise it will unnecessarily try to match
# every folder/file in parent, potentially leading to permission errors
# Without recursion `Get-ChildItem` does not find subdirectories.
$getChildItemParams['Recurse'] = $true
# Append a backslash to the parent path during recursion. Without it, recursion will unintentionally
# operate on the parent's parent directory.
if (!$parentDirectory.EndsWith('/')) {
$parentDirectory += '\'
}
}
$getChildItemParams['Path'] = $parentDirectory
try {
$itemsToDelete = @(Get-ChildItem @getChildItemParams -ErrorAction Stop)
} catch [System.Management.Automation.ItemNotFoundException] { # Not run `Test-Path` before, it's unreliable for globs requiring extra permissions
$itemsToDelete = @()
}
if (!$itemsToDelete) {
$formattedParams = ($getChildItemParams.GetEnumerator() | ForEach-Object { "$($_.Key): `"$($_.Value)`"" }) -Join ', '
Write-Host "Skipping, no items available for deletion with search parameters: $($formattedParams)."
exit 0
}
Write-Host "Initiating deletion of $($itemsToDelete.Count) items from `"$expandedPath`"."
$deletedCount = 0
$failedCount = 0
foreach ($item in $itemsToDelete) {
if (-not (Test-Path $item.FullName)) { # Re-check existence as prior deletions might remove subsequent items (e.g., subdirectories).
Write-Host "Successfully deleted: $($item.FullName) (already deleted)."
duringIteration: |-
if (-not (Test-Path $path)) { # Re-check existence as prior deletions might remove subsequent items (e.g., subdirectories).
Write-Host "Successfully deleted: $($path) (already deleted)."
$deletedCount++
continue
}
try {
Remove-Item -Path $item.FullName -Force -Recurse -ErrorAction Stop
Remove-Item -Path $path -Force -Recurse -ErrorAction Stop
$deletedCount++
Write-Host "Successfully deleted: $($item.FullName)"
}
catch {
Write-Host "Successfully deleted: $($path)"
} catch {
$failedCount++
Write-Warning "Unable to delete $($item.FullName): $_"
}
Write-Warning "Unable to delete $($path): $_"
}
afterIteration: |-
Write-Host "Successfully deleted $($deletedCount) items."
if ($failedCount -gt 0) {
Write-Warning "Failed to delete $($failedCount) items."