SSPI introduction
Pavlo Myroniuk April 27, 2023 #sspi #windows #winapi #rustGoals
The main purpose of this article is to tell you about Windows SSPI. If you know nothing about it and want to start working with it, this article will benefit you.
If you are familiar with SSPI and have worked with it, then this article most likely will not be useful.
Goals:
- Provide a comprehensive SSPI description.
- Introduce and explain basic SSPI terms.
- Understand core SSPI functions, invocation order, and the purpose of each of them.
Non-goals:
- Explain how concrete SSP packages work (like
NTLM
,Kerberos
, etc). - Teach how to write a custom SSP package. I mean, after reading this article you will have a brief idea of what you need to write but it's obviously not enough.
Does this article replace reading the documentation? Of course not. It's just a good place to start.
Happy reading! 😃
What is SSPI
First of all, let's start with the official SSPI definition:
SSPI is a software interface. Distributed programming libraries such as RPC can use it for authenticated communications. One or more software modules provide the actual authentication capabilities...
So, now we know that it's an interface. It can be used for authentication. The general idea is to provide a unified interface for the authentication and if any program will need to authenticate some user, then it "just" can use SSPI. Moreover, the authentication process (function call order, etc) doesn't depend on the underlying authentication protocol. But buffer sizes and types can be different. We'll see how we can handle such cases.
Basic concepts and definitions
SSP - Security Support Provider - a dynamic library (.dll
file) that has one or more SSPI implementations. Security package - one SSPI implementation. Usually, it reflects some authorization protocol.
Good, now let's think a little bit. What do we mean under "successful authentication"? Usually, it's something like "username + password is correct, we can do our work further" or "we've got some token that proves our identity/permissions".
In SSPI, the process of authenticating is called context establishing. The result of authenticating is the (established) security context with the session key. The session key can be used for further encryption/decryption (you'll see it later), checksum calculation/verification, and so on. It's valid only for this session. After another context establishing the security context will have another session key. The Microsoft documentation gives us a good session context definition:
A security context is the set of security attributes and rules in effect during a communication session.
A communication session. After successful authentication, the client and the server can continue to send messages to each other. They can use the session key to secure this communication. It means you can still need SSPI even after the authentication process.
Okay, sounds pretty simple and reasonable. Time to see the actual SSPI interface. All mandatory functions are listed in one structure: the SSPI function table. Here it is:
typedef struct _SECURITY_FUNCTION_TABLE_W SecurityFunctionTableW, *PSecurityFunctionTableW;
Along with table W, also a similar table A exists. What's the difference? This is not a part of this story but here is the answer on SO. Today we'll work only with the W table. But remember that all things that apply to the W table, are also applicable to the A table.
SSPI
The SSPI functions table has a lot of (?terrible?) functions. We'll talk about them, and how and when you should use them.
Overview
To bring at least some order, we will split SSPI functions into four categories. The official documentation has a good description of it so we follow it:
- Package management. SSPI package management functions initiate a security package, enumerate available packages, and query the attributes of a security package.
EnumerateSecurityPackagesW
: list all available security packages in the SSP.InitSecurityInterfaceW
: initialize security table.QuerySecurityPackageInfoW
: query some information about the security package.
- Credential management. Functions used to obtain credentials handle for the security context, set credentials attributes, and query information about credentials.
AcquireCredentialsHandleW
ExportSecurityContext
FreeCredentialsHandle
ImportSecurityContextW
QueryCredentialsAttributesW
- Context management. Functions used to establish security context, query and set its attributes, etc.
AcceptSecurityContext
ApplyControlToken
CompleteAuthToken
DeleteSecurityContext
FreeContextBuffer
ImpersonateSecurityContext
InitializeSecurityContextW
QuerySecurityContextToken
QueryContextAttributesW
SetContextAttributesW
RevertSecurityContext
- Message support. Those functions usually are called when the security context is established. You can use them to secure messages (encryption, hashing, etc).
EncryptMessage
.DecryptMessage
.MakeSignature
.MakeSignature
.
Enough talking. Let's see SSPI in action. From the example below you will understand functions call order, and their purpose.
SSPI in action
Two things are needed to demonstrate SSPI in action:
- Client.
- Server.
Both of them should use the same security package to successfully authenticate. As the client, I took the NTLM security package from the Microsoft-provided SSP. To keep my code example clearer, as the server, I took this sspi implementation in Rust. All my code is written in Rust. To call Windows API functions I use crates with bindings like winapi, windows-sys. You should focus only on the client code and not on the server code.
Why NTLM? Because it's simple. It's a good protocol to use as an example (but bad for the real world [1] [2]. If you can avoid it then avoid it 🐺).
Here you can find the source code.
Initialization
Good so far. Let the journey begin! What do we need to start using SSPI? Correct! SSPI function table. How can we initialize it? By calling the InitSecurityInterfaceW
function. This function returns a structure (see it above) with function pointers. If we want to use Windows SSP then we can just use Win API. But if we decide to use some custom SSP, then we need to load the corresponding dll, find the InitSecurityInterfaceW
function and call it. Example:
// load our SSP
let sspi_handle = LoadLibraryW;
// get the pointer to the needed function
let init_security_interface_fn = GetProcAddress;
// transmute (cast) the pointer to the needed function type
let init_security_interface_w_fn: INIT_SECURITY_INTERFACE_W = unsafe ;
let sspi_function_table = init_security_interface_w_fn;
// now we have initialized function table
// ...further authentication
But in the scope of this article, I use Windows native SSP. So I don't need to initialize the function table. It already is initialized inside Windows. Time to see what Windows can offer us:
let mut number_of_packages = 0;
let mut packages = null_mut;
// Query information about the available Windows security packages
// https://learn.microsoft.com/en-us/windows/win32/api/sspi/nf-sspi-enumeratesecuritypackagesw
let result = EnumerateSecurityPackagesW;
When I printed it on the screen (src), I got such the output:
Number of packages: 13
-------------------------
fCapabilities: 8928179
wVersion: 1
wRPCID: 9
cbMaxToken: 48256
Name: Negotiate
Comment: Microsoft Package Negotiator
-------------------------
...
We can see a lot of security packages. But as I said above, today we are working only with NTLM. Let's gather more information about the NTLM security package (src):
let mut raw_package_name = str_to_win_wstring;
let mut package_info = null_mut;
let status = QuerySecurityPackageInfoW;
println!;
println!;
println!;
println!;
println!;
println!;
println!;
Output:
NTLM package info:
fCapabilities: 42478391
wVersion: 1
wRPCID: 10
cbMaxToken: 2888
Name: NTLM
Comment: NTLM Security Package
In general, different security packages support different attributes. So refer to the documentation for more concrete information.
Credentials
Before starting the actual authentication we need to prepare credentials. In other words, to acquire credentials handle. Basically, the credentials handle is a pointer to some structure that contains prepared credentials for use during the authentication, maybe some flags, and other credentials-related information. What type of this structure? We don't know and we don't need to know. The security provider creates and works with this object. It'll be good if you also read the official credentials handle definition. Enough talking. Now time back to the code (src).
let mut credentials_handle = default;
let mut expiry = default;
let mut package_name = str_to_win_wstring;
let mut domain = str_to_win_wstring;
let mut user = str_to_win_wstring;
let mut password = str_to_win_wstring;
let mut identity = SEC_WINNT_AUTH_IDENTITY_W ;
let status = AcquireCredentialsHandleW;
After the AcquireCredentialsHandleW
function call, the credentials_handle
variable will contain the credentials handle. Note. In SSPI all handles (context, credentials, etc) are instances of the SecHandle
structure. Each such structure has two numbers (pointers): lower and upper. When you are working with SSPI you don't need to know anything more about them. If you are writing your own SSP then you shouldn't expose any information about them.
However, to satisfy your curiosity, I will say what there may be (but don't tell anyone 😜): One of them can contain a pointer to some object (for example, a security context object), and another one can contain a pointer to the security package name. Because SSP can contain a lot of security packages, we need to store the security package name in some way that is currently used.
This function has a lot of parameters and they are well explained in the official documentation. Just don't forget that the type for the pauthdata
pointer is package-specific. It means that different security packages usually require different types of pauthdata
.
Also, the SSPI has a place for customization. Security packages implement the QueryCredentialsAttributesW
and SetCredentialsAttributesW
functions. With them, we can add and get additional information (attributes) about concrete credentials handle. Small example (src):
let mut credentials_name = default;
let status = QueryCredentialsAttributesW;
println!;
Output:
Credentials name: "testdomain\\testuser"
Authentication
Perfect. We have prepared credentials (credentials handle). Time to start actual authentication. The authentication process consists of sequential InitializeSecurityContext
function calls.
When we should stop?
When this function returns SEC_E_OK
, SEC_I_COMPLETE_AND_CONTINUE
, or SEC_I_COMPLETE_NEEDED
status code. i. e. we call the InitializeSecurityContext
function until it returns the appropriate status code. The generalized authentication flow is shown in the diagram below:
Great, pretty simple. Now let's implement it in the code (src):
let client_status = InitializeSecurityContextW;
Oh my gosh. So many parameters. We definitely need an explanation.
Agreed. But before I explain each of them, I would like to insert small note about the source code you will find in the repo: I didn't create any real servers or TCP/TLS connections. I just convert and pass buffers from one function to another. You will see it in the code comments.
Hardest part described below:
phcredential
: a pointer to the credentials handle. We have it after theAcquireCredentialsHandleW
function call.phcontext
: a pointer to the security context handle.
But wait. We don't have a such one.
Yes. On the first function call we pass the NULL
here. The security context handle will be created by the function itself during the first invocation and written in the phnewcontext
parameter.
psztargetname
: "a pointer to a null-terminated string that indicates the service principal name (SPN)" (docs).fcontextreq
: a context requirements flags. Basically, this flags tell the security package how to do authentication. Like, what session key to use, what additional things to require, etc. Those flags differ in the different security packages.reserved1
,reserved2
are reserved and not used.targetdatarep
: "The data representation, such as byte ordering, on the target." (docs)pinput
: input buffers for this function. On the first function call, we don't have any input buffers so we should pass emptySecBufferDesc
.
What types of security buffers should I choose, how many of them do I need, and in what sequence?
Docs. Search for it in the documentation of the corresponding security package. If you can't find it there, then search in the open-source projects. And remember one important thing: never change buffers' order or type.
phnewcontext
: a new security context handle will be written here. You should use this handle for the next function call.poutput
: same aspinput
but output buffers instead of input.pfcontextattr
: flags that describe established security context. We usefcontextreq
to specify some options for authentication. Ans usepfcontextattr
to see what options have been established (set).ptsexpiry
: the expiration time of the context.
The security context also has functions for setting and getting different attributes: QueryContextAttributesW
and SetContextAttributesW
. They have similar behavior to the credentials-related functions. For example, in the NTLM security package, we have the possibility to extract the established session key (src):
let mut session_key = default;
let status = QueryContextAttributesW;
println!;
Output:
Established session key: [96, 1, 13, 58, 82, 191, 222, 134, 149, 184, 3, 75, 254, 126, 225, 74]
You can run the code a few times. Each time you will have another session key.
Phew, the hardest part is gone. Now we (finally) have the established security context. What's next? Now we can do everything we want, but more importantly, we can safely transfer any messages to the server and receive server messages securely.
Communication
Imagine the situation: we want to send a very secret message to the server. In such case we should use the EncryptMessage
function:
let status = EncryptMessage;
// send `message` to the server
I think you already guessed it, but still: encryption/decryption, and signature generation/verification are always in-place. This is why we don't have the output buffer parameter in this function.
If we want to read encrypted messages (e. g. received from the server) then:
let status = DecryptMessage;
The same thing with the MakeSignature
and VerifySignature
functions.
Clean up
Yes, memory leaks are memory safe, but it's better to not forget to clean up everything. SSPI has three functions for that:
FreeContextBuffer
. (doc). Only one rule: if the buffer was allocated by the security package, then you should free it using this function. If the buffer was allocated by yourself, then you should free it in the usual way.FreeCredentialsHandle
. (doc).DeleteSecurityContext
. (doc).
// ...
let status = FreeCredentialsHandle;
let status = DeleteSecurityContext;
Conclusion
Finally, this story goes to the end. If you want to use SSPI on the server side then just replace the InitializeSecurityContextW
function with AcceptSecurityContext
.
Wait! You haven't described other SSPI functions!
Don't panic, I know it. The other functions are rarely used and do not take a direct part in the authorization process.
I hope you enjoy reading it and it'll help you in some way. If you want to share some feedback, ask a question, just task to me, then use this page to contact me.