from 0xc000142 to understanding windows login setup

For few weeks, I got the idea to develop a Windows security support provider / authentication package for zero fun and no profit except some burdensome knowledge.

I thought It would help me to understand Windows authentication and its extensions like access tokens, delegation or impersonation. It actually did it but drove me way beyond my initial point, which was related to the SECPKG_SUPPLEMENTAL_CRED_ARRAY in the context of a PKINIT authentication abuse with U2U, leading me to accept all side quests offered not by PNG but by Microsoft documentation.

My initial research will be the subject of another, way longer, article, but I wanted to write down some information related to this side quest as I struggled a bit on some Windows errors not so really documented.

Writing this post made me read some parts of Windows Internals. The few lines I read make me realize that a significant part of this post is summarized in within the book. But, it’s always fine to observe what someone told you about or to find out that you found the same things.

a bit of context

To spoil a bit my initial research, I reached the point having a functional authentication package which is queried through the LsaLogonUser function and will return an access token for the authenticated user. The authentication flow will be the subject of the other article, not this one. For the comprehension of the reader side, there are generally two authentication packages :

  • msv1_0 for local authentication through the SAM database
  • kerberos for network authentication through NTDS

In the code bellow, a third authentication package, a custom one, is used. We will accept that my custom AP is called like this and return a valid access token :

HANDLE CallSPKG() {
    NTSTATUS status;
    HANDLE hlsa;
    ULONG ulAuthPackage;
    
    LSA_STRING krbAuth;

    krbAuth.Buffer = (PCHAR)"switchpkg"; // authentication package identifier
    krbAuth.Length = lstrlenA(krbAuth.Buffer);
    krbAuth.MaximumLength = lstrlenA(krbAuth.Buffer);

    // open a non secure access to LSA
    status = LsaConnectUntrusted(&hlsa);
    print_ntstatus(status);

    // get custom authentication pacakgge ID
    status = LsaLookupAuthenticationPackage(hlsa, &krbAuth, &ulAuthPackage);
    print_ntstatus(status);

    // random source identifier, doesn't really matter I guess ?
    TOKEN_SOURCE tks = {0};
    AllocateLocallyUniqueId(&tks.SourceIdentifier);
    memcpy(tks.SourceName, "SPKG\x00", 5);

    LUID luid;
    HANDLE tkn = NULL;
    QUOTA_LIMITS ql;
    NTSTATUS res = 0;
    ULONG profileBufferSize;
    PDWORD ProfileBuffer;
    HANDLE htkn;

    // structure defined by the custom AP, for the POC I choosed a simple string
    LPCWSTR AuthenticationInformationData = L"switch";

    // making a call to lsa which will route it to my custom package
    status = LsaLogonUser(hlsa, &krbAuth, Interactive, ulAuthPackage, (PVOID)AuthenticationInformationData, 64, 0, &tks, (PVOID*)&ProfileBuffer, &profileBufferSize, &luid, &tkn, &ql, &res);
    print_ntstatus(status);

    // if the token is NULL something went wrong
    if (tkn == NULL) {
        exit(0);
    }
    
    return tkn;
}

(this is dev code, it’s just intended to work, not to be secure or either acceptable)

At this point, if the custom authentication package authenticates the user through the supplied information (here just a LPCWSTR), it will return an access token. This access token is built with information supplied by the AP in addition to default option set by LSA (I do reference to LSA as the whole system and thus the DLL in charge of this part).

To validate my data added to the access token within my custom AP I wanted to execute a process in the context of this new token. Windows API offers 2 functions to do this :

CreateProcessAsUser requires SE_INCREASE_QUOTA_NAME and SE_ASSIGNPRIMARYTOKEN_NAME privilege whereas CreateProcessWithTokenW only requires SE_IMPERSONATE_NAME. They are used depending on the context.

These 2 functions are core primitive in some privilege escalation exploitation poc as generally Windows LPE aims to retrieve a SYSTEM token of its own and then use it through these two function to obtain a SYSTEM shell. Potatoes exploit or PrintSpoofer use it for example :

Printspoofer example

After getting the token, PrintSpoofer exploit code try to call CreateProcessAsUser then CreateProcessWithToken if the first fail.

Often, the token is fetched by calling OpenThreadToken on the current thread, after a pipe impersonation for instance) :

Printspoofer token fetch

However, there are legit ways to obtain the token like calling LogonUser. The internals will be described in my other article but under the hood it calls LSA which will selects the appropriate authentication package, authenticates the user and return an access token.

If a call to LogonUser has to be made but with the possibility to select the authentication package, you can use LsaLogonUser to obtain the required access token.

the original sin

Even if the original issue was discovered by calling my AP, I will use LogonUser for the poc. The mention of the LsaLogonUser is only to provide my context when I encountered the issue.

CheckAndEnablePrivilege and other functions are heavily copied from PrintSpoofer code, thanks again.

Code below imitate my issue, the purpose is to log a user, obtain their access token and execute an interactive process in its context - not more, not less.

void main(int argc, char **argv) {
    LPCWSTR userName = L"switch";
    LPCWSTR domain = L"";
    LPCWSTR password = L"thegame";
    TCHAR cmd[500] = TEXT("C:\\windows\\system32\\cmd.exe");
    LPVOID env = NULL;

    HANDLE hSystemToken = NULL;

    // Process information
    SECURITY_ATTRIBUTES sa;
    sa.nLength = sizeof(SECURITY_ATTRIBUTES);
    sa.lpSecurityDescriptor = NULL;
    sa.bInheritHandle = TRUE;

    // Set up the process information structure
    PROCESS_INFORMATION pi;
    STARTUPINFO si;
    ZeroMemory(&pi, sizeof(PROCESS_INFORMATION));
    ZeroMemory(&si, sizeof(si));

    // Call to authenticate the user localy and obtain the access token
    if (!LogonUserW(userName, domain, password, LOGON32_LOGON_INTERACTIVE, LOGON32_PROVIDER_DEFAULT, &hSystemToken)) {
        wprintf(L"LogonUserW failed : (0x%08x)", GetLastError());
        return;
    }

    // Required for CreateProcessAsUserW
    if (!CheckAndEnablePrivilege(NULL, SE_INCREASE_QUOTA_NAME)) {
        wprintf(L"Privilege is missing: '%ws'\n", SE_INCREASE_QUOTA_NAME);
        return;
    }
    
    // Required for CreateProcessAsUserW
    if (!CheckAndEnablePrivilege(NULL, SE_ASSIGNPRIMARYTOKEN_NAME)) {
        wprintf(L"Privilege is missing: '%ws'\n", SE_ASSIGNPRIMARYTOKEN_NAME);
        return;
    }

    // To get user environnment variable, on est pas des bêtes
    if (!CreateEnvironmentBlock(&env, hSystemToken, FALSE)) {
        wprintf(L"CreateEnvironmentBlock() failed. (0x%08x)\n", GetLastError());
    }

    // Create process
    if (!CreateProcessAsUserW(hSystemToken, NULL, cmd, &sa, &sa, TRUE,  CREATE_UNICODE_ENVIRONMENT, env, NULL, &si, &pi)) {
        printf("CreateProcess failed (0x%08x).\n", GetLastError());
    }
    else {
        DWORD exit_code;

        // Wait until child process exits.
        WaitForSingleObject(pi.hProcess, INFINITE);
        GetExitCodeProcess(pi.hProcess, &exit_code);
        printf("exit code : 0x%08x\n", exit_code);
        // Close process and thread handles.
        CloseHandle(pi.hProcess);
        CloseHandle(pi.hThread);
    }

    return;
}

Per the privileges requirements, the easiest way is to gather the privilege by running it as system. An easy way to get a SYSTEM shell is to use psexec :

psexec64.exe -i -s powershell.exe

Running this code throw no error until the process exit. There is no error at all, even for CreateProcessAsUserW.

However, the behavior is weird. whoami /all returns 0xc000142 and doesn’t return any information. Some actions within the new process seem working as other no.

The call to CreateProcessWithTokenW with the same parameters results equally.

culprit identification

If I was still a teenager, I would have simply uninstalled the cracked game and download it from another torrent until it works. But I grew up, and now I have to pay taxes and debug this kind of issue.

Using https://cable.ayra.ch/winerr/ to search for the error code 0xc0000142 I got STATUS_DLL_INIT_FAILED which seems to indicate the failure of a DLL loading. Some stackoverflow posts may indicate that it could be related to Window Station and Desktop. I don’t know what it is so It’s not that.

But, some deep researches later (reader must understand just more googling), I stated that it was related to desktop object. Some guys recommended specifying the Windows Desktop when creating the STARTUPINFO structure.

PrintSpoofer and CoercedPotato does it too so why not.

si.lpDesktop = const_cast<wchar_t*>(L"WinSta0\\Default");

But no, it would be too simple. But, it’s definitely related to Windows desktop. So, what about Windows Desktop ? Following this Microsoft Blog post:

This can happen from the user’s desktop (the user who logged on by entering their credentials) or from a Windows Service.  Programmatically, you launch applications using the family of CreateProcess APIs (CreateProcessAsUser, CreateProcessWithLogonW, CreateProcessWithTokenW. Another thing you have to look out for in the call is whether, a desktop was specified in the STARTUPINFO struture.

Definitely our case, but the desktop is now defined. And I believed that, even if it’s not defined, it will use Winsta0/Default (found it later).

If you debugged the launched process and reviewed why Windows wasn’t loading the DLL, it makes perfect sense that there was no error with CreateProcess (or CreateProcessAsUser) and the error was seen in the exit code of the launched process.

That also our case, CreateProcessAsUserW doesn’t fail at all. It’s the process that runs after which fails, else we would see the log : CreateProcess failed. The error happens when the process tries to load User32.dll.

There are 2 issues you have to consider with Window Stations & Desktops:

  • Desktop Heap Exhaustion - There is a limited amount of desktop heap (memory).
  • Desktop Heap Security - When moving between users or desktops from the calling process, you have to ensure that the launched process has appropriate permissions for the targeted process.

I think I am far way from exhausting my Desktop Heap, the system can handle multiple desktop and I got the same error across both virtual or physical machines, even after reboot. The second part is the right one, the blog author link an other blog post “How to launch a process interactively from a Windows Service?”.

From here you learn about the concept of session, station and desktop.

session, station and desktop

From Sessions, Desktops and Windows Stations :

A session consists of all of the processes and other system objects that represent a single user’s logon session.  These objects include all windows, desktops and windows stations.  A desktop is a session-specific paged pool area and loads in the kernel memory space.  This area is where session-private GUI objects are allocated from.  A windows station is basically a security boundary to contain desktops and processes.  So, a session may contain more than one Windows Station and each windows station can have multiple desktops.

And from Window Stations :

window station contains a clipboard, an atom table, and one or more desktop objects. Each window station object is a securable object. When a window station is created, it is associated with the calling process and assigned to the current session. The interactive window station is the only window station that can display a user interface or receive user input. It is assigned to the logon session of the interactive user, and contains the keyboard, mouse, and display device. It is always named “WinSta0”. All other window stations are noninteractive, which means they cannot display a user interface or receive user input.

More about Window Stations and Desktops here.

At login time, the system automatically creates the first Station object called Winsta0. It’s the winlogon.exe process, which is called by, logonUi.exe that will create it and assigns its security descriptor.

Decompilation of CreatePrimaryTerminal right after process main function, from winlogon.exe :

Right after the creation and the assignation of the station to the process in order to be allowed to interact with as stated in the documentation, the ACL are attributed by SetDefaultWinstaSecurity() :

The windows station specifics ACLs are defined here : Window Station Security and Access Rights. A short c++ program can list the ACLs for every Sid listed :

#include <windows.h>
#include <aclapi.h>
#include <sddl.h>
#include <iostream>

#define DESKTOP_TYPE 1
#define STATION_TYPE 2
#define ALL_TYPE 3

void PrintSidAndName(PSID pSid) {
    LPTSTR pszSid = NULL;
    if (ConvertSidToStringSid(pSid, &pszSid)) {
        std::wcout << L"SID: " << pszSid << L" ";
        LocalFree(pszSid);
    }
    else {
        std::cerr << "ConvertSidToStringSid Error: " << GetLastError() << std::endl;
        return;
    }

    TCHAR name[256], domain[256];
    DWORD nameLen = 256, domainLen = 256;
    SID_NAME_USE sidType;
    if (LookupAccountSid(NULL, pSid, name, &nameLen, domain, &domainLen, &sidType)) {
        std::wcout << L"Account: " << domain << L"\\" << name;
    }
    else {
        std::cerr << "LookupAccountSid Error: " << GetLastError() << std::endl;
    }
}

void PrintAccessMask(DWORD mask, int type) {
    if (mask & GENERIC_ALL) std::wcout << L"\tGENERIC_ALL\n";
    if (mask & GENERIC_READ) std::wcout << L"\tGENERIC_READ\n";
    if (mask & GENERIC_WRITE) std::wcout << L"\tGENERIC_WRITE\n";
    if (mask & GENERIC_EXECUTE) std::wcout << L"\tGENERIC_EXECUTE\n";
    if (mask & DELETE) std::wcout << L"\tDELETE\n";
    if (mask & READ_CONTROL) std::wcout << L"\tREAD_CONTROL\n";
    if (mask & WRITE_DAC) std::wcout << L"\tWRITE_DAC\n";
    if (mask & WRITE_OWNER) std::wcout << L"\tWRITE_OWNER\n";
    if (mask & SYNCHRONIZE) std::wcout << L"\tSYNCHRONIZE\n";
    
    if (type == DESKTOP_TYPE || type == ALL_TYPE) {
        if (mask & DESKTOP_READOBJECTS) std::wcout << L"\tDESKTOP_READOBJECTS\n";
        if (mask & DESKTOP_CREATEWINDOW) std::wcout << L"\tDESKTOP_CREATEWINDOW\n";
        if (mask & DESKTOP_CREATEMENU) std::wcout << L"\tDESKTOP_CREATEMENU\n";
        if (mask & DESKTOP_HOOKCONTROL) std::wcout << L"\tDESKTOP_HOOKCONTROL\n";
        if (mask & DESKTOP_JOURNALRECORD) std::wcout << L"\tDESKTOP_JOURNALRECORD\n";
        if (mask & DESKTOP_JOURNALPLAYBACK) std::wcout << L"\tDESKTOP_JOURNALPLAYBACK\n";
        if (mask & DESKTOP_ENUMERATE) std::wcout << L"\tDESKTOP_ENUMERATE\n";
        if (mask & DESKTOP_WRITEOBJECTS) std::wcout << L"\tDESKTOP_WRITEOBJECTS\n";
        if (mask & DESKTOP_SWITCHDESKTOP) std::wcout << L"\tDESKTOP_SWITCHDESKTOP\n";
    }
    if (type == STATION_TYPE || type == ALL_TYPE) {
        if (mask & WINSTA_ACCESSCLIPBOARD) std::wcout << L"\tWINSTA_ACCESSCLIPBOARD\n";
        if (mask & WINSTA_ACCESSGLOBALATOMS) std::wcout << L"\tWINSTA_ACCESSGLOBALATOMS\n";
        if (mask & WINSTA_CREATEDESKTOP) std::wcout << L"\tWINSTA_CREATEDESKTOP\n";
        if (mask & WINSTA_ENUMDESKTOPS) std::wcout << L"\tWINSTA_ENUMDESKTOPS\n";
        if (mask & WINSTA_ENUMERATE) std::wcout << L"\tWINSTA_ENUMERATE\n";
        if (mask & WINSTA_EXITWINDOWS) std::wcout << L"\tWINSTA_EXITWINDOWS\n";
        if (mask & WINSTA_READATTRIBUTES) std::wcout << L"\tWINSTA_READATTRIBUTES\n";
        if (mask & WINSTA_READSCREEN) std::wcout << L"\tWINSTA_READSCREEN\n";
        if (mask & WINSTA_WRITEATTRIBUTES) std::wcout << L"\tWINSTA_WRITEATTRIBUTES\n";
    }
    printf("\n");
}


void PrintAce(ACCESS_ALLOWED_ACE* pAce, int type) {

    LPTSTR pszSid = NULL;
    PSID pSid = &pAce->SidStart;
    PrintSidAndName(pSid);
    if (ConvertSidToStringSid(pSid, &pszSid)) {
        std::wcout << L" with access mask 0x" << std::hex << pAce->Mask << std::endl;
        PrintAccessMask(pAce->Mask, type);
        LocalFree(pszSid);
    }
    else {
        std::cerr << "ConvertSidToStringSid Error: " << GetLastError() << std::endl;
    }
}

void PrintAcl(PACL pAcl, int type) {
    if (pAcl == NULL) {
        std::cout << "No ACL found." << std::endl;
        return;
    }

    ACL_SIZE_INFORMATION aclSizeInfo;
    if (GetAclInformation(pAcl, &aclSizeInfo, sizeof(aclSizeInfo), AclSizeInformation)) {
        for (DWORD i = 0; i < aclSizeInfo.AceCount; i++) {
            LPVOID pAce;
            if (GetAce(pAcl, i, &pAce)) {
                PrintAce((ACCESS_ALLOWED_ACE*)pAce, type);
            }
            else {
                std::cerr << "GetAce Error: " << GetLastError() << std::endl;
            }
        }
    }
    else {
        std::cerr << "GetAclInformation Error: " << GetLastError() << std::endl;
    }
}

void ListAclForDesktop(LPCWSTR lpDesktopName, int type) {
    HDESK hDesktop = OpenDesktopW(lpDesktopName, 0, FALSE, READ_CONTROL);
    if (hDesktop == NULL) {
        std::cerr << "OpenDesktopW Error: " << GetLastError() << std::endl;
        return;
    }

    PSECURITY_DESCRIPTOR pSD = NULL;
    PACL pDacl = NULL;

    DWORD dwRes = GetSecurityInfo(
        hDesktop,                    // handle to the object
        SE_WINDOW_OBJECT,            // type of the object
        DACL_SECURITY_INFORMATION,   // type of security information
        NULL,                        // owner SID
        NULL,                        // primary group SID
        &pDacl,                      // DACL
        NULL,                        // SACL
        &pSD);                       // security descriptor

    if (dwRes == ERROR_SUCCESS) {
        PrintAcl(pDacl, type);
    }
    else {
        std::cerr << "GetSecurityInfo Error: " << dwRes << std::endl;
    }

    if (pSD) {
        LocalFree(pSD);
    }

    CloseDesktop(hDesktop);
}

void ListAclForWindowStation(LPCWSTR lpWinstaName, int type) {
    HWINSTA hWinsta = OpenWindowStationW(lpWinstaName, FALSE, READ_CONTROL | WRITE_DAC);
    if (hWinsta == NULL) {
        std::cerr << "OpenWindowStationW Error: " << GetLastError() << std::endl;
        return;
    }

    PSECURITY_DESCRIPTOR pSD = NULL;
    PACL pDacl = NULL;

    DWORD dwRes = GetSecurityInfo(
        hWinsta,                     // handle to the object
        SE_WINDOW_OBJECT,            // type of the object
        DACL_SECURITY_INFORMATION,   // type of security information
        NULL,                        // owner SID
        NULL,                        // primary group SID
        &pDacl,                      // DACL
        NULL,                        // SACL
        &pSD);                       // security descriptor

    if (dwRes == ERROR_SUCCESS) {
        PrintAcl(pDacl, type);
    }
    else {
        std::cerr << "GetSecurityInfo Error: " << dwRes << std::endl;
    }

    if (pSD) {
        LocalFree(pSD);
    }

    CloseWindowStation(hWinsta);
}

void ListAClDesktopAndStation(LPCWSTR station, LPCWSTR desktop) {
    ListAclForDesktop(desktop, ALL_TYPE);
    ListAclForWindowStation(station, ALL_TYPE);
}

void displayAllACL() {
    ListAClDesktopAndStation(L"Winsta0", L"Default");
}

From here, we can observe many behaviors in the default station and desktop ACLs :

  • The user XCHG\switch itself has not much permission ;
  • There is the NT AUTHORITY\LogonSessionId_0_220932 with way more permissions ;
  • Window Manager\DWM-1 has a lot of permissions too;

For the moment, it matches our ACLs set in winlogon.exe. For example, g_pSidWindowManager has 0xf037f bitmask.

This variable represents the SID of Windows Manager\DWM-1, we can verify it with WinDbg by attaching winlogon.exe. I solely recommend using WinDbg from your host to debug the process of a guest VM. You can run the remote debugger server with :

$ dbgsrvX64.exe -t tcp:port=4446

The global object is at 0x0007ff793d868f8 (thanks MSF pdb) and is a pointer to 0x21b3aaf5632. You can directly cast the struct with :

dx (nt!_SID*) address

Using the PSID structure we know that :

  • the first byte is the revision : 0x01
  • the amount of subAuthorities is : 0x3
  • the authorities (DWORD) are :

Making the SID S-1-5-90-1. Which is the Well-known SIDS for the Windows Manager Group where the last authorities is the session ID, which is 1 here.

From the two previous MSF blog posts, we learned that session 0 is system reserved since Windows Vista, then our session will always be 1 or greater. Other sessions could be created a user logon through RDP for example, with its own stations and desktops objects.

Few accounts have access to this station object :

  • S-1-15-2-1 : APPLICATION PACKAGE AUTHORITY\ALL APPLICATION PACKAGES
  • S-1-15-2-2 : APPLICATION PACKAGE AUTHORITY\ALL RESTRICTED APPLICATION PACKAGES
  • S-1-5-12 : NT AUTHORITY\RESTRICTED
  • S-1-5-18 : NT AUTHORITY\SYSTEM
  • S-1-5-32-544 : BUILTIN\Administrators
  • S-1-5-90-0-1 : Window Manager\DWM-1
  • S-1-5-96-0-1 : Font Driver Host\UMFD-1
  • S-1-5-5-0-220932 : NT AUTHORITY\LogonSessionId_0_220932
  • S-1-5-21-359176844-2696690345-3146353438-1001 : computer\user

Then, right after the ACL attribution, Winlogon.exe creates two Windows desktop objects with :

DesktopW = CreateDesktopW(L"Winlogon", 0LL, 0LL, 0, 0x2000000u, 0LL);
[...]
v16 = CreateDesktopW(L"Default", 0LL, 0LL, 0, 0x2000000u, 0LL);

Why two ? Actually it creates 3. One for before the user is logged-in called Winlogon, the one after login Default and for screen saver. From the documentation the desktop is automatically assigned to the windows station of the current process, Winsta0 for us.

Following their own ACLs attribution too :

v14 = SetWinlogonDesktopSecurity(*a3);
if ( v14 )
goto LABEL_60;
v14 = SetUserDesktopSecurity(*a4, 0LL);

The decompilation of SetUserDesktopSecurity is quite weird but enough to see which ACLs are positioned.

The important thing to notice is that no ACLs are created for the current user, and it was the same case for the station object. Only, default groups are mentioned and obtain ACLs.

ACLs for the Winsta0 station are now defined, time to investigate about the desktops. What are actually Desktops objects ? From Microsoft documentation about Desktops:

desktop has a logical display surface and contains user interface objects such as windows, menus, and hooks; it can be used to create and manage windows. Each desktop object is a securable object. When a desktop is created, it is associated with the current window station of the calling process and assigned to the calling thread. Like the station object we can list Default desktop ACLs.

Unlike Station object, the current user has no ACL at all for the desktop object, everything is managed through the NT AUTHORITY\LogonSessionId_0_220932 account. As aforementioned there was no ACL for this account in the default ACL. Why ? Just because the group is created when the user login and so can’t exist before.

a user enter the bar

After creating these objects and setting the ACL, along with a few other things, winlogon.exe initiate a state machine that will run few functions. To be honest, I didn’t identify how the functions were called and by who, but the logic guess is from logonUi.exe and its COM objects.

I tried to reverse logonUi.exe in order to identify which DLL it loads and who is making the call, but I didn’t spend much time on it. If you have some insight feel free to DM. The process is rather complex are a lot of COM interfaces are involved.

From here, nothing happens until someone logs in. Let’s forget how the LogonUI.exe calls winlogon, the fact is the function WLGeneric_Authenticating_Execute() is called when a user log in. The function is registered as a callback for the state machine.

One of the first interesting call in this function is AuthenticateUser() but in order to execute it, few important things are set up just before.

First it will create a SID which is called LogonSid according to the PDB. This will be the Logon Session Id, an important term to keep in mind. The function CreateLogonSid() is responsible for this.

It uses a LUID structure and its HighPart for the second SubAuthority and LowPart for the third. The first SubAuthority will always be 5.

The SID_IDENTIFIER_AUTHORITY is 5 and is weirdly assigned. It’s a 6 bytes array. The first 4 bytes are set to 0 with a and dword [rsp+offset], 0; mov word [rsp+offset+4], 500h. In the end the SID will look like S-1-5-5-X-Y. X is often (always ?) 0.

Then a call to local function AuthenticateUser is made, passing, among others, the newly created LogonId. I didn’t try to understand every action made by this function, but for our context we can focus on few things.

The main goal of this function is to achieve a call to LsaLogonUser in order to authenticate the user and get an access token representing it. The call according to it prototype :

NTSTATUS LsaLogonUser(
  [in]           HANDLE              LsaHandle, // Obtain from a previous call to LsaRegisterLogonProcess or LsaConnectUntrusted
  [in]           PLSA_STRING         OriginName, // Caller identifier, here "Winlogon"
  [in]           SECURITY_LOGON_TYPE LogonType, // depends on the context, see https://learn.microsoft.com/en-us/windows/win32/api/ntsecapi/ne-ntsecapi-security_logon_type 
  [in]           ULONG               AuthenticationPackage, // the one choosed
  [in]           PVOID               AuthenticationInformation, // depends on the authentication package, structure holding the creds to be honest.
  [in]           ULONG               AuthenticationInformationLength, // obvious 
  [in, optional] PTOKEN_GROUPS       LocalGroups, // The groups that will be inside the access token
  [in]           PTOKEN_SOURCE       SourceContext, // doesn't really matter I guess, here is User32 
  [out]          PVOID               *ProfileBuffer, // Struct to pass data back from the AP pov
  [out]          PULONG              ProfileBufferLength, // obvious too
  [out]          PLUID               LogonId, // The identifier of the session itself, which is not the logon session id
  [out]          PHANDLE             Token, // our access token 
  [out]          PQUOTA_LIMITS       Quotas, // mmh, see docs
  [out]          PNTSTATUS           SubStatus // see docs too
);

To prepare the call, it first selects the right authentication package, if the call came from LogonUi.exe it should be either msv1_0, kerberos or negotiate in order to choose the right SSP.

Then, it will create the TOKEN_GROUPS struct that will hold the groups SID associated to the returned access token. This is a way to assign groups in other words.

Only 2 groups are set up :

  • the first one SID psid is the LogonSid variable that looks like S-1-5-5-X-Y. The Attributes value is SE_GROUP_ENABLED | SE_GROUP_ENABLED_BY_DEFAULT | SE_GROUP_MANDATORY | SE_GROUP_LOGON_ID
  • the second one is the SID S-1-2-0 (LOCAL). The attribute value is SE_GROUP_ENABLED | SE_GROUP_ENABLED_BY_DEFAULT | SE_GROUP_MANDATORY = 0x7 (don’t mind the decompilation)

I will come back later on these ones, don’t forget them. Then the preparation of the LSA call may continue. SourceContext is set to User32, OriginName to Winlogon then the actual call to LsaLogonUser. We will not dive deeper, this is not need and the purpose of this blog post, It is more related to the incoming blog post.

I highly recommend James Forshaw project called TokenViewer which is a very nice and handy GUI letting you see token information.

The logon session is 1 which can be confirmed with LogonSession.exe. This is an Interactive logon, using the NTLM (msv1_0), and is assigned the 00000000-0006ee78 logon session id. The source User32 is fitting the one found in Winlogon.exe.

Looking at the groups will expose some interesting things as there are our 2 previous groups saw in IDA LOCAL and LogonSession_X_Y plus other groups. To spoil the future blog post, these other groups are added by LSA. Thus, making both the caller and called adding groups to the tokens.

We can even use the following whoami.exe parameters to display the logon session id for your current session. A session may have multiple logon session but logon session are tightened to one session.

whoami /logonid

Perfect, we know that this is the LogonSessionId_x_y group which has ACL on the Station and Desktop object.

We also know when and how this group is added to our users access token. But what about how the ACL are positioned for this SID ? We only saw the default ACL for some SID not for LogonSessionId_0_454117! Let’s grab IDA back.

The function in charge of this part is CSession:SetUserDesktopSecurity which calls SetUserDesktopSecurity.

You may have noticed that is the function initially called in order to set up ACL for the list of a given SID like g_pSidWindowManager. The function CSession:SetUserDesktopSecurity is called in WLGeneric_NotifyLogon_Execute function. As for WLGeneric_Authenticating_Execute, I don’t know how and when WLGeneric_NotifyLogon_Execute is called, and it could be the subject of another post, who knows.

But this time, the function is called with a 2nd parameter which is not harcoded to0 unlike the first call. And if this parameter is not 0, another security descriptor is attributed to the Desktop object with 0xF01FF mask.

a2, the second parameter different to 0, is a pointer on a SID. Without debugging my guess is that is the Logon Session SID (NT AUTHORITY\LogonSessionId_X_Y). The ACLs match the one saw in the Desktop object for this SID. I’m fairly enough convinced but without debugging and spending more time in IDA it will just be a strong hypothesis.

Later the same thing happen also for the Winsta0 station object with a call to AddUserToWinsta()

Looking form other unrelated information made me realize that everything is resumed in few lines in Windows internals from a higher point of view :

Finally, Winlogon creates a unique logon SID for each interactive logon session. A typical use of a logon SID is in an access control entry (ACE) that allows access for the duration of a client’s logon session. For example, a Windows service can use the LogonUser function to start a new logon session. The LogonUser function returns an access token from which the service can extract the logon SID. The service can then use the SID in an ACE (described in the section “Security descriptors and access control” later in this chapter) that allows the client’s logon session to access the interactive window station and desktop. The SID for a logon session is S-1-5-5-X-Y, where the X and Y are randomly generated.

Now, we should have all information to understand who have the right ACL on the desktop / station and how it is set.

My initial need for this research was to understand why I got a weird 0xc000142 issue, we found out that it was related to desktop and station security. Then we observed how it was handled and found It was related to some groups inside the access token.

Let’s compare two tokens, one from a normal process, one obtained after the call to LogonUserW and one obtained though my custom authentication package.

The first one is the one of a classic cmd.exe process, the one studied previously.

The second one is obtained by calling LogonUserW like this :

LogonUserW(userName, domain, password, LOGON32_LOGON_INTERACTIVE, LOGON32_PROVIDER_DEFAULT, &token)

The Source is Advapi as its LogonUserW origin DLL. Just below you can observe a call to this function until the final RPC call that will be handled by LSA and not discussed here.

[0x0]   RPCRT4!NdrClientCall3          0xc1dd3eb48   0x7ffdb42347c7    // final RPC call
[0x1]   SspiCli!SspipLogonUser+0x397   0xc1dd3eb50   0x7ffdb42343c4   
[0x2]   SspiCli!L32pLogonUser+0x264    0xc1dd3ed30   0x7ffdb4234011   
[0x3]   SspiCli!LogonUserExExW+0x201   0xc1dd3ee50   0x7ffdb6bdb9db   
[0x4]   ADVAPI32!LogonUserCommonW+0x73 0xc1dd3ef40   0x7ffdb6bdb959   
[0x5]   ADVAPI32!LogonUserW+0x39       0xc1dd3efc0   0x7ff7f0c421d2   
[0x6]   win_research!callLogon+0x132   0xc1dd3f030   0x7ff7f0c44170   
[0x7]   win_research!main+0x20         0xc1dd3f8d0   0x7ff7f0c44d59   

The third and last tokens is obtained by calling my custom AP though LsaLogonUser where, among other things, I specified SPKG as source.

But the main differences aren’t there. They are in the listed groups :

The left one is the control subject used to highlight differences from the two others.

There are one group less in the call to LogonUserW which is the LOCAL one. The group belonging is described as “users who log on to terminals locally (physically) connect to the system”, fair enough.

The two missing groups in my custom AP call are LOCAL too and NTLM Authentication which totally make sense as I used my custom AP and not the msv1_0.

An advised eye will have noticed that both 3 tokens have LogonSessionId_x_y and never the same value which is normal as it’s unrelated logon session and these values are generated randomly from a LUID.

For the first one, Winlogon creates the LogonSessionId SID but for LogonUserW and my AP call it’s not done by this process as we don’t go through Winlogon.

I’m not totally sure about LogonUserW but from my research I identified that lsasrv.dll function LsapBuildDefaultTokenGroups is in charge building this SID :

Every created tokens has a Logon ID SID but only tokens created from Winlogon have the right ACL on WinSta0\Default. If you check a gain the ACL you will see only referenced ACLs for LogonSession_0_454117 not for 0_6792317 or 0_6916531 :

This explaining why a process using these token would fail with 0xc000142. I will not cover the part linked to why user32.dll needs this or this ACL to successfully starts, we will assume that one of those is needed. However, some details are presented here Destop Heap Overview.

That’s all folks, we know have a better understanding of the requirements and when and by who they are meet in a ordinary login flow. The next step is to find a way to meet them in our non-ordinary scenario.

meddling with requirements

So here we are, with at least 3 possibilities offered :

  • we find the right group and add it to our TOKEN_GROUPS before the call to LsaLogonUser ;
  • we modify the ACLs on the desktop and station object to allow us ;
  • we do not use interactive logon in LogonUserW.

finding the right group

Finding the Loggon session id created by Winlogon.exe is a bad idea as it is not intended to log on a user interactively like this as it would require a previous Winlogon.exe authentication. That’s make no sense. However, for the sake of the lawful poc you could imagine doing it by browsing user access tokens until finding this group. This would not be covered here.

modifying the ACL

This is also a bad idea messing up with ACL on sensitive objects like these two. But the following code would solve any issue, but don’t run it as it allow S-1-1-0 to do everything.

void AddFullAccessForEveryone(LPCWSTR lpDesktopName) {
    // Open the desktop object with required permissions
    HDESK hDesktop = OpenDesktopW(lpDesktopName, 0, FALSE, READ_CONTROL | WRITE_DAC);
    if (hDesktop == NULL) {
        std::cerr << "OpenDesktopW Error: " << GetLastError() << std::endl;
        return;
    }

    PSECURITY_DESCRIPTOR pSD = NULL;
    PACL pDacl = NULL;

    // Retrieve the current DACL
    DWORD dwRes = GetSecurityInfo(
        hDesktop,                    // handle to the object
        SE_WINDOW_OBJECT,            // type of the object
        DACL_SECURITY_INFORMATION,   // type of security information
        NULL,                        // owner SID
        NULL,                        // primary group SID
        &pDacl,                      // DACL
        NULL,                        // SACL
        &pSD);                       // security descriptor

    if (dwRes != ERROR_SUCCESS) {
        std::cerr << "GetSecurityInfo Error: " << dwRes << std::endl;
        if (pSD) {
            LocalFree(pSD);
        }
        CloseDesktop(hDesktop);
        return;
    }

    // Initialize EXPLICIT_ACCESS structure
    EXPLICIT_ACCESS ea;
    ZeroMemory(&ea, sizeof(EXPLICIT_ACCESS));
    ea.grfAccessPermissions = GENERIC_ALL |
        DESKTOP_READOBJECTS |
        DESKTOP_CREATEWINDOW |
        DESKTOP_CREATEMENU |
        DESKTOP_HOOKCONTROL |
        DESKTOP_JOURNALRECORD |
        DESKTOP_JOURNALPLAYBACK |
        DESKTOP_ENUMERATE |
        DESKTOP_WRITEOBJECTS |
        DESKTOP_SWITCHDESKTOP;
    ea.grfAccessMode = GRANT_ACCESS;
    ea.grfInheritance = NO_INHERITANCE;

    // Create a SID for the Everyone group
    PSID pEveryoneSID = NULL;
    SID_IDENTIFIER_AUTHORITY SIDAuthWorld = SECURITY_WORLD_SID_AUTHORITY;
    if (!AllocateAndInitializeSid(&SIDAuthWorld, 1, SECURITY_WORLD_RID, 0, 0, 0, 0, 0, 0, 0, &pEveryoneSID)) {
        std::cerr << "AllocateAndInitializeSid Error: " << GetLastError() << std::endl;
        if (pSD) {
            LocalFree(pSD);
        }
        CloseDesktop(hDesktop);
        return;
    }

    ea.Trustee.TrusteeForm = TRUSTEE_IS_SID;
    ea.Trustee.TrusteeType = TRUSTEE_IS_WELL_KNOWN_GROUP;
    ea.Trustee.ptstrName = (LPTSTR)pEveryoneSID;

    // Create a new DACL with the new ACE
    PACL pNewDacl = NULL;
    dwRes = SetEntriesInAcl(1, &ea, pDacl, &pNewDacl);
    if (dwRes != ERROR_SUCCESS) {
        std::cerr << "SetEntriesInAcl Error: " << dwRes << std::endl;
        if (pEveryoneSID) {
            FreeSid(pEveryoneSID);
        }
        if (pSD) {
            LocalFree(pSD);
        }
        CloseDesktop(hDesktop);
        return;
    }

    // Set the new DACL to the desktop object
    dwRes = SetSecurityInfo(
        hDesktop,                    // handle to the object
        SE_WINDOW_OBJECT,            // type of the object
        DACL_SECURITY_INFORMATION,   // type of security information
        NULL,                        // owner SID
        NULL,                        // primary group SID
        pNewDacl,                    // DACL
        NULL);                       // SACL

    if (dwRes != ERROR_SUCCESS) {
        std::cerr << "SetSecurityInfo Error: " << dwRes << std::endl;
    }
    else {
        std::cout << "Successfully added full access for Everyone." << std::endl;
    }

    // Clean up
    if (pEveryoneSID) {
        FreeSid(pEveryoneSID);
    }
    if (pNewDacl) {
        LocalFree(pNewDacl);
    }
    if (pSD) {
        LocalFree(pSD);
    }

    CloseDesktop(hDesktop);
}

through network logon

This fix is the only one that should be implemented. Methods described above are only hacky ways to operate unintended behavior.

When calling LogonUserW you can specify a dwLogonType. The logon type specified in the initial not working poc was Interactive. This is not the intended way to log a non-interactive user in an authentication package context.

The expected way is to pass LOGON32_LOGON_NETWORK to LogonUserW for a network logon which is not interactive. Interactive means user provided credentials interactively though Winlogon.exefor example. If we do this the obtained token will be an impersonation token instead of a primary. But it’s fine for our case.

The tokens groups will reflect the network logon and even a LogonSessionId is created, and this one has still no ACL for the Station and Desktop object, but they are not needed this time.

We can even see the logon with Logonsession64.exe for the sanity check.

And everything work smoothly. Under the hood few tricks are used as the CreateProcessAsUserW documentation states that the tokens used must have TOKEN_DUPLICATE and the token must either be primary or to use DuplicateTokenEx to get one. However, I used the impersonation token without calling DuplicateToken and the process tokens is a primary one. Certainly because it is transformed later explaining the need for TOKEN_DUPLICATE.

Here is the full working code :

#include "utils.h"

int callNetworkLogon() {
    LPCWSTR userName = L"switch";
    LPCWSTR domain = L"";
    LPCWSTR password = L"ehe";
    TCHAR cmd[500] = TEXT("C:\\windows\\system32\\cmd.exe");
    HANDLE token = NULL;

    // Set up the process information structure
    PROCESS_INFORMATION pi;
    ZeroMemory(&pi, sizeof(PROCESS_INFORMATION));

    // Set up the startup info structure
    STARTUPINFO si;
    ZeroMemory(&si, sizeof(STARTUPINFO));

    if (!CheckAndEnablePrivilege(NULL, SE_INCREASE_QUOTA_NAME)) {
        wprintf(L"[*] A privilege is missing: '%ws'\n", SE_INCREASE_QUOTA_NAME);
        return 1;
    }

    if (!CheckAndEnablePrivilege(NULL, SE_ASSIGNPRIMARYTOKEN_NAME)) {
        wprintf(L"[*] A privilege is missing: '%ws'\n", SE_ASSIGNPRIMARYTOKEN_NAME);
        return 1;
    }

    wprintf(L"[*] call LogonUserW\n");

//  - if (!LogonUserW(userName, domain, password, LOGON32_LOGON_NETWORK, LOGON32_PROVIDER_DEFAULT, &token)) {
    + if (!LogonUserW(userName, domain, password, LOGON32_LOGON_INTERACTIVE, LOGON32_PROVIDER_DEFAULT, &token)) {
        wprintf(L"LogonUser failed with error: %x\n", GetLastError();
        return 1;
    }
    wprintf(L"[*] call CreateProcessAsUserW with token %x\n", token);

//  - if (!CreateProcessAsUserW(hSystemToken, NULL, cmd, &sa, &sa, TRUE,  CREATE_UNICODE_ENVIRONMENT, NULL, NULL, &si, &pi)) {
    + if (!CreateProcessAsUserW(token, NULL, cmd, NULL, NULL, FALSE, CREATE_UNICODE_ENVIRONMENT, NULL, NULL, &si, &pi)) {
        printf("CreateProcess failed (%d).\n", GetLastError());
    }
    else {
        // Wait until child process exits.
        WaitForSingleObject(pi.hProcess, INFINITE);
        DWORD exit_code;
        GetExitCodeProcess(pi.hProcess, &exit_code);
        printf("[*] exit code : %08x\n", exit_code);
        // Close process and thread handles.
        CloseHandle(pi.hProcess);
        CloseHandle(pi.hThread);
    }

    return 0;
}

As we didn’t dive into how ACL on desktop / station are checked at process creation I can’t explain why a network logon bypass these ACL checks.

conclusion

The fix may seem a bit simple, but it makes sense as my initial goal was absurd. An authentication package has no needs to execute a process in an interactive way for a user. An authentication package goal is to authenticate the user only. Once the user is authenticated, the caller may or may not want to execute interactive process for a user. If needed, It will be in charge of making it possible by creating the right ACL for its user.

Finally, nothing new is presented here. A lot of concepts are documented by Microsoft documentation and Windows Internals. It was just the opportunity to dive in and verifies !

This endless side quest over, I will go back to the development of my authentication package which will lead to a blog post describing the interaction between a process that authenticate a user, going through some RPC calls and finishing by the creation of an access token.

Of course many thanks to people who hinted me and nofix for his Windows insight, rpecli and proofreading.

edit

James Forshaw had a small blog post about it

references