I bumped into this cool research from eWhite Hats the other day and figured why not to build and RE it. Might learn something in the process ¯\_(ツ)_/¯. So here we go.


TL;DR version of the paper is that using the "NtSetValueKey" function we can create a registry key such that Windows OS interprets it just fine but RegEdit, PowerShell, OSQuery, and Sysinternals Autoruns will not be happy if we ask them  to display this key.

The "NtSetValueKey" function definition could be found on MSDN. It simply creates or replaces a registry value:

  [in]           HANDLE          KeyHandle,
  [in]           PUNICODE_STRING ValueName,
  [in, optional] ULONG           TitleIndex,
  [in]           ULONG           Type,
  [in, optional] PVOID           Data,
  [in]           ULONG           DataSize

For the "invisible" registry key recipe to work we need to append "/0/0" to the beginning of  the "ValueName" parameter passed to NtSetValueKey.

Out workflow will be first to define a UNICODE_STRING structure since we are operating from the user mode:

typedef struct _UNICODE_STRING {
    USHORT Length;
    USHORT MaximumLenght;
    PWSTR Buffer;

After that we need to locate the NtSetValueKey function address inside the ntdll.dll library using LoadLibrary and GetProcAddress:

typedef NTSTATUS(WINAPI* PNTSETVALUEKEY)(_In_ HANDLE KeyHandle, _In_ PUNICODE_STRING ValueName, _In_opt_ ULONG TitleIndex, _In_ ULONG Type, _In_opt_ PVOID Data, _In_ ULONG Size);

HMODULE hNtDll = LoadLibraryA("ntdll.dll");
PNTSETVALUEKEY pNtSetValueKey = (PNTSETVALUEKEY)GetProcAddress(hNtDll, "NtSetValueKey");

And finally we need to call NtSetValueKey using parameters:

UNICODE_STRING ValueName = {};

wchar_t runkeyPath[0x100] = L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run";
wchar_t runkeyPath_trick[0x100] = L"\0\0Software\\Microsoft\\Windows\\CurrentVersion\\Run";

ValueName.Buffer = runkeyPath_trick;
ValueName.Length = 2 * HIDDEN_KEY_LENGTH;
ValueName.MaximumLenght = 0;


The full source code could be found here if you need it.


Once you have the "invisible" registry key binary - lets run it and see if we can access the newly created registry key. We can use Procmon to confirm the registry key creation:

Fig. 1: Procmon RegSetValue operation.

So I guess let's try and view its content using RegEdit and PowerShell:

Fig. 2: Retrieving New Registry Key with PowerShell
Fig. 3: RegEdit Freaking Out

So far no luck eh?

I also tried using OSQuery and Sysinternal's Autoruns to retrieve the new registry key value but they did not like it either. So I guess if you know someone who constantly leaves their desktop unlocked - this is a good way to prank them 😄

There are tools you can use to clean up this registry entry if you need it. For me calling NtDeleteValueKey would be the laziest simplest clean up solution.


In any case we have out "malicious" binary 😂 so let's take a look at it in IDA. Of course if you are performing the actual RE work you would start first with the basic static and dynamic analysis before plopping things in IDA but in our case we can skip those steps.

Once in IDA let's examine the Imports and see where the LoadLibraryA function gets called since we were using it to get the NtSetValueKey function address. This takes us to the "sub_401050" subroutine which I renamed to "Registry_Operation":

Fig. 4: IDA LoadLibraryA Cross-Reference Search

Note that LoadLibraryA takes the DLL name as an input parameter and we can see the "ntdll.dll" string pushed onto the stack right before the LoadLibraryA function call (Fig. 5). The return value is the handle to the "ntdll.dll" library stored in EAX.

The GetProdAddress function that gets called after that takes two parameters: the name of the function we need to get the address for and the handle to the DLL module. Since those are pushed onto the stack in the reverse order we can see "NtSetValueKey" string pushed first and then EAX containing the handle to ntdll.dll.

If the GetProcAddress call successed - the return value is the address of the exported function placed into EAX and if it fails the EAX will contain NULL value. The return code from GetProcAddress is moved into EDX and then we can see "TEST EDX, EDX" which will perform bitwise AND on EDX register without affecting its value. If EDX is zero then ZF will be set to 1 and jnz  (jump if ZF not set) will take as to the "[-] Failed to import functions.\n" printf call (sub_401010):

Fig. 5 IDA LoadLibrary and GetProcAddress calls

In case all is well we proceed to the RegOpenKeyExW.

Fig. 6 IDA RegOpenKeyExW function call

Let's examine the RegOpenKeyExW function definition to understand the parameters pushed on the stack before the call. According to MSDN:

  [in]           HKEY    hKey,
  [in, optional] LPCWSTR lpSubKey,
  [in]           DWORD   ulOptions,
  [in]           REGSAM  samDesired,
  [out]          PHKEY   phkResult

So we know that a pointer to a variable that receives a handle to the opened key will be in EAX after the function execution. Next parameter pushed onto the stack is "2" and we know that it must be "samDesired" value. According to the MSDN documentation value 0x0002 means "KEY_SET_VALUE", no surprises here. Next we see that "ulOptions" is set to NULL, the address of the "SubKey" string is placed into EAX and then pushed on the stack, and finally the "hKey" value is set to 0x80000001. We can use "winreg.h" header file to look this one up:

Fig. 7 "winreg.h" Registry Hives Definitions

Upon the successful execution the RegOpenKeyExA will return "ERROR_SUCCESS" or a nonzero error code. And again we can see jnz instruction is used to test for a non-zero value and if it is zero we will proceed to calling the NtSetValueKey:

Fig. 8 IDA: NtSetValueKey function call.

"sub_401010" is our printf function which IDA did not recognize (perks of using a free version I guess 😑) and we saw it being called before. So that means that "CALL EDI" is the call to the NtSetValueKey. If you go back to the GetProcAddress call you will remember that EDI contains the address to NtSetValueKey function.

Ok so let's take a look at the NtSetValueKey parameters again just to get an idea on what is pushed onto the stack in which order:

  [in]           HANDLE          KeyHandle,
  [in]           PUNICODE_STRING ValueName,
  [in, optional] ULONG           TitleIndex,
  [in]           ULONG           Type,
  [in, optional] PVOID           Data,
  [in]           ULONG           DataSize

Looks like "DataSize" in out case is set to 0x38, "Data" is set to "C:\\Windows\\System32\\calc.exe", "Type" is set to "1" (REG_SZ), "TitleIndex" is set to NULL, "ValueName" stored in "var_410" which is dynamically generated 🤨😡, and finally "KeyHandle" is set to the RegOpenKeyExW return value.

Since we are very curious about "var_410" value let's start up the x32dbg, load our binary in it, set a breakpoint on RegOpenKeyExW function call, and run it. As expected we are opening the "SOFTWARE\Microsoft\Windows\CurrentVersion\Run" registry key,  so all good here:

Fig. 9: x32dbg RegOpenKeyExW function call.

Before the call to NtSetValueKey we can see the address "00DCF520" being pushed on the stack. So this is where the "ValueName" value must be stored:

Fig. 10: x32dbg NtSetValueKey function call.

Let check what's in there using the Dump window. Remember we set the "ValueName" to L"\0\0Software\\Microsoft\\Windows\\CurrentVersion\\Run" and that the ValueName is a UNICODE_STRING structure?

typedef struct _UNICODE_STRING {
  USHORT Length; <-- 2 bytes 16 00
  USHORT MaximumLength; <-- 2 bytes 00 00
  PWSTR  Buffer; <-- 4 bytes 00 DC F5 28  (little endian in Dump)

Our "Buffer" in this case is set to "oo DC F5 28" and points to out "ValueName" string with two NULL bytes at the start. We set "MaximumLength" to "22" which is 0x16, and we set "MaximumLenght" to o (0x0) so this explains what we see in our Dump:

Fig. 11: x32dbg NtSetValueKey's "ValueName" parameter.

And that's all I got for today ¯\_(ツ)_/¯....


InvisiblePersistence/InvisibleRegValues_Whitepaper.pdf at master · ewhitehats/InvisiblePersistence
Persisting in the Windows registry “invisibly”. Contribute to ewhitehats/InvisiblePersistence development by creating an account on GitHub.