Scoring Applocker rules

To be able to audit Applocker rules created or hunt for human error, I wanted to have a simple way to score Applocker rules.
Are they good or evil? Are they tuned and specific enough to allow or block something?
Is there any rule that is more permissive or not well designed due to a human error?

  • Issue

My Google-fu didn’t find anything relevant to help me score Applocker rules 😦

  • Solution

How are rules scored?
I’ve tried to score various aspects of the rule, based on the type (Publisher, Path or Hash), the identity targeted, the action (Allow or Deny) and the conditions.
The code aims at finding and identifying some rules that are more permissive than others, so that it’s easier to spot them and pay attention to these rules.
All the functions scoring rules follow a simple principle. The higher score, the more dangerous/permissive the rule is actually.
There’s a limitation, the code doesn’t evaluate at all the Exceptions and worse, having conditions could have a negative impact on the score (lowering it).
The other limitation is that the code assumes that there isn’t an administrator introducing malicious rules.

How does it perform? Show me examples !

Import-Module -Name Applocker -SkipEditionCheck
Import-Module ~/Documents/ApplockerScore.psm1 -Verbose
Get-ApplockerPolicyRuleScore -Type Script -Rule '(Default Rule) All scripts'


You can see that there’s tab completion for the parameter. You just need to begin by the Type to be able to display the rule names on the 2nd parameter using tab completion.
The default rule that allows anything for an admin is score over 1000. It means it’s too permissive. It’s a Bypass rule for anything (a wildcard) for the members of the local Administrators group. It’s not the highest score you can get.

cd\
Import-Module -Name Applocker -SkipEditionCheck
Import-Module ~/Documents/ApplockerScore.psm1
Get-Help Get-ApplockerPolicyRuleScore -Full
$rule = Get-ApplockerPolicyRuleScore -Type Script -Rule '(Default Rule) All scripts' -Verbose
 Write-Verbose -Message "Final score for '$($rule.Name)' is $($rule.Score)" -Verbose
$rule.Object
$rule.Object | gm


It shows how the score is calculated and what was evaluated to score the rule.
There’s an embedded object property that is its XML definition converted back to its original object type.

Besides simple examples, can it be used against exported Applocker policies to XML files?

Yes, it can. You just need to use the internal private functions and load them.
Here’s a practical example on Ultimate AppLocker ByPass List from Oddvar Moe

Let’s say, you’ve downloaded his Github repo as a zip file, extracted it and loaded the internal functions.
You can now do the following:

Get-ChildItem -Path '~\Downloads\UltimateAppLockerByPassList-master\UltimateAppLockerByPassList-master\AppLocker-BlockPolicies\*.xml' |
ForEach-Object {
([xml](Get-Content -Path $_.FullName -ReadCount 0)).AppLockerPolicy.RuleCollection.ChildNodes |
Out-GridView -PassThru |
ForEach-Object {
$TypeScore = Get-ApplockerRuleTypeScore $_.OuterXML -Verbose
$ActionSore = Get-ApplockerRuleActionScore $_.OuterXML -Verbose
$IdScore = Get-ApplockerRuleUserOrGroupSidScore $_.OuterXML -Verbose
$ConditionScore = Get-ApplockerRuleConditionsScore $_.OuterXML -Verbose
[PSCustomObject]@{
Id = $_.Id
Name = $_.Name
Score = ($TypeScore+$IdScore+$ConditionScore)*$ActionSore
XML = $_.OuterXML
}
}
}

Here’s an example on the rule named ‘Signed by Skype’:

Here’s a 2nd example on the rule about MSHTA:

Where’s the code?

It’s stored in a psm1 file uploaded as gist on GitHub.com:

#Requires -Version 3.0
#region Score the identity
Function Get-ApplockerRuleUserOrGroupSidScore {
[CmdletBinding()]
[OutputType([int32])]
Param(
[Parameter(Mandatory)]
[Alias('UserOrGroupSid','SID')]
$InputObject
)
Begin {}
Process {
$InputObject |
ForEach-Object {
Switch -Regex ($_) {
'S-1-1-0' { # EveryOne
Write-Verbose -Message 'Applocker score: 50 ; Everyone'
50 ; break
}
'S-1-5-32-544' { # Administrators
Write-Verbose -Message 'Applocker score: 10 ; Administrators'
10 ; break
}
'S-1-5-32-545' { # Users
Write-Verbose -Message 'Applocker score: 10 ; Users'
10 ; break
}
# S-1-5-11 # Authenticated Users
# S-1-5-32-546 # Guests
# S-1-5-32-547 # Power Users
default {
Write-Verbose -Message 'Applocker score: Id is specific'
10
}
}
}
}
End {}
}
#endregion
#region Score the rule type
Function Get-ApplockerRuleTypeScore {
[CmdletBinding()]
[OutputType([int32])]
Param(
[Parameter(Mandatory)]
[string[]]$InputObject
)
Begin {}
Process {
$InputObject |
ForEach-Object {
Switch -Regex ($_) {
'^<FilePath(Rule|Condition)' { Write-Verbose -Message 'Applocker score: 100 ; Path type' ; 100 }
'^<FilePublisher(Rule|Condition)' { Write-Verbose -Message 'Applocker score: 50 ; Path publisher' ; 50}
'^<FileHash(Rule|Condition)' {Write-Verbose -Message 'Applocker score: 50 ; Path Hash' ; 50}
default {}
}
}
}
End {}
}
#endregion
#region Score the rule action
Function Get-ApplockerRuleActionScore {
[CmdletBinding()]
[OutputType([int32])]
Param(
[Parameter(Mandatory)]
[string[]]$InputObject
)
Begin {}
Process {
$InputObject |
ForEach-Object {
Switch -Regex ($_) {
'\sAction="Allow"' {
Write-Verbose -Message 'Applocker score: *1 ; Action is Allow'
1
}
'\sAction="Deny"' {
Write-Verbose -Message 'Applocker score: /10 ; Action is Deny'
0.1
}
default {}
}
}
}
End {}
}
#endregion
#region Score the rule conditions
Function Get-ApplockerRuleConditionsScore {
[CmdletBinding()]
[OutputType([int32])]
Param(
[Parameter(Mandatory)]
[string[]]$InputObject
)
Begin {}
Process {
$InputObject |
ForEach-Object {
($(
Switch -Regex ($_) {
'^<FilePath(Rule|Condition)' {
Write-Verbose -Message 'Applocker score: Path type'
# Depth: count the occurrence of '\'
$depth = 110 - (($_ -split '\\').Count)*10
Write-Verbose -Message "Depth score is $($depth)"
$depth
# Evaluate if there's a wildcard in the path
if ($_ -match '\\\*\\') {
50
}
Switch -Regex ($_) {
# Just a specific extension
# .exe, .com *.msi, msp mst ps1 cmd bat vbs js appx msix dll ocx
'\sPath="\*\.([eE][xX][eE]|[cC][oO][mM]|[mM][sS][iI]|[mM][sS][pP]|[mM][sS][tT]|[pP][sS]1|[cC][mM][dD]|[bB][aA][tT]|[vV][bB][sS]|[jJ][sS])"\s' {
Write-Verbose -Message "Path score: 500 (only a *.ext)"
500
}
# Path letter
'\sPath="(c|C):\\' {
Write-Verbose -Message "Path score: 100 (Path on C: drive)"
100
}
'\sPath="[abABd-zD-Z]:\\' {
Write-Verbose -Message "Path score: 100 (Path not on C: drive)"
50
}
# Extension: just a wildcard
'\sPath="\*\.\*"\s' { # *.*
Write-Verbose -Message "Path score: 1000 (only a *.*)"
1000 ; break
}
'\sPath="\*"\s' { # *
Write-Verbose -Message "Path score: 1000 (only a *)"
1000 ; break
}
default {}
}
}
'^<FilePublisher(Rule|Condition)' {
Write-Verbose -Message 'Applocker score: Path publisher'
Switch -Regex ($_) {
'PublisherName="\*"\sProductName="\*"\sBinaryName="\*"\>\<BinaryVersionRange\sLowSection="0\.0\.0\.0"\sHighSection="\*"' {
Write-Verbose -Message "Publisher score: 1000 (default Appx PublisherName=*)"
1000 ; break
}
'ProductName="\*"\sBinaryName="\*"\>\<BinaryVersionRange\sLowSection="\*"\sHighSection="\*"' {
Write-Verbose -Message "Publisher score: 500 (only a PublisherName)"
500 ; break
}
'\sBinaryName="\*"\>\<BinaryVersionRange\sLowSection="\*" HighSection="\*"' {
Write-Verbose -Message "Publisher score: 500 (PublisherName and ProductName)"
200 ; break
}
'BinaryVersionRange\sLowSection="\*"\sHighSection="\*"' {
Write-Verbose -Message "Publisher score: 100 (PublisherName and ProductName and FileName)"
100 ; break
}
default {
Write-Verbose -Message "Publisher score: 50 (default)"
50
}
}
}
'^<FileHash(Rule|Condition)' {
Write-Verbose -Message "Hash score: 50"
50
}
default {}
}
) | Measure-Object -Sum).Sum
}
}
End {}
}
#endregion
#region Convert to Object from Xml
Function Get-ApplockerRuleObjectFromXml {
[CmdletBinding()]
[OutputType([int32])]
Param(
[Parameter(Mandatory)]
[string[]]$InputObject
)
Begin {}
Process {
$InputObject |
ForEach-Object {
$o = $_
Switch -Regex ($_) {
'^<FilePathRule' {
try {
[Microsoft.Security.ApplicationId.PolicyManagement.PolicyModel.FilePathRule]::FromXml($_)
} catch {
Write-Warning -Message "Failed to translate $($o) to Applocker FilePathRule because $($_.Exception.Message)"
}
break
}
'^<FilePublisherRule' {
try {
[Microsoft.Security.ApplicationId.PolicyManagement.PolicyModel.FilePublisherRule]::FromXml($_)
} catch {
Write-Warning -Message "Failed to translate $($o) to Applocker FilePublisherRule because $($_.Exception.Message)"
}
break
}
'^<FileHashRule' {
try {
[Microsoft.Security.ApplicationId.PolicyManagement.PolicyModel.FileHashRule]::FromXml($_)
} catch {
Write-Warning -Message "Failed to translate $($o) to Applocker FilePublisherRule because $($_.Exception.Message)"
}
break
}
default {
Write-Warning -Message 'Failed to transtale input into an Applocker rule'
}
}
}
}
End {}
}
#endregion
Function Get-ApplockerPolicyRuleScore {
<#
.SYNOPSIS
Score an Applocker policy rule
.DESCRIPTION
Score an Applocker policy rule
.PARAMETER Type
Parameter that indicates the RuleCollectionType to find a rule
.PARAMETER Rule
Dynamic parameter that indicates the Name of the rule to score
.EXAMPLE
Get-ApplockerPolicyRuleScore -Type Script -Rule '(Default Rule) All scripts'
Id Name Score XML
-- ---- ----- ---
ed97d0cb-15ff-430f-b82c-8d7832957725 (Default Rule) All scripts 1210 <FilePathRule Id
.EXAMPLE
$rule = Get-ApplockerPolicyRuleScore -Type Script -Rule '(Default Rule) All scripts' -Verbose
Write-Verbose -Message "Final score for '$($rule.Name)' is $($rule.Score)" -Verbose
$rule.Object
VERBOSE: Dealing with Rule Collection type: Script
VERBOSE: Dealing with Rule Name: (Default Rule) All scripts
VERBOSE: Applocker score: 100 ; Path type
VERBOSE: Applocker score: *1 ; Action is Allow
VERBOSE: Applocker score: 10 ; Administrators
VERBOSE: Applocker score: Path type
VERBOSE: Depth score is 100
VERBOSE: Path score: 1000 (only a *)
VERBOSE: Final score for '(Default Rule) All scripts' is 1210
PathConditions : {*}
PathExceptions : {}
PublisherExceptions : {}
HashExceptions : {}
Id : ed97d0cb-15ff-430f-b82c-8d7832957725
Name : (Default Rule) All scripts
Description : Allows members of the local Administrators group to run all scripts.
UserOrGroupSid : S-1-5-32-544
Action : Allow
#>
[CmdletBinding()]
Param(
[ValidateSet('Exe','Script','Msi','Appx','Dll')]
[Parameter(Mandatory)]
[String]$Type
)
DynamicParam {
$Dictionary = New-Object -TypeName System.Management.Automation.RuntimeDefinedParameterDictionary
#region helper function
Function New-ParameterAttributCollection {
[CmdletBinding(SupportsShouldProcess)]
Param(
[Switch]$Mandatory,
[Switch]$ValueFromPipeline,
[Switch]$ValueFromPipelineByPropertyName,
[String]$ParameterSetName,
[Parameter()]
[ValidateSet(
'Arguments','Count','Drive','EnumeratedArguments','Length','NotNull',
'NotNullOrEmpty','Pattern','Range','Script','Set','UserDrive'
)][string]$ValidateType,
[Parameter()]
$ValidationContent
)
Begin {}
Process {
if ($PSCmdlet.ShouldProcess('Create new Attribute')) {
$c = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
$a = New-Object System.Management.Automation.ParameterAttribute
if ($Mandatory) {
$a.Mandatory = $true
}
if ($ValueFromPipeline) {
$a.ValueFromPipeline = $true
}
if ($ValueFromPipelineByPropertyName) {
$a.ValueFromPipelineByPropertyName=$true
}
if ($ParameterSetName) {
$a.ParameterSetName = $ParameterSetName
}
$c.Add($a)
if ($ValidateType -and $ValidationContent) {
try {
$c.Add((New-Object "System.Management.Automation.Validate$($ValidateType)Attribute"(
$ValidationContent
)))
} catch {
Throw $_
}
}
$c
}
}
End {}
}
#endregion
try {
$ApplockerPolicyXml = [xml](Get-AppLockerPolicy -Effective -Xml -ErrorAction Stop)
} catch {
Throw 'Failed to read the effective Applocker policy into XML'
}
#region param Rule
$Dictionary.Add(
'Rule',
(New-Object System.Management.Automation.RuntimeDefinedParameter(
'Rule',
[string],
(New-ParameterAttributCollection -Mandatory -ValidateType Set -ValidationContent (
$ApplockerPolicyXml.SelectNodes("/AppLockerPolicy/RuleCollection[@Type='$($PSBoundParameters['Type'])']").ChildNodes| ForEach-Object { $_.Name }
))
))
)
$Dictionary
}
Begin {}
Process {
Write-Verbose -Message "Dealing with Rule Collection type: $($PSBoundParameters['Type'])"
Write-Verbose -Message "Dealing with Rule Name: $($PSBoundParameters['Rule'])"
# Select node
$n = $ApplockerPolicyXml.SelectNodes("/AppLockerPolicy/RuleCollection[@Type='$($PSBoundParameters['Type'])']").ChildNodes |
Where-Object { $_.Name -eq "$($PSBoundParameters['Rule'])" }
try {
$TypeScore = Get-ApplockerRuleTypeScore -InputObject $n.OuterXml
$ActionSore = Get-ApplockerRuleActionScore -InputObject $n.OuterXml
$IdScore = Get-ApplockerRuleUserOrGroupSidScore -InputObject $n.OuterXml
$ConditionScore = Get-ApplockerRuleConditionsScore -InputObject $n.OuterXml
$r = [PSCustomObject]@{
Id = $n.Id
Name = $n.Name
Score = ($TypeScore+$IdScore+$ConditionScore)*$ActionSore
XML = $n.OuterXml
Object = Get-ApplockerRuleObjectFromXml -InputObject $n.OuterXml
}
Update-TypeData -TypeName 'Applocker.Rule.Score' -DefaultDisplayPropertySet 'Id','Name','Score','XML' -Force
$r.PSTypeNames.Insert(0,'Applocker.Rule.Score')
$r
} catch {
Throw "Something went wrong while scoring applocker rule: $($_.Exception.Message)"
}
}
End {}
} # endof Get-ApplockerPolicyRuleScore
Export-ModuleMember -Function 'Get-ApplockerPolicyRuleScore'
view raw ApplockerScore.psm1 hosted with ❤ by GitHub

How to whitelist your AutoRuns artifacts

Have you ever wondered how can I create a whitelist of my known harmless AutoRuns artifacts?

There are various ways to filter and ignore known artifacts. It can be done either for hunting purposes and to spot more quickly what’s unknown that requires a review and (probably) further actions.

You can filter Autoruns artifacts by using the authenticode certificate, the SHA256 hash,…

All these methods will work but there are some known artifacts that are still difficult to filter out.

I’ll show here a way to deal with these artifacts and create a baseline or whitelist of known artifacts.

I’ve introduced a new function in the module to create a whitelist of artifacts stored in a .ps1 file.

You can directly send the output of the main Get-PSAutorun function to the new New-AutoRunsBaseLine so that a .ps1 file is created. This powershell script stores an array of artifcats stored as PSCustomobject.

You may wonder why a .ps1 file and not a json file. The reason is that you can use a code signing certificate to sign the created ps1 file using the built-in Set-AuthenticodeSignature cmdlet. It brings integrity. It’s a signed whitelist. Signing may also be required if you use the PowerShell constrained language mode.

You can for example store all the artifacts with the maximum properties available.

Get-PSAutorun -VerifyDigitalSignature -ShowFileHash|
New-AutoRunsBaseLine -Verbose

Later on, to see if there are any changes, you can use the second function I’ve introduced: Compare-AutoRunsBaseLine

Here’s an example where I duplicated an existing file and modified one of its properties: the ending digit of the version.

For hunting purposes, you may want to store a minimalist whitelist and use it as a filter to view what’s relevant.

Get-PSAutorun -VerifyDigitalSignature|
Where { -not($_.isOSbinary)}|
New-AutoRunsBaseLine

If you want to view what has been exported in the whitelist, you just need to run the script created.

~\Documents\PSAutoRunsBaseLine-20201102214715.ps1 |
Out-GridView -PassThru

Here’s an example where I selected only one difficult artifact to filter out from the grid output:

Here’s another example I use on my personal computer:

Get-PSAutorun -VerifyDigitalSignature -ShowFileHash |
Where {-not($_.Version)} | Where { -not($_.isOSbinary)} |
New-AutoRunsBaseLine -Verbose

If you want to see what that file contains, I’ve uploaded a version here.

If you think that these new functions are useful, please vote for them πŸ˜€

The above new functions are currently only part of the experimental branch of Autoruns repository on Github. It’s not signed digitally and the Autoruns.cat file has not been modified to reflect the changes of the main Autoruns files (.psd1 and psm1). It’s also not been published yet to the PowerShell Gallery. It all depends on your votes πŸ˜€ . If you think it’s useful, please let me know. If you think, it’s not, please let also me know. If you find issues with the code, please introduce it directly on Github using this link.

Enjoy and happy hunting 😎

Reinstall preinstalled (appx) Apps

  • Context

I’ve discovered recently this link to reinstall-preinstalledApps.zip that was provided on this page

  • Solution

The code is straightforward and probably does the job in most situations.
But, there are some assumptions about the version, admin rights,.. and the code has no error handling. You can see the file here.
I’ve decided to improve it by adding a help, a #require statement, error handling and the ability to use the -WhatIf parameter.

If you use my “slightly improved” version, you can do:

# Show what would be done using the -WhatIf parameter
Start-ReInstallAppx -Filter "*Microsoft.WindowsStore*" -WhatIf

# Do it:
Start-ReInstallAppx -Filter "*Microsoft.WindowsStore*" -Verbose

#Requires -RunAsAdministrator
Function Start-ReInstallAppx {
<#
.SYNOPSIS
Get back all the apps that come default with Windows 10
.DESCRIPTION
Get back all the apps that come default with Windows 10
.EXAMPLE
Start-ReInstallAppx -Filter "*Microsoft.WindowsStore*"
.NOTES
http://go.microsoft.com/fwlink/?LinkId=619547
Adapted from
http://download.microsoft.com/download/5/F/0/5F04003A-035E-4A0F-9662-43E32C546F6C/reinstall-preinstalledApps.zip
#>
[CmdletBinding(SupportsShouldProcess)]
Param(
[Parameter()]
[ValidateNotNullOrEmpty()]
[string]$Filter
)
Begin {
$HT = @{ ErrorAction = 'Stop'}
}
Process {
try {
# Get all the provisioned packages
$Packages = Get-Item -Path 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Appx\AppxAllUserStore\Applications' @HT |
Get-ChildItem @HT
} catch {
Write-Warning -Message "Failed to get packages list from the registry or filesystem because $($_.Exception.Message)"
}
if ($Packages) {
if ($Filter) {
$Packages = $Packages | Where-Object {$_.Name -like "$($Filter)" }
}
$Packages |
ForEach-Object -Process {
$p = $_
$PackageName = $PackagePath = $null
# get package name & path
try {
$d = $p | Get-ItemProperty @HT
if ($d) {
$PackageName = $d | Select-Object -ExpandProperty PSChildName
$PackagePath = [System.Environment]::ExpandEnvironmentVariables(($d| Select-Object -ExpandProperty Path))
}
} catch {
Write-Warning -Message "Failed to get package name and path for package $($p) because $($_.Exception.Message)"
}
# register the package
if ($PackageName -and $PackagePath) {
Write-Verbose -Message "Attempting to register package: $($PackageName)"
if ($PSCmdlet.ShouldProcess("$($PackageName))",'Register Appx package')) {
try {
Add-AppxPackage -Register $PackagePath -DisableDevelopmentMode @HT
Write-Verbose -Message "Successfully registered package from path $($PackagePath)"
} catch {
Write-Warning -Message "Failed to regiter from $($PackagePath) because $($_.Exception.Message)"
}
}
}
}
}
}
End {}
}

#PowerShell at #Microsoft #MSIgnite

The Microsoft Ignite conference took place this year on https://myignite.microsoft.com/home

Here’s some content related to #PowerShell, what else ! (NB: it’s not the only content in this year’s event):

  • Taking your automation to the next level with #PowerShell 7

Please allow me to add a quick note about the presentation and this slide:

WinRM and Remoting have been released in PowerShell 2.0 with the RTM release of Windows 7 and 2008 R2.
It means Remoting is available since August 2009.
PowerShell 3.0 was released in 2012 and shipped in Windows 8 and Server 2012.

What Joey Aiello meant on the slide is that Remoting is enabled by default since Windows Server 2012, released in September 2012.

Source: https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_remote_requirements

Windows Server 2012 and newer releases of Windows Server are enabled for PowerShell remoting by default.

Remoting is the killer feature that made me adopt PowerShell in 2009, it’s so cool because it enables remote management of anything in a secure manner 😎
Remoting is on and rocks since 11 years.

Please let me also add another thought about this presentation.
Windows PowerShell is part of the WMF, a.k.a. the Windows Management Framework that adds these components:

So, my question is: what are the investments made into the above components of the WMF since Microsoft open-sourced PowerShell?

  • PowerShell Unplugged – Challenge Edition

What a mind-blowing kickboxing match of new features πŸ˜‰ !

History of previous feature updates

  • Context

I’ve discovered that the history of previous versions you had before running a feature update is stored in the registry under HKLM\System\Setup

  • Solution

I wrote a function to read the information under this key:

Function Get-PreviousUpgrade {
<#
.SYNOPSIS
Get info about previous feature updates
.DESCRIPTION
Get info about previous feature updates being used to upgrade Windows 10 to a new branch.
.EXAMPLE
Get-PreviousUpgrade
#>
[CmdletBinding()]
Param()
Begin {}
Process {}
End {
try {
$names = (Get-Item -Path 'HKLM:\SYSTEM\Setup' -ErrorAction Stop).GetSubKeyNames() |
Where-Object { $_ -match '^Source\sOS\s\(Updated\son\s' }
} catch {
Write-Warning -Message "Failed to read the registry key because $($_.Exception.Message)"
}
if ($names) {
$names |
ForEach-Object {
$date = ([regex]'^Source\sOS\s\(Updated\son\s(?<Date>.+)\)').Matches($_) |
Select-Object -ExpandProperty Groups |
Select-Object -Last 1 -ExpandProperty Value
$FromBranch = (Get-ItemProperty -Path (Join-Path -Path 'HKLM:\SYSTEM\Setup' -ChildPath $_) -Name 'ReleaseId').'ReleaseId'
[PSCustomObject]@{
Date = [datetime]$date
Branch = $FromBranch
}
}
} else {
Write-Warning -Message 'No previous upgrades found'
}
}
}

Here’s how to use it πŸ™‚

Get-PreviousUpgrade | 
Where  { $_.Branch -eq '1809' } | 
Select -Property Date

Friday fun: Windows Feature Update progress

  • Context

I started recently to launch some feature updates of Windows 10 in a scheduled task running as ‘NT AUTHORIY\SYSTEM’ account. It also means that the UI is hidden for whoever is logged on the computer.

I was also looking for some additional info about:
– how to detect that a feature is running
– how to detect that a feature has just finished running
– how to detect that a reboot is pending because of the feature update that just ran

  • Solution

I started investigating using procmon.exe and tracked what’s written under HKLM\System\setup.

I noticed that I can track the progress of a feature update by using a one-liner:

While (ps setup* ) { Write-Progress -Activity Upgrade -PercentComplete "$((gp HKLM:\SYSTEM\Setup\MoSetup\Volatile -Name SetupProgress).SetupProgress)" -Status "at $((gp HKLM:\SYSTEM\Setup\MoSetup\Volatile -Name SetupProgress).SetupProgress)%" }

It appears that a SetupProgress value is written under the registry key HKLM:\SYSTEM\Setup\MoSetup\Volatile
Here’s a more readable splatted version of the one-liner:

While (Get-Process -Name setup*) {
 $k = 'HKLM:\SYSTEM\Setup\MoSetup\Volatile'
 $n = 'SetupProgress'
 $p = Get-ItemProperty -Path $k -Name $n
 $HT = @{
  Activity =  'Upgrade'
  PercentComplete = "$($($p).$n)"
  Status = "at $($($p).$n)%"
 }
 Write-Progress @HT
}

Quick post about MBSA

  • Context

A PM.org member asked to the list:

We use MBSA with a few down-level OS’s as well as Windows 10 in several offline environments, and until this month, it still worked.

  • Problem

It appears that MBSA isn’t supported anymore: https://docs.microsoft.com/en-us/windows/security/threat-protection/mbsa-removal-and-guidance

  • Solution

Another member suggested the following:

Why not use WUA script to scan for updates offline ?
https://docs.microsoft.com/en-us/windows/win32/wua_sdk/using-wua-to-scan-for-updates-offline

If you’ve a downloaded version of the wsusscn2.cab file that you indicate as the FilePath parameter of the following script, you can do:

Start-WUOfflineScan -FilePath C:\temp\wsusscn2.cab -Verbose

Function Start-WUOfflineScan {
<#
.SYNOPSIS
Start an offline WUA scan
.DESCRIPTION
Start an offline WUA (Windows Update Agent) scan using wsusscn2.cab.
.PARAMETER FilePath
Specifies the path to the wsusscn2.cab file to be used to perform the offline scan.
.PARAMETER IncludeSupersededUpdate
Specifies to include superseded updates in the results if any.
.EXAMPLE
Start-WUOfflineScan -FilePath C:\temp\wsusscn2.cab -Verbose
.NOTES
You can get the offline wsusscn2.cab from:
http://download.windowsupdate.com/microsoftupdate/v6/wsusscan/wsusscn2.cab
Official doc is:
https://docs.microsoft.com/en-us/windows/win32/wua_sdk/using-wua-to-scan-for-updates-offline
#>
[CmdletBinding()]
Param(
[Parameter(Mandatory)]
[ValidateScript({Test-Path -Path $_ -PathType Leaf})]
$FilePath,
[switch]$IncludeSupersededUpdate
)
Begin {
if ($FilePath -match '^~') {
$FilePath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($FilePath)
}
try {
$UpdateSvc = New-Object -ComObject Microsoft.Update.ServiceManager
} catch {
Write-Warning -Message "Failed to query if server is configured against a WSUS server because $($_.Exception.Message)"
}
Function Test-WUARebootRequired {
try {
(New-Object -ComObject 'Microsoft.Update.SystemInfo').RebootRequired
} catch {
Write-Warning -Message "Failed to query COM object because $($_.Exception.Message)"
}
}
}
Process {
if (-not(Test-WUARebootRequired)) {
try {
# Create a session
$Session = New-Object -ComObject Microsoft.Update.Session
# Import the the offline cab
$UpdateService = $UpdateSvc.AddScanPackageService('Offline Sync Service',"$($FilePath)", 1)
$Searcher = $Session.CreateUpdateSearcher()
$Searcher.ServerSelection = 3 #ssOthers
if ($IncludeSupersededUpdate) {
$Searcher.IncludePotentiallySupersededUpdates = $true
}
$Searcher.ServiceID = $UpdateService.ServiceID.ToString()
$Criteria = "IsInstalled=0 and DeploymentAction='Installation' or IsPresent=1 and DeploymentAction='Uninstallation' or IsInstalled=1 and DeploymentAction='Installation' and RebootRequired=1 or IsInstalled=0 and DeploymentAction='Uninstallation' and RebootRequired=1"
# Search for updates
$SearchResult = $Searcher.Search($Criteria)
} catch {
Write-Warning -Message "Failed to search for missing updates because $($_.Exception.Message)"
if ($SearchResult) {
Write-Warning -Message "The search result status code was $($SearchResult.ResultCode)"
}
break
}
if ($SearchResult.ResultCode -eq 2) {
# Output what was found
if (($SearchResult.Updates).Count -ne 0) {
$SearchResult.Updates |
ForEach-Object {
Write-Verbose -Message "Missing update: $($_.Title)"
$_
}
} else {
Write-Verbose -Message 'There are no updates to install' -Verbose
}
} else {
Write-Warning -Message 'Failed to search for updates'
Write-Warning -Message "The search result status code was $($SearchResult.ResultCode)"
}
} else {
Write-Warning -Message 'A reboot is pending'
}
}
End {}
}

MSEdge start page

  • Context

I needed to configure a few Windows 10 computers as if they were a kiosk computer.
These 5 computers are part of a workgroup. They are not part of a domain or AD joined or whatever.
I installed the new Microsoft Edge (chromium based) browser and have been tasked to set a start page.

  • Problem

Some settings that can be defined to set the StartPage for example don’t work when the computer is part of a workgroup.
They only work when the computer is part of a domain or is managed by a MDM 😦

It feels like the Apple guru told me that I cannot change the default browser on an iPhone, that Safari is good for me and that I’ve to stick with it 😦 D’oh!

The behavior is documented on this page and says:

If you browse the address edge://policy, you can see the settings that are not applied. Their Status reports ‘Error, Ignored’:

  • Solution

I took me 4 days to come up with a solution. 2 more days than I took me to come up with ideas that led to @sys:doesnotexist : Disallow AutoPlay/Autorun from Autorun.inf.

It wasn’t straightforward. Here’s a quick overview of the ideas I explored:

  1. Observe the behavior of msedge.exe: what dll it loads, what registry keys it queries to find out that the computer is part of a workgroup and is not domain joined or Azure AD joined.
  2. Lie somehow to the computer and make it believe it’s part of a domain.
  3. Provision a temporary Azure AD tenant, configure Intune, push a MSEdge configuration, enroll the device and observe what’s being done at enrolloment.

It appears that MSEdge.exe reads all the registry values under HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Edge.
It determines after that if the device is part of a workgroup, or is domain-joined or AAD-joined.
It uses and loads the following Dlls: C:\Windows\System32\mdmregistration.dll, C:\Windows\System32\dsreg.dll,…

To join the computer to a domain that’s unreachable. I provisioned a offline blob using djoin.exe.

.\djoin.exe /requestODJ /loadfile C:\blob.dat /windowspath C:\Windows /localos

The 3 most interesting registry keys written by the above djoin.exe command are:
HKLM\SYSTEM\CurrentControlSet\Services\Netlogon\JoinDomain
HKLM\CurrentControlSet\Control\Lsa\OfflineProvisioning
HKLM\SYSTEM\CurrentControlSet\Control\Lsa\OfflineJoin

The problem with an offline join is that there’s a slowdown because the netlogon service is trying to reach the domain controller and the Built-in local groups (users and administrators) have the SID of the Domain Users and Domain Admins assigned as members.

The Intune provisioning path is actually the most promising. It takes only a few hours to configure before you can enroll the device. The device should be Azure AD joined and not Azure AD registered to have its security baseline being pushed.

Procmon.exe shows that MSEdge.exe reads the following registry keys:

It looks first for the key name (a GUID) under HKLM\SOFTWARE\Microsoft\Provisioning\OMADM\Accounts.

It uses this key name to open HKLM\SOFTWARE\Microsoft\Enrollments\ and look for a DWORD value named EnrollmentType

0x0 means not enrolled or to be reset according to this page, 0x6 MDM enrolled.

It also reads HKLM\SYSTEM\CurrentControlSet\Control\CloudDomainJoin but this key and its info aren’t required to make MSEdge.exe think it’s MDM enrolled.

Here’s my solution:

@'
Windows Registry Editor Version 5.00
[HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Edge]
"BrowserSignin"=dword:00000000
"BrowserAddProfileEnabled"=dword:00000000
"NonRemovableProfileEnabled"=dword:00000001
"HomepageIsNewTabPage"=dword:00000000
"NewTabPageLocation"="about:blank"
"SmartScreenEnabled"=dword:00000001
"SSLErrorOverrideAllowed"=dword:00000000
"ShowHomeButton"=dword:00000001
"HideFirstRunExperience"=dword:00000001
"PasswordManagerEnabled"=dword:00000000
"SSLVersionMin"="tls1.2"
"RestoreOnStartup"=dword:00000004
"SitePerProcess"=dword:00000001
"AuthSchemes"="ntlm,negotiate"
"SmartScreenPuaEnabled"=dword:00000001
"PreventSmartScreenPromptOverride"=dword:00000001
"DefaultPluginsSetting"=dword:00000002
"NativeMessagingUserLevelHosts"=dword:00000000
"HomepageLocation"="https://myhomepage&quot;
"ClearBrowsingDataOnExit"=dword:00000001
"PreventSmartScreenPromptOverrideForFiles"=dword:00000001
[HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Edge\ExtensionInstallBlocklist]
"1"="*"
[HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Edge\RestoreOnStartupURLs]
"1"="https://myhomepage&quot;
[-HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Provisioning\OMADM\Accounts]
[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Provisioning\OMADM\Accounts]
[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Provisioning\OMADM\Accounts\FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF]
[-HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Enrollments\FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF]
[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Enrollments\FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF]
"EnrollmentType"=dword:00000000
'@ |
Out-File 'C:\Windows\edge.dat' -Encoding 'Ascii'
regedit.exe --% /s C:\Windows\edge.dat
view raw fix-MSedge.ps1 hosted with ❤ by GitHub

At the end, I’ve got Edge Chromium running on a workgroup based computer, reading and applying all the settings under the HKLM\SOFTWARE\Policies key including the RestoreOnStartup and the HomepageLocation values.

Happy days 😎

AutoLogon on Windows 10

  • Context

I’ve been using Autologon for a while on some dedicated computers since Windows XP.
I needed it to configure the user environment/session on hundreds of computers. In other words, I simply needed automation. The combination of both the autologon and a logonscript allowed to do it quickly in a consistent, repeated and reliable manner.
It worked well on Windows 7 by following the steps described in this support page, named How to turn on automatic logon in Windows and without modifying anything in the registry keys used to set it up on Windows XP.

  • Problem

We had to change our Windows 7 computers before its end of life on the 14th of January, 2020.
We decided to use the 1909 branch of Windows 10 but failed to get the autologon working using the technique and registry keys that worked on Windows 7 and since XP 😦
The above support page were not updated yet. In January 2020, it still applied to Windows XP, Windows 7,…
Now that it has been updated, any client Windows operating system has been removed.

We were not the only ones who reported the same issue: it just didn’t work on Windows 10.

There was no explanation on the behavior we observed and no official guidance on how to do it.

There was something underlying that was modifying the registry values. The result was that autologon failed.
It looked like the keyboard layout behavior I described earlier in this blog post.

  • Solution

I used the technique I’ve shown in my previous blog post. Running a scheduled task as NT AUTHORITY\SYSTEM account (S-1-5-18) that sets the registry keys made our day πŸ™‚

Here’s what the code looks like in the post-installation script of our computers.
There’s a script autologon.ps1 dropped in C:\Windows.
It already contains everything required to autologon. The domain name, the username, the password,..
It first takes the last 2 digits of the computername and uses it in the username.
All the standard domain accounts have the same password and they can only logon interactively. There’s a group policy that prevents any lateral movement so that the credential of one account cannot be used on the other computer over the wire.

@'
$desk = -join "$($env:computername)"[-2..-1]
$key = 'HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon'
& (Get-Command "$($env:systemroot)\system32\reg.exe") @('delete',"$($key)",'/v','AutoLogonCount','/f')
& (Get-Command "$($env:systemroot)\system32\reg.exe") @('delete',"$($key)",'/v','AutoLogonChecked','/f')
& (Get-Command "$($env:systemroot)\system32\reg.exe") @('add',"$($key)",'/v','DefaultUserName','/t','REG_SZ','/d',"MyUserPrefix$($desk)",'/f')
& (Get-Command "$($env:systemroot)\system32\reg.exe") @('add',"$($key)",'/v','DefaultPassword','/t','REG_SZ','/d','""','/f')
& (Get-Command "$($env:systemroot)\system32\reg.exe") @('add',"$($key)",'/v','DefaultDomainName','/t','REG_SZ','/d','MyDomainName','/f')
& (Get-Command "$($env:systemroot)\system32\reg.exe") @('add',"$($key)",'/v','AutoAdminLogon','/t','REG_SZ','/d','1','/f')
& (Get-Command "$($env:systemroot)\system32\reg.exe") @('add',"$($key)",'/v','ForceAutoLogon','/t','REG_DWORD','/d','0x1','/f')
'@ | Out-File 'C:\Windows\autologon.ps1' -Encoding 'Ascii'
try {
$errHT = @{ ErrorAction = 'Stop' }
$aHT = @{
Execute = 'C:\Windows\system32\WindowsPowerShell\v1.0\powershell.exe'
Argument = '-Exec Bypass -File "C:\Windows\autologon.ps1"'
}
$HT = @{
TaskName = 'Set-AutoLogon'
User = 'S-1-5-18' #'nt authority\system'
Force = [switch]::Present
Action = (New-ScheduledTaskAction @aHT @errHT)
}
Register-ScheduledTask @HT @errHT
Write-Verbose -Message "Successfully registered autlogon scheduled task" -Verbose
} catch {
Write-Warning -Message "Failed to register autologon scheduled task because $($_.Exception.Message)"
}
view raw autologon.ps1 hosted with ❤ by GitHub

About the Applocker service

  • Problem

I use both PowerShell and Applocker a lot. It’s quite natural to do the following

Set-Service -Name AppIDSvc -StartupType Automatic

Instead of configuring it, I get an ‘Access Denied’ 😦

  • Cause

It appears that

Starting with Windows 10, the Application Identity service is now a protected process. Because of this, you can no longer manually set the service Startup type to Automatic by using the Sevices snap-in.

Source: https://docs.microsoft.com/en-us/windows/security/threat-protection/windows-defender-application-control/applocker/configure-the-application-identity-service

It also documents the official two ways of configuring the service StartupType to automatic.
Note that if your device is domain joined, you can use a Domain based GPO to change the service StartupType instead of using LGPO.exe

It’s protected and I can see in the registry the following indicator: the LaunchProtected dword value set to 0x2

  • Solution

I came up with a 3rd (longer) way of doing it (that could be a very long one-liner).

# Get the StartupType
Get-CimInstance -ClassName Win32_Service -Filter "Name='AppIDSvc'"

$cmd = 'Set-Service -Name AppIDSvc -StartupType Auto'
$aHT = @{
 Execute = 'C:\Windows\system32\WindowsPowerShell\v1.0\powershell.exe'
 Argument = '-Exec Bypass -Command "{0}"' -f "$($cmd)"
}
$HT = @{
 TaskName = 'ConfigAppIdSvc'
 User = 'S-1-5-18' # 'NT Authority\System'
 Force = [switch]::Present
 Action = (New-ScheduledTaskAction @aHT)
}
Register-ScheduledTask @HT | 
Start-ScheduledTask

# Wait a little bit and get the StartupType
Get-CimInstance -ClassName Win32_Service -Filter "Name='AppIDSvc'"

NB: Although the service is running under the NT Authority\LocalService (S-1-5-19), it requires the NT Authority\System (S-1-5-18) to modify its StartupType.