Named synchronization objects and elevated access permissions

RLWA32 46,111 Reputation points
2020-10-20T03:10:04.687+00:00

I created a named event, named mutex, named semaphore and a named waitable timer. CreateEvent, CreateMutex, CreateSemaphore and CreateWaitableTimer were passed nullptr as the lpEventAttributes, lpMutexAttributes, lpSemaphoreAttributes and lpTimerAttributes parameters, respectively. According to the documentation for these functions they should receive a default security descriptor with ACLs based on the primary or impersonation token of the user.

The default DACL from the user's process token shows that it grants GENERIC_READ and GENERIC_EXECUTE to the logon sid for the session. The user's account and the SYSTEM account get GENERIC_ALL. However, the security descriptor created for each named synchronization object grants the logon sid the same access permissions (Full Control) as the creating user account and the SYSTEM account. These permissions granted to the logon sid exceed what would be expected from the GENERIC_READ and GENERIC_EXECUTE that are contained in the process token's default DACL. Consequently, any other process running in the same session under a different account that receives the logon sid in its token will have full control over the synchronization objects. This is a security consideration that should be addressed.

This can be seen with the following sample code -

#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#include <AclAPI.h>
#include <sddl.h>

#include <stdio.h>
#include <tchar.h>

void PrintDefaultDACL();
void PrintDACL(HANDLE h);

int main()
{
    _tprintf(_T("\nPrint Default DACL from process token\n"));
    PrintDefaultDACL();

    _tprintf(_T("\nPrint DACL for named event\n"));
    HANDLE h1 = CreateEvent(nullptr, FALSE, FALSE, _T("EventTest"));
    PrintDACL(h1);

    _tprintf(_T("\nPrint DACL for named mutex\n"));
    HANDLE h2 = CreateMutex(nullptr, FALSE, _T("MutexTest"));
    PrintDACL(h2);

    _tprintf(_T("\nPrint DACL for named semaphore\n"));
    HANDLE h3 = CreateSemaphore(nullptr, 5, 5, _T("SemaphoreTest"));
    PrintDACL(h3);

    _tprintf(_T("\nPrint DACL for named waitable timer\n"));
    HANDLE h4 = CreateWaitableTimer(nullptr, FALSE, _T("WaitableTimerTest"));
    PrintDACL(h4);

    CloseHandle(h1);
    CloseHandle(h2);
    CloseHandle(h3);
    CloseHandle(h4);

    return 0;
}

void PrintDefaultDACL()
{
    HANDLE hToken = NULL;

    if (OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &hToken))
    {
        DWORD dwBytes = 0;
        PTOKEN_DEFAULT_DACL pDefaultDacl = nullptr;
        if (!GetTokenInformation(hToken, TokenDefaultDacl, pDefaultDacl, 0, &dwBytes) &&
            GetLastError() == ERROR_INSUFFICIENT_BUFFER)
        {
            pDefaultDacl = (PTOKEN_DEFAULT_DACL)LocalAlloc(LPTR, dwBytes);
            if (GetTokenInformation(hToken, TokenDefaultDacl, pDefaultDacl, dwBytes, &dwBytes))
            {
                PSECURITY_DESCRIPTOR pSD = LocalAlloc(LPTR, SECURITY_DESCRIPTOR_MIN_LENGTH);
                if (InitializeSecurityDescriptor(pSD, SECURITY_DESCRIPTOR_REVISION))
                {
                    if (SetSecurityDescriptorDacl(pSD, TRUE, pDefaultDacl->DefaultDacl, FALSE))
                    {
                        LPTSTR pszSD = nullptr;
                        ULONG uLen = 0;
                        if (ConvertSecurityDescriptorToStringSecurityDescriptor(pSD,
                            SDDL_REVISION_1, DACL_SECURITY_INFORMATION, &pszSD, &uLen))
                        {
                            _tprintf_s(_T("%s\n"), pszSD);
                            LocalFree(pszSD);
                        }
                    }
                }
                LocalFree(pSD);
            }
            LocalFree(pDefaultDacl);
        }
        CloseHandle(hToken);
    }
}

void PrintDACL(HANDLE h)
{
    PSECURITY_DESCRIPTOR pSD = nullptr;
    if (GetSecurityInfo(h, SE_KERNEL_OBJECT, DACL_SECURITY_INFORMATION,
        nullptr, nullptr, nullptr, nullptr, &pSD) == ERROR_SUCCESS)
    {
        LPTSTR pszSD = nullptr;
        ULONG ulen = 0;
        if (ConvertSecurityDescriptorToStringSecurityDescriptor(pSD,
            SDDL_REVISION_1, DACL_SECURITY_INFORMATION, &pszSD, &ulen))
        {
            _tprintf_s(_T("DACL: %s\n"), pszSD);
            LocalFree(pszSD);
        }
        LocalFree(pSD);
    }
}
Windows API - Win32
Windows API - Win32
A core set of Windows application programming interfaces (APIs) for desktop and server applications. Previously known as Win32 API.
2,689 questions
C++
C++
A high-level, general-purpose programming language, created as an extension of the C programming language, that has object-oriented, generic, and functional features in addition to facilities for low-level memory manipulation.
3,788 questions
{count} votes

3 answers

Sort by: Most helpful
  1. Drake Wu - MSFT 991 Reputation points
    2020-10-21T01:48:52.797+00:00

    Hi @RLWA32 ,
    Thank you for reporting this for us.
    I can both reproduce this 2 cases you metioned in windows 2004,1909 version.
    For the original sample(logon sid is assigned with *_ALL_ACCESS in the document):
    33854-1.png

    For the global namespace:
    33931-2.png

    I have reported with 2 samples internally, any progress will be update here.

    Some Updates:

    1. The default DACL being created for mutexs, events, timers, etc… appears to be hard coded and it is not using the default dacl in the token to create the DACL. It only changes the creator owner who gets full access and LocalSystem and the logon session get full access as well.
      You can experiment by changing the default dacl and you’ll discover it doesn’t change the dacl on any of the synchronization objects. If you test with a private object, it does change the security descriptor based on the default dacl so the default dacl mechanism definitely works for secured objects.
    2. For "Consequently, any other process running in the same session under a different account that receives the logon sid in its token will have full control over the synchronization objects. This is a security consideration that should be addressed"
      We would say the following:
      • The logon sesion is going to be associated with the interactively logged on user via local or RDP logon. This user may or may not be an administrator. This user can launch other processes as other users and they will be a member of the same logon session. (This is how CreateProcecesswithLogonW() works due to interactive desktop access) Typically if you do runas or CreateProcessWithLogonW(), you’ll be running a higher privileged user so does it matter that this user has access to your secured objects? Even if they didn’t have direct access since they are a member of the administrator group they could get access to the named synchronization object.
      • The other way an application can make a user be a member of the same logon session, they would need to be localsystem and have the “act as part of the operating system” privilege and it has to be enabled to add a group to a new user via LsaLogonUser() If a hacker is already running as LocalSystem, they already have full access to the system and they can easily get access to the named synchronization object.
      • Services, users running in other logon sessions will not have access to these kernel objects since they are running in a different logon session unless they are configured for LocalSystem or a user who is a member of the administrator group.
      • If a hacker can run a process as the logged on user, they will already have full access to your named synchronization objects.
      • Our thoughts on why we grant full access to the logon session is for a couple of reasons:
        1. WindowsStations/desktops for runas
        2. Processes, when the user logs off, Windows needs to be able to terminate all processes associated with the logon session.

    If the answer is helpful, please click "Accept Answer" and upvote it.
    Note: Please follow the steps in our documentation to enable e-mail notifications if you want to receive the related email notification for this thread.

    1 person found this answer helpful.

  2. RLWA32 46,111 Reputation points
    2021-01-04T12:13:18.363+00:00

    The default DACL being created for mutexs, events, timers, etc… appears to be hard coded and it is not using the default dacl in the token to create the DACL

    According to this explanation the current documentation is inaccurate since the default DACL is not always used.

    Typically if you do runas or CreateProcessWithLogonW(), you’ll be running a higher privileged user so does it matter that this user has access to your secured objects? Even if they didn’t have direct access since they are a member of the administrator group they could get access to the named synchronization object.

    This doesn't make sense. Neither runas or CreateProcessWithLogonW is capable of starting a process that requires elevation when UAC is in effect even if the caller is running with elevated privileges. It is also an irrelevant assumption.

    Yes, I (User A) can start a process as a different user (User B) in my interactive session. But it doesn't mean that User A wants a process running as User B to have unrestricted access to User A resources. However, the current implementation automatically grants those other processes unrestricted access to kernel objects in the same session. Why is this not the crossing of a security boundary? By that way of thinking, it should be fine for those other processes to have unrestricted access to User A's profile data. After all, the User B process was started by User A and they are in the same session. Uggh!!

    What is the rationale for not providing FULL Control to the logon sid when kernel objects are created in the Global namespace? Now User A can start a process as User B but the User B process doesn't have FULL control based on the logon sid. This seems to me to be a repudiation of the reasoning for allowing FULL control for kernel objects that are not in the Global namespace. How do you explain this glaring inconsistency?

    At a minimum, the documentation should be corrected.

    0 comments No comments

  3. Drake Wu - MSFT 991 Reputation points
    2021-02-12T02:23:42.36+00:00

    Hi @RLWA32

    > According to this explanation the current documentation is inaccurate since the default DACL is not always used.

    I agree, the documentation will need to be revised.

    > Neither runas or CreateProcessWithLogonW is capable of starting a process that requires elevation when UAC is in effect even if the caller is running with elevated privileges. It is also an irrelevant assumption.

    Let me clarify.
    Actually, you can start an elevated process if you use the local administrator account. I do this all the time.
    You are correct that If you specify a user who is a member of the administrator group, you cannot run the user elevated via runas/CreateProcessWithLogonW(). (The API/Command does not process the manifest which specifies UAC security settings which is why this doesn’t work)

    Now, if you do run an application as user B via runas from user A. User A needs the credentials of user B so I’m not sure how secure this is since user A is in charge of running the application as user B. If the notion is that running an application as user B from user A is secure, I would have to disagree. You already have a security issue since both user A and user B share the same Interactive Windows Desktop so the security boundary between the 2 processes isn’t secure (they can send windows messages to each other, see shatter attack). If you really need to run an application as User B and you wanted to securely isolate the application from User A. You would want to do this from a Windows Service since you would be running in a different RDP session.

    > What is the rationale for not providing FULL Control to the logon sid when kernel objects are created in the Global namespace?

    It appears that named kernel objects created in the Global Namespace do use the default DACL. If the logon SID had full access, this would be a security issue since Global Named Objects span RDP/Console session space. This is quite different if all the processes are running in the same RDP/Console session.

    Based on this information, if you wanted a more secured named object, you should create it in the Global Namespace vs the Local Namespace. This would prevent processes invoked by runas/CPLW from have full access to the named kernel object.


    If the answer is helpful, please click "Accept Answer" and upvote it.
    Note: Please follow the steps in our documentation to enable e-mail notifications if you want to receive the related email notification for this thread.

    0 comments No comments

Your answer

Answers can be marked as Accepted Answers by the question author, which helps users to know the answer solved the author's problem.