Windows在实现上几乎将所有的事物都抽象为对象来进行管理,常见的比如Process,Thread,Event,Window,Menu,Font,Bitmap等。同时将所有的对象分为三类:KernelObject,GDIObject,和 UserObject。具体参加下表:
Object | Instance |
---|---|
Kernel Object | Access token, Change notification, Communications device, Console input, Console screen buffer, Desktop, Event, Event log, File, File mapping, Find file, Heap, I/O completion port, Job, Mailslot, Memory resource notification, Module, Mutex, Pipe, Process, Semaphore, Socket, Thread, Timer, Update resource, Window station |
User Object | Acceleratortable, Caret, Cursor, DDEconversation, Hook, Icon, Menu, Window, Windowposition |
GDI Object | Bitmap, Brush, DC, Enhanced metafile, Enhanced-metafile DC, Font, Memory DC, Metafile, Metafile DC, Palette, Pen and extended pen, Region |
User Object和GDI Object的管理是由图像化组件win32k来进行管理的,在理解User Object的句柄管理之前,需要先了解下与其相关的初始化动作:
初始化
为了方便理解,下文相关的代码内容被精简,删除了大部分不想关的逻辑。
Win32UserInitialize
Win32UserInitialize中包含句柄管理的初始化动作,其由Win32k入口例程DriverEntry调用;调用流程:DriverEntry -> Win32UserInitialize
NTSTATUS Win32UserInitialize(VOID)
{
NTSTATUS Status;
...
Status = InitCreateSharedSection();
...
gpsi = (PSERVERINFO)SharedAlloc(sizeof(SERVERINFO));
...
// Initialize the handle manager.
HMInitHandleTable(gpvSharedBase);
// Setup shared info block.
gSharedInfo.psi = gpsi;
gSharedInfo.pDispInfo = gpDispInfo;
...
}
InitCreateSharedSection
该函数负责创建共享的Section结构,同时映射到Session地址空间,最终在此Section的地址范围中,将其一部分地址用来创建Heap;涉及到的全局变量有ghSectionShared,gpvSharedBase和gpvSharedAlloc 。
由此可以得到一个概念:句柄表和Heap(gpvSharedAlloc)在地址上是连续的。
调用流程:Win32UserInitialize -> InitCreateSharedSection
NTSTATUS InitCreateSharedSection(VOID)
{
ULONG ulHeapSize;
ULONG ulHandleTableSize;
NTSTATUS Status;
LARGE_INTEGER SectionSize;
SIZE_T ViewSize;
PVOID pHeapBase;
ulHeapSize = ROUND_UP_TO_PAGES(USRINIT_SHAREDSECT_SIZE * 1024);
ulHandleTableSize = ROUND_UP_TO_PAGES(0x10000 * sizeof(HANDLEENTRY));
SectionSize.LowPart = ulHeapSize + ulHandleTableSize;
SectionSize.HighPart = 0;
Status = Win32CreateSection(&ghSectionShared,
SECTION_ALL_ACCESS,
(POBJECT_ATTRIBUTES)NULL,
&SectionSize,
PAGE_EXECUTE_READWRITE,
SEC_RESERVE,
(HANDLE)NULL,
NULL,
TAG_SECTION_SHARED);
ViewSize = 0;
gpvSharedBase = NULL;
Status = Win32MapViewInSessionSpace(ghSectionShared, &gpvSharedBase, &ViewSize);
pHeapBase = ((PBYTE)gpvSharedBase + ulHandleTableSize);
// Create shared heap.
gpvSharedAlloc = UserCreateHeap(
ghSectionShared,
ulHandleTableSize,
pHeapBase,
ulHeapSize,
UserCommitSharedMemory);
UserAssert(Win32HeapGetHandle(gpvSharedAlloc) == pHeapBase);
return STATUS_SUCCESS;
}
SharedAlloc
从Heap(gpvSharedAlloc)中申请空间,如:gpsi。
__inline PVOID
SharedAlloc(ULONG cb)
{
return Win32HeapAlloc(gpvSharedAlloc, cb, 0, 0);
}
重点全局变量介绍
gpsi
描述全局的server info,就User Object对象管理,涉及如下两个成员:
typedef struct tagSERVERINFO {
...
+0x8 cHandleEntries : QWORD // Handle Entry的个数
...
+0x360 cbHandleEntries:DWORD // 所有Handle Entry总共字节大小
...
} SERVERINFO;
PSERVERINFO gpsi = NULL;
cHandleEntries 保存当前所有的Handle Entry个数;cbHandleEntries表示所有的Handle Entry的内存字节数。
gahti
全局对象类型信息数组,用来描述所有User Object的相关信息,比如对象大小,类型index等,结构及部分注释如下:
struct tagHANDLETYPEINFO
{
PVOID fnDestroy;
ULONG types;
ULONG dwCreateFlag;
ULONG dwAllocSize;
ULONG dwAllocTag;
};
gSharedInfo
tagSharedInfo变量,非指针,其保存gpsi指针和句柄表指针
struct tagSHAREDINFO
{
PVOID psi; // 指向ServerInfo的指针 == gpsi
PVOID aheList; // 句柄表指针
ULONG aheSize; // 句柄表Entry大小
ULONG unknown0;
PVOID pDispInfo;
};
SHAREDINFO gSharedInfo;
句柄表指针指向的是一个Handle Entry数组,每个Entry大小为0x20,结构如下:
struct __declspec(align(8)) _HANDLEENTRY
{
PVOID phead;
ULONG_PTR nOwnerID;
PVOID pDesktop;
BYTE bType;
BYTE bFlags;
WORD wUniq;
};
gpKernelHandleTable
内核独享的句柄表的指针,保存实际对象的地址,结构如下:
struct _KERNEL_HANDLENTRY
{
PVOID pObject; // User Object对象地址,初始化时其值代表下一个空闲的index值;
PVOID pOwner;
PVOID pUnknown;
};
gHandlePages
HANDLEPAGE全局变量,用以描述句柄表所在页面是否有空闲index,从Pool中申请
struct _HANDLEPAGE
{
ULONG_PTR iheLimit;
ULONG_PTR iheFreeEven;
ULONG_PTR iheFreeOdd;
};
HANDLEPAGE gHandlePages;
其中iheLimit用来描述当前HandlePage所描述的所有HandleEntries的个数,iheFreeEven描述空闲Handle的偶数Index,iheFreeOdd用来描述空闲Handle奇数index。
User Object的句柄管理
在理解了上述全局变量后,再来看句柄管理就容易理解很多了,先看初始化:
句柄表初始化:HMInitHandleTable()
__int64 HMInitHandleTable()
{
PHANDLEENTRYpSharedBase; // rdi
unsigned intstatus; // ebx
PKERNEL_HANDLENTRYpKernelHandleTable; // rsi
char v4; // [rsp+20h] [rbp-18h]
pSharedBase=(PHANDLEENTRY)gpvSharedBase;
CLockDomainSharedAllowAllRecursion<DLT_HANDLEMANAGER>::CLockDomainSharedAllowAllRecursion<DLT_HANDLEMANAGER>(&v4);
status= 0;
pKernelHandleTable= (PKERNEL_HANDLENTRY)gpKernelHandleTable;
gHandlePages.iheLimit = 0i64;
gHandlePages.iheFreeOdd = 0i64;
gHandlePages.iheFreeEven = 0i64;
gSharedInfo.aheList =pSharedBase;
gSharedInfo.aheSize =0x20;
*((_QWORD *)gpsi + 1) = 0i64; // cHandleEntries
*((_DWORD *)gpsi + 0xD8) = 0; // cbHandleEntrie
if ( (unsigned int)HMGrowHandleTable() )
{
pKernelHandleTable->pObject= 0i64;
pSharedBase->bType= 0;
status= 1;
pSharedBase->wUniq= 1;
gHandlePages.iheFreeEven = 2i64;
}
else
{
gSharedInfo.aheList = 0i64;
}
returnstatus;
}
从上面代码初始化代码可以看到,gSharedInfo.aheList就是gpvSharedBase,与InitCreateSharedSection描述的一致。其次由于当前还没有一个有效的句柄表页面,因此初始化gHandlePages和gpsi描述的成员为0,同时调用HMGrowHandleTable()开始申请第一个页面。
句柄表页面管理:HMGrowHandleTable()
__int64 HMGrowHandleTable(void)
{
char *pCurTableEnd; // rax
signed __int64 iheLimit; // rbx
signed __int64 iheLimit_New; // rdx
ULONG_PTR *v3; // r8
_WORD *v4; // rcx
__int64 result; // rax
__int64 nGrowSize; // [rsp+30h] [rbp+8h]
if ( *((_QWORD *)gpsi + 1) == 0xFFFEi64 ) // gpis + 1是cHandleEntries
return 0i64;
pCurTableEnd = (char *)gSharedInfo.aheList + *((unsigned int *)gpsi + 0xD8);
if ( (unsigned __int64)pCurTableEnd >= gpvSharedAlloc )// can't above heap base
return 0i64;
nGrowSize = 0x1000i64;
if ( (signed int)CommitReadOnlyMemory(
ghSectionShared,
&nGrowSize,
(unsigned int)((_DWORD)pCurTableEnd - gpvSharedBase),// offset
0i64) < 0
|| (signed int)MmCommitSessionMappedView(0x18i64 * *((_QWORD *)gpsi + 1) + gpKernelHandleTable, nGrowSize) < 0 )
{
return 0i64;
}
*((_DWORD *)gpsi + 0xD8) += 0x1000; // 0xD8*4 偏移是cbHandleEntires
*((_QWORD *)gpsi + 1) = (unsigned __int64)*((unsigned int *)gpsi + 0xD8) >> 5;// Handle Entry大小是0x20, Kernel Handle Entry是0x18
if ( *((_QWORD *)gpsi + 1) > 0xFFFEui64 )
*((_QWORD *)gpsi + 1) = 0xFFFEi64;
iheLimit = gHandlePages.iheLimit;
memset(
(char *)gSharedInfo.aheList + 0x20 * gHandlePages.iheLimit,
0,
0x20 * (*((_QWORD *)gpsi + 1) - gHandlePages.iheLimit));// 初始化新增加的句柄Entries
memset((void *)(gpKernelHandleTable + 24 * iheLimit), 0, 24 * (*((_QWORD *)gpsi + 1) - iheLimit));// 初始化新增加的内核句柄Entries
iheLimit_New = *((_QWORD *)gpsi + 1) - 1i64;
v3 = (ULONG_PTR *)(gpKernelHandleTable + 24 * iheLimit_New);
if ( iheLimit_New >= iheLimit )
{
v4 = (char *)gSharedInfo.aheList + 0x20 * (*((_QWORD *)gpsi + 1) - 1i64) + 26;
do
{
*v4 = 1; // uniq 固定为1
if ( iheLimit_New & 1 )
{
*v3 = gHandlePages.iheFreeOdd;
gHandlePages.iheFreeOdd = iheLimit_New;
}
else
{
*v3 = gHandlePages.iheFreeEven;
之前已经知道句柄表使用的地址范围为ghSectionShared代表的空间,因此该函数内部通过提交内存的方式动态增长handle entry和kernel handle entry页面,同时负责初始化entry中的值。需要注意的是handle entry中每一个新增的uniq固定为1,kernel handle entry中的pObject默认保存的是下一个空闲的index值。
句柄表的申请:HMAllocObject(…)
PVOID HMAllocObject(
PTHREADINFO ptiOwner,
PDESKTOP pdeskSrc,
BYTE bType,
DWORD size)
在上面逻辑很清楚后,HMAllocObject所做动作的理解就比较简单了。
首先通过gHandlePages来获取空闲句柄的index值,若空间不够,则调用HMGrowHandleTable动态增长句柄表页面:
while ( 1 )
{
if ( (_BYTE)nObjectType != 1 && gHandlePages.iheFreeOdd )
{
pHandleIndex = &gHandlePages.iheFreeOdd;
goto LABEL_6;
}
if ( gHandlePages.iheFreeEven )
break;
if ( !(unsigned int)HMGrowHandleTable() )
goto LABEL_74;
}
pHandleIndex = &gHandlePages.iheFreeEven;
其次,利用参数bType从全局变量gahi获取指定对象的信息,包括CreateFlag,AllocSize,AllocTag等;其中CreateFlag用以标记该对象从何种内存区域分配空间,常见有如下三种类型:
#define OCF_DESKTOPHEAP 0x10 Desktop Heap区域
#define OCF_USEPOOLIFNODESKTOP 0x20 直接从pool中分配,若标记了0x200,则使用type isolation
#define OCF_SHAREDHEAP 0x40 gpvSharedAlloc区域
根据不同的类型在不同的区域申请对象,同时填充一些必要的信息;
之后,利用获取到的HandleIndex值,定位到对应的Handle Entry和Kernel Handle Entry,并初始化其中成员值:
nHandleIndex = *pHandleIndex;
pHandleEntry = (PHANDLEENTRY)((char *)gSharedInfo.aheList + 0x20 * *pHandleIndex);// gSharedInfo+0x8
v16 = (unsigned int)nHandleIndex < giheLast;
v17 = (_DWORD)nHandleIndex == giheLast;
pKernelHandleEntry = (PKERNEL_HANDLENTRY)(gpKernelHandleTable + 24 * nHandleIndex);
*piheFree2 = (ULONG_PTR)pKernelHandleEntry->pObject;
而最终句柄值的计算公式:
nHandleValue= (signed int)nHandleIndex | (unsigned __int64)(*(unsigned __int16 *)((char *)gSharedInfo.aheList + nHandleIndex * gSharedInfo.aheSize + 0x1A) << 16);
*pObjectHead = nHandleValue;
句柄值最终保存在开辟的对象的前8个字节中。
总结
- User Object所涉及到的多个全局变量的解惑。
- User Object的句柄的初始化及申请过程。
- User Object的申请位置,及共享区域保存的数据值信息。
待进一步思考
由于DesktopAlloc和gpvSharedAlloc都是用了内核下的HEAP,而且用户态通过HMValidateHandle返回的对象地址(用户态地址)所使用的的物理页面和HEAP是一致的,因此可以从用户态读取对象信息,从而获得了句柄值和偏移值。
因此可以衍生如下问题:
- win32k中是否有大量的共享页面,这些共享页面是否有内核地址信息泄露?
- 内核下的Heap和用户态下的Heap有什么区别?它的实现机制是如何;
- NtUserCall 使用了函数表win32kfull!apfnSimpleCall,因此可以Fuzz下函数。
附
HMAllocObject逆向后的完整伪代码
非必须,作为在上文理解过程中辅助。
PVOID __fastcall HMAllocObject(_QWORD *ThreadOwner, _QWORD *DeskSrc, unsigned __int8 ObjectType, unsigned int ObjectSize)
{
__int64 v4; // rsi
size_t nObjectSize; // rdi
_QWORD *v6; // rbp
__int64 nObjectType; // r14
_QWORD *pDeskSrc2; // r12
__int16 nCreateFlag; // bx
ULONG_PTR *pHandleIndex; // r15
size_t v11; // rbp
ULONG v12; // edi
_QWORD *pObjectHead; // rdi
ULONG_PTR nHandleIndex; // r12
PHANDLEENTRY pHandleEntry; // r15
bool v16; // cf
bool v17; // zf
PKERNEL_HANDLENTRY pKernelHandleEntry; // r14
unsigned __int8 v19; // r8
__int64 v20; // rax
unsigned __int64 nHandleValue; // rdx
unsigned int v22; // eax
PVOID result; // rax
__int64 v24; // rax
unsigned __int64 *v25; // rcx
__int64 (__fastcall *pObjectBody)(_QWORD, _QWORD, _QWORD); // rax
void *v27; // r14
__int64 v28; // rcx
unsigned int v29; // eax
__int64 v30; // rdx
__int64 pObjectBody2; // rax
__int64 v32; // rax
signed __int64 v33; // rcx
_QWORD *v34; // rax
char v35; // al
char v36; // cl
int v37; // er8
BOOL fUsePoolIfNoDesktop; // [rsp+44h] [rbp-54h]
__int64 nTypeOff; // [rsp+48h] [rbp-50h]
ULONG_PTR *piheFree2; // [rsp+50h] [rbp-48h]
_QWORD *ptiOwner; // [rsp+A0h] [rbp+8h]
_QWORD *pDeskSrc; // [rsp+A8h] [rbp+10h]
unsigned __int8 bType; // [rsp+B0h] [rbp+18h]
bType = ObjectType;
pDeskSrc = DeskSrc;
ptiOwner = ThreadOwner;
v4 = 0i64;
nObjectSize = ObjectSize;
v6 = ThreadOwner;
nObjectType = ObjectType;
pDeskSrc2 = DeskSrc;
fUsePoolIfNoDesktop = 0;
GetDomainLockRef(14i64);
nTypeOff = nObjectType;
nCreateFlag = gahti[nObjectType].dwCreateFlag;// 注意这是word* ++,因此每个Entry大小是24字节,偏移为12
if ( nCreateFlag & 3 ) // bCreateFlags & (OCF_PROCESSOWNED | OCF_THREADOWNED)
{
v4 = v6[0x34];
if ( *(_DWORD *)(v4 + 0x44) >= gUserProcessHandleQuota )
{
LABEL_74:
v33 = 0x486i64;
LABEL_77:
UserSetLastError(v33);
return 0i64;
}
}
while ( 1 )
{
if ( (_BYTE)nObjectType != 1 && gHandlePages.iheFreeOdd )
{
pHandleIndex = &gHandlePages.iheFreeOdd;
goto LABEL_6;
}
if ( gHandlePages.iheFreeEven )
break;
if ( !(unsigned int)HMGrowHandleTable() )
goto LABEL_74;
}
pHandleIndex = &gHandlePages.iheFreeEven;
LABEL_6:
piheFree2 = pHandleIndex;
if ( nCreateFlag & 0x10 && pDeskSrc2 ) // OCF_DESKTOPHEAP
{
if ( !qword_1C025D6A8 )
goto LABEL_76;
if ( (signed int)qword_1C025D6A8() < 0 )
goto LABEL_76;
pObjectHead = HMAllocateUserOrIsolatedType(nObjectSize, nCreateFlag, nObjectType);
if ( !pObjectHead )
goto LABEL_76;
pObjectBody = DesktopAlloc;
if ( DesktopAlloc )
pObjectBody = (__int64 (__fastcall *)(_QWORD, _QWORD, _QWORD))DesktopAlloc(
pDeskSrc2,
gahti[nObjectType].dwAllocSize,
((_DWORD)nObjectType << 16) | 5u);
pObjectHead[5] = pObjectBody;
if ( !pObjectBody )
{
HMFreeUserOrIsolatedType(nCreateFlag, nObjectType, pObjectHead);
goto LABEL_76;
}
v27 = (void *)pObjectHead[3];
ObfReferenceObject(pDeskSrc2);
pObjectHead[3] = pDeskSrc2;
if ( v27 )
ObfDereferenceObject(v27); // may be bug
v28 = pObjectHead[5];
pObjectHead[4] = pObjectHead;
pObjectHead[6] = v28 - pDeskSrc2[0x10];
}
else if ( nCreateFlag & 0x40 ) // OCF_SHAREDHEAP
{
if ( gahti[nTypeOff].dwAllocSize )
{
v30 = gahti[nTypeOff].types;
pObjectHead = Win32AllocPoolZInit(nObjectSize);
if ( !pObjectHead )
goto LABEL_76;
pObjectBody2 = RtlAllocateHeap(gpvSharedAlloc, 0i64, gahti[nTypeOff].dwAllocSize);
pObjectHead[5] = pObjectBody2;
if ( !pObjectBody2 )
{
Win32FreePool(pObjectHead);
goto LABEL_76;
}
pObjectHead[3] = 0i64;
pObjectHead[4] = 0i64;
pObjectHead[6] = pObjectBody2 - gpvSharedAlloc;
}
else
{
v34 = (_QWORD *)RtlAllocateHeap(gpvSharedAlloc, 0i64, nObjectSize);
pObjectHead = v34;
if ( !v34 )
goto LABEL_76;
v34[3] = 0i64;
v34[4] = 0i64;
v34[6] = (char *)v34 - gpvSharedAlloc;
v34[5] = 0i64;
}
}
else
{
fUsePoolIfNoDesktop = !pDeskSrc2 && nCreateFlag & 0x20;
v11 = nObjectSize;
if ( nCreateFlag & 0x200 )
{
pObjectHead = HMAllocateIsolatedType();
}
else
{
v12 = gahti[nTypeOff].types;
if ( qword_1C025DBD0 && (signed int)qword_1C025DBD0() >= 0 )
{
if ( Win32AllocPoolImpl )
pObjectHead = (_QWORD *)Win32AllocPoolImpl(33i64, v11, v12);
else
pObjectHead = 0i64;
if ( !pObjectHead )
goto LABEL_76;
memset(pObjectHead, 0, v11);
}
else
{
pObjectHead = 0i64;
}
}
if ( !pObjectHead )
{
LABEL_76:
v33 = 8i64;
goto LABEL_77;
}
if ( (_BYTE)nObjectType == 1 ) // TYPE_WINDOW
{
v32 = Win32AllocPoolWithQuotaZInit(0x140ui64);
pObjectHead[5] = v32;
if ( !v32 )
{
HMFreeUserOrIsolatedType(nCreateFlag, 1u, pObjectHead);
pObjectHead = 0i64;
}
}
if ( nCreateFlag & 0x100 )
{
LockObjectAssignment(pObjectHead + 3, pDeskSrc2);
pObjectHead[4] = pObjectHead;
}
v6 = ptiOwner;
}
if ( !pObjectHead )
goto LABEL_76;
nHandleIndex = *pHandleIndex;
pHandleEntry = (PHANDLEENTRY)((char *)gSharedInfo.aheList + 0x20 * *pHandleIndex);// gSharedInfo+0x8
v16 = (unsigned int)nHandleIndex < giheLast;
v17 = (_DWORD)nHandleIndex == giheLast;
pKernelHandleEntry = (PKERNEL_HANDLENTRY)(gpKernelHandleTable + 24 * nHandleIndex);
*piheFree2 = (ULONG_PTR)pKernelHandleEntry->pObject;
if ( !v16 && !v17 )
giheLast = nHandleIndex;
v19 = bType;
pHandleEntry->bType = bType;
pKernelHandleEntry->pObject = pObjectHead;
if ( nCreateFlag & 0x40 )
{
pHandleEntry->phead = (PVOID)pObjectHead[6];
}
else if ( nCreateFlag & 0x10 && pDeskSrc )
{
pHandleEntry->phead = (PVOID)pObjectHead[6];
pHandleEntry->pDesktop = **(PVOID **)pDeskSrc[1];
}
else
{
pHandleEntry->phead = 0i64;
}
if ( fUsePoolIfNoDesktop )
pHandleEntry->bFlags |= 0x40u;
if ( nCreateFlag & 2 )
{
*((_DWORD *)pObjectHead + 4) = 0;
pKernelHandleEntry->pOwner = (PVOID)v6[0x34];
v20 = PsGetProcessId(*(_QWORD *)v6[0x34]);
v19 = bType;
pHandleEntry->nOwnerID = v20;
if ( nCreateFlag & 4 )
pObjectHead[3] = v6[0x34];
}
else if ( nCreateFlag & 1 )
{
pKernelHandleEntry->pOwner = v6;
v24 = PsGetThreadId(*v6, 0i64);
v19 = bType;
pHandleEntry->nOwnerID = v24;
pObjectHead[2] = pKernelHandleEntry->pOwner;
}
nHandleValue = (signed int)nHandleIndex | (unsigned __int64)(*(unsigned __int16 *)((char *)gSharedInfo.aheList
+ nHandleIndex * gSharedInfo.aheSize
+ 0x1A) << 16);
*pObjectHead = nHandleValue;
if ( gahti[nTypeOff].dwAllocSize )
{
v25 = (unsigned __int64 *)pObjectHead[5];
*v25 = nHandleValue;
v25[1] = pObjectHead[6];
}
if ( v4 )
{
v22 = ++*(_DWORD *)(v4 + 0x44);
if ( v22 > *(_DWORD *)(v4 + 0x48) )
*(_DWORD *)(v4 + 0x48) = v22;
}
if ( ++giheCount > (unsigned int)giheCountPeak )
giheCountPeak = giheCount;
if ( nCreateFlag & 3 )
{
PsGetProcessId(*(_QWORD *)v6[52]);
if ( Microsoft_Windows_Win32kEnableBits & 0x20000000000i64 )
{
v35 = GetEtwUserHandleType(bType);
McTemplateK0pqqq_EtwWriteTransfer(v36, (unsigned __int64)&UserCreateHandle, v37, *pObjectHead, v35);
}
}
else
{
v29 = GetEtwUserHandleType(v19);
EtwTraceUserCreateHandle(*pObjectHead, v29, 0i64);
}
result = pKernelHandleEntry->pObject;
pKernelHandleEntry->pUnknown = 0i64;
return result;
}