Intro
I've been recently involved in the research of the logon and authentication process implemented in Windows, and would like to share some general information that may be useful to anyone starting to learn the Windows authentication concepts. Additionally, I've done some research on implementation of the logon via a physical device, such as a smart card, RFID token, or using an external web server, that I will also share further down this blog post.
Table Of Contents
Here's the table of contents for a quick access:
- Credential Provider vs. Authentication provider
- Authentication Via a Smart Card
- Kerberos
- Authentication With External Web Server
- Password-less Authentication
- Conclusion
Credential Provider vs. Authentication provider
There's some confusion over the purpose of each of the components involved in the Windows logon process, namely: credential providers and authentication providers.
I'm sure, you can read the links above, but in a nutshell, a credential provider is a
COM Interface that executes in the LogonUI.exe
process.
The main purpose of the credential provider is to collect credentials from the user (such as the user name, password, PIN, etc.) and pass it to the system.
A credential provider itself does not allow or deny access to the user. It is mainly just a UI and a conduit to pass the credentials.
I would say that the main method that any credential provider needs to implement is
ICredentialProviderCredential::GetSerialization
.
That is where the system requests from a credential provider the login information packaged in the
CREDENTIAL_PROVIDER_CREDENTIAL_SERIALIZATION
struct,
in which ulAuthenticationPackage
is the ID of the authentication provider that will process the data, and rgbSerialization
is a pointer to the serialized user
credentials themselves of cbSerialization
size. This method is called when a user presses "Submit" button after specifying their user name and password.
Windows 10 has an undocumented way of invoking a submit action for the user login, with the ICredentialProviderCredentialEvents4::RequestSerialization
method:
If credential provider successfully returns some serialized data, the system in turn calls
LsaLogonUser
function with it. This isn't done directly from LogonUI
, but
instead the request is passed to winlogon.exe
. It's very easy to see the similarity between the parameters:
ulAuthenticationPackage -> AuthenticationPackage
rgbSerialization -> AuthenticationInformation
cbSerialization -> AuthenticationInformationLength
LsaLogonUser
in turn makes an RPC call to lsass.exe
. That process contains all available authentication providers.
All authentication providers are registered in theHKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Lsa
key, underAuthentication Packages
.
But quite often for a workstation there's only one default provider called MSV1_0
.
And a computer joined into a domain also has additional providers with security packages, such as Kerberos
, which may also contain
custom subauthentication packages.
To complete authentication, the chosen authentication provider receives a call to its
LsaApLogonUserEx2
method,
which then requests permission for the specified user to acquire a logon to the system. One of the most important results that it needs to return, in case of success, is
a pointer to one of the LSA_TOKEN_INFORMATION_V*
structs
in the TokenInformation
parameter.
If we look into the LSA_TOKEN_INFORMATION_V3
struct,
you will see the following important logon parameters, such as:
TOKEN_USER User;
PTOKEN_GROUPS Groups;
TOKEN_PRIMARY_GROUP PrimaryGroup;
PTOKEN_PRIVILEGES Privileges;
That is, a User SID, different user groups that the user is a member of, a set of privileges. All of these are required to make a new user token.
Thus the system calls ZwCreateToken[Ex]
function with the parameters supplied by the credential provider:
NTSTATUS ZwCreateToken
(
PHANDLE hToken,
ACCESS_MASK DesiredAccess,
POBJECT_ATTRIBUTES ObjectAttributes,
TOKEN_TYPE Type,
PLUID AutenticId,
PLARGE_INTEGER ExpirationTime,
PTOKEN_USER User,
PTOKEN_GROUPS Groups,
PTOKEN_PRIVILEGES Provileges,
PTOKEN_OWNER Owner,
PTOKEN_PRIMARY_GROUP PrimaryGroup,
PTOKEN_DEFAULT_DACL DefaultDacl,
PTOKEN_SOURCE Source
);
That call creates and returns a user token back to winlogon
(via LsaLogonUser
call.) And then winlogon
starts new process(-es), that are registered under the HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon
key, under Userinit
. In most cases, it is C:\Windows\system32\userinit.exe
. Such process(-es) are started with the new user token using the
CreateProcessAsUserW
call.
userinit.exe
in turn runs the logon scripts, sets up a network connection, applies the Group Policies,
and loads the Windows user shell.
This is the process of a local user logon in Windows, in a nutshell.
A Recap
So to recap, the job of a credential provider is to collect the login information from the user (or from a physical security device.) Such information may be user name and password, but may also include a TOTP security PIN, and some other biometric details.
The job of an authentication provider is to validate credentials passed by a credential provider, and to return information (LSA_TOKEN_INFORMATION_V1
,
LSA_TOKEN_INFORMATION_V2
or LSA_TOKEN_INFORMATION_V3
) needed for creation of a user token for a successful login.
The format of the information that a credential provider passes to the system (via its
GetSerialization
method) must adhere to the format understandable by an authentication provider. In other words, those two must be able to communicate with each other.
For example: MSV1_0
authentication provider only uses
MSV1_0_INTERACTIVE_LOGON
format,
which always requires a user name and a password!
Microsoft's defaultMSV1_0
authentication provider uses the SAM database (stored in the\REGISTRY\MACHINE\SAM\SAM\Domains\Account\Users
key) to check if requested user name and password match. (The database stores only encrypted password hashes.) If authentication passes, it returns the results in theLSA_TOKEN_INFORMATION_V*
struct.
Kerberos
, on the other hand, also accepts KERB_SMART_CARD_LOGON
and KERB_CERTIFICATE_LOGON
formats.
And that's where the authentication via a physical device, such as a smart card, or an external web server comes into play.
Authentication Via a Smart Card
A smart card is technically a digital certificate in a physical form that holds containers, that have the certificate and a private key, that corresponds to a public key from the certificate.
A smart card implements a primitive internal file system, that holds several crypto-containers, with data pertaining to the digital certificate. For instance, such containers have the certificate info and a private key.It is important to note that a private key never leaves the smart card. And a public key is stored in the certificate and is publicly accessible.
When a smart card is inserted, the smart card credential provider checks if it has a certificate, and for the certificate selected by the user, it offers to enter a PIN.
The smart card replies whether the PIN matched or not. If its a match, it passes the name of the smart card and the digital certificate (or container),
along with the PIN into the
GetSerialization
method.
Technically, instead of the password a smart card uses its private key that is stored inside and is never exposed in any request.
After a request a smart card can sign data with its private key, or create an encrypted hash. In other words, it realizes the
BCryptSignHash
function, that can encrypt and decrypt data.
The details of the sign-in process flow with smart cards in Windows contains multiple steps. So just to clarify some of them:
- 11. A smart card needs to establish the relation between the
certificate
and provided user account. This is usually done using the "Subject alternative name" in the certificate. (Ex:
user@domain
.) - 12. It needs to verify trust of the certificate in the system. It must be digitally signed by another certificate, whose
Issuer
is present in the "NTAUTH certificate store", and is trusted by the system (which usually means that it is present in the "Trusted ROOT store".) - 12. Additionally, there's also a check that a smart card contains a public key corresponding to the user's public key. This can be done as follows: a random hash is generated and is sent to the smart card, which digitally signs it. Then the signature is verified using the public key. Or, some random hash is first encrypted and then sent into smart card for decryption. If the smart card can decrypt it and return it, the hash is compared for a match.
Kerberos
The way Kerberos
performs its authentication is as follows:
- It checks if the digital certificate that it receives is registered in the system.
- If yes, it then reads the public key from that certificate.
- Then calls
BCryptImportKeyPair
withBCRYPT_RSAPUBLIC_BLOB
. - It then generates random bytes with a call to
BCryptGenRandom
and hashed withBCryptHashData
. - The resulting hash is passed into the smart card with a request to digitally sign it.
(The smart card technically emulates a call to the
BCryptSignHash
function.)The specification also requires a smart card to request a PIN, and if such is not done, or if the PIN is not valid, the smart card must refuse to digitally sign the hash.
In case of a virtual smart card, it may emulate the physical smart card actions in the following way: The private key can be stored encrypted in the
BCRYPT_RSAPRIVATE_BLOB
format. When the virtual card receives the PIN, it reads its hash and creates a symmetric key from it. It then decrypted the private key in memory fromBCRYPT_RSAPRIVATE_BLOB
and callsBCryptImportKeyPair
. And if the PIN doesn't match, the private key cannot be decrypted, and a call toBCryptImportKeyPair
returns an error. Otherwise, the card returns a digitally signed hash back. - It then calls
BCryptVerifySignature
on the digitally signed hash returned by the smart card, that verifies the signature.
Additionally, there's another method that is used by the certutil
tool:
- It encrypts a hash to some random data with a public key:
pseudo-code: certutil[Copy]
cert -> BCRYPT_RSAPUBLIC_BLOB BCryptImportKeyPair(BCRYPT_RSAPUBLIC_BLOB) -> key BCryptGenRandom -> data BCryptHashData(data) -> hash BCryptEncrypt(key, hash) -> xxx
- I then sends such encrypted data to the smart card (again, along with the PIN) with a request to decrypt it and return the result.
- When it receives the result, it decrypts it and compares to the original:
pseudo-code:[Copy]
decrypt (BCRYPT_RSAPRIVATE_BLOB) with PIN BCryptImportKeyPair(BCRYPT_RSAPRIVATE_BLOB) -> key BCryptDecrypt(key, xxx) -> yyy
Lastly, a software developer can write their own cryptographic algorithm for the authentication provider, or add some extra security to the system, such as an outside web server that can perform additional authentication and storage of sensitive logon information.
Authentication With External Web Server
This method could be used for an implementation of the classic 2FA credential provider via an external web server:
It works like an external wrapper over the built-in system authentication via a password, PIN, WLID, smart card, etc.
- On the first step, our credential provider lets the default credential provider to collect login data from the user. It can be a user name and password,
but it can also be a PIN from another provider, and even some biometric data. All that data is eventually transferred in the
MSV1_0_INTERACTIVE_LOGON
format for theMSV1_0
to be able to understand it. - Then we ask the user to enter a one-time-password (or OTP). After that we send it to our
authentication web server for verification.
One classic approach here is to use the time-based-one-time-password algorithm to generate the OTP in our credential provider and on the web server.
Additionally we can use a random nonce for authentication. But then the question is, where do you store that nonce? We can store it on the server, which is understandable. But what about the local system? There, it has to be encrypted. For that we have to use a PIN provided by the user before they enter the OTP, so that we can use it to decrypt our random nonce.
- If the server replies with OK, we pass on the
MSV1_0_INTERACTIVE_LOGON
struct that we received from the default credential provider, which allows user login. - But if the server replies that the OTP is not correct, we do not continue with the serialization and return an error from the call to
GetSerialization
. This effectively prevents user login.
Password-less Authentication
One has to remember that a credential provider must pass a password to a (default) authentication provider. But where do you get that password from, if the authentication process was not intended to have one? Say, if some external device, such as a smartphone was used to verify a user's identity.
One option is to store such password on the web server. Or, to store it locally encrypted by the PIN. The built-in PIN-provider in Windows 10 uses this scheme: it first receives a PIN from the user and then decrypts the password with it, that it uses later with the authentication provider.
Or, here's the password-less authentication that I developed with the use of RFID cards and a web server:
- The device contains a unique ID and the credential provider sends it to the web server.
- The web server checks if such ID is registered in its database.
- Then the user sends in the OTP passcode, generated by the smart card.
- The web server checks the OTP and if it's correct, replies back with the password for the authentication provider. Otherwise returns an error.
In many ways, a password-less authentication doesn't mean that the password is not used. As I showed above, the password is either returned by a web server, or by some outside device, or is stored on the local system and is decrypted after receiving a PIN or some other uniquely identifying information from the user. In other words, even though the authentication password is not directly exposed to the user it may still be used internally.
To create a true password-less authentication in Windows one needs to create their own authentication provider besides a credential provider. The latter one is needed to provide user interface for the custom authentication provider. That way, it can be written to implement any of its own authentication schemes.
Conclusion
This was just the basic information on writing a custom logon & authentication in Windows. It seems like this subject started to gain traction now, so if anyone is interested to learn more internals about this concept I can expand this blog post upon further requests from the readers.