深度学习——从网络威胁情报中收集TTPs

摘要

long long ago,我作为小白接触了网络安全领域,我们导师安排我们研究了网络威胁情报(Cyber Threat Intelligence,CTI)。这篇博客作为对我硕士研究生涯的总结,将会向大家解释如何利用深度学习从网络威胁情报中获取行为描述,形成TTPs实体。

为啥要用网络威胁情报

被动防御 & 主动防御

在理解为啥要使用网络威胁情报帮助网络安全之前,首先要理解主被动防御。根据 百度文库 的相关概念,主被动防御可以被解释为:

  • 被动防御
    为降低恶意⾏为⼏率以及尽量减少恶意⾏为引发的损害⽽采取的措施,⽽⾮主动采取⾏动
  • 主动防御
    在⼊侵⾏为对计算机系统造成恶劣影响之前,能够及时精准预警,实时构建弹性防御体系,避免、转移、降低信息系统⾯临的风险的主动防御

根据上述概念,被动防御的劣势在于滞后性:即有“特征”,才有方法。这种性质使得在网络威胁爆发的时代背景下,防御无法跟得上进攻的速度。因此,提出主动防御,其目的是在攻击发生前判断自身遭受攻击的时间、对象、技术方案、对手等信息,提前布局,避免盲目防御,使得原本“被动挨打”的局面能够形成“知己知彼,百战不殆”的局面。

而要想形成该局面,就意味着“己方和对手”的信息都必须要了解。恰巧,网络威胁情报,能够提供相关帮助。

网络威胁情报的概念

何为情报(Intelligence)?

根据 知乎-Osintclub 的解释:

情报的三个要素:需求,信息,展现。

成分说明
需求情报一定要有需求方,把信息提供给根本不需要这些信息的一方,哪怕这些信息可能很全、很准,也没有丝毫意义,这些信息不构成情报。
信息就是情报一定要有信息源,没有或者得不到相关数据以及由数据提炼出的信息,情报则无从谈起。
展现就是情报一定要以某种形式展示,包括但不局限于眼神、手势、语音、文字、图表、视频等各种展现形式。

因此,研究“情报”,本质上是:"在特定的需求下,针对特定的目标进行收集、处理、展示信息"的过程。

何为网络威胁(Cyber Threat)?

根据 海在阳光下 的回答:

网络安全的威胁包括窃听、重传、伪造、篡改、非授权访问、拒绝服务攻击、行为否认、旁路控制、电磁/射频截获、人为疏忽。……(网络数据)有很多是敏感信息,甚至是国家机密。所以难免会吸引来自世界各地的各种人为攻击(例如信息泄漏、信息窃取、数据篡改、数据删添、计算机病毒等)。同时,网络实体还要经受诸如水灾、火灾、地震、电磁辐射等方面的考验。

知乎-ielab 对网络安全威胁的定义是:

网络安全威胁是指网络系统所面临的,由已经发生的或潜在的安全事件对某一资源的保密性、完整性、可用性或合法使用所造成的威胁。……网络安全威胁一般在四个方面:信息泄露、数据完整性破坏、合法业务的拒绝、资源非法使用等。

因此,本文认为:网络威胁,本质上是使用违规会违法操作,以网络环境为载体,对网络资源、环境实施窃取、破坏、静默、伪造、控制等系列行为,以实现金融、间谍等活动目标

何为网络威胁情报?

根据上述概念,不难推理出:网络威胁情报是与网络威胁相关的情报。

梦飞科技 的解释是

网络威胁情报 (CTI) 考虑了网络威胁的完整背景,以便为高度针对性的防御行动的设计提供信息。

根据 Gartner 对威胁情报的定义

威胁情报是某种基于证据的知识,包括上下文、机制、标示、含义和能够执行的建议,这些知识与资产所面临已有的或酝酿中的威胁或危害相关,可用于资产相关主体对威胁或危害的响应或处理决策提供信息支持。

根据 ATT&CK 的定义

Cyber threat intelligence is all about knowing what your adversaries do and using that information to improve decision-making.

使用网络威胁情报,本质上是利用信息分析网络威胁的过程。根据该过程,研究者 一般倾向于使用4种类型和5个步骤表示。

情报类型说明
战略威胁情报提供一个全局视角看待威胁环境和业务问题,它的目的是告知执行董事会和高层人员的决策。战略威胁情报通常不涉及技术性情报,主要涵盖诸如网络攻击活动的财务影响、攻击趋势以及可能影响高层商业决策的领域
运营威胁情报与具体的、即将发生的或预计发生的攻击有关。它帮助高级安全人员预测何时何地会发生攻击,并进行针对性的防御
战术威胁情报关注于攻击者的TTP,其与针对特定行业或地理区域范围的攻击者使用的特定攻击向量有关。并且由类似应急响应人员确保面对此类威胁攻击准备好相应的响应和行动策略
技术威胁情报主要是失陷标识,可以自动识别和阻断恶意攻击行为
分析步骤说明
明确需求和目标明确所需要的威胁情报类型,以及使用威胁情报所期望达到的目标。可以明确需要保护的资产和业务,评估其遭受破坏和损失时的潜在影响;明确其优先级顺序,最终确认所需要的威胁情报类型。
收集威胁情报收集从来源上包含如下渠道:企业内部网络、终端和部署的安全设备产生的日志数据;订阅的安全厂商、行业组织产生的威胁数据;新闻网站、博客、论坛、社交网络;一些较为封闭的来源,如暗网,地下论坛
分析分析环节是由人结合相关分析工具和方法提取多种维度数据中涵盖的信息,并形成准确而有意义的知识,并用于后续步骤的过程。常用的威胁情报分析方法和模型包括Lockheed Martin的Cyber Kill Chain,钻石分析法,MITRE ATT&CK
传播和分享当产生威胁情报后,需要按照需要进行传播和分享。对于企业内部,不同类型和内容的威胁情报会共享给如管理层。对于乙方的威胁情报服务商通常会采用威胁情报平台(TIP),或者直接以威胁情报数据服务提供,其中通常采用的威胁情报分享格式为STIX和OpenIOC
评估和反馈确认威胁情报是否满足原始需求和是否达到目的,否则就需要重新执行步骤1的阶段进行调整。

网络威胁情报的特点

网络威胁情报充分利用了“内部情报+外部情报”,能够帮助了解安全大环境、自身环境和威胁点。理论上,一切与网络威胁相关的信息都可以被称为网络威胁情报,因此网络威胁情报具备以下性质:

  • 多源异构
    网络威胁情报来源广泛。内源情报包括:日志、流量、IDS记录等;外源情报包括:漏洞数据、POC数据、IOC数据、攻击事件分析报告、攻击组织分析报告、恶意软件分析报告等。不同的来源决定了情报的信息组成有所差异,根据信息是否对机器阅读理解友好,可分为:
    • 结构化数据
      有具体结构,可以被特定工具直接解析形成数据结构的数据,例如JSON、CSV等
    • 非结构化数据
      无具体结构、形式,一般为自然语言文本,可能带插图、视频等多媒体信息
  • 长难文本
    网络威胁情报一般由专业安全人员撰写,因此理解内容上可能有门槛。同时由于撰写习惯、语言、侧重点的差异,网络威胁情报的内容千差万别且存在误差。
  • 数目惊人
    自计算机诞生便有安全问题,网络威胁情报的历史积累无比丰富。同时,随着网络技术发展,各类来源进入爆炸式增长。导致人工分析网络威胁情报的效率下滑。

因此,需要自动化方法辅助人工分析网络威胁情报

网络威胁情报的分析

网络威胁情报分析是在理解情报内容的前提下,收集、梳理、清晰、关联、推理出与分析目标相关的信息。分析目标一般可以是攻击发生的条件(OS环境、网络环境、漏洞条件)、实现的方法(入侵路径、C&C方法、窃取方法、破坏方法等)、特定的对象(数据、服务、硬件、人员)等。

David Bianco 在“痛苦金字塔” 系列博客 中,对分析的内容做了如下图的梳理。

痛苦金字塔
根据该图,分析内容可以分为从低到高、从简单到复杂、从具象到抽象的6个层级。一般情况下,大多数研究者把从Hash到Domain的层级划分为失陷指标(Indicators of Compromise,IOC),把TTPs(Tactics,Techniques, and Procedures)单独作为最高层级。

Ryan在其 博客 中,对分析目标做了相关定义:

分析目标分析需求获取难度定义
目标(Goals)最终目标极难目标是对手行为背后的真实意图。它们几乎不可能在一个环境中(直接)被发现,但最肯定的是可以通过情报行动来收集
战略(Strategy)次级目标极难战略是敌人实际上开始建立一个或多个实现这些目标的可行方法。特别地,一些军事模式可能解释了目标与战略之间存在的“战役”概念
战术(Tactics)间接目标较难战术指运用现有手段达到目的的艺术或技巧。战术不强调具体行为、原因和工具,而是概括地说明对手“正在做什么”
技术(Techniques)间接目标较难技术描绘了执行者具体执行任务、职能和职务的方式方法,具有个人在行为模式、背景、技能、习惯上的差异
过程(Procedures)间接目标较难过程是一系列以某种方式或顺序完成的行动。程序不是对单个原子指标的观察,而是对两个或多个指标的连续观察,这些指标确定了一个过程正在执行的趋势
工具(Tools)辅助目标困难敌人用来完成目标的任何工具,无论是恶意的还是良性的,都属于工具
主机和网络构件(Host & Network Artifacts)辅助目标困难主机和网络构件包含在主机、网络或事件数据级别遗留下来的任何工件,这些工件指示正在使用的工具或标识的TTPs。主机和网络构件还包含上下文,例如观察到它们的位置和方式,并且通常包括一个或多个原子指示符
原子指示器(Atomic Indicators)初始目标容易原子指示器是信息可能的最低分母,代表与工具使用或标识的TTP相关的信息和元数据的最低可分解级别。这些数据通常是按指示符类型组织的,可以很好地折叠成表和行,并作为“威胁情报”在社区中传递。这些例子包括IP、域、电子邮件地址、文件散列,甚至匹配原子指示符的正则表达式模式

在这种分析划分下,围绕网络威胁情报的分析将主要可以分为IOC分析和TTPs分析

网络威胁情报的IOC分析方法

虽然IOC分析不是本文的侧重点,但是是现阶段网络威胁情报的分析重点。因此,本文将简要说明该分析领域的方法。IOC分析根据方法对先验的依赖可以分为基于先验规则的方法和先验缺失的方法。前者一般可以是正则规则方法;后者包含机器学习和深度学习方法。

  • 正则方法
    正则方法主要考虑IOC的结构化特点。由于IP,域名等来源于人工制定的协议,因此机读特性丰富。
  • 机器学习/深度学习
    这类方法将网络威胁情报文本通过TF-IDF、Word2Vec等方法转化为数值矩阵,训练映射函数,拟合IOC实体在网络威胁情报的分布。

相比之下,正则方法由于规则是人工制定的,难以根据网络威胁情报内容判断抽取的IOC实体是否是恶意的、有效的。可能会出现把情报来源中正常的广告、表头等无关信息一起处理为网络威胁的情况。而机器/深度学习方法不依赖人工规则,而是在数据集中学习IOC实体或语义的规律,可以避免“一棒子打死全部”的情况,但依赖高质量的训练数据。

当然,随着人工智能技术的发展,逐渐出现了利用无监督、半监督、少样本、零样本为出发点的方法,利用后验技术,使得计算机自己学会验证IOC的抽取质量。这些方法不在本文的讨论范围内。

网络威胁情报的TTPs分析方法

铺垫了那么久,终于来到了本文的核心章节。与IOC分析不同,TTPs分析注重攻击行为的解释和关联。因此,针对网络威胁情报的TTPs分析,将不再止步于抽取IOC特征,而是需要根据IOC特征或其他行为描述,确定攻击行为发生的条件、技术方式、先后顺序,使得攻击“完完全全”地呈现。

因此,TTPs分析本身应该被分解为3个主要步骤:

  1. 定位特征
    从网络威胁情报中抽取关键IOC证据或人工分析人员表达的方式,定位可能的行为特征。
  2. 识别行为
    从特征中关联同源性的条件,从技术、战术角度合并同类项,形成人工可理解、有参考价值和共通性的行为模板。一般需要回答:“针对什么目标,使用什么方法”的抽象概括。
  3. 关联过程
    根据行为的上下文、目标和条件,制定行为发生的因果次序,还原整个攻击事件过程。

围绕这3个基本过程,已有一部分方法做了相关研究。

EX-ActionTTPDrillActionMiner 将过程1和2分开实现行为的识别,他们先是使用StanfordNLP项目对网络威胁情报文本进行词性识别(Part of Speech,POS)操作,然后生成“主谓宾”行为短语(Subject,Verb and Object,SVO),利用信息熵或机器学习实现攻击行为短语的筛选。这种方法虽然使得行为的判断可解释,但过程复杂,对安全新人或新攻击行为的分析不是很友好;也无法获取非主谓宾短语的行为描述(就是说没法获取IOC);甚至可能会破坏掉上下文信息导致无法形成过程。

rcATTHM-ACNNATHRNNRENet 等方法利用端到端学习,将过程1和2合并,直接把网络威胁情报抽取为攻击行为。这么做的好处是,不必如分开的方法一般损失掉文本信息和上下文;新人入门门槛低,自动化程度高,人工依赖低,过程简单。但坏处是人工智能的“黑盒性”,使得行为识别存在无法解释依据、灾难性遗忘和模型参数庞大等问题。

围绕过程3,普遍的做法是本体模型。但该模型一般需要知识图谱和大规模人工分析,目前尚处于初级阶段。

网络威胁情报TTPs识别案例

在本章将会给大家一个自己能够跑的TTPs端到端识别方法

数据来源是由TTPDrill提供的数据集:https://raw.githubusercontent.com/KaiLiu-Leo/TTPDrill-0.5/master/ontology/examples/All.csv

网络威胁情报转矩阵使用的是Bert文本嵌入方法 百度网盘,提取码:dx86,以处理OOV问题。

我们能使用Tensorflow作为端到端代码实现方式,代码可以在 github 下载

首先引入必要包

from tensorflow.keras import layers, metrics, models, optimizers
import json
import numpy as np
import tensorflow as tf
import tensorflow.keras.backend as K

然后定义技战术种类

stdTechniqueIds = ['T1001', 'T1003', 'T1005', 'T1007', 'T1008', 'T1010', 'T1011', 'T1012', 'T1014', 'T1016', 'T1018', 'T1020', 'T1021', 'T1025', 'T1027', 'T1029', 'T1030', 'T1033', 'T1036', 'T1037', 'T1039', 'T1040', 'T1041', 'T1046', 'T1047', 'T1048', 'T1049', 'T1052', 'T1053', 'T1055', 'T1056', 'T1057', 'T1059', 'T1068', 'T1069', 'T1070', 'T1071', 'T1072', 'T1074', 'T1078', 'T1080', 'T1082', 'T1083', 'T1087', 'T1090', 'T1091', 'T1092', 'T1095', 'T1098', 'T1102', 'T1104', 'T1105', 'T1106', 'T1110', 'T1111', 'T1112', 'T1113', 'T1114', 'T1115', 'T1119', 'T1120', 'T1123', 'T1124', 'T1125', 'T1127', 'T1129', 'T1132', 'T1133', 'T1134', 'T1135', 'T1136', 'T1137', 'T1140', 'T1176', 'T1185', 'T1187', 'T1189', 'T1190', 'T1195', 'T1197', 'T1199', 'T1200', 'T1201', 'T1202', 'T1203', 'T1204', 'T1205', 'T1207', 'T1210', 'T1211', 'T1213', 'T1216', 'T1217', 'T1218', 'T1219', 'T1220', 'T1221', 'T1222', 'T1480', 'T1482', 'T1484', 'T1485', 'T1486', 'T1489', 'T1490', 'T1491', 'T1496', 'T1497', 'T1498', 'T1499', 'T1505', 'T1518', 'T1528', 'T1529', 'T1531', 'T1534', 'T1539', 'T1542', 'T1543', 'T1546', 'T1547', 'T1548', 'T1550', 'T1552', 'T1553', 'T1554', 'T1555', 'T1556', 'T1557', 'T1558', 'T1559', 'T1560', 'T1561', 'T1562', 'T1563', 'T1564', 'T1565', 'T1566', 'T1567', 'T1568', 'T1569', 'T1570', 'T1571', 'T1572', 'T1573', 'T1574', 'T1583', 'T1584', 'T1585', 'T1587', 'T1588', 'T1601']
stdTacticIds = ['TA0001', 'TA0002', 'TA0003', 'TA0004', 'TA0005', 'TA0006', 'TA0007', 'TA0008', 'TA0009', 'TA0010', 'TA0011', 'TA0040', 'TA0042', 'TA0043']

然后,建立独立分类器方法single_classifier

def single_classifier(input_layer, output_shape, activation='sigmoid', withRCNN=False,
                      dense_units=[512,256,128], lstm_dim=256, dropout=0.5, cnn_dim=[1,2],
                      cnn_count=256, output_name=None):
    if lstm_dim > 0:
        birnn_layer = layers.Bidirectional(layers.GRU(lstm_dim, return_sequences=True))(input_layer)
        if withRCNN:
            #             reshape_layer = layers.Dense(lstm_dim, activation='relu')(input_layer)
            reshape_layer = input_layer
            birnn_layer = layers.Concatenate(axis=2)([birnn_layer, reshape_layer])
        x = layers.Dropout(dropout)(birnn_layer)
    else:
        x = input_layer
    cnn_layers = []
    for cd in cnn_dim:
        cx = layers.Conv1D(cnn_count, kernel_size=cd, padding='same')(x)
        cx = layers.LayerNormalization()(cx)
        cx = layers.ReLU()(cx)

        # 新 归一化
        cx  = layers.LayerNormalization()(cx)

        cx = layers.GlobalMaxPooling1D()(cx)
        cx = layers.Reshape((1,cx.shape[1]))(cx)
        cnn_layers.append(cx)
    if len(cnn_layers) > 1:
        x = layers.Concatenate(axis=1)(cnn_layers)
        #         x = layers.Conv1D(cnn_count,len(cnn_layers))(x)
        #         x = layers.GlobalMaxPool1D()(x)
        x = layers.Flatten()(x)
    else:
        x = layers.Flatten()(cnn_layers[0])
    for units in dense_units:
        x = layers.Dense(units)(x)
        x = layers.BatchNormalization()(x)
        x = layers.ReLU()(x)
        x = layers.Dropout(dropout)(x)
    x = layers.Dense(output_shape, activation=activation)(x) if not output_name else \
        layers.Dense(output_shape, activation=activation, name=output_name)(x)
    return x

如果是单独分类技术和战术只需要使用完成网络威胁情报向量的预测

model_input = layers.Input(input_shape)
model = single_classifier(model_input, [len(stdTechniqueIds),])
model.predict(cti_vec)

当然,由于技术种类多,其识别率较低,可以使用技战术关联性,利用门控机制,使得技术的识别范围减小。(如果技术A属于战术B,则如果战术B不存在,则技术A也不应该存在)

因此构建关联增强模块

def get_rel_weights(from_ids=None, to_ids=None, relation_file='datas/attck_tactic_tech_relation.json', default_fill=0):
    tact_ids = from_ids if from_ids else stdTacticIds
    tech_ids = to_ids if to_ids else stdTechniqueIds
    theJson = json.load(open(relation_file, encoding='utf-8-sig'))
    relWeights = np.zeros((len(tact_ids), len(tech_ids)), dtype='float32')
    if default_fill != 0:
        relWeights.fill(-0.1)
    for tac in theJson:
        for tec in tac['techs']:
            try:
                tacindex = tact_ids.index(tac['id'])
                tecindex = tech_ids.index(tec['id'])
                if tacindex in range(len(tact_ids)) and tecindex in range(len(tech_ids)):
                    relWeights[tacindex, tecindex]=0.1
            except Exception as e:
                print(e)
                continue
    return relWeights

def relevance_enhancement(tact_output_layer, tech_output_layer, type='n', weights=None):
    if weights.shape[0] == len(stdTacticIds) and weights.shape[1] == len(stdTechniqueIds) and not weights:
        weights = get_rel_weights()
    if type in ['0', 'zero']:
        relevanceTransLayer = layers.Dense(tech_output_layer.shape[1], use_bias=False, name='relevance-transform-layer',
                                           trainable=True,
                                           kernel_regularizer=tf.keras.regularizers.l2())(tact_output_layer)
    elif type in ['a', 'artificial']:
        if weights.shape[0] == tact_output_layer.shape[-1] and weights.shape[1] == tech_output_layer.shape[-1]:
            relevanceTransLayer = layers.Dense(tech_output_layer.shape[1], use_bias=False,
                                               name='relevance-transform-layer',
                                               kernel_initializer=tf.keras.initializers.Constant(weights),
                                               trainable=True,
                                               kernel_regularizer=tf.keras.regularizers.l2())(tact_output_layer)
        else:
            relevanceTransLayer = layers.Dense(tech_output_layer.shape[1], use_bias=False,
                                               name='relevance-transform-layer',
                                               trainable=True,
                                               kernel_regularizer=tf.keras.regularizers.l2())(tact_output_layer)
    elif type in ['la', 'lock-artificial']:
        if weights.shape[0] == tact_output_layer.shape[-1] and weights.shape[1] == tech_output_layer.shape[-1]:
            relevanceTransLayer = layers.Dense(tech_output_layer.shape[1], use_bias=False,
                                               name='relevance-transform-layer',
                                               kernel_initializer=tf.keras.initializers.Constant(weights),
                                               trainable=False,
                                               kernel_regularizer=tf.keras.regularizers.l2())(tact_output_layer)
        else:
            relevanceTransLayer = layers.Dense(tech_output_layer.shape[1], use_bias=False,
                                               name='relevance-transform-layer',
                                               trainable=True,
                                               kernel_regularizer=tf.keras.regularizers.l2())(tact_output_layer)
    else:
        return tech_output_layer
    minGateLayer = layers.Minimum(name='min')([relevanceTransLayer, tech_output_layer])
    return minGateLayer

构建使用关联增强模块的技战术识别方法

def focal_loss(gamma=2., alpha=.25):
    def focal_loss_fixed(y_true, y_pred):
        pt_1 = tf.where(tf.equal(y_true, 1), y_pred, tf.ones_like(y_pred))
        pt_0 = tf.where(tf.equal(y_true, 0), y_pred, tf.zeros_like(y_pred))
        return -K.sum(alpha * K.pow(1. - pt_1, gamma) * K.log(K.epsilon()+pt_1))-K.sum((1-alpha) * K.pow( pt_0, gamma) * K.log(1. - pt_0 + K.epsilon()))
    return focal_loss_fixed

def RENet(input_shape, output_shapes, activation='sigmoid', withRCNN=False,
          dense_units=[512, 256, 128], lstm_dim=256, dropout=0.5, cnn_dim=[1, 2],
          cnn_count=256, output_names=None, renet_pairs=None):
    input_layer = layers.Input(input_shape)
    output_layers = []
    for i, otsh in enumerate(output_shapes):
        output_name = output_names[i] if output_names and output_names[i] else None
        output_layers.append(single_classifier(input_layer=input_layer, output_shape=otsh,
                                               activation=activation, withRCNN=withRCNN,
                                               dense_units=dense_units, lstm_dim=lstm_dim,
                                               dropout=dropout, cnn_dim=cnn_dim, cnn_count=cnn_count,
                                               output_name=output_name))

    if len(output_layers) > 1:
        new_outs = output_layers
        if not renet_pairs or type(renet_pairs) != list or len(renet_pairs) == 0:
            for i in range(len(output_layers)-1):
                new_outs[i+1] = relevance_enhancement(output_layers[i], output_layers[i+1], type='0')
        else:
            for pair in renet_pairs:
                if type(pair) != dict:
                    continue
                from_index = pair['from'] if 'from' in pair.keys() else None
                to_index = pair['to'] if 'to' in pair.keys() else None
                net_type = pair['type'] if 'type' in pair.keys() else None
                weights = pair['weights'] if 'weights' in pair.keys() else None
                if from_index and to_index:
                    new_outs[to_index] = relevance_enhancement(output_layers[from_index],
                                                               output_layers[to_index],
                                                               type=net_type, weights=weights)
        output_layers = new_outs
    elif output_layers == 0:
        return None

    model = models.Model(input_layer, output_layers)
    model.compile(optimizer=optimizers.Adam(), loss=focal_loss(),
                  metrics=[metrics.Precision(), metrics.Recall()])
    model.summary()
    return model

使用该方法完成网络威胁情报向量的预测

model = RENet(input_shape, [(len(stdTechniqueIds),),(len(stdTacticIds),)])
model.predict(cti_vec)
评论 19
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

MuscleFish

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值