滴水逆向三期15611 进程通信 项目练习

概要

最终效果

  1. 创建进程通信
    在这里插入图片描述

  2. 通信测试
    在这里插入图片描述

  3. IATHOOK 剪切板内容在这里插入图片描述

  4. inlineHook 创建进程
    在这里插入图片描述

文章分如下几部分

  1. 步骤
  2. 进程通信
  3. dll注入
  4. 问题总结
    本篇文章不是一个实现过程, 而是一个关于这个项目的一个总结, 包含了比较多外链, 帮助更好的理解.

步骤

  1. 实现进程通信
  2. 无模块注入dll
  3. hook

进程通信

为什么要进程通信

比较明显的一个作用是, 能够分工工作, 那多线程不是也可以做吗? 但是多线程都在同一个空间, 容易造成异常, 进程隔离后使不同的工作能够不影响, 最典型的例子就是浏览器: Chrome为什么使用多进程, 再给出一个多线程, 多进程的例子, 第二个例子很纠结放不放, 随便看看吧, 依然困惑也没关系(我就是…), 没关系下面的具体应用让你恍然大悟.

如果是这些, 课程为什么这样设置, 海哥为啥让做呢…
如果你对外挂有一点了解, 就知道现在的网络游戏都有驱动防护也就是R0, 内核级. 这就导致了我们没法在R3进行常规的读写申请释放内存(WriteProcessMemory, ReadProcessMemory, Allocate…), 这个时候就需要编写相应的过保护程序(当然是R0层的).
现在假设我们过掉了保护, 可以在R0对游戏进行读写了. 画方框为例(方块透视), 在R0读到人物坐标, R3实现绘制. 换为自瞄就是R0读坐标, R3控制鼠标.(你要非说全在R0实现这些操作, 那无话可说了…)
R0 R3分别对应驱动程序和普通应用程序, 这里就是内核和用户层的进程通信了.
以方块透视来说, 我们希望人物实时的显示在屏幕, 而不是读出了人物坐标, 一两秒才更新, 总是滞后于实际人物位置. 也就引出进程通信最最重要的功能, 速度…
所以才会有这么多通信方式, 当然能搜到的常用方式就不用想着能用在现在的辅助上了.
这里就不过多引申, 毕竟我们只是了解, 实际应用还是靠自己去发掘了.
本文就用练习要求的命名管道.

命名管道

这个基本就是面向api编程, 其中比较难理解的是PIPE_TYPE, 有字节和消息两个类型.
就不拾人牙慧了, 给出两个网址 以及一个demo
代码有点多且没啥意思, 但不加又有违标题哈哈, 希望csdn能支持代码折叠…
点此跳过代码

https://www.cnblogs.com/lizhanzhe/p/10966253.html 命名管道的基本使用
http://blog.sina.com.cn/s/blog_71b3a9690100usem.html 命名管道的延伸, 以及传递方式

demo

Server

class HandleGatewayServer {
public:
	int Init();
	int Gateway();
	BOOL AnswerMsg();

protected:
	BOOL m_clientConnected = FALSE;
	DWORD  m_threadId = 0;
	HANDLE m_pipeHandle = INVALID_HANDLE_VALUE;
	LPTSTR m_lpszPipename = (LPTSTR)TEXT("\\\\.\\pipe\\mynamedpipe");
};

int HandleGatewayServer::Init() 
{
	while (1) {
		m_pipeHandle = CreateNamedPipe(m_lpszPipename, PIPE_ACCESS_DUPLEX, PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT, PIPE_UNLIMITED_INSTANCES, MAXPIPEFILESIZE, MAXPIPEFILESIZE, 0, NULL);
		printf(">>>管道建立成功, 等待客户端.\n");
		if (m_pipeHandle == INVALID_HANDLE_VALUE) {
			printf(">>>>>管道建立失败, LastError: %d\n", (DWORD)GetLastError());
			return -1;
		}

		//等待客户端连接, 如果成功, 返回非零值, 否则GetLastError返回ERROR_PIP_CONNECTED
		m_clientConnected = ConnectNamedPipe(m_pipeHandle, NULL);
		if (m_clientConnected) {
			printf(">>>客户端已连接, 建立网关\n");
			if (HandleGatewayServer::Gateway() == ERROR_BROKEN_PIPE)
				return -1;
		}
		else {
			CloseHandle(m_pipeHandle);
		}
	}

	return 0;
}

int HandleGatewayServer::Gateway() 
{
	HANDLE hHeap = ::GetProcessHeap();
	void* request = ::HeapAlloc(hHeap, 0, MSG);
	if (!request)
		return -1;
	DWORD cbBytesRead = 0, cbReplyBytes = 0, cbWritten = 0;
	BOOL fSuccess = FALSE;

	while (1) { // Loop until done reading
		fSuccess = ReadFile(m_pipeHandle, request, MSG, &cbBytesRead, NULL);

		if (!fSuccess || cbBytesRead == 0) {
			if (GetLastError() == ERROR_BROKEN_PIPE) {
				printf(">>>>>客户端连接失败, LastError: %d\n", (DWORD)GetLastError());
			}
			else {
				printf(">>>>>读管道失败, LastError: %d\n", (DWORD)GetLastError());
			}
			//printf(">>>当前没有数据.\n");
			break;
		}

		DWORD orderID = *(DWORD*)request;

		if (orderID == MSG) {
			HandleGatewayServer::AnswerMsg();
			printf(">>>完成了一次通讯, 消息类型为%d\n", orderID);
		}
		else {
			printf(">>>>>未知命令, Order: %d\n", orderID);
		}
	}

    
	FlushFileBuffers(m_pipeHandle);
	DisconnectNamedPipe(m_pipeHandle);
	CloseHandle(m_pipeHandle);

	HeapFree(hHeap, 0, request);

	printf("网关关闭... ");
	return GetLastError();
}

BOOL HandleGatewayServer::AnswerMsg()
{
	BOOL fSuccess = FALSE;
	DWORD bytesWritten = 0;
	DWORD msg = 1;
	MessageBoxA(NULL, "执行消息成功", "来自客户端的指令", 0);
	fSuccess = WriteFile(m_pipeHandle, &msg, MSG, &bytesWritten, NULL);
	if (!fSuccess)
		printf(">>>>>写管道失败, LastError: %d\n", (DWORD)GetLastError());
	return TRUE;
}

#include "client.h"
int main(void)
{
	HandleGatewayServer handleGatewayServer;
	handleGatewayServer.Init();
}
Client
class HandleGatewayClient {
public:
	int ConnectPipe();
	BOOL DisconnectPipe();
	int SetPipeMode(DWORD mode = { PIPE_READMODE_MESSAGE });


	//简单的printf通信
	bool RequestMsgBox();
	DWORD ReceiveMsgBox();
	DWORD RemoteMsgBox();

protected:
	HANDLE m_pipeHandle = INVALID_HANDLE_VALUE;
	LPTSTR m_lpszPipename = (LPTSTR)TEXT("\\\\.\\pipe\\mynamedpipe");
};

int HandleGatewayClient::ConnectPipe() {
	while (1) {
		m_pipeHandle = CreateFile(m_lpszPipename, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);

		if (m_pipeHandle != INVALID_HANDLE_VALUE)
			break; 

		if (GetLastError() != ERROR_PIPE_BUSY) { 
			printf(">>>>>管道打开失败, LastError: %d\n", (DWORD)GetLastError());
			return -1;
		}


		if (!WaitNamedPipe(m_lpszPipename, PIPEWAITTIMOUTIFBUSY)) {
			printf(">>>>>等待超时, LastError: %d\n", (DWORD)GetLastError());
			return -1;
		}
	}

	printf(">>>管道已连接.\n");

	HandleGatewayClient::SetPipeMode(PIPE_READMODE_MESSAGE);

	return 0;
}

int HandleGatewayClient::SetPipeMode(DWORD mode) {
	BOOL fSuccess = FALSE;
	fSuccess = SetNamedPipeHandleState(m_pipeHandle, &mode, NULL, NULL);
	if (!fSuccess) {
		printf(">>>>>设置管道属性失败, LastError: %d\n", (DWORD)GetLastError());
		return -1;
	}

	printf("管道属性已改变.\n");
	return 0;
}

BOOL HandleGatewayClient::DisconnectPipe() {
	return CloseHandle(m_pipeHandle);
}

bool HandleGatewayClient::RequestMsgBox()
{
	BOOL fSuccess = FALSE;
	DWORD bytesWritten = 0;
	DWORD msg = 4;
	fSuccess = WriteFile(m_pipeHandle, &msg, MSG, &bytesWritten, NULL);

	if (!fSuccess) {
		printf(">>>>>向管道写失败, LastError: %d\n", (DWORD)GetLastError());
		return false;
	}

	printf(">>>发送了%d个字节", bytesWritten);

	return true;
}

DWORD HandleGatewayClient::ReceiveMsgBox()
{
	DWORD response;
	BOOL fSuccess = FALSE;
	DWORD bytesRead = 0;

	do { // Read from the pipe.
		fSuccess = ReadFile(m_pipeHandle, &response, MSG, &bytesRead, NULL);

		if (!fSuccess && GetLastError() != ERROR_MORE_DATA)
			break;

	} while (!fSuccess);  // repeat loop if ERROR_MORE_DATA 

	if (!fSuccess)
		printf(">>>>>读管道失败, LastError: %d\n", (DWORD)GetLastError());

	return response;
}

DWORD HandleGatewayClient::RemoteMsgBox()
{
	DWORD response = 0;
	if (HandleGatewayClient::RequestMsgBox()) {
		response = HandleGatewayClient::ReceiveMsgBox();
	}
	return response;
}
#include "client.h"
using namespace std;

int main() {
	HandleGatewayClient gatewayClient;
	gatewayClient.ConnectPipe();

	int orderUser = 0;
	do {
		printf("输入请求:\n");
		cin >> dec >> orderUser;
		cout << endl;

		if (orderUser == MSG)
		{
			gatewayClient.RemoteMsgBox();
		}
		system("cls");
	} while (orderUser != 9);

	gatewayClient.DisconnectPipe();

	system("pause");
	return EXIT_SUCCESS;
}

dll 无模块注入

第一个想法是向贴exe一样, 直接把dll整个贴过去然后运行, 随之而来的问题是通过远程线程启动什么?
这个问题困扰了挺久, 最后想到以shellcode为启动方式.

为什么必须是dll

一开始准备使用之前的内存注入, 直接贴整个exe, 但发现一个致命问题: 当在别的进程空间的时候没法创建命名管道…LastError总返回2, 那么既然有一个问题, 就可能有更多未知的问题, 放弃这种方式采用dll方式

shellcode 注入

内存注入里, 就提及了shellcode注入这种方式.

BYTE shellCode[25] = {
		0x68, 0x11, 0x11, 0x11, 0x11,        		//push eip
		0x9C,                                       //pushfd
		0x60,                                       //pushad
		0x6A, 0x00,                                 //push 0
		0x6A, 0x00,                                 //push 0
		0x6A, 0x00,                                 //push 0
		0x6A, 0x00,                                 //push 0
		0xB8, 0x11, 0x32, 0x56, 0x78,		        //mov eax, MessageBox
		0xFF, 0xD0,                                 //call eax
		0x61,                                       //popad
		0x9D,                                       //popfd
		0xC3,                                       //ret
};

通过暂停主线程再将EIP设置为shellcode主地址, 就能够实现执行一个MessageBox.
那么启动一个dll的shellcode呢, 该怎么获取, 写完之后调试从内存里dump出来再面临着各种小bug…试了挺久, 觉着不能死磕了, 太影响效率, 做不出就是做不出. 一个个的疑问只能找写好的范例, 所幸在github找到个相当完备的无模块注入, 并支持x86下注入x64.

PE章节海哥让我们做过一个pe文件shellcode注入, 通过修改EntryPoint到自己的代码再跳转回来, 那时候只是个简单的MessageBox. 如果掌握了下面作者对shellcode的编写方法, 那我们能做的就不简单单是一小段代码了…当然这只是一个用法
这个等弄懂了原理再写一篇专门的shellcode编写方法吧, 现在毕竟只能照葫芦画瓢.
以下内容皆为对源代码的解读

https://github.com/UserExistsError/DllLoaderShellcode 生成shellcode
https://github.com/UserExistsError/InjectDll 注入例程

模块注入

实现dll手动加载, 需要修复重定位, 修复IAT, 这些我们都在之前的内存注入里实现过, 就不再多讲, 这一部分更多的是细节内容.
整个过程大致为 shellcode(修复重定位, IAT, 启动dll)在内存中, shellcode后面紧跟着就是dll(也就是最后他们会合并为一个文件)

shellcode 部分

这部分讲了shellcode的实现方式, 如果想直击重点, 不感兴趣可以跳过
目录结构
在这里插入图片描述
TestDLL, TestExe我们不讨论, 只是测试范例, 重要的是前两个.

Extract

这是个生成shellcode的程序, 使用方式Extract.exe demo.exe demo.bin

但是作者后面使用了Build Event来实现编译后自动调用这个程序生成shellcode
在这里插入图片描述在这里插入图片描述Build完之后调用Extract.exe Loader.exe Loder.bin ,这么长一串是适配各种情况…debug还是Release, x86还是x64

代码

在这里插入图片描述用图片感觉说起来比较直观, 直接保存.text段. 这里就有个大大的疑问, 难道整个文件里没有其它干扰代码吗? 怎么保证.text就只有我们自己需要的代码于是引出下面的Loader配置

Loader

分两个部分来说

  1. 属性配置

  2. 代码

属性配置

由于要注意的地方比较多, 这里就图片列出几个最重要的, 这个部分涉及了大量编译原理的知识(学无止境…).不能误人子弟只说说作用, 原理放出链接自己理解吧.

  1. 函数级链接(Function-level Linking)
    在这里插入图片描述
    MSDN 函数级链接
    被包装函数 COMDATA

  2. 函数顺序(Function Order)在这里插入图片描述1 2是相关联的属性, 缺一不可, 他们就保证了整个代码段只会生成定义的这个函数, 无其它相关
    在这里插入图片描述

  3. EntryPoint
    在这里插入图片描述这个属性也是第一次接触到可以这么用, 设置为自己的函数之后就不会再报没有main入口点的错误

  4. 杂项
    还有几个非决定性因素(我自己实验过了), 但是为了不发生奇奇怪怪的错误, 把一些检查相关给关掉(所以用vc6只用设置前三个就行了), 图太多影响阅读 只把路径给出来, 可能翻译不准确见谅…
    c/c++ --> 优化–> 优化–> /O2
    c/c++ --> 代码生成 --> 安全检查 --> 禁用
    c/c++ --> 语言 --> 符合模式 --> 禁用 (这个属性关于字符串处理)
    链接器 --> 启用增量链接 --> 不启用(这个在release自动禁用, 且开启12后这里也失效)
    链接器 --> 高级 --> 数据执行保护(dep) --> 禁用

代码

shellcode应该不依赖于当前环境, 所以不能用到任何api, 除非你修复了他们.
这个一定时时刻刻记住.
说是代码, 但贴代码毕竟不是自己写的, 还不如大家自己去观摩.
只讲其中的细节.

  1. 获取EIP.
#pragma intrinsic( _ReturnAddress )
__declspec(noinline) ULONG_PTR caller( VOID ) { return (ULONG_PTR)_ReturnAddress(); }

调用_ReturnAddress()函数会返回[ebp+4]即上层EIP地址, 由于最后写入内存的是shellcode后面紧跟着dll, 那么获得dll地址就是如下代码了
在这里插入图片描述

  1. 从PEB(这个内核课程有讲, 但有了基础是能够看懂做什么的)中获取目标程序的kernel地址, 从而获取需要用的函数地址. 这里的一个问题就是获取这些内容的时候需要比对dll名字和函数名字, 不可避免的要strcpy . 但是使用api又是禁忌, 这也是为什么要用PEB获取, 所以只能手写. 但作者使用比较名字的hash值来判断是否找到了对应的dll/函数.
    在这里插入图片描述整个流程和exe内存注入一样, 但因为dll不属于自己控制, 我们只能用shellcode方式完成, 而又局限于不能使用api, 需要注意的地方就比较多.
    最后进入dll为
    在这里插入图片描述这里进行了清楚指令缓存的操作, 经测试删掉后无影响.
    但作者注释必须清除防止执行了重定位之前的代码, 查阅msdn后有如下解释, 所以安全起见, 我们还是加上这一句.

Applications should call FlushInstructionCache if they generate or modify code in memory. The CPU cannot detect the change, and may execute the old code it cached.
程序如果在内存中生成或修改了代码, 那么应当使用FlushInstructionCache. CPU无法检测变化, 可能执行它已经捕获到的旧代码.

小结

cmd内使用
copy /b loader.x86.bin + testdll.dll test.bin
将shellcode和测试dll组合生成最终的二进制文件
(当然注入部分不会这样, 直接将dll在内存里写到shellcode就行, 一样的效果, 这里作者是让我们直观的感受shellcode作用)
再执行
Test.exe test.bin
就可以看到测试dll注入到了自身进程(弹出个MsgBox)
当然以上的命令你都可以写入BuildEvent里面, 让他每次Build都自动执行

注入

这部分没什么需要注意的地方, 就是依次写入shellcode dll然后开一个远程线程地址为shellcode地址即可.

总结

写到这感觉这篇文章完全就是在讲dll无模块注入了, 而且还是照着前辈的代码学习. 不过至少学会怎么编shellcode了不是吗…而且到目前自己写的都是x86, 怎么写出同时适配x86 x64的程序不也是个极好的例子吗, 甚至包括了ARM的.
hook部分就不写了, 对了, hook的CreateProcess涉及到程序内部安全问题, 所以想hook这种函数要考虑的东西就比较多了, 我只是很简单的打开了另外一个程序, 其它的根本没考虑. 这种东西, 还是要到实践中去体会. 而且要求对api熟悉, 就是有一定正向基础. 开始讲废话…

这是自己写的, x86Build后运行Client.exe即可, 哦要打开某云
在这里插入图片描述

https://github.com/UserExistsError/DllLoaderShellcode 生成shellcode
https://github.com/UserExistsError/InjectDll 注入例程
https://github.com/saiveen1/dishui15611 项目练习

第1讲:2015-01-12(进制01) 第2讲:2015-01-13(进制02) 第3讲:2015-01-14(数据宽度-逻辑运算03) 第4讲:2015-01-15(通用寄存器-内存读写04) 第5讲:2015-01-16(内存寻址-堆栈05) 第6讲:2015-01-19(EFLAGS寄存器06) 第7讲:2015-01-20(JCC) 第8讲:2015-01-21(堆栈图) 第8讲:2015-01-21(宝马问题) 第9讲:2015-01-22(堆栈图2) 第10讲:2015-01-23(C语言01_后半段) 第10讲:2015-01-23(C语言完整版) 第11讲:2015-01-26(C语言02_数据类型) 第12讲:2015-01-27(C语言03_数据类型_IF语句) 第13讲:2015-01-28(C语言04_IF语句逆向分析上) 第14讲:2015-01-28(C语言04_IF语句逆向分析下) 第15讲:2015-01-29(C语言04_正向基础) 第16讲:2015-01-30(C语言05_循环语句) 第17讲:2015-02-02(C语言06_参数_返回值_局部变量_数组反汇编) 第18讲:2015-02-02(2015-01-30课后练习) 第19讲:2015-02-03(C语言07_多维数组) 第20讲:2015-02-03(2015-02-02课后练习) 第21讲:2015-02-04(C语言08_结构体) 第22讲:2015-02-05(C语言09_字节对齐_结构体数组) 第23讲:2015-02-06(C语言10_Switch语句反汇编) 第24讲:2015-02-26(C语言11_指针1) 第25讲:2015-02-27(C语言11_指针2) 第26讲:2015-02-28(C语言11_指针3) 第27讲:2015-02-28(C语言11_指针4) 第28讲:2015-03-02(C语言11_指针5) 第29讲:2015-03-03(C语言11_指针6) 第30讲:2015-03-04(C语言11_指针7) 第31讲:2015-03-06(C语言11_指针8) 第32讲:2015-03-09(位运算) 第33讲:2015-03-10(内存分配_文件读写) 第34讲:2015-03-11(PE头解析_手动) 第35讲:2015-03-12(PE头字段说明) 第36讲:2015-03-13(PE节表) 第37讲:2015-03-16(FileBuffer转ImageBuffer) 第38讲:2015-03-17(代码节空白区添加代码) 第39讲:2015-03-18(任意节空白区添加代码) 第40讲:2015-03-19(新增节添加代码) 第41讲:2015-03-20(扩大节-合并节-数据目录) 第42讲:2015-03-23(静态连接库-动态链接库) 第43讲:2015-03-24(导出表) 第44讲:2015-03-25(重定位表) 第45讲:2015-03-26(移动导出表-重定位表) 第46讲:2015-03-27(IAT表) 第47讲:2015-03-27(导入表) 第48讲:2015-03-30(绑定导入表) 第49讲:2015-03-31(导入表注入) 第50讲:2015-04-01(C++ this指针 类 上) 第51讲:2015-04-01(C++ this指针 类 下) 第52讲:2015-04-02(C++ 构造-析构函数 继承) 第53讲:2015-04-03(C++ 权限控制) 第54讲:2015-04-07(C++ 虚函数表) 第55讲:2015-04-08(C++ 动态绑定-多态-上) 第56讲:2015-04-08(C++ 动态绑定-多态-下) 第57讲:2015-04-09(C++ 模版) 第58讲:2015-04-10(C++ 引用-友元-运算符重载) 第59讲:2015-04-13(C++ new-delete-Vector) 第60讲:2015-04-14(C++Vector实现) 第61讲:2015-04-15(C++链表) 第62讲:2015-04-16(C++链表实现) 第63讲:2015-04-16(C++二叉树) 第64讲:2015-04-17(C++二叉树实现) 第65讲:2015-04-20(Win32 宽字符) 第66讲:2015-04-21(Win32 事件-消息-消息处理函数) 第67讲:2015-04-22(Win32 ESP寻址-定位回调函数-条件断点) 第68讲:2015-04-23(Win32 子窗口-消息处理函数定位) 第69讲:2015-04-24(Win32 资源文件-消息断点) 第70讲:2015-04-27(Win32 提取图标-修改标题) 第71讲:2015-04-28(Win32 通用控件-VM_NOTIFY) 第72讲:2015-04-29(Win32 PE查看器-项目要求) 项目一:PE查看器 开发周期(5天) 需求文档 第73讲:2015-05-07(Win32 创建线程) 第74讲:2015-05-08(Win32 线程控制_CONTEXT) 第75讲:2015-05-11(Win32 临界区) 第76讲:2015-05-12(Win32 互斥体) 第77讲:2015-05-13(Win32 事件) 第78讲:2015-05-14(Win32 信号量) 第79讲:2015-05-15(Win32 线程同步与线程互斥) 第80讲:2015-05-18(Win32 进程创建_句柄表) 第81讲:2015-05-20(Win32 以挂起形式创建进程) 第82讲:2015-05-21(Win32 加密壳_项目说明) 项目二:加密壳 开发周期(5天) 需求文档 第83讲:2015-05-28(Win32 枚举窗口_鼠标键盘事件) 第84讲:2015-05-29(Win32 CE练习) 第85讲:2015-06-01(Win32 OD练习) 第86讲:2015-06-03(Win32 ShellCode_远程线程注入) 第87讲:2015-06-04(Win32 加载EXE_模块隐藏) 第88讲:2015-06-09(Win32 IAT_HOOK) 第89讲:2015-06-10(Win32 InlineHook) 第90讲:2015-06-11(Win32 进程通信) 第91讲:2015-06-11(Win32 进程监控_项目说明) 项目三:进程监控 开发周期(5天) 需求文档 第92讲:2015-06-15(硬编码_01) 第93讲:2015-06-16(硬编码_02) 第94讲:2015-06-17(硬编码_03) 第95讲:2015-06-18(硬编码_04) 第96讲:2015-06-19(硬编码_05)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

四位

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值