Introduction
Working with smart cards and PKI stuff is an interesting field. You can see the state-of-art of computer security and how it can be used in the real environment from real users. But, sometimes, debugging and testing applications that work with smart cards is a real pain, especially when you have to deal with negative test cases and, as it often happens, you don't have many test smart cards to play with. What if you accidentally block a PIN? Or your CSP issues a wrong command, leaving the card in an inconsistent state? These and many other issues are quite common in this field, so one of the first things I realized when I started to work with smart cards was that I needed an emulator: something to play with without the risk of doing any damage. In this article, I will not speak about smart card OS emulation (perhaps it will be covered in the future...), but about a driver for a virtual smart card reader.
Searching the internet for virtual drivers leads you to find many interesting resources, but not the “guide for dummies” that I was hoping to find. I’m not an expert in driver developing; this is not by any means an article on “how to write drivers”. I’m just explaining my approach to a new subject, hoping that it will be useful for someone.
An alternative approach to the driver is just writing your own version on winscard.dll, and put it in the folder of the application you wish to debug. That's easier, in some cases, but has some drawbacks:
- To fully emulate the behavior of Windows Smart Card Resource Manager, you must implement lots of functions.
- It could be a pain to implement functions like
SCardGetStatusChange
, specially if you should mix real and simulated readers.
- You can't replace system's real winscard.dll, since it's subject to system file protection, so it could be tricky to override it in some applications.
Having tried both approaches, I think that developing a driver is better, having learned some base lessons on how to do it (or having this article as a guide :)).
Background
It needed just a few clicks on Google to realize that, to keep things easy, I had to use UMDF (User Mode Driver Framework) as a basis for the development of the driver. From my point of view, and my understanding of the subject, the main reasons are:
- If you make a mistake, you don't get an ugly blue screen - so, easy developing
- You can debug your code with your old good user mode debugger - eg. VS2008 - no need for kernel mode debugging - so, easy debugging
- In my case performance is not critical, and the little overhead introduced by the framework is not a problem
These are the reasons that led me to use UMDF. Considering the little effort and the satisfaction with the result, I think it was a good choice.
The code is base on the UMDFSkeleton
sample of WDK 7.1. I will first comment on the important points of the code, then I will explain the installation procedure.
As an addiction, the virtual card reader will communicate with a desktop application to provide the virtual smart card behavior; so, we'll see some IPC between a UMDF driver and a user process.
A Look at an UMDF Driver Structure
As I said, UMDF simplifies the development of a driver a lot. You just need to write some COM (actually, COM-like) objects implementing some core interfaces and that's it. Let's take a look at how it all works.
A user mode driver is like a COM object. So, like a COM object, we are building a DLL that exposes a DllGetClassObject
function, that will be called by the UMDF framework to obtain a ClassFactory
to create the actual driver object.
With ATL, it is very easy to create COM objects, so we'll use it to further simplify our job. The only function exposed by the DLL is:
STDAPI DllGetClassObject(__in REFCLSID rclsid, __in REFIID riid, __deref_out LPVOID* ppv)
{
return _AtlModule.DllGetClassObject(rclsid, riid, ppv);
}
Nothing strange here. The object we are creating (CMyDriver
) must implement the IDriverEntry
interface, that defines the main entry points of our driver. We can use the OnInitialize
method to do all initialization stuff before the actual job begins, but it is not needed in our case.
The OnDeviceAdd
method is called by the framework whenever a device is connected to the system that is managed by our driver. In our case, we create a CMyDriver
object (through CMyDevice::CreateInstance
method), that will hold a reference to a IWDFDevice
object, created by the CreateDevice
function. This is the initialization of CMyDriver
:
HRESULT
CMyDevice::CreateInstance(
__in IWDFDriver *FxDriver,
__in IWDFDeviceInitialize * FxDeviceInit
)
{
inFunc
CComObject<cmydevice>* device = NULL;
HRESULT hr;
hr = CComObject<cmydevice>::CreateInstance(&device);
if (device==NULL)
{
return E_OUTOFMEMORY;
}
device->AddRef();
FxDeviceInit->SetLockingConstraint(WdfDeviceLevel);
CComPtr<iunknown> spCallback;
hr = device->QueryInterface(IID_IUnknown, (void**)&spCallback);
CComPtr<iwdfdevice> spIWDFDevice;
if (SUCCEEDED(hr))
{
hr = FxDriver->CreateDevice(FxDeviceInit, spCallback, &spIWDFDevice);
}
if (spIWDFDevice->CreateDeviceInterface(&SmartCardReaderGuid,NULL)!=0)
OutputDebugString(L"CreateDeviceInterface Failed");
SAFE_RELEASE(device);
return hr;
}
We don't want synchronization issues, so we use SetLockingConstraint(WdfDeviceLevel)
: only one event handler of the device can run at a given moment. Then we ask the IWDFDriver
object to create a IWDFDevice
.
These objects are the actual objects maintained by UMDF through which we interact with the underlying driver and device. Since these objects are tightly coupled, in CMyDevice
we keep a reference to the IWDFDevice
object.
Moreover, we need to call CreateDeviceInterface
to create an interface for the device of a type specified by a GUID. In our case, a Smart Card Reader. This interface is automatically enabled by the framework.
We should note, at this point, that our CMyDevice
objects implement some interfaces:
IPnpCallbackHardware
IPnpCallback
IRequestCallbackCancel
In the CreateDevice
call, we passed spCallback
(a pointer to the IUnknown
interface of CMyDriver
), to inform the UMDF framework that we want to be notified about some events. The framework, according to the interfaces implemented by the callback object, calls its methods when specific events are fired.
IPnpCallbackHardware
contains methods to manage hardware insertion and removal.
IPnpCallback
contains methods to manage lifetime events on the driver.
IRequestCallbackCancel
contains method to manage deletion of I/O Request received by the device. We'll see it in detail later.
The first notification received by our driver is OnPrepareHardware
: the hardware is ready and the driver should prepare to use it:
HRESULT CMyDevice::OnPrepareHardware(
__in IWDFDevice* pWdfDevice
)
{
inFunc
m_pWdfDevice = pWdfDevice;
HRESULT hr = CMyQueue::CreateInstance(m_pWdfDevice, this);
DWORD pipeThreadID;
CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)pipeServerFunc,this,0,&pipeThreadID);
return hr;
}
HRESULT CMyQueue::CreateInstance(__in IWDFDevice* pWdfDevice, CMyDevice* pMyDevice)
{
inFunc
CComObject<cmyqueue>* pMyQueue = NULL;
if(NULL == pMyDevice)
{
return E_INVALIDARG;
}
HRESULT hr = CComObject<cmyqueue>::CreateInstance(&pMyQueue);
if(SUCCEEDED(hr))
{
pMyQueue->AddRef();
pMyQueue->m_pParentDevice = pMyDevice;
pMyQueue->m_pParentDevice->AddRef();
CComPtr<iunknown> spIUnknown;
hr = pMyQueue->QueryInterface(IID_IUnknown, (void**)&spIUnknown);
if(SUCCEEDED(hr))
{
CComPtr<iwdfioqueue> spDefaultQueue;
hr = pWdfDevice->CreateIoQueue( spIUnknown,
TRUE, WdfIoQueueDispatchParallel, FALSE, TRUE, &spDefaultQueue
);
if (FAILED(hr))
OutputDebugString (L"IoQueue NOT Created");
else
OutputDebugString (L"IoQueue Created");
}
SAFE_RELEASE(pMyQueue);
}
return hr;
}
We do two things: first, we create the default queue for this driver, and attach a callback interface to it (a CMyQueue
object that implements IQueueCallbackDeviceIoControl
, that will receive I/O events notifications). A driver queue receives all I/O requests from the system when applications try to interact with our device.
Second, since our driver needs to communicate with another processes, we should start a thread that will handle this communication. We'll see later how this works.
At this point, our driver is ready to receive requests and send appropriate responses to the system. This happens by means of calls made to the CMyQueue::OnDeviceIoControl
:
STDMETHODIMP_ (void) CMyQueue::OnDeviceIoControl(
__in IWDFIoQueue* pQueue,
__in IWDFIoRequest* pRequest,
__in ULONG ControlCode,
SIZE_T InputBufferSizeInBytes,
SIZE_T OutputBufferSizeInBytes
)
{
m_pParentDevice->ProcessIoControl
(pQueue,pRequest,ControlCode,InputBufferSizeInBytes,OutputBufferSizeInBytes);
}
void CMyDevice::ProcessIoControl(__in IWDFIoQueue* pQueue,
__in IWDFIoRequest* pRequest,
__in ULONG ControlCode,
SIZE_T inBufSize,
SIZE_T outBufSize)
{
inFunc
UNREFERENCED_PARAMETER(pQueue);
wchar_t log[300];
swprintf(log,L"[IOCT]IOCTL %08X -
In %i Out %i",ControlCode,inBufSize,outBufSize);
OutputDebugString(log);
if (ControlCode==IOCTL_SMARTCARD_GET_ATTRIBUTE) {
IoSmartCardGetAttribute(pRequest,inBufSize,outBufSize);
return;
}
else if (ControlCode==IOCTL_SMARTCARD_IS_PRESENT) {
IoSmartCardIsPresent(pRequest,inBufSize,outBufSize);
return;
}
else if (ControlCode==IOCTL_SMARTCARD_GET_STATE) {
IoSmartCardGetState(pRequest,inBufSize,outBufSize);
return;
}
else if (ControlCode==IOCTL_SMARTCARD_IS_ABSENT) {
IoSmartCardIsAbsent(pRequest,inBufSize,outBufSize);
return;
}
else if (ControlCode==IOCTL_SMARTCARD_POWER) {
IoSmartCardPower(pRequest,inBufSize,outBufSize);
return;
}
else if (ControlCode==IOCTL_SMARTCARD_SET_ATTRIBUTE) {
IoSmartCardSetAttribute(pRequest,inBufSize,outBufSize);
return;
}
else if (ControlCode==IOCTL_SMARTCARD_TRANSMIT) {
IoSmartCardTransmit(pRequest,inBufSize,outBufSize);
return;
}
swprintf(log,L"[IOCT]ERROR_NOT_SUPPORTED:%08X",ControlCode);
OutputDebugString(log);
pRequest->CompleteWithInformation(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED), 0);
return;
}
OnDeviceIoControl
is called when the driver receives a request, and it just dispatches the request to the CMyDevice
object. ControlCode
contains the IO control code of the request, and through the pRequest
object, we gain access to the associated input and output memory buffers.
The memory buffers are accessed through IWDFMemory
objects. The interface is quite straightforward, and doesn't need many explications.
In the code, there are some helper functions to set and get an integer, a buffer or a string
to and from the output and input buffers.
I/O Control Codes
Let's see which are the I/O control codes that a Smart Card Reader driver can receive:
IOCTL_SMARTCARD_GET_ATTRIBUTE
Quite easy. We just need to answer some easy questions: which is the vendor name, the reader name, the device unit (in case we have more than one reader with the same name), the communication protocol we support (our reader only supports T=1) and the ATR string of the inserted card. The ATR is the only element that requires to communicate with the virtual card, as we'll see later.
These I/O requests are immediately completed, so pRequest->CompleteWithInformation
is called at the end of the ProcessIoControl
method.
IOCTL_SMARTCARD_IS_PRESENT and IOCTL_SMARTCARD_IS_ABSENT
This is a bit trickier. We are asked if a smart card is in the reader. This is the code:
void CMyDevice::IoSmartCardIsPresent(IWDFIoRequest* pRequest,
SIZE_T inBufSize,SIZE_T outBufSize) {
UNREFERENCED_PARAMETER(inBufSize);
UNREFERENCED_PARAMETER(outBufSize);
OutputDebugString(L"[IPRE]IOCTL_SMARTCARD_IS_PRESENT");
BYTE ATR[100];
DWORD ATRSize;
if (QueryATR(ATR,&ATRSize))
pRequest->CompleteWithInformation(STATUS_SUCCESS, 0);
else {
waitInsertIpr=pRequest;
IRequestCallbackCancel *callback;
QueryInterface(__uuidof(IRequestCallbackCancel),(void**)&callback);
pRequest->MarkCancelable(callback);
callback->Release();
}
}
We try to communicate with the virtual card to ask its ATR; if the request succeeds, there's a card in the reader, otherwise not. In the first case, we just complete the I/O request to confirm that a card is present.
In the second case, we are actually starting to monitor the reader for card insertion. The I/O request is left pending (if we do not call CompleteWithInformation
, the UMDF framework automatically handles the pending request), and it will be completed as soon as a card is inserted. We just store the pointer to the pending request in waitInsertIpr
to remember that this request is still open.
Moreover ,we should call pRequest->MarkCancelable
to inform the framework that this request is cancellable (in case the device is deactivated, or when the system is shut down). CMyDevice
implements IRequestCallbackCancel
, so it can be notified of the deletion request.
For IOCTL_SMARTCARD_IS_ABSENT
it is obviously the opposite: the I/O request is completed when the smart card is removed.
IOCTL_SMARTCARD_GET_STATE
We are queried for the device state. This is quite easy, in our case: we just support two states: card absent and card present with protocol negotiated. In a real driver, we should of course handle more precise states. We just ask the virtual card ATR to check if it is present or not.
IOCTL_SMARTCARD_POWER
The card should be reset or unpowered. In case of a reset, we also return the ATR (if the virtual card is present).
IOCTL_SMARTCARD_SET_ATTRIBUTE
We could just ignore it. We just return SUCCESS
in case the SCARD_ATTR_DEVICE_IN_USE
parameter is set.
IOCTL_SMARTCARD_TRANSMIT
A command APDU should be sent to the smart card, and we should return the response. Not difficult, just some stuff to communicate to and from the virtual smart card handling process. We should remember to remove the SCARD_IO_REQUEST
structure before the APDU, and insert it before the response.
IPC with the Virtual Smart Card Process
Obviously, a driver can't have a user interface. But if I need to change the behavior of the virtual smart card, perhaps load and save its state, or just simulate its insertion and removal from the virtual reader, I definitely need a user interface to do it! So, the virtual reader driver should happily communicate with the outer world, sending requests and receiving responses to a process that will simulate the virtual smart card behavior. But - because there's always a but - perhaps we should remember that a driver, even a user mode driver, is not exactly a simple application. In this case, the problem is that this application lives in Session 0, isolated from the rest of the Session 1 - world.
I will not explain in detail the concept of Session 0 and 1, and the isolation of Session 0 in Vista and later OS. I will just say that a Session 0 and a Session 1 process can't communicate in some IPC modes: no thread and window messages, no Memory Mapped Files and no global names, so no shared synchronization objects (I also don't want the session 1 application to be elevated)... so, I see two alternatives:
And I choose Named Pipes. Fast, easy synchronization, reliable... that's enough. Perhaps in other scenarios I could make a different choice, but in this case it seemed to me the best alternative. So, let's go back to the communication thread and look at how it works:
DWORD CMyDevice::pipeServer() {
SECURITY_ATTRIBUTES sa;
CreateMyDACL(&sa);
while (true) {
HANDLE _pipe=CreateNamedPipe(L\\\\.\\pipe\\SCardSimulatorDriver,
PIPE_ACCESS_DUPLEX|FILE_FLAG_OVERLAPPED,PIPE_TYPE_BYTE,
PIPE_UNLIMITED_INSTANCES,0,0,0,&sa);
HANDLE _eventpipe=CreateNamedPipe
(L\\\\.\\pipe\\SCardSimulatorDriverEvents,
PIPE_ACCESS_DUPLEX|FILE_FLAG_OVERLAPPED,PIPE_TYPE_BYTE,
PIPE_UNLIMITED_INSTANCES,0,0,0,&sa);
OutputDebugString(L"Pipe created");
ConnectNamedPipe(_pipe,NULL);
ConnectNamedPipe(_eventpipe,NULL);
OutputDebugString(L"Pipe connected");
pipe=_pipe;
eventpipe=_eventpipe;
if (waitInsertIpr!=NULL) {
if (CheckATR()) {
if (waitInsertIpr->UnmarkCancelable()==S_OK)
waitInsertIpr->CompleteWithInformation
(STATUS_SUCCESS, 0);
waitInsertIpr=NULL;
}
}
while (true) {
DWORD command=0;
DWORD read=0;
if (!ReadFile(eventpipe,&command,sizeof(DWORD),&read,NULL)) {
pipe=NULL;
eventpipe=NULL;
if (waitRemoveIpr!=NULL) { if (waitRemoveIpr->UnmarkCancelable()==S_OK)
waitRemoveIpr->
CompleteWithInformation(STATUS_SUCCESS, 0);
waitRemoveIpr=NULL;
}
if (waitInsertIpr!=NULL) { waitInsertIpr->CompleteWithInformation
(HRESULT_FROM_WIN32(ERROR_CANCELLED), 0);
waitInsertIpr=NULL;
}
break;
}
if (command==0 && waitRemoveIpr!=NULL) { if (waitRemoveIpr->UnmarkCancelable()==S_OK)
waitRemoveIpr->CompleteWithInformation
(STATUS_SUCCESS, 0);
waitRemoveIpr=NULL;
}
else if (command==1 && waitInsertIpr!=NULL) { if (waitInsertIpr->UnmarkCancelable()==S_OK)
waitInsertIpr->CompleteWithInformation
(STATUS_SUCCESS, 0);
waitInsertIpr=NULL;
}
}
}
OutputDebugString(L"Pipe quit!!!");
return 0;
}
First of all, we create two Named Pipes. Why two? Easy: The first (SCardSimulatorDriver
) is for requests from the driver to the virtual smart card; the second (SCardSimulatorDriverEvents
) is for event notifications from the virtual smart card to the driver (insertion and removal).
With ConnectNamedPipe
, we wait for a client to connect. The call is blocking, so until someone opens the pipe, the thread stands waiting.
The utility function CreateMyDACL
(straight from MSDN) is used to set the appropriate DACL for the Named Pipe. In fact, if we used NULL
as DACL, the object would inherit the default settings of the parent process, and it would become inaccessible to client applications. Our custom DACL grants access to authenticated users, so all user processes are allowed to connect.
When the connection is established, we check if a card is already inserted and we are monitoring (waitInsertIpr
and CheckATR()
); in this case, in fact, it is not sure that the virtual card would notify the driver, since there's no insertion event. The driver would continue to believe that there's no card inserted.
Then the main communication loop starts. The driver thread stands waiting for events from the virtual smart card, calling ReadFile
. The data sent on this pipe is trivial: a single DWORD
with value 0
for removal and 1
for insertion. When an event arrives, if we are waiting for that event (waitRemoveIpr
or waitInsertIpr
), we complete that I/O request accordingly. Since these requests were marked as cancellable, we need to unmak them with UnmarkCancelable
and, if possible, complete them (UnmarkCancelable
could fail if it is too late and the request was already cancelled) (note that in the OnCancel
callback, we don't need to call UnmarkCancelable
).
If a ReadFile
call fails, it means that we lost the connection with the virtual card. Perhaps the application was closed, and so the pipes. In this case, we simply notify the removal of the card (if requested) and we restart waiting for a new client to connect.
Let's see what happens when the driver needs to send a request to the virtual smart card; the method QueryTransmit
does this job:
bool CMyDevice::QueryTransmit(BYTE *APDU,int APDUlen,BYTE *Resp,int *Resplen) {
if (pipe==NULL)
return false;
DWORD command=2;
DWORD read=0;
if (!WriteFile(pipe,&command,sizeof(DWORD),&read,NULL)) {
pipe=NULL;
return false;
}
DWORD dwAPDUlen=(DWORD)APDUlen;
if (!WriteFile(pipe,&dwAPDUlen,sizeof(DWORD),&read,NULL)) {
pipe=NULL;
return false;
}
if (!WriteFile(pipe,APDU,APDUlen,&read,NULL)) {
pipe=NULL;
return false;
}
FlushFileBuffers(pipe);
DWORD dwRespLen;
if (!ReadFile(pipe,&dwRespLen,sizeof(DWORD),&read,NULL)) {
pipe=NULL;
return false;
}
if (!ReadFile(pipe,Resp,dwRespLen,&read,NULL)) {
pipe=NULL;
return false;
}
(*Resplen)=(int)dwRespLen;
return true;
}
First we check if a pipe is connected; otherwise, we return a fail. Then we write on the pipe the APDU command. The protocol is very simple:
- a
DWORD
containing the command code (TRANSMIT=2
)
- a
DWORD
containing the length of the APDU
- the APDU buffer
We don't need anything more... we just wait for the response to come. we read a DWORD
containing the length of the response, and the response buffer. That's it. If any operation in the pipe fails, probably the communication is broken, so we return a fail and we wait for a new connection to come.
Every time we write something on the pipe, we should always call FlushFileBuffers
to be sure that the buffer is sent to the other side of the pipe and is not buffered; otherwise, the listening application could not receive our data.
Notes
This implementation of IPC is very simple. A bit too
simple. The I/O requests are synchronized, but the events received from the virtual smart cards are not. so, before setting the pipe handle to NULL
, it would be wise to acquire a lock on it... I'm lazy, it works 99% of times and I just need it for testing purposes... so I didn't do it. Shame on me.
A Sample Virtual Smart Card Application
In the VirtualSmartCard folder, you can find a sample .NET 3.5 application for a virtual smart card that communicates with the driver. The card answers to the ATR request and can be inserted and removed, but all the APDUs will return 9000.
The application just uses two NamedPipeClientStream
objects for data end event pipe, waits for requests on the data pipe and sends notifications to the event pipe:
void PipeClient()
{
pipe = new NamedPipeClientStream(".", "SCardSimulatorDriver",
PipeDirection.InOut, PipeOptions.Asynchronous);
eventPipe = new NamedPipeClientStream(".", "SCardSimulatorDriverEvents",
PipeDirection.InOut, PipeOptions.Asynchronous);
pipe.Connect();
eventPipe.Connect();
chkCardPresent.Enabled = true;
BinaryReader brPipe = new BinaryReader(pipe);
BinaryWriter bwPipe = new BinaryWriter(pipe);
bwEventPipe = new BinaryWriter(eventPipe);
while (true)
{
try
{
int command = brPipe.ReadInt32();
switch (command)
{
case 0:
case 1:
if (chkCardPresent.Checked)
{
bwPipe.Write((Int32)ATR.Length);
bwPipe.Write(ATR, 0, ATR.Length);
bwPipe.Flush();
}
else {
bwPipe.Write((Int32)0);
bwPipe.Flush();
}
if (command == 0)
logMessage("Reset");
else
logMessage("getATR");
break;
case 2:
int apduLen = brPipe.ReadInt32();
byte[] APDU = new byte[apduLen];
brPipe.Read(APDU, 0, apduLen);
logMessage(hexDump(APDU));
byte[] resp = new byte[] { 0x90, 0x00 };
bwPipe.Write((Int32)resp.Length);
bwPipe.Write(resp, 0, resp.Length);
bwPipe.Flush();
break;
}
}
catch (Exception e)
{
if (running)
MessageBox.Show("Error listening driver pipe");
return;
}
}
}
private void chkCardPresent_CheckedChanged(object sender, EventArgs e)
{
Int32 command;
if (chkCardPresent.Checked)
{
logMessage("Card Inserted");
command = 1;
}
else
{
logMessage("Card Removed");
command = 0;
}
bwEventPipe.Write(command);
bwEventPipe.Flush();
}
Requests are monitored on a specific thread, while event notifications are sent on the event handler of the user interface.
The application is really simple: it just notifies insertion and removal events in response of state change of a checkbox
, and answers to requests coming from the driver sending the ATR or a fixed response 9000. As we already said, after all writes to the BinaryWriter
, we flush it to ensure that the driver receives the data. Not that the NamedPipeClientStream
are created with the PipeOptions.Asynchronous
option because, even if we do not use asynchronous reads and writes, we want to be able to Close
the stream from the UI thread while another thread is waiting for a ReadInt32
to return. If we do not specify the PipeOptions.Asynchronous
flag, the call to Close
would be blocked until the ReadInt32
returns. The same thing is accomplished in the driver using the FILE_FLAG_OVERLAPPED
flag.
Compile, Install and Debug
No surprise, to compile the driver you need to install WDK 7.1. Just 620MB to download from Microsoft and we are ready to build the driver. Compiling a driver is not exactly the same as compiling a regular DLL. For example, the subsystem is not WINDOWS but is NATIVE; and the library path should be set accordingly to the architecture for which the driver is being compiled. This is the reason why we don't have a Visual Studio Solution for it, but we'll use the build environment that comes with the WDK, already correctly configured, from a command line. If you installed the WDK correctly, in the start menu, you have the links to various environments: Start > Programs > Windows Drivers Kit > WDK 7600.16385.1 > Build Environments > %OS version% > %TargetCPU% Free/Checked Build Environment.
The Checked Build environment is like a Debug build: optimizations are turned off and conditional code for debugging is included.
The Free Build is like a Release build: the code is optimized and debugging code is disabled.
Note that even in a Free Build, you can use a user mode debugger to step through your code.
To compile the virtual driver, open the correct Build Environment, cd
to the directory that contains the code and the subdir for the correct OS version (tested by now on Win 7 and Win XP - the settings of the sources
file are slightly different), and just type build
.
Et voilà, the DLL is built! ... as long as there's no compilation error. Note that in this build environments, warnings are treated as errors!. They are not reported by the build
tool, but they are dumped to a file named, for example, "buildfre_win7_amd64.wrn", if you are building a driver for windows 7, x64, Free Build.
To configure the build
tool, modify the sources
file, adding new source files or libraries to link against if needed.
Once the driver if built, you can find the DLL in the folder (if Win7, x64, Free) objfre_win7_amd64\amd64, together with the .inf file for installation.
To install the driver, we'll use the DevCon utility that comes with WDK (in %WinDDK%\tools\devcon\%Architecture%). Also, we need to copy the file WUDFUpdate_01009.dll (in %WinDDK%\redist\wdf\%Architecture%) to the path where our DLL is located.
To install the driver, run from an elevated shell:
devcon install BixVReader.inf root\BixVirtualReader
The syntax of devcon install
is:
devcon [-r] install <inf> <hwid>
Where <inf>
is the path to the .inf file, and <hwid>
is the hardware ID of the device that we are installing. It should match the ID in the .inf file at the line:
%ReaderDeviceName%=VReader_Install,root\BixVirtualReader
The -r
switch asks to reboot only if a reboot is required.
If the installation succeeds, the user is asked for confirmation, since the driver is not signed, and the virtual reader is installed.
You can download the zip archive with the compiled binaries for Windows 7, 32 and 64 bits and Windows XP. In the zip file, you can find the DLL and the inf file, but you'll need devcon.exe for your architecture (it is not redistributable, but is part of WDK).
If you start the Virtual Smart Card application, you should be able to connect to the virtual card. In Win7, as soon as you insert the card, some minidrivers will try to communicate with the card, sending various commands. Since the card always answers 9000 (that is "OK"), a minidriver could be faked to match it, and you'll see a known card in the device manager! In the log listbox
of the application, you'll see the APDUs sent to the card.
Debugging the driver is actually very easy: since it's a user-mode driver, you can use a user-mode debugger: Visual Studio, for example. All you have to do is attach to a running process, and select WUDFHost.exe. Since you could have more than one UMDF driver in your system, you could also have more than one instance of WUDFHost.exe. Which is the correct one? You can use Procexp from Sysinternals to search which processes load your DLL, and check the PID of the correct WUDFHost.exe. Remember that both Procexp and Visual Studio should be Run as Administrator, otherwise they will not be able to attach to the driver process.
When you need to re-compile and re-test your driver, you need to update the driver DLL file.
You can do it simply launching the command:
devcon update BixVReader.inf root\BixVirtualReader
from the same shell used to install it.
Note that the device will be disabled, updated and reenabled, but if you have pending I/O requests, the system will not be able to disable it, and you will need to reboot the system for changes to take place.
In that case, you can do the following:
- Open Device Manager
- Disable the virtual device
- If there are still pending I/O requests, kill the process of WUDFHost.exe (the one that hosts BixVReader.dll)
- Update the driver with the command line above
- Enable the device
As soon as you enable the device, the driver is loaded and executed. If you need to debug your driver from the very beginning, you can set the registry key
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\WUDF\Services\{193a1820-d9ac-4997-8c55-be817523f6aa}/HostProcessDbgBreakOnStartto the number of seconds that you want the host process to wait for a debugger to connect before starting.
It is useful, for debugging purposes, to have a trace or a kind of output from the driver. Even if WDK has a built in tracing system, through the WPP trace, I preferred to use a simple OutputDebugString
. Just for speed of development, I preferred to use a well-known method instead of learning a new one, but this is just personal taste... Using Dbgview, also from Sysinternals, Run as Administrator, or attaching a debugger, you can easily see the trace from your driver.
The .INF File
The .INF file used to install the virtual device is almost identical to the one from the UMDFSkeleton
example. Just one row was added:
UmdfKernelModeClientPolicy=AllowKernelModeClients
To allow a kernel-mode driver to load above the user-mode driver and to deliver requests from the kernel-mode to the user-mode driver.
I'm not exactly sure of which kernel mode driver runs above the virtual reader driver, but removing this line from the inf file, we simply do not get any I/O request notifications in our Queue object.
Conclusion
As I already said above, this is not by far an article on how a driver should be made. I'm quite sure that an expert driver developer would scream looking at my code. This is just a development tool for who, like me, works often on smart cards and needs to "play" with them. I hope that it will be useful for someone, and of course I wait for some serious driver developer to tell me how it should have been done. :)