• 三、系统调用


    三、系统调用

    3.1.系统调用

    OpenProcess和ReadProcessMemory从3环进0环的过程

    • kernel32.OpenProcess

    • KernelBase.OpenProcess

    • Ntdll.NtOpenProcess

    • 7FFE0300(获取KiFastCallEntry)

    • sysenter

    kernel32.OpenProcess(什么都不干,直接jmp到KernelBase) --> KernelBase.OpenProcess -->Ntdll.NtOpenProcess (导出的函数是ZwOpenProcess) -->7FFE0300(调用KiFastCallEntry) -->sysenter-->进入内核
    
    kernel32.ReadProcessMemory --> KernelBase.ReadProcessMemory -->Ntdll.NTReadProcessMemory 
    -->7FFE0300 -->sysenter

    ZwOpenProcess

    所有进程在R3和R0共享同一个结构_KUSER_SHARED_DATA

    • R3地址为:7FFE0000

    • R0地址为:FFFE0000

    //R3
    kd> dt _KUSER_SHARED_DATA 7FFE0000
       +0x300 SystemCall       : 0x772b70b0
    
    //R0
    kd> dt _KUSER_SHARED_DATA FFDF0000
       +0x300 SystemCall       : 0x772b70b0
            
    kd> u 0x772b70b0
    ntdll!KiFastSystemCall:
    772b70b0 8bd4            mov     edx,esp
    772b70b2 0f34            sysenter
    ntdll!KiFastSystemCallRet:
    772b70b4 c3              ret
    772b70b5 8da42400000000  lea     esp,[esp]
    772b70bc 8d642400        lea     esp,[esp]
    ntdll!KiIntSystemCall:
    772b70c0 8d542408        lea     edx,[esp+8]
    772b70c4 cd2e            int     2Eh
    772b70c6 c3              ret

    KiFastSystemCall

    API实现OpenProcess功能

    main.cpp

    
    #include <Windows.h>
    #include <winternl.h>
    
    typedef struct _CLIENT_ID
    {
    	HANDLE UniqueProcess;
    	HANDLE UniqueThread;
    }CLIENT_ID, *PCLIENT_ID; 
    
    //ZwOpenProcess函数原型
    typedef NTSTATUS (WINAPI *ZwOpenProcessProc)
    (PHANDLE ProcessHandle, ACCESS_MASK DesiredAccess, 
    	POBJECT_ATTRIBUTES ObjectAttributes, PCLIENT_ID ClientId);
    
    int main()
    {
    	HMODULE hModule = LoadLibraryA("ntdll.dll");
    	PUCHAR temp = (PUCHAR)GetProcAddress(hModule, "ZwOpenProcess");
    	ULONG size = 0;
    	for (int i = 0; i < 100; i++)
    	{
    		if (temp[i] == 0xc2)    
    		{
    			size = i+ 2;	//ZwOpenProcess函数结尾
    			break;
    		}
    	}
    
    	ZwOpenProcessProc func =(ZwOpenProcessProc)VirtualAlloc(NULL, 0x1000, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    	memcpy(func, temp, size);
    
    	HANDLE outProcess = NULL;
    	CLIENT_ID client = {0};
    	client.UniqueProcess = (HANDLE)5776;    //要打开的进程pid
    	OBJECT_ATTRIBUTES obattr = {0};
    	obattr.Length = sizeof(OBJECT_ATTRIBUTES);
    	NTSTATUS status = func(&outProcess, PROCESS_ALL_ACCESS, &obattr, &client);
    
        return 0;
    }

    3.2.系统调用进内核

    1.在 Ring3 的代码调用了 sysenter 指令之后,CPU 会做出如下的操作:

    1. 将 SYSENTER_CS_MSR 的值装载到 cs 寄存器    174
    
    2. 将 SYSENTER_EIP_MSR 的值装载到 eip 寄存器  176
    
    3. 将 SYSENTER_CS_MSR 的值加 8(Ring0 的堆栈段描述符)装载到 ss 寄存器。
    
    4. 将 SYSENTER_ESP_MSR 的值装载到 esp 寄存器 175
    
    5. 将特权级切换到 Ring0
    
    6. 如果 EFLAGS 寄存器的 VM 标志被置位,则清除该标志
    
    7. 开始执行指定的 Ring0 代码

    2.查看msr寄存器

    kd> rdmsr 174
    msr[174] = 00000000`00000008		//CS
    
    kd> rdmsr 176
    msr[176] = 00000000`83e7a760		//EIP
    
    kd> u 83e7a760
    nt!KiFastCallEntry:
    83e7a760 b923000000      mov     ecx,23h
    83e7a765 6a30            push    30h
    83e7a767 0fa1            pop     fs
    83e7a769 8ed9            mov     ds,cx
    83e7a76b 8ec1            mov     es,cx
    83e7a76d 648b0d40000000  mov     ecx,dword ptr fs:[40h]
    83e7a774 8b6104          mov     esp,dword ptr [ecx+4]
    83e7a777 6a23            push    23h
    
    kd> rdmsr 175
    msr[175] = 00000000`80790000		//ESP
    
    //读写MSR的方式,只能在0环,MSR寄存器属于特权指令
    //1.读  eax:存低32位   edx:存高32位	
        mov ecx,0x175
        rdmsr
        
    //2,写   
        mov eax,0x8402a9dc
        mov edx,0
        mov ecx,176h
        wrmsr

    3._KTRAP_FRAME

    dt _KTRAP_FRAME:内核的上下文

    ntdll!_KTRAP_FRAME
       +0x000 DbgEbp           : Uint4B
       +0x004 DbgEip           : Uint4B
       +0x008 DbgArgMark       : Uint4B
       +0x00c DbgArgPointer    : Uint4B
       +0x010 TempSegCs        : Uint2B
       +0x012 Logging          : UChar
       +0x013 Reserved         : UChar
       +0x014 TempEsp          : Uint4B
       +0x018 Dr0              : Uint4B
       +0x01c Dr1              : Uint4B
       +0x020 Dr2              : Uint4B
       +0x024 Dr3              : Uint4B
       +0x028 Dr6              : Uint4B
       +0x02c Dr7              : Uint4B
       +0x030 SegGs            : Uint4B
       +0x034 SegEs            : Uint4B
       +0x038 SegDs            : Uint4B
       +0x03c Edx              : Uint4B
       +0x040 Ecx              : Uint4B
       +0x044 Eax              : Uint4B
       +0x048 PreviousPreviousMode : Uint4B
       +0x04c ExceptionList    : Ptr32 _EXCEPTION_REGISTRATION_RECORD
       +0x050 SegFs            : Uint4B
       +0x054 Edi              : Uint4B
       +0x058 Esi              : Uint4B
       +0x05c Ebx              : Uint4B
       +0x060 Ebp              : Uint4B
       +0x064 ErrCode          : Uint4B
       +0x068 Eip              : Uint4B
       +0x06c SegCs            : Uint4B
       +0x070 EFlags           : Uint4B
       +0x074 HardwareEsp      : Uint4B
       +0x078 HardwareSegSs    : Uint4B
       +0x07c V86Es            : Uint4B
       +0x080 V86Ds            : Uint4B
       +0x084 V86Fs            : Uint4B
       +0x088 V86Gs            : Uint4B

    dt _KPCR 每个cpu逻辑核的状态

    kd> dt _KPCR
    ntdll!_KPCR
       +0x000 NtTib            : _NT_TIB
       +0x000 Used_ExceptionList : Ptr32 _EXCEPTION_REGISTRATION_RECORD
       +0x004 Used_StackBase   : Ptr32 Void
       +0x008 Spare2           : Ptr32 Void
       +0x00c TssCopy          : Ptr32 Void
       +0x010 ContextSwitches  : Uint4B
       +0x014 SetMemberCopy    : Uint4B
       +0x018 Used_Self        : Ptr32 Void
       +0x01c SelfPcr          : Ptr32 _KPCR
       +0x020 Prcb             : Ptr32 _KPRCB
       +0x024 Irql             : UChar
       +0x028 IRR              : Uint4B
       +0x02c IrrActive        : Uint4B
       +0x030 IDR              : Uint4B
       +0x034 KdVersionBlock   : Ptr32 Void
       +0x038 IDT              : Ptr32 _KIDTENTRY
       +0x03c GDT              : Ptr32 _KGDTENTRY
       +0x040 TSS              : Ptr32 _KTSS           //fs:40的位置为 TSS
       +0x044 MajorVersion     : Uint2B
       +0x046 MinorVersion     : Uint2B
       +0x048 SetMember        : Uint4B
       +0x04c StallScaleFactor : Uint4B
       +0x050 SpareUnused      : UChar
       +0x051 Number           : UChar
       +0x052 Spare0           : UChar
       +0x053 SecondLevelCacheAssociativity : UChar
       +0x054 VdmAlert         : Uint4B
       +0x058 KernelReserved   : [14] Uint4B
       +0x090 SecondLevelCacheSize : Uint4B
       +0x094 HalReserved      : [16] Uint4B
       +0x0d4 InterruptMode    : Uint4B
       +0x0d8 Spare1           : UChar
       +0x0dc KernelReserved2  : [17] Uint4B
       +0x120 PrcbData         : _KPRCB

    5.KUSER_SHARED_DATA

    kd> dt _KUSER_SHARED_DATA
    ntdll!_KUSER_SHARED_DATA
       +0x304 SystemCallReturn : Uint4B

    6.KUSER_SHARED_DATA FFDF0000

    304位置是 SystemCallReturn

    kd> dt _KUSER_SHARED_DATA FFDF0000
    ntdll!_KUSER_SHARED_DATA
       +0x304 SystemCallReturn : 0x76de70b4

    7.ntdll.dll --> ZwOpenProcess

    8.ntdll.dll --> KiFastSystemCall

    9.ntoskrnl.exe -->kiFastCallEntry

    10.SSDT表

    KeServiceDescriptorTable

    • ServiceTable:指向函数地址表,KeServiceDescriptorTable+服务号*4 = 函数地址

    • Count:系统服务表被调用的次数

    • ServiceLimit:函数数量

    • ArgmentTable:函数参数表,每个参数4个字节.获取字节数后处以4,得到函数参数个数

    kd> .thread
    Implicit thread is now 83f87380
    kd> dt _KTHREAD 83f87380
    	+0x0bc ServiceTable     : 0x83fbcb00 Void      //线程的0xbc是ServiceTable
    
    kd> dd 0x83fbcb00 
    83fbcb00  83ed052c 00000000 00000191 83ed0b74
    83fbcb10  00000000 00000000 00000000 00000000
    83fbcb20  842175b0 841804f2 83f2f567 00000000
    83fbcb30  025224f9 000000bb 00000011 00000100
    83fbcb40  83ed052c 00000000 00000191 83ed0b74
    83fbcb50  a1496000 00000000 00000339 a149702c
    83fbcb60  5385d2ba d717548f 00000000 00000000
    83fbcb70  83fbcb6c 00000340 00000340 85ce1a38
    
            
    kd> dd KeServiceDescriptorTable
    83fbcb00  83ed052c 00000000 00000191 83ed0b74
    83fbcb10  00000000 00000000 00000000 00000000
    83fbcb20  842175b0 841804f2 83f2f567 00000000
    83fbcb30  025224f9 000000bb 00000011 00000100
    83fbcb40  83ed052c 00000000 00000191 83ed0b74
    83fbcb50  a1496000 00000000 00000339 a149702c
    83fbcb60  5385d2ba d717548f 00000000 00000000
    83fbcb70  83fbcb6c 00000340 00000340 85ce1a38
            
    kd> dd 83ed052c +0xbe*4  //通过服务号,获取到当前函数地址
    83ed0824  84065cb1 840b852f 840a5ffe 83fd1118
    83ed0834  840bdbab 840390ae 840daf91 840a1fad
    83ed0844  840b22ae 840cc83a 840a6115 8415ecc3
    83ed0854  841474ed 84148785 840374e8 84094d6b
    83ed0864  8414709e 84146dbe 84147156 84146e76
    83ed0874  8404a98f 84019e7c 84034a34 84148ee0
    83ed0884  84148fa6 840967ff 840e7bbe 840abef8
    83ed0894  841598e2 84159d25 83f172b6 840cb0e2
    
    kd> u 84065cb1 
    nt!NtOpenProcess:
    84065cb1 8bff            mov     edi,edi
    84065cb3 55              push    ebp
    84065cb4 8bec            mov     ebp,esp
    84065cb6 51              push    ecx
    84065cb7 51              push    ecx
    84065cb8 64a124010000    mov     eax,dword ptr fs:[00000124h]
    84065cbe 8a803a010000    mov     al,byte ptr [eax+13Ah]
    84065cc4 8b4d14          mov     ecx,dword ptr [ebp+14h]
                    
     //根据服务号,算出参数的字节数为10,转换为10进制是16,然后16/4=4,得出参数个数为4       
    kd> db 83ed0b74+be   
    83ed0c32  10 0c 10 14 0c 0c 0c 0c-10 10 14 0c 14 18 0c 14  ................
    83ed0c42  08 08 08 08 0c 14 18 10-0c 14 08 08 08 08 08 08  ................
    83ed0c52  04 2c 1c 08 24 14 08 14-14 14 14 14 14 14 14 14  .,..$...........
    83ed0c62  14 14 14 04 08 14 14 14-18 14 14 08 10 08 00 24  ...............$
    83ed0c72  14 18 14 14 0c 10 14 10-18 04 14 0c 18 18 14 14  ................
    83ed0c82  18 0c 18 24 24 08 18 14-08 04 04 14 04 10 08 0c  ...$$...........
    83ed0c92  04 14 18 08 08 08 0c 0c-08 10 14 08 08 0c 08 0c  ................
    83ed0ca2  0c 04 08 08 08 08 08 08-0c 0c 24 00 08 08 08 0c  ..........$.....
      

    3.3.系统调用返回R3

    在 Ring0 代码执行完毕,调用 SYSEXIT 指令退回 Ring3 时,CPU 会做出如下操作

    1. 将 SYSENTER_CS_MSR 的值加 16(Ring3 的代码段描述符)装载到 cs 寄存器
    
    2. 将寄存器 edx 的值装载到 eip 寄存器
    
    3. 将 SYSENTER_CS_MSR 的值加 24(Ring3 的堆栈段描述符)装载到 ss 寄存器
    
    4. 将寄存器 ecx 的值装载到 esp 寄存器
    
    5. 将特权级切换到 Ring3
    
    6. 继续执行 Ring3 的代码

    3.4.SSDThook

    tools.h

    #pragma once
    #include <ntifs.h>
    
    PWCH GetSystemRootNtdllPath();
    
    PUCHAR MapOfViewFile(PWCH path);
    
    VOID UmMapOfViewFile(PVOID mapBase);
    
    ULONG64 ExportTableFuncByName(char* pData, char* funcName);

    tools.c

    #include "Tools.h"
    #include <ntstrsafe.h>
    #include <ntimage.h>
    
    
    EXTERN_C NTSTATUS MmCreateSection(
    	__deref_out PVOID* SectionObject,
    	__in ACCESS_MASK DesiredAccess,
    	__in_opt POBJECT_ATTRIBUTES ObjectAttributes,
    	__in PLARGE_INTEGER InputMaximumSize,
    	__in ULONG SectionPageProtection,
    	__in ULONG AllocationAttributes,
    	__in_opt HANDLE FileHandle,
    	__in_opt PFILE_OBJECT FileObject
    );
    
    PWCH GetSystemRootNtdllPath()
    {
    	//获取根目录 256
    	PWCH  systemPath = ExAllocatePool(PagedPool, PAGE_SIZE);
    	memset(systemPath, 0, PAGE_SIZE);
    	RtlStringCbPrintfW(systemPath, PAGE_SIZE, L"\\??\\%s\\system32\\ntdll.dll", SharedUserData->NtSystemRoot);
    	DbgPrintEx(77, 0, "[db]:%S\r\n", systemPath);
    	//ExFreePool(systemPath);
    	return systemPath;
    }
    
    PUCHAR MapOfViewFile(PWCH path)
    {
    
    	UNICODE_STRING fileName = { 0 };
    	RtlInitUnicodeString(&fileName, path);
    	OBJECT_ATTRIBUTES ObjectAttributes;
    	InitializeObjectAttributes(&ObjectAttributes, &fileName, OBJ_CASE_INSENSITIVE, NULL, NULL);
    
    	IO_STATUS_BLOCK IoStatusBlock = { 0 };
    
    	HANDLE hFile = NULL;
    	NTSTATUS status = ZwCreateFile(&hFile, GENERIC_READ, &ObjectAttributes, &IoStatusBlock, NULL,
    		FILE_ATTRIBUTE_NORMAL, FILE_SHARE_READ, FILE_OPEN, FILE_NON_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT, NULL, NULL);
    
    	if (!NT_SUCCESS(status))
    	{
    		return 0;
    
    	}
    
    
    	//创建节区
    	PVOID hSection = NULL;
    	OBJECT_ATTRIBUTES sectionAddr;
    	InitializeObjectAttributes(&sectionAddr, NULL, OBJ_CASE_INSENSITIVE, NULL, NULL);
    	LARGE_INTEGER InputMaximumSize = { 0 };
    	status = MmCreateSection(&hSection, SECTION_ALL_ACCESS, &sectionAddr,
    		&InputMaximumSize, PAGE_EXECUTE_READWRITE, 0x1000000, hFile, NULL);
    
    	if (!NT_SUCCESS(status))
    	{
    		ZwClose(hFile);
    		return 0;
    	}
    
    	PVOID mapBase = NULL;
    	SIZE_T viewSize = { 0 };
    	status = MmMapViewInSystemSpace(hSection, &mapBase, &viewSize);
    
    	ObDereferenceObject(hSection);
    	ZwClose(hFile);
    
    	if (NT_SUCCESS(status))
    	{
    		return mapBase;
    
    	}
    
    
    
    	return 0;
    }
    
    
    VOID UmMapOfViewFile(PVOID mapBase)
    {
    	if (!mapBase) return;
    	MmUnmapViewInSystemSpace(mapBase);
    }
    
    //获取到 LoadLibraryExW
    ULONG64 ExportTableFuncByName(char* pData, char* funcName)
    {
    	PIMAGE_DOS_HEADER pHead = (PIMAGE_DOS_HEADER)pData;
    	PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(pData + pHead->e_lfanew);
    	int numberRvaAndSize = pNt->OptionalHeader.NumberOfRvaAndSizes;
    	PIMAGE_DATA_DIRECTORY pDir = (PIMAGE_DATA_DIRECTORY)&pNt->OptionalHeader.DataDirectory[0];
    
    	PIMAGE_EXPORT_DIRECTORY pExport = (PIMAGE_EXPORT_DIRECTORY)(pData + pDir->VirtualAddress);
    
    	ULONG64 funcAddr = 0;
    	for (int i = 0; i < pExport->NumberOfNames; i++)
    	{
    		int* funcAddress = pData + pExport->AddressOfFunctions;
    		int* names = pData + pExport->AddressOfNames;
    		short* fh = pData + pExport->AddressOfNameOrdinals;
    		int index = -1;
    		char* name = pData + names[i];
    
    		if (strcmp(name, funcName) == 0)
    		{
    			index = fh[i];
    		}
    
    
    
    		if (index != -1)
    		{
    			funcAddr = pData + funcAddress[index];
    			break;
    		}
    
    
    	}
    
    	if (!funcAddr)
    	{
    		KdPrint(("没有找到函数%s\r\n", funcName));
    
    	}
    	else
    	{
    		KdPrint(("找到函数%s addr %p\r\n", funcName, funcAddr));
    	}
    
    
    	return funcAddr;
    }

    ssdt.h

    #pragma once
    #include <ntifs.h>
    
    typedef struct _SsdtItem
    {
    	PULONG funcTable;
    	ULONG_PTR totalCount;
    	ULONG_PTR funcMaxNumber;
    	PUCHAR paramTable;
    }SsdtItem;
    
    typedef struct _SsdtTable
    {
    	SsdtItem ssdt;
    	SsdtItem sssdt;
    }SsdtTable, * PSsdtTable;
    
    
    
    BOOLEAN SsdtInit();
    
    VOID SsdtDestory();
    
    ULONG SsdtGetFunctionIndex(char* funName);
    
    ULONG_PTR SsdtSetHook(char* funName, ULONG_PTR newFunction);

    ssdt.c

    #include "ssdt.h"
    #include "tools.h"
    #include <intrin.h>
    
    PUCHAR gMapNtdll = NULL;
    
    //导出服务表
    extern PSsdtTable KeServiceDescriptorTable;
    
    //关闭写保护
    ULONG wpOff()
    {
    	ULONG cr0 = __readcr0();
    	_disable();   //关闭中断
    	__writecr0(cr0 & (~0x10000));   //关闭写保护
    	return cr0;
    }
    
    VOID wpOn(ULONG value)
    {
    	__writecr0(value);
    	_enable();
    }
    
    BOOLEAN SsdtInit()
    {
    	if (gMapNtdll) return TRUE;
    	PWCH path = GetSystemRootNtdllPath();
    	gMapNtdll = MapOfViewFile(path);
    	ExFreePool(path);
    	return TRUE;
    }
    
    VOID SsdtDestory()
    {
    	if (!gMapNtdll) return;
    	UmMapOfViewFile(gMapNtdll);
    	gMapNtdll = NULL;
    }
    
    PSsdtTable SsdtGet()
    {
    	return (PSsdtTable)((PUCHAR)KeServiceDescriptorTable + 0x40);
    }
    
    ULONG SsdtGetFunctionIndex(char* funName)
    {
    	PUCHAR func = (PUCHAR)ExportTableFuncByName(gMapNtdll, funName);
    	if (!func) return -1;
    	return *(PULONG)(func + 1);
    }
    
    ULONG_PTR SsdtSetHook(char* funName, ULONG_PTR newFunction)
    {
    	PSsdtTable ssdtTable = SsdtGet();
    
    	ULONG index = SsdtGetFunctionIndex(funName);
    
    	if (index == -1) return 0;
    
    	ULONG function = ssdtTable->ssdt.funcTable[index];
    
    	ULONG _cr0 = wpOff();
    
    	ssdtTable->ssdt.funcTable[index] = newFunction;
    
    	wpOn(_cr0);
    
    	return function;
    
    }

    main.c

    #include <ntifs.h>
    #include <ntstrsafe.h>
    #include "tools.h"
    #include "ssdt.h"
    
    
    ULONG_PTR goldFunc = 0;
    
    typedef NTSTATUS(NTAPI* OpenProcessProc)(
    	_Out_ PHANDLE ProcessHandle,
    	_In_ ACCESS_MASK DesiredAccess,
    	_In_ POBJECT_ATTRIBUTES ObjectAttributes,
    	_In_opt_ PCLIENT_ID ClientId
    	);
    
    NTSTATUS NTAPI MyOpenProcess(
    	_Out_ PHANDLE ProcessHandle,
    	_In_ ACCESS_MASK DesiredAccess,
    	_In_ POBJECT_ATTRIBUTES ObjectAttributes,
    	_In_opt_ PCLIENT_ID ClientId
    )
    {
    	DbgPrintEx(77, 0, "[db]:%s\r\n", __FUNCTION__);
    	OpenProcessProc func = (OpenProcessProc)goldFunc;
    
    	return func(ProcessHandle, DesiredAccess, ObjectAttributes, ClientId);
    }
    
    VOID DriverUnload(PDRIVER_OBJECT pDriver)
    {
    	SsdtSetHook("ZwOpenProcess", goldFunc);
    	SsdtDestory();
    
    	//延时
    	LARGE_INTEGER inTime = { 0 };
    	inTime.QuadPart = -10000 * 3000;  //3s
    	KeDelayExecutionThread(KernelMode, FALSE, &inTime);
    }
    
    NTSTATUS DriverEntry(PDRIVER_OBJECT pDriver, PUNICODE_STRING pReg)
    {
    	SsdtInit();
    
    	goldFunc = SsdtSetHook("ZwOpenProcess", MyOpenProcess);
    
    	pDriver->DriverUnload = DriverUnload;
    	return STATUS_SUCCESS;
    }
  • 相关阅读:
    eclipse中svn的各种状态图标详解
    Invalid configuation file. File "**********" was created by a VMware product with more feature than this version of VMware Workstation and cannot be
    linux下tomcat无法访问问题(换一种说法:无法访问8080端口)
    安装MySQL start Service(无法启动服务)
    eclipse下SVN subclipse插件
    tomcat启动窗口中的时间与系统时间不一致
    关于如果从SQLSERVER中获取 数据库信息 或者 表信息
    有关google的appengine部署服务器的简单教程
    部署到Google App Engine时中途退出后引起的问题
    重温WCF之数据契约中使用枚举(转载)(十一)
  • 原文地址:https://www.cnblogs.com/derek1184405959/p/16570813.html
Copyright © 2020-2023  润新知