内核级别的内存读写可用于绕过各类驱动保护,从而达到强制读写对端内存的目的,本人闲暇之余封装了一个驱动级的内核读写接口,使用此接口可实现对远程字节,字节集,整数,浮点数,多级偏移读写等。

如下将简单介绍该内核读写工具各类API接口是如何调用的,鉴于驱动读写商业价值较大故暂时不放出源码(后期考虑)。
GitHUB项目地址:https://github.com/lyshark/LyMemory
驱动读写首先要看的就是驱动支持的控制信号,如下是我封装的几个驱动控制器。
#define IOCTL_IO_ReadProcessMemory 0x801 #define IOCTL_IO_WriteProcessMemory 0x802 #define IOCTL_IO_ReadDeviationIntMemory 0x803 #define IOCTL_IO_WriteDeviationIntMemory 0x804 #define IOCTL_IO_ReadProcessMemoryByte 0x805 #define IOCTL_IO_WriteProcessMemoryByte 0x806
#define IOCTL_IO_SetPID CTL_CODE(FILE_DEVICE_UNKNOWN, 0x807, METHOD_BUFFERED, FILE_ANY_ACCESS) #define IOCTL_IO_ReadMemory CTL_CODE(FILE_DEVICE_UNKNOWN, 0x808, METHOD_BUFFERED, FILE_ANY_ACCESS) #define IOCTL_IO_WriteMemory CTL_CODE(FILE_DEVICE_UNKNOWN, 0x809, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define IOCTL_IO_GetModuleAddress CTL_CODE(FILE_DEVICE_UNKNOWN, 0x810, METHOD_BUFFERED, FILE_ANY_ACCESS) #define IOCTL_IO_GetProcessID CTL_CODE(FILE_DEVICE_UNKNOWN, 0x811, METHOD_BUFFERED, FILE_ANY_ACCESS) #define IOCTL_IO_GetSystemRoutineAddr CTL_CODE(FILE_DEVICE_UNKNOWN, 0x812, METHOD_BUFFERED, FILE_ANY_ACCESS) #define IOCTL_IO_CreateAllocMemory CTL_CODE(FILE_DEVICE_UNKNOWN, 0x813, METHOD_BUFFERED, FILE_ANY_ACCESS) #define IOCTL_IO_RemoveAllocMemory CTL_CODE(FILE_DEVICE_UNKNOWN, 0x814, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define IOCTL_IO_ReadDeviationMemory 0x815
|
内核驱动读写类库在2022年9月24日升级了功能,函数列表功能一览。
传统读写函数是每次都会附加到进程中,这种方式效率较低,但也还是可以使用的。
BYTE ReadProcessMemoryByte(DWORD Pid, ULONG64 Address)
BOOL WriteProcessMemoryByte(DWORD Pid, ULONG64 Address, BYTE bytef)
DWORD ReadProcessMemoryInt32(DWORD Pid, ULONG64 Address)
DWORD ReadProcessMemoryInt64(DWORD Pid, ULONG64 Address)
BOOL WriteProcessMemoryInt32(DWORD Pid, ULONG64 Address, DWORD write)
BOOL WriteProcessMemoryInt64(DWORD Pid, ULONG64 Address, DWORD write)
FLOAT ReadProcessMemoryFloat(DWORD Pid, ULONG64 Address)
DOUBLE ReadProcessMemoryDouble(DWORD Pid, ULONG64 Address)
BOOL WriteProcessMemoryFloat(DWORD Pid, ULONG64 Address, FLOAT write)
BOOL WriteProcessMemoryDouble(DWORD Pid, ULONG64 Address, DOUBLE write)
INT32 ReadProcessDeviationInt32(ProcessDeviationIntMemory *read_offset_struct)
INT64 ReadProcessDeviationInt64(ProcessDeviationIntMemory *read_offset_struct)
BOOL WriteProcessDeviationInt32(ProcessDeviationIntMemory *write_offset_struct)
BOOL WriteProcessDeviationInt64(ProcessDeviationIntMemory *write_offset_struct)
DWORD ReadDeviationMemory32(ProcessDeviationMemory *read_offset_struct)
DWORD64 ReadDeviationMemory64(ProcessDeviationMemory *read_offset_struct)
BYTE ReadDeviationByte(ProcessDeviationMemory *read_offset_struct)
FLOAT ReadDeviationFloat(ProcessDeviationMemory *read_offset_struct)
BOOL WriteDeviationByte(ProcessDeviationMemory *write_offset_struct,BYTE write_byte)
BOOL WriteDeviationFloat(ProcessDeviationMemory *write_offset_struct,FLOAT write_float)
|
全局读写函数封装相对于传统驱动读写,虽然也传入PID但本质上可以SetPid
只设置一次PID即可实现后续直接读写内存。
BOOL SetPid(DWORD Pid)
BOOL Read(DWORD pid, ULONG64 address, T* ret)
BOOL Write(DWORD pid, ULONG64 address, T data)
void ReadMemoryDWORD(DWORD pid, ULONG64 addre, DWORD * ret)
void ReadMemoryDWORD64(DWORD pid, ULONG64 addre, DWORD64 * ret)
void ReadMemoryBytes(DWORD pid, ULONG64 addre, BYTE **ret, DWORD sizes)
void ReadMemoryFloat(DWORD pid, ULONG64 addre, float* ret)
void ReadMemoryDouble(DWORD pid, ULONG64 addre, double* ret)
void WriteMemoryBytes(DWORD pid, ULONG64 addre, BYTE * data, DWORD sizes)
void WriteMemoryDWORD(DWORD pid, ULONG64 addre, DWORD ret)
void WriteMemoryDWORD64(DWORD pid, ULONG64 addre, DWORD64 ret)
void WriteMemoryFloat(DWORD pid, ULONG64 addre, float ret)
void WriteMemoryDouble(DWORD pid, ULONG64 addre, double ret)
DWORD64 GetModuleAddress(DWORD pid, std::string dllname)
DWORD GetProcessID(std::string procname)
DWORD64 GetSystemRoutineAddress(std::string funcname)
DWORD64 CreateRemoteMemory(DWORD length)
DWORD DeleteRemoteMemory(DWORD64 address, DWORD length)
|
内核读/写字节集: 对远端指定内存地址出读写字节集数组,该功能可用于强制代码注入等。
#define _CRT_SECURE_NO_WARNINGS #include <iostream> #include <Windows.h> #include <LyMemoryKernel.h>
#pragma comment(lib,"user32.lib") #pragma comment(lib,"advapi32.lib")
int main(int argc, char *argv[]) { cDrvCtrl DriveControl;
DriveControl.InstallAndRun();
BYTE buffer[8] = { 0 }; BYTE* bufferPtr = buffer;
DriveControl.ReadMemoryBytes(2564, 0x7713639c, &bufferPtr, sizeof(buffer));
for (int x = 0; x < 8; x++) { printf("读取字节: 0x%x \n", buffer[x]); }
DriveControl.RemoveAndStop(); system("pause"); return 0; }
|
内核读取字节集效果如下:

与读取对应的一个函数是写入,写入代码如下。
#define _CRT_SECURE_NO_WARNINGS #include <iostream> #include <Windows.h> #include <LyMemoryKernel.h>
#pragma comment(lib,"user32.lib") #pragma comment(lib,"advapi32.lib")
int main(int argc, char *argv[]) { cDrvCtrl DriveControl;
DriveControl.InstallAndRun();
BYTE writebuff[8] = { 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90 }; DriveControl.WriteMemoryBytes(2564, 0x7713639c, writebuff, sizeof(writebuff));
DriveControl.RemoveAndStop(); system("pause"); return 0; }
|
写入后再次查看内存会发现已经变更了。

读写内核数值类型: 数值类型包括了,整数,64位整数,浮点数,双精度浮点等类型。
#define _CRT_SECURE_NO_WARNINGS #include <iostream> #include <Windows.h> #include <LyMemoryKernel.h>
#pragma comment(lib,"user32.lib") #pragma comment(lib,"advapi32.lib")
int main(int argc, char *argv[]) { cDrvCtrl DriveControl;
DriveControl.InstallAndRun();
DWORD data; DWORD64 data64; FLOAT floats; DOUBLE doubles;
DriveControl.ReadMemoryDWORD(2564, 0x771362fc, &data); printf("dword = %d \n", data);
DriveControl.ReadMemoryDWORD64(2564, 0x771362fc, &data64); printf("dword = %d \n", data); printf("dword = %d \n", data+4);
DriveControl.ReadMemoryFloat(2564, 0x771362fc, &floats); printf("float = %f \n", floats);
DriveControl.ReadMemoryDouble(2564, 0x771362fc, &doubles); printf("double = %f \n", doubles);
DriveControl.RemoveAndStop(); system("pause"); return 0; }
|
读数值类型效果:

驱动写数值类型与读取类似,这里给出如何应用的案例。
#define _CRT_SECURE_NO_WARNINGS #include <iostream> #include <Windows.h> #include <LyMemoryKernel.h>
#pragma comment(lib,"user32.lib") #pragma comment(lib,"advapi32.lib")
int main(int argc, char *argv[]) { cDrvCtrl DriveControl;
DriveControl.InstallAndRun();
DWORD data; DWORD64 data64; FLOAT floats; DOUBLE doubles;
DriveControl.WriteMemoryDWORD(2564, 0x771362fc, 100);
DriveControl.WriteMemoryDWORD64(2564, 0x771362fc, 100);
DriveControl.WriteMemoryFloat(2564, 0x771362fc, 10.5);
DriveControl.WriteMemoryDouble(2564, 0x771362fc, 100.5);
DriveControl.RemoveAndStop(); system("pause"); return 0; }
|
结构体版读整数: 传递结构体解析参数读取。
#define _CRT_SECURE_NO_WARNINGS #include <iostream> #include <Windows.h> #include <LyMemoryKernel.h>
#pragma comment(lib,"user32.lib") #pragma comment(lib,"advapi32.lib")
int main(int argc, char *argv[]) { cDrvCtrl DriveControl;
DriveControl.InstallAndRun();
ProcessIntMemory write_struct;
write_struct.pid = 6348; write_struct.address = 0x748c405c; write_struct.bytes_toread = 4; write_struct.data = 999; DriveControl.IoControl(0x802, &write_struct, sizeof(write_struct), 0, 0, 0);
ProcessIntMemory read_struct;
read_struct.pid = 6348; read_struct.address = 0x748c405c; read_struct.bytes_toread = 2; read_struct.data = 0;
DriveControl.IoControl(0x801, &read_struct, sizeof(read_struct), &read_struct, sizeof(read_struct), 0); std::cout << "read: " << (int)read_struct.data << std::endl;
DriveControl.RemoveAndStop(); system("pause"); return 0; }
|
结构版本与类内函数调用方式不同,结构板需要手动调用控制器。

结构版读写字节集: 同理与整数读写一致,需要调用控制器,传入控制信号以及结构体。
#define _CRT_SECURE_NO_WARNINGS #include <iostream> #include <Windows.h> #include <LyMemoryKernel.h>
#pragma comment(lib,"user32.lib") #pragma comment(lib,"advapi32.lib")
int main(int argc, char *argv[]) { cDrvCtrl DriveControl;
DriveControl.InstallAndRun();
ProcessByteMemory write_byte_struct;
write_byte_struct.pid = 6348; write_byte_struct.base_address = 0x76295a04; write_byte_struct.OpCode = { 0x90 };
DriveControl.IoControl(0x806, &write_byte_struct, sizeof(write_byte_struct), 0, 0, 0);
ProcessByteMemory read_byte_struct; BYTE read_byte = 0;
for (int x = 0; x < 10; x++) { read_byte_struct.pid = 6348; read_byte_struct.base_address = 0x76295a04 + x; DriveControl.IoControl(0x805, &read_byte_struct, sizeof(read_byte_struct), &read_byte, sizeof(read_byte), 0); if (read_byte == 0) { break; } printf("0x%02X ", read_byte); }
DriveControl.RemoveAndStop(); system("pause"); return 0; }
|
写入后在读取,效果如下:

结构版多级偏移读写: 针对整数型读写的封装,增加了多级偏移读写机制。读写多级偏移整数型(最大32级)
#define _CRT_SECURE_NO_WARNINGS #include <iostream> #include <Windows.h> #include <LyMemoryKernel.h>
#pragma comment(lib,"user32.lib") #pragma comment(lib,"advapi32.lib")
int main(int argc, char *argv[]) { cDrvCtrl DriveControl; DriveControl.InstallAndRun();
ProcessDeviationIntMemory read_offset_struct;
read_offset_struct.pid = 1468; read_offset_struct.base_address = 0x601660; read_offset_struct.offset_len = 4; read_offset_struct.data = 0; read_offset_struct.offset[0] = 0x18; read_offset_struct.offset[1] = 0x0; read_offset_struct.offset[2] = 0x14; read_offset_struct.offset[3] = 0x0c;
DriveControl.IoControl(0x803, &read_offset_struct, sizeof(read_offset_struct), &read_offset_struct, sizeof(read_offset_struct), 0); std::cout << "read offset: " << read_offset_struct.data << std::endl;
DriveControl.RemoveAndStop(); system("pause"); return 0; }
|
读取结果:

内核读取模块基地址: 内核中强制读取指定进程中模块的基地址。
#define _CRT_SECURE_NO_WARNINGS #include <iostream> #include <Windows.h> #include <LyMemoryKernel.h>
#pragma comment(lib,"user32.lib") #pragma comment(lib,"advapi32.lib")
int main(int argc, char *argv[]) { cDrvCtrl DriveControl; DriveControl.InstallAndRun();
DWORD64 dllbase = DriveControl.GetModuleAddress(952, "user32.dll"); printf("dllbase = 0x%016I64x \n", dllbase);
DriveControl.RemoveAndStop(); system("pause"); return 0; }
|
读取效果如下:

根据进程名得到进程PID: 传入进程名,获取到该进程的PID序号。
#define _CRT_SECURE_NO_WARNINGS #include <iostream> #include <Windows.h> #include <LyMemoryKernel.h>
#pragma comment(lib,"user32.lib") #pragma comment(lib,"advapi32.lib")
int main(int argc, char *argv[]) { cDrvCtrl DriveControl; DriveControl.InstallAndRun();
DWORD pid = DriveControl.GetProcessID("dbgview64.exe"); printf("进程PID: %d \n", pid);
DriveControl.RemoveAndStop(); system("pause"); return 0; }
|
效果如下:

获取系统函数内存地址: 获取SSDT内核函数的内存地址。
#define _CRT_SECURE_NO_WARNINGS #include <iostream> #include <Windows.h> #include <LyMemoryKernel.h>
#pragma comment(lib,"user32.lib") #pragma comment(lib,"advapi32.lib")
int main(int argc, char *argv[]) { cDrvCtrl DriveControl; DriveControl.InstallAndRun();
DWORD64 addr = DriveControl.GetSystemRoutineAddress("NtReadFile"); printf("模块地址: 0x%016I64x \n", addr);
DriveControl.RemoveAndStop(); system("pause"); return 0; }
|
效果如下:

开辟释放堆空间: 在对端内存中开辟,或者释放堆空间,带有读写执行属性。
#define _CRT_SECURE_NO_WARNINGS #include <iostream> #include <Windows.h> #include <LyMemoryKernel.h>
#pragma comment(lib,"user32.lib") #pragma comment(lib,"advapi32.lib")
int main(int argc, char *argv[]) { cDrvCtrl DriveControl; DriveControl.InstallAndRun();
DriveControl.SetPid(952);
DWORD64 ref = DriveControl.CreateRemoteMemory(1024); printf("create = %x \n", ref);
DWORD del_flag = DriveControl.DeleteRemoteMemory(ref, 1024); printf("del flag = %d \n", del_flag);
DriveControl.RemoveAndStop(); system("pause"); return 0; }
|
效果如下:

驱动级DLL注入: 注入DLL功能是驱动注入的一个功能,同样使用该控制器控制。
#define _CRT_SECURE_NO_WARNINGS #include <iostream> #include <Windows.h> #include <LyMemoryKernel.h>
#pragma comment(lib,"user32.lib") #pragma comment(lib,"advapi32.lib")
int main(int argc, char *argv[]) { cDrvCtrl DriveControl; DriveControl.InstallAndRun();
DriveControl.InjectDll("dbgview64.exe", "c://test1.dll");
DriveControl.RemoveAndStop(); system("pause"); return 0; }
|
效果如下:

传统模式读写封装: 传统模式读写封装函数可对整数,浮点数,字节进行灵活读写。
#define _CRT_SECURE_NO_WARNINGS #include <iostream> #include <Windows.h> #include <LyMemoryKernel.h>
int main(int argc, char *argv[]) { cDrvCtrl DriveControl; DriveControl.InstallAndRun();
DWORD ref = DriveControl.ReadProcessMemoryInt32(6056, 0x003AF4CC); printf("驱动读取:value = %d \n", ref);
DWORD64 dref = DriveControl.ReadProcessMemoryInt64(6056, 0x003AF4CC); printf("驱动读取:value64 = %d \n", dref);
FLOAT float_ref = DriveControl.ReadProcessMemoryFloat(6056, 0x01A1BC90); printf("驱动读取:value = %f \n", float_ref);
FLOAT double_ref = DriveControl.ReadProcessMemoryDouble(6056, 0x01A1BC90); printf("驱动读取:value = %f \n", double_ref);
BYTE byf = DriveControl.ReadProcessMemoryByte(6056, 0x01A1BC90); printf("驱动读取:value = %x \n", byf);
for (size_t i = 0; i < 10; i++) { BYTE byf1 = DriveControl.ReadProcessMemoryByte(6056, 0x01A1BC90 + i); printf("驱动读取:value = %x \n", byf1); }
system("pause"); return 0; }
|
读取效果如下:

写入功能与读取一致,这里以读写整数为案例。
#define _CRT_SECURE_NO_WARNINGS #include <iostream> #include <Windows.h> #include <LyMemoryKernel.h>
int main(int argc, char *argv[]) { cDrvCtrl DriveControl; DriveControl.InstallAndRun();
DriveControl.WriteProcessMemoryInt32(6056, 0x003AF4CC, 9999);
DWORD ref = DriveControl.ReadProcessMemoryInt32(6056, 0x003AF4CC); printf("驱动读取:value = %d \n", ref);
system("pause"); return 0; }
|
写出效果如下:

内存多级偏移读写: 此功能并不是读写偏移中的数据,而是通过基地址计算出动态地址的一个函数,后续的读写可以自定义操作。
#define _CRT_SECURE_NO_WARNINGS #include <iostream> #include <Windows.h> #include <LyMemoryKernel.h>
int main(int argc, char *argv[]) { cDrvCtrl DriveControl; DriveControl.InstallAndRun();
ProcessDeviationMemory read_offset_struct = { 0 };
read_offset_struct.pid = 3124; read_offset_struct.base_address = 0x6566e0; read_offset_struct.offset_len = 4; read_offset_struct.data = 0; read_offset_struct.offset[0] = 0x18; read_offset_struct.offset[1] = 0x0; read_offset_struct.offset[2] = 0x14; read_offset_struct.offset[3] = 0x0c;
DWORD ref = DriveControl.ReadDeviationMemory32(&read_offset_struct);
printf("计算出基地址:0x%x \n", ref);
system("pause"); return 0; }
|
定位内存地址如下:

内存整数多级偏移读写: 一个简单的案例实现对内存整数型偏移读写。
#define _CRT_SECURE_NO_WARNINGS #include <iostream> #include <Windows.h> #include <LyMemoryKernel.h>
int main(int argc, char *argv[]) { cDrvCtrl DriveControl; DriveControl.InstallAndRun();
ProcessDeviationIntMemory write_offset_struct = { 0 };
write_offset_struct.pid = 3124; write_offset_struct.base_address = 0x6566e0; write_offset_struct.offset_len = 4; write_offset_struct.data = 999; write_offset_struct.offset[0] = 0x18; write_offset_struct.offset[1] = 0x0; write_offset_struct.offset[2] = 0x14; write_offset_struct.offset[3] = 0x0c;
DriveControl.WriteProcessDeviationInt32(&write_offset_struct);
ProcessDeviationIntMemory read_offset_struct = { 0 };
read_offset_struct.pid = 3124; read_offset_struct.base_address = 0x6566e0; read_offset_struct.offset_len = 4; read_offset_struct.data = 0; read_offset_struct.offset[0] = 0x18; read_offset_struct.offset[1] = 0x0; read_offset_struct.offset[2] = 0x14; read_offset_struct.offset[3] = 0x0c;
DWORD ref = DriveControl.ReadProcessDeviationInt32(&read_offset_struct);
printf("当前偏移内的数据:%d \n", ref);
system("pause"); return 0; }
|
读写效果如下:

读取多级偏移字节型: 读取偏移数据内的字节数据,可循环多次读写。
#define _CRT_SECURE_NO_WARNINGS #include <iostream> #include <Windows.h> #include <LyMemoryKernel.h>
int main(int argc, char *argv[]) { cDrvCtrl DriveControl; DriveControl.InstallAndRun();
ProcessDeviationMemory read_offset_struct = { 0 };
read_offset_struct.pid = 3124; read_offset_struct.base_address = 0x6566e0; read_offset_struct.offset_len = 4; read_offset_struct.data = 0; read_offset_struct.offset[0] = 0x18; read_offset_struct.offset[1] = 0x0; read_offset_struct.offset[2] = 0x14; read_offset_struct.offset[3] = 0x0c;
DWORD ref = DriveControl.ReadDeviationByte(&read_offset_struct); printf("%x \n", ref);
for (size_t i = 0; i < 10; i++) { read_offset_struct.pid = 3124; read_offset_struct.base_address = 0x6566e0 + i; read_offset_struct.offset_len = 4; read_offset_struct.data = 0; read_offset_struct.offset[0] = 0x18; read_offset_struct.offset[1] = 0x0; read_offset_struct.offset[2] = 0x14; read_offset_struct.offset[3] = 0x0c;
DWORD ref = DriveControl.ReadDeviationByte(&read_offset_struct); printf("%x ", ref);
}
system("pause"); return 0; }
|
读取效果如下:

写入多级偏移字节型: 如读取一致,传入偏移,以及写出的字节即可替代目标字节。
#define _CRT_SECURE_NO_WARNINGS #include <iostream> #include <Windows.h> #include <LyMemoryKernel.h>
int main(int argc, char *argv[]) { cDrvCtrl DriveControl; DriveControl.InstallAndRun();
ProcessDeviationMemory write = { 0 };
write.pid = 3124; write.base_address = 0x6566e0; write.offset_len = 4; write.data = 0; write.offset[0] = 0x18; write.offset[1] = 0x0; write.offset[2] = 0x14; write.offset[3] = 0x0c;
DriveControl.WriteDeviationByte(&write, 0x90);
ProcessDeviationMemory read_offset_struct = { 0 };
read_offset_struct.pid = 3124; read_offset_struct.base_address = 0x6566e0; read_offset_struct.offset_len = 4; read_offset_struct.data = 0; read_offset_struct.offset[0] = 0x18; read_offset_struct.offset[1] = 0x0; read_offset_struct.offset[2] = 0x14; read_offset_struct.offset[3] = 0x0c;
BYTE ref = DriveControl.ReadDeviationByte(&read_offset_struct); printf("读出数据:%x \n", ref);
system("pause"); return 0; }
|
写出后,原始指针失效:

读取字节并反汇编: 运用反汇编引擎可实现对读出字节反汇编输出。
#define _CRT_SECURE_NO_WARNINGS #include <iostream> #include <Windows.h> #include <LyMemoryKernel.h>
#include <inttypes.h> #include <capstone/capstone.h>
#pragma comment(lib,"capstone32.lib")
int main(int argc, char *argv[]) { cDrvCtrl DriveControl; DriveControl.InstallAndRun();
BYTE arr[1024] = { 0 };
for (size_t i = 0; i < 1023; i++) { BYTE by = DriveControl.ReadProcessMemoryByte(3344, 0x402c00 + i);
arr[i] = by; }
csh handle; cs_insn *insn; size_t count;
int size = 1023;
printf("By: LyShark \n\n"); if (cs_open(CS_ARCH_X86, CS_MODE_32, &handle) != CS_ERR_OK) { return 0; }
count = cs_disasm(handle, (unsigned char *)arr, size, 0x402c00, 0, &insn);
if (count > 0) { size_t index; for (index = 0; index < count; index++) { for (int x = 0; x < insn[index].size; x++) { }
printf("地址: 0x%"PRIx64" | 长度: %d 反汇编: %s %s \n", insn[index].address, insn[index].size, insn[index].mnemonic, insn[index].op_str); }
cs_free(insn, count); } else { printf("反汇编返回长度为空 \n"); }
cs_close(&handle); system("pause"); return 0; }
|
反汇编效果:
