windows内核通讯

驱动程序通讯详解


数据传输成为IO,驱动程序中,IO通过IRP传递给设备对象,所以和驱动程序通讯最常用的方式就是创建IRP。

0. 驱动通讯

  1. 0环
    1. 创建设备对象
    2. 绑定符号链接
    3. 绑定派遣函数
  2. 3环
    1. 通过CreateFile打开设备
    2. 通过ReadFile和WriteFile发起IRP请求.

0.0 通讯过程中的数据的传输

  1. 将3环程序中的一段字符串传递给0环的设备, 并在内核中输出该字符串.

    WriteFile

  2. 将0环设备中的一段字符串传递给3环程序, 并在3环程序中输出该字符串.

    ReadFile

0.1 R0

DriverEntry:

  1. 创建设备对象IoCreateDevice
  2. 绑定符号链接IoCreateSymbolicLink
  3. 赋值驱动对象派遣函数
#include <ntddk.h>

#define NAME_DEVICE	L"\\Device\\mydevice"
#define NAME_SYMBOL	L"\\DosDevices\\mysymbol"


void OnUnload(DRIVER_OBJECT* pDriver);
NTSTATUS OnCreate(DEVICE_OBJECT* pDeviceObject, IRP* pIrp);
NTSTATUS OnRead(DEVICE_OBJECT* pDeviceObject, IRP* pIrp);
NTSTATUS OnWrite(struct _DEVICE_OBJECT* pDeviceObject, IRP* pIrp);
NTSTATUS OnClose(DEVICE_OBJECT* pDevice, IRP* pIrp);

NTSTATUS DriverEntry(DRIVER_OBJECT * pDriver, UNICODE_STRING *regPath)
{
	NTSTATUS status = STATUS_SUCCESS;
	pDriver->DriverUnload = OnUnload;
	regPath;
	KdBreakPoint();

	// 1. Create Device Object
	UNICODE_STRING uStrNameDev = RTL_CONSTANT_STRING(NAME_DEVICE);
	PDEVICE_OBJECT pDevice = NULL;
	status = IoCreateDevice(
		pDriver,
		0,
		&uStrNameDev,
		FILE_DEVICE_UNKNOWN,
		0,
		FALSE,
		&pDevice);
	if (!NT_SUCCESS(status))
	{
		KdPrint(("CreateDevice() error: 0x%08x\n", status));
		return status;
	}
	pDevice->Flags |= DO_BUFFERED_IO;

	// 2. Bind Symbol Link

	UNICODE_STRING uStrNameSymbol = RTL_CONSTANT_STRING(NAME_SYMBOL);
	status = IoCreateSymbolicLink(
		&uStrNameSymbol,
		&uStrNameDev);
	if (!NT_SUCCESS(status))
	{
		KdPrint(("IoCreateSymbolicLink() error: 0x%08x\n", status));
		return status;
	}

	// 3. Dispatch Function
	pDriver->MajorFunction[IRP_MJ_CREATE] = OnCreate;
	pDriver->MajorFunction[IRP_MJ_READ] = OnRead;
	pDriver->MajorFunction[IRP_MJ_WRITE] = OnWrite;
	pDriver->MajorFunction[IRP_MJ_CLOSE] = OnClose;

	return status;
}

void OnUnload(DRIVER_OBJECT* pDriver)
{
	pDriver->DeviceObject;/*设备对象链表*/
	// 删除符号链接
	UNICODE_STRING symbolName = RTL_CONSTANT_STRING(NAME_SYMBOL);
	IoDeleteSymbolicLink(&symbolName);
	// 销毁设备对象.
	IoDeleteDevice(pDriver->DeviceObject);
}


NTSTATUS OnCreate(DEVICE_OBJECT* pDeviceObject, IRP* pIrp)
{
	pDeviceObject;
	NTSTATUS status = STATUS_SUCCESS;
	DbgBreakPoint();

	KdPrint(("OnCreate(): Device is being created\n"));
	// 设置R3 CreateFile执行结果
	pIrp->IoStatus.Status = STATUS_SUCCESS;
	pIrp->IoStatus.Information = 0; // 完成字节数
	/*
	IoStatus
			Contains the IO_STATUS_BLOCK structure in which a driver stores status 
		and information before calling IoCompleteRequest.
	*/

	// 设置IRP为操作完成
	IoCompleteRequest(pIrp, IO_NO_INCREMENT);
	return status;
}

void* GetUserBuf(IRP* pIrp)
{
	// Communication method
	void* pBuf = NULL;
	if (pIrp->AssociatedIrp.SystemBuffer)
	{
		pBuf = pIrp->AssociatedIrp.SystemBuffer;
	}
	else if (pIrp->MdlAddress)
	{
		// 需要使用一个函数, 让MDL对象映射出一个新的系统领空的虚拟地址.
		// MmGetSystemAddressForMdlSafe - 获取系统MDL对象映射出来的新的虚拟地址.
		// 这个虚拟地址可以直接修改到用户层的虚拟地址空间.
		pBuf = MmGetSystemAddressForMdlSafe(pIrp->MdlAddress, NormalPagePriority);
	}
	else if (pIrp->UserBuffer)
	{
		pBuf = pIrp->UserBuffer;
	}
	else
	{
		pBuf = NULL;
	}
	
	return pBuf;
}

NTSTATUS OnRead(DEVICE_OBJECT* pDeviceObject, IRP* pIrp)
{
	pDeviceObject;
	NTSTATUS status = STATUS_SUCCESS;
	DbgBreakPoint();

	KdPrint(("OnRead():read from device"));
	char* pBuf = GetUserBuf(pIrp);
	
	if (pBuf)
	{
		// 获取IRP栈
		IO_STACK_LOCATION* pIoStack = IoGetCurrentIrpStackLocation(pIrp);
		// 获取缓冲区长度
		ULONG ulLen = pIoStack->Parameters.Write.Length;
		KdPrint(("%s\nlength:%d", pBuf, ulLen));

		// 设置完成的字节数, 如果没有设置. 并且该缓冲区的
		// 获得方式是缓冲IO的方式, 那么,即使把数据写入到缓冲区
		// 了, 系统也不会将这给缓冲区拷贝到用户层的地址.
		// 因为系统在拷贝时,是 pIrp->IoStatus.Information 
		// 来提供拷贝的字节数的.
		// 同时pIrp->IoStatus.Information 的值, 也决定了
		// 用户层函数ReadFile的第4个参数(lpNumberOfBytesRead)
		// 被输出多少字节.
		pIrp->IoStatus.Information = 10; /*IO实际完成的字节数*/
	}
	pIrp->IoStatus.Status = STATUS_SUCCESS;

	IoCompleteRequest(pIrp, IO_NO_INCREMENT);
	return status;
}

NTSTATUS OnWrite(DEVICE_OBJECT* pDeviceObject, IRP* pIrp)
{
	NTSTATUS status = STATUS_SUCCESS;
	DbgBreakPoint();

	char* pBuf = GetUserBuf(pIrp);
	if (pBuf)
	{
		// 将数据写入到缓冲区(用户层就能够接收到数据了)
		//RtlCopyMemory(pBuf, "str from R0", 22);

		// 获取IRP栈
		IO_STACK_LOCATION* pIoStack = IoGetCurrentIrpStackLocation(pIrp);
		// 获取缓冲区长度
		ULONG ulLen = pIoStack->Parameters.Write.Length;

		if (ulLen < 22)
		{
			ulLen = 22;
			status = STATUS_BUFFER_TOO_SMALL;
		}
		else
		{
			__asm {
				push eax;
				mov eax, dword ptr[pBuf];
				sgdt[eax];	 // 读取GDT表地址和大小.
				pop eax;
			}
		}


		pIrp->IoStatus.Status = status;
		pIrp->IoStatus.Information = ulLen;
	}

	IoCompleteRequest(pIrp, IO_NO_INCREMENT);
	return status;
}

NTSTATUS OnClose(DEVICE_OBJECT* pDevice, IRP* pIrp)
{
	NTSTATUS status = STATUS_SUCCESS;
	KdPrint(("OnClose\n"));

	

	pIrp->IoStatus.Status = STATUS_SUCCESS;
	pIrp->IoStatus.Information = 0;
	IoCompleteRequest(pIrp, IO_NO_INCREMENT);
	return status;
}

0.2 R3

  1. 通过CreateFile打开设备
  2. 通过ReadFile和WriteFile发起IRP请求.

最好静态编译后再放进虚拟机。

#include <stdio.h>
#include <windows.h>

int main()
{
	HANDLE hDev = CreateFileW(
		L"\\\\.\\mysymbol",
		GENERIC_READ | GENERIC_WRITE,
		FILE_SHARE_READ,
		NULL,
		OPEN_EXISTING,
		FILE_ATTRIBUTE_NORMAL,
		NULL);
	if (INVALID_HANDLE_VALUE == hDev)
	{
		LPVOID lpMsgBuf;
		FormatMessage(
			FORMAT_MESSAGE_ALLOCATE_BUFFER |
			FORMAT_MESSAGE_FROM_SYSTEM |
			FORMAT_MESSAGE_IGNORE_INSERTS,
			NULL,
			GetLastError(),
			0, // Default language
			(LPTSTR)&lpMsgBuf,
			0,
			NULL
		);

		MessageBox(NULL, (LPCTSTR)lpMsgBuf, L"Error", MB_OK | MB_ICONINFORMATION);
		// Free the buffer.
		LocalFree(lpMsgBuf);
		system("pause");
		return 0;
	}

	char wrBuf[] = "str from R3";
	DWORD dwSize = 0;
	WriteFile(hDev, wrBuf, sizeof(wrBuf), &dwSize, NULL);

	char rdBuf[100] = { 0 };
	// GDTR: 48bit
	if (0 == ReadFile(hDev, rdBuf, 6, &dwSize, NULL))
	{
		printf("ReadFile() error. dwSize = %d\n", dwSize);
	}

	printf("GDT==0x%08X, sizse==0x%04X\n",
		*(unsigned int*)(rdBuf + 2),
		*(unsigned short*)rdBuf);


	system("pause");
	return 0;
}

1. 高级内核通讯

上面的通讯方式有一个缺点是:只能读/写,不能传递额外的命令,例如,想让驱动读取整个GDT表的内容,想让驱动遍历所有驱动对象,这些命令都无法传达。此时,通过DeviceIoControl就可以办到。

通过RreadFile和WriteFile进行的通讯方式是单向的,不能同时既传入数据,又获取数据。

DeviceIoControl()就能够做到双向通讯,在传入数据的同时也能接收数据,而且还能配置每次通讯时的缓冲区使用方式, 和每次通讯时的控制码。

驱动对象对应的派遣函数是IRP_MJ_DEVICE_CONTROL,在内核层的派遣函数中,通过IO_STACK_COMPLATE.Parameters.DeviceIoControl.IoControlCode来得到用户层传入进来的控制码。

BOOL
WINAPI
DeviceIoControl(
    _In_ HANDLE hDevice, /*通过CreateFile打开的设备对象句柄*/
    _In_ DWORD dwIoControlCode, /*控制码,重要*/
    _In_reads_bytes_opt_(nInBufferSize) LPVOID lpInBuffer, /*输入给驱动处理的换城
    _In_ DWORD nInBufferSize,/*缓冲区的字节数*/
    _Out_writes_bytes_to_opt_(nOutBufferSize,*lpBytesReturned) LPVOID lpOutBuf
    _In_ DWORD nOutBufferSize, /*缓冲区字节数*/
    _Out_opt_ LPDWORD lpBytesReturned, /*驱动实际要输出的字节数*/
    _Inout_opt_ LPOVERLAPPED lpOverlapped
);

1.0 dwIoControlCode

这个参数在内核层的派遣函数中通过IO_STACK_COMPLATION.Parameters.DeviceIoControl.IoControlCode获取到.

dwIoControlCode参数结构:

    31          | 30 -  16      |       15-14     |     13 |    12 - 2    |     1-0      |
common()ioctl   |  Device Type  | Required Access | Custom | FunctionCode | TransferType |
  • DeviceType一般为FILE_DEVICE_UNKNOWN
  • FunctionCode,0-0x800已经定义,,可以使用0x800-0xFFF的值(类似WM_USER),一般使用这个值最为一个功能代码传递给驱动,例如,传递0x801可用于表示获取GDT;
  • RequiredAccess:FILE_ANY_ACCESS, FILE_READ_ACCESS, FILE_WRITE_ACCESS
  • TransferType,非常的重要,它决定了用户层的输入输出缓冲区和内核层的输入输出缓冲区采用何种方式传输。它可以是以下的值:
    1. METHOD_BUFFERED,对I/O进行缓冲,将输入和输出的数据通过内核
      层空间拷贝
    2. METHOD_IN_DIRECT,输入不缓冲
    3. METHOD_OUT_DIRECT,输出不缓冲
    4. METHOD_NEITHER,都不缓冲

无论传递的是哪种方式,在内核层中都可以通过irp‐>AssociatedIrp.SystemBuffer来获取到输入缓冲区。

而且,关于TransferType,MSDN有以下说明:

This field is ignored by Windows Embedded Compact. You should always use the METHOD_BUFFERED value unless compatibility with Windows-based desktop platforms is required using a different Method value.

控制码使用下面的宏来定制:

#define CTL_CODE( DeviceType, Function, Method, Access ) (                 \
    ((DeviceType) << 16) | ((Access) << 14) | ((Function) << 2) | (Method) \
)

1.1 其它参数

lpInBuffer以及lpOutBuffer在内核层的派遣函数中被合并在一起, 根据参数2的IO设备控制代码来决定使用IRP结构中的哪个字段作为输入输出缓冲区,一般将传输方式指定为METHOD_OUT_DIRECTMETHOD_IN_DIRECT时,使用Irp‐>MdlAddress来得到输入输出缓冲区。

nInBufferSize,nOutBufferSize在内核层的派遣函数中,分别通过下面两个获得:

IO_STACK_COMPLATE.Parameters.DeviceIoControl.InputBufferLength
IO_STACK_COMPLATE.Parameters.DeviceIoControl.OutputBufferLength

1.2 R0代码示例

#include <ntddk.h>


#define NAME_DEVICE	L"\\Device\\mydevice"
#define NAME_SYMBOL	L"\\DosDevices\\mysymbol"


#define MY_CTL_CODE(code) CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800+(code), METHOD_BUFFERED, FILE_ANY_ACCESS)

void OnUnload(DRIVER_OBJECT* pDriver);
void* GetUserBuf(IRP* pIrp, _Out_ void** pBuf);
NTSTATUS OnCreate(DEVICE_OBJECT* pDeviceObject, IRP* pIrp);
NTSTATUS OnClose(DEVICE_OBJECT* pDevice, IRP* pIrp);
NTSTATUS OnDeviceIoControl(DEVICE_OBJECT *pDevice, IRP* pIrp);

typedef enum _MyCtlCode {
	DEVICE_CTRL_CODE_READ = MY_CTL_CODE(0),
	DEVICE_CTRL_CODE_WRITE = MY_CTL_CODE(1),
}MyCtlCode;

NTSTATUS OnDeviceCtrlRead(DEVICE_OBJECT* pDeviceObject, IRP* pIrp);
NTSTATUS OnDeviceCtrlWrite(DEVICE_OBJECT* pDeviceObject, IRP* pIrp);

typedef struct _DeviceIoCtrlHandler {
	ULONG ulCtrlCode;
	NTSTATUS (*callback)(DEVICE_OBJECT* pDeviceObject, IRP* pIrp);
}DeviceIoCtrlHandler;

DeviceIoCtrlHandler g_DeviceIoCtrlHandlers[] = {
	{DEVICE_CTRL_CODE_READ, OnDeviceCtrlRead},
	{DEVICE_CTRL_CODE_WRITE, OnDeviceCtrlWrite}
};

NTSTATUS DriverEntry(DRIVER_OBJECT* pDriver, UNICODE_STRING* strRegPath)
{
	KdBreakPoint();
	strRegPath;
	NTSTATUS status = STATUS_SUCCESS;
	pDriver->DriverUnload = OnUnload;

	UNICODE_STRING uStrNameDevice = RTL_CONSTANT_STRING(NAME_DEVICE);
	PDEVICE_OBJECT pDevice = NULL;
	status = IoCreateDevice(
		pDriver,
		0,
		&uStrNameDevice,
		FILE_DEVICE_UNKNOWN,
		0,
		FALSE,
		&pDevice);
	if (!NT_SUCCESS(status))
	{
		KdPrint(("CreateDevice() error: 0x%08x\n", status));
		return status;
	}
	pDevice->Flags |= DO_DIRECT_IO;


	UNICODE_STRING uStrNameSymbol = RTL_CONSTANT_STRING(NAME_SYMBOL);
	status = IoCreateSymbolicLink(
		&uStrNameSymbol,
		&uStrNameDevice);
	if (!NT_SUCCESS(status))
	{
		KdPrint(("IoCreateSymbolicLink() error: 0x%08x\n", status));
		return status;
	}
	pDriver->MajorFunction[IRP_MJ_CREATE] = OnCreate;
	pDriver->MajorFunction[IRP_MJ_CLOSE] = OnClose;
	pDriver->MajorFunction[IRP_MJ_DEVICE_CONTROL] = OnDeviceIoControl;


	return status;

}


void OnUnload(DRIVER_OBJECT* pDriver)
{
	pDriver->DeviceObject;/*设备对象链表*/
	// 删除符号链接
	UNICODE_STRING symbolName = RTL_CONSTANT_STRING(NAME_SYMBOL);
	IoDeleteSymbolicLink(&symbolName);
	// 销毁设备对象.
	IoDeleteDevice(pDriver->DeviceObject);
}


NTSTATUS OnCreate(DEVICE_OBJECT* pDeviceObject, IRP* pIrp)
{
	pDeviceObject;
	NTSTATUS status = STATUS_SUCCESS;
	DbgBreakPoint();

	KdPrint(("OnCreate(): Device is being created\n"));
	// 设置R3 CreateFile执行结果
	pIrp->IoStatus.Status = STATUS_SUCCESS;
	pIrp->IoStatus.Information = 0; // 完成字节数
	/*
	IoStatus
			Contains the IO_STATUS_BLOCK structure in which a driver stores status
		and information before calling IoCompleteRequest.
	*/

	// 设置IRP为操作完成
	IoCompleteRequest(pIrp, IO_NO_INCREMENT);
	return status;
}

void* GetUserBuf(IRP* pIrp, _Out_ void **ppBuf)
{
	IO_STACK_LOCATION* pStack = IoGetCurrentIrpStackLocation(pIrp);
	ULONG ulDeviceCtrlCode = pStack->Parameters.DeviceIoControl.IoControlCode;

	if (pIrp->MdlAddress
		&& (METHOD_FROM_CTL_CODE(ulDeviceCtrlCode) & METHOD_OUT_DIRECT))
	{
		*ppBuf = MmGetSystemAddressForMdlSafe(pIrp->MdlAddress, 0);
	}
	else if (pIrp->AssociatedIrp.SystemBuffer)
	{
		*ppBuf = pIrp->AssociatedIrp.SystemBuffer;
	}
	else
	{
		*ppBuf = NULL;
		KdPrint(("[WARNING]pBuf == NULL"));
	}
	
}

NTSTATUS OnClose(DEVICE_OBJECT* pDevice, IRP* pIrp)
{
	NTSTATUS status = STATUS_SUCCESS;
	pDevice;
	KdPrint(("OnClose\n"));

	pIrp->IoStatus.Status = STATUS_SUCCESS;
	pIrp->IoStatus.Information = 0;
	IoCompleteRequest(pIrp, IO_NO_INCREMENT);
	return status;
}


NTSTATUS OnDeviceIoControl(DEVICE_OBJECT* pDevice, IRP* pIrp)
{
	NTSTATUS status = STATUS_SUCCESS;

	// 通过IRP栈获取用户层参数
	IO_STACK_LOCATION* pStack = IoGetCurrentIrpStackLocation(pIrp);
	ULONG ulDeviceCtrlCode = pStack->Parameters.DeviceIoControl.IoControlCode;
	ULONG ulInBufLen = pStack->Parameters.DeviceIoControl.InputBufferLength;
	ULONG ulOutBufLen = pStack->Parameters.DeviceIoControl.OutputBufferLength;

	KdPrint(("控制码:%08X 输入长度:%d 输出长度:%d\n",
		ulDeviceCtrlCode,
		ulInBufLen,
		ulOutBufLen));

	size_t nCode = sizeof(g_DeviceIoCtrlHandlers) / sizeof(g_DeviceIoCtrlHandlers[0]);
	for (int i = 0; i < nCode; ++i) {
		if (g_DeviceIoCtrlHandlers[i].ulCtrlCode == ulDeviceCtrlCode) {
			status = g_DeviceIoCtrlHandlers[i].callback(pDevice, pIrp);
		}
	}

	/*switch (ulDeviceCtrlCode)
	{
	case DEVICE_CTRL_CODE_READ:
	{
		break;
	}
	case DEVICE_CTRL_CODE_WRITE:
	{

		break;
	}
	default:
		break;
	}*/

	//pIrp->IoStatus.Status = status;
	//pIrp->IoStatus.Information = 0;
	IoCompleteRequest(pIrp, IO_NO_INCREMENT);
	return status;
}

NTSTATUS OnDeviceCtrlRead(DEVICE_OBJECT* pDeviceObject, IRP* pIrp)
{
	pDeviceObject;
	NTSTATUS status = STATUS_SUCCESS;
	KdPrint(("[R0]OnDeviceCtrlRead()...\n"));

	KdBreakPoint();
	void* pBuf = NULL;
	GetUserBuf(pIrp, &pBuf);


	RtlCopyMemory(pBuf, "aaaaaaaaa", 10);
	// 设置输出的字节数.
	pIrp->IoStatus.Information = 10;
	pIrp->IoStatus.Status = status;

	return status;
}
NTSTATUS OnDeviceCtrlWrite(DEVICE_OBJECT* pDeviceObject, IRP* pIrp)
{
	pDeviceObject; pIrp;
	NTSTATUS status = STATUS_SUCCESS;
	KdPrint(("[R0]OnDeviceCtrlWrite()...\n"));

	void* pBuf = NULL;
	GetUserBuf(pIrp, &pBuf);
	return status;
}

1.3 R3代码示例

#include <stdio.h>
#include <windows.h>


#define MY_CTL_CODE(code) CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800+(code), METHOD_BUFFERED, FILE_ANY_ACCESS)
typedef enum _MyCtlCode {
	DEVICE_CTRL_CODE_READ = MY_CTL_CODE(0),
	DEVICE_CTRL_CODE_WRITE = MY_CTL_CODE(1),
}MyCtlCode;

int main()
{
	HANDLE hDev = CreateFileW(
		L"\\\\.\\mysymbol",
		GENERIC_READ | GENERIC_WRITE,
		FILE_SHARE_READ,
		NULL,
		OPEN_EXISTING,
		FILE_ATTRIBUTE_NORMAL,
		NULL);
	if (INVALID_HANDLE_VALUE == hDev)
	{
		LPVOID lpMsgBuf;
		FormatMessage(
			FORMAT_MESSAGE_ALLOCATE_BUFFER |
			FORMAT_MESSAGE_FROM_SYSTEM |
			FORMAT_MESSAGE_IGNORE_INSERTS,
			NULL,
			GetLastError(),
			0, // Default language
			(LPTSTR)&lpMsgBuf,
			0,
			NULL
		);

		MessageBox(NULL, (LPCTSTR)lpMsgBuf, L"Error", MB_OK | MB_ICONINFORMATION);
		// Free the buffer.
		LocalFree(lpMsgBuf);
		system("pause");
		return 0;
	}

	char buf[100] = { 0 };
	DWORD dwSize = 0;
	DWORD dwPID = 1234;
	DeviceIoControl(hDev,
		DEVICE_CTRL_CODE_READ,
		&dwPID,
		sizeof(dwPID),
		buf,
		100,
		&dwSize,
		NULL);
	printf("DeviceIoControl() finished\n size: %d\n buf:%s\n", dwSize, buf);

	CloseHandle(hDev);
	system("pause");
	return 0;
}

2. 小结

R3:

  1. 要将R3数据传递给R0时,使用WriteFile
  2. 想要从R0获取数据,在R3调用ReadFile

R0:

  • 从R3传递过来的参数保存在IRP, IO_STACK_COMPLATE中,并且有3种缓冲区;
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值