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 databasekerberos
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
.Buffer = (PCHAR)"switchpkg"; // authentication package identifier
krbAuth.Length = lstrlenA(krbAuth.Buffer);
krbAuth.MaximumLength = lstrlenA(krbAuth.Buffer);
krbAuth
// open a non secure access to LSA
= LsaConnectUntrusted(&hlsa);
status (status);
print_ntstatus
// get custom authentication pacakgge ID
= LsaLookupAuthenticationPackage(hlsa, &krbAuth, &ulAuthPackage);
status (status);
print_ntstatus
// random source identifier, doesn't really matter I guess ?
= {0};
TOKEN_SOURCE tks (&tks.SourceIdentifier);
AllocateLocallyUniqueId(tks.SourceName, "SPKG\x00", 5);
memcpy
;
LUID luid= NULL;
HANDLE tkn ;
QUOTA_LIMITS ql= 0;
NTSTATUS res ;
ULONG profileBufferSize;
PDWORD ProfileBuffer;
HANDLE htkn
// structure defined by the custom AP, for the POC I choosed a simple string
= L"switch";
LPCWSTR AuthenticationInformationData
// making a call to lsa which will route it to my custom package
= LsaLogonUser(hlsa, &krbAuth, Interactive, ulAuthPackage, (PVOID)AuthenticationInformationData, 64, 0, &tks, (PVOID*)&ProfileBuffer, &profileBufferSize, &luid, &tkn, &ql, &res);
status (status);
print_ntstatus
// if the token is NULL something went wrong
if (tkn == NULL) {
(0);
exit}
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 :
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) :
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) {
= L"switch";
LPCWSTR userName = L"";
LPCWSTR domain = L"thegame";
LPCWSTR password [500] = TEXT("C:\\windows\\system32\\cmd.exe");
TCHAR cmd= NULL;
LPVOID env
= NULL;
HANDLE hSystemToken
// Process information
;
SECURITY_ATTRIBUTES sa.nLength = sizeof(SECURITY_ATTRIBUTES);
sa.lpSecurityDescriptor = NULL;
sa.bInheritHandle = TRUE;
sa
// Set up the process information structure
;
PROCESS_INFORMATION pi;
STARTUPINFO si(&pi, sizeof(PROCESS_INFORMATION));
ZeroMemory(&si, sizeof(si));
ZeroMemory
// Call to authenticate the user localy and obtain the access token
if (!LogonUserW(userName, domain, password, LOGON32_LOGON_INTERACTIVE, LOGON32_PROVIDER_DEFAULT, &hSystemToken)) {
(L"LogonUserW failed : (0x%08x)", GetLastError());
wprintfreturn;
}
// Required for CreateProcessAsUserW
if (!CheckAndEnablePrivilege(NULL, SE_INCREASE_QUOTA_NAME)) {
(L"Privilege is missing: '%ws'\n", SE_INCREASE_QUOTA_NAME);
wprintfreturn;
}
// Required for CreateProcessAsUserW
if (!CheckAndEnablePrivilege(NULL, SE_ASSIGNPRIMARYTOKEN_NAME)) {
(L"Privilege is missing: '%ws'\n", SE_ASSIGNPRIMARYTOKEN_NAME);
wprintfreturn;
}
// To get user environnment variable, on est pas des bêtes
if (!CreateEnvironmentBlock(&env, hSystemToken, FALSE)) {
(L"CreateEnvironmentBlock() failed. (0x%08x)\n", GetLastError());
wprintf}
// Create process
if (!CreateProcessAsUserW(hSystemToken, NULL, cmd, &sa, &sa, TRUE, CREATE_UNICODE_ENVIRONMENT, env, NULL, &si, &pi)) {
("CreateProcess failed (0x%08x).\n", GetLastError());
printf}
else {
;
DWORD exit_code
// Wait until child process exits.
(pi.hProcess, INFINITE);
WaitForSingleObject(pi.hProcess, &exit_code);
GetExitCodeProcess("exit code : 0x%08x\n", exit_code);
printf// Close process and thread handles.
(pi.hProcess);
CloseHandle(pi.hThread);
CloseHandle}
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 :
.exe -i -s powershell.exe psexec64
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.
- https://stackoverflow.com/questions/18628985/createprocessasuser-not-working
- https://stackoverflow.com/questions/78004192/createprocessasuser-results-in-process-exit-and-error-0xc0000142-when-the-proces/
- https://stackoverflow.com/questions/76210203/createprocessasuser-returns-c0000142-in-one-scenario-but-works-in-another
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.
.lpDesktop = const_cast<wchar_t*>(L"WinSta0\\Default"); si
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 :
A 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) {
= NULL;
LPTSTR pszSid if (ConvertSidToStringSid(pSid, &pszSid)) {
std::wcout << L"SID: " << pszSid << L" ";
(pszSid);
LocalFree}
else {
std::cerr << "ConvertSidToStringSid Error: " << GetLastError() << std::endl;
return;
}
[256], domain[256];
TCHAR name= 256, domainLen = 256;
DWORD nameLen ;
SID_NAME_USE sidTypeif (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";
}
("\n");
printf}
void PrintAce(ACCESS_ALLOWED_ACE* pAce, int type) {
= NULL;
LPTSTR pszSid = &pAce->SidStart;
PSID pSid (pSid);
PrintSidAndNameif (ConvertSidToStringSid(pSid, &pszSid)) {
std::wcout << L" with access mask 0x" << std::hex << pAce->Mask << std::endl;
(pAce->Mask, type);
PrintAccessMask(pszSid);
LocalFree}
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 aclSizeInfoif (GetAclInformation(pAcl, &aclSizeInfo, sizeof(aclSizeInfo), AclSizeInformation)) {
for (DWORD i = 0; i < aclSizeInfo.AceCount; i++) {
;
LPVOID pAceif (GetAce(pAcl, i, &pAce)) {
((ACCESS_ALLOWED_ACE*)pAce, type);
PrintAce}
else {
std::cerr << "GetAce Error: " << GetLastError() << std::endl;
}
}
}
else {
std::cerr << "GetAclInformation Error: " << GetLastError() << std::endl;
}
}
void ListAclForDesktop(LPCWSTR lpDesktopName, int type) {
= OpenDesktopW(lpDesktopName, 0, FALSE, READ_CONTROL);
HDESK hDesktop if (hDesktop == NULL) {
std::cerr << "OpenDesktopW Error: " << GetLastError() << std::endl;
return;
}
= NULL;
PSECURITY_DESCRIPTOR pSD = NULL;
PACL pDacl
= GetSecurityInfo(
DWORD dwRes , // handle to the object
hDesktop, // type of the object
SE_WINDOW_OBJECT, // type of security information
DACL_SECURITY_INFORMATION, // owner SID
NULL, // primary group SID
NULL&pDacl, // DACL
, // SACL
NULL&pSD); // security descriptor
if (dwRes == ERROR_SUCCESS) {
(pDacl, type);
PrintAcl}
else {
std::cerr << "GetSecurityInfo Error: " << dwRes << std::endl;
}
if (pSD) {
(pSD);
LocalFree}
(hDesktop);
CloseDesktop}
void ListAclForWindowStation(LPCWSTR lpWinstaName, int type) {
= OpenWindowStationW(lpWinstaName, FALSE, READ_CONTROL | WRITE_DAC);
HWINSTA hWinsta if (hWinsta == NULL) {
std::cerr << "OpenWindowStationW Error: " << GetLastError() << std::endl;
return;
}
= NULL;
PSECURITY_DESCRIPTOR pSD = NULL;
PACL pDacl
= GetSecurityInfo(
DWORD dwRes , // handle to the object
hWinsta, // type of the object
SE_WINDOW_OBJECT, // type of security information
DACL_SECURITY_INFORMATION, // owner SID
NULL, // primary group SID
NULL&pDacl, // DACL
, // SACL
NULL&pSD); // security descriptor
if (dwRes == ERROR_SUCCESS) {
(pDacl, type);
PrintAcl}
else {
std::cerr << "GetSecurityInfo Error: " << dwRes << std::endl;
}
if (pSD) {
(pSD);
LocalFree}
(hWinsta);
CloseWindowStation}
void ListAClDesktopAndStation(LPCWSTR station, LPCWSTR desktop) {
(desktop, ALL_TYPE);
ListAclForDesktop(station, ALL_TYPE);
ListAclForWindowStation}
void displayAllACL() {
(L"Winsta0", L"Default");
ListAClDesktopAndStation}
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 :
.exe -t tcp:port=4446 $ dbgsrvX64
The global object is at 0x0007ff793d868f8
(thanks MSF
pdb) and is a pointer to 0x21b3aaf5632
. You can directly
cast the struct with :
(nt!_SID*) address dx
Using the PSID
structure we know that :
- the first byte is the revision :
0x01
- the amount of subAuthorities is :
0x3
- the authorities (DWORD) are :
0x0 0x0 0x0 0x0 0x0 0x5
(SID_IDENTIFIER_AUTHORITY , BYTE[6])0x5A000000
0x00000000
0x0100000
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 :
= CreateDesktopW(L"Winlogon", 0LL, 0LL, 0, 0x2000000u, 0LL);
DesktopW [...]
= CreateDesktopW(L"Default", 0LL, 0LL, 0, 0x2000000u, 0LL); v16
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 :
= SetWinlogonDesktopSecurity(*a3);
v14 if ( v14 )
goto LABEL_60;
= SetUserDesktopSecurity(*a4, 0LL); v14
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:
A 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 theLogonSid
variable that looks likeS-1-5-5-X-Y
. TheAttributes
value isSE_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 isSE_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.
/logonid whoami
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 :
(userName, domain, password, LOGON32_LOGON_INTERACTIVE, LOGON32_PROVIDER_DEFAULT, &token) LogonUserW
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 toLsaLogonUser
; - 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
= OpenDesktopW(lpDesktopName, 0, FALSE, READ_CONTROL | WRITE_DAC);
HDESK hDesktop if (hDesktop == NULL) {
std::cerr << "OpenDesktopW Error: " << GetLastError() << std::endl;
return;
}
= NULL;
PSECURITY_DESCRIPTOR pSD = NULL;
PACL pDacl
// Retrieve the current DACL
= GetSecurityInfo(
DWORD dwRes , // handle to the object
hDesktop, // type of the object
SE_WINDOW_OBJECT, // type of security information
DACL_SECURITY_INFORMATION, // owner SID
NULL, // primary group SID
NULL&pDacl, // DACL
, // SACL
NULL&pSD); // security descriptor
if (dwRes != ERROR_SUCCESS) {
std::cerr << "GetSecurityInfo Error: " << dwRes << std::endl;
if (pSD) {
(pSD);
LocalFree}
(hDesktop);
CloseDesktopreturn;
}
// Initialize EXPLICIT_ACCESS structure
;
EXPLICIT_ACCESS ea(&ea, sizeof(EXPLICIT_ACCESS));
ZeroMemory.grfAccessPermissions = GENERIC_ALL |
ea|
DESKTOP_READOBJECTS |
DESKTOP_CREATEWINDOW |
DESKTOP_CREATEMENU |
DESKTOP_HOOKCONTROL |
DESKTOP_JOURNALRECORD |
DESKTOP_JOURNALPLAYBACK |
DESKTOP_ENUMERATE |
DESKTOP_WRITEOBJECTS ;
DESKTOP_SWITCHDESKTOP.grfAccessMode = GRANT_ACCESS;
ea.grfInheritance = NO_INHERITANCE;
ea
// Create a SID for the Everyone group
= NULL;
PSID pEveryoneSID = SECURITY_WORLD_SID_AUTHORITY;
SID_IDENTIFIER_AUTHORITY SIDAuthWorld if (!AllocateAndInitializeSid(&SIDAuthWorld, 1, SECURITY_WORLD_RID, 0, 0, 0, 0, 0, 0, 0, &pEveryoneSID)) {
std::cerr << "AllocateAndInitializeSid Error: " << GetLastError() << std::endl;
if (pSD) {
(pSD);
LocalFree}
(hDesktop);
CloseDesktopreturn;
}
.Trustee.TrusteeForm = TRUSTEE_IS_SID;
ea.Trustee.TrusteeType = TRUSTEE_IS_WELL_KNOWN_GROUP;
ea.Trustee.ptstrName = (LPTSTR)pEveryoneSID;
ea
// Create a new DACL with the new ACE
= NULL;
PACL pNewDacl = SetEntriesInAcl(1, &ea, pDacl, &pNewDacl);
dwRes if (dwRes != ERROR_SUCCESS) {
std::cerr << "SetEntriesInAcl Error: " << dwRes << std::endl;
if (pEveryoneSID) {
(pEveryoneSID);
FreeSid}
if (pSD) {
(pSD);
LocalFree}
(hDesktop);
CloseDesktopreturn;
}
// Set the new DACL to the desktop object
= SetSecurityInfo(
dwRes , // handle to the object
hDesktop, // type of the object
SE_WINDOW_OBJECT, // type of security information
DACL_SECURITY_INFORMATION, // owner SID
NULL, // primary group SID
NULL, // DACL
pNewDacl); // SACL
NULL
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) {
(pEveryoneSID);
FreeSid}
if (pNewDacl) {
(pNewDacl);
LocalFree}
if (pSD) {
(pSD);
LocalFree}
(hDesktop);
CloseDesktop}
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.exe
for 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() {
= L"switch";
LPCWSTR userName = L"";
LPCWSTR domain = L"ehe";
LPCWSTR password [500] = TEXT("C:\\windows\\system32\\cmd.exe");
TCHAR cmd= NULL;
HANDLE token
// Set up the process information structure
;
PROCESS_INFORMATION pi(&pi, sizeof(PROCESS_INFORMATION));
ZeroMemory
// Set up the startup info structure
;
STARTUPINFO si(&si, sizeof(STARTUPINFO));
ZeroMemory
if (!CheckAndEnablePrivilege(NULL, SE_INCREASE_QUOTA_NAME)) {
(L"[*] A privilege is missing: '%ws'\n", SE_INCREASE_QUOTA_NAME);
wprintfreturn 1;
}
if (!CheckAndEnablePrivilege(NULL, SE_ASSIGNPRIMARYTOKEN_NAME)) {
(L"[*] A privilege is missing: '%ws'\n", SE_ASSIGNPRIMARYTOKEN_NAME);
wprintfreturn 1;
}
(L"[*] call LogonUserW\n");
wprintf
// - if (!LogonUserW(userName, domain, password, LOGON32_LOGON_NETWORK, LOGON32_PROVIDER_DEFAULT, &token)) {
+ if (!LogonUserW(userName, domain, password, LOGON32_LOGON_INTERACTIVE, LOGON32_PROVIDER_DEFAULT, &token)) {
(L"LogonUser failed with error: %x\n", GetLastError();
wprintfreturn 1;
}
(L"[*] call CreateProcessAsUserW with token %x\n", token);
wprintf
// - 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)) {
("CreateProcess failed (%d).\n", GetLastError());
printf}
else {
// Wait until child process exits.
(pi.hProcess, INFINITE);
WaitForSingleObject;
DWORD exit_code(pi.hProcess, &exit_code);
GetExitCodeProcess("[*] exit code : %08x\n", exit_code);
printf// Close process and thread handles.
(pi.hProcess);
CloseHandle(pi.hThread);
CloseHandle}
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
- https://learn.microsoft.com/en-us/archive/blogs/winsdk/what-is-up-with-the-application-failed-to-initialize-properly-0xc0000142-error
- https://learn.microsoft.com/en-us/archive/blogs/winsdk/how-to-launch-a-process-interactively-from-a-windows-service
- https://learn.microsoft.com/en-us/archive/blogs/ntdebugging/desktop-heap-overview
- https://github.com/reactos/reactos/blob/c02289a08add0b7096be8343e3aaeb675a62d133/dll/win32/advapi32/misc/logon.c#L1137
- https://support2421.rssing.com/chan-9619973/all_p1.html
- https://www.installsetupconfig.com/win32programming/windowstationsdesktops13index.html
- https://www.ossir.org/windows/supports/2007/2007-10-08/Secrets%20d%27authentification%20sous%20Windows.pdf
- https://www.bitvise.com/wug-logontype
- https://www.ampliasecurity.com/research/WCE_Internals_RootedCon2011_ampliasecurity.pdf
- Windows Internals ofc