ActiveX控件的安全初始化和脚本操作 和 数字签名SIGN

摘要:数字签名SIGN保证控件在下载时候的安全性。如果你的代码已经经过数字签名,即使用户IE的安全设置很高也能下载,安装并登记。但是在页面上初始化,或者用脚本运行这个控件,为了保证安全性,还需要进行MARK。         数字签名SIGN     曹晓峰         摘要:数字签名保证控件的安全性。数字签名使用证书。证书一般有个人证书和授信公司证书。个人证书是对个人的信任,由个人承担责任,控件每次下载时需要进行确认。公司证书是由第三方公司发布的,保证控件的安全性,公司证书需要付费。Windows授信的证书公司有VeriSign,SecureSign等等。由这些公司证书签名的控件在下载的时候不需要确认。         一.工具     工具包括以下几个软件:     makecert.exe     制作cer格式的证书,即X.509证书,同时可以创建私钥(防止抵赖)     cert2spc.exe     将cer格式证书转换成spc格式证书,即PKCS   #7证书     signcode.exe     将证书签署到ocx上去     chktrust.exe     检查签署证书后的ocx是否正确     certmgr.exe,是管理证书用的,可以从这里面导出root.cer来,不过没有私钥用不上。         二.步骤     下面是具体的步骤:     1、创建一个自己的证书文件:     makecert   /sv   "Record.PVK"   /n   "CN=SinoWave"   dream.cer     这里,Record.PVK表示新创建的私人密钥保存文件名                 SinoWave是你想显示的公司名                 dream.cer是你创建最后的证书文件名     这些根据你自己的要求填写,最后得到Record.PVK和dream.cer两个文件。其中,运行过程中需要输入私人密钥的保护密码(sw),一定要输入一致,不要出错。         2、转换cer格式为spc格式(可以省略),得到dream.spc文件。         cert2spc   dream.cer   dream.spc         3、用VS6工具中的   cabarc生成internet分发的CAB包,     cabarc.exe   N   DataTransfer.cab   DataTransfer.ocx         4、同时制作分发代码(.htm,其中包含使IE可以自动下载安装包的代码)。     现在得到了2个文件DataTransfer.CAB和DataTransfer.htm。     .htm中包含类似如下的代码:     <OBJECT   ID="   DataTransfer   "   CLASSID="CLSID:   CA466D54-0684-49D2-B0C3-DD7E09EA76D3"   CODEBASE="http://192.9.200.8/DataTransfer.CAB#version=1,0,0,0"></OBJECT>     注意:一定要写上"http://   192.9.200.8/",真正发行时最好使用url。         5、给CAB文件签名     运行signcode,命令行的我没有试验通过,我是通过界面实现的。signcode运行后会出现数字签名向导,首先选择DataTransfer.CAB,下一步后会出现签名选项,一种是典型,一种是自定义。选择自定义,这样才能从文件选择证书,选择前面制作的dream.spc,再下一步是选择私钥文件,选择Record.PVK,输入私人密钥的保护密码,选择散列算法,一般用md5就可以了,下一步是选择其他证书,直接下一步,填写一下这个控件的声明,用户用ie浏览的时候,会弹出证书说明,再下一步是加盖时间戳,例如http://timestamp.sheca.com/timestamp           6、用chktrust检查是否正确     chktrust   -v   DataTransfer.CAB         7、将签名后的DataTransfer.CAB和DataTransfer.htm复制到IIS的某个目录下。并在IE中打开DataTransfer.htm文件进行测试。  

ActiveX控件的安全初始化和脚本操作MARK     曹晓峰         简介     很多微软的ActiveX控件(本地/远程)都需要使用持久性数据进行初始化,而且它们大多数都是可以通过脚本进行操作的   (支持一个方法,事件和属性的集合提供脚本语言操作)。初始化(使用持久性数据)和脚本操作都需要一个确定的安全性机制保证其安全性不被违背。         初始化安全性     当一个控件初始化时,可以从一个   IPersist*   接口获得数据   (来自一个本地/远端的URL)提供初始化状态。这是一个潜在的安全隐患,因为数据可能来自一个不可信的数据源。不提供安全性保证的控件将无视数据源的安全性。     有两种方法可以检测控件的初始化安全性。第一种使用组件分组管理器(Component   Categories   Manager)创建一个正确的入口到系统注册表。IE检测注册表之后才调用你的控件决定是否这些入口存在。第二种方法实现一个名称为IObjectSafe的接口到你的控件。如果IE发现你的控件支持IObjectSafety,它调用   IObjectSafety::SetInterfaceSafetyOptions   方法然后才载入你的控件。     注意,第一种方法是基于组件的安全性,也就是如果设置了注册表,那么整个组件的所有的接口都被标注为安全的;而第二种方法(实现IObjectSafety)是基于接口的,也就是它的作用范围是接口,必须一个接口一个接口地标注。第二种方法的运行性能要优于第一种。在保证安全的情况下,两种方法同时使用更好。         脚本操作安全性     代码签字可以保证用户其代码的可信度。但是运行一个   ActiveX   控件可以被脚本访问将带来几个新的安全性问题。即使控件被认为是可靠的,如果使用不可信脚本代码访问也不能保证其安全性。比如,微软的   Word   被认为是一个安全的程序,但是一个宏可以使用自动化模型的脚本删除用户计算机上的文件,载入宏病毒或者蠕虫病毒。     有两种方法提供你的控件的脚本操作安全性保证。第一种是使用组件分组管理器   (Component   Categories   Manager)   ——在组件导入以后在注册表上面创建正确的入口。IE在脚本操作之前检查注册表确认安全性。第二种是实现一个   IObjectSafety   接口到你的控件。如果IE发现你的控件支持   IObjectSafety,就在导入控件之前调用   IObjectSafety::SetInterfaceSafetyOptions   方法来确保安全性脚本操作。     注意,第一种方法是基于组件的安全性,也就是如果设置了注册表,那么整个组件的所有的接口都被标注为安全的;而第二种方法是基于接口的,也就是它的作用范围是接口,必须一个接口一个接口地标注。第二种方法的运行性能要优于第一种。在保证安全的情况下,两种方法同时使用更好。         方法一:IObjectSafety方法:     MFC:实现接口IObjectSafety     ATL:继承接口IObjectSafetyImpl<CPolyCtl,   INTERFACESAFE_FOR_UNTRUSTED_CALLER|   INTERFACESAFE_FOR_UNTRUSTED_DATA   >         具体方法:     一. MFC     1. 在控件类的头文件里     (1)加入文件#include   <objsafe.h>     (2)在类的定义里,声明接口映射表,并加入接口定义(以嵌套类的形式)     DECLARE_INTERFACE_MAP()     BEGIN_INTERFACE_PART(ObjSafe,   IObjectSafety)     STDMETHOD_(HRESULT,   GetInterfaceSafetyOptions)   (                               /*   [in]   */   REFIID   riid,                             /*   [out]   */   DWORD   __RPC_FAR   *pdwSupportedOptions,                             /*   [out]   */   DWORD   __RPC_FAR   *pdwEnabledOptions     );                                         STDMETHOD_(HRESULT,   SetInterfaceSafetyOptions)   (                               /*   [in]   */   REFIID   riid,                             /*   [in]   */   DWORD   dwOptionSetMask,                             /*   [in]   */   DWORD   dwEnabledOptions     );     END_INTERFACE_PART(ObjSafe);         2. 在控件类的CPP文件里     (1)建立接口映射表     BEGIN_INTERFACE_MAP(   CMyCtrl,   COleControl   )     INTERFACE_PART(CMyCtrl,   IID_IObjectSafety,   ObjSafe)     END_INTERFACE_MAP()     (2)加入接口类的实现     ULONG   FAR   EXPORT   CMyCtrl::XObjSafe::AddRef()     {             METHOD_PROLOGUE(CMyCtrl,   ObjSafe)             return   pThis->ExternalAddRef();     }         ULONG   FAR   EXPORT   CMyCtrl::XObjSafe::Release()     {             METHOD_PROLOGUE(CMyCtrl,   ObjSafe)             return   pThis->ExternalRelease();     }         HRESULT   FAR   EXPORT   CMyCtrl::XObjSafe::QueryInterface(             REFIID   iid,   void   FAR*   FAR*   ppvObj)     {             METHOD_PROLOGUE(CMyCtrl,   ObjSafe)             return   (HRESULT)pThis->ExternalQueryInterface(&iid,   ppvObj);     }         const   DWORD   dwSupportedBits   =       INTERFACESAFE_FOR_UNTRUSTED_CALLER   |     INTERFACESAFE_FOR_UNTRUSTED_DATA;     const   DWORD   dwNotSupportedBits   =   ~   dwSupportedBits;         /     //   CStopLiteCtrl::XObjSafe::GetInterfaceSafetyOptions     //   Allows   container   to   query   what   interfaces   are   safe   for   what.   We're     //   optimizing   significantly   by   ignoring   which   interface   the   caller   is     //   asking   for.     HRESULT   STDMETHODCALLTYPE       CMyCtrl::XObjSafe::GetInterfaceSafetyOptions(       /*   [in]   */   REFIID   riid,                     /*   [out]   */   DWORD   __RPC_FAR   *pdwSupportedOptions,                     /*   [out]   */   DWORD   __RPC_FAR   *pdwEnabledOptions)     {     METHOD_PROLOGUE(CMyCtrl,   ObjSafe)         HRESULT   retval   =   ResultFromScode(S_OK);         //   does   interface   exist?     IUnknown   FAR*   punkInterface;     retval   =   pThis->ExternalQueryInterface(&riid,       (void   *   *)&punkInterface);     if   (retval   !=   E_NOINTERFACE)   { //   interface   exists     punkInterface->Release();   //   release   it--just   checking!     }         //   we   support   both   kinds   of   safety   and   have   always   both   set,     //   regardless   of   interface     *pdwSupportedOptions   =   *pdwEnabledOptions   =   dwSupportedBits;         return   retval;   //   E_NOINTERFACE   if   QI   failed     }         /     //   CStopLiteCtrl::XObjSafe::SetInterfaceSafetyOptions     //   Since   we're   always   safe,   this   is   a   no-brainer--but   we   do   check   to   make     //   sure   the   interface   requested   exists   and   that   the   options   we're   asked   to     //   set   exist   and   are   set   on   (we   don't   support   unsafe   mode).     HRESULT   STDMETHODCALLTYPE       CMyCtrl::XObjSafe::SetInterfaceSafetyOptions(                       /*   [in]   */   REFIID   riid,                     /*   [in]   */   DWORD   dwOptionSetMask,                     /*   [in]   */   DWORD   dwEnabledOptions)     {             METHOD_PROLOGUE(CMyCtrl,   ObjSafe)         //   does   interface   exist?     IUnknown   FAR*   punkInterface;     pThis->ExternalQueryInterface(&riid,   (void   *   *)&punkInterface);     if   (punkInterface)   { //   interface   exists     punkInterface->Release();   //   release   it--just   checking!     }     else   {   //   interface   doesn't   exist     return   ResultFromScode(E_NOINTERFACE);     }         //   can't   set   bits   we   don't   support     if   (dwOptionSetMask   &   dwNotSupportedBits)   {       return   ResultFromScode(E_FAIL);     }         //   can't   set   bits   we   do   support   to   zero     dwEnabledOptions   &=   dwSupportedBits;     //   (we   already   know   there   are   no   extra   bits   in   mask   )     if   ((dwOptionSetMask   &   dwEnabledOptions)   !=       dwOptionSetMask)   {     return   ResultFromScode(E_FAIL);     }     //   don't   need   to   change   anything   since   we're   always   safe     return   ResultFromScode(S_OK);     }     二. ATL     在控件类加一个继承接口即可     public   IObjectSafetyImpl<CAtlCtrl,   INTERFACESAFE_FOR_UNTRUSTED_CALLER                                                           |   INTERFACESAFE_FOR_UNTRUSTED_DATA>  方法二:使用组件分组管理器     前面提到,IE通过检测注册表决定一个控件是否是可以安全性初始化和脚本操作的。IE通过调用   ICatInformation::IsClassOfCategories   方法决定是否控件支持给出的安全性分组。     如果一个控件使用组件分组管理器将自己注册为安全的,该控件的注册表入口就包含一个实现的分组关键字,该关键字含有一个或者两个子键。一个子键设置控件支持安全性初始化,另一个设置支持安全性脚本操作。安全性初始化子键依赖于   CATID_SafeForInitializing;   安全性脚本操作子键依赖于   CATID_SafeForScripting。(其他组件分组子键定义在   Comcat.h   文件,而安全性初始化和脚本操作子键定义在   Objsafe.h   文件。)       下列演示显示了一个注册表入口(Tabular   Data   Control),该ActiveX控件同IE绑定支持创建数据驱动的网页。因为控件是可以安全性脚本操作和初始化的,注册表中将其标记为安全性脚本操作     (7DD95801-9882-11CF-9FA9-00AA006C42C4)   和安全性初始化     (7DD95802-9882-11CF-9FA9-00AA006C42C4)。     注意,这两个GUID值是固定的。     将一个控件注册为安全的     系统注册表含有一个组件分组键来罗列每一个安装在系统中的组件的功能性分组。下面演示了一个组件分组键。假设   CATID_SafeForScripting   (7DD95801-9882-11CF-9FA9-00AA006C42C4)   和   CATID_SafeForInitializing   (7DD95802-9882-11CF-9FA9-00AA006C42C4)   在列表之中。       要创建一个组件分组的子键,你的控件必须包含以下步骤:     1.创建一个组件分组管理器(Component   Categories   Manager)实例来接收   ICatRegister   接口的地址。     2.设置正确的   CATEGORYINFO   结构分量。     3.调用   ICatRegister::RegisterCategories   方法,将初始化的   CATEGORYINFO   结构变量传递给这个方法。           下面的例子演示如何使用这些步骤来完成一个名称为   CreateComponentCategory全局函数,生成组件分组。     #include   "comcat.h"     HRESULT   CreateComponentCategory(CATID   catid,WCHAR*   catDescription)     {     ICatRegister*   pcr   =   NULL   ;     HRESULT   hr   =   S_OK   ;             //创建一个组件管理器实例(进程内)     hr   =   CoCreateInstance(CLSID_StdComponentCategoriesMgr,     NULL,CLSCTX_INPROC_SERVER,IID_ICatRegister,(void**)&pcr);     if   (FAILED(hr))     return   hr;             //   确信   HKCR\Component   Categories\{..catid...}   键已经被注册     CATEGORYINFO   catinfo;     catinfo.catid   =   catid;     catinfo.lcid   =   0x0409   ;   //   英语             //   确信提供的描述在127个字符以内     int   len   =   wcslen(catDescription);     if   (len>127)     len   =   127;     wcsncpy(catinfo.szDescription,catDescription,len);     //   确信描述使用'\0'结束     catinfo.szDescription[len]   =   '\0';             hr   =   pcr->RegisterCategories(1,&catinfo);     pcr->Release();             return   hr;     }         当一个子键被创建到需要的分组,控件应该注册到该分组,需要以下步骤:     1.创建一个组件分组管理器实例接收   ICatRegister   接口地址。     2.调用   ICatRegister::RegisterClassImplCategories   方法,将控件的   CLSID   和需要的   category   ID   作为参数传递给函数。             下面的例子演示如何将一个名称为   RegisterCLSIDInCategory   加入实例控件。             #include   "comcat.h"     HRESULT   RegisterCLSIDInCategory(REFCLSID   clsid,CATID   catid)     {     //   注册你的组件分组信息     ICatRegister*   pcr   =   NULL   ;     HRESULT   hr   =   S_OK   ;     hr   =   CoCreateInstance(CLSID_StdComponentCategoriesMgr,     NULL,CLSCTX_INPROC_SERVER,IID_ICatRegister,(void**)&pcr);     if   (SUCCEEDED(hr))     {     //   注册已实现的类到分组     CATID   rgcatid[1]   ;     rgcatid[0]   =   catid;     hr   =   pcr->RegisterClassImplCategories(clsid,1,rgcatid);     }             if   (pcr   !=   NULL)     pcr->Release();             return   hr;     }             一个控件应该在调用   DLLRegisterServer   函数是注册安全性初始化和脚本操作。(DLLRegisterServer   由组件对象模型   [COM]   调用创建注册表入口)   在实例组件中   DLLRegisterServer   函数调用了   CreateComponentCategory   和   RegisterCLSIDInCategory   函数   (它们保证控件的安全性初始化和脚本操作)。下面的就是   DLLRegisterServer   的实现。             STDAPI   DllRegisterServer(void)     {     HRESULT   hr;   //   return   for   safety   functions             AFX_MANAGE_STATE(_afxModuleAddrThis);             if   (!AfxOleRegisterTypeLib(AfxGetInstanceHandle(),_tlid))       return   ResultFromScode(SELFREG_E_TYPELIB);             if   (!COleObjectFactoryEx::UpdateRegistryAll(TRUE))     return   ResultFromScode(SELFREG_E_CLASS);             RegisterTwo(CLSID_SafeItem);             return   NOERROR;     }             HRESULT   RegisterTwo(REFCLSID   clsid)     {     //   注册控件是安全性初始化的     /     HRESULT   hr;     hr   =   CreateComponentCategory(CATID_SafeForInitializing,   L"Controls   safely   initializable   from   persistent   data!");     if   (FAILED(hr))     return   hr;         hr   =   RegisterCLSIDInCategory(clsid,   CATID_SafeForInitializing);     if   (FAILED(hr))     return   hr;     //   注册控件是安全性脚本操作的         hr   =   CreateComponentCategory(CATID_SafeForScripting,   L"Controls   safely   scriptable!");     if   (FAILED(hr))     return   hr;         hr   =   RegisterCLSIDInCategory(clsid,   CATID_SafeForScripting);     if   (FAILED(hr))     return   hr;         return   S_OK;     }     作为一个创建所有安全性分组入口到注册表的控件,它也应该负责卸载所有的分组信息。COM   调用控件的   DLLUnRegisterServer   函数删除相应的注册表入口然后卸载该控件。             要卸载一个安全性初始化和脚本操作控件,控件应该完成以下任务:     1   创建一个组件分类管理器实例接收   ICatRegister   接口地址。     2   调用   ICatRegister::UnRegisterClassImplCategories   方法,将控件的   CLSID   和必要的   category   ID   作为参数传递             下面的例子演示如何完成一个   UnRegisterCLSIDInCategory   。             HRESULT   UnRegisterCLSIDInCategory(REFCLSID   clsid,CATID   catid)     {     ICatRegister*   pcr   =   NULL   ;     HRESULT   hr   =   S_OK   ;             hr   =   CoCreateInstance(CLSID_StdComponentCategoriesMgr,     NULL,CLSCTX_INPROC_SERVER,IID_ICatRegister,(void**)&pcr);     if   (SUCCEEDED(hr))     {     //   从分组卸载组件     CATID   rgcatid[1]   ;     rgcatid[0]   =   catid;     hr   =   pcr->UnRegisterClassImplCategories(clsid,1,rgcatid);     }             if   (pcr   !=   NULL)     pcr->Release();             return   hr;     }             我们前面提过,一个控件负责删除安全性初始化和脚本操作入口,下面演示如何完成这两个步骤:               STDAPI   DllUnregisterServer(void)     {     AFX_MANAGE_STATE(_afxModuleAddrThis);         if   (!AfxOleUnregisterTypeLib(_tlid,   _wVerMajor,   _wVerMinor))     return   ResultFromScode(SELFREG_E_TYPELIB);         if   (!COleObjectFactoryEx::UpdateRegistryAll(FALSE))     return   ResultFromScode(SELFREG_E_CLASS);         UnRegisterTwo(CLSID_SafeItem);         return   NOERROR;     }         HRESULT   UnRegisterCLSIDInCategory(REFCLSID   clsid,CATID   catid)     {     ICatRegister*   pcr   =   NULL   ;     HRESULT   hr   =   S_OK   ;         hr   =   CoCreateInstance(CLSID_StdComponentCategoriesMgr,     NULL,CLSCTX_INPROC_SERVER,IID_ICatRegister,(void**)&pcr);     if   (SUCCEEDED(hr))     {     //   从分组卸载组件     CATID   rgcatid[1]   ;     rgcatid[0]   =   catid;     hr   =   pcr->UnRegisterClassImplCategories(clsid,   1,   rgcatid);     }         if   (pcr   !=   NULL)     pcr->Release();         return   hr;     }     附件,   CXFMARK.h     以上函数形成一个文件,     #include   "comcat.h"         const   GUID   CATID_SafeForInitializing   =       {0x7DD95802,0x9882,0x11CF,{0x9F,0xA9,0x00,0xAA,0x00,0x6C,0x42,0xC4}};     const   GUID   CATID_SafeForScripting   =       {0x7DD95801,0x9882,0x11CF,{0x9F,0xA9,0x00,0xAA,0x00,0x6C,0x42,0xC4}};   HRESULT   CreateComponentCategory(CATID   catid,WCHAR*   catDescription)     {     ICatRegister*   pcr   =   NULL   ;     HRESULT   hr   =   S_OK;         //创建一个组件管理器实例(进程内)     hr   =   CoCreateInstance(CLSID_StdComponentCategoriesMgr,     NULL,CLSCTX_INPROC_SERVER,IID_ICatRegister,(void**)&pcr);     if   (FAILED(hr))     return   hr;         //   确信   HKCR\Component   Categories\{..catid...}   键已经被注册     CATEGORYINFO   catinfo;     catinfo.catid   =   catid;     catinfo.lcid   =   0x0409   ;   //   英语         //   确信提供的描述在127个字符以内     int   len   =   wcslen(catDescription);     if   (len>127)     len   =   127;     wcsncpy(catinfo.szDescription,catDescription,len);     //   确信描述使用'\0'结束     catinfo.szDescription[len]   =   '\0';         hr   =   pcr->RegisterCategories(1,&catinfo);     pcr->Release();         return   hr;     }         HRESULT   RegisterCLSIDInCategory(REFCLSID   clsid,   CATID   catid)     {     //   注册你的组件分组信息     ICatRegister*   pcr   =   NULL   ;     HRESULT   hr   =   S_OK   ;     hr   =   CoCreateInstance(CLSID_StdComponentCategoriesMgr,     NULL,CLSCTX_INPROC_SERVER,IID_ICatRegister,(void**)&pcr);     if   (SUCCEEDED(hr))     {     //   注册已实现的类到分组     CATID   rgcatid[1]   ;     rgcatid[0]   =   catid;     hr   =   pcr->RegisterClassImplCategories(clsid,1,rgcatid);     }         if   (pcr   !=   NULL)     pcr->Release();         return   hr;     }         HRESULT   UnRegisterCLSIDInCategory(REFCLSID   clsid,CATID   catid)     {     ICatRegister*   pcr   =   NULL   ;     HRESULT   hr   =   S_OK   ;         hr   =   CoCreateInstance(CLSID_StdComponentCategoriesMgr,     NULL,CLSCTX_INPROC_SERVER,IID_ICatRegister,(void**)&pcr);     if   (SUCCEEDED(hr))     {     //   从分组卸载组件     CATID   rgcatid[1]   ;     rgcatid[0]   =   catid;     hr   =   pcr->UnRegisterClassImplCategories(clsid,   1,   rgcatid);     }         if   (pcr   !=   NULL)     pcr->Release();         return   hr;     }         //下面来两个函数分别在DllRegisterServer和DllUnregisterServer中使用,参数均为控件的GUID。     HRESULT   RegisterTwo(REFCLSID   clsid)//   注意clsid就是控件的GUID     {     //   注册控件是安全性初始化的     /     HRESULT   hr;     hr   =   CreateComponentCategory(CATID_SafeForInitializing,   L"Controls   safely   initializable   from   persistent   data!");     if   (FAILED(hr))     return   hr;         hr   =   RegisterCLSIDInCategory(clsid,   CATID_SafeForInitializing);     if   (FAILED(hr))     return   hr;     //   注册控件是安全性脚本操作的         hr   =   CreateComponentCategory(CATID_SafeForScripting,   L"Controls   safely   scriptable!");     if   (FAILED(hr))     return   hr;         hr   =   RegisterCLSIDInCategory(clsid,   CATID_SafeForScripting);     if   (FAILED(hr))     return   hr;         return   S_OK;     }         HRESULT   UnRegisterTwo(REFCLSID   clsid)   //   注意clsid就是控件的GUID     {     //   删除注册表入口     HRESULT   hr;     hr   =   UnRegisterCLSIDInCategory(clsid,   CATID_SafeForInitializing);     if   (FAILED(hr))     return   hr;     //REFCLSID     hr   =   UnRegisterCLSIDInCategory(clsid,   CATID_SafeForScripting);     if   (FAILED(hr))     return   hr;         return   S_OK;     }   

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值