Smart card container name
Pavlo Myroniuk April 22, 2024 #debugging #windows #scardBefore I start
Goals
- Explain how the Windows minidriver treats the smart card container name.
- Tell how a bad container name can break scard auth.
- Fun ðĨģ.
Non-goals
- Explain every piece of smart card architecture in Windows.
- Explain every smart card minidriver function.
Getting Started
In my previous article, I explained the different smart card cache items (files), their format, and purpose. The current one is some continuation of talking about PIV smart cards in Windows. The setup is the same: Windows smart card minidriver + PIV smartcard. But in this case, we'll use a fully emulated smart card ðĨ.
Note. The current article is full of technical things related to debugging (WinDbg) and reversing (IDA). Depending on your purpose, choose one of the following paths:
- If you want to reproduce each step with me, then perform all actions from the Debugging environment section and continue reading the article.
- If you don't want to set up the whole environment but want to reproduce each step on your machine, then download my TTD recoding and log files. But I still recommend reading the Debugging environment section (at least to know how I came up with it).
- If you only want to read my findings and smart card container name structure, then jump to the What? section.
else
: read it fully.
Happy reading ð. Enjoy ð.
Debugging environment
My goal is to simulate the data signing using an emulated smart card. To achieve this I need two key things:
- Some program signs the data using the smart card.
- Hook somehow the
winscard.dll
to use an emulated smart card.
A brief explanation of our debugging environment:
- Test VM + certificate with private key suitable for the data signing.
- We'll have a small program written in C#. The only thing this program does is sign the hardcoded data using the smart card.
- We'll have a
bad.dll
that hooks the originalwinscard.dll
with our one. - And the last but not least thing is a "launcher": another program that creates a signing process, then loads the
bad.dll
into it to hook thewinscard.dll
, and then continues the execution.
Pheww, it starts looking like some kind of Frankenstein ðĪŠ
Yes. You are right ð.
Compile the data signer
The code is very simple. It uses some high level C# API for the data signing. The full code can be found here. Just clone the project and build it with Visual Studio. Note: it required net6.0.
The overall algorithm is pretty simple: we create a CSP that matches our smart card and then sign the data using the RSA crypto provider. The simplified code looks like this:
// Create needed CSP.
CspParameters csp = ;
csp.KeyPassword = pwd;
// Sign the data.
RSACryptoServiceProvider rsaCsp = ;
byte signature = rsaCsp.;
As a result, you should get SignDataTmp.exe
+ SignDataTmp.dll
.
Compile your own version of winscard.dll
Of course, during my first debugging, I had a dev version of the sspi-rs
. But now all developed functionality has been merged, so we need to pick a specific commit. After that you will have sspi.dll
in the target/debug
directory. You can rename it to winscard.dll
if you want (but it's not required).
Compile bad.dll
This bad.dll
represents how I do the hooking. There are many hooking techniques. My choice criteria were that it should work and not be hard to implement. So, I chose the MinHook library. It is easy to use and does its job well. Project source code can be found here. Just clone the project and build it with Visual Studio. Note: do not forget to properly link it with MinHook
.
But here is a surprise. When you try to read the code you'll find out that I hook some weird functions instead of the winscard.dll
. The reason for that is delayed loading. We can not simply hook the winscard.dll
because the basecsp.dll
loads it via delayed loading ð.
But it is still possible to hook. Thanks to @awakecoding I know how to do it. He had researched and implemented it in the MsRdpEx: fix winscard.dll delay-loading interception from basecsp.dll. I reused some code and make it work for my debugging program ð .
So, as a result, you should get bad.dll
. Do not forget to set your own paths in the code!
Compile the launcher
The "launcher" is named as "HookLibrary" due to my own historical reasons ðĪŠ. The full source code can be found here. Important: do not forget to change hardcoded paths to your ones. It works as follows:
- Runs the data signer executable in a separate process, but does not run it.
- Injects the
bad.dll
into the process memory forcing it to load this dll. In turn, thebad.dll
will hook all needed methods. - Continues the execution.
So, as a result, you should get HookLoadLibrary.exe
.
Run it
Before trying to run this machinery we need to configure the emulated smart card. To do this set up the following environment variables:
Name | Value | Example |
---|---|---|
SSPI_LOG_PATH | Path to the log file | D:\test_data\sspi.log |
SSPI_LOG_LEVEL | Log level | trace |
WINSCARD_PIN | Smart card PIN code | 214653 |
WINSCARD_CERT_PATH | Path to the .cer file containing the smart card certificate | D:\test_data\user.cer |
WINSCARD_PK_PATH | Path to the .key file containing the smart card certificate | D:\test_data\user.key |
WINSCARD_CONTAINER | Container name | 1b22c362-46ba-4889-ad5c-01f7abcabcabedw |
WINSCARD_READER | Reader name | Microsoft Virtual Smart Card 2 |
And finally, run HookLoadLibrary.exe
.
Debugging
The first run
Let's try to sign the data using the emulated smart card.
Expectedly (otherwise, this article would not exist ð), we got an exception. Our goal for the rest of the article is to figure out the cause of the error and fix it.
Error location
At this point, I don't know what caused the error. So, let's use WinDbg and make a TTD recording. Finding a function that returns an error in this way will be easier. With TTD recording I can walk through the execution without rerunning the app.
Note. Because the HookLoadLibrary.exe
runs the SignDataTpm.exe
in a separate process, you should attach WinDbg to this process instead of running the HookLoadLibrary.exe
in the WinDbg.
Good. Now we can start debugging.
At this point, I can assume only something: some function in msclmd.dll
or basecsp.dll
fails and breaks the signing. Let's see the last sspi-rs
FII calls in the logs to have at least some orientation on where to dig. If we catch a moment of the last winscard calls, we'll retrieve a call stack and then analyze those functions. These are the last meaningful records in the log file:
We can see the SCardTransmit
function call. I'll search for them in the WinDbg. But because I use the custom winscard.dll
, all WinSCard-related functions have a Rust_
prefix. Example:
I have omitted some manipulations so that this article is not too boring. In short, on the screenshot below we can see the last SCardTransmit
call and part of the call stack:
As I expected, we see the msclmd.dll
and basecsp.dll
functions. The next algorithm is the following: I take the highest function from the call stack and check the resulting status code in the WinDbg. If it succeeds, I take a lower function from the call stack and check its status code. When I finally find a failing function, I'll use WinDbg and IDA to see the exact failing point. I expect something like return 0x80100004;
or any other status code.
Hmmm ðĪ. All functions up to the C#-related ones were succeeded. Perhaps the last calls to SCardTransmit
occurred after a signing error and Windows was trying to finalize the error/gather some information OR the signing process is not even started yet. I need another function to start with. Let's try SCardReadCacheW
. I had a "great" experience with smart card caches in the past, so maybe it'll help me this time. Thanks to TTD I don't need to rerun it and can walk through the recording again and again.
After some time I finally found something that looks like data signing ð.
Let's see the status codes of the highlighted functions:
Cool! Now we know what the msclmd!I_PIVCardSignData
function returns the SCARD_E_INVALID_PARAMETER
status code.
I need to find where in this function body the error has been thrown. Walking through every ASM instruction is boring and time-consuming, so I took an IDA and reversed this function as much as I could. After that, I marked some places (other function calls, if
s, and so on) that could fail and checked them with WinDbg.
Okaaay, so, the key algorithm and id extraction have failed ðĪ. Reversed pseudo-code:
return_code = ;
I dug into it and found exact place where the fail happens. I skipped this part for you and show you the result:
Here is the reversed part of the ASM code above. We can see that invalid cert file tag causes the SCARD_E_INVALID_PARAMETER
error:
On the screenshot above the part of the msclmd!I_GetPIVKeyIDFromPIVCertFileTag
function is shown. The msclmd!I_IsValidPIVCertFileTag
function has returned 0
(false
). It means that the cert file tag is not valid.
So, the cert_file_tag
must meet highlighted conditions to be valid. In our case it's equal to 0xaaaaaa
which is obviously not valid. To fix it we need to figure out how the cert_file_tag
is constructed and how we can change it to met one of the needed values. I did it for you, so take a look at the following screenshot:
The screenshot above contains a part of the msclmd!I_GetKeyAndAlgFromMapIndex
function. This piece of code gives us answers to all our questions. First of all, the cert_file_tag
value is extracted from the cmap_record
using the swscanf_s
function. In simple words, it takes the cmap_record
buffer pointer, skips the first 30 characters (bytes), scans the following 6 characters (bytes), converts them from hex to decimal, and, finally, assigns it to the cert_file_tag
variable. This is our cert_file_tag
which is not valid. In turn, the cmap_record
is a pointer to the CONTAINER_MAP_RECORD
structure where the first field is a container name:
typedef struct _CONTAINER_MAP_RECORD
CONTAINER_MAP_RECORD, *PCONTAINER_MAP_RECORD;
According to the Microsoft's specification:
The
wszGuid
member consists of a UNICODE character string representation of an identifier that CAPI assigned to the container. This is usually, but not always, a GUID string. Identifier names cannot contain the special character â\â. When read-only cards are provisioned, the provisioning process must follow the same guidelines for identifier names.
What?
ðŪâðĻ Let's summarize it all.
The CONTAINER_MAP_RECORD
structure contains a smart card container name. The container map record value is extracted from the smart card cache. This container name (wszGuid
) is not a random GUID value. This is a special value that must contain one of the allowed certificate file tags. The smart card driver (msclmd.dll
) decides how to sign the data based on this cer_file_tag
. All possible certificate file tags are defined and can be found in the PIV smart card specification and open-source projects that follow this spec.
Soooo, if we just change the container name, then data signing should work? ðð
It seems like yes, it should. We have nothing to lose. Let's try it.
The fix
Change the WINSCARD_CONTAINER
environment variable.
Name | Value | Example |
---|---|---|
WINSCARD_CONTAINER | Container name | 1b22c362-46ba-4889-ad5c-01f7aa5fc10awww |
ðĪðĪðĪ
ð It works! ð Ignore the error at the bottom of the terminal. The only important thing is the resulting signature. It means, that the data signing using the emulated smart card works well ð. Even in the sspi-rs
logs we can see SCardTransmit
calls with input padded digest and output signature:
ððð I don't know why this container name format is not documented anywhere (at least I didn't find any mention about it on the Internet).
Conclusion
There are a lot of ways of debugging programs. In this article, I told you about one of my cases where I need to be creative and patient. In the end, I could tell that Windows is bad and so on, but I'll say the following instead:
Everything is possible. Any bug and problem is solvable. Be creative and have patience.
Wish you more interesting bugs and fun debugging! ð