D

Deleted member 65228

Guest
#1
Hello.

Since Windows Vista, when a user-mode program relies on the Win32 API to start up a new program as a process, a function called NtCreateUserProcess (NTDLL) is called. On Windows XP there was RtlCreateUserProcess (NTDLL) instead of NtCreateUserProcess (RtlCreateUserProcess still exists on modern versions of Windows however it will call NtCreateUserProcess).

If you would like to monitor process execution and would like to intercept before the main thread of a process is resumed and are not able to rely on a kernel-mode device driver for this task, I have made a demonstration source code for educational purposes on how you can hook ntdll!NtCreateUserProcess for 32-bit processes.

For NTDLL functions which perform system calls on 32-bit processes (ntdll.dll from SysWow64) the function stub will be 15 bytes in size.

The demonstration source code does the following:
1. Retrieve the address of NtCreateUserProcess, NtProtectVirtualMemory and NtAllocateVirtualMemory.
2. Change the memory protection of NtCreateUserProcess to allow read/write access.
3. Allocate memory for the trampoline function (which will allow us to pass the call to the original destination depending on callback filtering results).
4. Copy the bytes from the original function to our trampoline.
5. Insert our own bytes at the memory where NtCreateUserProcess is present.
6. Re-protect the memory of NtCreateUserProcess and also protect the memory of our trampoline function.

For virtual memory protection changes, NtProtectVirtualMemory is used.
For virtual memory allocation, NtAllocateVirtualMemory is used.
For virtual memory write attempts, RtlCopyMemory is used. RtlCopyMemory is actually memcpy re-named... You can use NtWriteVirtualMemory if you want.

Before the detour:


Code:
77602910 B8 C0 00 00 00       mov         eax,0C0h
77602915 BA 80 61 61 77       mov         edx,77616180h
7760291A FF D2                call        edx
7760291C C2 2C 00             ret         2Ch
After the detour:


Code:
77602910 E9 62 E8 5E 89       jmp         ProxyNtCreateUserProcess (0BF1177h)
77602915 BA 80 61 61 77       mov         edx,77616180h
7760291A FF D2                call        edx
7760291C C2 2C 00             ret         2Ch
77616180h (the address that gets moved into the EDX register and then code execution moves to due to 0xFF 0xD2) is related to WOW64 since this is an X86-X64 process (I am on an 64-bit OS but the demo code is for an x86 process).

header.h
Code:
#pragma once
#include <Windows.h>
#include <winternl.h>
#include <iostream>
#include "ntdef.h"
ntdef.h
Code:
#pragma once
#include "header.h"

#define STATUS_SUCCESS 0x00000000
#define STATUS_ACCESS_DENIED 0xC0000022

// thank the PH developer for PS structures
typedef struct _PS_ATTRIBUTE {
    ULONG Attribute;
    SIZE_T Size;
    union
    {
        ULONG Value;
        PVOID ValuePtr;
    };
    PSIZE_T ReturnLength;
} PS_ATTRIBUTE, *PPS_ATTRIBUTE;

typedef struct _PS_ATTRIBUTE_LIST {
    SIZE_T TotalLength;
    PS_ATTRIBUTE Attributes[1];
} PS_ATTRIBUTE_LIST, *PPS_ATTRIBUTE_LIST;

typedef enum _PS_CREATE_STATE
{
    PsCreateInitialState,
    PsCreateFailOnFileOpen,
    PsCreateFailOnSectionCreate,
    PsCreateFailExeFormat,
    PsCreateFailMachineMismatch,
    PsCreateFailExeName,
    PsCreateSuccess,
    PsCreateMaximumStates
} PS_CREATE_STATE;

typedef struct _PS_CREATE_INFO
{
    SIZE_T Size;
    PS_CREATE_STATE State;
    union
    {
        struct
        {
            union
            {
                ULONG InitFlags;
                struct
                {
                    UCHAR WriteOutputOnExit : 1;
                    UCHAR DetectManifest : 1;
                    UCHAR IFEOSkipDebugger : 1;
                    UCHAR IFEODoNotPropagateKeyState : 1;
                    UCHAR SpareBits1 : 4;
                    UCHAR SpareBits2 : 8;
                    USHORT ProhibitedImageCharacteristics : 16;
                };
            };
            ACCESS_MASK AdditionalFileAccess;
        } InitState;

        struct
        {
            HANDLE FileHandle;
        } FailSection;

        struct
        {
            USHORT DllCharacteristics;
        } ExeFormat;

        struct
        {
            HANDLE IFEOKey;
        } ExeName;

        struct
        {
            union
            {
                ULONG OutputFlags;
                struct
                {
                    UCHAR ProtectedProcess : 1;
                    UCHAR AddressSpaceOverride : 1;
                    UCHAR DevOverrideEnabled : 1;
                    UCHAR ManifestDetected : 1;
                    UCHAR ProtectedProcessLight : 1;
                    UCHAR SpareBits1 : 3;
                    UCHAR SpareBits2 : 8;
                    USHORT SpareBits3 : 16;
                };
            };
            HANDLE FileHandle;
            HANDLE SectionHandle;
            ULONGLONG UserProcessParametersNative;
            ULONG UserProcessParametersWow64;
            ULONG CurrentParameterFlags;
            ULONGLONG PebAddressNative;
            ULONG PebAddressWow64;
            ULONGLONG ManifestAddress;
            ULONG ManifestSize;
        } SuccessState;
    };
} PS_CREATE_INFO, *PPS_CREATE_INFO;

typedef NTSTATUS(NTAPI *pNtProtectVirtualMemory)(HANDLE ProcessHandle, PVOID *BaseAddress, PSIZE_T NumberOfBytesToProtect, ULONG NewAccessProtection, PULONG OldAccessProtection);

typedef NTSTATUS(NTAPI *pNtAllocateVirtualMemory)(HANDLE ProcessHandle, PVOID *BaseAddress, ULONG_PTR ZeroBits, PSIZE_T RegionSize, ULONG AllocationType, ULONG Protect);

typedef NTSTATUS(NTAPI *pNtWriteVirtualMemory)(HANDLE ProcessHandle, PVOID BaseAddress, PVOID Buffer, ULONG NumberOfBytesToWrite, PULONG NumberOfBytesWritten);

typedef NTSTATUS(NTAPI *pNtCreateUserProcess)(PHANDLE ProcessHandle, PHANDLE ThreadHandle, ACCESS_MASK ProcessDesiredAccess, ACCESS_MASK ThreadDesiredAccess, POBJECT_ATTRIBUTES ProcessObjectAttributes, POBJECT_ATTRIBUTES ThreadObjectAttributes, ULONG ulProcessFlags, ULONG ulThreadFlags, PRTL_USER_PROCESS_PARAMETERS RtlUserProcessParameters, PPS_CREATE_INFO PsCreateInfo, PPS_ATTRIBUTE_LIST PsAttributeList);
main.cpp
Code:
#include "header.h"
using namespace std;

// global variables
FARPROC fNtCreateUserProcess = 0; // used later to store address of the callback memory

NTSTATUS NTAPI ProxyNtCreateUserProcess(PHANDLE ProcessHandle, PHANDLE ThreadHandle, ACCESS_MASK ProcessDesiredAccess, ACCESS_MASK ThreadDesiredAccess, POBJECT_ATTRIBUTES ProcessObjectAttributes, POBJECT_ATTRIBUTES ThreadObjectAttributes, ULONG ulProcessFlags, ULONG ulThreadFlags, PRTL_USER_PROCESS_PARAMETERS RtlUserProcessParameters, PPS_CREATE_INFO PsCreateInfo, PPS_ATTRIBUTE_LIST PsAttributeList)
{
    // compare to see if we want to allow the process or not
    if (!wcscmp(RtlUserProcessParameters->ImagePathName.Buffer, L"EXEPATH"))
    {
        // access denied error
        return STATUS_ACCESS_DENIED;
    }

    // return with the callback function
    return ((pNtCreateUserProcess)fNtCreateUserProcess)(ProcessHandle, ThreadHandle, ProcessDesiredAccess, ThreadDesiredAccess, ProcessObjectAttributes, ThreadObjectAttributes, ulProcessFlags, ulThreadFlags, RtlUserProcessParameters, PsCreateInfo, PsAttributeList);
}

BOOL HookNtCreateUserProcess(FARPROC fpCallbackAddress)
{
    NTSTATUS NtStatus = STATUS_SUCCESS;
    HANDLE CurrentProcess = GetCurrentProcess();
    SIZE_T sNtdllStubSize32 = 15, sHookSize = 5;
    ULONG ulOldAccessProtection = 0;
    PVOID pvFunctionAddress = 0;
    PVOID pvTrampolineMemory = 0;

    unsigned char Opcode[5] = {
        0xE9, // JMP
        0x00, 0x00, 0x00, 0x00 // address, use 4 bytes for x86 process
    };

    // get the addresses of the NTDLL functions
    FARPROC fpAddresses[3] = {
        GetProcAddress(GetModuleHandle("ntdll.dll"), "NtCreateUserProcess"),
        GetProcAddress(GetModuleHandle("ntdll.dll"), "NtProtectVirtualMemory"),
        GetProcAddress(GetModuleHandle("ntdll.dll"), "NtAllocateVirtualMemory")
    };

    // loop through the array
    for (FARPROC &fpAddress : fpAddresses)
    {
        // check if the address is invalid -> stop the operation
        if (!fpAddress)
        {
            return FALSE;
        }
    }

    // setup memory for the NTDLL functions
    pNtProtectVirtualMemory fNtProtectVirtualMemory = (pNtProtectVirtualMemory)fpAddresses[1];
    pNtAllocateVirtualMemory fNtAllocateVirtualMemory = (pNtAllocateVirtualMemory)fpAddresses[2];

    pvFunctionAddress = (PVOID)fpAddresses[0]; // used for virtual memory operations

    // NtProtectVirtualMemory (NTDLL) -> change protection so we can write to the memory
    NtStatus = fNtProtectVirtualMemory(CurrentProcess, &pvFunctionAddress, &sHookSize, PAGE_EXECUTE_READWRITE, &ulOldAccessProtection);
    if (!NT_SUCCESS(NtStatus)) // check for error -> stop operation
    {
        // return false
        return FALSE;
    }

    // allocate memory for our trampoline function
    NtStatus = fNtAllocateVirtualMemory(CurrentProcess, &pvTrampolineMemory, 0, &sNtdllStubSize32, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    if (!NT_SUCCESS(NtStatus)) // check for error -> stop operation
    {
        // re-protect memory of the target function address
        fNtProtectVirtualMemory(GetCurrentProcess(), &pvFunctionAddress, &sHookSize, ulOldAccessProtection, &ulOldAccessProtection);

        // return false
        return FALSE;
    }

    // copy the bytes from target address to our trampoline -> 15 bytes = system call function stub size in NTDLL for 32-bit processes
    RtlCopyMemory(pvTrampolineMemory, fpAddresses[0], 15);

    // assign our trampoline address to the address of the memory we allocated and inserted the bytes into
    fNtCreateUserProcess = (FARPROC)pvTrampolineMemory;

    // offset calculation to add our address of callback into the byte array for the hook
    *(ULONG*)(Opcode + 1) = ((ULONG)fpCallbackAddress - ((ULONG)fpAddresses[0] + 5));

    // insert our own bytes at the start of the target function prologue (in memory)
    RtlCopyMemory(fpAddresses[0], Opcode, 5);

    // re-protect the memory of the function we hooked & also the trampoline function
    fNtProtectVirtualMemory(GetCurrentProcess(), &pvFunctionAddress, &sHookSize, ulOldAccessProtection, &ulOldAccessProtection);
    fNtProtectVirtualMemory(GetCurrentProcess(), &pvTrampolineMemory, &sHookSize, PAGE_EXECUTE, &ulOldAccessProtection);

    return TRUE;
}

int main()
{
    // try to set the hook
    if (HookNtCreateUserProcess((FARPROC)ProxyNtCreateUserProcess))
    {
        // if the hook worked...
    }
    getchar();
    return 0;
}
Written and tested on Windows 10 x64 for x86 processes. You can use the EAX register to hold the address instead if you'd like, you'll need to change the unsigned char array and the calculations though.

Use the source code responsibly if you make use of it, good purposes only. You could always use it for a user-mode anti-executable (would not be as secure but it would still work).
 
Last edited by a moderator:
D

Deleted member 65228

Guest
#2
Additional information:
There's a function exported by NTDLL called NtCreateSection. Since this thread is about API hooking for monitoring process creation, I gathered it'd be appropriate to demonstrate how you would handle a callback function should you need to hook the function to identify if it is called for a program start-up request.

Luckily for us the function is documented over at MSDN under its driver routine name (Zw* prefix version) so it'll be easier to explain:
The function creates a section object which is a section in memory which is shared.

ZwCreateSection routine (Windows Drivers)
S (Windows Drivers)
Section Objects and Views

The function prototype:
Code:
NTSTATUS ZwCreateSection(
  _Out_    PHANDLE            SectionHandle,
  _In_     ACCESS_MASK        DesiredAccess,
  _In_opt_ POBJECT_ATTRIBUTES ObjectAttributes,
  _In_opt_ PLARGE_INTEGER     MaximumSize,
  _In_     ULONG              SectionPageProtection,
  _In_     ULONG              AllocationAttributes,
  _In_opt_ HANDLE             FileHandle
);
The two parameters you'll be interested in for identifying process startup would be SectionPageProtection (data-type ULONG) and AllocationAttributes (also ULONG). The Windows loader will use this function in the background, and the SectionPageProtection will be PAGE_EXECUTE and the AllocationAttributes will be SEC_IMAGE.

Below is an example demonstration of how you could go about adding a callback to this function if you hooked it:
Code:
NTSTATUS ProxyNtCreateSection(
    _Out_    PHANDLE            SectionHandle,
    _In_     ACCESS_MASK        DesiredAccess,
    _In_opt_ POBJECT_ATTRIBUTES ObjectAttributes,
    _In_opt_ PLARGE_INTEGER     MaximumSize,
    _In_     ULONG              SectionPageProtection,
    _In_     ULONG              AllocationAttributes,
    _In_opt_ HANDLE             FileHandle
)
{
    if (SectionPageProtection == PAGE_EXECUTE && AllocationAttributes == SEC_IMAGE)
    {
        // new program start-up
    }

    // todo: return with the trampoline
}
SEC_IMAGE is defined in winnt.h:
Code:
#define SEC_IMAGE 0x01000000
PAGE_EXECUTE is also defined in winnt.h:
Code:
#define PAGE_EXECUTE            0x10
I wouldn't hard-code 0x01000000 or 0x10 though in case any changes are made across OS versions.

I haven't tested the above just now however I did investigate with detouring the function a long time ago and from what I can remember the above should be right, but I am not actually sure if it would conflict with DLLs being loaded into memory so you'd have to do tests with that and find a work-around. However based on experience I do know it works - it won't be useful for performing any virtual memory operations with the newly started up program though (since it won't yet exist, this is very early stages...), if you need to do something like that then look into CreateProcessInternalA/W (the Unicode version should be called by the Ascii one so no need to hook the Ascii version, correct me if I am wrong) or NtResumeThread.

After ntdll!NtCreateUserProcess a call to NtResumeThread will be made, which is responsible for resuming the main thread of the newly started up program (within its process) so it can start executing code.