Tutorial RtlNtStatusToDosError (Convert NT error to DOS error)

D

Deleted member 65228

Guest
#1
Hello all.

Recently I used a function called RtlNtStatusToDosError in user-mode, it's exported by NTDLL. Since I found it to be very helpful, I wanted to share it here for those that are interested to maybe find useful in the future one day, too.

The function can be used to convert an NTSTATUS error code (data-type of ULONG) to a DOS error code. The difference in the two is that the NTSTATUS error code is passed back down from the kernel-mode routines and the DOS error code variant is Win32 level. When you use the Win32 API, Windows will automatically convert NTSTATUS returns to DOS variant, but whilst using the NTAPI by invoking NTDLL routines yourself, you lose this handy automatic feature by default.

Sometimes when dealing with NTDLL you can have NTSTATUS codes returned which appear to be absolutely meaningless due to how they're being presented in Visual Studio. An example would be when it is displaying negative numbers. This is because the error code is unsigned, and not signed. Using this function is an easy but effective route while debugging to get the Win32-level error code.

You can find a collection of what various NTSTATUS error codes mean here: [MS-ERREF]: NTSTATUS Values

You can find a collection of what various DOS error codes mean here: https://msdn.microsoft.com/en-gb/library/cc231199.aspx

The RtlNtStatusToDosError function prologue is below.
ULONG WINAPI RtlNtStatusToDosError(
_In_ NTSTATUS Status
);
The function is documented by Microsoft over at MSDN: RtlNtStatusToDosError function (Windows)

Below is an example on how to use the function.

Code:
typedef ULONG(WINAPI *pRtlNtStatusToDosError)(
            _In_ NTSTATUS Status
            );

....

pRtlNtStatusToDosError fRtlNtStatusToDosError = (pRtlNtStatusToDosError)fpFindExportAddress(GetModuleHandle("ntdll.dll"),
            "RtlNtStatusToDosError");

...

INT DosErrorCode = (INT)fRtlNtStatusToDosError(NtStatus);
In the above code snippet, we setup a dynamic import for the RtlNtStatusToDosError function by using the function prototype definition in memory with the address of the function, and we pass a variable called NtStatus to the function. The function returns an unsigned long (ULONG) however we type-cast it to INT (INT) which is an integer; the return value is set as the value for the DosErrorCode variable. NtStatus variable is of type NTSTATUS (ULONG) however it is undefined in the snippet.

Therefore, if we think theoretically, if we were to get the error code 0xC0000022 which stands for STATUS_ACCESS_DENIED, by converting the error code with RtlNtStatusToDosError, we'll end up getting ERROR_ACCESS_DENIED (0x00000005) instead. Which is the same error you'd get from GetLastError() -> GetLastError() returns a DOS error code.

You could setup a custom wrapper function of SetLastError. Whenever you get an error NTSTATUS code value (e.g. !NT_SUCCESS(...) depending on the function being called, there's also others such as NT_ERROR) you could pass the NTSTATUS to the wrapper function which will convert the error code with RtlNtStatusToDosError, and then apply the converted error code which is now in DOS form with SetLastError. I believe that this is exactly how Windows handles it.

Code:
VOID NtSetLastError(
    NTSTATUS NtStatus
)
{
    ULONG DosErrorCode = fRtlNtStatusToDosError(NtStatus);
    SetLastError(DosErrorCode);
}
Thanks for reading as always. :)
 
D

Deleted member 65228

Guest
#3
Also, we can use FormatMessage to get string representation of an error
If you convert the NTSTATUS to DOS beforehand then FormatMessage will work. :)

I usually stick to using NTSTATUS and checking the NTSTATUS error codes if NT_SUCCESS is not TRUE (e.g. !NT_SUCCESS(NtStatus)) ) because most of the time I'm developing in kernel-mode or using Native API in User-Mode but FormatMessage can be very useful sometimes... Saves time debugging to check the GetLastError() code or having to look it up sometimes.

If I'm honest I think that FormatMessage is too bloated because of the parameters available. An easier way when working with it would be making a wrapper function (e.g. "WrapFormatMessage") and then calling that instead, passing only the unsigned long (ULONG/DWORD) DOS error code. Then the wrapper function would call FormatMessage the normal way, using the parameter for the DOS error code. That way you only ever have to call one function with a short name, passing one parameter, each time... Instead of typing out all the other parameter values each time which can use up lots of space if you're using it heavily. ;)
 
D

Deleted member 65228

Guest
#4
An easier way when working with it would be making a wrapper function (e.g. "WrapFormatMessage") and then calling that instead
Wrote a quick example.

Code:
#include <Windows.h>

CHAR * WINAPI WrapFormatMessage(
    ULONG MessageId
)
{
    CHAR *DosErrorMessage = "";

    return (FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
        NULL,
        MessageId,
        MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
        (LPTSTR)&DosErrorMessage,
        NULL,
        NULL)) ? DosErrorMessage : "Failed";
}

int main()
{
    INT CurrentDosError = GetLastError();
    CHAR *DosErrorMessage = WrapFormatMessage(CurrentDosError);

    return 0;
}
As you can see, all you need to do is call WrapFormatMessage and pass the DOS error code as the parameter now. You can adjust it for the flags and what-not, but I intentionally made it take in only one parameter so it would be really quick and short to call it X amount of times. If you ever need to regularly use it, you'll cut back on lots of code in a large project. For example, if you have a project which consists of around half a million lines of code and need to regularly use that routine to format messages to output them instead of handling with custom messages internally setup by yourself, you may end up calling it a lot of times... Under those circumstances, you'd cut back a ton of code and thus make the end-compiled binary smaller (and the code less-messier and easier to read). If you make a mistake when using it in multiple scenarios, it could be easier to fix by making one change in the wrapper function, instead of hunting down all references to individually fix the bug for each call to the function, too! :)

In-case you're wondering, FormatMessage works by scanning for a table within modules like kernel32.dll. There is a table which holds all the error messages corresponding to the request error code I believe, and this is found via a pattern scan I think and then retrieved... I could be wrong but that is the impression I got when taking a look. Therefore you could probably improve optimisation by making a manual implementation of the entire function (potentially - as long as your copy is better than the original).

If you had the guts to use a switch statement and manually handle every single error code yourself, even though it would take time to do this (despite it being documented), you would achieve a copy faster than the original because you'd cut back on the time it takes to find the table of error messages (which Windows will do internally). Probably meaningless to you and most people but I tend to love looking into optimisation to make things as fast as possible without cutting back on important functionality so thought to note this here for educational theory.