Introduction to Windows API
The purpose of this page is to go through some of the Windows API concepts, how these functions are called, how errors are handled, and some naming conventions that Microsoft uses that are not always immediately clear.
Windows API Documentation
The Windows documentation related to their API is generally clear, but it helps to understand how to read it. For example, consider the documentation for CreateThread 1.
It starts by explaining what the function does, followed by the syntax to use to call it:
HANDLE CreateThread( [in, optional] LPSECURITY_ATTRIBUTES lpThreadAttributes, [in] SIZE_T dwStackSize, [in] LPTHREAD_START_ROUTINE lpStartAddress, [in, optional] __drv_aliasesMem LPVOID lpParameter, [in] DWORD dwCreationFlags, [out, optional] LPDWORD lpThreadId);There is the return type (HANDLE, more on this later), followed by the name of the function (for instance CreateThread or CreateProcess 2) and the list of arguments it accepts.
Each argument has an in/out annotation and possibly optional. The in defines that this parameter is passed as an argument to the function and the value is never changed by the function but only used. out, on the other hand, are always pointers and are used to allow the caller of the function to extract data that the function defines. For example, in CreateThread the return value is a HANDLE to the thread, but the caller might need to get the ID of the thread. C does not allow returning multiple values, so the usual convention is to have the return value of a function be an error code or a handle, and additional data are returned in the out pointers. The caller can then take the value pointed to by the pointer and use it. It is optional because if the caller doesn’t need to use it, they can avoid passing it to the function.
Functions can have multiple out arguments like CryptDecrypt 3. This function also introduces another possibility: arguments that are both input and output.
BOOL CryptDecrypt( [in] HCRYPTKEY hKey, [in] HCRYPTHASH hHash, [in] BOOL Final, [in] DWORD dwFlags, [in, out] BYTE *pbData, [in, out] DWORD *pdwDataLen);In this case, pbData is the buffer containing the bytes that need to be decrypted, and pdwDataLen is the number of bytes. When the decryption function is executed, the decrypted data is saved in pbData and the new size in pdwDataLen.
The documentation then continues with arguments which are pretty straightforward. One point worth noting is that some arguments, usually with the flags word in the name, allow multiple values to be passed separated by the | (bitwise OR) operator. Arguments separated by the OR operator are meant to be read as “both x and y”. For example, two of the available flags for the CreateFile function are GENERIC_READ and GENERIC_WRITE. To request both read and write access to the file, the flags would be specified as GENERIC_READ | GENERIC_WRITE.
The return value section of the documentation defines what the returned value means; only the return value, not the out arguments. This section often includes details on the error codes that are returned and how to access them. This is discussed later.
The remark section might be useful at times, but mostly for developers, as it includes specific information about the function. For example, CreateFile seems to be used to create a file, however it does support opening a file as well. The remark section explains why and the capabilities available.
API Names
The main point of this paragraph is to explain API calls that have leading or trailing capital letters. Before that, it is worth noting that assuming what a function does based only on the name can be misleading. For example, CreateFile might not create the file at all, but only open it. There are other examples, like GetProcAddress that seems like it might return the address of a process; however, it returns the address of a procedure (function). The documentation is usually the most reliable reference until that specific function becomes familiar.
Also, additional attention to leading and trailing letters is useful, as they might change the function slightly. The most popular ones are:
- suffix
Ex→ which meansExtended. The extended version introduces new functionalities and can change the way you call the function. For exampleReadFileandReadFileEx.
Note how the lpBuffer is not optional in ReadFile but it is in ReadFileEx, lpOverlapped is both input and output in the extended version but not on the standard one, and the last argument is completely different.
BOOL ReadFile( [in] HANDLE hFile, [out] LPVOID lpBuffer, [in] DWORD nNumberOfBytesToRead, [out, optional] LPDWORD lpNumberOfBytesRead, [in, out, optional] LPOVERLAPPED lpOverlapped);
BOOL ReadFileEx( [in] HANDLE hFile, [out, optional] LPVOID lpBuffer, [in] DWORD nNumberOfBytesToRead, [in, out] LPOVERLAPPED lpOverlapped, [in] LPOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine);- suffix
W→ This doesn’t usually bring new features, except that input/output strings are wide-chars to allow for UTF-8/UTF-16 glyphs. An example isMessageBoxW. - suffix
A→ This indicates that the strings arguments are ASCII. An example isMessageBoxA. - prefix
ZwandNt→ those are functions that are respectively the kernel-mode and user-mode implementations. They are, in theory, not meant to be used by developers as their implementation might change without notice; however, malware authors often use them to avoid detection. It might happen that the documentation of those functions is not available or redacted; for example, theSYSTEM_PROCESS_INFORMATIONreturned by theNtQuerySystemInformationhas certain arguments redacted, see 4. Seeing these functions in pestudio often raises suspicion.
Windows Data Types
Looking at any of the functions used above, it is noticeable that the types of the arguments are not like those known by a C programmer. For example:
BOOL ReadFileEx( [in] HANDLE hFile, [out, optional] LPVOID lpBuffer, [in] DWORD nNumberOfBytesToRead, [in, out] LPOVERLAPPED lpOverlapped, [in] LPOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine);None of HANDLE, LPVOID, DWORD or LPOVERLAPPED are common outside Windows. These are in fact either struct types defined in the Windows library or just simple aliases. To explain a few aliases:
DWORDis the alias ofunsigned longBYTEis the alias ofunsigned charSIZE_Tis the alias ofunsigned long longBOOLis the alias ofintNTSTATUSis the alias oflong
There are many more than those listed above, but the point is that it is useful to become familiar with these types, and a full reference is given in 5.
Certain structs, like HANDLE, have a deeper meaning in the operating system than other structs like BYTE. In the next chapter, handles are discussed in more detail. For more on mutexes see the learn section Mutex.
Handles
A HANDLE type is nothing more than a pointer. It is defined as a PVOID meaning a pointer to void (void * in C). How can something be a pointer to void? This is a way to say that the pointer returned can be of any type, depending on the function that returned it. Internally it is mapped to a specific kernel object that the user does not need to care about. Of course, that might make it a bit harder to understand which type of HANDLE needs to be passed to a function; however, the documentation does a decent job in explaining it. A couple of examples of functions returning a HANDLE are:
HANDLE OpenProcess( [in] DWORD dwDesiredAccess, [in] BOOL bInheritHandle, [in] DWORD dwProcessId);
HANDLE CreateMutexA( [in, optional] LPSECURITY_ATTRIBUTES lpMutexAttributes, [in] BOOL bInitialOwner, [in, optional] LPCSTR lpName);
HANDLE CreateFileA( [in] LPCSTR lpFileName, [in] DWORD dwDesiredAccess, [in] DWORD dwShareMode, [in, optional] LPSECURITY_ATTRIBUTES lpSecurityAttributes, [in] DWORD dwCreationDisposition, [in] DWORD dwFlagsAndAttributes, [in, optional] HANDLE hTemplateFile);Three completely different meanings for the same object. Handles can be used to track down chains of execution; for example, if a sample opens multiple files and performs operations on them, the execution path might become hard to follow. However, looking at the HANDLE ID in the debugger usually helps.
Using a single unified object (HANDLE) for all the API calls that return a pointer type is a newer approach from Microsoft; previously, each function was returning a different HANDLE type, and some still do for backwards compatibility. For example RegCreateKeyExA 6, which returns a HKEY handle.
LSTATUS RegCreateKeyExA( [in] HKEY hKey, [in] LPCSTR lpSubKey, DWORD Reserved, [in, optional] LPSTR lpClass, [in] DWORD dwOptions, [in] REGSAM samDesired, [in, optional] const LPSECURITY_ATTRIBUTES lpSecurityAttributes, [out] PHKEY phkResult, [out, optional] LPDWORD lpdwDisposition);Once a handle is not needed anymore it is usually closed either with CloseHandle 7 or with a specific close function like InternetCloseHandle 8. There are some cases, like the HANDLE populated by EnumProcessModules that must not be closed, as specified in the documentation 9:
Do not call CloseHandle on any of the handles returned by this function. The information comes from a snapshot, so there are no resources to be freed.
So, when encountering phrases like “a handle to a file” or “a handle to a process”, this can be read as “something that uniquely defines and points to the file/process”. It is possible to act on it; for example WriteFile 10 expects as first argument a HANDLE representing the file it can write to:
BOOL WriteFile( [in] HANDLE hFile, [in] LPCVOID lpBuffer, [in] DWORD nNumberOfBytesToWrite, [out, optional] LPDWORD lpNumberOfBytesWritten, [in, out, optional] LPOVERLAPPED lpOverlapped);Windows API Constructs
In this section a few topics are discussed that, if not known, might seem confusing, especially for readers who are not C programmers or have never programmed using the Windows API.
Calling the same function twice or more
There are scenarios in which the same function is called twice, and often the second call is inside a loop. One example is in the Example section of the RegQueryValueExA documentation 11, reported below:
#include <windows.h>#include <malloc.h>#include <stdio.h>
#define TOTALBYTES 8192#define BYTEINCREMENT 4096
void main(){ DWORD BufferSize = TOTALBYTES; DWORD cbData; DWORD dwRet;
PPERF_DATA_BLOCK PerfData = (PPERF_DATA_BLOCK) malloc( BufferSize ); cbData = BufferSize;
printf("\nRetrieving the data...");
dwRet = RegQueryValueEx( HKEY_PERFORMANCE_DATA, TEXT("Global"), NULL, NULL, (LPBYTE) PerfData, &cbData ); while( dwRet == ERROR_MORE_DATA ) { // Get a buffer that is big enough.
BufferSize += BYTEINCREMENT; PerfData = (PPERF_DATA_BLOCK) realloc( PerfData, BufferSize ); cbData = BufferSize;
printf("."); dwRet = RegQueryValueEx( HKEY_PERFORMANCE_DATA, TEXT("Global"), NULL, NULL, (LPBYTE) PerfData, &cbData ); } if( dwRet == ERROR_SUCCESS ) printf("\n\nFinal buffer size is %d\n", BufferSize); else printf("\nRegQueryValueEx failed (%d)\n", dwRet);}RegQueryValueEx is called once, but since the PerfData buffer might be too small to hold all the data stored in the registry key, it continues to be called with a bigger size every time until it succeeds inside the while loop. This is due to the fact that the size of the object being retrieved is unknown and there is no API that can give that value directly. Some APIs like EnumProcessModules specify this in their documentation:
It is a good idea to specify a large array of HMODULE values, because it is hard to predict how many modules there will be in the process at the time you call EnumProcessModules. To determine if the lphModule array is too small to hold all module handles for the process, compare the value returned in lpcbNeeded with the value specified in cb. If lpcbNeeded is greater than cb, increase the size of the array and call EnumProcessModules again.
Calling a First and a Next function
Sometimes functions like Process32First are followed by Process32Next. This is due to the fact that Microsoft sometimes uses a stateful enumeration pattern approach where the first call takes a snapshot of the processes, and the second call inside a loop returns each process. There are a few functions that behave this way:
- Process32First / Process32Next
- Thread32First / Thread32Next
- Module32First / Module32Next
- FindFirstFile / FindNextFile
An example usage of Process32First is shown in the learn section Process Enumeration.
Assembly Calling Conventions
When reading the decompiled assembly call from Ghidra, it is common to see functions preceded by a string like __thiscall, for example:
undefined __thiscall FUN_10001910 (void * this , wchar_t)The undefined is the return type that the decompiler was not able to understand, while the second string __thiscall is the calling convention.
There are 4 main conventions that are commonly encountered. These conventions determine:
- How to pass arguments to the function and which registers or stack to use
- Where the return value is stored
- Who cleans the stack used by the function
The conventions are summarized below:
| Convention | Arguments Location | Return Location | Stack Cleanup |
|---|---|---|---|
cdecl | Stack (pushed right → left) | EAX | Caller |
stdcall | Stack (pushed right → left) | EAX | Callee |
fastcall | First args in registers (commonly ECX, EDX), rest on stack | EAX | Callee |
thiscall | this in ECX, other args on stack | EAX | Callee (usually) |
Below is a comparison of the same function (FUN) which sums two numbers, with one example for each convention, and a more in-depth explanation of the convention.
cdecl
The arguments in a cdecl function are pushed on the stack in a very specific order, which is often mentioned as “from right to left”. If we were to look at the function in C we would have the signature as:
int FUN(int a, int b)The rightmost argument, b, is the first one pushed on the stack, the leftmost is the last one.
In this convention, the caller clears the stack space used for the arguments after the function returns. printf is a good example of a cdecl function as it is variadic, so the callee cannot know how many arguments were passed. Therefore it uses a convention like cdecl where the caller cleans the stack, because the caller knows exactly how many arguments it pushed.
Looking at the FUN implementation, the function definition is very straightforward: it saves the base pointer (EBP), which is popped at the end so the program knows where to continue from once the function finishes. It moves the stack pointer to the base pointer to have a stable reference to the arguments and return values. At this point the stack looks like this:
[ebp+0] = old EBP[ebp+4] = return address[ebp+8] = first argument (a)[ebp+12] = second argument (b)The first element is loaded in EAX and the second is summed to it. It is moved to EAX because this is where a cdecl expects the return value to be, and since the registers do not clear up after the function finishes, the caller will be able to use EAX as needed.
FUN PROC push ebp mov ebp, esp mov eax, [ebp+8] ; a add eax, [ebp+12] ; + b pop ebp ret ; no stack adjustment (caller cleans)FUN ENDPThe caller will call the function pushing the arguments, and at the end of the function it cleans the stack of 8 bytes, 4 per argument, given that they are integers and take 4 bytes each in most of the implementations.
push bpush acall FUNadd esp, 8 ; caller cleans; EAX = resultstdcall
stdcall is very similar to cdecl with the only exception that the function clears up the stack internally and the caller doesn’t have to care about it. At the end of the function it executes ret 8 which does two operations at once:
- by default it pops the return address into
EIP - if it is called with an operand, like
8it cleans up the stack withADD ESP, 8
FUN PROC push ebp mov ebp, esp mov eax, [ebp+8] ; a add eax, [ebp+12] ; + b pop ebp ret 8 ; callee cleans 2 args (8 bytes)FUN ENDPThe caller calls the function the same way as cdecl but after the call, the stack is already cleaned:
push bpush acall FUN; EAX = resultfastcall
In fastcall convention the first two arguments are stored in ECX and EDX while all the others, if needed, are pushed on the stack.
In this specific function the ret call is used without any operand only due to the fact that no extra arguments were pushed onto the stack, so there is no need to clean it; in other functions it might be called like in the stdcall example.
FUN PROC ; a is already in ECX, b is already in EDX mov eax, ecx ; eax = a add eax, edx ; eax += b ret ; no stack args here, so plain retFUN ENDPThe caller initializes the ECX and EDX registers to be used by the function.
mov ecx, amov edx, bcall FUN; EAX = resultthiscall
The thiscall convention is Microsoft-specific for C++, so it appears often in C++ samples.
The this argument is passed via ECX, while the rest are pushed in the stack. this is a pointer to an instance of a struct, and inside the function the struct arguments are generally used or initialized. In the example below, the first argument of the struct (x) is used by dereferencing it [ecx]. If a second argument was needed it would be taken via [ecx + 4] or any other number based on the size of the previous arguments.
FUN PROC push ebp mov ebp, esp ; ECX = this mov eax, [ecx] ; eax = this->x add eax, [ebp+8] ; + b (first stack arg) pop ebp ret 4 ; callee cleans 1 stack arg (b)FUN ENDPThe caller simply populates ECX and pushes any extra arguments on the stack:
mov ecx, thisPtrpush bcall FUN_thiscall ; EAX = result