2

My app has 2 processes, one requires elevation, another doesn’t, but otherwise they’re running under the same user account on the same desktop.

I need to create a file (not on disk, other type of file) in elevated process which reads from the file, but have my non-elevated process write access to the file.

With nullptr SECURITY_ATTRIBUTES, non-elevated process can’t open the file, CreateFile fails with access denied code. That’s expected and the SetSecurityDescriptorDacl workaround similar to this answer works OK.

However, I don’t like that workaround. I don’t want to give write access to that file to Everyone. I only want to give access to the current user. This is kinda sensitive, the elevated reader process will run for hours and I don’t want Everyone to be able to write to that file.

How do I get/construct SECURITY_ATTRIBUTES that would be the default security for non-elevated process running on the same desktop?

Soonts
  • 20,079
  • 9
  • 57
  • 130
  • The [tag:c++] tag is reserved to ask about problems specifically with c++ code. Your question doesn't contain any code example, hence I removed it (again). – πάντα ῥεῖ Sep 02 '18 at 16:36
  • Possible duplicate of [Win32 API: Creating file public for current user but private for everyone else](https://stackoverflow.com/questions/40077701/) – Remy Lebeau Sep 02 '18 at 17:05
  • Do the processes run simultaneously? If so, you could also duplicate the file handle from the elevated process into the non-elevated process, allowing it to write to the file even though it would not be able to open it itself. The processes would need some way to communicate (so the elevated process could know the ID of the non-elevated process, and so the non-elevated process could learn the value of the duplicated handle), but there are plenty of ways that could be achieved. – Iridium Sep 02 '18 at 17:12
  • 1
    You are referring to `CreateFile`, but want a file, that's not backed by storage on the disk. It's not really clear, what you are asking for. Can you clarify? @πάν: That's not, what the [tag:c++] tag is for. From the info: *"Use this tag for questions about code (to be) compiled with a C++ compiler."* That does not imply, that the tag should be used for C++ specific problems only (although that would probably make for better tag searchability). – IInspectable Sep 02 '18 at 17:14
  • @Iridium They run simultaneously, but neither of them launches each other (writer is launched by a system service), and that file is the main method how they communicate. – Soonts Sep 02 '18 at 17:59
  • 1
    @IInspectable CreateFile can open many other types of named Win32 objects. In my app the file’s a mailslot, but that’s not too important, it would be same for named pipes of some other Win32 things. – Soonts Sep 02 '18 at 18:06
  • So you are looking for a securable object, *including* files on disk. If that is the case, you should probably leave out that part of the question, as it only causes confusion. Unless there is a specific reason you do not want that after all. – IInspectable Sep 02 '18 at 18:16
  • @IInspectable Yes, there is, and the specific reason is inherited permissions. For a file on disk, the simple answer is “just create it in any place where inherited permissions do the job, e.g. `%LOCALAPPDATA%`” – Soonts Sep 02 '18 at 18:20
  • You can explicitly remove inherited permissions. If that is the only reason you do not want to use a file on disk, then that is not really a reason. You'd have to use a different DACL, but that's really it. – IInspectable Sep 02 '18 at 18:22
  • @IInspectable The reason why I don’t want file on disk is because I don’t need file on disk. The reason why I mentioned that’s not on disk is to exclude the obvious file system inheritance answer only applicable to disk files. – Soonts Sep 02 '18 at 18:25
  • @πάνταῥεῖ Returned the tag. Read the tag wiki. I'm using a C++ compiler. – Soonts Sep 02 '18 at 18:30
  • In that case it would really be better to state, what you need (generic solution), instead of using a cryptic message to exclude, what you don't want. Concentrate on the secured IPC, stating the lifetime relationships of the consumer and producer. That will probably make for a more useful Q&A. – IInspectable Sep 02 '18 at 18:38
  • 1
    `CreateFile` creates or opens only a File object. NT has 3 types of create calls for the kernel function [`IoCreateFile`](https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/content/wdm/nf-wdm-iocreatefile): `CreateFileTypeNone` (i.e. `NtCreateFile` system call for a regular file or device), `CreateFileTypeNamedPipe` (i.e. `NtCreateNamedPipeFile`), and `CreateFileTypeMailslot` (i.e. `NtCreateMailslotFile`). The latter two use the reserved `InternalParameters` to create a server-side named pipe or mailslot. – Eryk Sun Sep 02 '18 at 23:12
  • Interestingly, socket files are handled differently by the AFD device, which uses `NtCreateFile` with extended attributes (`EaBuffer`) instead of adding `CreateFileTypeSocket`. – Eryk Sun Sep 02 '18 at 23:14
  • @eryksun - this mean that pipe and mailslot is special internal file types. and exist `IRP_MJ_CREATE_NAMED_PIPE` and `IRP_MJ_CREATE_MAILSLOT` which got device(driver) on create. instead `IRP_MJ_CREATE` for all other normal files. sockets only usual file on *AFD* device. from another side we can implement own pipe/mailslot on self custom device - so possible custome implementations of pipes/mailsots, different from built-in. – RbMm Sep 03 '18 at 00:11
  • also interesting number or `IRP_MJ_CREATE_MAILSLOT` - are this mean that on very early build was named pipes but yet no mailslots, pnp, etc ? also interesting why msdn say about so called *anonymous* pipes, like this is another (from named) pipes object type. when really this is single object type - pipe – RbMm Sep 03 '18 at 00:14
  • 1
    I was addressing the claim that "CreateFile can open many other types of named Win32 objects". This call is implemented as `NtCreateFile` => `IoCreateFile` : `CreateFileTypeNone`, which creates a File object that references either a device directly or a file on a device (e.g. a file in a file system or in a device namespace). Named pipes and mailslots are created in rudimentary file systems (e.g. "\Device\NamedPipe\" is listable via `NtQueryDirectoryFile`), in which a file is created by the first server-side File object reference. – Eryk Sun Sep 03 '18 at 04:15

1 Answers1

4

if we run elevated process (by admin user) - it have not elevated linked session. (and non elevated process have elevated linked session) you need:

  1. open process token
  2. query linked session token for this token via TokenLinkedToken
  3. query default dacl for this linked token via TokenDefaultDacl
  4. initialize security descriptor with this DACL

code for get default dacl for not elevated session:

ULONG BOOL_TO_ERROR(BOOL f)
{
    return f ? 0 : GetLastError();
}

ULONG GetNotElevatedDefaultDacl(PTOKEN_DEFAULT_DACL* DefaultDacl)
{
    HANDLE hToken;
    ULONG err = BOOL_TO_ERROR(OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &hToken));

    if (!err)
    {
    ULONG cb;
        union {
            TOKEN_LINKED_TOKEN tlt;
            TOKEN_ELEVATION_TYPE tet;
        };

        err = BOOL_TO_ERROR(GetTokenInformation(hToken, TokenElevationType, &tet, sizeof(tet), &cb));

        if (!err)
        {
            if (tet == TokenElevationTypeFull)
            {
                err = BOOL_TO_ERROR(GetTokenInformation(hToken, TokenLinkedToken, &tlt, sizeof(tlt), &cb));
            }
            else
            {
                err = ERROR_ELEVATION_REQUIRED;
            }
        }
        CloseHandle(hToken);

        if (!err)
        {
            union {
                PTOKEN_DEFAULT_DACL p;
                PVOID buf;
            };

            cb = 0x100;

            do 
            {
                if (buf = LocalAlloc(0, cb))
                {
                    if (err = BOOL_TO_ERROR(GetTokenInformation(
                        tlt.LinkedToken, TokenDefaultDacl, buf, cb, &cb)))
                    {
                        LocalFree(buf);
                    }
                    else
                    {
                        *DefaultDacl = p;
                    }
                }
                else
                {
                    err = GetLastError();
                    break;
                }

            } while (err == ERROR_INSUFFICIENT_BUFFER);

            CloseHandle(tlt.LinkedToken);
        }
    }

    return err;
}

and using it (this for any object which take SECURITY_ATTRIBUTES on create)

PTOKEN_DEFAULT_DACL DefaultDacl;
ULONG err = GetNotElevatedDefaultDacl(&DefaultDacl);

SECURITY_DESCRIPTOR sd;
SECURITY_ATTRIBUTES sa = { sizeof(sa), &sd, FALSE };

InitializeSecurityDescriptor(&sd, SECURITY_DESCRIPTOR_REVISION);

if (!err)
{
    SetSecurityDescriptorDacl(&sd, TRUE, DefaultDacl->DefaultDacl, FALSE);
}

HANDLE hObject = CreateMailslot(
        L"\\\\?\\Global\\MailSlot\\12345678", 0, MAILSLOT_WAIT_FOREVER, &sa);

if (!err)
{
    LocalFree(DefaultDacl);
}

if (hObject)
{
    // CheckObjectSD(hObject);
    CloseHandle(hObject);
}

if create object(mailslot in this case) from elevated process with default dacl - security DACL will be look like:

T FL AcessMsK Sid
A 00 001F01FF S-1-5-32-544 'Administrators'
A 00 001F01FF S-1-5-18 'SYSTEM'
A 00 001200A9 S-1-5-5-0-x 'LogonSessionId_0_x'

so all access for SYSTEM and Administrators and read+execute access for current Logon Session. as result not elevated process from same logon session have only read access.

if use explicit DACL from not elevated session - result:

T FL AcessMsK Sid
A 00 001F01FF S-1-5-21-a-b-c-d 'SomeUser'
A 00 001F01FF S-1-5-18 'SYSTEM'
A 00 001200A9 S-1-5-5-0-x 'LogonSessionId_0_x'

so all access for SYSTEM and SomeUser and read+execute access for current Logon Session.

note because elevated process have SomeUser as TokenUser he have all access for this object

for check object security descriptor we can use for example next code:

void CheckObjectSD(HANDLE hObject)
{
    union {
        PSECURITY_DESCRIPTOR psd;
        PVOID buf;
    };

    ULONG cb = 0, rcb = 0x30;
    volatile static UCHAR guz;
    buf = alloca(guz);
    PVOID stack = alloca(guz);

    ULONG err;
    do 
    {
        if (cb < rcb)
        {
            cb = (ULONG)((ULONG_PTR)stack - (ULONG_PTR)(buf = alloca(rcb - cb)));
        }

        if (!(err = BOOL_TO_ERROR(GetKernelObjectSecurity(hObject, 
            DACL_SECURITY_INFORMATION|LABEL_SECURITY_INFORMATION|OWNER_SECURITY_INFORMATION, psd, cb, &rcb))))
        {
            PWSTR psz;
            if (ConvertSecurityDescriptorToStringSecurityDescriptorW(psd, SDDL_REVISION, 
                DACL_SECURITY_INFORMATION|LABEL_SECURITY_INFORMATION|OWNER_SECURITY_INFORMATION, &psz, 0))
            {
                DbgPrint("%S\n", psz);
                LocalFree(psz);
            }
        }

    } while (err == ERROR_INSUFFICIENT_BUFFER);
}

if we have not admin user account, elevation was via another user account. elevated process in this case have no more linked session (if we try query linked token we got error - A specified logon session does not exist. It may already have been terminated. ). possible solution here(and in generic case too) in next:

for users process default DACL usually grant GENERIC_ALL for System and UserSid and GENERIC_READ | GENERIC_EXECUTE for logon session SID. we can query process token, get it default DACL, found LogonSession SID in DACL and change it access mask to GENERIC_ALL. this can be done by next code:

ULONG GetDaclForLogonSession(HANDLE hToken, PTOKEN_DEFAULT_DACL* DefaultDacl)
{
    ULONG err;

    ULONG cb = 0x100;

    union {
        PTOKEN_DEFAULT_DACL p;
        PVOID buf;
    };

    do 
    {
        if (buf = LocalAlloc(0, cb))
        {
            if (!(err = BOOL_TO_ERROR(GetTokenInformation(hToken, TokenDefaultDacl, buf, cb, &cb))))
            {
                err = ERROR_NOT_FOUND;

                if (PACL Dacl = p->DefaultDacl)
                {
                    if (USHORT AceCount = Dacl->AceCount)
                    {
                        union {
                            PVOID pv;
                            PBYTE pb;
                            PACE_HEADER pah;
                            PACCESS_ALLOWED_ACE paaa;
                        };

                        pv = Dacl + 1;

                        static const SID_IDENTIFIER_AUTHORITY NtAuth = SECURITY_NT_AUTHORITY;
                        do 
                        {
                            switch (pah->AceType)
                            {
                            case ACCESS_ALLOWED_ACE_TYPE:
                                PSID Sid = &paaa->SidStart;
                                if (*GetSidSubAuthorityCount(Sid) == SECURITY_LOGON_IDS_RID_COUNT &&
                                    *GetSidSubAuthority(Sid, 0) == SECURITY_LOGON_IDS_RID &&
                                    !memcmp(GetSidIdentifierAuthority(Sid), &NtAuth, sizeof(NtAuth)))
                                {
                                    paaa->Mask = GENERIC_ALL;
                                    *DefaultDacl = p;
                                    return 0;
                                }

                                break;
                            }
                            pb += pah->AceSize;

                        } while (--AceCount);
                    }
                }
            }

            LocalFree(buf);
        }
        else
        {
            return GetLastError();
        }

    } while (err == ERROR_INSUFFICIENT_BUFFER);

    return err;
}

ULONG GetDaclForLogonSession(PTOKEN_DEFAULT_DACL* DefaultDacl)
{
    HANDLE hToken;
    ULONG err = BOOL_TO_ERROR(OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &hToken));

    if (!err)
    {
        err = GetDaclForLogonSession(hToken, DefaultDacl);

        CloseHandle(hToken);
    }

    return err;
}

as result we got DACL we grant all access to current logon session. usage the same - simply replace call from GetNotElevatedDefaultDacl to GetDaclForLogonSession

RbMm
  • 31,280
  • 3
  • 35
  • 56
  • Thanks a lot, this looks very promising. I didn’t know about linked tokens. Will try later. – Soonts Sep 02 '18 at 18:31
  • 1
    @Soonts - linked token this is token from linked logon session. when admin user enter in system interactive with UAC active - system create 2 logon session and link it. - elevated and not elevated. not elevated process can get token for elevated session and process from elevated session can get token from not elevated. use not elevated session default dacl - this is most precise for your case - as result you create object with dacl - exactly if you create such object from not elevated process with default dacl – RbMm Sep 02 '18 at 18:38
  • 1
    Elevating a standard user requires an OTS consent, which will *not create a linked token*. Instead you could add an ACE for the logon-session SID, which can be queried via `GetTokenInformation` : `TokenGroups`; it has the flag `SE_GROUP_LOGON_ID`. You can also query this SID via `OpenDesktop` ("Default") and `GetUserObjectInformation` : `UOI_USER_SID`. If that's too broad, use the session user SID, queried either via `WTSQuerySessionInformation` : `WTSUserName` and `LookupAccountName` or via impersonating SYSTEM and then `WTSQueryUserToken` and `GetTokenInformation` : `TokenUser`. – Eryk Sun Sep 02 '18 at 22:42
  • @eryksun - yes, my solution only for main case when we have admin account. in case not admin user elevation was via another account - admin or built-in admin. no linked logon session exist (It already have been terminated. ). also in this case even elevated process have no write access to objects created by not elevated (because by default generic_all access only for system and user sid, but elevated have another user sid in this case). best way think query and modify default dacl for grant all access to logon sid instead generic_read_execute by default – RbMm Sep 02 '18 at 23:15