Skip to content

Commit

Permalink
Implement a generic way of analyzing an object's DACL
Browse files Browse the repository at this point in the history
  • Loading branch information
itm4n committed Jan 8, 2025
1 parent 7f7387a commit 9a16b10
Show file tree
Hide file tree
Showing 5 changed files with 166 additions and 41 deletions.
2 changes: 2 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@
"harmj",
"Hijackable",
"HISI",
"HKCC",
"HKCR",
"HKCU",
"HKEY",
"HKLM",
Expand Down
1 change: 1 addition & 0 deletions info/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### Added

- Wrapper function for querying an object's security information in a generic way.
- Implement a generic way of analyzing an object's DACL.

### Changed

Expand Down
87 changes: 84 additions & 3 deletions src/core/WinApi.Wrappers.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -129,12 +129,18 @@ function Get-FileHandle {

[UInt32] $AccessRights = $script:FileAccessRight::GenericRead,

[UInt32] $ShareMode = $script:FileShareMode::None
[UInt32] $ShareMode = $script:FileShareMode::Read + $script:FileShareMode::Write + $script:FileShareMode::Delete,

[Switch] $Directory = $false
)

process {
$FlagsAndAttributes = 0

# A directory must be opened with the flag FILE_FLAG_BACKUP_SEMANTICS
if ($Directory) { $FlagsAndAttributes = $FlagsAndAttributes -bor 0x02000000 }

$FileHandle = $script:Kernel32::CreateFile($Path, $AccessRights, $ShareMode, [IntPtr]::Zero, 3, 0, [IntPtr]::Zero)
$FileHandle = $script:Kernel32::CreateFile($Path, $AccessRights, $ShareMode, [IntPtr]::Zero, 3, $FlagsAndAttributes, [IntPtr]::Zero)
if ($FileHandle -eq -1) {
$LastError = [Runtime.InteropServices.Marshal]::GetLastWin32Error()
Write-Warning "CreateFile('$($Path)') - $(Format-Error $LastError)"
Expand All @@ -144,6 +150,81 @@ function Get-FileHandle {
}
}

function Get-RegistryKeyHandle {
<#
.SYNOPSIS
Wrapper - Open a file using the 'RegOpenKeyEx' Windows API.
Author: @itm4n
License: BSD 3-Clause
.DESCRIPTION
This cmdlet is a wrapper for the 'RegOpenKeyEx' Windows API. It attempts to open a registry key using a user-supplied path.
.PARAMETER Path
A mandatory parameter representing the path of a registry key to open.
.PARAMETER AccessRights
An optional set of registry key access rights. If not specified, this parameter defaults to $script:RegistryKeyAccessRight::GenericRead.
.EXAMPLE
PS C:\> $KeyHandle = Get-RegistryKeyHandle -Path "HKLM\SYSTEM\CurrentControlSet\Control\Lsa"
PS C:\> $script:Kernel32::CloseHandle($KeyHandle)
.EXAMPLE
PS C:\> $KeyHandle = Get-RegistryKeyHandle -Path "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Lsa"
PS C:\> $script:Kernel32::CloseHandle($KeyHandle)
.EXAMPLE
PS C:\> $KeyHandle = Get-RegistryKeyHandle -Path 'HKLM\SYSTEM\CurrentControlSet\Control\Lsa' -AccessRights $script:RegistryKeyAccessRight::ReadControl
PS C:\> $script:Kernel32::CloseHandle($KeyHandle)
#>

[OutputType([IntPtr])]
[CmdletBinding()]
param (
[Parameter(Position=0, Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[String] $Path,

[UInt32] $AccessRights = $script:RegistryKeyAccessRight::GenericRead
)

begin {
$RootKeyShortNames = @{
"HKEY_CLASSES_ROOT" = "HKCR"
"HKEY_CURRENT_CONFIG" = "HKCC"
"HKEY_CURRENT_USER" = "HKCU"
"HKEY_LOCAL_MACHINE" = "HKLM"
"HKEY_USERS" = "HKU"
}
}

process {
$RootKeyShortNames.Keys | ForEach-Object { $Path = $Path -replace $_, $RootKeyShortNames[$_] }
$RootKeyName = $Path.Split('\')[0]
$RootKeyPath = ($Path -replace $RootKeyName, "").Trim('\')

switch ($RootKeyName) {
"HKCR" { $RootKeyValue = 0x80000000 }
"HKCU" { $RootKeyValue = 0x80000001 }
"HKLM" { $RootKeyValue = 0x80000002 }
"HKU" { $RootKeyValue = 0x80000003 }
"HKCC" { $RootKeyValue = 0x80000005 }
default { throw "Unhandled root key: $($RootKeyName)" }
}

$RegistryKeyHandle = [IntPtr]::Zero
$Status = $script:Advapi32::RegOpenKeyEx($RootKeyValue, $RootKeyPath, 0, $AccessRights, [ref] $RegistryKeyHandle)
if ($Status -ne $script:SystemErrorCode::ERROR_SUCCESS) {
$LastError = [Runtime.InteropServices.Marshal]::GetLastWin32Error()
Write-Warning "RegOpenKeyEx('$($Path)') - $(Format-Error $LastError)"
}

return $RegistryKeyHandle
}
}

function Get-ServiceHandle {
<#
.SYNOPSIS
Expand Down Expand Up @@ -1396,7 +1477,7 @@ function Get-ObjectSecurityInfo {
[IntPtr] $Handle,

[Parameter(Mandatory=$true)]
[ValidateSet("File", "Directory", "Service")]
[ValidateSet("File", "Directory", "RegistryKey", "Service")]
[String] $Type
)

Expand Down
1 change: 1 addition & 0 deletions src/core/WinApi.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ $FunctionDefinitions = @(
(New-Function advapi32 ConvertStringSecurityDescriptorToSecurityDescriptor ([Bool]) @([String], [UInt32], [IntPtr].MakeByRefType(), [UInt32].MakeByRefType()) ([Runtime.InteropServices.CallingConvention]::Winapi) ([Runtime.InteropServices.CharSet]::Unicode) -SetLastError -EntryPoint ConvertStringSecurityDescriptorToSecurityDescriptorW),
(New-Function advapi32 GetSidSubAuthority ([IntPtr]) @([IntPtr], [UInt32]) -SetLastError -EntryPoint GetSidSubAuthority),
(New-Function advapi32 GetSidSubAuthorityCount ([IntPtr]) @([IntPtr]) -SetLastError -EntryPoint GetSidSubAuthorityCount),
(New-Function advapi32 RegOpenKeyEx ([UInt32]) @([IntPtr], [String], [UInt32], [UInt32], [IntPtr].MakeByRefType()) ([Runtime.InteropServices.CallingConvention]::Winapi) ([Runtime.InteropServices.CharSet]::Unicode) -SetLastError -EntryPoint RegOpenKeyExW),

(New-Function firewallapi FWOpenPolicyStore ([Void]) @([UInt32], [IntPtr], $script:FW_STORE_TYPE, $script:FW_POLICY_ACCESS_RIGHT, [UInt32], [IntPtr].MakeByRefType()) -EntryPoint FWOpenPolicyStore),
(New-Function firewallapi FWClosePolicyStore ([UInt32]) @([IntPtr]) -EntryPoint FWClosePolicyStore),
Expand Down
116 changes: 78 additions & 38 deletions src/helper/AccessControl.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,13 @@ function Get-ModificationRight {
[String] $Path,

[Parameter(Mandatory=$true)]
[ValidateSet("File", "Directory", "RegistryKey")]
[ValidateSet("File", "Directory", "RegistryKey", "Service")]
[String] $Type
)

begin {
$Handle = [IntPtr]::Zero

$FileModificationRights = @(
$script:FileAccessRight::WriteData,
$script:FileAccessRight::AppendData,
Expand Down Expand Up @@ -83,6 +85,13 @@ function Get-ModificationRight {
$script:RegistryKeyAccessRight::AllAccess
)

$ServiceModificationRights = @(
$script:ServiceAccessRight::ChangeConfig,
$script:ServiceAccessRight::WriteDac,
$script:ServiceAccessRight::WriteOwner,
$script:ServiceAccessRight::AllAccess
)

$CurrentUserSids = Get-CurrentUserSid
$CurrentUserDenySids = Get-CurrentUserDenySid

Expand Down Expand Up @@ -111,42 +120,35 @@ function Get-ModificationRight {
switch ($Type) {
"File" {
$AccessRights = $script:FileAccessRight
$AccessRightsType = "FileSystemRights"
$ModificationRights = $FileModificationRights
$Handle = Get-FileHandle -Path $Path -AccessRights $script:FileAccessRight::ReadControl
}
"Directory" {
$AccessRights = $script:DirectoryAccessRight
$AccessRightsType = "FileSystemRights"
$ModificationRights = $DirectoryModificationRights
$Handle = Get-FileHandle -Path $Path -AccessRights $script:DirectoryAccessRight::ReadControl -Directory
}
"RegistryKey" {
$AccessRights = $script:RegistryKeyAccessRight
$AccessRightsType = "RegistryRights"
$ModificationRights = $RegistryKeyModificationRights
$Handle = Get-RegistryKeyHandle -Path $Path -AccessRights $script:RegistryKeyAccessRight::ReadControl
}
"Service" {
$AccessRights = $script:ServiceAccessRight
$ModificationRights = $ServiceModificationRights
$Handle = Get-ServiceHandle -Name $Path -AccessRights $script:ServiceAccessRight::ReadControl
}
default {
throw "Unhandled object type: $($Type)"
}
}

# First things first, try to get the ACL of the object given its path.
$Acl = Get-Acl -Path $Path -ErrorAction SilentlyContinue -ErrorVariable GetAclError
if ($GetAclError) { return }

# If no ACL is returned, it means that the object has a "null" DACL, in which case everyone is
# granted full access to the object. We can therefore simply return a "virtual" ACE that grants
# Everyone the "FullControl" right and exit.
if ($null -eq $Acl) {
$Result = New-Object -TypeName PSObject
$Result | Add-Member -MemberType "NoteProperty" -Name "ModifiablePath" -Value $Path
$Result | Add-Member -MemberType "NoteProperty" -Name "IdentityReference" -Value (Convert-SidToName -Sid "S-1-1-0")
$Result | Add-Member -MemberType "NoteProperty" -Name "Permissions" -Value $AccessRights::AllAccess
$Result
return
}
$SecurityInfo = Get-ObjectSecurityInfo -Handle $Handle -Type $Type
if ($null -eq $SecurityInfo) { return }

$DenyAces = [Object[]]($Acl | Select-Object -ExpandProperty Access | Where-Object { $_.AccessControlType -match "Deny" })
$AllowAces = [Object[]]($Acl | Select-Object -ExpandProperty Access | Where-Object { $_.AccessControlType -match "Allow" })
$DenyAces = $SecurityInfo.Dacl | Where-Object { $_.AceType -eq "AccessDenied" }
$AllowAces = $SecurityInfo.Dacl | Where-Object { $_.AceType -eq "AccessAllowed" }

# Before checking the object permissions, we first need to enumerate deny ACEs (if any) that
# would restrict the rights we may have on the target object.
Expand All @@ -158,19 +160,18 @@ function Get-ModificationRight {

# Ignore "InheritOnly" ACEs because they only apply to child objects, not to the object itself
# (e.g.: a file in a directory or a sub-key of a registry key).
if ($DenyAce.PropagationFlags -band ([System.Security.AccessControl.PropagationFlags]"InheritOnly").value__) { continue }
if ($DenyAce.PropagationFlags -band [System.Security.AccessControl.PropagationFlags]::InheritOnly.value__) { continue }

# Convert the ACE's identity reference name to its SID. If the SID is not in the list
# of deny-only SIDs of the current Token, ignore it. If the SID does not match the
# current user SID or the SID of any of its groups, ignore it as well.
# Note: deny-only SIDs are only used to check access-denied ACEs.
# https://docs.microsoft.com/en-us/windows/win32/secauthz/sid-attributes-in-an-access-token
$IdentityReferenceSid = Convert-NameToSid -Name $DenyAce.IdentityReference
if ($CurrentUserDenySids -notcontains $IdentityReferenceSid) { continue }
if ($CurrentUserSids -notcontains $IdentityReferenceSid) { continue }
if ($CurrentUserDenySids -notcontains $DenyAce.SecurityIdentifier) { continue }
if ($CurrentUserSids -notcontains $DenyAce.SecurityIdentifier) { continue }

$AccessRights.GetEnumValues() |
Where-Object { ($DenyAce.$AccessRightsType.value__ -band $AccessRights::$_.value__) -eq $AccessRights::$_.value__ } |
Where-Object { ($DenyAce.AccessMask -band $AccessRights::$_.value__) -eq $AccessRights::$_.value__ } |
ForEach-Object { $Restrictions += $_ }
}
}
Expand All @@ -182,12 +183,12 @@ function Get-ModificationRight {

# Ignore "InheritOnly" ACEs because they only apply to child objects, not to the object itself
# (e.g.: a file in a directory or a sub-key of a registry key).
if ($AllowAce.PropagationFlags -band ([System.Security.AccessControl.PropagationFlags]"InheritOnly").value__) { continue }
if ($AllowAce.PropagationFlags -band [System.Security.AccessControl.PropagationFlags]::InheritOnly.value__) { continue }

# Here, we simply extract the permissions granted by the current ACE
$Permissions = @()
$AccessRights.GetEnumValues() |
Where-Object { ($AllowAce.$AccessRightsType.value__ -band $AccessRights::$_.value__) -eq $AccessRights::$_.value__ } |
Where-Object { ($AllowAce.AccessMask -band $AccessRights::$_.value__) -eq $AccessRights::$_.value__ } |
ForEach-Object { $Permissions += $_ }

# If the ACE grants 'AllAccess', then all access rights match. In such a case,
Expand All @@ -202,22 +203,57 @@ function Get-ModificationRight {

# Here, we filter out ACEs that do not apply to the current user by checking whether the ACE's
# identity reference is in the current user's SID list.
$IdentityReferenceSid = Convert-NameToSid -Name $AllowAce.IdentityReference
if ($CurrentUserSids -notcontains $IdentityReferenceSid) { continue }
if ($CurrentUserSids -notcontains $AllowAce.SecurityIdentifier) { continue }

# We compare the list of permissions (minus the potential restrictions) against a list of
# predefined modification rights. If there is no match, we ignore the ACE.
$GrantedModificationRights = $Permissions | Where-Object { $ModificationRights -contains $_ }
if ($null -eq $GrantedModificationRights) { continue }

$ResolvedIdentity = Convert-SidToName -Sid $AllowAce.SecurityIdentifier
if ($ResolvedIdentity) {
$IdentityReference = "$($ResolvedIdentity) ($($AllowAce.SecurityIdentifier))"
}
else {
$IdentityReference = $AllowAce.SecurityIdentifier
}

$Result = New-Object -TypeName PSObject
$Result | Add-Member -MemberType "NoteProperty" -Name "ModifiablePath" -Value $Path
$Result | Add-Member -MemberType "NoteProperty" -Name "IdentityReference" -Value $AllowAce.IdentityReference
$Result | Add-Member -MemberType "NoteProperty" -Name "IdentityReference" -Value $IdentityReference
$Result | Add-Member -MemberType "NoteProperty" -Name "Permissions" -Value $Permissions
$Result
}
}
}

end {
switch ($Type) {
"File" {
if (($Handle -ne [IntPtr]::Zero) -and ($Handle -ne -1)) {
$null = $script:Kernel32::CloseHandle($Handle)
}
}
"Directory" {
if (($Handle -ne [IntPtr]::Zero) -and ($Handle -ne -1)) {
$null = $script:Kernel32::CloseHandle($Handle)
}
}
"RegistryKey" {
if ($Handle -ne [IntPtr]::Zero) {
$null = $script:Kernel32::CloseHandle($Handle)
}
}
"Service" {
if ($Handle -ne [IntPtr]::Zero) {
$null = $script:Advapi32::CloseServiceHandle($Handle)
}
}
default {
throw "Unhandled object type: $($Type)"
}
}
}
}

function Get-ModifiablePath {
Expand Down Expand Up @@ -321,9 +357,7 @@ function Get-ModifiableRegistryPath {

process {
$Path | ForEach-Object {
$RegPath = "Registry::$($_)"
$OrigPath = $_
Get-ModificationRight -Path $RegPath -Type RegistryKey | ForEach-Object { $_.ModifiablePath = $OrigPath; $_ }
Get-ModificationRight -Path $_ -Type RegistryKey
}
}
}
Expand Down Expand Up @@ -669,13 +703,13 @@ function Get-ModifiableService {
$ServiceHandle = Get-ServiceHandle -Name $ServiceObject.Name -AccessRights $script:ServiceAccessRight::ReadControl
if ($ServiceHandle -eq [IntPtr]::Zero) { continue }

$ServiceDacl = Get-ServiceDiscretionaryAccessControlList -Handle $ServiceHandle
if ($null -eq $ServiceDacl) {
$SecurityInfo = Get-ObjectSecurityInfo -Handle $ServiceHandle -Type Service
if ($null -eq $SecurityInfo) {
$null = $script:Advapi32::CloseServiceHandle($ServiceHandle)
continue
}

foreach ($Ace in $ServiceDacl) {
foreach ($Ace in $SecurityInfo.Dacl) {

# Ignore ACEs that do not match our identity.
if ($CurrentUserSids -notcontains $Ace.SecurityIdentifier) { continue }
Expand All @@ -686,13 +720,19 @@ function Get-ModifiableService {
continue
}

$Permissions = $Ace.AccessMask -as $script:ServiceAccessRight

foreach ($ModificationPermission in $ModificationPermissions) {
if ((([UInt32] $ModificationPermission) -band $Ace.AccessRights) -eq $ModificationPermission) {

if ($Permissions -contains $ModificationPermission) {

$Result = $ServiceObject.PSObject.Copy()
$Result | Add-Member -MemberType "NoteProperty" -Name "AccessRights" -Value $Ace.AccessRights
$Result | Add-Member -MemberType "NoteProperty" -Name "AccessRights" -Value $Permissions
$Result | Add-Member -MemberType "NoteProperty" -Name "IdentityReference" -Value $(Convert-SidToName -Sid $Ace.SecurityIdentifier)
$Result
}

break
}
}

Expand Down

0 comments on commit 9a16b10

Please sign in to comment.