(转)TDI FILTER 网络过滤驱动完全解析

http://blog.csdn.net/charlesprince/article/details/5924376

  TDI FILTER 过滤驱动的功能一般用来进行整个系统中的所有网络流量的分析,记录和管理,可以实现非常强大的管理功能,这里就将讨论它的设计架构,和具体实现的方法。

  进行系统级网络数据包的过滤,很明显,第一步需要在系统内核中截取到网络数据包,那么在WINDOWS平台下,应该如何实现这样的功能?
  在WINDOWS内核中,数据的通信载体是IRP包,如果希望截取到IRP数据包,当然必须生成核模块以驱动的方式加载至内核之中。如果只是需要用来进行IRP数据包的截取,进而进行数据的分析,及下一步工作的控制。比较合适的方式就是使用TDI FILTER驱动的方式。


它在内核中的结构如图所示:
TDI FILTER ( 你的DRIVER )
TDI DRIVER ( AFD.SYS )

附加至TDI设备的方法:
  在DriverEntry时,生成两个设备,将其附加至(Attach)至Tdi驱动的Udp和Tcp设备,实现IRP包过滤功能,具体代码如下:

复制代码
#define UDP_DEVICE_NAME   L"//Device//Udp"
#define TCP_DEVICE_NAME   L"//Device//Tcp"
#define TDI_FILTER_DEVICE_NAME   L"//Device//TdiFilter"

typedef struct __TDI_FILTER_DEVICE_EXTENSION
{
    //过滤设备至少需要记录下真正Tdi网络设备的指针,来调用真正的TDI设备功能。
    PDEVICE_OBJECT pTdiDeviceObject; 
} TDI_FILTER_DEVICE_EXTENSION, *PTDI_FILTER_DEVICE_EXTENSION;

DriverEntry( PDRIVER_OBJECT pDriverObject, PUNICODE_STRING pRegistryPath )
{
    UNICODE_STRING TdiDeviceName;
    ......
    RtlInitUnicodeString( ( PUNICODE_STRING )&TdiDeviceName, UDP_DEVICE_NAME );

    ntStatus = IoCreateDevice( 
        DriverObject, 
        sizeof( TDI_FILTER_DEVICE_EXTENSION ), //指定设备扩展长度
        NULL, 
        FILE_DEVICE_NETWORK, //网络类型设备
        0, 
        0, 
        &DeviceObject 
        ); //生成一个无名、网络类型设备,附加至TDI TCP/UDP设备,实现过滤功能。

    if( NT_SUCCESS( ntStatus ) )
    {
        DeviceObject->Flags |= DO_DIRECT_IO; //生成新的页表将同样的用户内存空间映射至系统虚拟内存空间来进行通信
        ntStatus = IoAttachDevice( 
        DeviceObject, 
        TdiDeviceName, 
        ( PDEVICE_OBJECT* )DeviceObject->DeviceExtension //附加至的设备的指针将会输出至此参数中,这样就将真正的TDI设备的指针记录在过滤设备的扩展中
        ); 
    }
}
复制代码

 

TDI驱动的组织结构分为两个部分:
1.庞大的INTERNAL IO CONTROL子功能,包括以下功能:
  TDI_ASSOCIATE_ADDRESS 可以通过它截取出自己和对端的套接字信息,一般就是IP地址+端口号,可以在此IRP功能响应中进行套接字信息的记录。
  TDI_DISASSOCIATE_ADDRESS 它的IRP包是在closesocket函数时发生的,所以如果我们在     TDI_ASSOCIATE_ADDRESS中记录了信息,需要在此IRP的功能响应中取消之前的记录。
  TDI_CONNECT:主动连接
  TDI_LISTEN 
  TDI_ACCEPT
  TDI_DISCONNECT
  TDI_SEND 它的IRP包是在调用send函数时发生的,必然,对它的响应将会实现对基于TCP协议的网络上传流量的截取。
  TDI_RECEIVE 它的IRP包是在调用recv函数时发生的,必然,对它的响应将会实现对基于TCP协议的网络下载流量的截取。
  TDI_SEND_DATAGRAM 它的IRP包是在调用sendto函数时发生的,必然,对它的响应将会实现对基于UDP协议的网络上传流量的截取。
  TDI_RECEIVE_DATAGRAM 它的IRP包是在调用recvfrom函数时发生的,必然,对它的响应将会实现对基于UDP协议的网络下载流量的截取。
  TDI_SET_EVENT_HANDLER 它的IRP包是在TDI驱动中注册一些回调用函数,当接收到数据包时,将会首先执行它们,它的具体功能将会在下一步讲述。
  TDI_QUERY_INFORMATION 
  TDI_SET_INFORMATION 
  TDI_ACTION 
  TDI_DIRECT_SEND 
  TDI_DIRECT_SEND_DATAGRAM 

在TDI_SET_EVENT_HANDLER子功能中,可以注册以下回调涵数:
  TDI_EVENT_CONNECT:被动连接
  TDI_EVENT_DISCONNECT
  TDI_EVENT_ERROR 
  TDI_EVENT_RECEIVE 对应于recv函数有返回数据时,将会调用此回调函数。
  TDI_EVENT_RECEIVE_DATAGRAM 对应于recvfrom函数接收到数据时,将会调用此回调函数。
  TDI_EVENT_RECEIVE_EXPEDITED 对应于函数接收到带外数据时,将会调用此回调函数。( 带外数也就是OOB数据, 在全部IRP数据包中会优先进行发送或接收,TCP协议功能 )
  TDI_EVENT_SEND_POSSIBLE 

  以下将讲述数据具体传输回调功能的过滤方法


4.实现事件回调函数挂钩的方法:
  响应IRP_MJ_INTERNAL_DEVICE_CONTROL中的TDI_SET_EVENT_HANDLER子功能,记录下原始的注册事件回调函数和参数,但真正注册的是自己的回调函数,来截取所有的事件回调函数调用,实现过滤功能。
具体代码如下:

复制代码
typedef struct __TDI_EVENT_CONTEXT_WRAP
{
    DWORD dwEventContextMark; //对自己生成的结构实例加一个四字节的标志,可以不使用。
    DWORD dwEventType; //记录事件回调函数的类型
    PVOID pOrgEventHandler; //记录原始的事件回调函数
    PVOID pOrgEventContext; //记录原始的事件回调函数参数
    PFILE_OBJECT pAssocAddr; //记录事件回调函数所绑定的本机套接字
    PDEVICE_OBJEXT pDeviceObjext; //记录注册事件IRP所发送至的TDI设备
} TDI_EVENT_HANDLER_WRAP, *PTDI_EVENT_HANDLER_WRAP;

typedef struct __TDI_EVENT_HANDLER_LINK
{
    LIST_ENTRY List; //将事件回调钩子记录以链表形式进行管理
    PTDI_EVENT_HANDLER_WRAP pTdiEventHandlerWrap;
} TDI_EVENT_HANDLER_LIST, *PTDI_EVENT_HANDLER_LIST;

LIST_ENTRY g_TdiEventHandlerInfoList;


NTSTATUS DeviceInternalIoControl( PDEVICE_OBJECT DeviceObject, PIRP Irp )
{
    PKIRQL OldIrql;
    PLIST_ENTRY pListEntry;
    PIO_STACK_LOCATION IrpSp;
    PTDI_EVENT_HANDLER_WRAP pTdiEventHandlerWrap;
    PTDI_EVENT_HANDLER_LIST pTdiEventHandlerList;
    PTDI_EVENT_HANDLER_WRAP pTdiEventHandlerWrap_;
    PTDI_EVENT_HANDLER_LIST pTdiEventHandlerList_;
    PTDI_FILTER_DEVICE_EXTENSION pTdiDeviceExtension; 
    
    pTdiDeviceExtension = ( PTDI_FILTER_DEVICE_EXTENSION )DeviceObject->DeviceExtension;

    switch( IrpSp->MinorFunction )
    {
        case TDI_SET_EVENT_HANDLER:
        pTdiSetEvent = ( PTDI_REQUEST_KERNEL_SET_EVENT )&pIrpSp->Parameters;

        if( TDI_EVENT_RECEIVE == pTdiSetEvent->EventType )//|| 
        TDI_EVENT_RECEIVE_EXPEDITED == pTdiSetEvent->EventType || 
        TDI_EVENT_CHAINED_RECEIVE == pTdiSetEvent->EventType || 
        TDI_EVENT_CHAINED_RECEIVE_EXPEDITED == pTdiSetEvent->EventType || 
        TDI_EVENT_RECEIVE_DATAGRAM == pTdiSetEvent->EventType )
        {
            pTdiEventHandlerList = NULL;
            pTdiEventHandlerWrap = NULL;
            pProcessNetWorkTrafficInfo = NULL;
            
            if( NULL == pTdiSetEvent->EventHandler )
            {
            //注意!如果注册的事件回调函数是NULL的话,它表示的取消之前曾经注册过的事件回调函数, 这里当然不能挂钩,可以加入释放钩子资源的操作。
            goto CALL_PDO_DRIVER;
            }
        
            KeAcquireSpinLock( &g_SpLockTdiEventHandlerInfo, &OldIrql ); //对事件回调函数钩子列表写操作加锁保护
            
            pListEntry = g_TdiEventHandlerInfoList.Flink;
        
            for( ; ; )
            {
                if( pListEntry == &g_TdiEventHandlerInfoList )
                {
                pTdiEventHandlerWrap_ = NULL;
                break;
                }
            
                pTdiEventHandlerList_ = ( PTDI_EVENT_HANDLER_LIST )pListEntry;
                pTdiEventHandlerWrap_ = pTdiEventHandlerList_->pTdiEventHandlerWrap;
            
                if( pTdiEventHandlerWrap_->pAssocAddr == pFileObject &&
                pTdiEventHandlerWrap_->dwEventType == dwEventType ) //如果此本机套接字对象的相应事件回调函数已经存在,则直接对其进行修改就可以了,而不是不断的新建事件件回调钩子
                {
                pTdiEventHandlerWrap_->pOrgEventHandler = pEventHandler;
                pTdiEventHandlerWrap_->pOrgEventContext = pEventContext;
                break;
                }
            }
        
            if( NULL == pTdiEventHandlerWrap_ ) //没有找到,加入新的事件回调函数钩子
            {
                pTdiEventHandlerWrap = ( PTDI_EVENT_HANDLER_WRAP )ExAllocatePoolWithTag( NonPagedPool, sizeof( TDI_EVENT_HANDLER_WRAP ), 0 );
                if( NULL == pTdiEventHandlerWrap )
                {
                    goto RELEASE_RESOURCE;
                }
            
                pTdiEventHandlerList = ( PTDI_EVENT_HANDLER_LIST )ExAllocatePoolWithTag( NonPagedPool, sizeof( TDI_EVENT_HANDLER_LIST ), 0 );
                if( NULL == pTdiEventHandlerList )
                {
                    goto RELEASE_RESOURCE;
                }
            
                pTdiEventHandlerWrap->dwEventContextMark = TDI_EVENT_CONTEXT_MARK;
                pTdiEventHandlerWrap->dwEventType = dwEventType;
                pTdiEventHandlerWrap->pOrgEventHandler = pEventHandler;
                pTdiEventHandlerWrap->pOrgEventContext = pEventContext;
                pTdiEventHandlerWrap->pAssocAddr = pFileObject;
                pTdiEventHandlerWrap->pDeviceObject = pTdiDeviceExtension->pTdiDeviceObject;
            
                pTdiEventHandlerList->pTdiEventHandlerWrap = pTdiEventHandlerWrap;
                
                InsertTailList( &g_TdiEventHandlerInfoList, pTdiEventHandlerList );
            }
            else
            {
                pTdiEventHandlerWrap = pTdiEventHandlerWrap_;
                pTdiEventHandlerList = pTdiEventHandlerList_;
            }
        
            KeReleaseSpinLock( &g_SpLockTdiEventHandlerInfo, OldIrql ); //释放事件回调钩子列表锁
        
            if( TDI_EVENT_RECEIVE == pTdiSetEvent->EventType || 
            TDI_EVENT_RECEIVE_EXPEDITED == pTdiSetEvent->EventType )
            {
                pTdiSetEvent->EventHandler = TdiFilterRecvEventHandler; //加入自己的事件过滤回调函数
            }
            else if( TDI_EVENT_CHAINED_RECEIVE == pTdiSetEvent->EventType ||
            TDI_EVENT_CHAINED_RECEIVE_EXPEDITED == pTdiSetEvent->EventType )
            {
                pTdiSetEvent->EventHandler = TdiFilterChainedRecvHandler;
            }
            else
            {
                pTdiSetEvent->EventHandler = TdiFilterRecvDatagramEventHandler;
            }
        
            pTdiSetEvent->EventContext = pTdiEventHandlerWrap;
        
            IoSkipCurrentIrpStackLocation( pIrp );
            ntStatus = IoCallDriver( pDeviceExtension->pTdiDeviceObject, pIrp );
        
            if( !NT_SUCCESS( ntStatus ) )
            {
                if( NULL == pTdiEventHandlerWrap_ )
                {
                    //如果是新加入的事件回调函数钩子,可以在出错时将其释放, 也可以保留至套接字关闭时,再进行释放
                    KeAcquireSpinLock( &g_SpLockTdiEventHandlerInfo, &OldIrql ); //对事件回调函数钩子列表写操作加锁保护
                    RemoveEntryList( ( PLIST_ENTRY )pTdiEventHandlerList );
                    ExFreePoolWithTag( pTdiEventHandlerWrap );
                    ExFreePoolWithTag( pTdiEventHandlerList );
                    KeReleaseSpinLock( &g_SpLockTdiEventHandlerInfo, OldIrql ); //释放事件回调钩子列表锁
                }
            }
        
            return ntStatus;
        }
        break;
    
    default:
        goto CALL_PDO_DRIVER;
        break;
    }

    RELEASE_RESOURCE:
    if( NULL != pTdiEventHandlerWrap )
    {
        ExFreePoolWithTag( pTdiEventHandlerWrap, NonPagedPool );
    }
    
    if( NULL != pTdiEventHandlerList )
    {
        ExFreePoolWithTag( pTdiEventHandlerList, NonPagedPool );
    }

CALL_PDO_DRIVER:
    IoSkipCurrentIrpStackLocation( pIrp );
    return IoCallDriver( pDeviceExtension->pTdiDeviceObject, pIrp );
}
复制代码

 

  以上对事件回调函数加入了钩子,下一步,必须考虑对其释放的问题,否则,当原始回调函数对应的套接字释放后,你的系统将会崩溃,以下为具体代码:

在套接字关闭后,要在IRP_MJ_CLEANUP功能函数中将相关的事件回调钩子释放掉:

复制代码
NTSTATUS TdiFilterCleanUp(PDEVICE_OBJECT DeviceObject, PIRP pIrp )
{
    NTSTATUS ntStatus;
    KIRQL OldIrql;
    PTDI_EVENT_HANDLER_LIST pTdiEventHandlerList;
    PTDI_EVENT_HANDLER_WRAP pTdiEventHandlerWrap;
    PFILE_OBJECT pFileObject;

    TDI_FILTER_DEVICE_EXTENSION *pDeviceExtension;
    PIO_STACK_LOCATION pIrpSp;

    pDeviceExtension = ( TDI_FILTER_DEVICE_EXTENSION* )DeviceObject->DeviceExtension;
    pIrpSp = IoGetCurrentIrpStackLocation( pIrp );
    pFileObject = pIrpSp->FileObject;

    ...
    //如果是主控制设备,要将调用IoCompleteIrp完成Irp, 如果是过滤设备,调用PDO设备驱动

    IoSkipCurrentIrpStackLocation( pIrp );
    ntStatus = IoCallDriver( pDeviceExtension->pTdiDeviceObject, pIrp );

    if( !NT_SUCCESS( ntStatus ) )
    {
        DebugPrintEx( CLEANUP_INFO,"netmon TdiFilterCleanUp IoCallDriver return ERROR/n" );
        return ntStatus;
    }

    //下一步,释放套接字对应的事件回调钩子
    KeAcquireSpinLock( &g_SpLockTdiEventHandlerInfo, &OldIrql );
    
    FIND_LIST_AGAIN:
    pListEntry = g_TdiEventHandlerInfoList.Flink;

    for( ; ; )
    {
        if( pListEntry == &g_TdiEventHandlerInfoList )
        {
            break;
        }
    
        pTdiEventHandlerList = ( PTDI_EVENT_HANDLER_LIST )pListEntry;
        pTdiEventHandlerWrap = pTdiEventHandlerList->pTdiEventHandlerWrap;
    
        if( pTdiEventHandlerWrap->pAssocAddr == pFileObject )
        {
            RemoveEntryList( pListEntry );
            
            ExFreePoolWithTag( pTdiEventHandlerWrap, 0 );
            ExFreePoolWithTag( pTdiEventHandlerList, 0 );
            goto FIND_LIST_AGAIN;
        }
    
        pListEntry = pListEntry->Flink;
    }

    KeReleaseSpinLock( &g_SpLockTdiEventHandlerInfo, OldIrql );
    return ntStatus;
}
复制代码

 

那么,可以在事件回调过滤钩子函数对数据进行处理了

复制代码
NTSTATUS TdiFilterRecvEventHandler( IN PVOID TdiEventContext,
    IN CONNECTION_CONTEXT ConnectionContext,
    IN ULONG ReceiveFlags,
    IN ULONG BytesIndicated,
    IN ULONG BytesAvailable,
    OUT ULONG *BytesTaken,
    IN PVOID Tsdu,
    OUT PIRP *IoRequestPacket)
{
    NTSTATUS ntStatus;
    PIO_STACK_LOCATION pIrpSp;
    PTDI_EVENT_HANDLER_WRAP pEventHandlerWrap;
    PTDI_COMPLETION_WRAP pCompletionWrap;
    LARGE_INTEGER RecvedDataSize;

    pEventHandlerWrap = ( PTDI_EVENT_HANDLER_WRAP )TdiEventContext;
    
    if( FALSE == g_bFiltering ) //是否进行过滤
    {
        goto CALL_ORIGINAL_EVENT_HANDLER;
    }

    if( FALSE != bStopRecv )
    { 
        ntStatus = STATUS_DATA_NOT_ACCEPTED;
        goto RELEASE_PROCESS_IO_INFO_RETURN;
    }

    ntStatus = ( ( ClientEventReceive )pEventHandlerWrap->pOrgEventHandler )( 
        pEventHandlerWrap->pOrgEventContext, 
        ConnectionContext, 
        ReceiveFlags, 
        BytesIndicated, 
        BytesAvailable, 
        BytesTaken, 
        Tsdu, 
        IoRequestPacket );

    if( NULL != BytesTaken && 0 != *BytesTaken )
    {
        //这里对数据进行处理, 比如可以进行通信数据量的统计
    }

    if( STATUS_MORE_PROCESSING_REQUIRED != ntStatus )
    {
        goto RELEASE_PROCESS_IO_INFO_RETURN;
    }

    if( NULL == *IoRequestPacket )
    {
        goto RELEASE_PROCESS_IO_INFO_RETURN;
    }

    //IoRequestPacket表示当前接收IRP中的数据如果并不完整, 并且认为接下来的数据是有价值,需要接收的话,那么需要自己新建一个IRP包,将其指针传入此参数中,并返回STATUS_MORE_PROCESSING_REQUIRED,通知IO管理不终止此IRP,TDI驱动将继续接收接下来的数据。
    //所以如果此IRP包存在,可以截取它的信息,具体方法下一步讲述。
    return ntStatus;

CALL_ORIGINAL_EVENT_HANDLER:
    return ( ( ClientEventReceive )pEventHandlerWrap->pOrgEventHandler )( //直接调用原始的IRP钩子函数,不进行处理
        pEventHandlerWrap->pOrgEventContext, 
        ConnectionContext, 
        ReceiveFlags, 
        BytesIndicated, 
        BytesAvailable, 
        BytesTaken, 
        Tsdu, 
        IoRequestPacket);
}
复制代码

  上面讲述了使用事件回调函数钩子的方式进行通信数据的截取方法,下面讲述直接IRP包数据传输方式,也就是以下4个子功能的截取方法:

  TDI_SEND 
  TDI_RECEIVE
  TDI_SEND_DATAGRAM
  TDI_RECEIVE_DATAGRAM

  在IRP_MJ_INTERNAL_DEVICE_CONTROL函数中响应以上子功能时,确认参数DeviceObject为TDI过滤设备,对所有截取到的IRP加入自己的完成函数,在此IRP被完成时( 调用IoCompleteRequest )此完成函数被调用,取得IRP处理的返回结果,进行处理。具体数据的处理对应于TDI_SEND子功能可以在IoCallDriver之前得到,因为它是应用程序传给你的,而TDI_RECEIVE子功能,应该在TDI事件回调函数或Completion回调函数中取得。
相关代码如下:

typedef struct __TDI_COMPLETION_WRAP
{
  ...//可以加入用来记录/处理数据的成员, 比如通信标志, 流量统计等
  PIO_COMPLETION_ROUTINE pCompletionRoutine;
  LPVOID pContext;
} TDI_COMPLETION_WRAP, *PTDI_COMPLETION_WRAP;

加入自定义的CompletionRoutine的方法:

复制代码
if( TDI_SEND == MinorFunction ||
    TDI_SEND_DATAGRAM == MinorFunction || 
    TDI_RECEIVE == MinorFunction || 
    TDI_RECEIVE_DATAGRAM == MinorFunction )
{
    if( TDI_RECEIVE == MinorFunction && 
    TDI_RECEIVE_PEEK == ( ULONG )pIrpSp->Parameters.Others.Argument2 )
    {
        //TDI_RECEIVE_PEEK不会真正接收数据,可以不需要对其进行过滤。
        goto SKIP_CURRENT_STACK_LOCATION;
    }

    pCompletionWrap = ( PTDI_COMPLETION_WRAP )ExAllocateFromNPagedLookasideList( &g_CompletionWrapList ); //可以使用链表或HASH等数据结构来管理所有的CompletionRoutine包装信息,这里使用了NPAGED_LOOKASIDE_LIST,它的优势在于系统中所有的NPAGED_LOOKASIDE_LIST资源的最大占用量将会被内存管理器动态管理

    if( NULL == pCompletionWrap )
    {
        goto SKIP_CURRENT_STACK_LOCATION;
    }

    //这里可以设置CompletionRoutine的具体工作参数,比如具体操作的类型,原始的Completion函数等,在用户层传送至的IRP中是不会设置CompletionRoutine函数的,但其它驱动传送至的IRP中可能会进行设置,如在Receive事件回调函数中的IoRequestPacket参数
    IoCopyCurrentIrpStackLocationToNext( pIrp ); //设置下一个设备栈工作参数

    IoSetCompletionRoutine( pIrp, 
        TdiFilterCompletion, 
        pCompletionWrap, 
        TRUE, 
        TRUE, 
        TRUE);//这里就为这个IRP加入自己的CompletionRoutine函数

goto CALL_PDO_DRIVER;
    
    SKIP_CURRENT_STACK_LOCATION:
    IoSkipCurrentIrpStackLocation( pIrp );
    
    CALL_PDO_DRIVER:
    return IoCallDriver( pDeviceExtension->pTdiDeviceObject, pIrp );
}
复制代码

具体的Completion函数的工作:

复制代码
NTSTATUS TdiFilterCompletion( PDEVICE_OBJECT pDeviceObject, PIRP pIrp, LPVOID pContext )
{
    NTSTATUS ntStatus;
    PTDI_COMPLETION_WRAP pCompletionWrap;
    LARGE_INTEGER TransferredDataSize;
    PIRP pMasterIrp;
    PIO_STACK_LOCATION pIrpSp;
    
    ntStatus = pIrp->IoStatus.Status;
    
    pCompletionWrap = ( PTDI_COMPLETION_WRAP )pContext;
    
    if( NT_SUCCESS( ntStatus ) )
    {
        //可以在这里对成功传输的数据进行处理
    }
    
    //这里可以调用原始的Completion函数
    
    RETURN_SUCCESS:
    return ntStatus;
}
复制代码

 

  需要注意的是,如果为IRP包加入了CompletionRoutine之后,那么在驱动卸载( Unload )之前,必须保证所有IRP已经执行过此Completion函数, 如果在驱动被从内存中卸载后才执行, 将会使系统崩溃。
处理方法为:
  1.不实现DriverUnload函数,使驱动只有在系统关闭,底层设备被卸载时,才能完成真正的卸载。这是的一般    FILTER驱动的工作方式,
  2.使用线程同步的方法保证Completion函数的执行,Windows XP或之后的系统也提供了一个API,         SetCompletionRoutineEx来保证驱动在Completion函数完成前不被卸载。

  至此,讲述TDI过滤驱动组织框架,可以为它添加一些更加完善的功能。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值