This is a very brief article about some facts that should be common knowledge to Windows developers, but that Linux developers might not know.
The main API of Windows is provided through several DLLs (Dynamic Link Libraries). An application can import functions from those DLL and call them. This way, the internal APIs of the Kernel can change from a version to the next without compromising the portability of normal user mode applications.
PE file format
Executables and DLLs are PE (Portable Executable) files. Each PE includes an import and an export table. The import table specifies the functions to import and in which files they are located. The export table specifies the exported functions, i.e. the functions that can be imported by other PE files.
PE files are composed of various sections (for code, data, etc…). The .reloc section contains information to relocate the executable or DLL in memory. While some addresses in code are relative (like for the relative jmps), many are absolute and depends on where the module is loaded in memory.
The Windows loader searches for DLLs starting with the current working directory, so it is possible to distribute an application with a DLL different from the one in the system root (\windows\system32). This versioning issue is called DLL-hell by some people.
One important concept is that of a RVA (Relative Virtual Address). PE files use RVAs to specify the position of elements relative the base address of the module. In other words, if a module is loaded at an address B and an element has an RVA X, then the element’s absolute address in memory is simply B+X.
If you’re used to Windows, there’s nothing strange about the concept of threads, but if you come form Linux, keep in mind that Windows gives CPU-time slices to threads rather than to processes like Linux. Moreover, there is no fork() function. You can create new processes with CreateProcess() and new threads with CreateThreads(). Threads execute within the address space of the process they belong to, so they share memory.
Threads also have limited support for non-shared memory through a mechanism called TLS (Thread Local Storage). Basically, the TEB of each thread contains a main TLS array of 64 DWORDS and an optional TLS array of maximum 1024 DWORDS which is allocated when the main TLS array runs out of available DWORDs. First, an index, corresponding to a position in one of the two arrays, must be allocated or reserved with TlsAlloc(), which returns the index allocated. Then, each thread can access the DWORD in one of its own two TLS arrays at the index allocated. The DWORD can be read with TlsGetValue(index) and written to with TlsSetValue(index, newValue).
As an example, TlsGetValue(7) reads the DWORD at index 7 from the main TLS array in the TEB of the current thread.
Note that we could emulate this mechanism by using GetCurrentThreadId(), but it wouldn’t be as efficient.
Tokens and Impersonation
Tokens are representations of access rights. Tokens are implemented as 32-bit integers, much like file handles. Each process maintains an internal structure which contains information about the access rights associated with the tokens.
There are two types of tokens: primary tokens and secondary tokens. Whenever a process is created, it is assigned a primary token. Each thread of that process can have the token of the process or a secondary token obtained from another process or the LoginUser() function which returns a new token if called with correct credentials.
To attach a token to the current thread you can use SetThreadToken(newToken) and remove it with RevertToSelf() which makes the thread revert to primary token.
Let’s say a user connects to a server in Windows and send username and password. The server, running as SYSTEM, will call LogonUser() with the provided credentials and if they are correct a new token is returned. Then the server creates a new thread and that thread calls SetThreadToken(new_token) where new_token is the token previously returned by LogonUser(). This way, the thread executes with the same privileges of the user. When the thread is finished serving the client, either it is destroyed, or it calls revertToSelf() and is added to the pool of free threads.
If you can take control of a server, you can revert to SYSTEM by calling RevertToSelf() or look for other tokens in memory and attach them to the current thread with SetThreadToken().
One thing to keep in mind is that CreateProcess() use the primary token as the token for the new process. This is a problem when the thread which calls CreateProcess() has a secondary token with more privileges than the primary token. In this case, the new process will have less privileges than the thread which created it.
The solution is to create a new primary token from the secondary token of the current thread by using DuplicateTokenEx(), and then to create the new process by calling CreateProcessAsUser() with the new primary token.