论文学习_Identifying Open-Source License Violation and 1-day Security Risk at Large Scale

论文名称发表时间发表期刊期刊等级研究单位
Identifying Open-Source License Violation and 1-day Security Risk at Large Scale2017年CCSCCF A佐治亚理工学院

1. 引言

研究背景:移动应用市场正在迅速变得拥挤。 据 AppBrain 称,仅 Google Play 商店就有 260 万个应用程序。 为了在如此拥挤的领域中脱颖而出,开发者为他们的应用程序构建了独特的特性和功能,但更重要的是,他们试图尽快将他们的应用程序推向市场,以获得先发优势和随后的网络效应。 常见的开发实践是对必要但“通用”的组件使用开源软件(OSS),以便开发人员可以专注于独特的功能和工作流程。 随着 GitHub 和 Bitbucket 等公共源代码托管服务的出现,使用 OSS 进行更快的应用程序开发从未如此简单。 截至 2016 年 10 月,GitHub 报告托管了超过 4600 万个源存储库 (repos),使其成为世界上最大的源托管服务。

现存问题:尽管 OSS 有很多好处,但必须谨慎使用。 根据论文的研究,不小心使用开源软件会产生两个常见问题:软件许可违规和安全风险。

  • 软件许可违规:在应用程序中使用 OSS 代码可能会导致复杂的许可证合规性问题。 OSS 在各种许可证下发布,从高度宽松的 BSD 和 MIT 许可证到高度限制的通用公共许可证 (GPL) 和 Affero 通用公共许可证 (AGPL)。 使用 OSS 隐含地约束开发人员遵守相关许可条款,这些条款受版权法保护。 因此,不遵守这些条款可能会产生法律后果。 例如,Cisco 和 VMWare 因未遵守 Linux 内核的许可条款而卷入法律纠纷。
  • 安全风险:OSS 还可能包含可利用的漏洞。 例如,最近报告的 Facebook 和 Dropbox SDK 中的漏洞可能会被用来劫持用户的 Facebook 帐户,并将其设备分别链接到攻击者控制的 Dropbox 帐户。 OSS 中发现的漏洞通常会在后续版本中进行修补,而使用旧的、未修补版本的应用程序可能会将最终用户的安全和隐私置于危险之中。

研究内容:论文开发了 OSSPolice,这是一种可扩展且全自动的工具,可快速分析应用程序二进制文件,以识别潜在的软件许可证违规行为以及已知易受攻击的 OSS 版本的使用情况。 OSSPolice 使用软件相似性比较来检测应用程序二进制文件中的 OSS 重用。 具体来说,它从目标应用程序二进制文件中提取固有的特征(又名软件胎记),并将它们与从数十万个 OSS 源中提取的特征数据库进行有效比较,以便准确检测正在使用的 OSS 版本。 如果我们的数据库中缺少正确的版本,或者两个版本没有明显的特征,根据我们的发现,将检测到最接近的 OSS 版本。

实验结果:OSSPolice 能够有效地搜索数十万个源代码库(数十亿行代码)。论文使用 FDroid 上的开源 Android 应用程序以及手动标记的真实情况来评估 OSSPolice 的准确性。 OSSPolice 在检测 C/C++ OSS 使用情况时实现了 82% 的召回率和 87% 的准确率,在检测 Java OSS 使用情况时实现了 89% 的召回率和 92% 的准确率,优于 BAT 和 LibScout 。对于版本精确定位,OSSPolice 能够检测到的 OSS 版本比 LibScout 多 65%。

2. OSSPolice 设计

2.1 研究假设与研究目标

研究假设:论文设想 OSSPolice 作为移动应用程序开发人员的网络服务(或独立工具),可以快速将他们的应用程序与包含数十万个 OSS 源的数据库进行比较,以识别自由软件许可证违规行为以及正在使用的已知易受攻击的 OSS。

尽管如此,软件许可证违规的检测涉及法律和技术方面,OSSPolice 仅关注后者。其目标是仅收集表明许可证违规行为的统计证据,而不得出任何法律结论。 同样,OSSPolice 也不是一个发现新的或现有的安全漏洞的系统。其目标只是强调应用程序中已知易受攻击的 OSS 版本的重用,而不是查找或提供漏洞的具体证据。 

OSSPolice 认为这些违规行为是无意造成的,并不构成故意软件盗窃或盗版。 因此,它假设应用程序二进制文件没有被篡改以阻止代码重用检测。

研究目标:准确检测应用程序二进制文件中使用的 OSS 版本;收集表明许可证违规和存在已知易受攻击的 OSS 版本的证据;有效利用硬件资源;可扩展性以搜索数十万个 OSS 源(数十亿行代码)。

2.2 应用程序与开源软件

Android 应用程序主要分为 dalvik executable(dex)和 native libraries,OSSPolice 单独分析 Android 应用程序中的每种二进制类型,并将其与 OSS 源进行比较,以检测正在使用的特定版本。

Native Libraries:本机库直接针对机器架构(例如来自 C/C++ 源的 ARM 和 x86)构建,并在运行时按需加载。 应用程序开发人员出于各种原因在 Android 应用程序中使用本机库,例如代码重用、更好的性能或跨平台开发。检测应用程序本机库中 OSS 重用的一种方法是首先从主题 OSS 源构建本机库,然后利用现有的二进制相似性测量技术将其与目标应用程序库进行比较。 然而,这种方法存在以下限制。 首先,它意味着自动化构建 OSS 源以实现可扩展,这即使不是不可能,也是不平凡的。 用低级语言(例如C/C++)编写的OSS需要专门的构建环境,包括所有依赖项、构建工具和特定于目标的配置。 例如,Android 应用程序中存在的本机库必须使用 Android 本机开发套件 (NDK) 工具链构建。 因此,从 C/C++ OSS 源自动构建二进制文件并不是一步完成的过程; 相反,必须遵循复杂的构建说明来创建所需的构建环境。 然而,此类特定的构建指令可能无法作为源的一部分从 OSS 开发人员处获得。 其次,即使能够成功构建 OSS 源,由于不同的编译标志(例如优化)或不匹配的系统配置,生成的 OSS 库可能与目标应用程序库有很大差异。 例如,在编译期间创建的捕获主机系统类型(例如,体系结构数据类型等)的系统配置标头在不同的系统上会有所不同。 为了避免此类陷阱,论文直接将应用程序原生库与 OSS 源进行比较。

Dex Files:与本机库相比,Android dex 文件是从 Java 源构建的,并在沙盒 Java 虚拟机运行时下执行。由于易于逆向工程,dex 文件通常会被混淆以隐藏专有细节。事实上,官方的 Android 开发 IDE Android Studio 附带了一个名为 ProGuard 的内置混淆工具,它可以删除未使用的代码并重命名类,包括任何具有语义模糊名称的字段和函数,以隐藏专有信息。实施细节。例如,包名称 com.google.android 重命名为 a.g.c。 OSSPolice 的设计能够抵御常见的混淆技术,例如用于分析 Java dex 二进制文件的标识符重命名和控制流随机化。尽管应用程序开发人员还可以采用高级代码混淆方法,例如字符串或类加密以及基于反射的 API 隐藏,但论文发现此类情况在数据集中很少见,可能是因为此类机制会产生较高的运行时开销。

2.3 特征选择

OSSPolice采用软件相似性比较来检测OSS重用。 具体来说,在分析移动应用程序二进制文件时,OSSPolice 使用软件胎记来比较它们与 OSS 源的相似性,以准确检测 OSS 版本的使用情况。 软件胎记是软件的一组固有特征,可用于识别软件。 换句话说,如果软件 X 和 Y 具有相同或统计上相似的胎记,那么它们很有可能是彼此的副本。

选择胎记(又称特征)需要平衡软件相似性检测的性能、可扩展性和准确性; 根据设计目标,可以进行适当的权衡。 例如,语法特征(例如字符串文字)很容易提取并保留在二进制文件中,但也可以被混淆(例如字符串加密)以击败检测。 简单的语法特征在应用于恶意软件克隆检测和应用程序重新打包检测问题时并不可靠。 因此,过去针对此类对抗性问题的工作经常采用程序依赖图或动态分析来击败先进的规避技术。 然而,此类语义特征不仅难以正确提取,而且会消耗大量的CPU和内存资源,限制了系统的可扩展性。

OSSPolice 既不是在应用程序中查找恶意软件的工具,也不是旨在检测故意的软件盗窃或盗版。 因此,论文通过牺牲代码转换的准确性来获得设计空间的性能和可扩展性。 特别是,论文假设应用程序二进制文件没有被篡改以逃避 OSS 检测,并依赖于简单的语法功能,例如字符串文字和函数,以便将 Android 原生二进制文件与 C/C++ OSS 源进行比较。 上表展示了 OSSPolice 使用的所有特征的列表。 选择他们的原因是多方面的。 除了易于提取之外,论文发现这些特征对于代码重构来说是稳定的,足够精确以区分不同的 OSS 版本,并且即使在剥离的库中也能保留(ASCII 可读)。 在论文对 160 万个免费 Google Play 商店 Android 应用程序进行分析时,论文发现 85% 的本机库保留了 50 多个功能(字符串和函数)。 我们进一步发现,对于大多数原生库来说,函数的数量随着库大小的增长而线性增加,这表明大多数应用程序不会剥离或隐藏原生库中的函数。 事实上,只有 11.6% 的库大小大于 40KB,但可见函数少于 50 个。 最后,这些特征已在各种二进制克隆检测方案中得到广泛使用并被证明是有效的。

OSSPolice 使用类似的语法特征将应用程序 dex 文件与 Java OSS 源进行匹配,即字符串常量和类签名。然而,为了抵御常见的混淆技术(例如标识符重命名),论文在生成签名之前对类进行规范化,这样它们会丢失所有用户定义的详细信息,但保留它们与通用框架 API 的交互。规范化类已被证明能够在 ProGuard 混淆过程中幸存下来。签名分两步得出。首先,通过删除除参数列表和返回类型之外的所有内容,并进一步用占位符替换非框架类型,对类中的所有函数进行规范化。接下来,对生成的标准化函数进行排序和散列以获得它们的类签名。然而,论文的分析表明,虽然字符串常量和规范化类签名可以检测 Java dex 文件中的 OSS 重用,但无法准确检测 OSS 版本。因此,论文还使用函数质心来获得额外的熵。函数的质心是通过对其过程内图进行确定性遍历来生成的。它捕获函数的控制流特征,并生成其签名作为三维数据点,表示基本块索引、传出度和循环深度。然而,计算和比较函数质心是计算成本高昂的任务。因此,我们将它们推迟到相似性检测的后期阶段,并且仅用于查明 OSS 版本。

2.4 相似性检测

给定来自应用程序二进制文件(由 BIN 表示)和 OSS 源(由 OSS 表示)的特征集,典型的软件相似性检测方案是比较两个特征集并计算基于比率的相似性评分来检测 OSS 使用情况。 然而,设计一个大规模相似性测量系统来准确检测应用程序二进制文件中的 OSS 重用也面临着一系列挑战。

2.4.1 相似性检测挑战

内部代码克隆:使用 OSS 的一个众所周知的优点是代码重用。 OSS 开发人员经常重用第三方 OSS 源来利用现有功能。 重用的代码通常在内部克隆和维护,作为 OSS 开发源的一部分(例如,以允许轻松定制、确保兼容性等)。 论文将这种嵌套的第三方 OSS 克隆称为内部代码克隆。 内部代码克隆会导致 OSS 源之间的大量代码重复。 因此,用于相似性搜索的原始 OSS 源数据库不仅会对硬件提出很高的要求,从而损害系统的可扩展性,而且还会导致 OSSPolice 报告针对内部第三方代码克隆的误报匹配。下图是 C/C++ OSS MuPDF 与 OpenCV 的源码结构。

这两个存储库都包含 LibPNG 的代码克隆作为其源树的一部分。 因此,当尝试将 LibPNG 二进制文件中的特征与 LibPNG、MuPDF、OpenCV 源进行匹配时,尽管 LibPNG 是唯一真正的正匹配,但所有三个都将被报告为匹配。 如果真实的和报告的匹配存储库处于不同的软件许可证下,则此类误报可能会导致不正确的许可证违规。

代码部分重用:应用程序开发人员还可以选择仅包含 OSS 的部分功能。 例如,特定于一种机器架构(例如,x86)的源代码不会被编译成针对不同架构(例如,arm)的二进制文件。 许多 C/C++ OSS 源提供配置选项来选择性地启用/禁用特定于体系结构的功能。 同样,某些 OSS 源也可能包含未编译到目标二进制文件中的源文件和目录,例如示例和测试套件。 虽然可以通过分析构建脚本(例如,gradle、Makefile 等)来识别此类未使用的源,但正常方法需要投入大量的人力。因此OSSPolice 需要支持构建自动化工具,以便正确解析构建脚本并过滤掉未使用的零件; 然而,该过程仍然容易出错。 此外,常用的应用程序压缩工具(例如 ProGuard)会分析 Java dex 字节码并删除未使用的类、字段和方法。 虽然在这种情况下二进制文件在功能上保持等效,但从源到二进制文件保留的功能数量可能会显着减少。 论文将这些二进制文件称为部分构建的二进制文件。 当将来自此类二进制文件 (BIN ) 的特征与来自相应 OSS 源的特征 (OSS) 进行比较时,匹配率可以任意低,即使 BIN 中的所有元素都在开源软件。 事实上,检测到的未使用特征越多,匹配分数就越低,表明漏报匹配。

融合应用程序二进制文件:在应用程序构建过程中,来自不同 OSS 源的多个二进制文件可以紧密耦合在一起以生成单个应用程序二进制文件。 例如,Android 应用程序中的所有 Java 类文件(包括任何导入的 OSS jar)都会编译为单个 dex 字节码文件 (classes.dex)。 同样,从各种 C/C++ OSS 源构建的多个本机库可以静态链接到单个共享库,从而模糊它们之间的界限。 在此类多二进制文件中,跨多个 OSS 组件的功能被有效地融合到一个超集中。 论文将它们称为融合二进制文件,例如 MuPDF 二进制文件包含 LibJPEG 的函数。 因此,当将融合特征集 (BIN ) 与来自单个 OSS 的一组特征 (OSS) 进行匹配时,匹配率将任意低,即使 BIN 包括 OSS 的所有元素。 事实上,融合在一起的不同二进制文件数量越多,匹配分数就越低,从而导致漏报。

2.4.2 运行机制

为了在相似性比较过程中进行高效且可扩展的查找,OSSPolice 维护一个从 OSS 源提取的特征的索引数据库。 索引 OSS 源的直观方法是将每个 OSS 视为文档,将其特征视为单词,并创建特征到目标 OSS 的直接映射,上图描述了此类索引数据库的布局。 BAT 使用类似的方案来维护从 OSS 源提取的特征(字符串文字)数据库。 然而,这种方法假设每个 OSS 都是相互独立的,并且未能考虑由于内部代码克隆而导致跨 OSS 源的大量代码重复。 因此,这种索引方案不仅会导致与内部克隆的第三方 OSS 源的误报率较高,而且会带来较高的存储要求,并且不会随着要索引的 OSS 数量的增长而扩展。 对 OSS 的多个版本进行索引以实现版本精确定位进一步增加了代码重复的问题。

论文通过利用 OSS 源结构丰富的树状布局来解决上述挑战。 为了说明目的,论文将在本节中使用MuPDF 与 OpenCV 的 OSS 源存储库布局。 论文的主要观察结果是,OSS 开发人员通常遵循软件开发的最佳实践,以改善协作并实现更快的开发。 因此,OSS 源以模块化和分层的方式组织良好,易于维护。 例如,源文件(例如,C/C++ 或 Java 类文件)通常封装相关函数。 源树每一层的目录 (dir) 节点将所有相关的子文件和目录聚集在一起。 具体来说,OpenCV 和 MuPDF 中的 src 和源目录分别将所有相关源文件和目录分组在它们下面。 同样,第三方 OSS(例如 LibPNG 和 LibJPEG)的内部代码克隆也维护在单独的目录中(分别为 thirdparty 和 3rdparty)。论文利用此属性对 OSS 源树层次结构中的每个文件或目录节点执行基于比率的特征匹配,而不是针对整个 OSS 存储库进行匹配(在部分 OSS 重用的情况下可能会导致精度较低)。如果基于比率的特征匹配报告了OSS源树层次结构中特定级别l的节点n(例如LibPNG)的高分,但在级别>l与n的父节点p(例如OpenCV)之一匹配时报告了低聚合分数,那么论文只报告了与节点n(即LibPNG)匹配,而不报告与父节点p或同一级别的任何兄弟节点的匹配。在这个例子中,OSSPolice报告的匹配OSS路径将是OpenCV/LibPNG。

为了检测内部克隆并过滤掉针对它们的虚假匹配,论文应用了多种利用 OSS 源的模块化布局的附加启发式方法。在索引过程中,论文访问 OSS 源中的每个目录节点 n 并检查是否存在常见软件开发文件,例如 LICENSE 或 COPYING(OSS 许可条款)、CREDITS(致谢)和 CHANGELOG(软件更改历史记录)。 这些文件通常放置在 OSS 项目存储库的顶级源目录中。C/C++ OSS 源通常还托管构建自动化脚本(例如,顶级源目录中的 configure 和 autogen)。因此,克隆的第三方 OSS 源可能会保留这些文件,这些文件可用于识别内部 OSS 克隆。但是, 由于某些 OSS 源可能组织得不好,论文进一步利用 OSS 重用导致的 OSS 源之间的大量代码重复来识别此类内部克隆,论文的观察结果是,由于 OSS 重用,经常重用的 OSS 的目录节点 (n) 。 与唯一的 OSS 源目录(例如 MuPDF/source/pdf)相比,源在我们的数据库中将有多个父项 p,这有助于我们识别数据库中所有流行的 OSS 克隆,以便对所有已识别的克隆进行进一步注释。 在匹配阶段将其排除,以最大程度地减少误报。

2.4.3 分层索引

论文设计了一种新颖的分层索引方案,保留了 OSS 源的结构化分层布局(如算法 1 所示)。 具体来说,论文不是将特征直接映射到目标 OSS(即 OSS 源树中的顶级目录),而是将功能映射到其直接父节点(即文件和中级目录)。论文在索引数据库中填充 OSS,从代表特征(例如字符串、函数等)的叶节点开始,以自下而上的方式单独处理其源树中的每个节点(特征、文件或目录)。 )。 为了保留OSS源的结构化布局,论文将父节点(即文件和目录)的标识符视为特征,并进一步对其进行索引以进行高效查找。 论文将它们称为分层特征。 在OSS源层次结构的每个级别 l上,对于给定的节点 n,论文为其下的每个特征f创建两种类型的映射:f 到 n(级别l的直接父级)的反向映射和 n 到 f 的直接映射。 给定一个特征,第一个映射允许我们快速找到其匹配的父项,而我们使用后者来执行基于比率的相似性检测。 论文的分层索引方案有效地捕获了每个层次结构级别的特征的唯一性。 例如,索引后可以知道 LibPNG 中的特征包含在源目录 LibPNG 中,而LibPNG 又包含在多个节点中,例如 OpenCV 中的 3rdparty 和 MuPDF 中的 thirdparty。

论文利用内部 OSS 克隆来执行代码重复数据删除,以便在索引期间有效利用硬件资源。为此,论文将基于内容的标识符分配给源树中的所有节点。 论文使用 128 位 md5 哈希为特征(叶)节点生成此类标识符,并使用 Simhash 算法分配父(非叶)节点的标识符,该标识符源自其下所有特征(叶节点)的标识符。 Simhash 是一种局部敏感哈希 (LSH) 算法,它采用高维特征集并将它们映射到固定大小的哈希。 哈希值之间的汉明距离揭示了原始特征集之间的余弦相似度。 由于不同标识符之间的汉明距离反映了它们的相似性,因此在插入从特征 f 到父节点 n 的新映射之前,我们查找 f 是否已经映射到汉明距离小于特定阈值 D 的相似父节点 n′(即 H ( n,n′) < D)。 如果这样的父节点 n' 已经存在,那么我们只需跳过用 n' 的映射填充索引表。 请注意,如果 n 恰好是一个大型中层目录节点,其中包含多个源文件和目录(例如,thirdparty/LibPNG)并且类似于现有节点(即我们数据库中的 3rdparty/LibPNG),那么我们的内容 基于内容的重复数据删除设计实现了与现有节点(即我们数据库中的 3rdparty/LibPNG)类似的显着存储节省,那么我们基于内容的重复数据删除设计实现了显着的存储节省。此外,某些功能可能非常受欢迎。例如,常见的功能 诸如 main 或 test 之类的功能不会有助于 OSS 的唯一性,更糟糕的是,它们的父映射列表(f 到 n)浪费了存储空间并增加了搜索时间。 每个子节点的最大父节点数 (TNp )。

此外,为了实现准确的版本定位,论文在索引阶段跟踪每个 OSS 跨 OSS 版本的独特功能。 这是使用两个列表单独维护的(Listover all 包含 OSS 中出现过的所有功能,Listunique 记录每个版本中的唯一功能),因为利用基于索引阶段相似性的重复数据删除的好处,论文也失去了相似项之间的唯一性。

2.4.4 分层匹配

论文的匹配算法(如算法 2 所示)利用索引表中保存的 OSS 布局信息来提高基于比率的相似性检测的准确性并过滤掉重复的 OSS 源。为此,论文使用 TF-IDF 指标,为每个父节点(文件和目录)的唯一部分分配更高的分数,并惩罚非唯一部分。

基于 TF-IDF 的度量。 令c表示子节点,p表示分层索引结构中的父节点,Np表示数据库中父类型节点的总数。 设fci、Fci和Rci分别表示第i个子节点的匹配特征数、总特征数和匹配父节点(引用)的数量。 然后,我们将 log Np 1+Rci 定义为第 i 个子节点的 IDF,衡量其对父节点的重要性。 最后,我们使用每个孩子的 IDF 进行权重,并将加权匹配率定义为等式 1 中的 NormScore。

当与索引表匹配时,我们首先查询特征来获取文件,然后查询文件来获取目录,依此类推。 每轮查询结束后,我们使用NormScore为父节点的独特部分分配更高的权重,并根据NormScore过滤这些父节点以进行下一轮查询。 有了这个归一化分数,当我们搜索 LibPNG 的二进制时,我们可以获得接近 1.0 的分数,但是当我们在 OpenCV 中从 LibPNG 升级到 3rdparty 时,分数显着下降,我们可以得出 LibPNG 的匹配。 此外,我们还跟踪匹配特征的总数,表示为 CumScore,以补充 NormScore,因为后者仅跟踪匹配率,而前者显示匹配计数。 利用索引阶段提取的丰富信息和定义的指标,我们应用以下匹配规则来过滤误报:

  • 跳过有许可证的目录,因为它们很可能是第三方 OSS 克隆。
  • 跳过与低比例功能匹配的源文件或与低比例功能匹配的头文件,因为它们可能是测试、示例或未使用的代码(例如部分构建)。
  • 通过检查流行文件/目录是否比同级文件/目录更流行来跳过流行文件/目录,其中流行度是指每个节点的匹配父节点的数量 (Rci )。

然后,根据检测到的 OSS,我们将应用程序二进制文件中的功能与 OSS 版本之间的独特功能进行比较,以识别匹配的 OSS 版本。 然而,在实践中,我们发现独特的特征可能会交叉匹配。 例如,OkHTTP 中的版本字符串“2.0.0”可能会匹配 MoPub 的版本“2.0.0”,而 MoPub 实际匹配的版本是“3.0.0”。 为了解决这个问题,我们利用二进制和索引表中保存的共置信息( n 和 f 之间的双向映射),并且如果同一文件/类中的所有其他特征也匹配,则认为唯一特征有效。

Profiling is the process of analyzing the performance of a program or function in order to identify bottlenecks or areas for optimization. cProfile is a built-in Python module that allows you to profile your code and generate a report of the performance metrics. To optimize the performance of a slow-running function using cProfile, you can follow these steps: 1. Import the cProfile module at the top of your Python file: ``` import cProfile ``` 2. Define the function that you want to profile: ``` def my_function(): # code goes here ``` 3. Run the function with cProfile: ``` cProfile.run('my_function()') ``` This will generate a report of the performance metrics for your function. 4. Analyze the report to identify bottlenecks or areas for optimization. The cProfile report will show you the number of times each function was called, the total time spent in each function, and the amount of time spent in each function call. Look for functions that are called frequently or that take a long time to execute. 5. Make changes to optimize the function. Once you have identified the bottlenecks, you can make changes to your code to optimize the function. This may involve simplifying the code, reducing the number of function calls, or using more efficient algorithms or data structures. 6. Repeat the profiling process to measure the impact of your changes. After making changes to your code, run the function again with cProfile to see if the performance has improved. If not, you may need to make additional changes or try a different approach. By using cProfile to profile your code and identify bottlenecks, you can optimize the performance of slow-running functions and improve the overall efficiency of your Python programs.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值