Click here to Skip to main content
Click here to Skip to main content

An UMDF Driver for a Virtual Smart Card Reader

, 23 Jan 2011
Rate this:
Please Sign up or sign in to vote.
A simple implementation of a driver for a virtual smart card reader, based on UMDF
This is an old version of the currently published article.

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 Smile | :) ).

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;

    //
    // Allocate a new instance of the device class.
    //
    hr = CComObject<cmydevice>::CreateInstance(&device);

    if (device==NULL)
    {
        return E_OUTOFMEMORY;
    }

    //
    // Initialize the instance.
    //
    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
    // Store the IWDFDevice pointer
    m_pWdfDevice = pWdfDevice;

    // Configure the default IO Queue
    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))
    {
        // AddRef the object
        pMyQueue->AddRef();

        // Store the parent device object
        pMyQueue->m_pParentDevice = pMyDevice;

        // Increment the reference for the lifetime of the CMyQueue object.
        pMyQueue->m_pParentDevice->AddRef();

        CComPtr<iunknown> spIUnknown;
        hr = pMyQueue->QueryInterface(IID_IUnknown, (void**)&spIUnknown);

        if(SUCCEEDED(hr))
        {
            // Create the framework queue
            CComPtr<iwdfioqueue> spDefaultQueue;
            hr = pWdfDevice->CreateIoQueue( spIUnknown,
                                            TRUE,                        // DefaultQueue
                                            WdfIoQueueDispatchParallel,  // Parallel 
								// queue handling 
                                            FALSE,                       // PowerManaged
                                            TRUE,              // AllowZeroLengthRequests
                                            &spDefaultQueue 
                                            );
            if (FAILED(hr))
		OutputDebugString (L"IoQueue NOT Created");
            else
		OutputDebugString (L"IoQueue Created");

        }

        // Release the pMyQueue pointer when done. 
        // Note: UMDF holds a reference to it above
        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:

  • Named pipes
  • TCP/IP

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 I'm waiting for insertion, 
               		// I verify if a card is already present
			if (CheckATR()) {
				if (waitInsertIpr->UnmarkCancelable()==S_OK)
					waitInsertIpr->CompleteWithInformation
					(STATUS_SUCCESS, 0);
				waitInsertIpr=NULL;
			}
		}
		while (true) {
			// wait for an event
			DWORD command=0;
			DWORD read=0;
			if (!ReadFile(eventpipe,&command,sizeof(DWORD),&read,NULL)) {
				pipe=NULL;
				eventpipe=NULL;
				if (waitRemoveIpr!=NULL) {// card removed
					if (waitRemoveIpr->UnmarkCancelable()==S_OK)
						waitRemoveIpr->
					CompleteWithInformation(STATUS_SUCCESS, 0);
					waitRemoveIpr=NULL;
				}
				if (waitInsertIpr!=NULL) {// card inserted
					waitInsertIpr->CompleteWithInformation
					(HRESULT_FROM_WIN32(ERROR_CANCELLED), 0);
					waitInsertIpr=NULL;
				}
				break;
			}
			if (command==0 && waitRemoveIpr!=NULL) {// card removed
				if (waitRemoveIpr->UnmarkCancelable()==S_OK)
					waitRemoveIpr->CompleteWithInformation
						(STATUS_SUCCESS, 0);
				waitRemoveIpr=NULL;
			}
			else if (command==1 && waitInsertIpr!=NULL) {// card inserted
				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

VirtualSmartCard.gif

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.

Device.gif

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. Smile | :)

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Fabio Ottavi
Engineer
Italy Italy
No Biography provided

Comments and Discussions


Discussions posted for the Published version of this article. Posting a message here will take you to the publicly available article in order to continue your conversation in public.
 
QuestionSTL specially vector Pinmembervinaysingh123414-Jul-14 13:54 
AnswerRe: STL specially vector PinmemberFabio Ottavi16-Jul-14 4:48 
GeneralRe: STL specially vector Pinmembervinaysingh12348-Aug-14 6:34 
QuestionDriver communication with java application Pinmemberxxjxxhxx9-Jun-14 21:40 
AnswerRe: Driver communication with java application PinmemberFabio Ottavi10-Jun-14 1:48 
GeneralRe: Driver communication with java application Pinmemberxxjxxhxx11-Jun-14 2:50 
AnswerRe: Driver communication with java application PinmemberFabio Ottavi11-Jun-14 6:12 
QuestionHow can I install this driver in win 8.1 x64 system PinmemberMember 99999635-Jun-14 22:38 
AnswerRe: How can I install this driver in win 8.1 x64 system PinmemberFabio Ottavi6-Jun-14 1:52 
GeneralRe: How can I install this driver in win 8.1 x64 system [modified] PinmemberMember 99999636-Jun-14 2:06 
GeneralRe: How can I install this driver in win 8.1 x64 system PinmemberFabio Ottavi6-Jun-14 2:41 
GeneralRe: How can I install this driver in win 8.1 x64 system PinmemberMember 99999636-Jun-14 3:00 
AnswerRe: How can I install this driver in win 8.1 x64 system PinmemberFabio Ottavi6-Jun-14 3:10 
GeneralRe: How can I install this driver in win 8.1 x64 system PinmemberMember 99999636-Jun-14 3:28 
GeneralRe: How can I install this driver in win 8.1 x64 system PinmemberMember 99999638-Jun-14 23:50 
AnswerRe: How can I install this driver in win 8.1 x64 system PinmemberFabio Ottavi8-Jun-14 23:52 
GeneralRe: How can I install this driver in win 8.1 x64 system PinmemberMember 99999636-Jun-14 3:11 
Questionhow we start tcp/ip PinmemberKritchai Phromros20-Apr-14 5:58 
AnswerRe: how we start tcp/ip PinmemberFabio Ottavi20-Apr-14 9:53 
GeneralRe: how we start tcp/ip PinmemberKritchai Phromros20-Apr-14 15:51 
QuestionQuestions: Pinmembervinaysingh123428-Mar-14 12:29 
AnswerRe: Questions: PinmemberFabio Ottavi31-Mar-14 2:26 
GeneralRe: Questions: Pinmembervinaysingh123431-Mar-14 7:45 
GeneralRe: Questions: Pinmembervinaysingh12341-Apr-14 17:11 
QuestionCan we talk by mail? :) PinmemberMember 1068151018-Mar-14 21:21 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web02 | 2.8.140827.1 | Last Updated 23 Jan 2011
Article Copyright 2010 by Fabio Ottavi
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid