Last active
October 13, 2021 19:25
-
-
Save brianreitz/f3af4136e2417b8b859018644f97be08 to your computer and use it in GitHub Desktop.
A port of Joakim Schicht's RegKeyFixer using PSReflect. Will find and delete "hidden" value entries created by PSReflect-RegHide.ps1
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# A port of Joakim Schicht's RegKeyFixer in PowerShell. | |
# https://github.com/jschicht/RegKeyFixer | |
# | |
# This script will recursively search keys starting from the Keyname | |
# for any value entry names with null characters | |
# Example usage: | |
# $SID = [System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value | |
# $KeyName = "\Registry\User\$SID\SOFTWARE\Microsoft\Windows\CurrentVersion" | |
# $Results = Get-HiddenNames -KeyName $KeyName | |
# $Results | Remove-HiddenNames | |
# or | |
# Get-HiddenNames -KeyName $KeyName | Remove-HiddenNames | |
# requires PSReflect.ps1 to be saved in the same directory | |
. .\PSReflect.ps1 | |
$Module = New-InMemoryModule -ModuleName RegKeyFixer | |
#region enums/structs | |
$UNICODE_STRING = struct $Module UNICODE_STRING @{ | |
Length = field 0 UInt16 | |
MaximumLength = field 1 UInt16 | |
Buffer = field 2 IntPtr | |
} | |
$LARGE_INTEGER = struct $Module _LARGE_INTEGER @{ | |
QUADPART = field 0 Int64 -Offset 0 | |
LOWPART = field 1 UInt32 -Offset 0 | |
HIGHPART = field 2 Int32 -Offset 4 | |
} -ExplicitLayout | |
$OBJECT_ATTRIBUTES = struct $Module OBJECT_ATTRIBUTES @{ | |
Length = field 0 UInt32 | |
RootDirectory = field 1 IntPtr | |
ObjectName = field 2 IntPtr | |
Attributes = field 3 UInt32 | |
SecurityDescriptor = field 4 IntPtr | |
SecurityQualityOfService = field 5 IntPtr | |
} | |
# ACCESS_MASK enum used to determine key permissions, used by NtOpenKey. | |
$KEY_ACCESS = psenum $Module KEY_ACCESS UInt32 @{ | |
KEY_QUERY_VALUE = 0x0001 | |
KEY_SET_VALUE = 0x0002 | |
KEY_CREATE_SUB_KEY = 0x0004 | |
KEY_ENUMERATE_SUB_KEYS = 0x0008 | |
KEY_NOTIFY = 0x0010 | |
KEY_CREATE_LINK = 0x0020 | |
KEY_WOW64_64KEY = 0x0100 | |
KEY_WOW64_32KEY = 0x0200 | |
KEY_WRITE = 0x20006 | |
KEY_READ = 0x20019 | |
KEY_EXECUTE = 0x20019 | |
KEY_ALL_ACCESS = 0xF003F | |
} -Bitfield | |
# ATTRIBUTES enum passed to an OBJECT_ATTRIBUTES struct. | |
$OBJ_ATTRIBUTE = psenum $Module OBJ_ATTRIBUTE UInt32 @{ | |
OBJ_INHERIT = 0x00000002 | |
OBJ_PERMANENT = 0x00000010 | |
OBJ_EXCLUSIVE = 0x00000020 | |
OBJ_CASE_INSENSITIVE = 0x00000040 | |
OBJ_OPENIF = 0x00000080 | |
OBJ_OPENLINK = 0x00000100 | |
OBJ_KERNEL_HANDLE = 0x00000200 | |
OBJ_FORCE_ACCESS_CHECK = 0x00000400 | |
OBJ_VALID_ATTRIBUTES = 0x000007f2 | |
} -Bitfield | |
$KEY_INFORMATION_CLASS = psenum $Module KEY_INFORMATION_CLASS UInt16 @{ | |
KeyBasicInformation = 0 | |
KeyNodeInformation = 1 | |
KeyFullInformation = 2 | |
KeyNameInformation = 3 | |
KeyCachedInformation = 4 | |
KeyFlagsInformation = 5 | |
KeyVirtualizationInformation = 6 | |
KeyHandleTagsInformation = 7 | |
MaxKeyInfoClass = 8 | |
} | |
$KEY_VALUE_INFORMATION_CLASS = psenum $Module KEY_VALUE_INFORMATION_CLASS UInt16 @{ | |
KeyValueBasicInformation = 0 | |
KeyValueFullInformation = 1 | |
KeyValuePartialInformation = 2 | |
KeyValueFullInformationAlign64 = 3 | |
KeyValuePartialInnformationAlign64= 4 | |
MaxKeyValueInfoClass = 5 | |
} | |
$KEY_FULL_INFORMATION = struct $Module KEY_FULL_INFORMATION @{ | |
LastWriteTime = field 0 $LARGE_INTEGER | |
TitleIndex = field 1 UInt32 | |
ClassOffset = field 2 UInt32 | |
ClassLength = field 3 UInt32 | |
SubKeys = field 4 UInt32 | |
MaxNameLen = field 5 UInt32 | |
MaxClassLen = field 6 UInt32 | |
Values = field 7 UInt32 | |
MaxValueNameLen = field 8 UInt32 | |
MaxValueDataLen = field 9 UInt32 | |
Class = field 10 string -MarshalAs @('ByValTStr', 260) | |
} -CharSet Unicode | |
$KEY_NODE_INFORMATION = struct $Module KEY_NODE_INFORMATION @{ | |
LastWriteTime = field 0 $LARGE_INTEGER | |
TitleIndex = field 1 UInt32 | |
ClassOffset = field 2 UInt32 | |
ClassLength = field 3 UInt32 | |
NameLength = field 4 UInt32 | |
Name = field 5 byte[] -MarshalAs @('ByValArray', 520) | |
} -CharSet Unicode | |
$KEY_VALUE_BASIC_INFORMATION = struct $Module KEY_VALUE_BASIC_INFORMATION @{ | |
TitleIndex = field 0 UInt32 | |
Type = field 1 UInt32 | |
NameLength = field 2 UInt32 | |
Name = field 3 byte[] -MarshalAs @('ByValArray', 520) | |
} -CharSet Unicode | |
$KEY_VALUE_FULL_INFORMATION = struct $Module KEY_VALUE_FULL_INFORMATION @{ | |
TitleIndex = field 0 UInt32 | |
Type = field 1 UInt32 | |
DataOffset = field 2 UInt32 | |
DataLength = field 3 UInt32 | |
NameLength = field 4 UInt32 | |
Name = field 5 byte[] -MarshalAs @('ByValArray', 520) | |
} -CharSet Unicode | |
#endregion | |
#region function definitions | |
$FunctionDefinitions = @( | |
(func ntdll NtOpenKey ([UInt32]) @( | |
[IntPtr].MakeByRefType(), #_Out_ PHANDLE KeyHandle, | |
[Int32], #_In_ ACCESS_MASK DesiredAccess, | |
$OBJECT_ATTRIBUTES.MakeByRefType() #_In_ POBJECT_ATTRIBUTES ObjectAttributes | |
) -EntryPoint NtOpenKey), | |
(func ntdll NtSetValueKey ([UInt32]) @( | |
[IntPtr], #_In_ HANDLE KeyHandle, | |
$UNICODE_STRING.MakeByRefType(), #_In_ PUNICODE_STRING ValueName, | |
[Int32], #_In_opt_ ULONG TitleIndex, | |
[Int32], #_In_ ULONG Type, | |
[IntPtr], #_In_opt_ PVOID Data, | |
[Int32] #_In_ ULONG DataSize | |
) -EntryPoint NtSetValueKey), | |
(func ntdll NtQueryKey ([UInt32]) @( | |
[IntPtr], #_In_ HANDLE KeyHandle, | |
$KEY_INFORMATION_CLASS, #_In_ KEY_INFORMATION_CLASS KeyInformationClass, | |
[IntPtr], #_Out_opt_ PVOID KeyInformation, | |
[UInt32], #_In_ ULONG Length, | |
[UInt32].MakeByRefType() #_Out_ PULONG ResultLength | |
) -EntryPoint NtQueryKey), | |
(func ntdll NtQueryValueKey ([UInt32]) @( | |
[IntPtr], #_In_ HANDLE KeyHandle, | |
$UNICODE_STRING.MakeByRefType(), #_In_ PUNICODE_STRING ValueName, | |
$KEY_VALUE_INFORMATION_CLASS, #_In_ KEY_VALUE_INFORMATION_CLASS KeyValueInformationClass, | |
[IntPtr], #_Out_opt_ PVOID KeyValueInformation, | |
[UInt32], #_In_ ULONG Length, | |
[UInt32].MakeByRefType() #_Out_ PULONG ResultLength | |
) -EntryPoint NtQueryValueKey), | |
(func ntdll NtEnumerateKey ([UInt32]) @( | |
[IntPtr], #_In_ HANDLE KeyHandle, | |
[UInt32], #_In_ ULONG Index, | |
$KEY_INFORMATION_CLASS, #_In_ KEY_INFORMATION_CLASS KeyInformationClass, | |
[IntPtr], #_Out_opt_ PVOID KeyInformation, | |
[UInt32], #_In_ ULONG Length, | |
[UInt32].MakeByRefType() #_Out_ PULONG ResultLength | |
) -EntryPoint NtEnumerateKey), | |
(func ntdll NtEnumerateValueKey ([UInt32]) @( | |
[IntPtr], #_In_ HANDLE KeyHandle, | |
[UInt32], #_In_ ULONG Index, | |
$KEY_VALUE_INFORMATION_CLASS, #_In_ KEY_INFORMATION_CLASS KeyValueInformationClass, | |
[IntPtr], #_Out_opt_ PVOID KeyValueInformation, | |
[UInt32], #_In_ ULONG Length, | |
[UInt32].MakeByRefType() #_Out_ PULONG ResultLength | |
) -EntryPoint NtEnumerateValueKey), | |
(func ntdll NtDeleteKey ([UInt32]) @( | |
[IntPtr] #_In_ HANDLE KeyHandle | |
) -EntryPoint NtDeleteKey), | |
(func ntdll NtDeleteValueKey ([UInt32]) @( | |
[IntPtr], #_In_ HANDLE KeyHandle, | |
$UNICODE_STRING.MakeByRefType() #_In_ PUNICODE_STRING ValueName | |
) -EntryPoint NtDeleteValueKey), | |
(func ntdll NtClose ([UInt32]) @( | |
[IntPtr] #_In_ HANDLE ObjectHandle | |
) -EntryPoint NtClose), | |
(func ntdll RtlInitUnicodeString ([void]) @( | |
$UNICODE_STRING.MakeByRefType(), #_Inout_ PUNICODE_STRING DestinationString | |
[string] #_In_opt_ PCWSTR SourceString | |
) -EntryPoint RtlInitUnicodeString) | |
) | |
$Types = $FunctionDefinitions | Add-Win32Type -Module $Module -Namespace RegHide | |
$ntdll = $Types['ntdll'] | |
#endregion | |
# function to make a UNICODE_STRING of a given length | |
# can be used in place of RtlInitUnicodeString when using null characters | |
function Get-CountedUnicodeString | |
{ | |
param | |
( | |
[Parameter(Mandatory = $true)] | |
[string] | |
$Source | |
) | |
$Output = [Activator]::CreateInstance($UNICODE_STRING) | |
$Output.Length = $Source.Length * 2 | |
$Output.MaximumLength = $Source.Length * 2 | |
$Output.Buffer = [System.Runtime.InteropServices.Marshal]::StringToCoTaskMemUni($Source) | |
Write-Output $Output | |
} | |
# short function to duplicate the InitializeObjectAttributes macro | |
# returns an OBJECT_ATTRIBUTES structure with the given ObjectName | |
function InitializeObjectAttributes | |
{ | |
param | |
( | |
[Parameter()] | |
[string] | |
$ObjectName, | |
[Parameter()] | |
[UInt32] | |
$Attributes = $OBJ_ATTRIBUTE::OBJ_CASE_INSENSITIVE, | |
[Parameter()] | |
[IntPtr] | |
$RootDirectory = [IntPtr]::Zero | |
) | |
$ObjectAttributes = [Activator]::CreateInstance($OBJECT_ATTRIBUTES) | |
$ObjectAttributes.Length = $OBJECT_ATTRIBUTES::GetSize() | |
$ObjectAttributes.Attributes = $Attributes | |
$ObjectAttributes.RootDirectory = $RootDirectory | |
$ObjectAttributes.ObjectName = [System.Runtime.InteropServices.Marshal]::AllocHGlobal($UNICODE_STRING::GetSize()) | |
$ObjectPtr = Get-CountedUnicodeString -Source $ObjectName | |
[System.Runtime.InteropServices.Marshal]::StructureToPtr($ObjectPtr, $ObjectAttributes.ObjectName, $true) | |
$ObjectAttributes.SecurityDescriptor = [IntPtr]::Zero | |
$ObjectAttributes.SecurityQualityOfService = [IntPtr]::Zero | |
# You must free $ObjectAttributes.ObjectName after using the pointer | |
# Otherwise, you'll leak memory | |
# [System.Runtime.InteropServices.Marshal]::FreeHGlobal($ObjectAttributes.ObjectName) | |
Write-Output $ObjectAttributes | |
} | |
# Our main function, this will do a recursive depth-first search of all Key Names and Value Entry Names | |
function Get-HiddenNames | |
{ | |
param | |
( | |
[Parameter(Mandatory = $true)] | |
[string] | |
$KeyName, | |
[Parameter()] | |
[switch] | |
$DeleteEntries | |
) | |
$KeyHandle = [IntPtr]::Zero | |
$DesiredAccess = $KEY_ACCESS::KEY_ALL_ACCESS | |
$ObjectAttributes = InitializeObjectAttributes -ObjectName $KeyName | |
$status = $ntdll::NtOpenKey([ref]$KeyHandle, $DesiredAccess, [ref]$ObjectAttributes) | |
#"OpenKey status: 0x{0:x8}" -f $status | |
# free our alloc'd pointers | |
[System.Runtime.InteropServices.Marshal]::FreeHGlobal($ObjectAttributes.ObjectName) | |
# if we get an NTSTATUS of 0, there were no errors | |
if(!$status) { | |
# initial query to determine size of ptr | |
$KeyInformationClass = $KEY_INFORMATION_CLASS::KeyFullInformation | |
$KeyInfoPtrSize = 0 | |
$status = $ntdll::NtQueryKey($KeyHandle, $KeyInformationClass, 0, $KeyInfoPtrSize, [ref]$KeyInfoPtrSize) | |
#"QueryKey status: 0x{0:x8}" -f $status | |
# second query with correct size should return our data | |
[IntPtr]$KeyInfoPtr = [System.Runtime.InteropServices.Marshal]::AllocHGlobal($KeyInfoPtrSize) | |
$status = $ntdll::NtQueryKey($KeyHandle, $KeyInformationClass, $KeyInfoPtr, $KeyInfoPtrSize, [ref]$KeyInfoPtrSize) | |
#"QueryKey status: 0x{0:x8}" -f $status | |
# cast the result to the correct struct, here's where we'll hit our memory errors if the struct fields are incorrect | |
$KeyFullInformation = $KeyInfoPtr -as $KEY_FULL_INFORMATION | |
[System.Runtime.InteropServices.Marshal]::FreeHGlobal($KeyInfoPtr) # free our alloc'd ptr | |
# begin the recursive lookup | |
If($KeyFullInformation.SubKeys -gt 0) { | |
0..($KeyFullInformation.SubKeys-1) | ForEach-Object { | |
# determine subkey size | |
$SubKeyPtrSize = 0 | |
$status = $ntdll::NtEnumerateKey($KeyHandle, $_, $KEY_INFORMATION_CLASS::KeyNodeInformation, 0, $SubKeyPtrSize, [ref]$SubKeyPtrSize) | |
# if it returns STATUS_NO_MORE_ENTRIES, that means the Index is not valid (i.e. there isn't a subkey at that index) | |
if($status -eq 0x8000001A) { | |
throw [System.IndexOutOfRangeException] "Index out-of-bounds, or the given registry key has no subkeys." | |
} | |
# allocate the correct size and assign the value to our buffer | |
[IntPtr]$SubKeyPtr = [System.Runtime.InteropServices.Marshal]::AllocHGlobal($SubKeyPtrSize) | |
$status = $ntdll::NtEnumerateKey($KeyHandle, $_, $KEY_INFORMATION_CLASS::KeyNodeInformation, $SubKeyPtr, $SubKeyPtrSize, [ref]$SubKeyPtrSize) | |
#"EnumerateValueKey status: 0x{0:x8}" -f $status | |
# because this is recursive, we want to blank out the $KeyNodeInformation var | |
$KeyNodeInformation = $null | |
$KeyNodeInformation = $SubKeyPtr -as $KEY_NODE_INFORMATION | |
[System.Runtime.InteropServices.Marshal]::FreeHGlobal($SubKeyPtr) | |
# if there weren't any errors, let's check the subkey's name for nulls | |
if(!$status) { | |
if($KeyNodeInformation.NameLength -gt 0) { | |
# create a byte array of the appropriate length for our string, copy the correct length bytes, then convert back to Unicode | |
$SubKeyName = New-Object byte[] $KeyNodeInformation.NameLength | |
[System.Array]::Copy($KeyNodeInformation.Name, $SubKeyName, $KeyNodeInformation.NameLength) | |
$SubKeyNameString = [System.Text.Encoding]::Unicode.GetString($SubKeyName) | |
if($SubKeyNameString -match "`0") { | |
Write-Verbose "Found key name with null character: $KeyName\$SubKeyNameString" | |
$HiddenKey = New-Object PSObject -Property @{ | |
Type = "Key" | |
Name = $SubKeyNameString | |
Path = "$KeyName\$SubKeyNameString" | |
} | |
Write-Output $HiddenKey | |
} | |
#Write-Output "Checking $KeyName\$SubKeyNameString" | |
# Recurse and check for any keys | |
if($DeleteEntries) { | |
Get-HiddenNames -KeyName "$KeyName\$SubKeyNameString" -DeleteEntries | |
} | |
else { | |
Get-HiddenNames -KeyName "$KeyName\$SubKeyNameString" | |
} | |
} | |
} | |
} | |
} | |
If($KeyFullInformation.Values -gt 0) { | |
0..($KeyFullInformation.Values-1) | ForEach-Object { | |
$SubKeyValuePtrSize = 0 | |
$status = $ntdll::NtEnumerateValueKey($KeyHandle, $_, $KEY_VALUE_INFORMATION_CLASS::KeyValueBasicInformation, 0, $SubKeyValuePtrSize, [ref]$SubKeyValuePtrSize) | |
# if it returns STATUS_NO_MORE_ENTRIES, that means the Index is not valid (i.e. there isn't a subkey at that index) | |
if($status -eq 0x8000001A) { | |
throw [System.IndexOutOfRangeException] "Index out-of-bounds, or the given registry key has no value entries." | |
} | |
# allocate the correct size and assign the value to our buffer | |
[IntPtr]$SubKeyValuePtr = [System.Runtime.InteropServices.Marshal]::AllocHGlobal($SubKeyValuePtrSize) | |
# EnumerateValueKey accesses ValueKeys by index so we don't need to know the value entry's name | |
$status = $ntdll::NtEnumerateValueKey($KeyHandle, $_, $KEY_VALUE_INFORMATION_CLASS::KeyValueBasicInformation, $SubKeyValuePtr, $SubKeyValuePtrSize, [ref]$SubKeyValuePtrSize) | |
#"EnumerateValueKey status: 0x{0:x8}" -f $status | |
$KeyValueBasicInformation = $null | |
$KeyValueBasicInformation = $SubKeyValuePtr -as $KEY_VALUE_BASIC_INFORMATION | |
# free the pointer after we're done with it | |
[System.Runtime.InteropServices.Marshal]::FreeHGlobal($SubKeyValuePtr) | |
# create a byte array of the appropriate length for our string | |
$SubKeyValueName = New-Object byte[] $KeyValueBasicInformation.NameLength | |
# copy the bytes of the value entry name into the buffer | |
[System.Array]::Copy($KeyValueBasicInformation.Name, $SubKeyValueName, $KeyValueBasicInformation.NameLength) | |
# convert the byte array into a human-readable Unicode string | |
$ValueName = [System.Text.Encoding]::Unicode.GetString($SubKeyValueName) | |
if($ValueName -match "`0") { | |
Write-Verbose "Found value name with null character: $ValueName, $KeyName" | |
$HiddenValue = New-Object PSObject -Property @{ | |
Type = "Value" | |
Name = $ValueName | |
Path = "$KeyName" | |
} | |
Write-Output $HiddenValue | |
} | |
} | |
} | |
$status = $ntdll::NtClose($KeyHandle) | |
#"CloseKey status: 0x{0:x8}" -f $status | |
} | |
} | |
function Remove-HiddenNames { | |
param | |
( | |
[Parameter(Mandatory = $true, ValueFromPipelineByPropertyName=$true)] | |
[ValidateSet("Key","Value")] | |
[string] | |
$Type, | |
[Parameter(Mandatory = $true, ValueFromPipelineByPropertyName=$true)] | |
[string] | |
$Name, | |
[Parameter(Mandatory = $true, ValueFromPipelineByPropertyName=$true)] | |
[string] | |
$Path | |
) | |
switch($Type) { | |
Key { | |
$KeyHandle = [IntPtr]::Zero | |
$DesiredAccess = $KEY_ACCESS::KEY_ALL_ACCESS | |
$ObjectAttributes = InitializeObjectAttributes -ObjectName $Path | |
$status = $ntdll::NtOpenKey([ref]$KeyHandle, $DesiredAccess, [ref]$ObjectAttributes) | |
[System.Runtime.InteropServices.Marshal]::FreeHGlobal($ObjectAttributes.ObjectName) | |
if (!$status){ | |
$status = $ntdll::NtDeleteKey($KeyHandle) | |
if(!$status) { | |
Write-Verbose "Successfully deleted key $Name." | |
} | |
else { | |
Write-Verbose $("Couldn't delete key $Name, call returned 0x{0:x8}" -f $status) | |
} | |
} | |
else { | |
throw "Couldn't open key $Name!" | |
} | |
$status = $ntdll::NtClose($KeyHandle) | |
} | |
Value { | |
if(!$Name) { | |
throw [System.ArgumentException] "ValueName required to delete ValueKeys!" | |
} | |
$KeyHandle = [IntPtr]::Zero | |
$DesiredAccess = $KEY_ACCESS::KEY_ALL_ACCESS | |
$ObjectAttributes = InitializeObjectAttributes -ObjectName $Path | |
$status = $ntdll::NtOpenKey([ref]$KeyHandle, $DesiredAccess, [ref]$ObjectAttributes) | |
[System.Runtime.InteropServices.Marshal]::FreeHGlobal($ObjectAttributes.ObjectName) | |
if (!$status){ | |
$ValueNameBuffer = Get-CountedUnicodeString -Source $Name | |
$status = $ntdll::NtDeleteValueKey($KeyHandle, [ref]$ValueNameBuffer) | |
if(!$status) { | |
Write-Verbose "Successfully deleted value key $Name" | |
} | |
else { | |
Write-Verbose $("Couldn't delete value key $Name, call returned 0x{0:x8}" -f $status) | |
} | |
} else { | |
throw "Couldn't open value $Name!" | |
} | |
$status = $ntdll::NtClose($KeyHandle) | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment