In fact, one could simply answer this question: “Memory is allocated in a heap” and calm down on this. However, I wondered why, in all the examples related to the security token, a pair of PTOKEN_* - GlobalAlloc . Why can't I just create a local variable of type TOKEN_* ?
The question turned out to be more complicated than it might seem at first glance, and therefore the answer to it became quite large. In fact, not one but three interrelated questions were considered, each of which was given its own section of the answer.
1. Why allocate memory
Let's talk about the need for dynamic memory allocation for a token. It would seem, what could be so complicated about the unsophisticated structure of TOKEN_OWNER ? It seems nothing special - know yourself, create a local variable and pass a pointer to it in the GetTokenInformation function:
// ... TOKEN_OWNER to; DWORD len = sizeof(to); GetTokenInformation(hToken, TokenOwner, &to, len, &len); // ...
However, a program with such a fragment returns, at best, very strange results, and at worst it crashes its work. What's the matter?
The catch is that TOKEN_OWNER is a “double bottom” structure. On the one hand, it contains a field pointer to the SID (security identifier), and on the other hand, some additional hidden fields in which the operating system stores internal information that is inaccessible for direct reading and modification.
For ease of understanding, you can imagine that TOKEN_OWNER is the base class provided to the programmer, and Windows itself operates with some TOKEN_OWNER child class inherited from TOKEN_OWNER .
The strangest thing is that this feature is not described anywhere . Although MSDN honestly says the following about the TOKEN_OWNER::Owner field:
The structure identifier (SID) structure is a variable-length structure.
In translation:
A security identifier (SID) is a variable-length structure used to uniquely identify a user or group.
From all of the above, we can draw a number of important conclusions:
- The exact structure of
TOKEN_OWNER unknown. We are simply given a pointer to the only known Owner field, and the rest is suggested to be considered as a “black box”. - Hidden fields are not part of the official API, and therefore their composition and purpose may change between versions of Windows when new security aspects are introduced. As a result, even if we find out what exactly is stored in the hidden part, direct access to these fields is fraught with the inoperability of our program in the future.
- Once the full version of the structure is known exclusively to the operating system, then the size of the memory required for its placement must be learned from the same operating system. The
GetTokenInformation(hToken, TokenOwner, NULL, 0, &len); string GetTokenInformation(hToken, TokenOwner, NULL, 0, &len); is responsible for getting the size GetTokenInformation(hToken, TokenOwner, NULL, 0, &len); where occurs:- send empty buffer
- ignoring error
GetLastError() == STATUS_BUFFER_TOO_SMALL , that is, an error about the size of the transferred buffer - and finally, getting the minimum required buffer size into the variable
len (that's why we pass this variable by pointer so that the function fills it for us).
- If the size of the structure in advance (that is, at the compilation stage) is unknown, then we are forced to allocate memory for it on the heap. We could just as well use the
alloca function, which dynamically allocates memory on the stack, but it is not included in the standard, and therefore may be missing from some compilers.
That is why we are forced to replace
TOKEN_OWNER to; DWORD len = sizeof(to);
on
DWORD len = 0; GetTokenInformation(hToken, TokenOwner, NULL, 0, &len); PTOKEN_OWNER to = (PTOKEN_OWNER)LocalAlloc(LPTR, len);
where PTOKEN_OWNER defined as typedef struct TOKEN_OWNER* PTOKEN_OWNER; .
2. Why LocalAlloc
Now the question may arise, why LocalAlloc ? What's wrong with GlobalAlloc , HeapAlloc , yes, in the end, malloc and new ? After all, all five functions allocate space in the current process heap and return the same pointer to it.
Um ... Not really. More precisely, it was once not quite.
The fact is that in the 16-bit era (Windows 1.0 – Me) virtual memory was either not yet invented (Windows 1.0–2.11), or it could not be used (Windows 3.0 – Me). [Information taken from the Windows Support Center “ Windows Version History ”] .
How could 16-bit Windows support multitasking if, in the absence of virtual memory, all the programs were located in the same address space, the total size of which cannot exceed 2 16 = 64KB? And this is without taking into account deductions for the needs of the operating system and equipment.
Um ... And again, not quite 64 kilobytes. Fortunately for programmers of those years, there was such a thing as segmentation - memory partitioning into sections, the addressing within which was continuous and produced by a 16-bit, near pointer. Here is the size of the segment, that is, the area directly addressed by the near memory pointer, and was equal to 64KB. Intersegment calls were made using an extended, distant pointer containing, in addition to the linear address, the segment number. Such long-distance calls were more expensive (especially considering the loading and unloading of Windows segments, which can be read by Raymond Chen in the article “ What happened in real-mode Windows when? ” And on Habrajabr in the article “ Memory Management in Real Windows Mode ”).
Windows divided the segments into two types - private , which are entirely related to a particular application or library, and common , shared between all applications at once. Accordingly, there were two types of heaps: private, accessible through LocalAlloc (which returned the near pointer), and global, accessible through GlobalAlloc (which returned the far pointer). And if for the internal data of the application any of them could be used, the exchange of data with other applications and functions of the operating system should have passed exclusively through the global heap. That is why all the examples of working with functions from the Windows API use GlobalAlloc when dynamically allocating memory.
Now it does not make any sense, since the segments were abolished, 32-bit applications received the full 32-bit pointer, and GlobalAlloc and LocalAlloc became stubs over the newly introduced HeapAlloc , exactly like malloc and new .
3. What is hidden inside the structure TOKEN_OWNER
So, we’ve figured out why getting a security token comes with mandatory heap memory allocation. At the same time, we found out that the TOKEN_OWNER structure contains some additional data hidden from the programmer’s eyes.
Why was this done? And what could be hiding there? The first question we, being incompetent in information security, can not give an answer. However, the answer to the second answer is quite possible. The fact is that currently there are two open implementations of the Windows API aimed at ensuring maximum compatibility with the original. These are Wine (discussed below) and the ReactOS API .
Before proceeding further, I repeat:
Hidden fields are not part of the official API, and therefore their composition and purpose may vary between versions of Windows.
All of the following is intended solely to satisfy curiosity and is true for one particular implementation. The author is not responsible for data corruption and / or vulnerabilities arising from the use of undocumented features of the Windows API.
Why did we even decide that something extra was written in the structure? The fact is that the buffer size required to store the TOKEN_OWNER structure is 32 bytes (relevant for the WoW x32 subsystem in Windows 7 SP1):
DWORD len = 0; GetTokenInformation(hToken, TokenOwner, NULL, 0, &len); std::cout << len << std::endl; // Выводит «32».
although the size of the TOKEN_OWNER::Owner pointer TOKEN_OWNER::Owner only 4 bytes. The difference between these two values, 28 bytes, is the size of the additional data.
So let's go. First, consider the implementation of the function GetTokenInformation , called from the code in question:
BOOL WINAPI GetTokenInformation(...) { TRACE(...); return set_ntstatus( NtQueryInformationToken( token, tokeninfoclass, tokeninfo, tokeninfolength, retlen)); }
That is, it is just a wrapper over NtQueryInformationToken . Go ahead . The function code is long, and therefore we consider only the necessary pieces:
NTSTATUS WINAPI NtQueryInformationToken(...) { static const ULONG info_len [] = { ... 0, /* TokenOwner */ ... }; ULONG len = 0; NTSTATUS status = STATUS_SUCCESS; TRACE(...); if (tokeninfoclass < MaxTokenInfoClass) len = info_len[tokeninfoclass]; if (retlen) *retlen = len; if (tokeninfolength < len) return STATUS_BUFFER_TOO_SMALL; switch (tokeninfoclass) { ... case TokenOwner: SERVER_START_REQ( get_token_sid ) { TOKEN_OWNER *towner = tokeninfo; PSID sid = towner + 1; DWORD sid_len = tokeninfolength < sizeof(TOKEN_OWNER) ? 0 : tokeninfolength - sizeof(TOKEN_OWNER); req->handle = wine_server_obj_handle( token ); req->which_sid = tokeninfoclass; wine_server_set_reply( req, sid, sid_len ); status = wine_server_call( req ); if (retlen) *retlen = reply->sid_len + sizeof(TOKEN_OWNER); if (status == STATUS_SUCCESS) towner->Owner = sid; } SERVER_END_REQ; break; ... } return status;
What do we have? Wine (or rather, the NtQueryInformationToken function implemented in it) treats PTOKEN_OWNER as a pointer to a structure of the following form:
TOKEN_OWNER { PSID* Owner ---+ } | SID <----------+ { ... }
where TOKEN_OWNER is addressed by the local variable towner ( TOKEN_OWNER *towner = tokeninfo; ) and SID is the local variable sid ( PSID sid = towner + 1; ).
That is, the “hidden” part is just what the TOKEN_OWNER::Owner field indicates. In other words, everything related to the token is collected in one place.