2.3 Windows驱动开发:内核字符串转换方法

在内核编程中字符串有两种格式ANSI_STRINGUNICODE_STRING,这两种格式是微软推出的安全版本的字符串结构体,也是微软推荐使用的格式,通常情况下ANSI_STRING代表的类型是char *也就是ANSI多字节模式的字符串,而UNICODE_STRING则代表的是wchar*也就是UNCODE类型的字符,如下文章将介绍这两种字符格式在内核中是如何转换的。

在Windows内核中,字符串的处理十分重要。不同于用户态程序,内核中的字符串必须遵循严格的安全规则,以确保不会引发各种安全漏洞。

ANSI_STRINGUNICODE_STRING是微软在内核中推出的两种安全版本的字符串结构体,ANSI_STRING代表的是ANSI多字节模式的字符串,而UNICODE_STRING则代表的是UNCODE类型的字符。这两种字符串类型可以相互转换,因此在内核编程中,需要经常进行类型转换。

ANSI_STRINGUNICODE_STRING之间的转换可以通过内核中提供的一系列函数实现。其中,最常用的是RtlUnicodeStringToAnsiStringRtlAnsiStringToUnicodeString这两个函数。这两个函数分别用于将UNICODE_STRING类型的字符串转换成ANSI_STRING类型的字符串,以及将ANSI_STRING类型的字符串转换成UNICODE_STRING类型的字符串。

2.3.1 初始化字符串

在内核开发模式下初始化字符串也需要调用专用的初始化函数,使用ANSI字符串时需要调用RtlInitAnsiString函数进行初始化,而使用Unicode字符串时则需要调用RtlInitUnicodeString函数进行初始化。这两个函数都需要传入要初始化的字符串和字符串长度,初始化完成后就可以对字符串进行使用了。如下分别初始化ANSIUNCODE字符串,我们来看看代码是如何实现的。

#include <ntifs.h>
#include <ntstrsafe.h>

VOID UnDriver(PDRIVER_OBJECT driver)
{
    DbgPrint("驱动卸载成功 \n");
}

NTSTATUS DriverEntry(IN PDRIVER_OBJECT Driver, PUNICODE_STRING RegistryPath)
{
    // 定义内核字符串
    ANSI_STRING ansi;
    UNICODE_STRING unicode;
    UNICODE_STRING str;

    // 定义普通字符串
    char * char_string = "hello lyshark";
    wchar_t *wchar_string = (WCHAR*)"hello lyshark";

    // 初始化字符串的多种方式
    RtlInitAnsiString(&ansi, char_string);
    RtlInitUnicodeString(&unicode, wchar_string);
    RtlUnicodeStringInit(&str, L"hello lyshark");

    // 改变原始字符串(乱码位置,此处仅用于演示赋值方式)
    char_string[0] = (CHAR)"A";         // char类型每个占用1字节
    char_string[1] = (CHAR)"B";

    wchar_string[0] = (WCHAR)"A";        // wchar类型每个占用2字节
    wchar_string[2] = (WCHAR)"B";

    // 输出字符串 %Z
    DbgPrint("输出ANSI: %Z \n", &ansi);
    DbgPrint("输出WCHAR: %Z \n", &unicode);
    DbgPrint("输出字符串: %wZ \n", &str);

    DbgPrint("驱动加载成功 \n");

    Driver->DriverUnload = UnDriver;
    return STATUS_SUCCESS;
}

代码输出效果如下图所示;

2.3.2 字符串与整数转换

内核中还可实现字符串与整数之间的灵活转换,内核中提供了RtlUnicodeStringToInteger这个函数来实现字符串转整数,与之对应的RtlIntegerToUnicodeString则是将整数转为字符串这两个内核函数也是非常常用的。

通常使用RtlUnicodeStringToInteger函数来将Unicode字符串转换为整数,函数原型为:

NTSYSAPI NTSTATUS NTAPI RtlUnicodeStringToInteger(
  PCUNICODE_STRING   String,
  ULONG              Base,
  PULONG             Value
);

其中,String参数为输入的Unicode字符串,Base参数为进制数(通常为10进制),Value参数为输出的整数。返回值为函数执行状态,如果成功则返回STATUS_SUCCESS

与之对应的是RtlIntegerToUnicodeString函数,用于将整数转换为Unicode字符串,函数原型为:

NTSYSAPI NTSTATUS NTAPI RtlIntegerToUnicodeString(
  ULONG  Value,
  ULONG  Base,
  PUNICODE_STRING  String
);

其中,Value参数为输入的整数,Base参数为进制数,String参数为输出的Unicode字符串。返回值同样为函数执行状态,如果成功则返回STATUS_SUCCESS

#include <ntifs.h>
#include <ntstrsafe.h>

VOID UnDriver(PDRIVER_OBJECT driver)
{
  DbgPrint("驱动卸载成功 \n");
}

NTSTATUS DriverEntry(IN PDRIVER_OBJECT Driver, PUNICODE_STRING RegistryPath)
{
  NTSTATUS flag;
  ULONG number;

  DbgPrint("hello lyshark \n");

  UNICODE_STRING uncode_buffer_source = { 0 };
  UNICODE_STRING uncode_buffer_target = { 0 };

  // 字符串转为数字
  RtlInitUnicodeString(&uncode_buffer_source, L"100");
  flag = RtlUnicodeStringToInteger(&uncode_buffer_source, 10, &number);

  if (NT_SUCCESS(flag))
  {
    DbgPrint("字符串 -> 数字: %d \n", number);
  }

  // 数字转为字符串
  uncode_buffer_target.Buffer = (PWSTR)ExAllocatePool(PagedPool, 1024);
  uncode_buffer_target.MaximumLength = 1024;

  flag = RtlIntegerToUnicodeString(number, 10, &uncode_buffer_target);

  if (NT_SUCCESS(flag))
  {
    DbgPrint("数字 -> 字符串: %wZ \n", &uncode_buffer_target);
  }

  // 释放堆空间
  RtlFreeUnicodeString(&uncode_buffer_target);

  DbgPrint("驱动加载成功 \n");

  Driver->DriverUnload = UnDriver;
  return STATUS_SUCCESS;
}

代码输出效果如下图所示;

2.3.3 字符串ANSI与UNICODE

UNICODE_STRING结构转换成ANSI_STRING结构,代码中调用了RtlUnicodeStringToAnsiString内核函数,该函数也是微软提供的。

UNICODE_STRING结构转换成ANSI_STRING结构的代码,核心部分可归纳为:

ANSI_STRING AnsiStr;
UNICODE_STRING UniStr;
RtlUnicodeStringToAnsiString(&AnsiStr, &UniStr, TRUE);

其中,AnsiStr是要存储转换后的ANSI字符串的结构体,UniStr是要转换的UNICODE字符串结构体,第三个参数TRUE表示要分配一个缓冲区来存储转换后的字符串。

注意,使用RtlUnicodeStringToAnsiString函数时,需要在使用完后调用RtlFreeAnsiString函数来释放所分配的缓冲区。

#include <ntifs.h>
#include <ntstrsafe.h>

VOID UnDriver(PDRIVER_OBJECT driver)
{
    DbgPrint("驱动卸载成功 \n");
}

NTSTATUS DriverEntry(IN PDRIVER_OBJECT Driver, PUNICODE_STRING RegistryPath)
{
    DbgPrint("hello lyshark \n");

    UNICODE_STRING uncode_buffer_source = { 0 };
    ANSI_STRING ansi_buffer_target = { 0 };

    // 初始化 UNICODE 字符串
    RtlInitUnicodeString(&uncode_buffer_source, L"hello lyshark");

    // 转换函数
    NTSTATUS flag = RtlUnicodeStringToAnsiString(&ansi_buffer_target, &uncode_buffer_source, TRUE);

    if (NT_SUCCESS(flag))
    {
        DbgPrint("ANSI: %Z \n", &ansi_buffer_target);
    }

    // 销毁ANSI字符串
    RtlFreeAnsiString(&ansi_buffer_target);

    Driver->DriverUnload = UnDriver;
    return STATUS_SUCCESS;
}

代码输出效果如下图所示;

如果将上述过程反过来,将ANSI_STRING转换为UNICODE_STRING结构,则需要调用RtlAnsiStringToUnicodeString这个内核专用函数实现。

RtlAnsiStringToUnicodeString函数的作用是将ANSI_STRING结构体转换成UNICODE_STRING结构体,其中ANSI_STRING代表的是ANSI格式的字符串,而UNICODE_STRING代表的是Unicode格式的字符串。具体实现过程如下:

首先需要定义一个ANSI_STRING结构体变量ansiStr,并初始化其中的Buffer、MaximumLengthLength成员变量,其中Buffer成员变量指向存储ANSI格式字符串的缓冲区,MaximumLength成员变量表示该缓冲区的最大长度,Length成员变量表示该缓冲区中已经使用的长度。

接着需要定义一个UNICODE_STRING结构体变量uniStr,并初始化其中的Buffer、MaximumLengthLength成员变量,其中Buffer成员变量指向存储Unicode格式字符串的缓冲区,MaximumLength成员变量表示该缓冲区的最大长度,Length成员变量表示该缓冲区中已经使用的长度。

调用RtlAnsiStringToUnicodeString函数,传入两个参数,第一个参数为要转换的UNICODE_STRING结构体指针,第二个参数为要转换的ANSI_STRING结构体指针。函数会将ANSI_STRING中的内容转换为Unicode格式,并将结果存储在UNICODE_STRING结构体的Buffer成员变量中。

调用完成后,uniStr.Buffer中就存储了转换后的Unicode格式字符串,可以进行后续的操作。

需要注意的是,RtlAnsiStringToUnicodeString函数在使用完毕后,还需要调用RtlFreeUnicodeString函数释放内存。

#include <ntifs.h>
#include <ntstrsafe.h>

VOID UnDriver(PDRIVER_OBJECT driver)
{
    DbgPrint("驱动卸载成功 \n");
}

NTSTATUS DriverEntry(IN PDRIVER_OBJECT Driver, PUNICODE_STRING RegistryPath)
{
    DbgPrint("hello lyshark \n");

    UNICODE_STRING uncode_buffer_source = { 0 };
    ANSI_STRING ansi_buffer_target = { 0 };

    // 初始化字符串
    RtlInitString(&ansi_buffer_target, "hello lyshark");

    // 转换函数
    NTSTATUS flag = RtlAnsiStringToUnicodeString(&uncode_buffer_source, &ansi_buffer_target, TRUE);
    if (NT_SUCCESS(flag))
    {
        DbgPrint("UNICODE: %wZ \n", &uncode_buffer_source);
    }

    // 销毁UNICODE字符串
    RtlFreeUnicodeString(&uncode_buffer_source);

    Driver->DriverUnload = UnDriver;
    return STATUS_SUCCESS;
}

代码输出效果如下图所示;

如上代码是内核通用结构体之间的转换类型,有时我们还需要将各类结构体转为普通的字符类型,例如下方的两个案例:

例如将UNICODE_STRING 转为 CHAR*类型。将UNICODE_STRING转换为CHAR*类型需要先将UNICODE_STRING转换为ANSI_STRING类型,然后再将ANSI_STRING类型转换为CHAR*类型。

具体步骤可以总结为如下:

  • 1.定义ANSI_STRINGUNICODE_STRING类型的变量,分别用于存储转换前后的字符串;
  • 2.调用RtlUnicodeStringToAnsiString函数,将UNICODE_STRING转换为ANSI_STRING类型;
  • 3.定义一个CHAR*类型的变量,用于存储转换后的字符串;
  • 4.将ANSI_STRING类型转换为CHAR*类型,可以使用ANSI_STRING.Buffer指向的字符数组作为CHAR*类型的字符串。

以下是示例代码,可用于测试两者的转换模式;

#define _CRT_SECURE_NO_WARNINGS
#include <ntifs.h>
#include <windef.h>
#include <ntstrsafe.h>

VOID UnDriver(PDRIVER_OBJECT driver)
{
    DbgPrint("驱动卸载成功 \n");
}

NTSTATUS DriverEntry(IN PDRIVER_OBJECT Driver, PUNICODE_STRING RegistryPath)
{
    DbgPrint("hello lyshark \n");

    UNICODE_STRING uncode_buffer_source = { 0 };
    ANSI_STRING ansi_buffer_target = { 0 };
    char szBuf[1024] = { 0 };

    // 初始化 UNICODE 字符串
    RtlInitUnicodeString(&uncode_buffer_source, L"hello lyshark");

    // 转换函数
    NTSTATUS flag = RtlUnicodeStringToAnsiString(&ansi_buffer_target, &uncode_buffer_source, TRUE);

    if (NT_SUCCESS(flag))
    {
        strcpy(szBuf, ansi_buffer_target.Buffer);
        DbgPrint("输出char*字符串: %s \n", szBuf);
    }

    // 销毁ANSI字符串
    RtlFreeAnsiString(&ansi_buffer_target);

    Driver->DriverUnload = UnDriver;
    return STATUS_SUCCESS;
}

代码输出效果如下图所示:

如果我们将上述过程反过来实现,将 CHAR*类型转为UNICODE_STRING结构此时有两种可行的方式;

第一种方式,可以通过调用 RtlCreateUnicodeStringFromAsciiz 函数来实现,该函数将 CHAR* 类型的字符串转换成 UNICODE_STRING 结构体。函数原型如下:

NTSYSAPI BOOLEAN RtlCreateUnicodeStringFromAsciiz(
  PUNICODE_STRING DestinationString,
  PCSZ            SourceString
);

函数接受两个参数,分别为目标 UNICODE_STRING 结构体指针和源字符串指针。函数内部将会动态分配内存并将转换后的 UNICODE_STRING 结构体写入到目标结构体指针所指向的内存空间中,同时返回一个布尔值表示操作是否成功。函数的具体用法如下:

CHAR* srcString = "Hello, lyshark!";
UNICODE_STRING destString;

RtlCreateUnicodeStringFromAsciiz(&destString, srcString);

// 对 destString 进行操作
RtlFreeUnicodeString(&destString);

需要注意的是,RtlCreateUnicodeStringFromAsciiz 函数创建的 UNICODE_STRING 结构体内存需要手动释放,否则会产生内存泄漏。可以使用 RtlFreeUnicodeString 函数来释放该内存,函数原型如下:

NTSYSAPI VOID RtlFreeUnicodeString(
  PUNICODE_STRING UnicodeString
);

该函数接受一个 UNICODE_STRING 结构体指针,用于指定需要释放内存的结构体。

而第二种方法则是通过中转的方式实现,首先用户可使用RtlInitString将一个CHAR*初始化为ANSI结构,然后再使用RtlAnsiStringToUnicodeString一次性完成ANSIUNICODE的类型转换;

#define _CRT_SECURE_NO_WARNINGS
#include <ntifs.h>
#include <windef.h>
#include <ntstrsafe.h>

VOID UnDriver(PDRIVER_OBJECT driver)
{
    DbgPrint("驱动卸载成功 \n");
}

NTSTATUS DriverEntry(IN PDRIVER_OBJECT Driver, PUNICODE_STRING RegistryPath)
{
    DbgPrint("hello lyshark \n");

    UNICODE_STRING uncode_buffer_source = { 0 };
    ANSI_STRING ansi_buffer_target = { 0 };

    // 设置CHAR*
    char szBuf[1024] = { 0 };
    strcpy(szBuf, "hello lyshark");

    // 初始化ANSI字符串
    RtlInitString(&ansi_buffer_target, szBuf);

    // 转换函数
    NTSTATUS flag = RtlAnsiStringToUnicodeString(&uncode_buffer_source, &ansi_buffer_target, TRUE);
    if (NT_SUCCESS(flag))
    {
        DbgPrint("UNICODE: %wZ \n", &uncode_buffer_source);
    }

    // 销毁UNICODE字符串
    RtlFreeUnicodeString(&uncode_buffer_source);

    Driver->DriverUnload = UnDriver;
    return STATUS_SUCCESS;
}

代码输出效果如下图所示:

2.3.4 字符串连接操作

字符串还可以进行连接操作,例如将两个不同变量中的字符串进行合并,以此来生成一个新的字符串,通过RtlAppendUnicodeToString这个内核函数即可实现连接。

RtlAppendUnicodeToString用于将 Unicode 字符串追加到另一个 Unicode 字符串的末尾。这个函数位于 ntdll.dll 中,可以通过 NtDll.lib 库来链接,函数的原型如下:

NTSTATUS RtlAppendUnicodeToString(
    PUNICODE_STRING DestinationString,
    PCWSTR SourceString
);

其中,DestinationString 是一个指向目标字符串的 UNICODE_STRING 结构体的指针,而 SourceString 则是一个指向源字符串的 wchar_t 类型的指针。

使用该函数可以很方便地将两个字符串连接起来,只需将第一个字符串作为 DestinationString 参数传递,第二个字符串作为 SourceString 参数传递即可。这个函数将会自动计算两个字符串的长度,并将第二个字符串的内容追加到第一个字符串的末尾。

以下是一个示例代码,将两个字符串 str1str2 连接起来,并输出结果:

#include <ntifs.h>

VOID UnDriver(PDRIVER_OBJECT driver)
{
    DbgPrint("驱动已卸载 \n");
}

NTSTATUS DriverEntry(IN PDRIVER_OBJECT Driver, PUNICODE_STRING RegistryPath)
{
    DbgPrint("hello lyshark \n");

    UNICODE_STRING dst;
    WCHAR dst_buf[256];
    NTSTATUS status;

    // 初始化字符串
    UNICODE_STRING src = RTL_CONSTANT_STRING(L"hello");

    // 字符串初始化为空串,长度为256
    RtlInitEmptyUnicodeString(&dst, dst_buf, 256 * sizeof(WCHAR));

    // 将src拷贝到dst
    RtlCopyUnicodeString(&dst, &src);

    // 在dst之后追加
    status = RtlAppendUnicodeToString(&dst, L" lyshark");

    if (status == STATUS_SUCCESS)
    {
        DbgPrint("输出链接后字符串:%wZ \n", &dst);
    }

    Driver->DriverUnload = UnDriver;
    return STATUS_SUCCESS;
}

最后,我们使用 DbgPrint 函数输出结果。在输出结果之前,我们需要使用 %wZ 格式化符号将 Unicode 字符串作为参数进行输出。

  • 10
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
这是书的光盘。共分为两个部分,这是第一部分。 本书由浅入深、循序渐进地介绍了Windows驱动程序的开发方法与调试技巧。本书共分23章,内容涵盖了 Windows操作系统的基本原理、NT驱动程序与WDM驱动程序的构造、驱动程序中的同步异步处理方法、驱 动程序中即插即用功能、驱动程序的各种调试技巧等。同时,还针对流行的PCI驱动程序、USB驱动程序 、虚拟串口驱动程序、摄像头驱动程序、SDIO驱动程序进行了详细的介绍,本书最大的特色在于每一节 的例子都是经过精挑细选的,具有很强的针对性。力求让读者通过亲自动手实验,掌握各类Windows驱动 程序的开发技巧,学习尽可能多的Windows底层知识。   本书适用于中、高级系统程序员,同时也可用做高校计算机专业操作系统实验课的补充教材。 原创经典,威盛一线工程师倾力打造。深入驱动核心,剖析操作系统底层运行机制,通过实例引导,快 速学习编译、安装、调试的方法。   从Windows最基本的两类驱动程序的编译、安装、调试入手讲解,非常容易上手,用实例详细讲解 PCI、USB、虚拟串口、虚拟摄像头、SDIO等驱动程序的开发,归纳了多种调试驱动程序的高级技巧,如 用WinDBG和VMWARE软件对驱动进行源码级调试,深入Windows操作系统的底层和内核,透析Windows驱动 开发的本质。 本书是作者结合教学和科研实践经验编写而成的,不仅详细介绍了Windows内核原理,而且介绍了编程技 巧和应用实例,兼顾了在校研究生和工程技术人员的实际需求,对教学、生产和科研有现实的指导意义 ,是一本值得推荐的专著。              ——中国工程院院士   院士推荐   目前,电子系统设计广泛采用通用操作系统,达到降低系统的设计难度和缩短研发周期。实现操作 系统与硬件快速信息交换是电子系统设计的关键。   通用操作系统硬件驱动程序的开发,编写者不仅需要精通硬件设备、计算机总线,而且需要Windows 操作系统知识以及调试技巧。学习和掌握Windows硬件驱动程序的开发是电子系统设计人员必备的能力。   本书是作者结合教学和科研实践经验编写而成的,不仅详细介绍了Windows内核原理,并且介绍了编 程技巧和应用实例,兼顾了在校研究生和工程技术人员的实际需求,对教学、生产和科研有现实的指导 意义,是一本值得推荐的专著。 第1篇 入门篇 第1章 从两个最简单的驱动谈起 本章向读者呈现两个最简单的Windows驱动程序,一个是NT式的驱动程序,另一个是WDM式的驱动程序。 这两个驱动程序没有操作具体的硬件设备,只是在系统里创建了虚拟设备。在随后的章节中,它们会作 为基本驱动程序框架,被本书其他章节的驱动程序开发所复用。笔者将带领读者编写代码、编译、安装 和调试程序。   1.1 DDK的安装   1.2 第一个驱动程序HelloDDK的代码分析    1.2.1 HelloDDK的头文件    1.2.2 HelloDDK的入口函数    1.2.3 创建设备例程    1.2.4 卸载驱动例程    1.2.5 默认派遣例程   1.3 HelloDDK的编译和安装    1.3.1 用DDK环境编译HelloDDK    1.3.2 用VC集成开发环境编译HelloDDK    1.3.3 HelloDDK的安装   1.4 第二个驱动程序HelloWDM的代码分析    1.4.1 HelloWDM的头文件    1.4.2 HelloWDM的入口函数    1.4.3 HelloWDM的AddDevice例程    1.4.4 HelloWDM处理PNP的回调函数    1.4.5 HelloWDM对PNP的默认处理    1.4.6 HelloWDM对IRP_MN_REMOVE_DEVICE的处理    1.4.7 HelloWDM对其他IRP的回调函数    1.4.8 HelloWDM的卸载例程   1.5 HelloWDM的编译和安装    1.5.1 用DDK编译环境编译HelloWDM    1.5.2 HelloWDM的编译过程    1.5.3 安装HelloWDM   1.6 小结  第2章 Windows操作驱动的基本概念  驱动程序被操作系统加载在内核模式下,它与Windows操作系统内核的其他组件进行密切交互。本章主 要介绍Windows操作系统内核的基本概念,同时还介绍应用程序和驱动程序之间的通信方法。   2.1 Windows操作系统概述    2.1.1 Windows家族    2.1.2 Windows特性    2.1.3 用户模式和内核模式    
Windows驱动程序中,将整数类型转换字符串类型需要使用内核模式下支持的字符串处理函数。其中,最常用的函数是`RtlStringCbPrintf`。 `RtlStringCbPrintf` 函数的原型如下: ```C NTSTATUS RtlStringCbPrintf( _Out_writes_bytes_(cbDest) NTSTRSAFE_PWSTR pszDest, _In_ size_t cbDest, _In_ NTSTRSAFE_PCWSTR pszFormat, ... ); ``` 该函数的作用是将格式化的字符串写入到缓冲区中。具体来说,它会将格式化的字符串写入到 `pszDest` 指向的缓冲区中,并且保证字符串长度不超过 `cbDest` 字节。在写入字符串时,如果发现缓冲区不足以存放整个字符串,那么函数会自动截断字符串,保证不会造成缓冲区溢出。 下面是一个示例,演示如何使用 `RtlStringCbPrintf` 函数将整数类型转换字符串类型: ```C NTSTATUS IntegerToString( _In_ int nValue, _Out_writes_(cbDest) PWSTR pszDest, _In_ size_t cbDest ) { NTSTATUS status = STATUS_SUCCESS; // 将整数类型转换字符串类型 status = RtlStringCbPrintfW(pszDest, cbDest, L"%d", nValue); if (!NT_SUCCESS(status)) { return status; } return STATUS_SUCCESS; } ``` 在上面的示例中,我们直接使用 `RtlStringCbPrintfW` 函数将整数类型转换字符串类型。这里使用了格式化字符串 `L"%d"`,它表示将整数类型按照十进制格式输出。如果想按照其他格式输出,可以使用其他的格式化字符串,例如 `L"%x"` 表示按照十六进制格式输出。如果缓冲区不足以存放整个转换后的字符串,那么函数会返回相应的错误码。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值