Rachit Arora

Windows System Programming: Fundamentals II

Feb 15, 2024

Continuation of the windows blog series. I have posted the part one about Windows Internals Theory.

Contents

Objects and handles

image.png

image.png

Showing Unnamed handles and Mappings

image.png

image.png

image.png

Objects which are exposed by Windows API

ObjectCreationAccess
ProcessCreateProcessOpenProcess
ThreadCreateThreadOpenThread
JobCreateJobObjectOpenJobObject
FileCreateFileCreateFile2
File Mapping (Section)CreateFileMappingOpenFileMapping
TokenLogonUserOpenProcessToken
Mutex (Mutant)CreateMutex(Ex)OpenMutex
EventCreateEvent(Ex)OpenEvent
SemaphoreCreateSemaphoreOpenSemaphore
TimerCreateWaitableTimerOpenWaitableTimer
I/O Completion PortCreateIoCompletionPort-
Window StationCreateWindowStationOpenWindowStation
DesktopCreateDesktopOpenDesktop

Handles Usage

If we do not close handle, we are leaking handle which essentially mean we are wasting memory

Example

#include <stdio.h>
#include <Windows.h>

int main() {
    HANDLE hEvent = ::CreateEvent(nullptr, TRUE, FALSE, nullptr);
    if (hEvent == nullptr) {
        printf("Failed to create event (%lu)\n", ::GetLastError());
    }
    else {
        ::SetEvent(hEvent);
        ::CloseHandle(hEvent);
    }

    //::SetPriorityClass(::GetCurrentProcess(), HIGH_PRIORITY_CLASS);
    return 0;
}

Explanation:

  1. HANDLE hEvent = ::CreateEvent(nullptr, TRUE, FALSE, nullptr);: This line declares a handle to an event object and attempts to create the event using CreateEvent. The parameters are as follows:

  2. nullptr: for security attributes, indicating default security.

  3. TRUE: indicates that the event is manual-reset, meaning the SetEvent function must be called to return the event object to the nonsignaled state.

  4. FALSE: indicates the initial state of the event object is nonsignaled.

  5. nullptr: for the name of the event, indicating it is unnamed.

  6. ::SetEvent(hEvent);: This function sets the event object to the signaled state. Any threads waiting for the event would be released if the event were being waited on.

  7. ::CloseHandle(hEvent);: This function closes the handle to the event object. When no more handles to the event object exist, the system can free the event object’s resources.

Key Takeaways From Example 1

image.png

image.png

image.png

image.png

Pseudo Handles

Regular valid handles in Windows are multiples of four, with the first valid handle value being 4. Pseudo handles are assigned specific values and cannot be closed like normal handles.

These include:

For Windows 8 and newer versions, there are additional pseudo handles:

All tokens are accessible with TOKEN_QUERY and TOKEN_QUERY_SOURCE permissions only.

::SetPriorityClass(::GetCurrentProcess(), HIGH_PRIORITY_CLASS);

Sharing Objects

Handles are specific to the process that created them There are occasions where objects must be accessible across different processes

Understanding the wmplayer Example

When we launch Windows Media Player (wmplayer), it creates a named object (likely a mutex) to check for an existing instance of itself.

This mechanism involves creating a named synchronization object, such as a mutex, which acts as a signal for the application to recognize an active instance.

image.png

Using tools like Process Explorer, which utilize kernel drivers, you can close handles directly. This bypasses the need for the application’s CloseHandle function.

Creating a single instance application

#include <Windows.h>
#include <stdio.h>

int main() {
    HANDLE hMutex = ::CreateMutex(nullptr, FALSE, L"MySingleInstanceMutex");
    if (!hMutex) {
        printf("Error creating mutex!\n");
        return 1;
    }

    if (::GetLastError() == ERROR_ALREADY_EXISTS) {
        printf("Second instance ... shutting down\n");
        return 0;
    }

    printf("First instance ... \n");
    char dummy[4];
    gets_s(dummy);

    ::CloseHandle(hMutex);
}

This C++ code is a Windows application that ensures only a single instance of it runs at any given time:

  1. HANDLE hMutex = ::CreateMutex(nullptr, FALSE, L”MySingleInstanceMutex”);
    This line attempts to create a mutex named “MySingleInstanceMutex”. If the mutex already exists, the function will return a handle to the existing mutex instead of creating a new one.

  2. if (::GetLastError() == ERROR_ALREADY_EXISTS) {
    The program checks if the last error code is ERROR_ALREADY_EXISTS, which means the mutex was already created by another instance of the program.

  3. char dummy[4];
    gets_s(dummy);
    These lines declare a buffer and then use gets_s to wait for user input. This is a simple way to keep the program running so you can observe its behavior.

  4. ::CloseHandle(hMutex);
    Finally, before the program ends, it closes the handle to the mutex, releasing the resource.

Sharing by Name

#include <Windows.h>
#include <stdio.h>
#include <conio.h>

void Read(void* p);
void Write(void* p);

int Error(const char* msg) {
    printf("%s (%u)\n", msg, ::GetLastError());
    return 1;
}

int main() {
    HANDLE hMemMap = ::CreateFileMapping(INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE, 0, 1 << 16, L"MySharedMemory");
    if (!hMemMap)
        return Error("Failed to create shared memory");

    void* p = ::MapViewOfFile(hMemMap, FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, 0);
    if (!p)
        return Error("Failed in MapViewOfFile");

    printf("Address: 0x%p\n", p);

    bool quit = false;
    while (!quit) {
        printf("1=write, 2=read, 0=quit: ");
        int selection = _getch();
        printf("%c\n", selection);

        switch (selection) {
        case '1':
            Write(p);
            break;

        case '2':
            Read(p);
            break;

        case '0':
            quit = true;
            break;
        }
    }

    ::UnmapViewOfFile(p);
    ::CloseHandle(hMemMap);
    return 0;

    // Presumably you would have more code here to utilize Read and Write functions
}

void Read(void* p) {
    printf("%s\n", (const char*)p);
}

void Write(void* p) {
    printf(">> ");
    char text[128];
    gets_s(text);
    strcpy_s((char*)p, _countof(text), text);
}

This allows us to share memory between processes

Explanation:

  1. HANDLE hMemMap: This declares a handle to a file mapping object. It is initialized by the CreateFileMapping function.

  2. INVALID_HANDLE_VALUE: This is a constant indicating that the file mapping object is not backed by any existing file on disk.

  3. PAGE_READWRITE: This constant specifies the protection for the file mapping object, allowing both reading and writing to the memory.

  4. void* p: This declares a void pointer p which will hold the starting address of the mapped view of the file.

  5. CreateFileMapping: This function creates or opens a named or unnamed file mapping object for a specified file.

  6. MapViewOfFile: This function maps a view of a file mapping into the address space of the calling process.

  7. FILE_MAP_READ FILE_MAP_WRITE: This constant specifies the type of access to the file mapping object, allowing both reading and writing to the memory.

image.png

image.png

Objects Names and Sessions

In terminal server environments, each session is isolated and has its own set of objects.

There must be a way to hide these objects because they are visible anywhere

BaseNamedObjects

Private Object Namespaces

Standard object names in Windows are public and can be easily identified and located with various tools or through code.

#define PRIVATE_NAMESPACE "MyPrivateNamespace"
#include<Windows.h>
#include<stdio.h>
#include<conio.h>

void Write(void* p);
void Read(void* p);

int Error(const char* msg) {
    printf("%s (%u)\n", msg, ::GetLastError());
    return 1;
}

int main() {
    // create the boundary descriptor
    HANDLE hBD = ::CreateBoundaryDescriptor(L"MyDescriptor", 0);
    if (!hBD)
        return Error("Failed to create boundary descriptor");

    BYTE sid[SECURITY_MAX_SID_SIZE];
    auto psid = (PSID)sid;
    DWORD sidLen;
    if (!::CreateWellKnownSid(WinBuiltinUsersSid, nullptr, psid, &sidLen))
        return Error("Failed to create SID");

    if (!::AddSIDToBoundaryDescriptor(&hBD, psid))
        return Error("Failed to add SID to Boundary Descriptor");

    HANDLE hNamespace = ::CreatePrivateNamespace(nullptr, hBD, L"PRIVATE_NAMESPACE");
    if (!hNamespace) { // maybe created already?
        hNamespace = ::OpenPrivateNamespace(hBD, L"PRIVATE_NAMESPACE");
        if (!hNamespace)
            return Error("Failed to create/open private namespace");
    }

    HANDLE hMemMap = ::CreateFileMapping(INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE, 0, 1 << 16, PRIVATE_NAMESPACE L"\\MySharedMemory");
    if (!hMemMap)
        return Error("Failed to create/open shared memory");

    void* pBuffer = ::MapViewOfFile(hMemMap, FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, 0);
    if (!pBuffer)
        return Error("Failed to map shared memory");

    printf("PID: %u. Shared memory created/opened (H=%p) mapped to %p\n",
        ::GetCurrentProcessId(), hMemMap, pBuffer);

    bool quit = false;
    while (!quit) {
        printf("1=write, 2=read, 0=quit: ");
        int selection = _getch();
        printf("%c\n", selection);

        switch (selection) {
        case '1':
            Write(pBuffer);
            break;

        case '2':
            Read(pBuffer);
            break;

        case '0':
            quit = true;
            break;
        }
    }

    ::UnmapViewOfFile(pBuffer);
    ::CloseHandle(hMemMap);
    return 0;

    // Presumably you would have more code here to utilize Read and Write functions
}

void Read(void* p) {
    printf("%s\n", (const char*)p);
}

void Write(void* p) {
    printf(">> ");
    char text[128];
    gets_s(text);
    strcpy_s((char*)p, _countof(text), text);
}

Explanation:

  1. CreateBoundaryDescriptor:
    A boundary descriptor named “MyDescriptor” is created, which is used to define the namespace boundary.

  2. CreateWellKnownSid:
    A security identifier (SID) for the built-in users group is created.

  3. AddSIDToBoundaryDescriptor:
    The created SID is added to the boundary descriptor.

  4. CreatePrivateNamespace / OpenPrivateNamespace:
    Attempts to create a private namespace with the boundary descriptor. If it already exists, it tries to open it instead.

  5. CreateFileMapping:
    A file mapping object for shared memory is created with read-write access, with the size of 64KB (1 « 16). This shared memory is created within the private namespace.

  6. MapViewOfFile:
    The shared memory is mapped to the process’s address space, and a pointer to the buffer is obtained.

This program is an example of using private namespaces to control access to shared resources, such as shared memory, making them available only to processes that have the required privileges based on the SID.

image.png

We cannot see the fullname in user mode

No way we can get it unless we have kernel access

Handle Inheritance

Handle inheritance in Windows allows a process to pass on its handles to a newly spawned process.

Example

#include <Windows.h>
#include <stdio.h>

int main() {
    HANDLE hEvent = ::CreateEvent(nullptr, TRUE, FALSE, nullptr);
    printf("HANDLE: 0x%p\n", hEvent);

    PROCESS_INFORMATION pi;
    STARTUPINFO si = { sizeof(si) };
    WCHAR name[] = L"Notepad";

    ::SetHandleInformation(hEvent, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT);

    if (::CreateProcess(nullptr, name, nullptr, nullptr, TRUE, 0, nullptr, nullptr, &si, &pi)) {
        printf("PID: %lu\n", pi.dwProcessId);
        ::CloseHandle(pi.hProcess);
        ::CloseHandle(pi.hThread);
    }

    return 0;
}

Explanation:

  1. HANDLE hEvent = ::CreateEvent(nullptr, TRUE, FALSE, nullptr);: Creates a manual-reset event object. The event is initially non-signaled (FALSE).

  2. printf(“HANDLE: 0x%p\n”, hEvent);: Prints the handle value of the event to the console.

  3. PROCESS_INFORMATION pi;: Declares a PROCESS_INFORMATION structure to receive information about the new process.

  4. STARTUPINFO si = { sizeof(si) };: Initializes a STARTUPINFO structure, which is used in the CreateProcess function to specify the main window properties if the new process creates a window.

  5. WCHAR name[] = L”Notepad”;: Declares and initializes a wide character array with the name of the program to be executed (“Notepad”).

  6. ::SetHandleInformation(hEvent, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT);: Modifies the event handle so it can be inherited by child processes.

  7. if (::CreateProcess(nullptr, name, nullptr, nullptr, TRUE, 0, nullptr, nullptr, &si, &pi)) {: Attempts to create a new process (Notepad). The TRUE parameter allows the new process to inherit handles.

  8. printf(“PID: %lu\n”, pi.dwProcessId);: If CreateProcess is successful, prints the process identifier (PID) of the newly created process.

  9. ::CloseHandle(pi.hProcess);: Closes the handle to the new process.

  10. ::CloseHandle(pi.hThread);: Closes the handle to the primary thread of the new process.

  11. return 0;: The program returns 0, indicating successful execution.

Handle Duplication

Handle duplication is a broadly applicable method for sharing resources among processes.

It is compatible with various types of objects and utilizes the DuplicateHandle API, which is straightforward in operation.

The main challenge associated with this method is communicating the existence of the duplicate handle to the process that is intended to use it.

#include <Windows.h>
#include <stdio.h>
#include <string>

int main() {
    HANDLE hEvent = ::CreateEvent(nullptr, TRUE, FALSE, nullptr);
    printf("HANDLE: 0x%p\n", hEvent);

    HANDLE hProcess = ::OpenProcess(PROCESS_DUP_HANDLE, FALSE, 34264);
    if (!hProcess) {
        printf("Error opening process (%u)\n", ::GetLastError());
        return 1;
    }

    HANDLE hTarget;
    if (::DuplicateHandle(::GetCurrentProcess(), hEvent, hProcess, &hTarget, 0, FALSE, DUPLICATE_SAME_ACCESS)) {
        printf("Success!\n");
    }

    return 0;
}

User and GDI Objects

image.png