Loading... # 我们为什么需要监控技术 无论杀软还是木马,谁先获取进程权,谁就能决定进程的生死,甚至修改进程的数据。Hook技术可以让我们在内核API函数进行挂钩操作,从而实现进程的监控,但随着Windows 64位系统的降临Path Guard的引入,许多在32位下能用的挂钩操作,在64位系统中就不能使用。不过微软提供了一些能监控系统的接口函数。从而实现间接监控。 # 破解内核函数强制签名 ``` // 编程方式绕过签名检查 BOOLEAN BypassCheckSign(PDRIVER_OBJECT pDriverObject) { #ifdef _WIN64 typedef struct _KLDR_DATA_TABLE_ENTRY { LIST_ENTRY listEntry; ULONG64 __Undefined1; ULONG64 __Undefined2; ULONG64 __Undefined3; ULONG64 NonPagedDebugInfo; ULONG64 DllBase; ULONG64 EntryPoint; ULONG SizeOfImage; UNICODE_STRING path; UNICODE_STRING name; ULONG Flags; USHORT LoadCount; USHORT __Undefined5; ULONG64 __Undefined6; ULONG CheckSum; ULONG __padding1; ULONG TimeDateStamp; ULONG __padding2; } KLDR_DATA_TABLE_ENTRY, * PKLDR_DATA_TABLE_ENTRY; #else typedef struct _KLDR_DATA_TABLE_ENTRY { LIST_ENTRY listEntry; ULONG unknown1; ULONG unknown2; ULONG unknown3; ULONG unknown4; ULONG unknown5; ULONG unknown6; ULONG unknown7; UNICODE_STRING path; UNICODE_STRING name; ULONG Flags; } KLDR_DATA_TABLE_ENTRY, * PKLDR_DATA_TABLE_ENTRY; #endif PKLDR_DATA_TABLE_ENTRY pLdrData = (PKLDR_DATA_TABLE_ENTRY)pDriverObject->DriverSection; pLdrData->Flags = pLdrData->Flags | 0x20; return TRUE; } ``` # 进程监控 | 函数 | 说明 | | - | - | | PsSetCreateProcessNotifyRoutinueEx | 设置回调监控的创建和退出,是否允许进程创建。 | | | | ``` NTSTATUS PsSetCreateProcessNotifyRoutineEx( PCREATE_PROCESS_NOTIFY_ROUTINE_EX NotifyRoutine, //回调函数的结构体 BOOLEAN Remove //是否添加或者删除 ); //返回 STATUS_SUCCESS //------------------------------------回调函数 VOID PcreateProcessNotifyRoutineEx( PEPROCESS pEProcess, // EEPROCESS HANDLE hProcessId, //创建进程的ID PPS_CREATE_NOTIFY_INFO CreateInfo //创建的信息结构体 ); //-----------------------------------回调函数原型 PCREATE_PROCESS_NOTIFY_ROUTINE_EX void SetCreateProcessNotifyRoutinueEx ( _In_ HANDLE RarentId, //父进程 _In_ HANDLE ProcessId, //进程ID _Inout_opt_ PPS_CREATE_NOTIFY_INFO CreateInfo //结构体指针进程退出为NULL 不为NULL表示进程创建。 ) //--------------------------------------PS_CREATE_NOTIFY_INFO 结构体信息 typedef struct _PS_CREATE_NOTIFY_INFO { SIZE_T Size; //该结构体大小 union { ULONG Flags; //保留 struct { ULONG FileOpenNameAvailable :1; ULONG IsSubsystemProcess :1; ULONG Reserved :30; //仅供系统使用 }; }; HANDLE ParentProcessId; //父进程ID CLIENT_ID CreatingThreadId; //创建新进程ID和线程ID CreatingThreadId->UniqueProcess 包换进程ID,CreatingThread->UniqueThread包含线程ID struct _FILE_OBJECT *FileObject; //指向进程可执行文件对象的指针,如果IsSubsystemProcess为TRUE,则此值可能为NULL PCUNICODE_STRING ImageFileName; //只想保存可执行文件名的UNICODE_STRING字符串结构体指针 PCUNICODE_STRING CommandLine; //执行附加命令 NTSTATUS CreationStatus; //进程创建操作返回NTSTATUS值。驱动程序可以将此值改为错误代码,阻止创建进程 } PS_CREATE_NOTIFY_INFO, *PPS_CREATE_NOTIFY_INFO; ``` 进程实例 在驱动程序中标记要拦截程序的名称对其拦截,注意如果有中文的话可能会比对错误。 ``` #include <ntddk.h> NTKERNELAPI UCHAR* PsGetProcessImageFileName(__in PEPROCESS Process); //获取进程文件名 VOID PcreateProcessNotifyRoutineEx(PEPROCESS pEProcess, HANDLE hProcessId, PPS_CREATE_NOTIFY_INFO CreateInfo) { //CreateInfo 为NULL标识进程退出 if (NULL == CreateInfo) { KdPrint(("进程退出\n")); return ; } //获取进程名 PCHAR pszImageFileName = PsGetProcessImageFileName(pEProcess); //显示创建信息 KdPrint(("[%s][%d][%wZ]\n",pszImageFileName,hProcessId,CreateInfo->ImageFileName)); if (0 == (_stricmp(pszImageFileName, "notepad.exe"))) { //进制创建 CreateInfo->CreationStatus = STATUS_UNSUCCESSFUL; KdPrint(("进制创建%s\n", pszImageFileName)); } KdPrint(("进制创建%s\n",pszImageFileName)); } NTSTATUS SetProcessNotifyRoutine() { NTSTATUS status = PsSetCreateProcessNotifyRoutineEx((PCREATE_PROCESS_NOTIFY_ROUTINE_EX)PcreateProcessNotifyRoutineEx, FALSE); if (!NT_SUCCESS(status)) { KdPrint(("设置回调错误---[%d]\n",status)); } return status; } VOID DriverUnload(PDRIVER_OBJECT pObject) { NTSTATUS status = PsSetCreateProcessNotifyRoutineEx((PCREATE_PROCESS_NOTIFY_ROUTINE_EX)PcreateProcessNotifyRoutineEx, TRUE); UNREFERENCED_PARAMETER(pObject); }; // 编程方式绕过签名检查 BOOLEAN BypassCheckSign(PDRIVER_OBJECT pDriverObject) { #ifdef _WIN64 typedef struct _KLDR_DATA_TABLE_ENTRY { LIST_ENTRY listEntry; ULONG64 __Undefined1; ULONG64 __Undefined2; ULONG64 __Undefined3; ULONG64 NonPagedDebugInfo; ULONG64 DllBase; ULONG64 EntryPoint; ULONG SizeOfImage; UNICODE_STRING path; UNICODE_STRING name; ULONG Flags; USHORT LoadCount; USHORT __Undefined5; ULONG64 __Undefined6; ULONG CheckSum; ULONG __padding1; ULONG TimeDateStamp; ULONG __padding2; } KLDR_DATA_TABLE_ENTRY, * PKLDR_DATA_TABLE_ENTRY; #else typedef struct _KLDR_DATA_TABLE_ENTRY { LIST_ENTRY listEntry; ULONG unknown1; ULONG unknown2; ULONG unknown3; ULONG unknown4; ULONG unknown5; ULONG unknown6; ULONG unknown7; UNICODE_STRING path; UNICODE_STRING name; ULONG Flags; } KLDR_DATA_TABLE_ENTRY, * PKLDR_DATA_TABLE_ENTRY; #endif PKLDR_DATA_TABLE_ENTRY pLdrData = (PKLDR_DATA_TABLE_ENTRY)pDriverObject->DriverSection; pLdrData->Flags = pLdrData->Flags | 0x20; return TRUE; } //第一个参数DriverObject是刚被初始化的驱动对象 第二个参数RegistryPath是驱动在注册表中的键值 NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) { //如果不破解签名可能PsSetCreateProcessNotifyRoutinueEx会出现错误 BypassCheckSign(DriverObject); UNREFERENCED_PARAMETER(DriverObject); UNREFERENCED_PARAMETER(RegistryPath); KdBreakPoint(); DriverObject->DriverUnload = DriverUnload; SetProcessNotifyRoutine(); return STATUS_SUCCESS; }; ``` ![image.png](http://www.irohane.top/usr/uploads/2021/03/60098054.png) # 模块监控 微软同样提供了一个PsSetLoadImageNotifyRoutine的回调函数来监控模块的加载<span style="color:#DC143C">【回调函数接收到加载信息时,模块已经加载完成,所以回调并不能直接控制模块】</span>,该函数可以设置对系统模块加载进行监控的回调函数,回调函数可以获取模块加载信息,也可以进行注入、拒绝特定驱动的加载。 ![image.png](http://www.irohane.top/usr/uploads/2021/04/3185017422.png) ## 函数介绍 PsSetLoadImageNotifyRoutine ```cpp NTSTATUS PsSetLoadImageNotifyRoutine (_in_ PLOAD_IMAGE_IMAGE_NOTIFY_ROUTINE NotifyRoutine) ``` 成功返回STATUS_SUCCESS,否则返回其他失败错误码 NTSTATUS PsRemoveLoadImageNotifyRoutine 移除模块监控函数 KeDelayExecutionThread 函数实现延迟加载 PLOAD_IMAGE_NOTIFY_ROUTINE 回调函数 ```cpp 可能有错误。这里只作为记录,请自行理解 typedef struct _IMAGE_INFO { union { ULONG Properties; struct { ULONG ImageAddressingMode : 8; //始终设置为IMAGE_ADDRESSING_MODE_32BIT ULONG SystemModeImage : 1; //设置一个新的加载内核模式组件(驱动程序),对应声到用户空间映像设置为0 ULONG ImageMappedToAllPids : 1; //始终为0 ULONG ExtendedInfoPresent : 1; //如果设置ExtendedInfoPersent标志,则IMAGE_INFO结构是图像信息结构体中较大版本的一部分 ULONG MachineTypeMismatch : 1; //始终为0 ULONG ImageSignatureLevel : 4; //代码完整性标记为映像签名级别 ULONG ImageSignatureType : 3; //代码完整性标记为映像的签名类型 ULONG ImagePartialMap : 1; //整个映像为0 部分映像为1 ULONG Reserved : 12 //为0 }; }; PVOID ImageBase; //映像的虚拟基址 ULONG ImageSelector; //始终为0 SIZE_T ImageSize; //影响虚拟大小 ULONG ImageSectionNumber; //始终为0 } IMAGE_INFO, *PIMAGE_INFO; void SetLoadImageNotifyRoutine ( _in_opt_ PUNICODE_STRING FullImageName, //.dll名称 如果无法获取全名为NULL _in_ Handle ProcessId, //模块所属进程ID _In_ PIMAGE_INFO ImageInfo //信息结构体 ) ``` ## 卸载模块 <span style="color:#DC143C">Loadlibrary是可以检测到的,远程线程注入,无法检测到载入的DLL</span> 首先我们需要知道一个问题,当程序函数接收到模块加载的信息的时候,模块已经加载完成,但是可以通过其他方法来卸载已经加载的模块。 ### 卸载驱动 卸载驱动的思路就是在驱动模块入口点 DriverEntry中直接返回NTSTATUS错误码,例如STATUS_ACCESS_DENIED(0xC0000022)。这样已经加载的驱动程序就会出错,导致程序启动失败,实现这种放大得找到驱动程序的入口点内存地址。 模块第三个参数imageInfo结构体提供了内存中加载基址。????????????????????????????????????? 获取DriverEntry函数之后,程序直接将入口函数的前几个字节数据修改为: Mov eax,0xC0000022 ret 对应的机器码 B8 22 00 00 C0 C3 ``` BOOLEAN DenyLoadDriver(PVOID pLoadImageBase) { PIMAGE_DOS_HEADER pDos = pLoadImageBase; PIMAGE_NT_HEADERS pNts = (PIMAGE_NT_HEADERS)(pDos->e_lfanew + (PCHAR)pLoadImageBase); PVOID pAddressOfEntryPoint = (PVOID)((PCHAR)pLoadImageBase + pNts->OptionalHeader.AddressOfEntryPoint); //MDL 方式写入 ULONG ulShellCodeSize = 6; UCHAR pShellCode[6] = { 0xB8,0x22,0x00,0x00,0xC0,0xC3 }; PMDL pMdl = MmCreateMdl(NULL, pAddressOfEntryPoint, ulShellCodeSize); MmBuildMdlForNonPagedPool(pMdl); PVOID pvoid = MmMapLockedPages(pMdl, KernelMode); //写入数据 RtlCopyMemory(pvoid, pShellCode, ulShellCodeSize); //释放MDL MmUnmapLockedPages(pvoid, pShellCode, ulShellCodeSize); IoFreeMdl(pvoid, pMdl); return TRUE; } ``` ### 卸载模块 由于DLL模块已经加载成功,不能像类似卸载驱动模块那样直接在入口点返回拒绝加载信息,因为DLL入口点DLLMain返回值并不能确定DLL释放加载成功,所以还达不到卸载DLL效果。 Windows提供内核API函数MmUnmapViewOfSection用来卸载进程中已加载的模块,MmUnmapViewOfSection是一个未导出函数,声明后可以直接使用。 注意PsSetLoadImageNotifyRoutine函数可以直接捕获模块加载信息,当加载进程模块的时候,系统会有一个内部锁,为了避免死锁,在进程模块加载回调函数时,不能进行进行映射、分配、查询、释放等操作。如果卸载DLL模块,必须等进程中所有模块加载完毕后,才可以释放。解决方法是使用多线程延时等待。进程模块加载完毕后在调用MmUnmapViewOfSection执行释放 .h 文件 ``` #include <ntifs.h> #include <ntimage.h> #include <stdlib.h> #include <ntddk.h> //卸载DLL NTSTATUS DenyLoadDll(HANDLE ProcessId, PVOID pImageBase); //设置模块监控 void SetMonitoringModule(); //卸载模块监控 void UnloadMonitoringModule(); //拒绝加载驱动模块 BOOLEAN DenyLoadDriver(PVOID pLoadImageBase); ``` ``` #include "Hunter_Module.h" NTSTATUS MmUnmapViewOfSection (IN PEPROCESS Process, //进程的 EPROCESS IN PVOID BaseAddress); //DLL 模块基址 //回调函数 void PlayLoadImageNotifyPro(PUNICODE_STRING FullImageName,HANDLE ProcessId,PIMAGE_INFO Image_info) { KdPrint(("%wZ\n", FullImageName)); //来什么卸载什么 DenyLoadDll(ProcessId, Image_info->ImageBase); } void SetMonitoringModule() { NTSTATUS Result = PsSetLoadImageNotifyRoutine(PlayLoadImageNotifyPro); } void UnloadMonitoringModule() { PsRemoveLoadImageNotifyRoutine(PlayLoadImageNotifyPro); } BOOLEAN DenyLoadDriver(PVOID pLoadImageBase) { PIMAGE_DOS_HEADER pDos = pLoadImageBase; PIMAGE_NT_HEADERS pNts = (PIMAGE_NT_HEADERS)(pDos->e_lfanew + (PCHAR)pLoadImageBase); PVOID pAddressOfEntryPoint = (PVOID)((PCHAR)pLoadImageBase + pNts->OptionalHeader.AddressOfEntryPoint); //MDL 方式写入 ULONG ulShellCodeSize = 6; UCHAR pShellCode[6] = { 0xB8,0x22,0x00,0x00,0xC0,0xC3 }; PMDL pMdl = MmCreateMdl(NULL, pAddressOfEntryPoint, ulShellCodeSize); MmBuildMdlForNonPagedPool(pMdl); PVOID pvoid = MmMapLockedPages(pMdl, KernelMode); //写入数据 RtlCopyMemory(pvoid, pShellCode, ulShellCodeSize); //释放MDL MmUnmapLockedPages(pvoid, pShellCode, ulShellCodeSize); IoFreeMdl(pvoid, pMdl); return TRUE; } NTSTATUS DenyLoadDll(HANDLE ProcessId, PVOID pImageBase) { NTSTATUS status = STATUS_SUCCESS; PEPROCESS pEProcess = NULL; status = PsLookupProcessByProcessId(ProcessId, &pEProcess); status = MmUnmapViewOfSection(pEProcess, pImageBase); return status; } ``` 由于svchost 加载的ntdll.dll被我们无限卸载会卡在这里,注意DLL是被加载之后卸载的。 ![image.png](http://www.irohane.top/usr/uploads/2021/04/2464023126.png) 注意对获取DLL的主程序,会卡死状态任务管理器杀不掉【Win7 测试是这样的】 ![image.png](https://www.irohane.top/usr/uploads/2021/04/1213186759.png) # 注册表监控 注册表是Windows重要的数据库,存储着许多重要的系统设置信息以及应用程序的设置信息,所以,一直以来注册表都是病毒程序和木马的主要攻击对象。 Windows提供CmRegisterCallback函数对系统设置注册表监控回调,实时监控注册表的操作。 CmRegisterCallback实例注册一个RegistryCallback,成功返回STATUS_SUCCESS ``` NTSTATUS CmRegisterCallBack( _In_ PEX_CALLBACK_FUNCTION Function, //指向RegistryCallBack _In_opt_ PVOID Context, //配置管理器作为CallBackContext参数传递给RegistryCallBack中由驱动程序定义 _Out_ PLARGE_INTEGER Cookie //指向LARHGE_INTEGER 变量的指针,该变量接受表示回调历程的值,当注销回调时,此值作为Cookie传递给CmUnRegisterCallback ) ``` 回调函数 PEX_CALLBACK_FUNCTION,成功返回STATUS_SUCCESS ``` NTSTATUS RegistryCallback( _In_ PVOID CallbackContext, //注册RegistryCallBack时,驱动程序作为Context参数传递给CmRegisterCallback或CmRegisterCallBackEx的值 _In_opt_ PVOID Argument1, //REG_NOTIFY_CALL类型的值,用于表示正在执行的注册表操作类型,以及是否在指向操作表操作之前或者之后调用RegistryCallback _In_opt_ PVOID Argument2 //指向特定用于注册表操作信息类型的结构体指针,结构体类型去决议Argument1中的REG_NOTIFY_CALL类型的值 ) ``` 卸载函数 ``` CmUnRegisterCallback(_In_ PLARGE_INTEGER Cookie) //卸载函数 ``` 当需要拒绝注册表指定键时,可以通过设置回调函数返回值来进行控制,若返回STATUS_SUCCESS错误码,表示注册表允许执行,除此之外拒绝执行,STATUS_ACCESS_DENIED 错误码表示系统会拒绝操作相应的注册表。 回调函数 Argument1 判断注册表的类型。RegNtPreCreateKey创建注册表,RegNtPreOpenKey打开注册表,RegNtPreDeleteKey删除注册表,RegNTPreDeleteKey删除注册表的键值,RegNtPreSetValueKey设置键值的调用进程,如果想要控制注册表,必须要在操作执行前控制。 Argument2参数获取操作类型对应结构体数据,从结构体数据中可以获取注册表路径对象,并调用ObQueryNameString函数根据路径对象获取有字符串表示的路径,不同操作类型对应不同结构体 返回值控制注册表是否正常运行 .h ``` #pragma once #include <ntddk.h> //全局Cookie LARGE_INTEGER g_cookie ; BOOLEAN GetRegisterObjectCompletePath(PUNICODE_STRING pRegistryPath, PVOID pRegistryObject); //回调函数 NTSTATUS RegisterMonCAllBack ( _In_ PVOID CallbackContext, _In_opt_ PVOID Argument1, _In_opt_ PVOID Argument2 ); NTSTATUS ObQueryNameString( _In_ PVOID Object, _Out_writes_bytes_opt_(Length) POBJECT_NAME_INFORMATION ObjectNameInfo, _In_ ULONG Length, _Out_ PULONG ReturnLength ); //是否时保护状态 BOOLEAN IsProtectReg(UNICODE_STRING ustrRegPath); //设置注册表监控 void SetRegHunter(); //删除注册表监控 void UnloadRegHunter(); ``` .cpp ``` #include "Hunter_Reg.h" // 获取注册表完整路径 BOOLEAN GetRegisterObjectCompletePath(PUNICODE_STRING pRegistryPath, PVOID pRegistryObject) { // 判断数据地址是否有效 if ((FALSE == MmIsAddressValid(pRegistryObject)) || (NULL == pRegistryObject)) { return FALSE; } // 申请内存 ULONG ulSize = 512; PVOID lpObjectNameInfo = ExAllocatePool(NonPagedPool, ulSize); if (NULL == lpObjectNameInfo) { KdPrint(("申请空间出错\n")); return FALSE; } // 获取注册表路径 ULONG ulRetLen = 0; NTSTATUS status = ObQueryNameString(pRegistryObject, (POBJECT_NAME_INFORMATION)lpObjectNameInfo, ulSize, &ulRetLen); if (!NT_SUCCESS(status)) { ExFreePool(lpObjectNameInfo); KdPrint(("申请空间出错\n")); return FALSE; } //KdBreakPoint(); // 复制 RtlCopyUnicodeString(pRegistryPath, (PUNICODE_STRING)lpObjectNameInfo); // 释放内存 ExFreePool(lpObjectNameInfo); return TRUE; } NTSTATUS RegisterMonCAllBack ( _In_ PVOID CallbackContext, _In_opt_ PVOID Argument1, _In_opt_ PVOID Argument2 ) { //获取操作类型 LONG lOperateType = (REG_NOTIFY_CLASS)Argument1; UNICODE_STRING ustrRegPath; NTSTATUS status = STATUS_SUCCESS; // 申请内存 ustrRegPath.Length = 0; ustrRegPath.MaximumLength = 1024 * sizeof(WCHAR); ustrRegPath.Buffer = ExAllocatePool(NonPagedPool, ustrRegPath.MaximumLength); //判断操作 switch (lOperateType) { //创建注册表之前 case RegNtPreCreateKey: { //获取注册表路径 GetRegisterObjectCompletePath(&ustrRegPath, ((PREG_CREATE_KEY_INFORMATION)Argument2)->RootObject); KdPrint(("[RegNtPreCreateKey][%wZ][%wZ]\n", &ustrRegPath, ((PREG_CREATE_KEY_INFORMATION)Argument2)->CompleteName)); break; } //打开注册表之前 case RegNtPreOpenKey: { //KdBreakPoint(); //获取注册表路径 GetRegisterObjectCompletePath(&ustrRegPath, ((PREG_CREATE_KEY_INFORMATION)Argument2)->RootObject); KdPrint(("[RegNtPreCreateKey][%wZ][%wZ]\n", &ustrRegPath, ((PREG_CREATE_KEY_INFORMATION)Argument2)->CompleteName)); break; } //删除键之前 case RegNtPreDeleteKey: { //KdBreakPoint(); //获取注册表路径 GetRegisterObjectCompletePath(&ustrRegPath, ((PREG_DELETE_KEY_INFORMATION)Argument2)->Object); KdPrint(("[RegNtPreDeleteKey][%wZ]\n",&ustrRegPath)); break; } //删除键值之前 case RegNtPreDeleteValueKey: { //KdBreakPoint(); //获取注册表路径 GetRegisterObjectCompletePath(&ustrRegPath, ((PREG_DELETE_VALUE_KEY_INFORMATION)Argument2)->Object); KdPrint(("[RegNtPreDeleteValueKey][%wZ][%wZ]\n", &ustrRegPath, ((PREG_DELETE_VALUE_KEY_INFORMATION)Argument2)->ValueName)); break; }; //修改键值之前 case RegNtPreSetValueKey: { //KdBreakPoint(); //获取注册表路径 GetRegisterObjectCompletePath(&ustrRegPath, ((PREG_SET_VALUE_KEY_INFORMATION)Argument2)->Object); KdPrint(("[RegNtPreSetValueKey][%wZ][%wZ]\n", &ustrRegPath, ((PREG_SET_VALUE_KEY_INFORMATION)Argument2)->ValueName)); break; } default: break; } //判断是否被保护 if (IsProtectReg(ustrRegPath)) { //拒绝操作 status = STATUS_ACCESS_DENIED; } return status; } // 判断是否是保护注册表路径 BOOLEAN IsProtectReg(UNICODE_STRING ustrRegPath) { if (NULL != wcsstr(ustrRegPath.Buffer, L"DemonGan")) { return TRUE; } return FALSE; } //设置注册表监控 void SetRegHunter() { //设置监控 NTSTATUS result = CmRegisterCallback(RegisterMonCAllBack, NULL, &g_cookie); if (!NT_SUCCESS(result)) { KdPrint(("注册表监控设置错误")); g_cookie.QuadPart = 0; return; } return; } //删除注册表监控 void UnloadRegHunter() { if (0 < g_cookie.QuadPart) { //删除监控 CmUnRegisterCallback(g_cookie); } } ``` ![image.png](https://www.irohane.top/usr/uploads/2021/04/636769524.png) 最后修改:2021 年 04 月 05 日 © 允许规范转载 赞 0 如果觉得我的文章对你有用,请随意赞赏