Checking Execution Permissions with the Windows API in C++.
For people who have never worked with Win32 before (AKA, me, a day before starting to write this blog post).
By 56dev_ <programsym987@gmail.com>August 25, 2025
Contents
- Introduction
- Security Descriptors
- Processes, Threads, and Impersonation
- Generic Mapping and Privileges
- AccessCheck and putting it all together
- THE FULL CODE
- Sources and Further Reading
Introduction
Finding execution permissions is not as simple in Windows as it is in
POSIX-style systems. Usually you would just use std::filesystem::status::permissions
,
which, in theory, could maybe work, but we're just going to learn to "proper" way to
do it in Windows, and learn a little bit about how the Windows API works along the way.
Environment
This is in Visual Studio 2022, with C++20. As far as I know, C++17 should work, as that
is the minimum version for the std::filesystem
library.
Disclaimer
If the subtitle wasn't clear enough, I'm no accredited expert on the Win32 API or even of C++. If you find an error or find a section which you would like some better wording for, please email!
Security Descriptors
To store file permissions, Windows uses Security Descriptors. These are made up of an owner SID, group SID, a DACL, and a SACL.
- The owner SID is usually the person who created the file, or generally a person with administrative permissions over it.
- The group SID is there for some level of compatibility with the POSIX permissions system.
- The DACL is the access control list that defines more specific Windows permissions.
- The SACL's only use is auditing.
To access the security descriptor, we use the GetNamedSecurityInfo
function.
The Code
To do this in C++, you need to use the GetNamedSecurityInfo
function.
(Or a suitable alternative, but we're using this one in this tutorial.) We will beam the information it gives
into a security descriptor variable.
First, make sure you have the necessary header files included: they are <windows.h>
and <aclapi.h>
. Then, let's create the object.
The function definition for GetNamedSecurityInfo
is as follows:
DWORD GetNamedSecurityInfo(
LPCSTR pObjectName,
SE_OBJECT_TYPE ObjectType,
SECURITY_INFORMATION SecurityInfo,
[out, opt] PSID* ppsidOwner,
[out, opt] PSID* ppsidGroup,
[out, opt] PACL ppDACL,
[out, opt] PACL* ppSACL,
[out, opt] PSECURITY_DESCRIPTOR* ppSecurityDescriptor)
Every out paramater is optional. The first four out parameters are if we wanted to access each data structure
individually, but we don't. So, we'll just pass in the address of a PESECURITY_DESCRIPTOR
object as the last
argument, and let everything else be null.
PSECURITY_DESCRIPTOR pSD{ nullptr };
DWORD result = GetNamedSecurityInfo(
path.c_str(),
SE_FILE_OBJECT,
OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION,
nullptr,
nullptr,
nullptr,
nullptr,
&pSD
);
if (result != ERROR_SUCCESS)
{
std::cerr << "unable to get sec info" << std::endl;
LocalFree(pSD);
return false;
}
What is a
Windows is very fond of using typedefs in its code, partly for clarity,
partly due to how old it is, and partly for compatibility's sake.
Microsoft itself admits that some of these definitions
are redundant. In our case, a DWORD
, and all of that other stuff?DWORD
is a 32-bit integer. Some others are:
LPCSTR
being a const char *
and PSECURITY_DESCRIPTOR
being
void *
.
Why not
The way I wrote it, I'm passing in the filepath to the executable, instead of a handle
to it. If you have the handle to your file, then you can totally use GetSecurityInfo
?GetSecurityInfo
instead.
Why
c_str()
?pObjectName
is of type LPCSTR
, which is really just a pointer to a C-string; that is,
const char *
.
How does the third argument work?
All of those all-caps INFORMATION
terms are macros to low-level representations of integers (you can CTRL
+ Click on Visual Studio to check). Each representation has a certain bit(s?) set to ON, each bit representing security
information we want. The |
operator ORs each integer together, meaning: if a bit is set to ON in one, it will be set
to ON in the final product. So the argument basically sets all the bits to ON which represent the information
we want!
Please note that you will need to free the PSECURITY_DESCRIPTOR
object later
using LocalFree
. The reason is this object is dynamically-allocated memory, and not freeing it
will result in memory leaks.
Processes, Threads, and Impersonation
You probably know that in Windows, processes have threads which are the actual paths of execution of a program. Processes and threads have access tokens, which are (oversimplified a bit) used so the operating system can judge their privileges, rights, and permissions.
Windows uses the impersonation model, for reasons related to preventing race conditions. In this model, there are two kinds of access tokens: primary tokens and impersonation tokens.
Let's not overcomplicate: primary tokens are process tokens, and impersonation tokens are thread tokens. Don't get too hung up on what "impersonation" even really implies: I don't even know myself.
Threads don't usually have their own tokens. They merely inherit the primary token, and use it to perform
their operations. However, certain actions need threads to have their own token, such as when
you want threads to execute in their own security context. Calling
AccessCheck
also requires you to have a thread token, even though, in this case,
we won't be altering the security context at all. Hence, we will need to create one. We don't need to change anything:
we just want the thread token to refer to the same security context as the process token.
The Code
if (!ImpersonateSelf(SecurityImpersonation))
{
std::cerr << "unable to impersonate" << std::endl;
LocalFree(pSD);
return false;
}
In reality, ImpersonateSelf
is shorthand for calls to several functions. It:
- Opens the primary token.
- Duplicates it as a thread token.
- Assigns the thread token to the thread.
In this way, you now have an actual thread-version copy of the process token!
NOTE: later, when you're done impersonating, you will need to call RevertToSelf
in order to discard the thread token.
Generic Mapping and Privileges
The function we will use, AccessCheck
, requires us to specify two other
additional arguments. These are GENERIC_MAPPING
and PRIVILEGE_SET
.
Basically, different objects have different kinds of rights. As a hypothetical example, what
would it mean to "read" a printer? You cannot read a printer the same way you could read a file.
To provide a level of abstraction so that programmers don't need to think about these little
differences each time, Windows provides generic rights. But, it is still the programmer's
responsibility to specify how these generic rights should be interpreted at the object level.
For this, we have the GENERIC_MAPPING
structure. In it are four ACCESS_MASK
s
representing the four generic rights: read, write, execute, and all. We will create a new GENERIC_MAPPING
structure and assign these masks to the proper specific rights.
Finally, let's view what a privilege is. Whereas rights control access to objects,
privileges control access to actions - such as, for example, the action of checking your
rights to an object. AccessCheck
requires that you pass in a pointer to a PRIVILEGE_SET
where it can store all of the privileges you needed, as well as a pointer to its size.
The Code
GENERIC_MAPPING mapping{ {} };
mapping.GenericRead = FILE_GENERIC_READ;
mapping.GenericWrite = FILE_GENERIC_WRITE;
mapping.GenericExecute = FILE_GENERIC_EXECUTE;
mapping.GenericAll = FILE_ALL_ACCESS;;
PRIVILEGE_SET privileges{ {} };
DWORD privilegeSetLength = sizeof(privileges);
It looks like we're mapping generic rights to more generic rights! What's up with that?
These are predefined macros that actually expand to the provided standard rights for a file, OR'd together.
It is yet another level of abstraction. You can check by CTRL + Clicking in Visual Studio. For example,
FILE_GENERIC_READ
expands to
(STANDARD_RIGHTS_READ |\
FILE_READ_DATA |\
FILE_READ_ATTRIBUTES |\
FILE_READ_EA |\
SYNCHRONIZE)
AccessCheck and Putting it All Together
At last, the final stretch! It really is as simple as passing everything we've done so far
into the proper positions in AccessCheck
, along with the file permission we are
requesting.
Just a few things: we need to declare
an ACCESS_MASK
to store the access we were granted, and a boolean to store
whether we were granted the access we requested.
Remember that after this, you need to free the pSD
and revert your
impersonation token.
The Code
ACCESS_MASK grantedAccess{ 0 };
BOOL accessStatus{ FALSE };
if (!AccessCheck(pSD, GetCurrentThreadToken(), FILE_EXECUTE, &mapping, &privileges, \
&privilegeSetLength, &grantedAccess, &accessStatus))
{
std::cerr << "Checking Security Permissions: Access check was unable to succeed for path " \
<< path << ": error " << GetLastError() << std::endl;
accessStatus = FALSE;
}
RevertToSelf();
LocalFree(pSD);
Why BOOL
and not bool
?
To make absolutely sure that we're playing by Windows' rules and synchronizing with
how its functions are written, we use the typedef instead of
the fundamental datatype. Interestingly, BOOL
is a typedef for an int
,
and, to make matters worse, Windows has its own guidelines for how to use this datatype.
THE FULL CODE
Sources and Further Reading
Books
- Programming Windows Security by Keith Brown