NDIS驱动学习笔记

Windows内核原理与实现之 NDIS(网络驱动程序接口规范) 文章摘录自《Windows内核原理与实现》一书。 在Windows的网络栈中,网络协议与网络适配器是分离的,协议驱动程序并不针对特定的网络适配器而设计,然而,当协议驱动程序正真运行时,它必须通过一个网络适配器才能发送和接收数据。协议驱动程序通过统一的接口与网络适配器驱动程序进行通信,此接口即为 Microsoft 和 3COM 于 1989 年联手开发的NDIS(Network Driver I... 阅读详情
by  ufphpc

从Ndis Intermediate Miniport driver说吧,参考passthru

NTSTATUS
DriverEntry(
    IN    PDRIVER_OBJECT        DriverObject,
    IN    PUNICODE_STRING        RegistryPath
    )
程序入口没什么好说的 而NDIS不符合WDM往下到了

NdisMInitializeWrapper(&NdisWrapperHandle, DriverObject, RegistryPath, NULL);
在DriverEntry里调用NdisMInitializeWrapper函数初始包裹来使得微端口驱动和NDIS相联系。它返回一个NdisWrapperHandle句柄,它是代表这个微端口驱动程序结构的句柄。

然而NdisMInitializeWrapper到底作了什么呢?我们看REACTOS里这个函数

EXPORT
NdisInitializeWrapper(
    OUT PNDIS_HANDLE    NdisWrapperHandle,
    IN  PVOID          SystemSpecific1,
    IN  PVOID          SystemSpecific2,
    IN  PVOID          SystemSpecific3)
/*
* FUNCTION: Notifies the NDIS library that a new miniport is initializing
* ARGUMENTS:
*    NdisWrapperHandle = Address of buffer to place NDIS wrapper handle
*    SystemSpecific1  = Pointer to the driver's driver object
*    SystemSpecific2  = Pointer to the driver's registry path
*    SystemSpecific3  = Always NULL
* NOTES:
*    - SystemSpecific2 goes invalid so we copy it
*/
{
  PNDIS_M_DRIVER_BLOCK Miniport;
  PUNICODE_STRING RegistryPath;
  WCHAR *RegistryBuffer;

  NDIS_DbgPrint(MAX_TRACE, ("Called./n"));

  ASSERT(NdisWrapperHandle);

  *NdisWrapperHandle = NULL;

#if BREAK_ON_MINIPORT_INIT
  __asm__ ("int $3/n");
#endif

  Miniport = ExAllocatePool(NonPagedPool, sizeof(NDIS_M_DRIVER_BLOCK));

  if (!Miniport)
    {
      NDIS_DbgPrint(MIN_TRACE, ("Insufficient resources./n"));
      return;
    }

  RtlZeroMemory(Miniport, sizeof(NDIS_M_DRIVER_BLOCK));

  KeInitializeSpinLock(&Miniport->Lock);

  Miniport->DriverObject = (PDRIVER_OBJECT)SystemSpecific1;

  /* set the miniport's driver registry path */
  RegistryPath = ExAllocatePool(PagedPool, sizeof(UNICODE_STRING));
  if(!RegistryPath)
    {
      NDIS_DbgPrint(MIN_TRACE, ("Insufficient resources./n"));
      return;
    }

  RegistryPath->Length = ((PUNICODE_STRING)SystemSpecific2)->Length;
  RegistryPath->MaximumLength = RegistryPath->Length + sizeof(WCHAR);    /* room for 0-term */

  RegistryBuffer = ExAllocatePool(PagedPool, RegistryPath->MaximumLength);
  if(!RegistryBuffer)
    {
      NDIS_DbgPrint(MIN_TRACE, ("Insufficient resources./n"));
      return;
    }

  RtlCopyMemory(RegistryBuffer, ((PUNICODE_STRING)SystemSpecific2)->Buffer, RegistryPath->Length);
  RegistryBuffer[RegistryPath->Length/sizeof(WCHAR)] = 0;

  RegistryPath->Buffer = RegistryBuffer;
  Miniport->RegistryPath = RegistryPath;

  InitializeListHead(&Miniport->DeviceList);

  /* Put miniport in global miniport list */
  ExInterlockedInsertTailList(&MiniportListHead, &Miniport->ListEntry, &MiniportListLock);

  *NdisWrapperHandle = Miniport;
}

它仅初始化了一个NDIS_M_DRIVER_BLOCK结构Miniport,并把他插入链表并返回这个结构
让我们看看它的结构
typedef struct _NDIS_M_DRIVER_BLOCK {
    LIST_ENTRY                      ListEntry;                /* Entry on global list */
    KSPIN_LOCK                      Lock;                    /* Protecting spin lock */
    NDIS_MINIPORT_CHARACTERISTICS  MiniportCharacteristics;  /* Miniport characteristics */
    WORK_QUEUE_ITEM                WorkItem;                /* Work item */
    PDRIVER_OBJECT                  DriverObject;            /* Driver object of miniport */
    LIST_ENTRY                      DeviceList;              /* Adapters created by miniport */
    PUNICODE_STRING                RegistryPath;            /* SCM Registry key */
} NDIS_M_DRIVER_BLOCK, *PNDIS_M_DRIVER_BLOCK;

注 意有一个 NDIS_MINIPORT_CHARACTERISTICS结构,我们下面必须初始化这一结构, NDIS_MINIPORT_CHARACTERISTICS结构指定了与微端口兼容的NDIS版本以及由微端口提供的可选上层函数 (MINIPORTXXX)。然后DriverEntry通过一个指向NDIS_MINIPORT_CHARACTERISTICS的指针调用 NdisIMRegisterLayeredMiniport或NdisMRegisterMiniport


        NdisZeroMemory(&MChars, sizeof(NDIS_MINIPORT_CHARACTERISTICS));

        MChars.MajorNdisVersion = PASSTHRU_MAJOR_NDIS_VERSION;
        MChars.MinorNdisVersion = PASSTHRU_MINOR_NDIS_VERSION;

        MChars.InitializeHandler = MPInitialize;
        MChars.QueryInformationHandler = MPQueryInformation;
        MChars.SetInformationHandler = MPSetInformation;
        MChars.ResetHandler = MPReset;
        MChars.TransferDataHandler = MPTransferData;
        MChars.HaltHandler = MPHalt;
#ifdef NDIS51_MINIPORT
        MChars.CancelSendPacketsHandler = MPCancelSendPackets;
        MChars.PnPEventNotifyHandler = MPDevicePnPEvent;
        MChars.AdapterShutdownHandler = MPAdapterShutdown;
#endif // NDIS51_MINIPORT

        //
        // We will disable the check for hang timeout so we do not
        // need a check for hang handler!
        //
        MChars.CheckForHangHandler = NULL;
        MChars.ReturnPacketHandler = MPReturnPacket;

        //
        // Either the Send or the SendPackets handler should be specified.
        // If SendPackets handler is specified, SendHandler is ignored
        //
        MChars.SendHandler = NULL;    // MPSend;
        MChars.SendPacketsHandler = MPSendPackets;

        Status = NdisIMRegisterLayeredMiniport(NdisWrapperHandle,
                                                 &MChars,
                                                 sizeof(MChars),
                                                  &DriverHandle);

我们来看看NdisMRegisterMiniport做了什么


NDIS_STATUS
EXPORT
NdisMRegisterMiniport(
    IN  NDIS_HANDLE                    NdisWrapperHandle,
    IN  PNDIS_MINIPORT_CHARACTERISTICS  MiniportCharacteristics,
    IN  UINT                            CharacteristicsLength)
/*
* FUNCTION: Registers a miniport's MiniportXxx entry points with the NDIS library
* ARGUMENTS:
*    NdisWrapperHandle      = Pointer to handle returned by NdisMInitializeWrapper
*    MiniportCharacteristics = Pointer to a buffer with miniport characteristics
*    CharacteristicsLength  = Number of bytes in characteristics buffer
* RETURNS:
*    Status of operation
*/
{
  UINT MinSize;
  PNDIS_M_DRIVER_BLOCK Miniport = GET_MINIPORT_DRIVER(NdisWrapperHandle);
  PNDIS_M_DRIVER_BLOCK *MiniportPtr;
  NTSTATUS Status;

  NDIS_DbgPrint(MAX_TRACE, ("Called./n"));

  switch (MiniportCharacteristics->MajorNdisVersion)
    {
      case 0x03:
        MinSize = sizeof(NDIS30_MINIPORT_CHARACTERISTICS);
        break;

      case 0x04:
        MinSize = sizeof(NDIS40_MINIPORT_CHARACTERISTICS);
        break;

      case 0x05:
        MinSize = sizeof(NDIS50_MINIPORT_CHARACTERISTICS);
        break;

      default:
        NDIS_DbgPrint(DEBUG_MINIPORT, ("Bad miniport characteristics version./n"));
        return NDIS_STATUS_BAD_VERSION;
    }

  if (CharacteristicsLength < MinSize)
    {
        NDIS_DbgPrint(DEBUG_MINIPORT, ("Bad miniport characteristics./n"));
        return NDIS_STATUS_BAD_CHARACTERISTICS;
    }

  /* Check if mandatory MiniportXxx functions are specified */
  if ((!MiniportCharacteristics->HaltHandler) ||
      (!MiniportCharacteristics->InitializeHandler)||
      (!MiniportCharacteristics->QueryInformationHandler) ||
      (!MiniportCharacteristics->ResetHandler) ||
      (!MiniportCharacteristics->SetInformationHandler))
    {
      NDIS_DbgPrint(DEBUG_MINIPORT, ("Bad miniport characteristics./n"));
      return NDIS_STATUS_BAD_CHARACTERISTICS;
    }

  if (MiniportCharacteristics->MajorNdisVersion == 0x03)
    {
      if (!MiniportCharacteristics->SendHandler)
        {
          NDIS_DbgPrint(DEBUG_MINIPORT, ("Bad miniport characteristics./n"));
          return NDIS_STATUS_BAD_CHARACTERISTICS;
        }
    }
  else if (MiniportCharacteristics->MajorNdisVersion >= 0x04)
    {
      /* NDIS 4.0+ */
      if ((!MiniportCharacteristics->SendHandler) &&
          (!MiniportCharacteristics->SendPacketsHandler))
        {
          NDIS_DbgPrint(DEBUG_MINIPORT, ("Bad miniport characteristics./n"));
          return NDIS_STATUS_BAD_CHARACTERISTICS;
        }
    }

  /* TODO: verify NDIS5 and NDIS5.1 */

  RtlCopyMemory(&Miniport->MiniportCharacteristics, MiniportCharacteristics, MinSize);

  /*
  * NOTE: This is VERY unoptimal! Should we store the NDIS_M_DRIVER_BLOCK
  * structure in the driver extension or what?
  */

  Status = IoAllocateDriverObjectExtension(Miniport->DriverObject, (PVOID)TAG('D','I','M','N'),
                                          sizeof(PNDIS_M_DRIVER_BLOCK), (PVOID*)&MiniportPtr);
  if (!NT_SUCCESS(Status))
    {
      NDIS_DbgPrint(DEBUG_MINIPORT, ("Can't allocate driver object extension./n"));
      return NDIS_STATUS_RESOURCES;
    }

  *MiniportPtr = Miniport;

  Miniport->DriverObject->MajorFunction[IRP_MJ_PNP] = NdisIDispatchPnp;
  Miniport->DriverObject->DriverExtension->AddDevice = NdisIAddDevice;

  return NDIS_STATUS_SUCCESS;
}

首 先检查NDIS_MINIPORT_CHARACTERISTICS结构的有效性,然后调用了 IoAllocateDriverObjectExtension分配了DriverObjectExtension然后就是设置PNP例程和 AddDevice例程,当有网卡插入设备时创建设备,让我们看看NdisIAddDevice
NTSTATUS
NTAPI
NdisIAddDevice(
    IN PDRIVER_OBJECT DriverObject,
    IN PDEVICE_OBJECT PhysicalDeviceObject)
/*
* FUNCTION: Create a device for an adapter found using PnP
* ARGUMENTS:
*    DriverObject        = Pointer to the miniport driver object
*    PhysicalDeviceObject = Pointer to the PDO for our adapter
*/
{
  static const WCHAR ClassKeyName[] = {'C','l','a','s','s','//'};
  static const WCHAR LinkageKeyName[] = {'//','L','i','n','k','a','g','e',0};
  PNDIS_M_DRIVER_BLOCK Miniport;
  PNDIS_M_DRIVER_BLOCK *MiniportPtr;
  WCHAR *LinkageKeyBuffer;
  ULONG DriverKeyLength;
  RTL_QUERY_REGISTRY_TABLE QueryTable[2];
  UNICODE_STRING ExportName;
  PDEVICE_OBJECT DeviceObject;
  PLOGICAL_ADAPTER Adapter;
  NTSTATUS Status;

  /*
  * Gain the access to the miniport data structure first.
  */

  MiniportPtr = IoGetDriverObjectExtension(DriverObject, (PVOID)TAG('D','I','M','N'));
  if (MiniportPtr == NULL)
    {
      NDIS_DbgPrint(DEBUG_MINIPORT, ("Can't get driver object extension./n"));
      return STATUS_UNSUCCESSFUL;
    }
  Miniport = *MiniportPtr;

  /*
  * Get name of the Linkage registry key for our adapter. It's located under
  * the driver key for our driver and so we have basicly two ways to do it.
  * Either we can use IoOpenDriverRegistryKey or compose it using information
  * gathered by IoGetDeviceProperty. I choosed the second because
  * IoOpenDriverRegistryKey wasn't implemented at the time of writing.
  */

  Status = IoGetDeviceProperty(PhysicalDeviceObject, DevicePropertyDriverKeyName,
                              0, NULL, &DriverKeyLength);
  if (Status != STATUS_BUFFER_TOO_SMALL)
    {
      NDIS_DbgPrint(DEBUG_MINIPORT, ("Can't get miniport driver key length./n"));
      return Status;
    }

  LinkageKeyBuffer = ExAllocatePool(PagedPool, DriverKeyLength +
                                    sizeof(ClassKeyName) + sizeof(LinkageKeyName));
  if (LinkageKeyBuffer == NULL)
    {
      NDIS_DbgPrint(DEBUG_MINIPORT, ("Can't allocate memory for driver key name./n"));
      return STATUS_INSUFFICIENT_RESOURCES;
    }

  Status = IoGetDeviceProperty(PhysicalDeviceObject, DevicePropertyDriverKeyName,
                              DriverKeyLength, LinkageKeyBuffer +
                              (sizeof(ClassKeyName) / sizeof(WCHAR)),
                              &DriverKeyLength);
  if (!NT_SUCCESS(Status))
    {
      NDIS_DbgPrint(DEBUG_MINIPORT, ("Can't get miniport driver key./n"));
      ExFreePool(LinkageKeyBuffer);
      return Status;
    }

  /* Compose the linkage key name. */
  RtlCopyMemory(LinkageKeyBuffer, ClassKeyName, sizeof(ClassKeyName));
  RtlCopyMemory(LinkageKeyBuffer + ((sizeof(ClassKeyName) + DriverKeyLength) /
                sizeof(WCHAR)) - 1, LinkageKeyName, sizeof(LinkageKeyName));

  NDIS_DbgPrint(DEBUG_MINIPORT, ("LinkageKey: %S./n", LinkageKeyBuffer));

  /*
  * Now open the linkage key and read the "Export" and "RootDevice" values
  * which contains device name and root service respectively.
  */

  RtlZeroMemory(QueryTable, sizeof(QueryTable));
  RtlInitUnicodeString(&ExportName, NULL);
  QueryTable[0].Flags = RTL_QUERY_REGISTRY_REQUIRED | RTL_QUERY_REGISTRY_DIRECT;
  QueryTable[0].Name = L"Export";
  QueryTable[0].EntryContext = &ExportName;

  Status = RtlQueryRegistryValues(RTL_REGISTRY_CONTROL, LinkageKeyBuffer,
                                  QueryTable, NULL, NULL);
  ExFreePool(LinkageKeyBuffer);
  if (!NT_SUCCESS(Status))
    {
      NDIS_DbgPrint(DEBUG_MINIPORT, ("Can't get miniport device name. (%x)/n", Status));
      return Status;
    }

  /*
  * Create the device object.
  */

  NDIS_DbgPrint(MAX_TRACE, ("creating device %wZ/n", &ExportName));

  Status = IoCreateDevice(Miniport->DriverObject, sizeof(LOGICAL_ADAPTER),
    &ExportName, FILE_DEVICE_PHYSICAL_NETCARD,
    0, FALSE, &DeviceObject);
  if (!NT_SUCCESS(Status))
    {
      NDIS_DbgPrint(MIN_TRACE, ("Could not create device object./n"));
      RtlFreeUnicodeString(&ExportName);
      return Status;
    }

  /*
  * Initialize the adapter structure.
  */

  Adapter = (PLOGICAL_ADAPTER)DeviceObject->DeviceExtension;
  KeInitializeSpinLock(&Adapter->NdisMiniportBlock.Lock);
  InitializeListHead(&Adapter->ProtocolListHead);
  Adapter->NdisMiniportBlock.DriverHandle = Miniport;

  Adapter->NdisMiniportBlock.MiniportName = ExportName;

  Adapter->NdisMiniportBlock.DeviceObject = DeviceObject;
  Adapter->NdisMiniportBlock.PhysicalDeviceObject = PhysicalDeviceObject;
  Adapter->NdisMiniportBlock.NextDeviceObject =
    IoAttachDeviceToDeviceStack(Adapter->NdisMiniportBlock.DeviceObject,
                                PhysicalDeviceObject);

  Adapter->NdisMiniportBlock.OldPnPDeviceState = 0;
  Adapter->NdisMiniportBlock.PnPDeviceState = NdisPnPDeviceAdded;

  KeInitializeDpc(&Adapter->NdisMiniportBlock.DeferredDpc, MiniportDpc, (PVOID)Adapter);

  DeviceObject->Flags &= ~DO_DEVICE_INITIALIZING;

  return STATUS_SUCCESS;
}

可以看到首先通过注册表得到设备名,然后创建设备对象,然后初始化了一个adapter structure,让我们看看这个结构
typedef struct _LOGICAL_ADAPTER
{
    NDIS_MINIPORT_BLOCK        NdisMiniportBlock;      /* NDIS defined fields */
    BOOLEAN                    MiniportBusy;          /* A MiniportXxx routine is executing */
    PNDIS_MINIPORT_WORK_ITEM    WorkQueueHead;          /* Head of work queue */
    PNDIS_MINIPORT_WORK_ITEM    WorkQueueTail;          /* Tail of work queue */
    LIST_ENTRY                  ListEntry;              /* Entry on global list */
    LIST_ENTRY                  MiniportListEntry;      /* Entry on miniport driver list */
    LIST_ENTRY                  ProtocolListHead;      /* List of bound protocols */
    ULONG                      MediumHeaderSize;      /* Size of medium header */
    HARDWARE_ADDRESS            Address;                /* Hardware address of adapter */
    ULONG                      AddressLength;          /* Length of hardware address */
    PUCHAR                      LookaheadBuffer;        /* Pointer to lookahead buffer */
    ULONG                      LookaheadLength;        /* Length of lookahead buffer */
    PMINIPORT_BUGCHECK_CONTEXT  BugcheckContext;        /* Adapter's shutdown handler */
} LOGICAL_ADAPTER, *PLOGICAL_ADAPTER


第一个是 NDIS_MINIPORT_BLOCK 结构,它包括的东西很多很全这里就不贴了,可以到DDK去看 哈哈。

好了按照passthru下面它应该调用NdisRegisterProtocol注册协议了,它有一个NDIS_PROTOCOL_CHARACTERISTICS结构必须先初始化,和上面那个差不多吧 哈哈

        // Now register the protocol.
        //
        NdisZeroMemory(&PChars, sizeof(NDIS_PROTOCOL_CHARACTERISTICS));
        PChars.MajorNdisVersion = PASSTHRU_PROT_MAJOR_NDIS_VERSION;
        PChars.MinorNdisVersion = PASSTHRU_PROT_MINOR_NDIS_VERSION;

        //
        // Make sure the protocol-name matches the service-name
        // (from the INF) under which this protocol is installed.
        // This is needed to ensure that NDIS can correctly determine
        // the binding and call us to bind to miniports below.
        //
        NdisInitUnicodeString(&Name, L"Passthru");    // Protocol name
        PChars.Name = Name;
        PChars.OpenAdapterCompleteHandler = PtOpenAdapterComplete;
        PChars.CloseAdapterCompleteHandler = PtCloseAdapterComplete;
        PChars.SendCompleteHandler = PtSendComplete;
        PChars.TransferDataCompleteHandler = PtTransferDataComplete;
   
        PChars.ResetCompleteHandler = PtResetComplete;
        PChars.RequestCompleteHandler = PtRequestComplete;
        PChars.ReceiveHandler = PtReceive;
        PChars.ReceiveCompleteHandler = PtReceiveComplete;
        PChars.StatusHandler = PtStatus;
        PChars.StatusCompleteHandler = PtStatusComplete;
        PChars.BindAdapterHandler = PtBindAdapter;
        PChars.UnbindAdapterHandler = PtUnbindAdapter;
        PChars.UnloadHandler = PtUnloadProtocol;

        PChars.ReceivePacketHandler = PtReceivePacket;
        PChars.PnPEventHandler= PtPNPHandler;

        NdisRegisterProtocol(&Status,
                            &ProtHandle,
                            &PChars,
                            sizeof(NDIS_PROTOCOL_CHARACTERISTICS));

可以看到在NDIS_PROTOCOL_CHARACTERISTICS中除了指定兼容版本以外还保存了强制的和非强制的ProtocolXxx函数地址。下面我们看一看NdisRegisterProtocol函数

VOID
EXPORT
NdisRegisterProtocol(
    OUT PNDIS_STATUS                    Status,
    OUT PNDIS_HANDLE                    NdisProtocolHandle,
    IN  PNDIS_PROTOCOL_CHARACTERISTICS  ProtocolCharacteristics,
    IN  UINT                            CharacteristicsLength)
/*
* FUNCTION: Registers an NDIS driver's ProtocolXxx entry points
* ARGUMENTS:
*    Status                  = Address of buffer for status information
*    NdisProtocolHandle      = Address of buffer for handle used to identify the driver
*    ProtocolCharacteristics = Pointer to NDIS_PROTOCOL_CHARACTERISTICS structure
*    CharacteristicsLength  = Size of structure which ProtocolCharacteristics targets
* NOTES:
*    - you *must* set NdisProtocolHandle before doing anything that could wind up
*      getting BindAdapterHandler, as it will probably call OpenAdapter with this handle
*    - the above implies that the initialization of the protocol block must be complete
*      by then
* TODO:
*    - break this function up - probably do a 'ndisRefreshProtocolBindings' function
*    - make this thing able to handle >1 protocol
*/
{
  PPROTOCOL_BINDING Protocol;
  NTSTATUS NtStatus;
  UINT MinSize;
  HANDLE DriverKeyHandle = NULL;
  PKEY_VALUE_PARTIAL_INFORMATION KeyInformation = NULL;
  WCHAR *DataPtr;

  NDIS_DbgPrint(MAX_TRACE, ("Called./n"));

  /* first validate the PROTOCOL_CHARACTERISTICS */
  switch (ProtocolCharacteristics->MajorNdisVersion)
    {
    case 0x03:
      /* we don't really want to support ndis3 drivers - so we complain for now */
      NDIS_DbgPrint(MID_TRACE, ("NDIS 3 protocol attempting to register/n"));
      MinSize = sizeof(NDIS30_PROTOCOL_CHARACTERISTICS);
      break;

    case 0x04:
      MinSize = sizeof(NDIS40_PROTOCOL_CHARACTERISTICS);
      break;

    case 0x05:
      MinSize = sizeof(NDIS50_PROTOCOL_CHARACTERISTICS);
      break;

    default:
      *Status = NDIS_STATUS_BAD_VERSION;
      NDIS_DbgPrint(MIN_TRACE, ("Incorrect characteristics size/n"));
      return;
    }

  if (CharacteristicsLength < MinSize)
    {
      NDIS_DbgPrint(DEBUG_PROTOCOL, ("Bad protocol characteristics./n"));
      *Status = NDIS_STATUS_BAD_CHARACTERISTICS;
      return;
    }

  /* set up the protocol block */
  Protocol = ExAllocatePool(NonPagedPool, sizeof(PROTOCOL_BINDING));
  if (!Protocol)
    {
      NDIS_DbgPrint(MIN_TRACE, ("Insufficient resources./n"));
      *Status = NDIS_STATUS_RESOURCES;
      return;
    }

  RtlZeroMemory(Protocol, sizeof(PROTOCOL_BINDING));
  RtlCopyMemory(&Protocol->Chars, ProtocolCharacteristics, MinSize);

  NtStatus = RtlUpcaseUnicodeString(&Protocol->Chars.Name, &ProtocolCharacteristics->Name, TRUE);
  if (!NT_SUCCESS(NtStatus))
    {
      NDIS_DbgPrint(MIN_TRACE, ("Insufficient resources./n"));
      ExFreePool(Protocol);
      *Status = NDIS_STATUS_RESOURCES;
      return;
    }

  KeInitializeSpinLock(&Protocol->Lock);

  InitializeListHead(&Protocol->AdapterListHead);

  /*
  * bind the protocol to all of its miniports
  *
  * open registry path
  * get list of devices from Bind key
  * call BindAdapterHandler for each
  */
  {
    OBJECT_ATTRIBUTES ObjectAttributes;
    UNICODE_STRING RegistryPath;
    WCHAR *RegistryPathStr;

    RegistryPathStr = ExAllocatePoolWithTag(PagedPool, sizeof(SERVICES_KEY) + ProtocolCharacteristics->Name.Length + sizeof(LINKAGE_KEY), NDIS_TAG + __LINE__);
    if(!RegistryPathStr)
      {
        NDIS_DbgPrint(MIN_TRACE, ("Insufficient resources./n"));
        ExFreePool(Protocol);
        *Status = NDIS_STATUS_RESOURCES;
        return;
      }

    wcscpy(RegistryPathStr, SERVICES_KEY);
    wcsncat(RegistryPathStr, ((WCHAR *)ProtocolCharacteristics->Name.Buffer), ProtocolCharacteristics->Name.Length / sizeof(WCHAR));
    RegistryPathStr[wcslen(SERVICES_KEY)+ProtocolCharacteristics->Name.Length/sizeof(WCHAR)] = 0;
    wcscat(RegistryPathStr, LINKAGE_KEY);

    RtlInitUnicodeString(&RegistryPath, RegistryPathStr);
    NDIS_DbgPrint(MAX_TRACE, ("Opening configuration key: %wZ/n", &RegistryPath));

    InitializeObjectAttributes(&ObjectAttributes, &RegistryPath, OBJ_CASE_INSENSITIVE, NULL, NULL);
    NtStatus = ZwOpenKey(&DriverKeyHandle, KEY_READ, &ObjectAttributes);

    ExFreePool(RegistryPathStr);

    if(!NT_SUCCESS(NtStatus))
      {
        NDIS_DbgPrint(MID_TRACE, ("Unable to open protocol configuration/n"));
        ExFreePool(Protocol);
        *Status = NDIS_STATUS_FAILURE;
        return;
      }
  }

  NDIS_DbgPrint(MAX_TRACE, ("Successfully opened the registry configuration/n"));

  {
    UNICODE_STRING ValueName;
    ULONG ResultLength;

    RtlInitUnicodeString(&ValueName, L"Bind");

    NtStatus = ZwQueryValueKey(DriverKeyHandle, &ValueName, KeyValuePartialInformation, NULL, 0, &ResultLength);
    if(NtStatus != STATUS_BUFFER_OVERFLOW && NtStatus != STATUS_BUFFER_TOO_SMALL)
      {
        NDIS_DbgPrint(MID_TRACE, ("Unable to query the Bind value for size/n"));
        ZwClose(DriverKeyHandle);
        ExFreePool(Protocol);
        *Status = NDIS_STATUS_FAILURE;
        return;
      }

    KeyInformation = ExAllocatePoolWithTag(PagedPool, sizeof(KEY_VALUE_PARTIAL_INFORMATION) + ResultLength, NDIS_TAG + __LINE__);
    if(!KeyInformation)
      {
        NDIS_DbgPrint(MIN_TRACE, ("Insufficient resources./n"));
        ZwClose(DriverKeyHandle);
        ExFreePool(Protocol);
        *Status = NDIS_STATUS_FAILURE;
        return;
      }

    NtStatus = ZwQueryValueKey(DriverKeyHandle, &ValueName, KeyValuePartialInformation, KeyInformation,
        sizeof(KEY_VALUE_PARTIAL_INFORMATION) + ResultLength, &ResultLength);

    if(!NT_SUCCESS(NtStatus))
      {
        NDIS_DbgPrint(MIN_TRACE, ("Unable to query the Bind value/n"));
        ZwClose(DriverKeyHandle);
        ExFreePool(KeyInformation);
        ExFreePool(Protocol);
        *Status = NDIS_STATUS_FAILURE;
        return;
      }
  }

  for (DataPtr = (WCHAR *)KeyInformation->Data;
      *DataPtr != 0;
      DataPtr += wcslen(DataPtr) + 1)
    {
      /* BindContext is for tracking pending binding operations */
      VOID *BindContext = 0;
      NDIS_STRING DeviceName;
      NDIS_STRING RegistryPath;
      WCHAR *RegistryPathStr = NULL;
      ULONG PathLength = 0;

      RtlInitUnicodeString(&DeviceName, DataPtr);    /* we know this is 0-term */

      /*
      * RegistryPath should be:
      *    /Registry/Machine/System/CurrentControlSet/Services/Nic1/Parameters/Tcpip
      *
      *  This is constructed as follows:
      *      SERVICES_KEY + extracted device name + Protocol name from characteristics
      */

      PathLength = sizeof(SERVICES_KEY) +                              /* /Registry/Machine/System/CurrentControlSet/Services/ */
          wcslen( DataPtr + 8 ) * sizeof(WCHAR) + /* Adapter1  (extracted from /Device/Adapter1)          */
          sizeof(PARAMETERS_KEY) +                                      /* /Parameters/                                        */
          ProtocolCharacteristics->Name.Length + sizeof(WCHAR);                        /* Tcpip                                                */

      RegistryPathStr = ExAllocatePool(PagedPool, PathLength);
      if(!RegistryPathStr)
        {
          NDIS_DbgPrint(MIN_TRACE, ("insufficient resources./n"));
          ExFreePool(KeyInformation);
          ExFreePool(Protocol);
          *Status = NDIS_STATUS_RESOURCES;
          return;
        }

      wcscpy(RegistryPathStr, SERVICES_KEY);
      wcscat(RegistryPathStr, DataPtr + 8 );
      wcscat(RegistryPathStr, PARAMETERS_KEY);
      wcsncat(RegistryPathStr, ProtocolCharacteristics->Name.Buffer, ProtocolCharacteristics->Name.Length / sizeof(WCHAR) );

      RegistryPathStr[PathLength/sizeof(WCHAR) - 1] = 0;

      RtlInitUnicodeString(&RegistryPath, RegistryPathStr);

      NDIS_DbgPrint(MAX_TRACE, ("Calling protocol's BindAdapter handler with DeviceName %wZ and RegistryPath %wZ/n",
          &DeviceName, &RegistryPath));

      /* XXX SD must do something with bind context */
      *NdisProtocolHandle = Protocol;

        {
          BIND_HANDLER BindHandler = ProtocolCharacteristics->BindAdapterHandler;
          if(BindHandler)
            BindHandler(Status, BindContext, &DeviceName, &RegistryPath, 0);
          else
            NDIS_DbgPrint(MID_TRACE, ("No protocol bind handler specified/n"));
        }

      /*
      (*(Protocol->Chars.BindAdapterHandler))(Status, BindContext, &DeviceName, &RegistryPath, 0);
      */

      if(*Status == NDIS_STATUS_SUCCESS)
        {
          /* Put protocol binding struct on global list */
          ExInterlockedInsertTailList(&ProtocolListHead, &Protocol->ListEntry, &ProtocolListLock);
        }

      /*
      else if(*Status != NDIS_STATUS_PENDING)
        {
          // what to do here?
        }
      */
    }

  *Status            = NDIS_STATUS_SUCCESS;
}

它 首先判断NDIS_PROTOCOL_CHARACTERISTICS的有效性,然后初始化了一个PPROTOCOL_BINDING Protocol结构,然后从Bind key获得设备链表,获得设备名,并创建了这样      *    /Registry/Machine/System/CurrentControlSet/Services/Nic1/Parameters/Tcpip
      *的注册表键,然后调用我们传进去的BindAdapterHandler进行绑定设备。并把Protocol加如链表。 从*NdisProtocolHandle = Protocol我们知道NdisProtocolHandle 指向Protocol那让我们看看这个Protocol是什么,
typedef struct _PROTOCOL_BINDING {
    LIST_ENTRY                    ListEntry;        /* Entry on global list */
    KSPIN_LOCK                    Lock;            /* Protecting spin lock */
    NDIS_PROTOCOL_CHARACTERISTICS Chars;            /* Characteristics */
    WORK_QUEUE_ITEM              WorkItem;        /* Work item */
    LIST_ENTRY                    AdapterListHead;  /* List of adapter bindings */
} PROTOCOL_BINDING, *PPROTOCOL_BINDING;
这里面有个指向adapter bindings的链表,来让我们看看adapter binding

typedef struct _ADAPTER_BINDING {
    NDIS_OPEN_BLOCK NdisOpenBlock;                            /* NDIS defined fields */

    LIST_ENTRY        ListEntry;                /* Entry on global list */
    LIST_ENTRY        ProtocolListEntry;        /* Entry on protocol binding adapter list */
    LIST_ENTRY        AdapterListEntry;        /* Entry on logical adapter list */
    KSPIN_LOCK        Lock;                    /* Protecting spin lock */
    PPROTOCOL_BINDING ProtocolBinding;          /* Protocol that opened adapter */
    PLOGICAL_ADAPTER  Adapter;                  /* Adapter opened by protocol */
} ADAPTER_BINDING, *PADAPTER_BINDING;
我 们看到第一项是一个NdisOpenBlock结构,现在很多的NDIS放火墙就是通过注册协议根据返回的NdisProtocolHandle来找到 NdisOpenBlock结构的,因为NdisOpenBlock结构是用来描述和该ndis protocol有联系的所有ndis miniport和该ndisprotocol绑定的状态每个绑定的send(packets)handler和 receive(packet)handler都在这个ndis open block里面。

struct _NDIS_OPEN_BLOCK
{
#ifdef __cplusplus
  NDIS_COMMON_OPEN_BLOCK NdisCommonOpenBlock;
#else
  NDIS_COMMON_OPEN_BLOCK;
#endif

#if defined(NDIS_WRAPPER)
  
  //
  // The stuff below is for CO drivers/protocols. This part is not allocated for CL drivers.
  //
  struct _NDIS_OPEN_CO
  {
  ....
  };
#endif
};

typedef struct _NDIS_COMMON_OPEN_BLOCK
{
  PVOID            MacHandle;     // needed for backward compatibility
  NDIS_HANDLE         BindingHandle;   // Miniport's open context
  PNDIS_MINIPORT_BLOCK    MiniportHandle;   // pointer to the miniport
  PNDIS_PROTOCOL_BLOCK    ProtocolHandle;   // pointer to our protocol
  NDIS_HANDLE         ProtocolBindingContext;// context when calling ProtXX funcs
  PNDIS_OPEN_BLOCK      MiniportNextOpen;  // used by adapter's OpenQueue
  PNDIS_OPEN_BLOCK      ProtocolNextOpen;  // used by protocol's OpenQueue
  NDIS_HANDLE         MiniportAdapterContext; // context for miniport
  BOOLEAN           Reserved1;
  BOOLEAN           Reserved2;
  BOOLEAN           Reserved3;
  BOOLEAN           Reserved4;
  PNDIS_STRING        BindDeviceName;
  KSPIN_LOCK         Reserved5;
  PNDIS_STRING        RootDeviceName;

  //
  // These are referenced by the macros used by protocols to call.
  // All of the ones referenced by the macros are internal NDIS handlers for the miniports
  //
  union
  {
    SEND_HANDLER      SendHandler;
    WAN_SEND_HANDLER    WanSendHandler;
  };
  TRANSFER_DATA_HANDLER    TransferDataHandler;

  //
  // These are referenced internally by NDIS
  //
  SEND_COMPLETE_HANDLER    SendCompleteHandler;
  TRANSFER_DATA_COMPLETE_HANDLER TransferDataCompleteHandler;
  RECEIVE_HANDLER       ReceiveHandler;
  RECEIVE_COMPLETE_HANDLER  ReceiveCompleteHandler;
  WAN_RECEIVE_HANDLER     WanReceiveHandler;
  REQUEST_COMPLETE_HANDLER  RequestCompleteHandler;

  //
  // NDIS 4.0 extensions
  //
  RECEIVE_PACKET_HANDLER   ReceivePacketHandler;
  SEND_PACKETS_HANDLER    SendPacketsHandler;

  //
  // More Cached Handlers
  //
  RESET_HANDLER        ResetHandler;
  REQUEST_HANDLER       RequestHandler;
  RESET_COMPLETE_HANDLER   ResetCompleteHandler;
  STATUS_HANDLER       StatusHandler;
  STATUS_COMPLETE_HANDLER   StatusCompleteHandler;
  
#if defined(NDIS_WRAPPER)
  ....
#endif

} NDIS_COMMON_OPEN_BLOCK;

  需要处理的,是ndis open block里面的SendHandler,ReceiveHandler,WanReceiveHandler,ReceivePacketHandler和SendPacketsHandler。

好 了在passthru里注册好协议以后  调用NdisIMAssociateMiniport(DriverHandle, ProtHandle)就可以了,因为passthru是中间层驱动程序,下面就应该分开说miniport和PROTOCOL,象 MPInitialize,还有上面我们调用了BindAdapterHandler,我们应该说我们的BindAdapterHandler例程了,还 有接包发包,和注册的各种例程,但现在就到这吧,要说下去不知什么时候能说完,这仅仅是一个开始,下面复杂的东西还很多,我这个小菜鸟就不现丑了。
windows ndis 详解 windows ndis 网络驱动 阅读详情

相关推荐

NDIS小端口驱动实现IOCTL分发

关键思路:为驱动创建一个设备对象 usbnwifi 例程中,MPInitialize函数通过NICRegisterDevice函数,注册了NICDispatch函数入口(该函数实现IRP的分发处理),从而为小端口驱动注册了一个IOCTL接口。这一过程根本上是调用了NDIS的NdisRegisterDeviceEx函数,传入了包含分发函数入口的_NDIS_DEVICE_OBJECT_...

Hooooxz的博客 1184

【2024最新版】超详细Metasploit安装保姆级教程,Wireshark抓包(网络分析),收藏这一篇就够了

Metasploit是一个渗透测试框架,可以帮助您发现和利用漏洞。Metasploit还为您提供了一个开发平台,您可以编写自己的安全工具或利用代码。今天,我将指导您了解如何使用Metasploit的基础知识:如何安装Metasploit,使用框架以及利用漏洞。

VN520的博客 2490

realtek 无线网卡驱动-亲测能用-winxp win7 win8 win10等

realtek 无线网卡驱动-亲测能用-支持winxp win7 win8 win10等,还能区分x86 x64平台。 安装教程 https://www.cnblogs.com/ningskyer/articles/10543787.html

Ndis网卡驱动是如何操控硬件的

在 DriverEntry 里设置一个_NDIS_MINIPORT_DRIVER_CHARACTERISTICS结构,初作为参数提供给 NdisMRegisterMiniportDriver 函数   NDIS_MINIPORT_DRIVER_CHARACTERISTICS MPChar; MPChar.InitializeHandlerEx = MPInitialize; // 初始化handle       ...    // 注册小端口驱动 Status =.

T76230169的专栏 902

从理论到波形:NE555多谐振荡器设计与示波器实战验证

本文详细介绍了NE555多谐振荡器的设计原理与实战验证方法。从基础理论到电路参数计算,再到面包板搭建与示波器测量,全面解析如何利用NE555芯片实现稳定的方波输出。通过实际案例演示了频率调整、占空比优化等技巧,并提供了常见问题排查指南,帮助电子爱好者快速掌握这一经典电路设计。

weixin_29159613的博客 282

vmware虚机Debian10磁盘空间无损扩容

该方法不适和LVM的磁盘管理 备份 一定要先做备份,虚机中直接创建一个快照备份 vm上增加空间 初始给的40G空间不够需要扩容,vm那给系统添加到100G 重新分区 查看当前分区情况 sudo fdisk -l

lms99251的博客 7456

NDIS小端口驱动开发(二)

NDIS小端口驱动

王马的博客 1166

同济大学C++mooc第五、六讲参考答案

同济大学C++moooc第5、6讲 第五讲 第一题 随机产生10个30~100(包括30,100)的正整数,求它们的最大值、最小值、平均值,并显示整个数组的值和结果。 代码片. // #include<iostream> const int n = 10; using namespace std; int main() { int s[n],i,j,t,sum=0; for (i = 0; i < n; i++)

cjzui6666的博客 1万+

NDIS驱动分析

链 接: http://bbs.pediy.com/showthread.php?t=65053从NdisIntermediateMiniportdriver说吧,参考passthruNTSTATUSDriverEntry(INPDRIVER_OBJECTDriverObject,INPUNICODE_STRINGRegistryPath)程序入口没什么好...

weixin_30586085的博客 271

【Qt】QByteArray详解

对象共享同一份数据,直到修改时才进行深拷贝,以节省内存和计算资源。:可存储任意二进制数据(如图片、音频、协议数据),不依赖字符编码。与编码(如 UTF-8、Latin-1)之间的桥梁(例如。• 补零规则确保奇数字符串能正确解析,避免数据截断。:内部自动分配和释放内存,无需手动管理。将输入的字符串视为十六进制编码数据,返回实际数据长度(不包括结尾的。的输入必须是有效的十六进制字符(,但针对 Qt 框架做了优化。:处理加密后的二进制结果(如。• 如果输入字符串长度为奇数,• 若输入长度为奇数,自动在。

CSUC的博客 1598

【AUCell打分】:评估一个基因集在单细胞转录组的每个细胞中特定的活性程度

AUCell使用曲线下面积来计算输入基因集的一个有意义的基因子集是否在每个细胞的表达基因中富集。AUC 分数在所有细胞中的分布允许探索特征的相对表达。由于评分方法是基于排名的,因此 AUCell 与基因表达单位和归一化程序无关。此外,由于细胞是单独评估的,因此可以很容易地应用于更大的数据集。

weixin_40695088的博客 1万+

Windows网络驱动、NDIS驱动(微端口驱动、中间层驱动、协议驱动)、TDI驱动(网络传输层过滤)、WFP(Windows Filtering Platfrom))

catalog 0.引言 1.Windows 2000网络结构和OSI模型 2.NDIS驱动 3.NDIS微端口驱动编程实例 4.NDIS中间层驱动编程实例 5.TDI驱动 6.TDI驱动 7.TDI的过滤框架 8.WFP(Windows Filtering Platform windows过滤平台) 0.引言 最早出现的网络驱动应该是网卡驱动,这是Windows的下进行...

zhangge3663的博客 6382

Misc-RAR隐写

题目来自于:XCTF的SimpleRAR,由于涉及到许多新知识,所以写篇小文子记录一下。 一、下载完后是一个压缩包。 里面就有一个flag.txt,打开之后发现有一串字符是flag is not here,而且我们查看他的文件大小,只要十六字节,绝对不可能再藏着什么信息了。 然后把压缩包拉到010editor里面,发现一下子出来好多新的内容,很明显是有夹杂着的附加文件。 二、rar文件头信息。 rar 是由一个一个 block 组成的,每个 block 有以下几个部分 ...

admin的博客 1362

欢迎使用优快云-markdown编辑器

**NDIS流程图** ====最近再学习NDIS协议驱动看了很多遍MSDN,同时也找了一些资料,总是不知道别人再说什么,直到这次自己用亿图将流程图画出ndis执行框架,总于清楚了到底哪是怎么一回事,等下分享出来,希望自己可以保存,也希望对想做一块的同行有所帮助。

kulu510的博客 319

Python 爬虫 + 数据分析:社会调查数据抓取与结果可视化分析

在当今数字化时代,数据已成为推动社会发展的关键资源之一。无论是市场调研、学术研究还是政策制定,社会调查数据都扮演着至关重要的角色。通过 Python 爬虫技术,我们可以高效地获取这些数据,并结合数据分析与可视化工具,深入挖掘数据背后的价值。本文将通过一个完整的实战项目,带你掌握如何使用 Python 爬虫抓取社会调查数据,并进行可视化分析。

yansideyucsdn的博客 126

软件设计师---UML

软件设计师---UML

我从不打没有准备的仗! 1万+

NDIS协议驱动开发指南

对于NDIS协议层驱动平时我们的使用场景不多,我们也没有能力(也没必要)设计一个完整的网络协议驱动。但是协议驱动是我们后面NDIS过滤驱动的基础,NDIS过滤驱动可以帮助我们获取本机接收到的网络帧数据包,并且对以太帧数据包进行过滤(例如ARP数据包等)。因此NDIS协议驱动还是非常值得我们去学习的,它是NDIS过滤驱动的基础,也是以太帧数据包过滤的重要手段。

xsinlink的博客 4194

Camera2Demo

在Camera2 的基础上进行封装,使得Camera2一堆初始化逻辑与Activity分离,降低耦合度,完成预览拍照并保存的操作

NDIS驱动

NDIS驱动分为3层 协议层驱动 绑定再所有网卡上,所以能截获接收到的包,但无法截获发送的包 中间层驱动 (P部分)绑定在了所有小端口驱动上(M部分)也被所有的协议驱动绑定,收发都能拦截。所有中间层小端口部分负责处理发送的包,协议部分负责处理接受的包 小端口驱动 网卡驱动 NDIS驱动开始 NdisMInitializeWrapper初始化NDIS句柄, NdisIMRegist...

kernweak的博客 5729

复分析 阿尔福斯(Ahlfors) 第三版答案 英文

答案分为十章,有部分习题没有答案,全英文复分析阿尔福斯(Ahlfors) 第三版答案

lwip-2.0.3

lwip是瑞典计算机科学院(SICS)的Adam Dunkels 开发的一个小型开源的TCP/IP协议栈。实现的重点是在保持TCP协议主要功能的基础上减少对RAM 的占用。

windows7以上平台NDIS6框架的NDIS协议驱动开发

                                                                                           by fanxiushu 2019-01-30 转载或引用请注明原始作者。 提到NDIS协议驱动,可能比较陌生,因为毕竟用得挺少的。 但是一提到WireShark或ethereal等抓包软件,大家就不再陌生了。 这些抓...

fanxiushu的专栏 6268

OV9734 DATASHEET,完整规格书

OV9734完整规格书,包括电气参数,尺寸规格,寄存器配置表等

NDIS驱动程序堆栈

NDIS驱动程序堆栈

王马的博客 1756

ecm、ppp、ndis 拨号

Modem拨号,PPP属于数据链路层协议,它使用HDLC协议格式来封装数据帧,而在数据流中如果遇到 0x7D,则表示后面的数据是控制信息,当它出现的时候,设备需从数据模式切换成控制模式,病处理 这些信息,设备需要对每一个字节的数据进行解析,并作出相应的处理,占用较多的资源。 NDIS拨号,Ethernet协议是通过使用不同的以太网帧的包头来区分控制信息和数据信息,一个以太网帧 的包头大小是固定的...

xingyuan1hao的专栏 2万+

NDIS驱动(一)协议驱动

NDIS网络驱动分类 协议驱动:上层直接提供应用层socket使用的数据传输接口,下层绑定小端口驱动用于发送和接收以太网包、 小端口驱动:直接针对网卡,给协议驱动提供接收和发生数据的能力 中间层驱动:以一种特殊的方式插入到协议驱动和小端口驱动之间,作为过滤驱动的最佳选择。 协议驱动 使用wdk下的ndisport工程,不提供传输层接口,只是只有ReadFile、WriteFile

World-wang 5140

NDIS 中间层驱动开发

1 NDIS中间层驱动程序1.1 NDIS中间层驱动程序(NDIS Intermediate Drivers)概述微软Windows网络驱动程序接口标准(NDIS 4.0)和Windows NT 4.0(SP3)引入了一种新的NDIS驱动程序,它可以嵌在NDIS 传输驱动程序TDI(如,TCP/IP)和底层的NDIS网络接口驱动程序的中间。这种新类型的驱动程序被称为NDIS中间层驱动,如图表

Kyline`s Blog 2855

关于Ndis驱动的一点个人理解

对写过驱动的一些总结:1,TDI层驱动,该层驱动位于windows kernel网络驱动的最顶层,个人觉得主要是针对连接时的redirect,filter比较好使,比如限制本地一些端口不能往外连接,限制外部的IP和PORT往里连接,只能连接到哪些外部IP,PORT等等。还有一个主要功能就是redirect,改变实际期望连接到的目的地址和端口,redirect到另一个IP和端口上,用这种方式好像

凡人樊语的专栏 6076
上一篇: 漫谈Fuzz测试技术 ——软件安全专家王清谈Fuzz技术与软件安全性测试
下一篇: MBRPROT -- Object Hijack(Deep disk filter without hook or function replace)
iiprogram
博客等级 码龄25年 1068粉丝 1515原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值