back

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

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.

We're not interested in auditing, so SACLs won't be in the scope of this post.

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 DWORD, and all of that other stuff? 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 is a 32-bit integer. Some others are: LPCSTR being a const char * and PSECURITY_DESCRIPTOR being void *.

Why not GetSecurityInfo? 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 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:

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_MASKs 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