Click here to Skip to main content
Click here to Skip to main content

Registry Manipulation Using NT Native APIs

, 5 Sep 2006
Rate this:
Please Sign up or sign in to vote.
An article on manipulating the registry using NT Native APIs.

This is the Native Registry Editor (NtRegEdit) article I wrote that uses the CNtRegistry class.

Important notice: Any registry manipulation can cause harm to your system and make it so it doesn't start or run properly. Back-up your registry before using this class. I am not responsible for any damage it causes.

Introduction

There are many registry classes out there, but none (that I know of) that use NT Native API calls to manipulate the registry. Normally, we use the Microsoft APIs to do the work, but they are a bit uncomfortable to use (to say the least), especially when it comes to copying, searching, and deleting keys and values. I also liked the simple example for RegHide by SysInternals that hides registry keys (in a matter of speaking).

Now, I use Robert Pittenger's CRegistry class (found here at CodeProject) whenever I write something that accesses the registry, because of its simplicity. I also liked the idea of hiding registry keys/values from the Registry Editor (RegEdit) by Microsoft, so I decided to combine the two and write a class that does both but only uses the NT Native Registry APIs to do it. This led to the creation of the CNtRegistry class.

Hidden Registry Keys, you say?

SysInternals says it the best (see below - verbatim from their website):

A subtle but significant difference between the Win32 API and the Native API (see Inside the Native API for more information on this largely undocumented interface) is the way that names are described. In the Win32 API, strings are interpreted as NULL-terminated ANSI (8-bit) or wide character (16-bit) strings. In the Native API, names are counted as Unicode (16-bit) strings. While this distinction is usually not important, it leaves open an interesting situation: there is a class of names that can be referenced using the Native API, but that cannot be described using the Win32 API.

How is this possible? The answer is that a name which is a counted as a Unicode string can explicitly include NULL characters (0) as part of the name. For example, "Key\0". To include the NULL at the end, the length of the Unicode string is specified as 4. There is absolutely no way to specify this name using the Win32 API since if "Key\0" is passed as a name, the API will determine that the name is "Key" (3 characters in length) because the "\0" indicates the end of the name.

When a key (or any other object with a name such as a named Event, Semaphore, or Mutex) is created with such a name, any application using the Win32 API will be unable to open the name, even though they might seem to see it.

This is where you can get a copy of RegHide from SysInternals.

What can it do?

The CNtRegistry class has some useful features ... You can:

  • Copy Keys and Values (including hidden ones) with in the same RootKey or across RootKeys.
    • BOOL CopyKeys(CString csSource, CString csTarget, BOOL bRecursively);
    • BOOL CopyValues(CString csSource, CString csTarget, CString csValueName, CString csNewValueName);
  • Delete Keys and Values (including hidden ones and recursively).
    • BOOL DeleteKey (CString csKey);
    • BOOL DeleteKeysRecursive (CString csKey);
    • BOOL DeleteValue (CString csName);
  • Rename Keys and Values.
    • BOOL RenameKey(CString csFullKey, CString csNewKeyName);
    • BOOL RenameValue(CString csOldName, CString csNewName);
  • Search for Keys, Value names, and Values. This allows you to do searches for certain "String" occurrences. It can also Find hidden Keys. Both functions (Search/Find) can perform them recursively, but only with in the current RootKey.
    • BOOL Search (CString csString, CString csStartKey, CStringArray& csaResults, int nRegSearchType=3, BOOL bCaseSensitive = TRUE);
    • BOOL FindHiddenKeys (CString csKey, BOOL bRecursive, CStringArray& csaResults);
  • Read and Write Values. Possible values/types are:
    • Binary (REG_BINARY, REG_RESOURCE_LIST, REG_FULL_RESOURCE_DECRIPTOR, REG_RESOURCE_REQUIREMENTS_LIST, REG_NONE)
      • UCHAR* ReadBinary(CString csKey, CString csName, UINT& uiLength);
      • BOOL WriteBinary(CString csKey, CString csName, UCHAR* pValue, UINT uiLength);
    • DWORD (REG_DWORD)
      • DWORD ReadDword(CString csKey, CString csName, DWORD dwDefault);
      • BOOL WriteDword(CString csKey, CString csName, DWORD dwValue);
    • String (REG_SZ, REG_EXPAND_SZ, REG_MULTI_SZ)
      • CString ReadString(CString csKey, CString csName, CString csDefault);
      • BOOL ReadMultiString(CString csKey, CString csName, CStringArray& csaReturn);
      • BOOL WriteString(CString csKey, CString csName, CString csValue);
      • BOOL WriteExpandString(CString csKey, CString csName, CString csValue);
      • BOOL WriteMultiString(CString csKey, CString csName, CStringArray& csaValue);
    • int (REG_DWORD)
      • int ReadInt(CString csKey, CString csName, int nDefault);
      • BOOL WriteInt(CString csKey, CString csName, int nValue);
    • float (REG_BINARY)
      • double ReadFloat(CString csKey, CString csName, double fDefault);
      • BOOL WriteFloat(CString csKey, CString csName, double fValue);
    • BOOL (REG_DWORD)
      • BOOL ReadBool(CString csKey, CString csName, BOOL bDefault);
      • BOOL WriteBool(CString csKey, CString csName, BOOL bValue);
    • COleDateTime (REG_BINARY)
      • COleDateTime ReadDateTime(CString csKey, CString csName, COleDateTime dtDefault);
      • BOOL WriteDateTime(CString csKey, CString csName, COleDateTime dtValue);
    • COLORREF (REG_BINARY)
      • COLORREF ReadColor(CString csKey, CString csName, COLORREF rgbDefault);
      • BOOL WriteColor(CString csKey, CString csName, COLORREF rgbValue);
    • Objects (REG_BINARY)
      • BOOL ReadFont(CString csKey, CString csName, CFont* pFont);
      • BOOL WriteFont(CString csKey, CString csName, CFont* pFont);
      • BOOL ReadPoint(CString csKey, CString csName, CPoint* pPoint);
      • BOOL WritePoint(CString csKey, CString csName, CPoint* pPoint);
      • BOOL ReadSize(CString csKey, CString csName, CSize* pSize);
      • BOOL WriteSize(CString csKey, CString csName, CSize* pSize);
      • BOOL ReadRect(CString csKey, CString csName, CRect* pRect);
      • BOOL WriteRect(CString csKey, CString csName, CRect* pRect);
  • Display errors that come from the calls to Native APIs in nt.dll.
  • Provide a default Value in case the operation fails.
  • Enable privileges for the current user which gives them the ability (if it doesn't exist already) to backup/restore Hives/Keys.

Using the code

Using the CNtRegistry class is actually very simple. Once you have declared CNtRegistry, initialize the class by calling InitNtRegistry(), call SetRootKey, then SetKey to set "SOFTWARE\MyApp". You can also use SetKey(HKEY hRoot, CString strKey, BOOL bCanCreate, BOOL bCanSaveCurrentKey) which simply combines the two functions (SetRootKey and SetKey). Now call almost any function ... like CreateHiddenKey to create a hidden key (or try a number of other functions).

Here is an example that creates a hidden key, then puts some values in it (which are also hidden).

#define "NtRegistry.h"

void CMyApp::ReadRegistry()
{
    CNtRegistry ntReg;
    ntReg.InitNtRegistry(); 
    //
    if (ntReg.SetKey(HKEY_LOCAL_MACHINE, 
        _T("Software\\MyApp\\Settings"), FALSE, TRUE))
    {
        if (ntReg.CreateHiddenKey(_T("Software\\MyApp\\Settings\\Hidden")))
        {
            // Write some stuff
            ntReg.WriteInt(_T("Data1"), 777);
            ntReg.WriteFloat(_T("Pi"), 3.14159);
            ntReg.WriteString(_T("UserName"), _T("DMadden61"));

            // Read some stuff
            int nData = ntReg.ReadInt(_T("Data1"), 0);
            pi = ntReg.ReadFloat(_T("Pi"), 0.0);
            CString csUserName = ntReg.ReadString(_T("UserName"), _T("ERR"));
        }
    }
    else
    {
        TRACE("Failed to open/set key\n");
    }
}

Simple enough? Well, it is, but how it is all put together was another thing.

Differences between Nt...() calls and Reg...() calls

I am going to show you some of the NT Native Registry APIs that CNtRegistry uses (or at least, is ready for use), talk about what makes these different, and how I modified a popular function called "EnablePrivileges" to use the NT Native APIs.

Some Native APIs
Related Win32 APIs
Required Privileges
NtCreateKey
RegCreateKey, RegCreateKeyEx
N/A
NtOpenKey
RegOpenKey, RegOpenKeyEx
N/A
NtDeleteKey
RegDeleteKey
N/A
NtFlushKey
RegFlushKey
N/A
NtSetInformationKey
None
N/A
NtQueryKey
RegQueryInfoKey
N/A
NtEnumerateKey
RegEnumerateKey, RegEnumerateKeyEx
N/A
NtNotifyChangeKey
RegNotifyChangeKeyValue
N/A
NtDeleteValueKey
RegDeleteValue
N/A
NtSetValueKey
RegSetValue, RegSetValueEx
N/A
NtQueryValueKey
RegQueryValue, RegQueryValueEx
N/A
NtEnumerateValueKey
RegEnumValue
N/A
NtQueryMultipleValueKey
RegQueryMultipleValues
N/A
NtEnumerateKey
RegEnumKey, RegEnumKeyEx
N/A
*NtSaveKey
RegSaveKey
SeBackupPrivilege
*NtRestoreKey
RegRestoreKey
SeRestorePrivilege
*NtLoadKey
RegLoadKey
SeRestorePrivilege
*NtLoadKey2
None
SeRestorePrivilege
*NtReplaceKey
RegReplaceKey
SeRestorePrivilege
*NtUnloadKey
RegUnloadKey
SeRestorePrivilege
NtClose
CloseHandle
N/A
NtCreateFile
CreateFile
N/A
NtOpenThread
OpenThread
N/A
NtOpenProcessToken
None
SeCreateTokenPrivilege
NtAdjustPrivilegesToken
AdjustTokenPrivileges
N/A
NtQueryInformationToken
GetTokenInformation
N/A

Sample NtRegistryAPI Image

The parameters used for NT Native Registry APIs are not the same ones you are familiar with. Did you know that there is actually only two (2) Root (main) Keys in the registry? The rest are simply symbolic links. The two Root Keys are "\Registry\Machine (HKEY_LOCAL_MACHINE)" and "\Registry\User (HKEY_USERS)". Look below to see the HKEY and the TEXT equivalent. Where you would normally write a path for a subkey (RegCreateKey) like this "SOFTWARE\MyApp" and also include the HKEY (HKEY_LOCAL_MACHINE), these Native APIs (NtCreateKey) need the entire "full" path to the subkey, like this: "\Registry\Machine\SOFTWARE\MyApp". The CNtRegistry class lets you make the call the way you are used to, but puts it all together for you (internally), simply by calling the two functions (see below) or one that does it both.

  • SetRootKey(HKEY_LOCAL_MACHINE);
  • SetKey(_T("SOFTWARE\\MyApp"),TRUE,TRUE);
  • or just the one below which combines the two
  • SetKey(HKEY_LOCAL_MACHINE,_T("SOFTWARE\\MyApp"),TRUE,TRUE);
HKEY_USERS           \Registry\User
HKEY_CURRENT_USER    \Registry\User\<Users_SID>
HKEY_LOCAL_MACHINE   \Registry\Machine
HKEY_CLASSES_ROOT    \Registry\Machine\SOFTWARE\Classes
HKEY_CURRENT_CONFIG  \Registry\Machine\SYSTEM\CurrentControlSet\
                                       Hardware Profiles\Current

Others are the UNICODE_STRING (U_S) and OBJECT_ATTRIBUTES (O_A) structures. The U_S structure holds the "full" Key path (Unicode string) and length. The O_A struct contains the properties. InitializeObjectAttributes(...) initializes the O_A structure that specifies the properties of an object handle to be opened. A pointer to this structure is then passed to the routine that actually opens the handle (e.g., NtOpenKey(...)).

// Used to define Unicode strings.
typedef struct _UNICODE_STRING 
{
    // The length in bytes of the string stored in Buffer.
    USHORT Length;
    // The maximum length in bytes of Buffer.
    USHORT MaximumLength;
    // Points to a buffer used to contain
    // a string of wide characters.
    PWSTR  Buffer;
} UNICODE_STRING;
typedef UNICODE_STRING *PUNICODE_STRING;

// Specifies the properties of an object handle.
typedef struct _OBJECT_ATTRIBUTES {
    ULONG Length;
    // Handle to the root object directory             
    HANDLE RootDirectory;
    // Name of the object to open a handle for.
    PUNICODE_STRING ObjectName;
    // Specifies flags (e.g. OBJ_CASE_INSENSITIVE)
    ULONG Attributes;
    // Points to type SECURITY_DESCRIPTOR
    PVOID SecurityDescriptor;
    // Points to type SECURITY_QUALITY_OF_SERVICE
    PVOID SecurityQualityOfService;
} OBJECT_ATTRIBUTES;
typedef OBJECT_ATTRIBUTES *POBJECT_ATTRIBUTES;

#define InitializeObjectAttributes( p, n, a, r, s ) { \
    (p)->Length = sizeof( OBJECT_ATTRIBUTES );        \
    (p)->RootDirectory = r;                           \
    (p)->Attributes = a;                              \
    (p)->ObjectName = n;                              \
    (p)->SecurityDescriptor = s;                      \
    (p)->SecurityQualityOfService = NULL;             \
    }

Code example using the above structures to create a registry key (the native way):

...
//
// Initialize the Unicode String from an ANSI String
//
ANSI_STRING asName;
RtlZeroMemory(&asName,sizeof(asName));
RtlInitAnsiString(&asName,csName);

RtlZeroMemory(&usName,sizeof(usName));

RtlAnsiStringToUnicodeString(&usName,&asName,TRUE);


//
// Initialize the data/properties for the actual call
//
OBJECT_ATTRIBUTES ObjectAttributes;
InitializeObjectAttributes(&ObjectAttributes,
                           &usName, 
                           OBJ_CASE_INSENSITIVE, 
                           NULL,NULL);

//
// We are ready to create the key...
//
HANDLE hKey = NULL;
m_NtStatus = NtCreateKey(&hKey, 
                         KEY_ALL_ACCESS, 
                         &ObjectAttributes,
                         0, 
                         NULL, 
                         REG_OPTION_NON_VOLATILE, 
                         &m_dwDisposition);

if (!NT_SUCCESS(m_NtStatus)) {
    return FALSE;
}
else {
    NtClose(hKey);
}

...

Because I wanted to use some of the NT Registry Hive APIs (NtSaveKey, etc...), I had to re-write the function EnablePrivileges below to use NT Native APIs (except one function that I couldn't find a native call for, LookupPrivilegeValue).

// The following code can be used to enable or disable the
// privilege. You can use this code to enable or disable privilege 
//
// Use the following to enable the privilege:
//   EnablePrivilege(SE_BACKUP_NAME, TRUE);
//
// Use the following to disable the privilege:
//   EnablePrivilege(SE_BACKUP_NAME, FALSE);
//
NTSTATUS CNtRegistry::EnablePrivilege(CString csPrivilege, BOOL bEnable)
{
    TOKEN_PRIVILEGES NewState;
    HANDLE           hToken   = NULL;
    NTSTATUS         NtStatus = STATUS_SUCCESS;

    // Open the process token for this process.
    NtStatus = NtOpenProcessToken(GetCurrentProcess(),
                                  TOKEN_ADJUST_PRIVILEGES|
                                  TOKEN_QUERY|TOKEN_QUERY_SOURCE,
                                  &hToken);
    if (!NT_SUCCESS(NtStatus)) {
        return NtStatus;
    }

    // Get the local unique id for the privilege. This
    // is a Win32 API :-\ I couldn't find a Native one.
    LUID luid;
    if ( !LookupPrivilegeValue(NULL,
                               (LPCTSTR)csPrivilege,
                               &luid))
    {
        NtClose( hToken );
        return (NTSTATUS) ERROR_FUNCTION_FAILED;
    }

    // Assign values to the TOKEN_PRIVILEGE structure.
    NewState.PrivilegeCount = 1;
    NewState.Privileges[0].Luid = luid;
    NewState.Privileges[0].Attributes = (bEnable ? SE_PRIVILEGE_ENABLED : 0);

    // Adjust the token privilege.
    NtStatus = NtAdjustPrivilegesToken( hToken, 
                                        FALSE, 
                                        &NewState, 
                                        sizeof(NewState), 
                                        (PTOKEN_PRIVILEGES)NULL, 
                                        0);
    // Close the handle.
    NtClose(hToken);

    return NtStatus;
}

In closing...

Being an "Intermediate" programmer, the problems I ran into were simply learning curves in the conversion of different types. I commented out the Hive functions because I was having some problems and I didn't want folks out there to screw up their systems. I also do not use all the Native Registry APIs in the nt.dll.

That is all I have got, I don't expect this to be perfect, so please give me your ideas to make it better. One thing to remember in life itself (and all the challenges that come with it) ... if you make a "mistake" and learn from it, then it never was a mistake to begin with...it was a "lesson"!

Thanks to ...

  • CRegistry by Robert Pittenger. The inspiration behind this class!!

Things To-Do

  • Incorporate the "Hive" functions in the code.
  • Ability to "Rename" Keys/Values (shouldn't be too hard with copying/deleting capabilities already there).
  • Take out "un-needed" code (used when I was writing it).
  • Make sure everything is commented (working).
  • There is a lot more...

History

There is a lot more that can be done to this class, but time is short and I thought that someone out there might like to help Smile | :)

  • August 10, 2006 (0.0.0.37)
    • Added recursive parameter to the CopyKey function.
    • Added "DeleteKeyRecursive()" function.
  • July 16, 2006 (0.0.0.36)
    • Added "ShowPermissionsDlg()" common dialog.
    • Added Key path in the statusbar.
  • July 2, 2006 (0.0.0.35)
    • Changed the parameters of "CopyKeys()/CopyValues()" functions.
      • This makes it easier to copy anything/anywhere.
    • Changed the parameters of the "FindHiddenKeys()" function.
      • This is so the output goes to a CStringArray (instead of a message box) which allowed me to display the output in a ListCtrl for display (thanks to a suggestion from "Tcpip2005" from CodeProject)!!
    • Added the "InitNtRegistry()" function which does all the initialization.
    • Added the CaseSensitive parameter to the "Search()" function.
    • Added "#pragma comment(linker...)" to stdafx.h to show XP themes.
    • Added some "Rtl...()" string functions.
      • RtlInitString()
      • RtlInitAnsiString()
      • RtlInitUnicodeString()
      • RtlAnsiStringToUnicodeString()
      • RtlUnicodeStringToAnsiString()
      • RtlFreeString()
      • RtlFreeAnsiString()
      • RtlFreeUnicodeString()
  • Jun 24, 2006 (0.0.0.34)
    • Added "RenameKey()" that uses the "NtRenameKey()".
    • Added "RenameValue()" that uses home-bread functions.
    • Reformated the header and source so that the order of the functions in the header match the order in the source.
  • Jun 22, 2006 (0.0.0.33)
    • Combined "SetRootKey() and SetKey" and added more explanations in the article.
    • Added "GetCurrentUsersTextualSid()" that returns the private variable "m_csSID".
  • Jun 11, 2006 - Release to public.
  • Jun 03, 2004 - Initial playing.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Dan Madden
Product Manager
Germany Germany
I have been programming (as a hobby) for 20+ years (Unix C, Scripting, VB, C/C++, C#). I am getting too old to talk about it and been in the Security line of work (both Military/Civilian) for 25+ years.

Comments and Discussions

 
QuestionVery bad code !! PinmemberMember 792380318-Oct-12 4:07 
AnswerRe: Very bad code !! PinmemberAssarbad16-Aug-13 9:09 
GeneralMy vote of 1 Pinmember__Andreas__29-May-10 4:09 
GeneralGood idea, but code is poor... [modified] Pinmember__Andreas__25-May-10 10:34 
GeneralRe: Good idea, but code is poor... [modified] PinmemberDan Madden30-May-10 0:50 
QuestionCan I use this in my commercial App? PinmemberBrian Nguyen18-Jun-09 4:00 
AnswerRe: Can I use this in my commercial App? PinmemberDan Madden1-Jul-09 11:05 
GeneralWhy LocateNTDLLEntryPoints returns FALSE when... PinmemberAble B.26-Jan-09 0:36 
GeneralDll of the CNtRegistry class Pinmemberstuart P27-Aug-08 0:22 
QuestionUnicode support? [modified] PinmemberHamed Mosavi12-May-08 7:02 
GeneralNtLoadKey fails with STATUS_PRIVILEGE_NOT_HELD PinmemberTurion20-Nov-07 6:45 
QuestionWhere to get latest source code ? PinmemberDefenestration5-Nov-07 19:08 
AnswerRe: Where to get latest source code ? PinmemberDan Madden27-Nov-07 19:44 
GeneralRe: Where to get latest source code ? PinmemberDefenestration28-Nov-07 4:57 
QuestionNTRegistry DLL? PinmemberEd Hardin23-Sep-07 9:58 
AnswerRe: NTRegistry DLL? PinmemberDan Madden30-Sep-07 17:58 
General32bit app calling NtOpenKey fails on x64 PinmemberJeffRoz16-May-07 13:35 
GeneralRe: 32bit app calling NtOpenKey fails on x64 PinmemberDan Madden12-Jun-07 13:49 
GeneralMemLeaks PinmemberThomT13-Mar-07 16:34 
GeneralRe: MemLeaks PinmemberDan Madden14-Mar-07 15:40 
GeneralRe: MemLeaks PinmemberThomT14-Mar-07 15:59 
GeneralRe: MemLeaks [modified] PinmemberDan Madden18-Mar-07 3:52 
GeneralRe: MemLeaks PinmemberThomT19-Mar-07 15:05 
GeneralRe: MemLeaks PinmemberDan Madden18-Mar-07 6:59 
GeneralNice class, but... PinmemberAlcnedlor10-Dec-06 21:01 
GeneralHere is THE solution PinmemberAlcnedlor18-Dec-06 2:19 
GeneralRe: Here is THE solution PinmemberDan Madden14-Mar-07 14:59 
GeneralRe: Here is THE solution PinmemberDan Madden18-Mar-07 7:00 
GeneralRe: Here is THE solution PinmemberAlcnedlor6-Jun-07 5:26 
GeneralVery good job indeed, but ... PinmemberSebaM20-Sep-06 10:39 
GeneralRe: Very good job indeed, but ... PinmemberDan Madden20-Sep-06 11:14 
Answer* DONE * - Regular Expression for Search in the Class PinmemberDan Madden21-Sep-06 7:23 
GeneralRe: * DONE * - Regular Expression for Search in the Class PinmemberSebaM22-Sep-06 12:31 
GeneralRe: * DONE * - Regular Expression for Search in the Class PinmemberDan Madden22-Sep-06 15:22 
GeneralRe: * DONE * - Regular Expression for Search in the Class PinmemberSebaM22-Sep-06 20:20 
GeneralRe: * DONE * - Regular Expression for Search in the Class PinmemberDan Madden23-Sep-06 13:52 
GeneralRe: * DONE * - Regular Expression for Search in the Class PinmemberSebaM24-Sep-06 9:40 
GeneralRe: * DONE * - Regular Expression for Search in the Class PinmemberSebaM1-Oct-06 4:52 
GeneralQuestion about changing registry values Pinmemberchris17512-Jul-06 10:02 
AnswerRe: Question about changing registry values PinmemberDan Madden12-Jul-06 17:36 
GeneralRe: Question about changing registry values Pinmemberchris17513-Jul-06 0:05 
AnswerRe: Question about changing registry values PinmemberDan Madden15-Jul-06 9:11 
GeneralVery good job Pinmember42887-Jul-06 1:54 
GeneralRe: Very good job PinmemberDan Madden8-Jul-06 13:12 
QuestionAuthor: New "NtRegEdit" Application Help (requested) PinmemberDan Madden29-Jun-06 8:39 
GeneralSearch Interface PinmemberTcpip200527-Jun-06 17:53 
GeneralRe: Search Interface PinmemberDan Madden27-Jun-06 17:59 
GeneralAuthor: Coming, NtRegistry Editor with a new look (Kinda) PinmemberDan Madden27-Jun-06 14:28 
GeneralRe: Author: Coming, NtRegistry Editor with a new look (Kinda) PinmemberDefenestration29-Jun-06 7:24 
AnswerRe: Author: Coming, NtRegistry Editor with a new look (Kinda) PinmemberDan Madden29-Jun-06 8:23 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web01 | 2.8.140721.1 | Last Updated 5 Sep 2006
Article Copyright 2006 by Dan Madden
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid