web威胁感知

1. 异常模型

1.1 参数异常模型

回顾一下Web威胁中的几大类攻击,SQLi、XSS、RCE……等虽然攻击方式各不相同,但基本都有一个通用的模式,即通过对参数进行注入payload来进行攻击,参数可能是出现在GET、POST、COOKIE、PATH等等位置。所以第一个异常模型,我们希望能覆盖掉参数中出现的异常,这样就能覆盖掉很大一部分的常见的Web攻击。

1.1.1 模型原理

假设有这样一条url:www.xxx.com/index.php?id=123。如果我们拉出所有这条url的访问记录,不难发现:正常用户的正常请求虽然不一定完全相同,但总是彼此相似;攻击者的异常请求总是彼此各有不同,同时又明显不同于正常请求。
在这里插入图片描述
正常总是基本相似,异常却各有各的异常。
基于这样一条观测经验,如果我们能够搜集大量参数id的正常的参数值,建立起一个能表达所有正常值的正常模型,那么一切不满足于该正常模型的参数值,即为异常。
在这里插入图片描述
异常由正常决定
如果我们把参数id的每个参数值看作一个序列(Sequence),那么参数值中的每个字符就是这个序列中的一个状态(State)。同时,对于一个序列,为123或者124甚至是345,其背后所表达的安全上的解释都是「数字 数字 数字」,我们用「N」来表示「数字」,这样就得到了对应的隐含序列。
在这里插入图片描述
参数值安全上的解释
到这里已经隐约看到了隐马尔可夫的影子。对于数字我们用N来表示,相应的对于其他unicode字符,也做类似的泛化对应关系。英文字符、中文字符、中文标点字符以及其他语言的所有字符对应状态「A」,对控制字符以及英文标点字符,对应隐含状态为自身。
在这里插入图片描述
观测序列与隐含序列
这样做的原因是因为通常一个参数注入式的Web攻击payload是由一些攻击关键词,加上一些特殊符号构成。特殊符号起到闭合前后正常语句,分隔攻击关键词的作用。通常这些特殊字符为英文标点符号、控制字符等,所以对这些字符不做泛化的对应。
在这里插入图片描述
XSS payload
但是,也有一些特殊情况,如上几个XSS payload是利用了字符集编码转换:故也可以考虑对「㸀㰀シセ¼¾……」等几个特殊字符单做处理。
在这里插入图片描述
隐马尔可夫模型
回顾一下HMM的三类应用:

解码问题,根据模型参数和观测序列,找出该观测序列最优的隐含状态序列;评估问题,根据模型参数和观测序列,计算该观测序列是由该模型生成的概率;学习问题,根据一系列观测序列,建立对应该系列序列最优的HMM模型。

这里我们只用得到后两个。在训练阶段,对应「学习问题」,用大量正常的参数值训练出站点www.xxx.com下index.php下的参数id的HMM模型;在检测阶段,对应「评估问题」,待检测的参数值带入模型检测是否是正常。
在这里插入图片描述
参数异常模型
不同的参数,正常的值不同。同时,有参数传递的地方,就有可能发生参数注入型攻击。所以,需要对站点下所有路径下,所有GET、POST、PATH、COOKIE中的所有参数都训练各自的正常模型。另外,对参数名本身,也训练其正常的模型,见如下case:
dedecms payload
dedecms payload

1.1.2 工程实现

整个模型包含4个主要模块,抽取器「Extractor」、训练器「Trainer」、检测器「Detector」、重训练器「reTrainer」。

对每条Http原始日志,先经过Extractor进行参数拆解,各种ETL,解码等处理。这一步是最容易描述但确是最难做好的。比如URL中虽然都是百分号编码,但字符集却有GBK、UTF-8、GB2312等等,如何选择正确的字符集来解码?再比如POST传递参数,可以通过urlencoded、multipart/form-data、json或xml等,如何保证能够正确的提取?
在这里插入图片描述
Extractor
良好的数据质量,才能保证上层建筑的精准。有句话是这么说的「宁愿去洗厕所,也不宁愿洗数据」,但是不经历过数据之脏,如何有资格经历数据之美?「Extractor」出来的数据,根据是否已经有对应的训练好的模型,拆分为两部分,没有对应模型的数据进入Trainer开始训练流程。
在这里插入图片描述
Extractor 分割
上文中我们提到,我们要训练的是正常参数值从而得到正常模型。这就需要保证进入训练集中的数据都必须是正常数据。如果都是异常数据,那么得到的将是关于异常的模型,也就是说模型将会被污染。那么问题来了,如何在不知道什么是正常的情况下保证正常?
在这里插入图片描述
Trainer 过滤
第一步,对某个ip,只要其当天内所有请求中有一条命中了WAF,其余所有请求不管是正常的还是异常的,均不进入Trainer;如果某个ip命中扫描器特征或扫描器行为(这个模型将在另外一篇文章中介绍),该ip所有请求也不进入Trainer。
在这里插入图片描述
Trainer 抽样
第二步,对每个要训练的参数,每个ip每天只能贡献一次参数值。这样能保证上述过滤失效的情况下也只能有一条异常数据混进该参数的Trainer中。只要大多数进入训练池的数据是正常的,那么对模型的影响也不大。这就是「正常由大多数投票表决来决定」。同时,每个参数的训练池有最低条数的限制,没达到条数限制的参数不做训练,继续等待更多的数据进入Trainer。

有最低限制,就相应的有最高限制,对于部分数据量很大的参数,过多的训练数据会导致训练时间太长。对这种情况,我们再做一个分层抽样。
在这里插入图片描述观测序列与隐含序列
在模型原理部分我们提到观测序列与隐含序列的对应关系,但在工程实现中这样去做,会存在很大的问题 。比如训练集中id参数的观测序列全为「123abc」,相应的隐含序列为「NNNAAA」,训练好模型后,待检测序列为「124abc」。由于「4」这个观测状态不在训练集的状态空间中,所以会被直接判定为异常。
在这里插入图片描述
工程实现中的观测状态数与隐含状态数
但事实上我们并不需要这么「敏感」的异常。所以我们直接使用泛化后的序列「NNNAAA」作为观测序列,而隐含状态数取训练集中所有观测序列的观测状态数均值四舍五入。

模型完成训练后,需要设定一个异常的阈值。什么样的概率值的序列为异常?0.8,0.5?如果训练集中所有参数值均为正常,那么只需取训练集中的最低概率值为阈值即可。但即便之前我们做了这么多步,也是有可能混入一两条异常数据进入训练集的。这里我们可以简单的使用3sigma来抵消,如果最低概率值位于3sigma区间外,取次低概率值,再求3sigma,如此反复。

「Trainer」部分结束后,开始「Detector」部分。从「Extractor」出来的有对应Model的数据,直接开始检测,如果概率P<异常概率阈值H – epsilon小量,则认为是异常,epsilon = (1/100) H。异常的数据最终再由data_id还原出对应的原始Http数据。

任何模型都有衰减期,尤其是攻防模型。昨天的异常不一定今天就是异常,这就需要有一个重训练模块「reTrainer」来持续迭代训练模型,用以抵消衰减的影响。比如/index.php?id=123在训练时id为123是正常的,但之后该Web应用修改了代码,正常的参数值变成了/index.php?id=abc+||+123,如果模型一层不变,所有之后的abc++123都会被认为是异常。
在这里插入图片描述
reTrainer
那么什么时候需要开始重训练呢?当过去的「大多数」已经不能代表现在的「大多数」的时候。若大量不同人对某个参数出现大量相同序列的异常,且这些异常都不会命中威胁模型(下一节将介绍到)。同时,数量上远大于对应的模型的训练集中的数据量,训练集中的参数值序列也持续一段时间没有再出现过,则将这部分异常开始重训练。至此,整个参数异常模型部分结束。下图为参数异常模型流程。
在这里插入图片描述

1.2 节点异常模型

不能寄希望于一个模型就能覆盖掉所有攻防上的异常,比如Webshell、敏感文件下载等这类Web威胁,在参数异常模型中,不会触发任何异常。所以,我们考虑从另一个角度,来覆盖这类节点异常。

1.2.1 模型原理

如果把站点看作一张大图,站点下的每个页面为这张大图中的每个节点,而不同页面之间的链接指向关系为节点与节点之间的有向边,那么我们能画出如下这张有向图:
在这里插入图片描述
Web Directed Graph
节点是否是异常,由其所处的环境中的其他节点来决定。类似一个简化版的PageRank,如果大量其他节点指向某个节点(入度较大),那么该节点是异常的概率就很小。相反,如果一个节点是Graph中的孤立点(入度为0),则是异常的概率就很大。

单有这张有向图还不够,诸如/robots.txt、/crossdomain.xml之类的正常节点却又无其他节点指向的情况太多了,这个层面的异常能表达的信息量太少,所以我们还需要引入另一个异常。

通常一个异常节点(如Webshell),大多数正常人是不会去访问的,只有少量的攻击者会去访问(这里不考虑修改页面写入webshell的情况,这个模型不能覆盖这类case)。用一个简单的二部有向图就能很好表达。入度越少的节点,同样越有可能是异常。
在这里插入图片描述
节点异常模型
联合两张图中的异常,其联合异常就会比单独任一张图产出的异常更具表达力。该模型较为简单,不再赘述。

1.2.2 工程实现

整个模型包含3个主要模块,抽取器「Extractor」、训练器「Trainer」、检测器「Detector」。对每条Http原始日志,同样先经过「Extractor」进行路径抽取,各种ETL,解码等处理。这里需要由于有向图中的边关系由referer->path来确定,而referer是可以轻易伪造的,所以,需要对fake referer做个检测。
在这里插入图片描述
Session Navigation Pattern
先做一个session identification,对每个session块中的数据,分析其导航模式。正常的referer必然会出现在当前session中之前的path数据中,同时,能够与当前session之前的数据形成连通路径。当然,导航模式并不能完全检测所有的伪造referer,攻击者完全可以先访问A,再构造一个referer为A访问B的请求。不过没关系,后面还有方法能避免这些数据进入Trainer。

「Extractor」出来后的数据,直接进入「Detector」,Detector检测path是否在有向图中,如果在,则更新有向图中节点的最后访问时间;如果不在则认为是有向图异常,进入到二部图中。二部图维护一个N天的生命周期,如果某path节点总的入度小于L,则为最终异常。

「Trainer」每天从二部图中过滤掉当天命中WAF、扫描器特征或扫描器行为的源ip所有请求,若节点总入度大于M的节点,则迭代节点极其链接关系加入到有向图中,并在二部图中去除该节点以及对应边。同时,对于有向图中最后访问时间超过30天的节点直接丢弃。下图为节点异常模型流程。
在这里插入图片描述

2. 威胁模型

2.1 攻击识别

99%的异常事实上都不是什么攻击,异常的发现并不难,难的是对异常的解读,或者说赋予异常一个安全业务上的解释。理论上来说,如果能有足够丰富的各个层面的数据,能够对每个异常都做出一个合理的解释。

上文中我们提到,多数的普通Web攻击都是由「特殊字符」加上「攻击关键词」这种模式构成。而参数异常模型本身就能够很好的表达「特殊字符」,剩下的我们其实只要能表达「攻击关键词」这一层的信息即可。

所以我们想尝试在参数异常数据的基础上,注入一些领域知识,从而构成一个分类器,从异常中剥离出攻击,并且能够对不同种类的攻击进行分类。但我们又不太想人为构造样本人工打标记,因为这样又会带来一些个体因素的影响,我们希望能用真实世界中的流量来获得领域知识。

首先第一步,我们先从WAF中提取近几年的数据,对各个类别的攻击payload做一个简单的分词。然后再从参数异常模型的历史数据中提取大量的「绝对正常」样本,也做一个简单的分词。显而易见,如果某一个词(Term)在一类攻击payload样本中出现的次数越多,那么我们认为该词与该类攻击的相关程度越大。同时,不同的词的重要程度是不同的,如果该词只在这类攻击中出现,而在正常样本中几乎没有出现,那么该词对与该类攻击重要性更高。
在这里插入图片描述
TF-IDF
自然就联想到了TF-IDF。数学意义上,TF用来表达相关程度,IDF用来表达重要程度。在TF中,分子部分,表示i这个term在攻击类别j中出现的次数。为了避免对「短payload攻击」的不利,需要将词数(term count)转换为词频(term frequency),所以分母部分表示在攻击类别j中所有term出现的总次数。在IDF中,分子部分表示所有样本库中(包含正常和攻击)的样本总数,分母部分表示包含term的样本数,加1是为了避免为0,取对数是为了表达tearm的信息量。最终TF与IDF二者相乘,用来刻画一个term对该类攻击的描述程度。

接下来就涉及到分类器的选择,事实上我们在测试了多个分类器后,发现在该场景下,仅仅就用简单的基于规则的分类器就已能满足大部分的情况。如果还想继续提高精度,可以尝试再表达term与term之间的顺序关系,如二元gram、长亭科技提到的基于编译原理的语法分析等等。另外一种思路是直接描述整个payload的「结构」特征。例如如下两个payload,如果采用类似参数异常模型中对序列泛化的思路(长度上也做压缩泛化),将得到一条相同的泛化序列:
在这里插入图片描述
payload 结构
不难发现,其结构上是相同的。事实上我们抽取了WAF 1000w+真实环境中的SQLi payload分析后发现,其泛化后的序列只有几万。所以从「结构」这条路上去探索,也是个不错的选择。

2.2 识别成功的攻击

在异常中,99%的异常都不是攻击,而在攻击中,99%的攻击又都是无害的攻击。而我们的精力总是有限的,我们希望能关注那些危害程度更高的攻击,这就迫使我们需要从攻击中识别出哪些是成功的攻击。

Kill Chain的思想本身是很好的,在攻击者的攻击链路上的几个关键节点,如果能串联起来,说明这是一次成功的攻击。但是Kill Chain设计最初是为了检测APT,所以在Web威胁中,我们只需要借鉴这种思路,而没必要生搬硬套7个阶段。
在这里插入图片描述
Kill Chain Model
Kill Chain的本质还是多源异构数据的关联,攻击路径上不同层面的数据来建立联系。我们可以采用很简单的二步验证,如,一个Http层出现的SQLi paylaod,相同的payload同时出现在SQL层的异常,即形成一个确认的SQLi攻击;同理,一个Http层的异常相同的payload出现在了命令日志层面的异常中,即形成一个确认的RCE。(对于其他非Http层数据的异常,会在之后的文章中介绍)
在这里插入图片描述
异构数据关联
相同的思路,我们也可以同安全产品的数据来关联。如,关联WAF数据,关联不上的即为WAF Bypass;成功的攻击数据同扫描器数据来关联,关联不上的即为扫描器漏报等等。

2.3 CMS Nday识别

涉及到Http这一层的数据,想必大家最感兴趣的,就是如何挖掘出一些Web CMS Nday。值得一提的是,如果只是挖出一些「可能」是Nday的请求,意义并不是很大,在大规模的环境中这样的请求每天实在是太多了,多到让人力不从心。抽象这个过程,我们希望能做到两点:1. 明确知道这是一个Nday;2. 明确知道这是哪个CMS的Nday。

Nday首先一定是一个攻击,所以不需要再从异常数据开始,攻击分类器中出来的数据可以直接作为源数据。但反过来攻击却不一定是Nday,通常情况下,大多数Nday是带Exp性质而非PoC性质的。比如SQLi的CMS Nday多数是直接读管理员表数据之类的「利用」行为,而非是「and 1=1」之类的「证明」行为。

而对于第二点,「知道这是哪个CMS」背后对应的其实是一个Web应用指纹识别的过程,不过比传统的应用指纹识别更具挑战的是,因为无法知道哪个页面会出现Nday,所以只能通过一条Http数据,就要识别出对应的CMS。

另外,如果站在攻击者角度来考虑的话,Nday通常不会只出现在一个站点上。这一点很重要,能帮助我们过滤掉很多噪声数据的同时,也能让我们感知某个Nday是否在大规模利用。
在这里插入图片描述
Nday 行为
我们先从SQLi Nday尝试入手,观察如下这条payload:
在这里插入图片描述
SQLi Nday
多数的SQLi Nday,都会有一个「from xxx」的pattern,而xxx为具体某个CMS数据库的表名,同时payload中也会出现CMS数据库的字段名。豁然开朗,我们需要做的,其实只是建立一个CMS DB Schema指纹库,以CMS表名和字段名作为指纹。一举两得,既能判定出是Nday,又能同时找到对应的CMS。

下面再来看看GetShell类型的Nday。多数的Getshell Nday,都会有「<? eval」「<? fputs」之类的webshell代码特征,或者是文件包含特征等等。同时,我们可以取路径以及参数名作为uri指纹用来识别CMS。
在这里插入图片描述
GetShell Nday
db指纹很容易建立,相比之下uri指纹就不那么容易建立了,我们需要借力于Web指纹识别产品。从指纹识别产品中提取CMS为wordpress的站点top 10000个,然后提取这1w个站点7天内的所有200的请求。去参数值后,分别对每条uri提取路径指纹和参数值指纹。只有多个wordpress站点都同时具有该指纹,并且其他CMS没有该指纹的,才最终进入wordpress的uri指纹库。同理,其他CMS的建立过程也类似。
在这里插入图片描述
CMS Nday 识别流程
整个识别流程如上图所示。先取出攻击分类器中出来的攻击数据,并且这些uri+payload出现在了多个站点上。紧接着,满足SQLi pattern的数据同db指纹库匹配,得到SQLi Nday;满足GetShell pattern的数据同uri指纹库匹配,得到GetShell Nday。

2.4 Webshell识别

以上几个场景都是基于参数异常的数据的应用,而对于节点异常数据,最为直接的应用就是识别Webshell。先来回想一下,安全工程师在做入侵复盘时怎么来确认一个url是否为webshell:1.从Http数据中发现一个url有点「异常」;2.访问一下该uri,通过安全经验知识从返回页面识别出是webshell(大马);3.如果返回页面为空白(疑似一句话),从代码层面来识别。

2.4.1 大马识别

先来看一下大马的识别。对于第一点节点异常数据已有,而第二点,本质上其实是引入response层面信息做网页相似度计算。对于相似度计算,不妨先将问题退化为「网页相同计算」。
在这里插入图片描述
webshell phpspy
问题变得非常好解。搜集大量的webshell样本,训练阶段对response数据计算md5,检测时对待检测样本同样算md5比对即可。显而易见,这种方法准确率极高,同样缺陷也足够明显,严重依赖训练样本库,也就是只能检测已知,不能感知未知。

而人总是贪婪的,一方面我们既想对已知保持高的检测精度,同时又希望能保持一些对未知的感知能力。自然而然就想到了simhash之类的LSH。类似md5、sha1之类的属于加密型hash,其设计的目的是为了让整个分布尽可能地均匀。所以对输入极其敏感,轻微变化的输入,就会导致输出大不一样。而我们更希望对相似的输入,产生相似的输出。从而通过对输出做相似计算,就能反映出输出的相似性。然而事实上simhash在这个场景下效果并不是很好,原因是simhash主要适用于文本内容的相似性检测。

抛开全文hash的想法,有一类方法是去关注全文中的片段。直观经验告诉我们,如果两个全文比较相似,那么全文中的部分全片,也很有可能很相似。反过来我们如果能计算出片段的相似或相同,进而就能得到全文的相似性。在网页判重领域,有一种简单而又有效的方法叫做最长句子签名。取网页内容中最长的top3的句子作为签名,检测时对签名做比对或者计算编辑距离。
在这里插入图片描述
webshell特征码识别
同样,杀毒领域的特征码技术也是类似的思想。应用到webshell检测的场景下就是,随机或特意的选取webshell response中多个片段作为片段指纹,记录下偏移位置和特征值,用来表征该webshell。这类方法都具有较高的对已知的检测精度,缺点依然是对新样本感知能力尚不够。

回到业务场景,一个网页主要包含两大部分:网页结构和网页内容。通常情况下,黑客对自己使用的webshell会做各种修改,但修改的大多都是网页内容,而不会去动网页结构。所以其实我们在该场景下更需要关注结构的相似而不是内容的相似。

(注:以下思路和模型均归属李景阳先生专利所有[1])要做结构的相似计算,首先我们需要对整个DOM结构找一个合适的数学对象来描述。常见的适合比较运算的数学对象有标量、向量、矩阵等,借鉴Vector Space Model的思想,我们选用高维向量来描述DOM结构。VSM中以一个词项作为向量空间的一个维度,词频作为权重,得到高维向量V(d)=(t1,ω1(d),…, (tn, ωn(d))。对应到DOM结构中,我们尝试用N-gram片段来描述每个维度的向量。

每个tag作为一元gram,相邻兄弟tag和相邻父子tag作为二元gram,去除掉如html、head、body之类的通用tag。同时,仅仅使用tag名称会损失掉很多表达结构样式的信息,所以我们取tag+部分表达格式的属性,hash之后映射到某一维度。同时,对不同的gram赋予不同的权值,用来体现其表达结构特点的能力,最终得到一个高维实向量。
在这里插入图片描述
特征向量抽取
直接计算高维向量一方面不同网页结构大小各有不同,形成的向量会变得很稀疏,另一方面,过高的维度也会带来「维数灾难」,所以我们还需要做一个降维处理。但在该场景下,没必要用太复杂的降维或projection手法,直接对M取模,将N维向量折叠压缩到M维即可。

构造好低维实向量,接下来就该定义相似度计算了。直观上,欧式距离和余弦距离是最容易想到的两种计算向量相似的距离。但是对长网页欧式明显不公平,而余弦只关注向量夹角的差异性不关注绝对长度上的差异,也就是说只关注tag之间的比例而非绝对数值,又会存在存在对长网页过分抑制。所以综合考虑,我们借鉴杰卡德相似的思想,定义伪距离如下,表达的物理意义为两个网页相同部分与不同部分的比值。
在这里插入图片描述
定义伪距离
有了向量和相似性计算,我们就不得不面临一个工程问题,这种笛卡尔积式的两两比较,在大规模环境中来应用就是灾难。回想一下生物指纹识别领域中的待识别指纹同指纹库中指纹匹配的过程:1.先比对指纹的模式区、核心区等大的总体特征;2.满足总体特征的,再比对特征点等局部特征。借鉴此思想,我们可以对离散的样本数据构造一个网格计算。

在向量空间中定义一套超立方网格,把空间切割成多个超网格,以超网格中心座标来表示其自身。对于待检测样本,先计算其会落到哪个超网格,然后再同该超网格中的训练样本进行一一比对,最终找到最相似的样本。但是需要注意的是,一套网格会出现「区域隔离」的问题,可以通过构造不同的多套网格来避免。
在这里插入图片描述
离散样本网格化
如图T1是待检测网页的特征向量,在红色网格中,命中了超网格L1,进而找到在红色网格中最相似样本为S1。而在黑色网格中,同样的方法找到了样本S2。最终再来比较与S1、S2的伪距离,得到最相似样本S2。

2.4.2 一句话webshell识别

对于一句话webshell,response层面数据不需要做太复杂的模型,简单的特征匹配就足够。或者也可以采用2.2中二步验证的方法来验证。如,联合主机agent做代码层面的关联,或者关联命令日志异常,又或者关联2.3中的GetShell Nday等等。

结语

在本文的最后,我们想表达一些对于数据驱动安全的看法。尽管在上文中我们介绍的都是模型在安全业务上的应用,但我们想强调的一点是,数据驱动的安全,一定不是靠模型来驱动。对模型的趋之若鹜与过于迷信,掩盖的是对数据对威胁的认知不足。在安全数据科学领域,只有对业务领域有充分认识的前提下,才有可能将数学上的解释对应到安全上的解释,从而才有可能产生一些有用的浅层线索。

但是永远记得,多数情况下模型产生的也只是浅层线索,最终数据驱动安全的顶层设计,一定是充分发挥机器智能与人的智能,机器做机器擅长的计算而人完成人擅长的分析。有机的结合二者,才能成为对抗威胁不对称的强力武器。所以在之后的文章中,我们希望能够回归数据本身,探索一些数据与数据之间的联系,以及通过这些联系我们能感知什么。

本文中提到的这些尝试永远只能覆盖小部分浅层的Web攻击,同复杂而又残酷的黑产威胁相比,这些都还只能算「温室里的宝宝,把玩着自己的小玩具」。我们开开心心的在充满阳光的温室里搭建着自己的小城堡,以为以此就能抵挡得了小怪兽。但殊不知黑产的攻击面,大多根本不在温室里。而温室的外面,没有熟悉的阳光,有的只有一无所知的黑暗。需要我们去探索的还有很多,对于未知,我们都才刚刚起步。

参考

[1] 李景阳 张波. 网页结构相似性确定方法及装置

[2] 威胁感知的方法论

转载自:数据科学在Web威胁感知中的应用(一)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值