一个虚拟智能卡读卡器的UMDF驱动程序






4.90/5 (70投票s)
一个基于 UMDF 的虚拟智能卡读卡器驱动程序的简单实现
引言
智能卡和 PKI 相关的工作是一个有趣的领域。你可以看到计算机安全领域的最新技术以及如何将其应用于真实用户的实际环境中。但是,有时调试和测试智能卡应用程序会非常痛苦,尤其是当你必须处理负面测试用例时,而且通常你没有很多测试智能卡可以玩。如果你不小心锁定了 PIN 码怎么办?或者你的 CSP 发出了错误的命令,导致卡处于不一致状态?这些以及许多其他问题在这个领域很常见,所以当我开始使用智能卡时,我意识到的第一件事就是我需要一个模拟器:一个可以安全玩耍而不会造成任何损坏的东西。在这篇文章中,我不会讨论智能卡操作系统模拟(也许将来会涉及到……),而是讨论一个虚拟智能卡读卡器驱动程序。
在互联网上搜索虚拟驱动程序可以找到许多有趣的资源,但不是我希望找到的“傻瓜指南”。我不是驱动程序开发专家;这绝不是一篇关于“如何编写驱动程序”的文章。我只是解释了我对一个新主题的方法,希望它能对某人有用。
除了驱动程序,另一种方法是编写自己的 `winscard.dll` 版本,并将其放入你要调试的应用程序的文件夹中。在某些情况下这更容易,但也有一些缺点
- 要完全模拟 Windows 智能卡资源管理器的行为,您必须实现许多功能。
- 实现像
SCardGetStatusChange
这样的函数可能会很痛苦,特别是当您需要混合使用真实和模拟的读卡器时。 - 您不能替换系统真实的 `winscard.dll`,因为它受到系统文件保护,因此在某些应用程序中覆盖它可能会很棘手。
尝试了两种方法后,我认为开发驱动程序更好,因为我学到了一些如何做的基础知识(或者有这篇文章作为指导:))。
背景
只需在 Google 上点击几下,我就意识到,为了简化事情,我必须使用 UMDF(用户模式驱动程序框架)作为驱动程序开发的基础。从我的角度和对这个主题的理解来看,主要原因有
- 如果你犯了错误,你不会得到一个丑陋的蓝屏——所以,开发简单
- 你可以用你的旧的用户模式调试器(例如 VS2008)调试你的代码——不需要内核模式调试——所以,调试简单
- 在我的情况下,性能不是关键,框架引入的微小开销也不是问题。
这些就是我选择 UMDF 的原因。考虑到付出的努力和对结果的满意度,我认为这是一个不错的选择。
代码基于 WDK 7.1 的 `UMDFSkeleton` 示例。我将首先评论代码中的重要点,然后解释安装过程。
此外,虚拟读卡器将与桌面应用程序通信,以提供虚拟智能卡行为;因此,我们将看到 UMDF 驱动程序与用户进程之间的 IPC。
UMDF 驱动程序结构一览
正如我所说,UMDF 大大简化了驱动程序的开发。你只需编写一些 COM(实际上是类似 COM)对象来实现一些核心接口即可。让我们看看这一切是如何运作的。
用户模式驱动程序就像一个 COM 对象。因此,就像 COM 对象一样,我们正在构建一个公开 `DllGetClassObject` 函数的 DLL,该函数将由 UMDF 框架调用以获取 `ClassFactory` 来创建实际的驱动程序对象。
使用 ATL 创建 COM 对象非常容易,因此我们将使用它来进一步简化我们的工作。DLL 唯一公开的函数是
STDAPI DllGetClassObject(__in REFCLSID rclsid, __in REFIID riid, __deref_out LPVOID* ppv)
{
return _AtlModule.DllGetClassObject(rclsid, riid, ppv);
}
这里没什么奇怪的。我们创建的对象(`CMyDriver`)必须实现 `IDriverEntry` 接口,该接口定义了驱动程序的主要入口点。我们可以使用 `OnInitialize` 方法在实际工作开始之前完成所有初始化工作,但在我们的案例中不需要。
每当一个由我们的驱动程序管理的设备连接到系统时,框架就会调用 `OnDeviceAdd` 方法。在我们的例子中,我们创建一个 `CMyDriver` 对象(通过 `CMyDevice::CreateInstance` 方法),它将持有对由 `CreateDevice` 函数创建的 `IWDFDevice` 对象的引用。
更新
一些用户要求拥有多个虚拟读卡器实例;在此更新版本中,我通过使用驱动程序的单个实例添加了此可能性。一个配置文件“BixVReader.ini”用于读取应向驱动程序注册多少个虚拟读卡器,以及一些其他配置参数,我们将在后续的“更新”部分中看到。
这是 `CMyDriver` 的初始化
HRESULT
CMyDevice::CreateInstance(
__in IWDFDriver *FxDriver,
__in IWDFDeviceInitialize * FxDeviceInit
)
{
inFunc
SectionLogger a(__FUNCTION__);
CComObject<CMyDevice>* device = NULL;
HRESULT hr;
OutputDebugString(L"[BixVReader]CreateInstance"); //
// Allocate a new instance of the device class.
//
hr = CComObject<CMyDevice>::CreateInstance(&device);
if (device==NULL)
{
return E_OUTOFMEMORY;
}
//
// Initialize the instance.
//
device->AddRef();
OutputDebugString(L"[BixVReader]SetLockingConstraint"); //
FxDeviceInit->SetLockingConstraint(WdfDeviceLevel);
CComPtr<IUnknown> spCallback;
OutputDebugString(L"[BixVReader]QueryInterface"); //
hr = device->QueryInterface(IID_IUnknown, (void**)&spCallback);
CComPtr<IWDFDevice> spIWDFDevice;
if (SUCCEEDED(hr))
{
OutputDebugString(L"[BixVReader]CreateDevice"); //
hr = FxDriver->CreateDevice(FxDeviceInit, spCallback, &spIWDFDevice);
}
device->numInstances=GetPrivateProfileInt(L"Driver",L"NumReaders",1,L"BixVReader.ini");
WSADATA wsaData;
int iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (iResult != NO_ERROR) {
OutputDebugString(L"[BixVReader]Error at WSAStartup()\n");
}
for (int i=0;i<device->numInstances;i++) {
OutputDebugString(L"[BixVReader]CreateDeviceInterface"); //
wchar_t name[10];
swprintf(name,L"DEV%i",i);
if (spIWDFDevice->CreateDeviceInterface(&SmartCardReaderGuid,name)!=0)
OutputDebugString(L"[BixVReader]CreateDeviceInterface Failed");
}
SAFE_RELEASE(device);
return hr;
}
我们不希望出现同步问题,因此我们使用 `SetLockingConstraint(WdfDeviceLevel)`:设备的事件处理程序在给定时刻只能运行一个。然后我们要求 `IWDFDriver` 对象创建一个 `IWDFDevice`。
这些对象是 UMDF 维护的实际对象,我们通过它们与底层驱动程序和设备进行交互。由于这些对象紧密耦合,在 `CMyDevice` 中我们保留对 `IWDFDevice` 对象的引用。
此外,我们需要调用 `CreateDeviceInterface` 为驱动程序管理的每个虚拟读卡器创建一个特定 GUID 类型的设备接口。在我们的案例中,它是一个智能卡读卡器。每个接口都有一个名称(在我们的案例中是“DEVx”)来区分队列中特定请求的接收者(稍后会详细介绍)。此接口由框架自动启用。
此时我们应该注意到,我们的 `CMyDevice` 对象实现了以下接口
IPnpCallbackHardware
IPnpCallback
IRequestCallbackCancel
在 `CreateDevice` 调用中,我们传递了 `spCallback`(`CMyDriver` 的 `IUnknown` 接口的指针),以通知 UMDF 框架我们希望收到某些事件的通知。框架根据回调对象实现的接口,在特定事件触发时调用其方法。
IPnpCallbackHardware
包含管理硬件插入和移除的方法。IPnpCallback
包含管理驱动程序生命周期事件的方法。IRequestCallbackCancel
包含管理设备接收到的 I/O 请求删除的方法。我们稍后将详细介绍。
我们的驱动程序收到的第一个通知是 `OnPrepareHardware`:硬件已准备就绪,驱动程序应准备使用它
HRESULT CMyDevice::OnPrepareHardware(
__in IWDFDevice* pWdfDevice
)
{
inFunc
SectionLogger a(__FUNCTION__);
// Store the IWDFDevice pointer
m_pWdfDevice = pWdfDevice;
// Configure the default IO Queue
HRESULT hr = CMyQueue::CreateInstance(m_pWdfDevice, this);
return hr;
}
我们为该驱动程序创建默认队列,并为其附加一个回调接口(一个实现 `IQueueCallbackDeviceIoControl` 的 `CMyQueue` 对象,它将接收 I/O 事件通知)。当应用程序尝试与我们的设备交互时,驱动程序队列会接收系统发来的所有 I/O 请求。
更新
驱动程序的生命周期有多个状态需要管理以避免不正确的行为:设备从空闲状态开始,然后切换到工作状态,在各种系统事件(关机、休眠)下可以禁用和重新启用。不深入细节,我们感兴趣的状态称为 D0 状态(设备完全正常运行)。我们将拦截进出此状态的转换以执行与设备功能相关的任务。对于我们的虚拟驱动程序,这仅仅意味着我们启动和停止进程间通信。我们稍后将看到它是如何工作的。UMDF 框架公开了两个函数来完成这项工作,它们是 OnD0Entry 和 OnD0Exit
HRESULT CMyDevice::OnD0Entry(IN IWDFDevice* pWdfDevice,IN WDF_POWER_DEVICE_STATE previousState) {
SectionLogger a(__FUNCTION__);
UNREFERENCED_PARAMETER(pWdfDevice);
UNREFERENCED_PARAMETER(previousState);
numInstances=GetPrivateProfileInt(L"Driver",L"NumReaders",1,L"BixVReader.ini");
readers.resize(numInstances);
for (int i=0;i<numInstances;i++) {
wchar_t section[300];
char sectionA[300];
swprintf(section,L"Reader%i",i);
sprintf(sectionA,"Reader%i",i);
int rpcType=GetPrivateProfileInt(section,L"RPC_TYPE",0,L"BixVReader.ini");
if (rpcType==0)
readers[i]=new PipeReader();
else if (rpcType==1)
readers[i]=new TcpIpReader();
readers[i]->instance=i;
readers[i]->device=this;
GetPrivateProfileStringA(sectionA,"VENDOR_NAME","Bix",readers[i]->vendorName,300,"BixVReader.ini");
GetPrivateProfileStringA(sectionA,"VENDOR_IFD_TYPE","VIRTUAL_CARD_READER",readers[i]->vendorIfdType,300,"BixVReader.ini");
readers[i]->deviceUnit=GetPrivateProfileInt(section,L"DEVICE_UNIT",i,L"BixVReader.ini");
readers[i]->protocol=0;
readers[i]->init(section);
DWORD pipeThreadID;
readers[i]->serverThread=CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)ServerFunc,readers[i],0,&pipeThreadID);
}
return S_OK;
}
HRESULT CMyDevice::OnD0Exit(IN IWDFDevice* pWdfDevice,IN WDF_POWER_DEVICE_STATE newState) {
SectionLogger a(__FUNCTION__);
UNREFERENCED_PARAMETER(pWdfDevice);
UNREFERENCED_PARAMETER(newState);
// dovrei fermare tutti i thread in ascolto dei lettori
shutDown();
return S_OK;
}
void CMyDevice::shutDown() {
SectionLogger a(__FUNCTION__);
for (int i=0;i<numInstances;i++) {
readers[i]->shutdown();
delete readers[i];
}
numInstances=0;
}
在驱动程序的更新版本中,我们还从 BixVReader.ini 读取驱动程序的配置。我们可以设置的参数有
- NumReaders:虚拟读卡器的数量
- RPC_PORT_BASE:TCP/IP 通信的基本端口
- RPC_TYPE:我们是使用管道还是 TCP/IP 与虚拟读卡器应用程序通信?
- VENDOR_NAME:SCardListReaders 返回的读卡器名称的一部分
- VENDOR_IFD_TYPE:SCardListReaders 返回的读卡器名称的一部分
- DEVICE_UNIT:SCardListReaders 返回的读卡器名称的一部分(名称、IFD 类型和设备单元串联以获得最终的读卡器名称)
- PIPE_NAME:用于从驱动程序到虚拟读卡器的请求的命名管道(如果使用管道)
- PIPE_EVENT_NAME:用于从虚拟读卡器到驱动程序的事件通知的命名管道(如果使用管道)
- TCP_PORT:用于从驱动程序到虚拟读卡器的请求的端口号(如果使用 TCP/IP)
- TCP_EVENT_PORT:用于从虚拟读卡器到驱动程序的事件通知的端口号(如果使用 TCP/IP)
BixVReader.ini 文件必须放置在 %SystemRoot% 中才能被驱动程序找到。如果此文件不存在,或某些参数未指定,则使用以下默认值:
- NumReaders:1个虚拟读卡器
- RPC_PORT_BASE:29500
- RPC_TYPE:管道
- VENDOR_NAME:"Bix"
- VENDOR_IFD_TYPE:"VIRTUAL_SCARD_READER"
- DEVICE_UNIT:虚拟读卡器的索引号
- PIPE_NAME:"SCardSimulatorDriver" 后跟读卡器索引
- PIPE_EVENT_NAME:"SCardSimulatorDriverEvents" 后跟读卡器索引
- TCP_PORT:PortBase + (读卡器索引 * 2)
- TCP_EVENT_PORT:PortBase + (读卡器索引 * 2) +1
这是一个 BixVReader.ini 示例文件
[Driver] NumReaders=2 [Reader0] RPC_TYPE=0 VENDOR_NAME=VirtualCard VENDOR_IFD_TYPE=BixReader DECIVE_UNIT=0 [Reader1] RPC_TYPE=1 VENDOR_NAME=VirtualCard VENDOR_IFD_TYPE=BixReader DECIVE_UNIT=1
在此示例中,我们有 2 个读卡器,分别命名为“VirtualCard BixReader 0”和“VirtualCard BixReader 1”。0 号接受命名管道连接,1 号接受 Tcp/Ip 连接。
所有这些参数都保存在一个 Reader 类中,该类实现了所有读卡器特定的功能。
此时,我们的驱动程序已准备好接收请求并向系统发送适当的响应。这通过调用 `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
SectionLogger a(__FUNCTION__);
UNREFERENCED_PARAMETER(pQueue);
wchar_t log[300];
swprintf(log,L"[BixVReader][IOCT]IOCTL %08X - In %i Out %i",ControlCode,inBufSize,outBufSize);
OutputDebugString(log);
//SectionLocker lock(m_RequestLock);
int instance=0;
{
CComPtr<IWDFFile> pFileObject;
pRequest->GetFileObject(&pFileObject);
if (pFileObject != NULL)
{
DWORD logLen=300;
pFileObject->RetrieveFileName(log,&logLen);
instance=_wtoi(log+(logLen-2));
}
}
Reader &reader=*readers[instance];
if (ControlCode==IOCTL_SMARTCARD_GET_ATTRIBUTE) {
reader.IoSmartCardGetAttribute(pRequest,inBufSize,outBufSize);
return;
}
else if (ControlCode==IOCTL_SMARTCARD_IS_PRESENT) {
reader.IoSmartCardIsPresent(pRequest,inBufSize,outBufSize);
return;
}
else if (ControlCode==IOCTL_SMARTCARD_GET_STATE) {
reader.IoSmartCardGetState(pRequest,inBufSize,outBufSize);
return;
}
else if (ControlCode==IOCTL_SMARTCARD_IS_ABSENT) {
reader.IoSmartCardIsAbsent(pRequest,inBufSize,outBufSize);
return;
}
else if (ControlCode==IOCTL_SMARTCARD_POWER) {
reader.IoSmartCardPower(pRequest,inBufSize,outBufSize);
return;
}
else if (ControlCode==IOCTL_SMARTCARD_SET_ATTRIBUTE) {
reader.IoSmartCardSetAttribute(pRequest,inBufSize,outBufSize);
return;
}
else if (ControlCode==IOCTL_SMARTCARD_SET_PROTOCOL) {
reader.IoSmartCardSetProtocol(pRequest,inBufSize,outBufSize);
return;
}
else if (ControlCode==IOCTL_SMARTCARD_TRANSMIT) {
reader.IoSmartCardTransmit(pRequest,inBufSize,outBufSize);
return;
}
swprintf(log,L"[BixVReader][IOCT]ERROR_NOT_SUPPORTED:%08X",ControlCode);
OutputDebugString(log);
pRequest->CompleteWithInformation(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED), 0);
return;
}
当驱动程序收到请求时,会调用 `OnDeviceIoControl`,它只是将请求分派给 `CMyDevice` 对象。`ControlCode` 包含请求的 IO 控制代码,通过 `pRequest` 对象,我们可以访问相关的输入和输出内存缓冲区。
更新
由于存在多个虚拟读卡器,我们需要确定此 I/O 请求针对的是哪一个。这通过使用 GetFileObject 函数和 IWDFFile 接口的 RetrieveFileName 方法来完成。文件名以我们在接口创建中设置的“DEVx”字符串结尾。字符串解析方法有点粗糙,但如果读卡器少于 10 个(...超过 10 个读卡器?你需要用它们做什么??),它就能工作。然后请求被转发到与所需虚拟读卡器对应的 Reader 对象。
通过 `IWDFMemory` 对象访问内存缓冲区。该接口相当简单,不需要过多解释。
在代码中,有一些辅助函数用于在输出和输入缓冲区之间设置和获取整数、缓冲区或字符串。
I/O 控制码
让我们看看智能卡读卡器驱动程序可以接收哪些 I/O 控制代码
IOCTL_SMARTCARD_GET_ATTRIBUTE
很简单。我们只需要回答一些简单的问题:供应商名称是什么,读卡器名称是什么,设备单元(如果多个读卡器具有相同的名称),我们支持的通信协议(根据 ATR)以及插入卡的 ATR 字符串。ATR 是唯一需要与虚拟卡通信的元素,我们稍后将看到。
这些 I/O 请求会立即完成,因此 `pRequest->CompleteWithInformation` 在 `ProcessIoControl` 方法的末尾被调用。
更新
由于我们增加了不同读卡器类型、读卡器名称和 RPC 方法的复杂性,我们向虚拟智能卡应用程序提供了一些读卡器配置信息;因此我实现了一些自定义值来读取虚拟读卡器的配置数据
void Reader::IoSmartCardGetAttribute(IWDFIoRequest* pRequest,SIZE_T inBufSize,SIZE_T outBufSize) { UNREFERENCED_PARAMETER(inBufSize); wchar_t log[300]=L""; char temp[300]; DWORD code=getInt(pRequest); swprintf(log,L"[BixVReader][GATT] - code %0X",code); OutputDebugString(log); switch(code) { case SCARD_ATTR_VALUE(SCARD_CLASS_VENDOR_DEFINED, 0xA009): // custom attribute; RPC_TYPE OutputDebugString(L"[BixVReader][GATT]RPC_TYPE"); setInt(device,pRequest,rpcType); return; case SCARD_ATTR_VALUE(SCARD_CLASS_VENDOR_DEFINED, 0xA00a): // custom attribute; PipeName if (rpcType==0) { PipeReader *pipe=(PipeReader *)this; OutputDebugString(L"[BixVReader][GATT]PIPE_NAME"); sprintf(temp,"%S",pipe->pipeName); setString(device,pRequest,(char*)temp,(int)outBufSize); } else { SectionLocker lock(device->m_RequestLock); pRequest->CompleteWithInformation(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED), 0); } return; case SCARD_ATTR_VALUE(SCARD_CLASS_VENDOR_DEFINED, 0xA00b): // custom attribute; EventPipeName if (rpcType==0) { PipeReader *pipe=(PipeReader *)this; OutputDebugString(L"[BixVReader][GATT]EVENT_PIPE_NAME"); sprintf(temp,"%S",pipe->pipeEventName); setString(device,pRequest,(char*)temp,(int)outBufSize); } else { SectionLocker lock(device->m_RequestLock); pRequest->CompleteWithInformation(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED), 0); } return; case SCARD_ATTR_VALUE(SCARD_CLASS_VENDOR_DEFINED, 0xA00c): // custom attribute; TCP port if (rpcType==1) { TcpIpReader *tcpIp=(TcpIpReader *)this; OutputDebugString(L"[BixVReader][GATT]PORT"); setInt(device,pRequest,tcpIp->port); } else { SectionLocker lock(device->m_RequestLock); pRequest->CompleteWithInformation(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED), 0); } return; case SCARD_ATTR_VALUE(SCARD_CLASS_VENDOR_DEFINED, 0xA00d): // custom attribute; TCP event port if (rpcType==1) { TcpIpReader *tcpIp=(TcpIpReader *)this; OutputDebugString(L"[BixVReader][GATT]EVENT_PORT"); setInt(device,pRequest,tcpIp->eventPort); } else { SectionLocker lock(device->m_RequestLock); pRequest->CompleteWithInformation(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED), 0); } return; case SCARD_ATTR_VALUE(SCARD_CLASS_VENDOR_DEFINED, 0xA00e): // custom attribute; TCP base port if (rpcType==1) { TcpIpReader *tcpIp=(TcpIpReader *)this; OutputDebugString(L"[BixVReader][GATT]BASE_PORT"); setInt(device,pRequest,tcpIp->portBase); tcpIp; } else { SectionLocker lock(device->m_RequestLock); pRequest->CompleteWithInformation(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED), 0); } return; case SCARD_ATTR_CHARACTERISTICS: // 0x00000000 No special characteristics OutputDebugString(L"[BixVReader][GATT]SCARD_ATTR_CHARACTERISTICS"); setInt(device,pRequest,0); return; case SCARD_ATTR_VENDOR_NAME: OutputDebugString(L"[BixVReader][GATT]SCARD_ATTR_VENDOR_NAME"); setString(device,pRequest,vendorName,(int)outBufSize); return; case SCARD_ATTR_VENDOR_IFD_TYPE: OutputDebugString(L"[BixVReader][GATT]SCARD_ATTR_VENDOR_IFD_TYPE"); setString(device,pRequest,vendorIfdType,(int)outBufSize); return; case SCARD_ATTR_DEVICE_UNIT: OutputDebugString(L"[BixVReader][GATT]SCARD_ATTR_DEVICE_UNIT"); setInt(device,pRequest,deviceUnit); return; case SCARD_ATTR_ATR_STRING: OutputDebugString(L"[BixVReader][GATT]SCARD_ATTR_ATR_STRING"); BYTE ATR[100]; DWORD ATRsize; if (!QueryATR(ATR,&ATRsize)) { SectionLocker lock(device->m_RequestLock); pRequest->CompleteWithInformation(STATUS_NO_MEDIA, 0); return; } setBuffer(device,pRequest,ATR,ATRsize); return; case SCARD_ATTR_CURRENT_PROTOCOL_TYPE: OutputDebugString(L"[BixVReader][GATT]SCARD_ATTR_CURRENT_PROTOCOL_TYPE"); setInt(device,pRequest,protocol); // T=0 or T=1 return; default: { swprintf(log,L"[BixVReader][GATT]ERROR_NOT_SUPPORTED:%08X",code); OutputDebugString(log); SectionLocker lock(device->m_RequestLock); pRequest->CompleteWithInformation(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED), 0); } } }
要读取这些配置值,您需要以 SCARD_DIRECT 模式连接到读卡器并发送一些 SCardGetAttribute 命令。此代码应返回第一个虚拟读卡器使用的 RpcType:
SCardConnect(hContext,"Bix VIRTUAL_CARD_READER 0",SCARD_SHARE_DIRECT, 0,&hCard, &dwProtocol); DWORD iRpcType; DWORD iRpcTypeLen=sizeof(DWORD); SCardGetAttrib(hCard,SCARD_ATTR_VALUE(SCARD_CLASS_VENDOR_DEFINED, 0xA009), (LPBYTE)&iRpcType, &iRpcTypeLen);
SCardGetAttrib
返回的自定义属性数据类型如下:
- RPC_PORT_BASE: DWORD
- RPC_TYPE: DWORD
- VENDOR_NAME:可变长度字符串
- VENDOR_IFD_TYPE:可变长度字符串
- DEVICE_UNIT: DWORD
- PIPE_NAME:可变长度字符串
- PIPE_EVENT_NAME:可变长度字符串
- TCP_PORT: DWORD
- TCP_EVENT_PORT: DWORD
IOCTL_SMARTCARD_IS_PRESENT 和 IOCTL_SMARTCARD_IS_ABSENT
这有点棘手。我们被问及读卡器中是否有智能卡。这是代码
void Reader::IoSmartCardIsPresent(IWDFIoRequest* pRequest,SIZE_T inBufSize,SIZE_T outBufSize) {
UNREFERENCED_PARAMETER(inBufSize);
UNREFERENCED_PARAMETER(outBufSize);
OutputDebugString(L"[BixVReader][IPRE]IOCTL_SMARTCARD_IS_PRESENT");
if (CheckATR()) {
// there's a smart card present, so complete the request
SectionLocker lock(device->m_RequestLock);
pRequest->CompleteWithInformation(STATUS_SUCCESS, 0);
}
else {
// there's no smart card present, so leave the request pending; it will be completed later
SectionLocker lock(device->m_RequestLock);
waitInsertIpr=pRequest;
IRequestCallbackCancel *callback;
device->QueryInterface(__uuidof(IRequestCallbackCancel),(void**)&callback);
pRequest->MarkCancelable(callback);
callback->Release();
}
}
我们尝试与虚拟卡通信以询问其ATR;如果请求成功,则读卡器中有一张卡,否则没有。在第一种情况下,我们只需完成I/O请求以确认卡存在。
在第二种情况下,我们实际上开始监控读卡器是否插入了卡。I/O 请求被挂起(如果我们不调用 `CompleteWithInformation`,UMDF 框架会自动处理挂起的请求),并且一旦卡被插入就会完成。我们只是将指向挂起请求的指针存储在 `waitInsertIpr` 中,以记住此请求仍处于打开状态。
此外,我们应该调用 `pRequest->MarkCancelable` 来通知框架此请求是可取消的(以防设备停用或系统关闭)。`CMyDevice` 实现了 `IRequestCallbackCancel`,因此它可以收到删除请求的通知。
对于 `IOCTL_SMARTCARD_IS_ABSENT`,情况恰好相反:当智能卡被移除时,I/O 请求完成。
IOCTL_SMARTCARD_GET_STATE
我们被查询设备状态。在我们的案例中,这非常容易:我们只支持两种状态:无卡和有卡且协议已协商。在真正的驱动程序中,我们当然应该处理更精确的状态。我们只需询问虚拟卡 ATR 来检查它是否存在。
IOCTL_SMARTCARD_POWER
卡应被重置或断电。在重置的情况下,我们还会返回 ATR(如果虚拟卡存在)。
IOCTL_SMARTCARD_SET_ATTRIBUTE
我们可以直接忽略它。如果设置了 `SCARD_ATTR_DEVICE_IN_USE` 参数,我们只返回 `SUCCESS`。
IOCTL_SMARTCARD_TRANSMIT
APDU 命令应该发送到智能卡,并且我们应该返回响应。这并不困难,只是与虚拟智能卡处理进程进行通信的一些操作。我们应该记住在 APDU 之前移除 `SCARD_IO_REQUEST` 结构,并在响应之前插入它。
与虚拟智能卡进程的 IPC
显然,驱动程序不能有用户界面。但是,如果我需要更改虚拟智能卡的行为,也许加载和保存其状态,或者只是模拟它从虚拟读卡器中的插入和移除,我绝对需要一个用户界面来完成这些操作!因此,虚拟读卡器驱动程序应该愉快地与外部世界通信,向一个模拟虚拟智能卡行为的进程发送请求和接收响应。但是——因为总是有但是——也许我们应该记住,驱动程序,即使是用户模式驱动程序,也并非简单的应用程序。在这种情况下,问题在于这个应用程序存在于会话 0 中,与会话 1 世界的其他部分隔离。
我不会详细解释会话 0 和 1 的概念,以及 Vista 及更高版本操作系统中会话 0 的隔离(因为我也不是这方面的权威)。我只想说,会话 0 和会话 1 进程无法通过某些 IPC 模式进行通信:没有线程和窗口消息,没有内存映射文件,也没有全局名称,因此没有共享同步对象(我也不希望会话 1 应用程序被提升)……所以,我看到两种替代方案
- 命名管道
- TCP/IP
更新
在本文的第一个版本中,只实现了命名管道。现在我实现了两种。它们具有几乎相同的行为,因此,根据您的领域,您可以拥有两种替代方案。
首先,我们来看管道服务器函数
DWORD PipeReader::startServer() {
SECURITY_ATTRIBUTES sa;
sa.nLength = sizeof(SECURITY_ATTRIBUTES);
sa.bInheritHandle = FALSE;
CreateMyDACL(&sa);
wchar_t temp[300];
swprintf(temp,L"\\\\.\\pipe\\%s",pipeName);
HANDLE _pipe=CreateNamedPipe(temp,PIPE_ACCESS_DUPLEX|FILE_FLAG_OVERLAPPED,PIPE_TYPE_BYTE,PIPE_UNLIMITED_INSTANCES,0,0,0,&sa);
swprintf(temp,L"\\\\.\\pipe\\%s",pipeEventName);
HANDLE _eventpipe=CreateNamedPipe(temp,PIPE_ACCESS_DUPLEX|FILE_FLAG_OVERLAPPED,PIPE_TYPE_BYTE,PIPE_UNLIMITED_INSTANCES,0,0,0,&sa);
wchar_t log[300];
swprintf(log,L"[BixVReader]Pipe created:%s:%08x",pipeName,_pipe);
OutputDebugString(log);
while (true) {
BOOL ris=ConnectNamedPipe(_pipe,NULL);
if (ris==0) {
swprintf(log,L"[BixVReader]Pipe NOT connected:%x",GetLastError());
OutputDebugString(log);
}
else {
swprintf(log,L"[BixVReader]Pipe connected");
OutputDebugString(log);
}
ris=ConnectNamedPipe(_eventpipe,NULL);
if (ris==0) {
swprintf(log,L"[BixVReader]Event Pipe NOT connected:%x",GetLastError());
OutputDebugString(log);
}
else {
swprintf(log,L"[BixVReader]Event Pipe connected");
OutputDebugString(log);
}
pipe=_pipe;
eventpipe=_eventpipe;
if (waitInsertIpr!=NULL) {
SectionLocker lock(device->m_RequestLock);
// if I'm waiting for card insertion, verify if there's a card present
if (initProtocols()) {
if (waitInsertIpr->UnmarkCancelable()==S_OK)
waitInsertIpr->CompleteWithInformation(STATUS_SUCCESS, 0);
waitInsertIpr=NULL;
state=SCARD_SWALLOWED;
}
}
while (true) {
// wait for a command
DWORD command=0;
DWORD read=0;
if (!ReadFile(eventpipe,&command,sizeof(DWORD),&read,NULL)) {
state=SCARD_ABSENT;
OutputDebugString(L"[BixVReader]Pipe error");
powered=0;
pipe=NULL;
eventpipe=NULL;
if (waitRemoveIpr!=NULL) {// card inserted
SectionLocker lock(device->m_RequestLock);
OutputDebugString(L"[BixVReader]complete Wait Remove");
if (waitRemoveIpr->UnmarkCancelable()==S_OK) {
OutputDebugString(L"[BixVReader]Wait Remove Unmarked");
waitRemoveIpr->CompleteWithInformation(STATUS_SUCCESS, 0);
OutputDebugString(L"[BixVReader]Wait Remove Completed");
}
waitRemoveIpr=NULL;
}
if (waitInsertIpr!=NULL) {// card removed
SectionLocker lock(device->m_RequestLock);
OutputDebugString(L"[BixVReader]cancel Wait Remove");
if (waitInsertIpr->UnmarkCancelable()==S_OK) {
OutputDebugString(L"[BixVReader]Wait Insert Unmarked");
waitInsertIpr->CompleteWithInformation(HRESULT_FROM_WIN32(ERROR_CANCELLED), 0);
OutputDebugString(L"[BixVReader]Wait Insert Cancelled");
}
waitInsertIpr=NULL;
}
DisconnectNamedPipe(_pipe);
DisconnectNamedPipe(_eventpipe);
break;
}
OutputDebugString(L"[BixVReader]Pipe data");
if (command==0)
powered=0;
if (command==0 && waitRemoveIpr!=NULL) {// card removed
SectionLocker lock(device->m_RequestLock);
state=SCARD_ABSENT;
if (waitRemoveIpr->UnmarkCancelable()==S_OK) {
waitRemoveIpr->CompleteWithInformation(STATUS_SUCCESS, 0);
}
waitRemoveIpr=NULL;
}
else if (command==1 && waitInsertIpr!=NULL) {// card inserted
SectionLocker lock(device->m_RequestLock);
state=SCARD_SWALLOWED;
initProtocols();
if (waitInsertIpr->UnmarkCancelable()==S_OK) {
waitInsertIpr->CompleteWithInformation(STATUS_SUCCESS, 0);
}
waitInsertIpr=NULL;
}
}
}
OutputDebugString(L"[BixVReader]Pipe quit!!!");
return 0;
}
TcpIp 服务器几乎相同
DWORD TcpIpReader::startServer() { breakSocket=false; wchar_t log[300]; while (true) { fd_set readfds; FD_ZERO(&readfds); // Set server socket to set FD_SET(socket, &readfds); // Timeout parameter timeval tv = { 0 }; tv.tv_sec = 5; while(true) { if (breakSocket) return 0; FD_SET(socket, &readfds); int ret = select(0, &readfds, NULL, NULL, &tv); if (ret > 0) break; if (ret<0) { wchar_t log[100]; DWORD err=WSAGetLastError(); swprintf(log,L"[BixVReader]wsa err:%x",err); OutputDebugString(log); if (err==0x2736) { socket=WSASocket(AF_INET,SOCK_STREAM,IPPROTO_TCP,NULL,0,0); sockaddr_in Service; Service.sin_family = AF_INET; Service.sin_addr.s_addr = inet_addr("127.0.0.1"); Service.sin_port = htons((u_short)(port)); bind(socket, (SOCKADDR *) &Service, sizeof (Service)); listen(socket, 1); FD_ZERO(&readfds); // Set server socket to set FD_SET(socket, &readfds); } } } SOCKET AcceptEventSocket; AcceptSocket = accept(socket, NULL, NULL); closesocket(socket); socket=NULL; if (AcceptSocket == INVALID_SOCKET) return 0; swprintf(log,L"[BixVReader]Socket connected:%i",AcceptSocket); OutputDebugString(log); FD_ZERO(&readfds); // Set server socket to set FD_SET(eventsocket, &readfds); while(true) { if (breakSocket) return 0; FD_SET(eventsocket, &readfds); int ret = select(0, &readfds, NULL, NULL, &tv); if (ret > 0) break; if (ret<0) { DWORD err=WSAGetLastError(); swprintf(log,L"[BixVReader]wsa err:%x",err); OutputDebugString(log); if (err==0x2736) { eventsocket=WSASocket(AF_INET,SOCK_STREAM,IPPROTO_TCP,NULL,0,0); sockaddr_in eventService; eventService.sin_family = AF_INET; eventService.sin_addr.s_addr = inet_addr("127.0.0.1"); eventService.sin_port = htons((u_short)(eventPort)); bind(eventsocket, (SOCKADDR *) &eventService, sizeof (eventService)); listen(eventsocket, 1); FD_ZERO(&readfds); // Set server socket to set FD_SET(eventsocket, &readfds); } } } AcceptEventSocket = accept(eventsocket, NULL, NULL); closesocket(eventsocket); eventsocket=NULL; if (AcceptEventSocket == INVALID_SOCKET) return 0; swprintf(log,L"[BixVReader]Event Socket connected:%i",AcceptEventSocket); OutputDebugString(log); if (waitInsertIpr!=NULL) { // if I'm waiting for card insertion, verify if there's a card present if (initProtocols()) { SectionLocker lock(device->m_RequestLock); if (waitInsertIpr->UnmarkCancelable()==S_OK) waitInsertIpr->CompleteWithInformation(STATUS_SUCCESS, 0); waitInsertIpr=NULL; state=SCARD_SWALLOWED; } } while (true) { // wait for a command DWORD command=0; int read=0; if ((read=recv(AcceptEventSocket,(char*)&command,sizeof(DWORD),MSG_WAITALL))<=0) { state=SCARD_ABSENT; OutputDebugString(L"[BixVReader]Socket error"); powered=0; ::shutdown(AcceptSocket,SD_BOTH); ::shutdown(AcceptEventSocket,SD_BOTH); if (waitRemoveIpr!=NULL) {// card inserted OutputDebugString(L"[BixVReader]complete Wait Remove"); SectionLocker lock(device->m_RequestLock); if (waitRemoveIpr->UnmarkCancelable()==S_OK) { OutputDebugString(L"[BixVReader]Wait Remove Unmarked"); waitRemoveIpr->CompleteWithInformation(STATUS_SUCCESS, 0); OutputDebugString(L"[BixVReader]Wait Remove Completed"); } waitRemoveIpr=NULL; } if (waitInsertIpr!=NULL) {// card removed OutputDebugString(L"[BixVReader]cancel Wait Remove"); SectionLocker lock(device->m_RequestLock); if (waitInsertIpr->UnmarkCancelable()==S_OK) { OutputDebugString(L"[BixVReader]Wait Insert Unmarked"); waitInsertIpr->CompleteWithInformation(HRESULT_FROM_WIN32(ERROR_CANCELLED), 0); OutputDebugString(L"[BixVReader]Wait Insert Cancelled"); } waitInsertIpr=NULL; } break; } OutputDebugString(L"[BixVReader]Socket data"); if (command==0) powered=0; if (command==0 && waitRemoveIpr!=NULL) {// card removed SectionLocker lock(device->m_RequestLock); state=SCARD_ABSENT; if (waitRemoveIpr->UnmarkCancelable()==S_OK) waitRemoveIpr->CompleteWithInformation(STATUS_SUCCESS, 0); waitRemoveIpr=NULL; } else if (command==1 && waitInsertIpr!=NULL) {// card inserted SectionLocker lock(device->m_RequestLock); state=SCARD_SWALLOWED; initProtocols(); if (waitInsertIpr->UnmarkCancelable()==S_OK) waitInsertIpr->CompleteWithInformation(STATUS_SUCCESS, 0); waitInsertIpr=NULL; } } } OutputDebugString(L"[BixVReader]Socket quit!!!"); return 0; }
首先,我们创建两个命名管道(或 Tcp/Ip 套接字)。为什么是两个?很简单:第一个(`SCardSimulatorDriver`)用于从驱动程序到虚拟智能卡的请求;第二个(`SCardSimulatorDriverEvents`)用于从虚拟智能卡到驱动程序的事件通知(插入和移除)。
使用 `ConnectNamedPipe`,我们等待客户端连接。调用是阻塞的,因此在有人打开管道之前,线程会一直等待。
实用函数 `CreateMyDACL`(直接来自 MSDN)用于为命名管道设置适当的 DACL。实际上,如果我们使用 `NULL` 作为 DACL,对象将继承父进程的默认设置,并且客户端应用程序将无法访问。我们的自定义 DACL 授予经过身份验证的用户访问权限,因此所有用户进程都可以连接。
在 TCP/IP 的情况下,客户端连接后我们会立即关闭监听套接字,因为这是一个单客户端场景。
当连接建立时,我们检查是否已插入卡且正在监控(`waitInsertIpr` 和 `CheckATR()`);在这种情况下,虚拟卡不一定会通知驱动程序,因为没有插入事件。驱动程序会继续认为没有插入卡。
更新
该驱动程序的第一个版本只支持 T=1 协议。现在它解析 ATR 以查找定义接受协议的 TDi 字节。因此,当客户端连接时,我们请求其 ATR 以了解特定协议是否可用。
然后主通信循环开始。驱动程序线程通过调用 `ReadFile` 等待来自虚拟智能卡的事件。此管道上传输的数据很简单:一个值为 `0` 表示移除,`1` 表示插入的单个 `DWORD`。当事件到达时,如果我们正在等待该事件(`waitRemoveIpr` 或 `waitInsertIpr`),我们将相应地完成该 I/O 请求。由于这些请求被标记为可取消,我们需要使用 `UnmarkCancelable` 取消标记它们,如果可能,完成它们(如果为时已晚且请求已被取消,`UnmarkCancelable` 可能会失败)(请注意,在 `OnCancel` 回调中,我们不需要调用 `UnmarkCancelable`)。
如果 `ReadFile` 调用失败,则表示我们与虚拟卡的连接已断开。也许应用程序已关闭,因此管道(或套接字)也已关闭。在这种情况下,我们只需通知卡的移除(如果需要),然后重新开始等待新的客户端连接。
让我们看看当驱动程序需要向虚拟智能卡发送请求时会发生什么;`QueryTransmit` 方法完成此工作
bool PipeReader::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=0;
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;
}
首先,我们检查管道是否已连接;否则,返回失败。然后我们将 APDU 命令写入管道。协议非常简单
- 一个包含命令代码 (
TRANSMIT=2
) 的DWORD
- 一个包含 APDU 长度的
DWORD
- APDU 缓冲区
我们不需要更多了……我们只需等待响应到来。我们读取一个包含响应长度的 `DWORD` 和响应缓冲区。就是这样。如果管道中的任何操作失败,则通信可能已中断,因此我们返回失败并等待新的连接到来。
每次向管道写入内容时,都应始终调用 `FlushFileBuffers`,以确保缓冲区被发送到管道的另一端,而不是被缓冲;否则,监听应用程序可能无法接收我们的数据。
注释
这种 IPC 实现非常简单。有点太简单了。I/O 请求是同步的,但从虚拟智能卡接收到的事件不是。所以,在将管道句柄设置为 `NULL` 之前,明智的做法是获取一个锁……我懒惰,它 99% 的时间都有效,我只是为了测试目的需要它……所以我没有这样做。我真羞愧。
更新:我不再懒惰了!
虚拟智能卡应用程序
更新
在本文更新期间,我编写了一个完整的 ISO7816 虚拟智能卡 .net 实现。
由于虚拟智能卡项目相当复杂,我决定将其独立成一篇不同的文章。文章正在起草中,但我希望尽快发布。
编译、安装和调试
毫不意外,要编译驱动程序,您需要安装 WDK 7.1。只需从 Microsoft 下载 620MB,我们就可以构建驱动程序了。编译驱动程序与编译普通 DLL 并不完全相同。例如,子系统不是 WINDOWS 而是 NATIVE;并且库路径应根据驱动程序编译的体系结构进行相应设置。这就是为什么我们没有 Visual Studio 解决方案,而是将使用 WDK 随附的已正确配置的构建环境,从命令行进行构建。如果您正确安装了 WDK,在开始菜单中,您将看到各种环境的链接:开始 > 程序 > Windows 驱动程序工具包 > WDK 7600.16385.1 > 构建环境 > %OS 版本% > %TargetCPU% Free/Checked 构建环境。
Checked Build 环境类似于 Debug 构建:优化被关闭,并且包含用于调试的条件代码。
Free Build 类似于 Release 构建:代码经过优化,调试代码被禁用。
请注意,即使在 Free Build 中,您也可以使用用户模式调试器来逐步执行代码。
要编译虚拟驱动程序,请打开正确的构建环境,`cd` 到包含代码和正确 OS 版本子目录的目录(目前已在 Win 7 和 Win XP 上测试 - `sources` 文件的设置略有不同),然后只需键入 `build`。
好了,DLL 构建完成了!...只要没有编译错误。请注意,在这些构建环境中,**警告被视为错误!**。它们不会由 `build` 工具报告,但会转储到一个文件中,例如,如果您正在为 windows 7 x64 Free Build 构建驱动程序,则文件名为 "buildfre_win7_amd64.wrn"。
要配置 `build` 工具,请修改 `sources` 文件,根据需要添加新的源文件或要链接的库。
驱动程序构建完成后,您可以在文件夹(如果是 Win7,x64,Free)`objfre_win7_amd64\amd64` 中找到 DLL,以及用于安装的 .inf 文件。
要安装驱动程序,我们将使用 WDK 随附的 DevCon 实用程序(位于 ` %WinDDK%\tools\devcon\%Architecture%`)。此外,我们需要将 `WUDFUpdate_01009.dll` 文件(位于 ` %WinDDK%\redist\wdf\%Architecture%`)复制到我们 DLL 所在的路径。
要安装驱动程序,请从提升的 shell 运行:
devcon install BixVReader.inf root\BixVirtualReader
devcon install
的语法是
devcon [-r] install <inf> <hwid>
其中 `
%ReaderDeviceName%=VReader_Install,root\BixVirtualReader
“-r”开关仅在需要重启时才请求重启。
如果安装成功,由于驱动程序未签名,用户会被要求确认,然后虚拟读卡器就会安装成功。

您可以下载包含 Windows 7 32 位和 64 位以及 Windows XP 编译二进制文件的 zip 存档。在 zip 文件中,您可以找到 DLL 和 inf 文件,但您需要适用于您架构的 devcon.exe(它不可再分发,但属于 WDK)。该驱动程序仅在 Win7 x64 下测试过。
如果您启动虚拟智能卡应用程序,您应该能够连接到虚拟卡。在 Win7 中,一旦您插入卡,一些微型驱动程序将尝试与卡通信,发送各种命令(只要驱动程序只模拟一个读卡器。请参阅此处“与即插即用不兼容的智能卡解决方案” - 多槽智能卡读卡器只为智能卡读卡器中的所有可用槽位创建一个设备节点。这正是微型驱动程序的情况)。由于卡总是回答 9000(即“OK”),微型驱动程序可能会被欺骗以匹配它,您将在设备管理器中看到一张已知的卡!在应用程序的日志 `listbox` 中,您将看到发送到卡的 APDU。
调试驱动程序实际上非常简单:由于它是用户模式驱动程序,您可以使用用户模式调试器:例如 Visual Studio。您所要做的就是附加到正在运行的进程,然后选择 `WUDFHost.exe`。由于您的系统中可能存在多个 UMDF 驱动程序,因此也可能存在多个 `WUDFHost.exe` 实例。哪个是正确的呢?您可以使用 Sysinternals 的 Procexp 搜索哪些进程加载了您的 DLL,并检查正确 `WUDFHost.exe` 的 PID。请记住,Procexp 和 Visual Studio 都应以管理员身份运行,否则它们将无法附加到驱动程序进程。
当您需要重新编译和重新测试驱动程序时,您需要更新驱动程序 DLL 文件。
您可以简单地从用于安装它的同一个 shell 启动命令来完成:
devcon update BixVReader.inf root\BixVirtualReader
从用于安装它的相同 shell。
请注意,设备将被禁用、更新和重新启用,但如果存在挂起的 I/O 请求,系统将无法禁用它,您需要重新启动系统才能使更改生效。
在这种情况下,您可以执行以下操作:
- 打开设备管理器
- 禁用虚拟设备
- 如果仍有挂起的 I/O 请求,请终止 `WUDFHost.exe` 进程(托管 `BixVReader.dll` 的进程)
- 使用上述命令行更新驱动程序
- 启用设备
设备一经启用,驱动程序即被加载并执行。如果您需要从一开始就调试驱动程序,可以设置注册表项
`HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\WUDF\Services\{193a1820-d9ac-4997-8c55-be817523f6aa}/HostProcessDbgBreakOnStart`,将其设置为您希望主机进程在启动前等待调试器连接的秒数。
出于调试目的,从驱动程序获取跟踪或某种输出是很有用的。尽管 WDK 具有内置的跟踪系统(通过 WPP 跟踪),但我更喜欢使用简单的 `OutputDebugString`。只是为了开发速度,我更喜欢使用一种众所周知的方法,而不是学习一种新的方法,但这只是个人喜好……使用 Dbgview(同样来自 Sysinternals,以管理员身份运行),或附加调试器,您可以轻松地看到驱动程序的跟踪。
.INF 文件
用于安装虚拟设备的 .INF 文件几乎与 `UMDFSkeleton` 示例中的相同。只添加了一行
UmdfKernelModeClientPolicy=AllowKernelModeClients
为了允许内核模式驱动程序加载在用户模式驱动程序之上,并将请求从内核模式传递给用户模式驱动程序。
我不太确定哪个内核模式驱动程序在虚拟读卡器驱动程序之上运行,但从 inf 文件中删除此行后,我们在 Queue 对象中根本不会收到任何 I/O 请求通知。
结论
正如我前面所说,这绝不是一篇关于如何制作驱动程序的文章。我很确定一个经验丰富的驱动程序开发人员看到我的代码会尖叫。这只是一个开发工具,适用于像我一样经常使用智能卡并需要“玩弄”它们的人。我希望它能对某人有用,当然我也在等待一些认真的驱动程序开发人员告诉我它应该如何完成。:)