Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve NtSetInformationProcess hook accuracy #5

Merged
merged 2 commits into from
Oct 21, 2016
Merged

Improve NtSetInformationProcess hook accuracy #5

merged 2 commits into from
Oct 21, 2016

Conversation

Mattiwatti
Copy link
Member

The following program will detect an NtSetInformationProcess hook by ScyllaHide in 3 different ways:

#include <Windows.h>
#include <stdio.h>
#include <conio.h>
#pragma warning(push)
#pragma warning(disable:4005) // macro redefinitions
#include <ntstatus.h>
#pragma warning(pop)

#define NT_SUCCESS(Status) (((NTSTATUS)(Status)) >= 0)

typedef enum _PROCESSINFOCLASS {
    ProcessBasicInformation     = 0,
    ProcessBreakOnTermination   = 29,
    ProcessHandleTracing        = 32,
} PROCESSINFOCLASS;

typedef struct _PROCESS_HANDLE_TRACING_ENABLE_EX {
    ULONG Flags;
    ULONG TotalSlots;
} PROCESS_HANDLE_TRACING_ENABLE_EX, *PPROCESS_HANDLE_TRACING_ENABLE_EX;

typedef NTSTATUS(NTAPI* NTSETINFORMATIONPROCESS)(
    IN HANDLE ProcessHandle,
    IN PROCESSINFOCLASS ProcessInformationClass,
    IN PVOID ProcessInformation,
    IN ULONG ProcessInformationLength
);

bool DisableDebugPrivileges()
{
    HANDLE hToken;
    LUID luid;
    TOKEN_PRIVILEGES tkp;
    bool success;
    if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken))
        return false;
    if (!LookupPrivilegeValue(nullptr, SE_DEBUG_NAME, &luid))
        success = true; // I'm not great on Win32 security, but I think this means the process can never have the token (enabled or otherwise)
    else
    {
        tkp.PrivilegeCount = 1;
        tkp.Privileges[0].Luid = luid;
        tkp.Privileges[0].Attributes = SE_PRIVILEGE_REMOVED;
        success = AdjustTokenPrivileges(hToken, false, &tkp, sizeof(tkp), nullptr, nullptr) == TRUE;
    }
    CloseHandle(hToken);
    return success;
}

int main(int argc, char* argv[])
{
    HMODULE ntdll = GetModuleHandleA("ntdll.dll");
    if (ntdll == nullptr)
        return -1;
    NTSETINFORMATIONPROCESS NtSetInformationProcess = (NTSETINFORMATIONPROCESS)GetProcAddress(ntdll, "NtSetInformationProcess");
    if (NtSetInformationProcess == nullptr)
        return -1;

    HANDLE ProcessHandle = GetCurrentProcess();
    PVOID Bogus = (PVOID)0x1234; // Needs to be not null
    ULONG ZeroLength = 0; // Valid value - when passed it disables tracing for a process
    NTSTATUS ret = NtSetInformationProcess(ProcessHandle, ProcessHandleTracing, &Bogus, ZeroLength);
    if (NT_SUCCESS(ret))
        printf("(1) No hook detected\n");
    else if (ret == STATUS_INFO_LENGTH_MISMATCH)
        printf("(1) ScyllaHide hooked NtSetInformationProcess! Expected STATUS_SUCCESS, received STATUS_INFO_LENGTH_MISMATCH\n");
    else
        printf("(1) Unexpected return value %08X (expected STATUS_SUCCESS)\n", ret);

    PROCESS_HANDLE_TRACING_ENABLE_EX PHTEx = { 0 };
    PHTEx.Flags = 1; // This is NOT valid. (specifically, anything other than 0 is invalid)
    ret = NtSetInformationProcess(ProcessHandle, ProcessHandleTracing, &PHTEx, sizeof(PROCESS_HANDLE_TRACING_ENABLE_EX));
    if (ret == STATUS_INVALID_PARAMETER)
        printf("(2) No hook detected\n");
    else if (NT_SUCCESS(ret))
        printf("(2) ScyllaHide hooked NtSetInformationProcess! Expected STATUS_INVALID_PARAMETER, received STATUS_SUCCESS\n");
    else
        printf("(2) Unexpected return value %08X (expected STATUS_INVALID_PARAMETER)\n", ret);

    // Remove debug privileges if we have them
    if (!DisableDebugPrivileges())
        printf("Failed to disable debug privileges. Skipping next test to save you from a BSOD...\n");
    else
    {
        ULONG ProcessBreakOnTerminationValue = 1;
        ret = NtSetInformationProcess(ProcessHandle, ProcessBreakOnTermination, &ProcessBreakOnTerminationValue, sizeof(ULONG));
        if (ret == STATUS_PRIVILEGE_NOT_HELD)
            printf("(3) No hook detected\n"); // Debug privileges are required to set the break on termination flag
        else if (NT_SUCCESS(ret))
            printf("(3) ScyllaHide hooked NtSetInformationProcess! Expected STATUS_PRIVILEGE_NOT_HELD, received STATUS_SUCCESS\n");
        else
            printf("(3) Unexpected return value %08X (expected STATUS_PRIVILEGE_NOT_HELD)\n", ret);

        // Note: on Windows XP x64 (and maybe Vista, but probably not) the process must also be a 64 bit process, otherwise the expected
        // return value should be STATUS_NOT_IMPLEMENTED. But this is irrelevant since the debuggee doesn't get to choose the target OS
    }

    printf("\nDone. Press any key to exit.\n");
    _getch();
    return 0;
}

This PR changes several aspects of the hook so that this program no longer works. Full details/explanation of the changes can be found in the commit message. The new behaviour is correct at least on Windows 7 and XP x64, but I haven't tested it on Windows 8/10 (though presumably it's unchanged). Reference for the function is the Windows Server 2003 kernel source, but the basic parameter validation seems to have stayed the same over many years.

…hook behaviour

- For ProcessInformationClass == ProcessHandleTracing, turned ProcessInformationLength == 0 into a valid case. This disables tracing and causes all other parameter checks to be skipped, so passing e.g. a bogus pointer with ProcessInformationLength 0 will still succeed. This is as per the real function
- For ProcessInformationClass == ProcessHandleTracing, ValueProcessBreakOnTermination can now be set to false (in the case that length is 0 as noted above). Previously calling the function would always 'enable' the flag, even when called with parameters that should disable it
- Moved 'ProcessInformation != 0' checks for ProcessHandleTracing and ProcessBreakOnTermination cases further down and turned them into faux access violations. We can let the real NtSetInformationProcess take the AVs, but the problem with this is that the order of parameter checks would be subtly changed.
- Test whether the struct passed in for ProcessHandleTracing (PROCESS_HANDLE_TRACING_ENABLE/PROCESS_HANDLE_TRACING_ENABLE_EX) has Flags set to 0, the only allowed value. Other values now cause STATUS_INVALID_PARAMETER instead of success
- The ProcessBreakOnTermination flag may only be set by a process that has debug privileges enabled. This is now enforced by querying the process token privileges and returning STATUS_PRIVILEGE_NOT_HELD if it doesn't have debug privileges. Because this check occurs fairly deep within the real NtSetInformationProcess, I've also had to add length and null pointer checks before the privilege check to preserve the order of parameter validation
…fault enabled privileges that have been removed during the lifetime of a process. In this case the privileges are NOT held
@mrexodia mrexodia merged commit 9e40e3a into x64dbg:vs13 Oct 21, 2016
@Mattiwatti Mattiwatti deleted the ntsetinformationprocess-fixes branch October 21, 2016 22:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants