Calling NTDLL functions directly
If you're reading this, then you've probably wanted to call some internal ntdll function that isn't exported and easily callable, right? If no, then let me explain what I mean. If we start the Visual Studio Command Prompt, we can use the dumpbin command to display all of the exported functions available in the ntdll.dll library. On the picture below, we can see that we've passed the /EXPORTS parameter to the dumpbin command, which dumps all the exported functions and pipes them into the C:exports.txt file.
Now we can open the C:exports.txt file and observe what was inputted in the file. We can see some of the contents of the file presented on the picture below:
Notice that the dumping first presents the DLL file that we're dumping the function names from, and also lists the number of functions, which is 1316 in our case. Next are the ordinals, names and their relative value addresses where the functions can be accessed. The problem with this is that only some of the functions are actually exported, but some are hidden and cannot be called directly by the program. If we open the URL address http://msdn.microsoft.com/en-us/library/bb432381(v=vs.85).aspx we can stumble upon the NtOpenFile function that is part of the ntdll.dll library, but isn't exported by it. Let's dump all the entries from the C:exports.txt file whose names start with NtOpen. On the picture below, we can see all of the names that start with NtOpen, and the NtOpenFile function is among the listed functions.
The problem is that some of the functions cannot be called directly because they are not exposed and are only used internally by the library itself. If we want to call those functions nevertheless, we need to obtain their addresses via the GetProcAddress function and call them manually. In the following article, we'll take a look how to actually do that and call some function.
Let's take a look at the following program that calls the NtOpenFile function directly. Basically, we've initializing some of the arguments that we have to pass to the NtOpenFile function. We need to construct the Unicode version of the string, which is why we're using the RtlInitUnicodeString function to pass the name of the file we're trying to write to. We also have to call the InitializeObjectAttributes to initialize the object that we're also passing to the NtOpenFile function.
[cpp]
#include "stdafx.h"
#include <stdio.h>
#include <windows.h>
#include <winternl.h>
int _tmain(int argc, _TCHAR* argv[]) {
/* call the NtOpenFile function */
HANDLE file;
OBJECT_ATTRIBUTES obja;
IO_STATUS_BLOCK iostatusblock;
PUNICODE_STRING filename;
RtlInitUnicodeString(filename, L"C:/temp.txt");
InitializeObjectAttributes(&obja, filename, OBJ_CASE_INSENSITIVE, NULL, NULL);
NTSTATUS stat = NtOpenFile(&file, FILE_WRITE_DATA, &obja, &iostatusblock, NULL, NULL);
getchar();
return 0;
}
[/cpp]
This is all fine and everything, but when trying to link the executable we'll received the following error:
[plain]
error LNK2019: unresolved external symbol _NtOpenFile@24 referenced in function _wmain
fatal error LNK1120: 1 unresolved external
[/plain]
The errors mean that the linker wasn't able to compile the executable, because some symbols were not resolved correctly. This further implies that the NtOpenFile function cannot be called directly.
Getting the address with GetProcAddress
The first thing that we need to do when trying to call some internal function of the ntdll.dll library is to call the GetProcAddress. The syntax of the function can be obtained from [1] and looks like this:
We need to pass two parameters to the function. The first parameter is a handle to the DLL module that contains the function we're trying to find. This is why we must call the GetModuleHandle on the ntdll.dll library to get the handle to the library. In the second argument, we need to pass the name of the function of which we're trying to obtain the address. The actual code that gets the address of the NtOpenFile function in the ntdll.dll library is the following:
[cpp]
#include "stdafx.h"
#include <stdio.h>
#include <windows.h>
#include <Winternl.h>
int _tmain(int argc, _TCHAR* argv[]) {
typedef NTSTATUS (__stdcall *NT_OPEN_FILE)(OUT PHANDLE FileHandle, IN ACCESS_MASK DesiredAccess, IN POBJECT_ATTRIBUTES ObjectAttributes, OUT PIO_STATUS_BLOCK IoStatusBlock, IN ULONG ShareAccess, IN ULONG OpenOptions);
NT_OPEN_FILE NtOpenFileStruct;
/* load the ntdll.dll */
PVOID Info;
HMODULE hModule = LoadLibrary(_T("ntdll.dll"));
NtOpenFileStruct = (NT_OPEN_FILE)GetProcAddress(hModule, "NtOpenFile");
if(NtOpenFileStruct == NULL) {
printf("Error: could not find the function NtOpenFile in library ntdll.dll.");
exit(-1);
}
printf("NtOpenFile is located at 0x%08x in ntdll.dll.n", (unsigned int)NtOpenFileStruct);
getchar();
return 0;
}
[/cpp]
Once we run the program above, the following will be displayed on the screen:
Notice that the RVA address 0x000d59e is the same as we already identified when dumping the functions from ntdll library with the dumpbin command. This is exactly what we're doing in a program: we're determining the address of the NtOpenFile function.
But the program above is not easily understandable, so we must further explain what it does. The typedef line defines a function prototype NT_OPEN_FILE, which we must later use and cast the address into. If we don't do this, we only have an address that's pointing to the some function in memory, but we can't call it directly. We can understand that line a little better if we take a look at the NtOPenFile prototype accessible at [2] and can be seen on the picture below:
Do you notice that the typedef is constructed exactly from the arguments that we pass to the NtOpenFile function? The _Out_ keyword needs to be changed into OUT, the _In_ needs to be changed into the IN and the _Inout_ needs to be changed into IN OUT. The names of the arguments as well as their types remain the same. Let's take a look at the typedef line again, this time keeping the parameters in their own lines as follows:
[plain]
typedef NTSTATUS (__stdcall *NT_OPEN_FILE) (
OUT PHANDLE FileHandle,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes,
OUT PIO_STATUS_BLOCK IoStatusBlock,
IN ULONG ShareAccess,
IN ULONG OpenOptions
);
[/plain]
Notice that we must use the "__stdcall *NAME" directive and pass the same arguments to it as listed in the function that we're trying to call. When defining a new type *NAME, we need to pass it the same arguments to make the address of the function callable with the same arguments. And since we're talking about the same function, we can understand why this must be so. Remember that this must be done only because we're dealing with the address of the function that can't be directly used as a function's pointer to call the needed function. Also remember that the *NAME is arbitrary and we can choose whatever we want; it's a good practice to choose the name that resembles the function that we're trying to call, which is why I choose the name of NT_OPEN_FILE.
After that, we're calling the function LoadLibrary and passing it the name of the DLL that we're trying to get address of: actually, this is a handle to the DLL in the memory. The handle to the ntdll library together with the name of the function needs to be passed to the GetProcAddress function to get back the address of the function. At the same time, we must also cast the pointer to NT_OPEN_FILE to make it a function pointer. If the GetProcAddress returns NULL, there is no specified function in the ntdll library; otherwise, we're printing the address of the function on the screen (as we've already seen in one of the previous pictures).
Calling the NtOpenFile function
We've obtained the address of the NtOpenFile function that we can call. But we still have to take a look at what we actually need to pass to that function as parameters. Let's present the prototype of the function again:
Let's describe each of the parameters in turn:
- FileHandle: A pointer where the handle for the open file will be saved.
- DesiredAccess: one or many of the following parameters taken from [3]:
- ObjectAttributes: a pointer to another structure that can be initialized with the InitializeObjectAttributes.
- IoStatusBlock: a pointer to the IO_STATUS_BLOCK structure.
- ShareAccess: one of the following options taken from [3]:
- OpenOptions: options to be applied when opening a file.
From the above description, it's not very easy to do what's requested of us. To provide a new storage space in memory for FileHandle, we can simply create a new variable like so:
[cpp]
HANDLE file;
[/cpp]
DesiredAccess can be FILE_WRITE_DATA or something else, depending what we're trying to do. But the things get complicated with the third parameter. So far, we know that we must pass a pointer to some structure as ObjectAttributes. We also know that we can use the InitializeObjectAttributes function to create such a structure easily. We'll take a look at this a little bit later. For now, let's continue with the parameters we must pass to the NtOpenFile function. For simplicity, we don't really need the IoStatusBlock parameter, so we can pass NULL. We can use FILE_SHARE_WRITE as the ShareAccess parameter and last parameter OpenOptions can also be NULL.
The ObjectAttributes parameter
So far, we've seen that we must pass the ObjectAttributes parameter as a pointer to the NtOpenFile function. First, we can take a look at the InitializeObjectAttributes function. This is actually a macro and not a function, and its prototype can be seen below (taken from [4]):
The first parameter InitializedAttributes accepts the pointer to the OBJECT_ATTRIBUTES structure. We can create such a structure by something as simple as the following instruction:
[cpp]
OBJECT_ATTRIBUTES obja;
[/cpp]
The ObjectName parameter must be a Unicode string that contains the name of the file that we want to write to. In our case, this is "C:/temp.txt," but the problem is that we can't simply create Unicode strings in WinAPI32. The name of the file can contain a relative path, but in such a case, the RootDirectory must specify the rest of the path. But we can also specify the absolute path in the ObjectName in which case we can pass a NULL value to the RootDirectory parameter, which simplifies things a little bit. In Attributes parameter, we must pass one of the following options taken from [4]:
The SecurityDescriptor parameter can be NULL to accept the default security for the object.
Creating the Unicode string
The ObjectName parameter passed to the InitializeObjectAttributes macro needs to be specified in unicode formatting, which complicates things, because we can't simply embed the name in double quotes as is normally the case. Let's take a look at the [5] where we can see the syntax of the UNICODE_STRING structure that is used to define Unicode strings. The prototype of the function can be seen below:
Do you notice that the UNICODE_STRING and *PUNICODE_STRING are actually aliases of the _UNICODE_STRING structure? This means that we can use whichever name we like to define the unicode string. We should use the RtlInitUnicodeString to initialize the unicode representation of the string that we need. The prototype of that function can be seen at [6] and is presented on the picture below:
To create the actual unicode string, we can use the following code:
[cpp]
PUNICODE_STRING filename;
RtlInitUnicodeString(filename, L"C:/temp.txt");
[/cpp]
When the above code is executed, the variable filename will contain the Unicode representation of the string "C:/temp.txt", which is exactly what we want to achieve. After that we can pass the filename variable to the InitializeObjectAttributes function. If we want to compile the code above, we can quickly notice that we get an error like the following:
[plain]
error LNK2001: unresolved external symbol _RtlInitUnicodeString@8
[/plain]
This means that the _RtlInitUnicodeString function couldn't be resolved, because the appropriate library wasn't found. To solve this, we need to add ntdll.lib to the "Additional Dependencies" under the Linker – Input in the project properties. We can see that on the picture below:
If the ntdll.lib can't be found, we also need to specify the right path in the "Additional Library Directories" under the Linker – General as can be seen below (note that the C:masm32lib directory was used, which contains the needed library, but in this case you have to have the Masm32 installed).
Now I won't actually provide a working example of the NtOpenFile function call, because it really goes down into details and presenting the details isn't actually a part of the article. If you really like, you can look at the details here: http://doxygen.reactos.org/dd/d83/dll_2win32_2kernel32_2client_2file_2create_8c_source.html.
The whole code is presented below, but keep in mind that it's not complete, because there are too many details, which really aren't relevant here:
[cpp]
#include "stdafx.h"
#include <stdio.h>
#include <windows.h>
#include <winternl.h>
int _tmain(int argc, _TCHAR* argv[]) {
typedef NTSTATUS (__stdcall *NT_OPEN_FILE)(OUT PHANDLE FileHandle, IN ACCESS_MASK DesiredAccess, IN POBJECT_ATTRIBUTES ObjectAttributes, OUT PIO_STATUS_BLOCK IoStatusBlock, IN ULONG ShareAccess, IN ULONG OpenOptions);
NT_OPEN_FILE NtOpenFileStruct;
/* load the ntdll.dll */
PVOID Info;
HMODULE hModule = LoadLibrary(_T("ntdll.dll"));
NtOpenFileStruct = (NT_OPEN_FILE)GetProcAddress(hModule, "NtOpenFile");
if(NtOpenFileStruct == NULL) {
printf("Error: could not find the function NtOpenFile in library ntdll.dll.");
exit(-1);
}
printf("NtOpenFile is located at 0x%08x in ntdll.dll.n", (unsigned int)NtOpenFileStruct);
/* create the string in the right format */
UNICODE_STRING filename;
RtlInitUnicodeString(&filename, L"C:temp.txt");
/* initialize OBJECT_ATTRIBUTES */
OBJECT_ATTRIBUTES obja;
InitializeObjectAttributes(&obja, &filename, OBJ_CASE_INSENSITIVE, NULL, NULL);
/* call NtOpenFile */
IO_STATUS_BLOCK iostatusblock;
HANDLE file = NULL;
NTSTATUS stat = NtOpenFileStruct(&file, FILE_WRITE_DATA, &obja, NULL, NULL, NULL);
if(NT_SUCCESS(stat)) {
printf("File successfully opened.n");
}
else {
printf("File could not be opened.n");
}
getchar();
return 0;
}
[/cpp]
As you can see the point of the article was presenting how one would go about calling functions in ntdll DLL library directly. We need to have a detailed knowledge about Windows internals and we must also often reference the WinAPI32.