Architecture, Memory Management, APIs & Processes

Windows Architecture

A processor inside a machine running the Windows operating system can operate under two different modes: User Mode and Kernel Mode.

Applications run in user mode, and operating system components run in kernel mode.

When an application wants to accomplish a task, such as creating a file, it cannot do so on its own. The only entity that can complete the task is the kernel, so applications must follow the following function call flow:

1. User Processes --> A program/application executed by the user such as Notepad, Google Chrome or Microsoft Word.

2. Subsystem DLLs --> DLLs that contain API functions that are called by user processes. An example of this would be kernel32.dll exporting the CreateFile Windows API (WinAPI) function, other common subsystem DLLs are ntdll.dll, advapi32.dll, and user32.dll.

3. Ntdll.dll --> A system-wide DLL which is the lowest layer available in user mode. This is a special DLL that creates the transition from user mode to kernel mode. This is often referred to as the Native API or NTAPI.

4. Executive Kernel --> This is what is known as the Windows Kernel and it calls other drivers and modules available within kernel mode to complete tasks. The Windows kernel is partially stored in a file called ntoskrnl.exe under "C:\Windows\System32".

- Function Call Flow

For example, an application that creates a file would run through the following flow:

  1. The user application calls the CreateFile WinAPI function which is available in kernel32.dll.

  2. Kernel32.dll is a critical DLL that exposes applications to the WinAPI and is therefore can be seen loaded by most applications.

  3. Next, CreateFile calls its equivalent NTAPI function, NtCreateFile, which is provided through ntdll.dll.

  4. Ntdll.dll then executes an assembly sysenter (x86) or syscall (x64) instruction, which transfers execution to kernel mode.

  5. The kernel NtCreateFile function is then used which calls kernel drivers and modules to perform the requested task.

- Directly Invoking The Native API (NTAPI)

It's important to note that applications can invoke syscalls (i.e. NTDLL functions) directly without having to go through the Windows API. The Windows API simply acts as a wrapper for the Native API.

The Native API is more difficult to use because it is not officially documented by Microsoft. Furthermore, Microsoft advises against the use of Native API functions because they can be changed at any time without warning (There are benefits of directly invoking the Native API for MalDev).

Windows Memory Management

Understanding how Windows handles memory is crucial to building advanced malware.

- Virtual Memory & Paging

Memory in modern operating systems is not mapped directly to physical memory (i.e the RAM). Instead, virtual memory addresses are used by processes that are mapped to physical memory addresses. The goal is to save as much physical memory as possible.

Virtual memory may be mapped to physical memory but can also be stored on disk. With virtual memory addressing it becomes possible for multiple processes to share the same physical address while having a unique virtual memory address. Virtual memory relies on the concept of Memory paging which divides memory into chunks of 4kb called "pages".

- Page State

The pages residing within a process's virtual address space can be in one of 3 states:

1. Free --> The page is neither committed nor reserved. The page is not accessible to the process. It is available to be reserved, committed, or simultaneously reserved and committed. Attempting to read from or write to a free page can result in an access violation exception.

2. Reserved --> The page has been reserved for future use. The range of addresses cannot be used by other allocation functions. The page is not accessible and has no physical storage associated with it. It is available to be committed.

3. Committed --> Memory charges have been allocated from the overall size of RAM and paging files on disk. The page is accessible and access is controlled by one of the memory protection constants. The system initializes and loads each committed page into physical memory only during the first attempt to read or write to that page. When the process terminates, the system releases the storage for committed pages.

- Page Protection Options

Once the pages are committed, they need to have their protection option set.

List of memory protection constants: https://learn.microsoft.com/en-us/windows/win32/memory/memory-protection-constants

Examples:

PAGE_NOACCESS --> Disables all access to the committed region of pages. An attempt to read from, write to or execute the committed region will result in an access violation.

PAGE_EXECUTE_READWRITE --> Enables Read, Write and Execute. This is highly discouraged from being used and is generally an IoC because it's uncommon for memory to be both writable and executable at the same time.

PAGE_READONLY --> Enables read-only access to the committed region of pages. An attempt to write to the committed region results in an access violation.

- Memory Protection

Modern operating systems generally have built-in memory protections to thwart exploits and attacks.

Data Execution Prevention (DEP)--> DEP is a system-level memory protection feature that is built into the operating system starting with Windows XP and Windows Server 2003. If the page protection option is set to PAGE_READONLY, then DEP will prevent code from executing in that memory region.

Address space layout randomization (ASLR) - ASLR is a memory protection technique used to prevent the exploitation of memory corruption vulnerabilities. ASLR randomly arranges the address space positions of key data areas of a process, including the base of the executable and the positions of the stack, heap and libraries.

- x86 vs x64 Memory Space

x86 processes have a smaller memory space of 4GB (0xFFFFFFFF).

x64 has a vastly larger memory space of 128TB (0xFFFFFFFFFFFFFFFF).

- Memory Allocation Example

The first step in interacting with memory is allocating memory (reserving a memory inside the running process).

Ways to allocate memory via C functions and Windows APIs:

// Allocating a memory buffer of *100* bytes
// Method 1 - Using malloc()
PVOID pAddress = malloc(100);
// Method 2 - Using HeapAlloc()
PVOID pAddress = HeapAlloc(GetProcessHeap(), 0, 100);
// Method 3 - Using LocalAlloc()
PVOID pAddress = LocalAlloc(LPTR, 100);

pAddress will be the base address of the memory block that was allocated. Using this pointer several actions can be taken such as reading, writing, and executing. The type of actions that can be performed will depend on the protection assigned to the allocated memory region.

- Memory Writing Example

The next step after memory allocation is generally writing to that buffer. Several options can be used to write to memory but for this example, memcpy:

// HeapAlloc uses the HEAP_ZERO_MEMORY flag which causes the allocated memory to be initialized to zero
PVOID pAddress = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, 100);
CHAR* cString = "Hello";
// The string is then copied to the allocated memory using memcpy
// The last parameter in memcpy is the number of bytes to be copied. Next, recheck the buffer to verify that the data was successfully written.
memcpy(pAddress, cString, strlen(cString));

- Freeing Allocated Memory

When the application is done using an allocated buffer, it is highly recommended to deallocate or free the buffer to avoid memory leaks.

Depending on what function was used to allocate memory, it will have a corresponding memory deallocation function. For example:

  • Allocating with malloc requires the use of the free function.

  • Allocating with HeapAlloc requires the use of the HeapFree function.

  • Allocating with LocalAlloc requires the use of the LocalFree function.

Windows APIs

The Windows API provides developers with a way for their applications to interact with the Windows operating system. For example, if the application needs to display something on the screen, modify a file or query the registry all of these actions can be done via the Windows API.

Windows API List: https://learn.microsoft.com/en-us/windows/win32/apiindex/windows-api-list

- Windows Data Types

https://learn.microsoft.com/en-us/windows/win32/winprog/windows-data-types

Common data types:

// DWORD is a 32-bit unsigned integer, on both 32-bit and 64-bit systems, used to represent values from 0 up to (2^32 - 1)
DWORD dwVariable = 42;

// size_t is used to represent the size of an object. 32-bit unsigned integer on 32-bit systems representing values from 0 up to (2^32 - 1). On the other hand, it's a 64-bit unsigned integer on 64-bit systems representing values from 0 up to (2^64 - 1)
SIZE_T sVariable = sizeof(int);

// VOID indicates the absence of a specific data type
void* pVariable = NULL; // This is the same as PVOID

// PVOID is a 32-bit or 4-byte pointer of any data type on 32-bit systems and a 64-bit or 8-byte pointer of any data type on 64-bit systems.
PVOID pVariable = &SomeData;

// HANDLE is a value that specifies a particular object that the operating system is managing (e.g. file, process, thread).
HANDLE hFile = CreateFile(...);

// HMODULE - A handle to a module. This is the base address of the module in memory. An example of a MODULE can be a DLL or EXE file.
HMODULE hModule = GetModuleHandle(...);

// LPCSTR/PCSTR - A pointer to a constant null-terminated string of 8-bit Windows characters (ANSI). "L" stands for "long", nowadays it doesn't affect the data type, but the naming convention still exists. The "C" stands for "constant" or read-only variable. Both these data types are equivalent to const char*.
LPCSTR lpcString = "Hello, world!";
PCSTR pcString = "Hello, world!";

// LPSTR/PSTR - The same as LPCSTR and PCSTR , the only difference is that LPSTR and PSTR do not point to a constant variable, and instead point to a readable and writable string. Both these data types are equivalent to char*.
LPSTR lpString = "Hello, world!";
PSTR pString = "Hello, world!";

// LPCWSTR\PCWSTR - A pointer to a constant null-terminated string of 16-bit Windows Unicode characters (Unicode). Both these data types are equivalent to const wchar*.
LPCWSTR lpwcString = L"Hello, world!";
PCWSTR pcwString = L"Hello, world!";

// PWSTR\LPWSTR - The same as LPCWSTR and PCWSTR , the only difference is that 'PWSTR' and 'LPWSTR' do not point to a constant variable, and instead point to a readable and writable string. Both these data types are equivalent to wchar* .
LPWSTR lpwString = L"Hello, world!";
PWSTR pwString = L"Hello, world!";

// wchar_t - The same as wchar which is used to represent wide characters.
wchar_t wChar = L'A';
wchar_t* wcString = L"Hello, world!";

// ULONG_PTR - Represents an unsigned integer that is the same size as a pointer on the specified architecture, meaning on 32-bit systems a ULONG_PTR will be 32 bits in size, and on 64-bit systems, it will be 64 bits in size.ULONG_PTR is usefullfot the manipulation of arithmetic expressions containing pointers (e.g. PVOID). Before executing any arithmetic operation, a pointer will be subjected to type-casting to ULONG_PTR. This approach is used to avoid direct manipulation of pointers which can lead to compilation errors.
PVOID Pointer = malloc(100);
// Pointer = Pointer + 10; // not allowed
Pointer = (ULONG_PTR)Pointer + 10; // allowed

Below is a summary table:

Win32 API Type

Standard C Equivalent

Description

DWORD

unsigned long

A 32-bit unsigned integer.

SIZE_T

size_t

Used to represent the size of an object.

VOID, PVOID

void, void*

Represents the absence of type or a pointer to any type.

HANDLE

void*

A pointer/handle to a system resource.

HMODULE

void*

A handle to a module (DLL/EXE), essentially a pointer.

LPCSTR/PCSTR

const char*

Pointer to a constant null-terminated string of ANSI characters.

LPSTR/PSTR

char*

Pointer to a non-constant null-terminated string of ANSI characters.

LPCWSTR/PCWSTR

const wchar_t*

Pointer to a constant null-terminated string of Unicode characters.

LPWSTR/PWSTR

wchar_t*

Pointer to a non-constant null-terminated string of Unicode characters.

wchar_t

wchar_t

Used to represent wide characters.

ULONG_PTR

Dependent on platform

Unsigned integer the same size as a pointer. Use uintptr_tin C99 or later.

- Data Types Pointers

The Windows API allows a developer to declare a data type directly or a pointer to the data type. This is reflected in the data type names where the data types that start with "P" represent pointers to the actual data type while the ones that don't start with "P" represent the actual data type itself.

This is useful when working with Windows APIs that have parameters that are pointers to a data type. Examples "P" data types and its non-pointer equivalent:

  • PHANDLE is the same as HANDLE*.

  • PSIZE_T is the same as SIZE_T*.

  • PDWORD is the same as DWORD*.

- ANSI & Unicode Functions

Windows API functions have two versions: "A" for ANSI and "W" for Unicode. ANSI functions handle 8-bit characters, and Unicode functions handle 16-bit characters. For example, CreateFileA uses ANSI strings, while CreateFileW uses Unicode strings. The size of data passed can vary.

char str1[] = "hola"; // 7 bytes (hola + null byte).
wchar str2[] = L"hola"; // 14 bytes, each character is 2 bytes (The null byte is also 2 bytes)

- In and Out Parameters

Windows APIs have in and out parameters.

IN parameter is a parameter that is passed into a function and is used for input.

OUT parameter is a parameter used to return a value back to the caller of.

- Windows API Debugging Errors

When functions fail they often return a non-verbose error. The error code must be retrieved using the GetLastError function.

Windows's System Error Codes List: https://learn.microsoft.com/en-us/windows/win32/debug/system-error-codes--0-499-

Example:

if (!CreateProcess()) {
	printf("[!] failed to create process, error: %ld", GetLastError());
	return EXIT_FAILURE;
}

return EXIT_SUCCESS

- Windows Native API Debugging Errors

NTAPIs are mostly exported from ntdll.dll. Unlike Windows APIs, these functions cannot have their error code fetched via GetLastError. Instead, they return the error code directly which is represented by the NTSTATUS data type.

A successful system call will return the value STATUS_SUCCESS, which is 0, if the call failed it will return a non zero value (https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-erref/596a1078-e883-4972-9bbc-49e60bebca55).

Error checking for system calls:

NTSTATUS STATUS = NativeSyscallExample(...);

if (STATUS != STATUS_SUCCESS) {
    // printing the error in unsigned integer hexadecimal format
    printf("[!] NativeSyscallExample Failed With Status : 0x%0.8X\n", STATUS);
}

// NativeSyscallExample succeeded

Another way to check the return value of NTAPIs is through the NT_SUCCESS macro. The macro returns TRUE if the function succeeded, and FALSE it fails:

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

NTSTATUS STATUS = NativeSyscallExample(...);

if (!NT_SUCCESS(STATUS)) {
    // printing the error in unsigned integer hexadecimal format
    printf("[!] NativeSyscallExample Failed With Status : 0x%0.8X\n", STATUS);
}

// NativeSyscallExample succeeded

Windows Processes

A Windows process is a program or application that is running on a Windows machine.

A process can be started by either a user or by the system itself. The process consumes

resources such as memory, disk space, and processor time to complete a task.

Process Threads

Windows processes are composed of one or more threads, which operate concurrently.

A thread is an independent set of instructions that can be executed within a process.

Threads within a process can communicate and share data.

Thread execution is scheduled and managed by the operating system in the context of a process.

Process Memory

Windows processes utilize memory to store both data and instructions.

Memory is allocated to a process when it is created, and the amount allocated can be determined by the process.

Memory management is handled using virtual and physical memory.

Virtual memory enables the OS to use more memory than physically available by creating a virtual address space, which applications can access.

This virtual address space is divided into "pages" that are allocated to processes.

Memory Types in Windows Processes

Private Memory: Dedicated to a single process and cannot be shared with other processes. It's used for storing process-specific data.

Mapped Memory: Can be shared among multiple processes. This type facilitates data sharing between processes, such as shared libraries, memory segments, and files. While visible to other processes, it's protected from being modified by them.

Image Memory: Contains the code and data of an executable file and is used to store a process's program code, data, and resources. Image memory often relates to DLL files loaded into a process's address space.

Process Environment Block (PEB)

The Process Environment Block (PEB) in Windows is a data structure that stores crucial information about a running process, including parameters, startup details, heap allocation data, loaded DLLs, and more. It's used by the OS to manage processes and by the Windows loader to launch applications. Each process has its own PEB, that will contain its own set of information about it.

- PEB Structure:

The reserved members of this struct can be ignored.

The non-reserved members are explained below.

BeingDebugged:

BeingDebugged is a flag in the PEB structure that indicates whether a Windows process is being debugged or not. It's set to 1 (TRUE) when the process is being debugged and 0 (FALSE) when it's not. It helps the Windows loader decide whether to launch the application with a debugger attached.

Ldr:

Ldr is a pointer to a PEB_LDR_DATA structure, which holds information about the loaded DLL modules in a process, including the list of DLLs, their base addresses, and sizes. This is used by the Windows loader to keep track of loaded DLLs.

ProcessParameters:

ProcessParameters is a data structure in the PEB that contains command line parameters passed to the process when it was created. It's useful for actions like command line spoofing.

AtlThunkSListPtr & AtlThunkSListPtr32:

AtlThunkSListPtr and AtlThunkSListPtr32 are used by the ATL module to store a linked list of thunking functions, which are used to call functions in different address spaces, often from DLL files.

PostProcessInitRoutine:

PostProcessInitRoutine stores a pointer to a function that the operating system calls after TLS (Thread Local Storage) initialization has been completed for all threads in the process. This function can be used for additional process initialization tasks.

SessionId:

SessionId is a unique identifier in the PEB assigned to a single session to track user activity during that session.

- Undocumented Structures

When referencing the Windows documentation for a structure, one may encounter several reserved members within the structure. These reserved members are often presented as arrays of BYTE or PVOID data types. This practice is implemented by Microsoft to maintain confidentiality and prevent users from understanding the structure to avoid modifications to these reserved members.

One way to determine what the PEB's reserved members hold is through the !peb command in WinDbg.

Thread Environment Block (TEB)

Thread Environment Block (TEB) is a data structure in Windows that stores information about a thread. It contains the thread's environment, security context, and other related information. It is stored in the thread's stack and is used by the Windows kernel to manage threads.

- TEB Structure

The reserved members of this struct can be ignored.

The non-reserved members are explained below.

ProcessEnvironmentBlock (PEB):

PEB is a pointer to the PEB structure located inside the Thread Environment Block (TEB). It stores information about the currently running process.

TlsSlots:

TLS (Thread Local Storage) Slots are found within the TEB and are used to store thread-specific data. Each thread in Windows has its own TEB with its set of TLS slots, allowing applications to store thread-specific information.

TlsExpansionSlots:

These slots in the TEB are reserved for system DLLs and are used to store thread-local storage data for a thread.

Process And Thread Handles:

On the Windows operating system, each process has a distinct process identifier or process ID (PID) which the operating system assigns when the process is created. PIDs are used to distinguish one running process from another. The same concept applies to a running thread, where a running thread has a unique ID that is used to differentiate it from the rest of the existing threads (in any process) on the system.

These identifiers can be used to open a handle to a process or a thread using the following WinAPIs:

OpenProcess - Opens an existing process object handle via its identifier.

OpenThread - Opens an existing thread object handle via its identifier.

Handles should always be closed once their use is no longer required to avoid handle leaking. This is achieved via the CloseHandle WinAPI call.

Last updated