C/C++安全开发指南

目  录

1 文件结构

1.1 头文件结构

1.2 定义文件的结构

1.3 头文件的作用

1.4 目录结构

2 程序的版式

2.1 空行

2.2 代码行

2.3 代码行内的空格

2.4 代码行内的空格

2.5 长行拆分

2.6 修饰符的位置

2.7 注释

2.8 类的版式

3 多线程

3.1 变量应确保线程安全性

3.2 注意signal handler导致的条件竞争

3.3 注意Time-of-check Time-of-use (TOCTOU) 条件竞争

4 表达式和基本语句

4.1 运算符的优先级

4.2 复合表达式

4.3 循环语句的效率

5 常量

5.1 常量的定义规则

5.2 const与#define的比较

5.3 类中的常量

6 函数设计

6.1 参数的规则

6.2 返回值的规则

6.2 函数内部实现的规则

6.3 引用与指针的比较

7 数字操作

7.1 防止整数溢出

7.2 防止Off-By-One

7.3 避免大小端错误

7.4 检查除以零异常

7.5 防止数字类型的错误强转

7.6 比较数据大小时加上最小/最大值的校验

8 C++函数的高级特性

8.1 函数重载的概念

8.2 成员函数的重载、覆盖与隐藏

8.3 参数的缺省值

8.4 运算符重载

9 文件操作

9.1 避免路径穿越问题

9.2 避免相对路径导致的安全问题(DLL、EXE劫持等问题)

9.3 文件权限控制

10 加密解密

10.1 不得明文存储用户密码等敏感数据

10.2 内存中的用户密码等敏感数据应该安全抹除

10.3 rand() 类函数应正确初始化

10.4 在需要高强度安全加密时不应使用弱PRNG函数

10.5 自己实现的rand范围不应过小

1 文件结构

每个C++/C程序通常分为两个文件。一个文件用于保存程序的声明(declaration), 称为头文件。另一个文件用于保存程序的实现(implementation),称为定义(definition) 文件。

C++/C程序的头文件以“.h”为后缀,C程序的定义文件以“.c”为后缀,C++程序的定义文件通常以“.cpp”为后缀(也有一些系统以“.cc"或“.cxx”为后缀)。

1.1 头文件结构

1. 头文件结构一般由四部分组成:

(1)头文件开头处的版权和版本声明。

(2)预处理块。

(3)inline函数的定义。

(4)函数和类结构声明等。

1.2 定义文件的结构

1. 定义文件有三部分内容:

(1) 定义文件开头处的版权和版本声明。

(2)对一些头文件的引用。

(3)程序的实现体(包括数据和代码)。

#include“graphics.h" // 引用头文件

//全局函数的实现体

void Function1(..){

...

}

//类成员函数的实现体

void Box::Draw...){

...

}

1.3 头文件的作用

1. 早期的编程语言如Basic、Fortran 没有头文件的概念,C++/C 语言的初学者虽然会用使用头文件,但常常不明其理。这里对头文件的作用略作解释:

(1)通过头文件来调用库功能。在很多场合,源代码不便(或不准)向用户公布,只要向用户提供头文件和二进制的库即可。用户只需要按照头文件中的接口声明来调用库功能,而不必关心接口怎么实现的。编译器会从库中提取相应的代码。

(2)头文件能加强类型安全检查。如果某个接口被实现或被使用时,其方式与头文件中的声明不一致,编译器就会指出错误,这一简单的规则能大大减轻程序员调试、改错的负担。

1.4 目录结构

如果一个软件的头文件数目比较多( 如超过十个),通常应将头文件和定义文件分别保存于不同的目录,以便于维护。例如可将头文件保存于include目录,将定义文件保存于source目录(可以是多级目录)。如果某些头文件是私有的,它不会被用户的程序直接引用,则没有必要公开其“声明”。为了加强信息隐藏,这些私有的头文件可以和定义文件存放于同一个目录。

2 程序的版式

2.1 空行

空行起着分隔程序段落的作用。空行得体(不过多也不过少)将使程序的布局更加清晰。空行不会浪费内存,虽然打印含有空行的程序是会多消耗一些纸张,但是值得。所以不要舍不得用空行。

1.在每个类声明之后、每个函数定义结束之后都要加空行。

2.在一个函数体内,逻揖上密切相关的语句之间不加空行,其它地方应加空行分隔。

2.2 代码行

1.一行代码只做一件事情,如只定义一个变量,或只写一条语句。这样的代码容易阅读,并且方便于写注释。

2. if、for、while、do等语句自占一行,执行语句不得紧跟其后。不论执行语句有多少都要加{}。这样可以防止书写失误。

2.3 代码行内的空格

1.关键字之后要留空格。象const、virtual、inline、case 等关键字之后至少要留一个空格,否则无法辨析关键字。象if、for、while等关键字之后应留一个空格再跟左括号‘(’, 以突出关键字。

2.函数名之后不要留空格,紧跟左括号‘(’, 以与关键字区别。

3.‘(’ 向后紧跟,‘)”、‘,’、‘;’向前紧跟,紧跟处不留空格。

4.‘,’之后要留空格,如Function(x,y,z)。如果‘;'不是一行的结束符号,其后要留空格,如for (initialization; condition; update)。

5.赋值操作符、比较操作符、算术操作符、逻辑操作符、位域操作符,如“_”、“+=”“>=”、“<="、“+”.“*”、“%”、“&&”、“I”、“<<”,“入”等二元操作符的前后应当加空格。

6.一元操作符如“!”、“~”“++”“_”“&”(地址运算符)等前后不加空格。

7.像“[]”、“.”、“->”这类操作符前后不加空格。

2.4 代码行内的空格

1.程序的分界符‘{’ 和‘}’应独占一行并且位于同一列,同时与引用它们的语句左对齐。

2.{ }之内的代码块在‘{' 右边数格处左对齐。

2.5 长行拆分

1.代码行最大长度宜控制在70至80个字符以内。代码行不要过长,否则眼睛看不过来,也不便于打印。

2.长表达式要在低优先级操作符处拆分成新行,操作符放在新行之首(以便突出操作符)。拆分出的新行要进行适当的缩进,使排版整齐,语句可读。

2.6 修饰符的位置

修饰符*和&应该靠近数据类型还是该靠近变量名,是个有争议的活题。

若将修饰符*靠近数据类型,例如: int* x; 从语义上讲此写法比较直观,即x是int类型的指针。

上述写法的弊端是容易引起误解,例如: int* x,y; 此处y容易被误解为指针变量。虽然将x和y分行定义可以避免误解,但并不是人人都愿意这样做。

1.应当将修饰符*和&紧靠变量名

例如:

char * name;

int * x,y; // 此处y不会被误解为指针

2.7 注释

1.注释是对代码的“提示”,而不是文档。程序中的注释不可喧宾夺主,注释太多了会让人眼花缭乱。注释的花样要少。

2.如果代码本来就是清楚的,则不必加注释。否则多此一举,令人厌烦。例如//i加1,多余的注释。

3.边写代码边注释,修改代码同时修改相应的注释,以保证注释与代码的一致性。不再有用的注释要删除。

4.注释应当准确、易懂,防止注释有二义性。错误的注释不但无益反而有害。

5.尽量避免在注释中使用缩写,特别是不常用缩写。

6.注释的位置应与被描述的代码相邻,可以放在代码的上方或右方,不可放在下方。

7.当代码比较长,特别是有多重嵌套时,应当在一些段落的结束处加注释,便于阅读。

2.8 类的版式

类可以将数据和函数封装在一起,其中函数表示了类的行为(或称服务)。类提供关键字public. protected 和private,分别用于声明哪些数据和函数是公有的、受保护的或者是私有的。这样可以达到信息隐藏的目的,即让类仅仅公开必须要让外界知道的内容,而隐藏其它一切内容。我们不可以滥用类的封装功能,不要把它当成火锅,什么东西都往里扔。

类的版式主要有两种方式:

(1)将private类型的数据写在前面,而将public类型的函数写在后面.采用这种版式的程序员主张类的设计“以数据为中心”,重点关注类的内部结构。

(2)将public类型的函数写在前面,而将private类型的数据写在后面。 采用这种版式的程序员主张类的设计“以行为为中心”,重点关注的是类应该提供什么样的接口(或服务)。

3 多线程

3.1 变量应确保线程安全性

1. 当一个变量可能被多个线程使用时,应当使用原子操作或加锁操作。

2. 关联漏洞:

高风险-内存破坏

中风险-逻辑问题

3.2 注意signal handler导致的条件竞争

1. 竞争条件经常出现在信号处理程序中,因为信号处理程序支持异步操作。攻击者能够利用信号处理程序争用条件导致软件状态损坏,从而可能导致拒绝服务甚至代码执行。

2. 当信号处理程序中发生不可重入函数或状态敏感操作时,就会出现这些问题。因为信号处理程序中随时可以被调用。比如,当在信号处理程序中调用free时,通常会出现另一个信号争用条件,从而导致双重释放。即使给定指针在释放后设置为NULL,在释放内存和将指针设置为NULL之间仍然存在竞争的可能。

3. 为多个信号设置了相同的信号处理程序,这尤其有问题——因为这意味着信号处理程序本身可能会重新进入。例如,malloc()和free()是不可重入的,因为它们可能使用全局或静态数据结构来管理内存,并且它们被syslog()等看似无害的函数间接使用;这些函数可能会导致内存损坏和代码执行。

4. 可以借由下列操作规避问题:

(1)避免在多个处理函数中共享某些变量。

(2)在信号处理程序中使用同步操作。

(3)屏蔽不相关的信号,从而提供原子性。

(4)避免在信号处理函数中调用不满足异步信号安全的函数。

5. 关联漏洞:

高风险-内存破坏

中风险-逻辑问题

3.3 注意Time-of-check Time-of-use (TOCTOU) 条件竞争

1. TOCTOU: 软件在使用某个资源之前检查该资源的状态,但是该资源的状态可以在检查和使用之间更改,从而使检查结果无效。当资源处于这种意外状态时,这可能会导致软件执行错误操作。当攻击者可以影响检查和使用之间的资源状态时,此问题可能与安全相关。这可能发生在共享资源(如文件、内存,甚至多线程程序中的变量)上。在编程时需要注意避免出现TOCTOU问题。

2. TOCTOU难以修复,但是有以下缓解方案:

(1)限制对来自多个进程的文件的交叉操作。如果必须在多个进程或线程之间共享对资源的访问,那么请尝试限制”检查“(CHECK)和”使用“(USE)资源之间的时间量,使他们相距尽量不要太远。这不会从根本上解决问题,但可能会使攻击更难成功。

(2)在Use调用之后重新检查资源,以验证是否正确执行了操作。确保一些环境锁定机制能够被用来有效保护资源。但要确保锁定是检查之前进行的,而不是在检查之后进行的,以便检查时的资源与使用时的资源相同。

3. 关联漏洞:

高风险-内存破坏

中风险-逻辑问题

4 表达式和基本语句

4.1 运算符的优先级

运算符的优先级确定表达式中项的组合。这会影响到一个表达式如何计算。某些运算符比其他运算符有更高的优先级,例如,乘除运算符具有比加减运算符更高的优先级。

例如 x = 7 + 3 * 2,在这里,x 被赋值为 13,而不是 20,因为运算符 * 具有比 + 更高的优先级,所以首先计算乘法 3*2,然后再加上 7。

下表将按运算符优先级从高到低列出各个运算符,具有较高优先级的运算符出现在表格的上面,具有较低优先级的运算符出现在表格的下面。在表达式中,较高优先级的运算符会优先被计算。

类别 

运算符 

结合性 

后缀 

() [] -> . ++ - -  

从左到右 

一元 

+ - ! ~ ++ - - (type)* & sizeof 

从右到左 

乘除 

* / % 

从左到右 

加减 

+ - 

从左到右 

移位 

<< >> 

从左到右 

关系 

< <= > >= 

从左到右 

相等 

== != 

从左到右 

位与 AND 

从左到右 

位异或 XOR 

从左到右 

位或 OR 

从左到右 

逻辑与 AND 

&& 

从左到右 

逻辑或 OR 

|| 

从左到右 

条件 

?: 

从右到左 

赋值 

= += -= *= /= %=>>= <<= &= ^= |= 

从右到左 

逗号 

从左到右

4.2 复合表达式

如a=b=c=0这样的表达式称为复合表达式。允许复合表达式存在的理由是: (1)书写简洁; (2) 可以提高编译效率。但要防止滥用复合表达式。

1.不要编写太复杂的复合表达式。

2.不要有多用途的复合表达式。

3.不要把程序中的复合表达式与“真正的数学表达式”混淆。

4.3 循环语句的效率

C++/C循环语句中,for 语句使用频率最高,while 语句其次,do 语句很少用。本节重点论述循环体的效率。提高循环体效率的基本办法是降低循环体的复杂性。

1.不可在for循环体内修改循环变量,防止for循环失去控制。

2.建议for语句的循环控制变量的取值采用“半开半闭区间”写法。

有了if语句为什么还要switch语句?

switch是多分支选择语句,而if语句只有两个分支可供选择。虽然可以用嵌套的if语句来实现多分支选择,但那样的程序冗长难读。这是switch语句存在的理由。

1.每个case语句的结尾不要忘了加break,否则将导致多个分支重叠(除非有意使多个分支重叠)。.

2.不要忘记最后那个default分支。即使程序真的不需要default 处理,也应该保留语句default : break;这样做并非多此- -举,而是为了防止别人误以为你忘了default处理。

5 常量

5.1 常量的定义规则

1.需要对外公开的常量放在头文件中,不需要对外公开的常量放在定义文件的头部。为便于管理,可以把不同模块的常量集中存放在一个公共的头文中。

2.如果某一常量与其它常量密切相关,应在定义中包含这种关系,而不应给出一-些孤立的值。

5.2 const与#define的比较

C++语言可以用const来定义常量,也可以用#define 来定义常量。但是前者比后者有更多的优点:

(1)const常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查。而对后者只进行字符替换,没有类型安全检查,并且在字符替换可能会产生意料不到的错误(边际效应)。

(2)有些集成化的调试工具可以对const常量进行调试,但是不能对宏常量进行调试。

5.3 类中的常量

有时我们希望某些常量只在类中有效。由于#define 定义的宏常量是全局的,不能达到目的,于是想当然地觉得应该用const修饰数据成员来实现。const 数据成员的确是存在的,但其含义却不是我们所期望的。const 数据成员只在某个对象生存期内是常量,而对于整个类而言却是可变的,因为类可以创建多个对象,不同的对象其const数据成员的值可以不同。

不能在类声明中初始化const数据成员。以下用法是错误的,因为类的对象未被创建时,编译器不知道SIZE的值是什么。

6 函数设计

6.1 参数的规则

1.参数的书写要完整,不要贪图省事只写参数的类型而省略参数名字。如果函数没有参数,则用void填充。

2.参数命名要恰当,顺序要合理。

3.如果参数是指针,且仅作输入用,则应在类型前加const,以防止该指针在函数体内被意外修改。

4.如果输入参数以值传递的方式传递对象,则宜改用“const &”方式来传递,这样可以省去临时对象的构造和析构过程,从而提高效率。

6.2 返回值的规则

1.不要省略返回值的类型。

2.函数名字与返回值类型在语义上不可冲突。

3.不要将正常值和错误标志混在一起返回。正常值用输出参数获得,而错误标志用return语句返回。

6.2 函数内部实现的规则

1.在函数体的“入口处”,对参数的有效性进行检查。

2.在函数体的“出口处”,对returm语句的正确性和效率进行检查。

6.3 引用与指针的比较

1.指针:指针就是内存地址,指针变量是用来存放内存地址的变量。不同类型的指针变量所占用的存储单元长度是相同的,而存放数据的变量因数据的类型不同,所占用的存储空间长度也不同。

2.引用:引用不是新定义一个变量,而是给已存在变量取一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。类型 & 引用变量名 = 引用实体; 且引用类型必须和引用实体是同种类型的。

3.引用的主要用途是:修饰函数的形参和返回值。

在C++语言中,函数的参数和返回值的传递方式有三种:值传递,指针传递和引用传递.引用具有指针的效率,又具有变量使用的方便性和直观性。

4.实际上引用可以做的事,指针都可以做,为什么还要引用呢?

引用体现了最小特权原则,即给予程序元素完成其功能的最小权限。 指针能够毫无约束的操作内存中的任何东西,尽管功能强大,但是非常危险。

7 数字操作

7.1 防止整数溢出

1. 在计算时需要考虑整数溢出的可能,尤其在进行内存操作时,需要对分配、拷贝等大小进行合法校验,防止整数溢出导致的漏洞。

2. 关联漏洞:

高风险-内存破坏

7.2 防止Off-By-One

1. 在进行计算或者操作时,如果使用的最大值或最小值不正确,使得该值比正确值多1或少1,可能导致安全风险。对于 C++ 代码,建议使用 string、vector 等组件代替原始指针和数组操作。

2. 关联漏洞:

高风险-内存破坏

7.3 避免大小端错误

1. 在一些涉及大小端数据处理的场景,需要进行大小端判断,例如从大段设备取出的值,要以大段进行处理,避免端序错误使用。

2. 关联漏洞:

中风险-逻辑漏洞

7.4 检查除以零异常

1. 在进行除法运算时,需要判断被除数是否为零,以防导致程序不符合预期或者崩溃。

2. 关联漏洞:

低风险-拒绝服务

7.5 防止数字类型的错误强转

1. 在有符号和无符号数字参与的运算中,需要注意类型强转可能导致的逻辑错误,建议指定参与计算时数字的类型或者统一类型参与计算。

2. 关联漏洞:

高风险-内存破坏

中风险-逻辑漏洞

7.6 比较数据大小时加上最小/最大值的校验

1. 在进行数据大小比较时,要合理地校验数据的区间范围,建议根据数字类型,对其进行最大和最小值的判断,以防止非预期错误。

2. 关联漏洞:

高风险-内存破坏

8 C++函数的高级特性

对比于C语言的函数,C++增加了重载( overloaded).内联(inline)、 const 和virtual四种新机制。其中重载和内联机制既可用于全局函数也可用于类的成员函数,const与virtual机制仅用于类的成员函数。

8.1 函数重载的概念

函数重载:是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数或类型或顺序)必须不同,常用来处理实现功能类似数据类型不同的问题。

1.重载的实现

几个同名的重载函数仍然是不同的函数,它们是如何区分的呢?我们自然想到函数接口的两个要素:参数与返回值。

如果同名函数的参数不同(包括类型、顺序不同),那么容易区别出它们是不同的函数。

如果同名函数仅仅是返回值类型不同,有时可以区分,有时却不能。例如:

void Function(void);

int Function (void);

上述两个函数,第一个没有返回值,第二个的返回值是int类型。如果这样调用函数:

int x = Fupgtion ();

则可以判断出A..ction 是第二个函数。问题是在C++/C程序中,我们可以忽略函数的返回值。在这种情况下,编译器和程序员都不知道哪个Function 函数被调用。

所以只能靠参数而不能靠返回值类型的不同来区分重载函数。编译器根据参数为每个重载函数产生不同的内部标识符。例如编译器为示例8-1-1中的三个Eat函数产生象_eat_ beef、_eat_ fish、_ eat_ chicken 之类的内部标识符(不同的编译器可能产生不同风格的内部标识符)。

2.当心隐式类型转换导致重载函数产生二义性

第一个output函数的参数是int类型,第二个output函数的参数是float类型。由于数字本身没有类型,将数字当作参数时将自动进行类型转换(称为隐式类型转换)。语句output(0.5)将产生编译错误,因为编译器不知道该将0.5转换成int还是float 类型的参数。隐式类型转换在很多地方可以简化程序的书写,但是也可能留下隐患。

8.2 成员函数的重载、覆盖与隐藏

1.重载与覆盖

成员函数被重载的特征:

(1)相同的范围(在同一个类中);

(2)函数名字相同;

(3)参数不同;

(4) virtual 关键字可有可无。

覆盖是指派生类函数覆盖基类函数,特征是:

(1)不同的范围(分别位于派生类与基类);

(2)函数名字相同:

(3)参数相同;

(4)基类函数必须有virtual关键字。

2.令人迷惑的隐藏规则

(1)如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。

(2)如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual 关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)。

8.3 参数的缺省值

有一些参数的值在每次函数调用时都相同,书写这样的语句会使人厌烦。C++语言采用参数的缺省值使书写变得简洁(在编译时,缺省值由编译器自动插入)。

1.参数缺省值只能出现在函数的声明中,而不能出现在定义体中。

2.如果函数有多个参数,参数只能从后向前挨个儿缺省,否则将导致函数调用语句怪模怪样。

8.4 运算符重载

在C++运算符集合中,有一些运算符是不允许被重载的。这种限制是出于安全方面的考虑,可防止错误和混乱。

(1)不能改变C++内部数据类型(如int,float等)的运算符。

(2)不能重载‘.’,因为‘.’在类中对任何成员都有意义,已经成为标准用法。

(3)不能重载目前C++运算符集合中没有的符号,如#,@,$等。原因有两点,一是难以理解,二是难以确定优先级。

(4)对已经存在的运算符进行重载时,不能改变优先级规则,否则将引起混乱。

9 文件操作

9.1 避免路径穿越问题

1. 在进行文件操作时,需要判断外部传入的文件名是否合法,如果文件名中包含 ../ 等特殊字符,则会造成路径穿越,导致任意文件的读写。

9.2 避免相对路径导致的安全问题(DLL、EXE劫持等问题)

1. 在程序中,使用相对路径可能导致一些安全风险,例如DLL、EXE劫持等问题。

2. 针对DLL劫持的安全编码的规范:

(1)调用LoadLibrary,LoadLibraryEx,CreateProcess,ShellExecute等进行模块加载的函数时,指明模块的完整(全)路径,禁止使用相对路径,这样就可避免从其它目录加载DLL。
(2)在应用程序的开头调用SetDllDirectory(TEXT("")); 从而将当前目录从DLL的搜索列表中删除。结合SetDefaultDllDirectories,AddDllDirectory,RemoveDllDirectory这几个API配合使用,可以有效的规避DLL劫持问题。

9.3 文件权限控制

1. 在创建文件时,需要根据文件的敏感级别设置不同的访问权限,以防止敏感数据被其他恶意程序读取或写入。

10 加密解密

10.1 不得明文存储用户密码等敏感数据

1. 用户密码应该使用 Argon2,scrypt,bcrypt,pbkdf2 等算法做哈希之后再存入存储系统,

2. 用户敏感数据,应该做到传输过程中加密,存储状态下加密
3. 传输过程中加密,可以使用 HTTPS 等认证加密通信协议

4. 存储状态下加密,可以使用 SQLCipher 等类似方案。

10.2 内存中的用户密码等敏感数据应该安全抹除

1. 例如用户密码等,即使是临时使用,也应在使用完成后应当将内容彻底清空。

10.3 rand() 类函数应正确初始化

1. rand类函数的随机性并不高。而且在使用前需要使用srand()来初始化。未初始化的随机数可能导致某些内容可预测。

10.4 在需要高强度安全加密时不应使用弱PRNG函数

1. 在需要生成 AES/SM1/HMAC 等算法的密钥/IV/Nonce, RSA/ECDSA/ECDH 等算法的私钥,这类需要高安全性的业务场景,必须使用密码学安全的随机数生成器 (Cryptographically Secure PseudoRandom Number Generator (CSPRNG) ), 不得使用 rand() 等无密码学安全性保证的普通随机数生成器。

2. rand()类函数的随机性并不高。敏感操作时,如设计加密算法时,不得使用rand()或者类似的简单线性同余伪随机数生成器来作为随机数发生器。

3. 当需要实现高强度加密,例如涉及通信安全时,不应当使用 rand() 作为随机数发生器。

10.5 自己实现的rand范围不应过小

1. 如果在弱安全场景相关的算法中自己实现了PRNG,请确保rand出来的随机数不会很小或可预测。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值