Skip to content

Instantly share code, notes, and snippets.

@brianreitz
Last active October 13, 2021 19:25
Show Gist options
  • Save brianreitz/f3af4136e2417b8b859018644f97be08 to your computer and use it in GitHub Desktop.
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
# 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