系统安全:面试相关

本文详细梳理了C++编程语言的基础知识,包括内存管理、异常处理、函数重载、线程同步等,并深入探讨了网络安全领域的核心概念,如PE文件结构、木马分析、反病毒策略、系统调用、DLL注入等。同时,介绍了Windows消息机制、进程通信方式以及线程同步的不同实现。此外,还涵盖了反调试技术、病毒特征码以及主动防御策略,为安全研究员和反病毒工程师的面试准备提供了全面的知识框架。
摘要由CSDN通过智能技术生成

参考

博客园:C++常见面试题

相关职位

安全研究员

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

反病毒工程师

在这里插入图片描述

面试题

对绿盟的了解

绿盟科技研究院致力于跟踪国内外最新网络安全攻防技术,天机、天枢、星云、格物、伏影五大实验室分别专注于攻防对抗技术,数据智能,云计算安全,工业互联网、物联网、车联网和威胁追踪研究,在基础安全研究和前沿安全领域进行积极的探索,为绿盟科技的核心竞争力和持续创新能力提供了有力的保障。抗拒绝服务攻击系统(ADS)、安全和漏洞管理(AIRO)、网络入侵防护系统(IDPS)、Web应用防火墙(WAF)等多款产品,知名度很高

对360的了解

在这里插入图片描述
在这里插入图片描述

基础部分

1. malloc / free和new / delete

都是在堆(heap)上进行动态的内存操作。用malloc函数需要指定内存分配的字节数并且不能初始化对象,new 会自动调用对象的构造函数。delete 会调用对象的destructor,而free 不会调用对象的destructor.

malloc和new从申请的内存所在位置、返回类型安全性、内存分配失败时的返回值、是否需要指定内存大小这四点区分。

  1. 申请的内存所在位置不同。new操作符从自由存储区(free store)上为对象动态分配内存空间。自由存储区是C++基于new操作符的一个抽象概念,凡是通过new操作符进行内存申请,该内存即为自由存储区。malloc函数从堆上动态分配内存。堆是操作系统中的术语,是操作系统所维护的一块特殊内存,用于程序的内存动态分配,C语言使用malloc从堆上分配内存,使用free释放已分配的对应内存。

  2. 返回类型安全性不同。new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符。malloc内存分配成功则是返回void * ,需要通过强制类型转换将void*指针转换成我们需要的类型。

  3. 内存分配失败时的返回值不同。new内存分配失败时,会抛出bac_alloc异常,它不会返回NULL。malloc分配内存失败时返回NULL。

  4. 是否需要指定内存大小不同。使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算。malloc则需要显式地指出所需内存的尺寸。

2. try-catch异常处理

  1. 若有异常则通过throw操作创建一个异常对象并抛掷。

  2. 将可能抛出异常的程序段嵌在try块之中。控制通过正常的顺序执行到达try语句,然后执行try块内的保护段。

  3. 如果在保护段执行期间没有引起异常,那么跟在try块后的catch子句就不执行。程序从try块后跟随的最后一个catch子句后面的语句继续执行下去。

  4. 如果发生异常, catch子句按其在try块后出现的顺序被检查。匹配的catch子句将捕获并处理异常(或继续抛掷异常)。

  5. 如果匹配的处理器未找到,则运行函数terminate将被自动调用,其缺省功能是调用abort终止程序。

  6. 不准备处理的异常,可以在catch的最后一个分支,使用throw语法,向上扔。

3. C++的重载

函数重载:

  1. 函数名称必须相同。
  2. 参数列表必须不同(个数不同、类型不同、参数排列顺序不同等)。
  3. 函数的返回类型可以相同也可以不相同。仅仅返回类型不同不足以成为函数的重载。

函数重载的作用:
重载函数通常用来在同一个作用域内用同一个函数名命名一组功能相似的函数,这样做减少了函数名的数量,避免了名字空间的污染,提高了程序的可读性。

运算符重载:
运算符重载其实就是定义一个函数,在函数体内实现想要的功能,当用到该运算符时,编译器会自动调用这个函数。也就是说,运算符重载是通过函数实现的,它本质上是函数重载。

运算符重载的格式为:

返回值类型 operator 运算符名称 (形参表列){
    //TODO:
}

4. 线程同步如何实现

  1. 临界区
    在任意时刻只允许一个线程对共享资源进行访问。假如有多个线程试图同时访问临界区,那么在有一个线程进入后其他任何试图访问此临界区的线程将被挂起,并一直持续到进入临界区的线程离开。临界区在被释放后,其他线程能够继续抢占,并以此达到用原子方式操作共享资源的目的。
  2. 互斥量
    只有拥有互斥对象的线程才具备访问资源的权限,由于互斥对象只有一个,因此就决定了任何情况下此共享资源都不会同时被多个线程所访问。当前占据资源的线程在任务处理完后应将拥有的互斥对象交出,以便其他线程在获得后得以访问资源。互斥量比临界区复杂。因为使用互斥不但仅能够在同一应用程式不同线程中实现资源的安全共享,而且能够在不同进程的线程之间实现对资源的安全共享。
  3. 信号量
    信号量允许多个线程同时使用共享资源,这和操作系统中的PV操作相同。他指出了同时访问共享资源的线程最大数目。他允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目。

总结
1. 互斥量和临界区的作用很相似,但互斥量是能够命名的,也就是说他能够跨越进程使用。所以创建互斥量需要的资源更多,所以假如只为了在进程内部是用的话使用临界区会带来速度上的优势并能够减少资源占用量。因为互斥量是跨进程的互斥量一旦被创建,就能够通过名字打开他。

2. 互斥量(Mutex),信号灯(Semaphore),事件(Event)都能够被跨越进程使用来进行同步数据操作,而其他的对象和数据同步操作无关,但对于进程和线程来讲,假如进程和线程在运行状态则为无信号状态,在退出后为有信号状态。所以能够使用WaitForSingleObject来等待进程和线程退出。

3. 通过互斥量能够指定资源被独占的方式使用,但假如有下面一种情况通过互斥量就无法处理,比如现在一位用户购买了一份三个并发访问许可的数据库系统,能够根据用户购买的访问许可数量来决定有多少个线程/进程能同时进行数据库操作,这时候假如利用互斥量就没有办法完成这个需要,信号灯对象能够说是一种资源计数器

5. Windows消息机制Send和Post的区别

  1. PostMessage 和SendMessage的区别主要在于是否等待其他程序消息处理完成。
    PostMessage只是把消息放入队列,不管其他程序是否处理都返回,然后继续执行。
    而SendMessage则必须等待其他程序处理消息完成后才返回继续执行。由于SendMessage消息不放进消息队列, 所以PreTranslateMessage里无法收到其消息。

  2. 这两个函数的返回值也不同
    原型:
    BOOL PostMessage(HWND hWnd,UINT Msg,WPARAM wParam,LPARAM lParam);
    LRESULT SendMessage(HWND hWnd,UINT Msg,WPARAM wParam,LPARAM IParam);
    SendMessage的返回值表示其他程序消息处理函数的返回值。
    PostMessage的返回值仅表示PostMessage函数执行是否成功,成功返回非零,否则返回零。

6. 进程通信方式

  1. 管道pipe:管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
  2. 命名管道FIFO:有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
  3. 消息队列MessageQueue:消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
  4. 共享存储SharedMemory:共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信。
  5. 信号量Semaphore:信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
  6. 套接字Socket:套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同主机及其进程间的进程通信。

7. call与jmp的区别

jmp无条件跳转,无返回,没有压栈(起到保护数据的作用)。call通过入口地址跳转有返回,返回地址压入堆栈。

8. 堆与栈的区别

  1. 堆栈空间分配区别
    栈(操作系统):由操作系统(编译器)自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
    堆(操作系统): 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收,分配方式类似于链表。

  2. 堆栈缓存方式区别
    栈使用的是一级缓存, 它们通常都是被调用时处于存储空间中,调用完毕立即释放。
    堆则是存放在二级缓存中,生命周期由虚拟机的垃圾回收算法来决定(并不是一旦成为孤儿对象就能被回收)。所以调用这些对象的速度要相对来得低一些。

  3. 堆栈数据结构区别
    堆(数据结构):堆可以被看成是一棵树,如:堆排序。
    栈(数据结构):一种先进后出的数据结构。

  4. 系统处理申请的区别
    栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。
    堆:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的 首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。

10. inc和add

INC op:
inc指令对操作数op加1(增量),它是一个单操作数指令。操作数可以是寄存器或存储器。由于增量指令主要用于对计数器和地址指针的调整,所以它不影响进位标志CF,对其他状态标志位的影响与add、ado指令一样。

ADD op1 op2:
op1与op2相加,存在op1的位置。是双操作数指令,操作数的长度必须相同。ADD可能会改变进位标志CF。

11. add eip, 1

12. Windows消息机制流程

在这里插入图片描述
在这里插入图片描述

13. struct与union的区别

  1. 结构体和联合体都是由多个不同的数据类型成员组成, 但在任何同一时刻, 联合体中只存放了一个被选中的成员(所有成员共用一块地址空间), 而结构体的所有成员都存在(不同成员的存放地址不同)。
  2. 对于联合体的不同成员赋值, 将会对其它成员重写, 原来成员的值就不存在了, 而对于结构的不同成员赋值是互不影响的。

14. 大小端及其判断

在这里插入图片描述

大端:高字节保存在低地址
小端:低字节保存在低地址

  1. 取值比较法
    在这里插入图片描述
  2. 转换比较法在这里插入图片描述

15. ifndef、define、endif

.h头文件中的ifndef/define/endif 的作用是防止头文件被重复引用。
“被重复引用”是指一个头文件在同一个cpp文件中被include了多次,这种错误常常是由于include嵌套造成的。比如:存在a.h文件#include "c.h"而此时b.cpp文件导入了#include “a.h” 和#include "c.h"此时就会造成c.h重复引用。

16. extern “C”

extern "C"的主要作用就是为了能够正确实现C++代码调用其他C语言代码。加上extern "C"后,会指示编译器这部分代码按C语言(而不是C++)的方式进行编译。由于C++支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名;而C语言并不支持函数重载,因此编译C语言代码的函数时不会带上函数的参数类型,一般只包括函数名。

17. 封装、继承与多态

面向对象的三个基本特征

  1. 封装:将客观事物抽象成类,每个类对自身的数据和方法实行protection(private, protected,public)

  2. 继承:广义的继承有三种实现形式:实现继承(指使用基类的属性和方法而无需额外编码的能力)、可视继承(子窗体使用父窗体的外观和实现代码)、接口继承(仅使用属性和方法,实现滞后到子类实现)。前两种(类继承)和后一种(对象组合=>接口继承以及纯虚函数)构成了功能复用的两种方式。

  3. 多态:系统能够在运行时,能够根据其类型确定调用哪个重载的成员函数的能力,称为多态性。

18. 重载与重写

从定义上来说:

重载:是指允许存在多个同名函数,而这些函数的参数表不同(或许参数个数不同,或许参数类型不同,或许两者都不同)。

重写:是指子类重新定义父类虚函数的方法。

从实现原理上来说:

重载:编译器根据函数不同的参数表,对同名函数的名称做修饰,然后这些同名函数就成了不同的函数(至少对于编译器来说是这样的)。如,有两个同名函数:function func(p:integer):integer;和function func(p:string):integer;。那么编译器做过修饰后的函数名称可能是这样的:int_func、str_func。对于这两个函数的调用,在编译器间就已经确定了,是静态的。也就是说,它们的地址在编译期就绑定了(早绑定),因此,重载和多态无关!

重写:和多态真正相关。当子类重新定义了父类的虚函数后,父类指针根据赋给它的不同的子类指针,动态的调用属于子类的该函数,这样的函数调用在编译期间是无法确定的(调用的子类的虚函数的地址无法给出)。因此,这样的函数地址是在运行期绑定的(晚绑定)。

19. 内存分配方式

描述内存分配方式以及它们的区别?
1) 从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static 变量。
2) 在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集。
3) 从堆上分配,亦称动态内存分配。程序在运行的时候用malloc 或new 申请任意多少的内存,程序员自己负责在何时用free 或delete 释放内存。动态内存的生存期由程序员决定,使用非常灵活,但问题也最多。

进阶部分

1. 多线程遍历整个磁盘

参考

  1. 使用递归的方法,深度优先或广度优先遍历整颗目录树。
  2. 递归比较慢,每一次调用都要压栈弹栈,系统开销比较大。考虑改写成多线程非递归的方法。但是多线程也不能开辟太多线程,不然创建线程和垃圾回收的开销也很大。
  3. 用递归深度遍历算法遍历文件夹,当遇到比较大的文件夹,比如说包含1000个子文件夹就记录下来,然后跳过继续遍历其他的文件夹,此时主线程有一个while循环一直在检查有没有新的大文件夹出现,如果有就开一个新线程去遍历大文件夹。
  4. 避免对系统冲击过大:历遍磁盘对系统冲击相当大,如果搜索时间较长,应该适当挂起程序,将时间片还给系统。

专业部分

1. PE文件如何添加节区

参考1
参考2

  1. 在最后一个节表位置添加一个IMAGE_SECTION_HEADER。如果没有空白位置.自己需要给扩展头扩大,修改SizeOfHeaders,并且修正所有节的偏移

  2. 修改文件头中节表个数NumberOfSections

  3. 添加的新节表修改节的读写属性.、节装载到内存的RVA 、节数据的大小、节的文件偏移

  4. 修改扩展头的PE镜像大小. sizeofImage,增加一个或数个内存页的大小。

2. 分析木马的执行流程

白加黑远控木马分析

3. 逆向工具

IDA、OllyDbg、x64dbg、C32ASM

4. 如何实现免杀

加壳:其实是利用特殊的算法,对EXE、DLL文件里的资源进行压缩,
改变其原来的特征码,隐藏一些字符串等等,使一些资源编辑软件不能
正常打开或者修改。

花指令:花指令是程序中的无用代码,程序对它没影响,少了它也能正常运行。加花指令后,杀毒软件对木马静态反汇编时,木马的代码就不会正常显示出来,加大杀毒软件的查杀难度。

5. 病毒木马自启技术

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

6. 手工查杀的思路

知了堂信安笔记 window入侵排查
Windows主机入侵痕迹排查办法
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

7. PE病毒感染技术

必要条件:

  1. 目标程序有足够空间可写入
  2. 写入的代码有机会被执行

实现:

  1. 空间问题,有两种方法比较容易。一是在PE文件中添加一个节区;二是利用节缝隙,将恶意代码填充到缝隙中。
  2. 执行问题,修改目标可执行文件的入口地址,先执行恶意代码,执行完毕之后再跳转到原程序的入口处继续执行。(类似内联钩子)

其他注意点:
避免二次感染:重复感染目标程序会导致其无法执行。

8. 无文件病毒

9. DLL劫持及防御

劫持DLL就是要制作一个“假”的DLL,但是功能又不能失真。
可执行文件在调用某函数时,要加载该函数所在的DLL。如果我们伪造一个DLL,让它包含所有被劫持DLL的导出函数。根据加载顺序,可执行文件会运行加载伪造的DLL,在伪造DLL里面做我们自己想做的事情。

防御策略:

  1. 保护游戏目录,不是自己的程序不让拷贝。(主要是防止被加入恶意的DLL到游戏的目录,驱动实现)。

  2. 创建一份游戏模块的白名单,游戏启动时对游戏目录下的文件进行检查,检查可疑的文件。白名单可本地加密存储。

  3. 将容易被劫持的Windows DLL 写进注册表,那么凡是此项下的DLL文件就会被禁止从EXE自身目录下调用,而只能从系统目录,也就是system32目录下调用。防止从游戏目录加载其它DLL。

  4. 确保windows文件保护功能是打开的。“计算机配置”→“管理模板”→“系统”→“Windows 文件保护”.

  5. 将游戏的DLL打上签名,防止自身的DLL被劫持。游戏运行时检查游戏目录下模块的签名。(没有签名拒绝加载–必须确保检查签名服务打开)
    注意:通过命令行“net stop cryptsvc”关闭签名检验服务可使得客户端签名校验失效。

  6. 使用Process Monitor 检查游戏进程的可劫持的DLL 。

10. ring0和ring3

ring0是指CPU的运行级别,ring0是最高级别,ring1次之,ring2更次之…… 拿Linux+x86来说, 操作系统(内核)的代码运行在最高运行级别ring0上,可以使用特权指令,控制中断、修改页表、访问设备等等。 应用程序的代码运行在最低运行级别上ring3上,不能做受控操作。如果要做,比如要访问磁盘,写文件,那就要通过执行系统调用(函数),执行系统调用的时候,CPU的运行级别会发生从ring3到ring0的切换,并跳转到系统调用对应的内核代码位置执行,这样内核就为你完成了设备访问,完成之后再从ring0返回ring3。这个过程也称作用户态和内核态的切换。

11. 通过Windows日志检查病毒感染

在Windows日志里发现入侵痕迹

12. RVA转FileOffset

首先要判断此RVA在哪一节。
依次计算各节的RVA范围:起始RVA 到 起始RVA+节数据大小
判断出在哪一节之后,用此RVA减去此节的起始RVA,得到一个偏移量。
无论是在内存中还是在文件中,这个偏移量都是相同的。
而节的FileOffset在节表中存着(IMAGE_SECTION_HEADER->PointerTORawData)
所以可以得到此RVA对应的FileOffset = 节FileOffset + 目标RVA相对于节的偏移量。

13. PE资源节

14. 反调试手段

26种对付反调试的方法

15. SHE

16. DLL注入

首先使用OpenProcess打开指定PID的进程,权限要足够,不然可能出现无法创建线程的情况。

第二,获得dll文件的绝对路径的字符串长度,并使用VirtualAllocEx在被注入进程中申请相应大小的内存。

第三,使用WriteProcessMemory将dll文件的绝对路径字符串写入被注入进程。

第四,以dll文件的句柄、要查找的函数名作为GetProcAddress的参数,查找本进程中加载的Kernel32.dll中LoadLibrary函数的始址,根据是否采用UNICODE有不同的函数W/ A。因为这个dll在所有进程中加载的位置都相同,所以本进程中的函数位置就是被注入进程中函数的位置。

第五,在被注入进程中使用CreateRemoteThread创建线程,参数为线程函数(LoadLibrary)地址,线程函数参数(dll文件的绝对路径)

第六,若成功注入,因为dll是首次被load,所以调用其DllMain时,fdwReason参数的值为DLL_PROCESS_ATTACH,判断一下,会弹窗提示成功。

17. 代码注入

代码远程线程注入,实质是自定义线程函数,然后将线程函数代码和线程函数中用到的数据分别注入到目标进程,最后将数据作为线程函数的参数创建远程线程,执行自定义操作。

示例:不依赖自定义dll,在被注入进程中创建线程实现弹窗,窗口标题为被注入进程的位置。

线程函数:
线程函数要做的事情是弹窗,弹窗需要得到MessageBoxA在被注入进程中的位置,因此需要加载user32.dll,还需要字符串"MessageBoxA"。加载要用到Kernel32.dll中的LoadLibrary、GetProcAddress,还需要字符串"user32.dll",在标题显示被注入进程的位置需要Kernal32.dll中的GetModuleFileName获取进程当前的运行目录。
上述用到的数据,在注入函数创建线程函数时被封装为一个结构体作为参数传给线程函数。
线程函数获得数据,调用被注入进程中的MessageBoxA弹窗

注入函数:
获取线程函数要用到的Kernel32.dll中的导出函数地址,用函数指针存放其在本进程中的地址,Kernel32.dll在各进程中位置相同,所以可用其在本进程的地址代替在被注入进程中的地址
上述所有线程函数用到的函数地址、数据等,再加一个弹窗要显示的字符串,封装到一个结构体DATA中。
将结构体、线程函数的代码注入进程
创建远程线程,将结构体作为参数传给线程函数。

18. 壳与脱壳

加壳基础

19. PE文件校验

BOOL PELoader::JudgePEFile() {
    // 拿到DOS头,始址为:内存映像始址
    this->pImageDosHeader = (PIMAGE_DOS_HEADER)pFileBuffer;
    // 判断MZ
    if (this->pImageDosHeader->e_magic != IMAGE_DOS_SIGNATURE){
        std::cout << "MZ judge failed." << std::endl;
        return false;
    }

    // 拿到PE头,始址为:内存映像始址 + DOS头中存的PE头起始位置
    this->pImageNtHeaders = (PIMAGE_NT_HEADERS32)((DWORD64)pFileBuffer + this->pImageDosHeader->e_lfanew);
    // 判断是否为PE文件
    if (pImageNtHeaders->Signature != IMAGE_NT_SIGNATURE){
        std::cout << "PE judge failed." << std::endl;
        return false;
    }

}

20. PE加载器

PE:模拟Windows加载器

21. 内核模式下的调试工具

22. GDT与LDT

23. Hook技术

Windows:32位Inline Hook

24. 病毒特征码

病毒特征码查杀之基本原理

35. 主动防御

36. 病毒行为特征

37. 病毒常用的API

病毒常用API

38. 计算机中毒的特征

在这里插入图片描述
计算机中毒的特征是:

1)在某些情况下屏幕上会出现一些异常字符或某些图片;

2)文件长度异常增加或减少,或者莫名其妙地生成了新文件;

3)某些文件异常打开或突然丢失;

4)系统无缘无故地执行大量磁盘读写操作,或者未经用户许可执行格式化操作;

5)系统异常重启,经常崩溃或蓝屏无法进入系统;

6)可用内存或硬盘空间变小;

7)打印机等外部设备工作异常;

8)在正常的汉字库中,汉字无法调用和打印,或者汉字库无故损坏;

9)该扇区无故损坏;

10)程序或数据神秘地消失了,无法识别文件名,等等。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值