Penetration Techniques - Further Testing on "Hidden" Registry

Onedaysec
5 min read
0 views
docx image 1770017968971 0 2774684c27

0x00 Preface

---

The previous article 'Penetration Techniques - Creation of "Hidden" Registry' introduced the registry hiding technique used by Poweliks, analyzed its principles, and implemented the functionality through a C program.

This article will conduct further testing and share a more "stealthy" method (this method has not been found in public materials yet, pending confirmation).

0x01 Introduction

---

This article will cover the following topics:

  • Errors when reading with Win32 API
  • The case of placing "\0" in the middle of a string
  • Application of other Native APIs (such as NtCreateFile)
  • More covert exploitation methods
  • Defense and detection

0x02 Hiding Principle

---

For Windows systems, "\0" (i.e., 0x0000) is recognized as the string terminator.

Therefore, during the reading of such a string, encountering a leading "\0" will be interpreted as the terminator, causing premature truncation and leading to a read error.

When using Native API to set the registry, the structure OBJECT_ATTRIBUTES is required as a parameter to specify the length of the string to be read.

As long as the length is set correctly, the correct string can be read, avoiding this bug.

The key to exploitation:

Using Native API provides an additional parameter that allows specifying the length of the string to be read.

Further consideration of this issue leads to the following test:

0x03 What exactly is the error when reading with Win32 API?

---

Using HiddenNtRegistry to create a test registry key-value, the C++ calling code is as follows:

printf("=================Normal Key=================\n");
printf("1.CreateKey:\n");
MyCreateKey("\\Registry\\Machine\\Software\\test1");
printf("2.OpenKey:\n");
hKey = MyOpenKey("\\Registry\\Machine\\Software\\test1");
printf("3.SetValueKey:\n");
MySetValueKey(hKey,"test1","0123456789abcdef",REG_SZ);

printf("=================Hidden Key=================\n");
printf("1.OpenKey:\n");
hKey = MyOpenKey("\\Registry\\Machine\\Software\\test1");
printf("2.SetHiddenValueKey:\n");
MySetHiddenValueKey(hKey,"\0test1","hidden0123456789abcdef",REG_SZ);
printf("3.QueryHiddenValueKey:\n");
MyQueryHiddenValueKeyString(hKey,"\0test1");

The program implements the following functions:

  • Create registry key value test1 with content 0123456789abcdef
  • Create registry key value \0test1 with content hidden0123456789abcdef

Run as shown in the figure below

0x03 What exactly is the error when reading with Win32 API? — technical illustration 1

Use Win32 API RegQueryValueEx to attempt reading the above two registry key values

The key code is as follows:

LONG lReturnCode = 0;
HKEY hkey;
LPCTSTR RegPath = _T("Software\\test1");
if (ERROR_SUCCESS == ::RegOpenKeyEx(HKEY_LOCAL_MACHINE, RegPath, 0, KEY_READ, &hkey))
{
char dwValue[1024];
DWORD dwSzType = REG_SZ;
DWORD dwSize = sizeof(dwValue);
lReturnCode = ::RegQueryValueEx(hkey, _T("test1"), 0, &dwSzType, (LPBYTE)&dwValue, &dwSize);
if(lReturnCode != ERROR_SUCCESS)
{
printf("lReturnCode:%d\n",lReturnCode);
if(lReturnCode = 2)
printf("ERROR_FILE_NOT_FOUND\n");
return 0;
}
printf("RegQueryValue:");
for (int i=0;i {
printf("%c",dwValue[i*2]);
}
}
::RegCloseKey(hkey);

Read registry key value test1, successfully obtained content

Read registry key value \0test1, modified code as follows:

lReturnCode = ::RegQueryValueEx(hkey, _T("\0test1"), 0, &dwSzType, (LPBYTE)&dwValue, &dwSize);

Read failed, returned ERROR_FILE_NOT_FOUND

Verify the principle above: Due to the effect of "\0", the string is truncated early, recognized as a null character, resulting in inability to obtain the name

Then proceed with further attempts

0x04 What happens when "\0" is placed in the middle of a string?

---

The code for HiddenNtRegistry is:

printf("1.OpenKey:\n");
hKey = MyOpenKey("\\Registry\\Machine\\Software\\test2");
printf("2.SetHiddenValueKey:\n");
MySetHiddenValueKey2(hKey,"test2\0abc","hidden0123456789abcdef",REG_SZ);
printf("3.QueryHiddenValueKey:\n");
MyQueryHiddenValueKeyString2(hKey,"test2\0abc");

Note:

The MySetHiddenValueKey and MyQueryHiddenValueKeyString functions in the original HiddenNtRegistry project need to be appropriately modified to recalculate string lengths. The new functions are named MySetHiddenValueKey2 and MyQueryHiddenValueKeyString2.

The program implements the following functions:

  • Create a registry key value test2\0abc with content hidden0123456789abcdef
  • Read the content of the registry key value test2\0abc

The runtime is as shown in the figure below

0x04 What happens when "\0" is placed in the middle of a string? — technical illustration 2

Using regedit.exe to query this key value, a pop-up prompts that it cannot be obtained, as shown in the figure below

0x04 What happens when "\0" is placed in the middle of a string? — technical illustration 3

Here we can make a bold attempt:

Since the "\0" in test2\0abc truncates the string, what happens if we create another key value named test2?

Create the registry key value test2 with content 0123456789abcdef, the key code is as follows:

hKey = MyOpenKey("\\Registry\\Machine\\Software\\test2");
MySetValueKey(hKey,"test2","0123456789abcdef",REG_SZ);

Using regedit.exe to view the registry again, something interesting happens, as shown in the figure below

0x04 What happens when "\0" is placed in the middle of a string? — technical illustration 4

Querying the registry key value \\Registry\\Machine\\Software\\test2 no longer pops up an error, but displays two key values named test2, both with content 0123456789abcdef

We know that the registry does not allow creating two key values with the same name. The two key values with the same name generated in the above test are actually because one of them is incorrectly truncated, resulting in the same displayed key name and content, both being 0123456789abcdef (actually the content is hidden0123456789abcdef)

Thus, we have another method to "hide" registry entries. Compared to the previous method of filling "\0" at the beginning, the biggest advantage of this hiding method is that using regedit.exe to view this key value does not pop up an error, providing better concealment and deception, while having the same content as a normal key value

Comparison as shown in the figure below

0x04 What happens when "\0" is placed in the middle of a string? — technical illustration 5

The displayed key value content is 0123456789abcdef, but the actual content is hidden0123456789abcdef

0x05 Can other Native APIs (such as NtCreateFile) be applied?

---

Referencing the implementation approach of NtCreateKey, test other Native APIs, such as NtCreateFile, to see if the same issue exists when creating files?

Using NtCreateFile to create a special file: \0c:\1\test.txt, key code is as follows:

HMODULE hModule = NULL;
NTCREATEFILE NtCreateFile = NULL;
UNICODE_STRING FileName = {0};
OBJECT_ATTRIBUTES ObjectAttributes = {0};
HANDLE hFile1 = NULL;
IO_STATUS_BLOCK IOsb = {0};
HANDLE hFile2 = INVALID_HANDLE_VALUE;
PWCHAR pBuffer = NULL;
DWORD dwRet = 0;
hModule = LoadLibrary(_T("ntdll.dll"));
if (!hModule)
{
printf("Could not GetModuleHandle of NTDLL.DLL");
return FALSE;
}
NtCreateFile = (NTCREATEFILE)GetProcAddress(hModule, "NtCreateFile");
if (!NtCreateFile)
{
printf("Could not find NtCreateFile entry point in NTDLL.DLL");
return FALSE;
}
char *Path = "\\Device\\\HarddiskVolume1\\1\\test.txt";
char *TempBuff;
TempBuff = (char*)malloc(strlen(Path+2)*2);
for(int i=0;i{
TempBuff[(i+2)*2] = Path[i];
TempBuff[(i+2)*2+1] = 0x00;
}
TempBuff[0] = 0x00;
TempBuff[1] = 0x00;
TempBuff[2] = 0x00;
TempBuff[3] = 0x00;
FileName.MaximumLength = MAX_PATH * sizeof(WCHAR);
FileName.Length = (strlen(Path)+2)*sizeof(WCHAR);
FileName.Buffer = (WCHAR *)TempBuff;
FileName.Buffer[FileName.Length] = L'\0';
InitializeObjectAttributes(&ObjectAttributes,&FileName,OBJ_CASE_INSENSITIVE,NULL,NULL);
NtStatus = NtCreateFile(&hFile1,
FILE_GENERIC_WRITE,
&ObjectAttributes,
&IOsb,
NULL,
FILE_ATTRIBUTE_NORMAL,
0,
FILE_SUPERSEDE,
FILE_SEQUENTIAL_ONLY,
NULL,
0
);
if (!NT_SUCCESS(NtStatus))
{
printf("NtCreateFile failed (%x) \n", NtStatus);
}
else
printf("NtCreateFile succeed \n");

Return error c000003b, indicating STATUS_OBJECT_PATH_SYNTAX_BAD

Debug the program, trace to InitializeObjectAttributes, and examine the parameters of the ObjectAttributes structure, as shown in the figure below

0x05 Can other Native APIs (such as NtCreateFile) be applied? — technical illustration 6

Examine the content of Buffer in memory, as shown in the figure below

0x05 Can other Native APIs (such as NtCreateFile) be applied? — technical illustration 7

Same parameter structure as used in NtCreateKey implementation

For NtCreateFile, currently not applicable

0x06 Exploitation Ideas and Detection

---

The registry hiding technique used by Poweliks has a major issue: it triggers an error pop-up when opened with regedit.exe. Inserting \0 in the middle of a string and creating a key with the same name as the substring before \0 can avoid this problem

The detection approach is to identify such unusual registry keys and check if two keys with the same name exist under a registry key

If this method is used to create registry keys in startup locations, Autoruns can detect them

0x07 Summary

---

This article further tests the registry hiding techniques used by Poweliks, shares a more covert exploitation method, and provides ideas for defense and detection. More testing is needed for the application of other Native APIs.

Related Questions & Answers

Can the same Native API technique be applied to file creation using NtCreateFile, and what was the result?

The article tests using `NtCreateFile` with a path that starts with a null byte (`\0c:\1\test.txt`). Although the same `OBJECT_ATTRIBUTES` structure was used, the call failed with `STATUS_OBJECT_PATH_SYNTAX_BAD`. This indicates that the null-byte trick does not work universally for file system objects in the same way it does for registry keys, likely due to different path parsing rules in the Object Manager.

Why does the Win32 API fail to read a registry value whose name contains a null byte?

The Win32 API treats the null byte (0x0000) as the standard C string terminator. When a function like `RegQueryValueEx` receives a name that starts with or contains \0, it truncates the name at the first null byte, leading to a lookup failure. This behavior is confirmed by the article, which shows that reading `\0test1` returns `ERROR_FILE_NOT_FOUND`. Native API functions avoid this by requiring an explicit length parameter.

What is the more stealthy method introduced in this article, and how does it deceive system administrators?

The more stealthy method places the null byte in the middle of a registry value name, such as `test2\0abc`. When viewed in `regedit.exe`, the name is truncated at the null byte, so it appears as `test2`. If a legitimate key named `test2` also exists, both appear identically in the registry editor, but the hidden key actually stores different data (e.g., `hidden0123456789abcdef` instead of `0123456789abcdef`). This provides greater deception than the leading-\0 method because it produces no error messages.

How does placing a null byte (\0) at the beginning of a registry value name help hide it?

When a registry value name starts with a null byte (\0), Win32 API functions like `RegQueryValueEx` interpret that byte as a string terminator, causing them to read an empty name and fail with `ERROR_FILE_NOT_FOUND`. However, using Native API functions such as `NtCreateKey` allows specifying the exact string length, bypassing this truncation. This technique is detailed in the original article [Penetration Techniques - Creating "Hidden" Registry Entries](/news/penetration-techniques-creating-hidden-registry-entries).

Continue Reading