COFF Loader及在C2中的实际应用
前言:COFF Loader及在C2中的实际应用
参考文章:https://github.com/trustedsec/COFFLoader
参考文章:https://github.com/BishopFox/sliver
参考文章:https://github.com/sliverarmory/COFFLoader
参考文章:https://github.com/praetorian-inc/goffloader
参考文章:https://otterhacker.github.io/Malware/CoffLoader.html
参考文章:https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#coff-file-header-object-and-image
参考文章:https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#storage-class
参考文章:https://www.cnblogs.com/mkmkbj/p/17942624
参考文章:https://github.com/Cracked5pider/CoffeeLdr
COFF 介绍
COFF 代表通用目标文件 (Common Object Files)。
COFF 格式最初用于 Linux ELF 可执行文件,但微软已使用多年。

恶意软件经常使用这种技术,因为该程序仅存在于内存中,从而限制了恶意软件的占用空间。此外,由于该程序完全在内存中执行,因此防病毒软件或 EDR 等检测解决方案更难以检测到并阻止其执行。
注意:实现COFF Loader的是Cobalt Strike,COFF所使用的是经过修改的程序,集成了可与信标交互的功能,CobaltStrike从而增强了名称Beacon Object File或简单的BOF。
COFF 结构
文件头(File Header)
https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#coff-file-header-object-and-image
注:coff文件头占20个字节
typedef struct coff_file_header {
uint16_t Machine;
uint16_t NumberOfSections;
uint32_t TimeDateStamp;
uint32_t PointerToSymbolTable;
uint32_t NumberOfSymbols;
uint16_t SizeOfOptionalHeader; // should be zero for an object file
uint16_t Characteristics;
}coff_file_header_t;
- Machine: 指明文件对应的机器类型,Windows中主要是:x86: 0x14c, IMAGE_FILE_MACHINE_I386x64: 0x8664, IMAGE_FILE_MACHINE_AMD64

- NumberOfSections:节区数量

- TimeDateStamp:文件生成时的时间戳

- PointerToSymbolTable: 符号表的位置(相对文件起始的偏移)

跟随到对应的0x49C的位置,如下图所示

- NumberOfSymbols:符号表中的符号个数

- SizeOfOptionalHeader:可选头,在Object文件中默认为0

- characteristics:文件属性


整体结构如下所示

节表(Section Header Table)
https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#section-table-section-headers
节表紧跟在文件头之后,包含多个CoffSectionHeader结构,每个CoffSectionHeader描述一个节区的信息。
注:每个节表大小占40个字节。
struct CoffSectionHeader {
char name[8]; // 节区名称
uint32_t virtual_size; // 虚拟内存大小,0,忽略
uint32_t virtual_address; // 虚拟地址,0,忽略
uint32_t size_of_raw_data; // 节区数据的大小
uint32_t pointer_to_raw_data; // 节区数据的偏移
uint32_t pointer_to_relocations; // 该节区的重定位表的偏移
uint32_t pointer_to_line_numbers; // 行号表偏移,忽略,已废弃
uint16_t number_of_relocations; // 重定位项数量
uint16_t number_of_line_numbers; // 行号项数量,忽略,已废弃
uint32_t characteristics; // 节区特征
};
- name: 节的名称

- virtual_size:该节加载到内存时的总大小,如果此值大于SizeOfRawData,则该节将以零填充。此字段仅对可执行映像有效,对于目标文件应设置为零。

- virtual_address:对于可执行映像,此字段表示该节加载到内存时,相对于映像基址的首字节地址。对于目标文件,此字段表示重定位前首字节的地址;为简单起见,编译器应将其设置为零。

- size_of_raw_data: 节的数据的大小。

- pointer_to_raw_data:节的数据的起始位置(相对偏移)。

- pointer_to_relocations: 节中包含的重定位项的起始位置(相对偏移)。

- pointer_to_line_numbers:节中包含的重定位项的个数。

- number_of_relocations:该段的重定位条目数。

- number_of_line_numbers:该节的行号条目数。对于图像,此值应为零,因为 COFF 调试信息已弃用。

- characteristics: 节映射到内存后的内存属性,比如代码节有可执行属性,数据节有读写属性等。


整体结构如下所示

节区数据(Section Data)
https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#section-data
节区数据包含实际的代码和数据内容,通过Coff Section Header中pointer_to_raw_data和size_of_raw_data定位到节区的位置和大小。
常见的节区包括:
-
.text:可执行代码
-
.data:已初始化的数据
-
.rdata:只读数据
-
.bss:未初始化数据
重定位表(Relocation Table)
https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#coff-relocations-object-only
注:每个重定位描述信息项占10个字节
struct CoffReloc {
uint32_t virtual_address; // 需要重定位的地址
uint32_t symbol_table_index; // 符号表索引
uint16_t type; // 重定位类型
};

- virtual_address:被应用重定位的项的地址。这是相对于节开头的偏移量,加上节的 RVA/偏移量字段的值。请参阅节表(节头)。例如,如果节的第一个字节的地址为 0x10,则第三个字节的地址为 0x12。

- symbol_table_index:符号表中从零开始的索引。此符号给出了用于重定位的地址。如果指定的符号具有节存储类别,则该符号的地址是第一个同名节的地址。

- type:指示应执行的重定位类型的值。有效的重定位类型取决于机器类型。请参阅类型指示符。

注意:关于type值的参考如下图所示



https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#coff-relocations-object-only

重定位表修复原理
假设,当前.text段起始位置为0x10,virtual_address的值为0x05,那么当前这个重定位项说明的就是定位信息就要写在0x15处。
综上所述抽象出来的话就是当前段+当前重定向项的virtual_address字段所指向内存地址是需要进行修复的地址。
注意:要修复的信息的长度要看你当前重定位条目的type字段类型,32位的代码要写4个字节,16位的就只要字2个字节,这个可以参考type字段的规定。
重定位表条目中的symbol_table_index字段的索引函数名称作用
该symbol_table_index字段是符号索引,这个成员指明了重定位信息所对映的符号。
注:这里是索引,不是偏移,它只是符号表中的一个记录的记录号。
symbol_table_index指向的符号名称总共分为两类,分别是如下
-
短符号名称
-
长符号名称
短符号名称



长符号名称


22*18=39c -> 18c
49c+18c=628 对应的值就是0x00350000

这里需要注意的是前四个字节的值为0,那么代表该符号名称为长符号名称
如果是长符号名称的情况的话,那么最终符号名称的定义
coff_sym_ptr+coff_header_ptr->NumberOfSymbols+coff_sym_ptr[coff_reloc_ptr->SymbolTableIndex].first.value[1]
进行相对应的数值代入换算结果如下
0x49c(符号表)+0x1a(当前coff文件总符号数量*0x18)+0x35(符号表[重定向符号索引].first.value[1]) -> 0x49c+0x1A*0x12+0x35=0x6a5

对应的位置也就是长符号名称的真实名称,如下图所示

注意:符号表+当前coff文件总符号数量*0x18的位置实际上就是符号字符串表的位置,如下图所示

符号表(Symbol Table)
https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#coff-symbol-table
注:每个符号表描述信息项占18字节
struct CoffSymbol {
union {
char name[8]; // 短名称(8字节以内)
uint32_t value[2]; // 长名称偏移(value[0]=0时,value[1]是字符串表中的偏移)
} first;
uint32_t value; // 符号值(节区内的偏移)
uint16_t section_number; // 节区号(0表示外部符号)
uint16_t type; // 符号类型
uint8_t storage_class; // 存储类
uint8_t number_of_aux_symbols; // 辅助符号数量
};
- Name:符号名称,由三个结构体的联合体表示。如果名称长度不超过 8 个字节,则使用 8 字节数组。

- Value:与符号关联的值,此字段的解释取决于
SectionNumber和StorageClass,典型含义是可重定位地址。

- SectionNumber:一个有符号整数,用于标识该区段,并使用区段表中从1开始的索引。某些值具有特殊含义,

- Type:表示类型的数字。Microsoft 工具将此字段设置为 0x20(函数)或 0x0(非函数)。

- StorageClass:表示存储类别的枚举值。

- NumberOfAuxSymbols:该记录后面的辅助符号表条目的数量。

注:这里最关键的其实就是标准符合和特殊符号
-
标准符号:可以直接执行重定位的符号,例如标准初始化变量或内部函数。
-
特殊符号:在重新分配之前必须进行预处理的符号,例如未初始化的变量或外部函数,类似
printf函数

标准符号
标准初始化变量或内部函数。
特殊符号
特殊符号需要满足下面两个判定条件:
-
sectionNumber该符号的值为 0 -
StorageClass的值为IMAGE_SYM_CLASS_EXTERNAL(0x0002)或者是IMAGE_SYM_CLASS_EXTERNAL_DEF(0x0005)
如下图所示,显示sectionNumber该符号的值为 0,说明当前符号无法解析,因为不可能在其中一个COFF文件中找到它的地址,同时IMAGE_SYM_CLASS_EXTERNAL(0x0002)或者是IMAGE_SYM_CLASS_EXTERNAL_DEF(0x0005)

当前对应特殊符号信息,如下图所示

代码实现重点
外部符号的处理
主要就是要能够区分可以直接重定位的标准符号和必须先进行预处理才能重定位的非标准符号。
非标准符号实际上是COFF文件中无法直接解析的符号。这通过未定义的节索引(结构体sectionNumber中的值设置为 0 CoffSymbol)和IMAGE_SYM_CLASS_EXTERNAL存储类别来实现。
- 标准符合
- 非标准符合
-
- 外部函数
-
- 未初始化的变量
// pseudo code
if(coffSymbol->storageClass == IMAGE_SYM_CLASS_EXTERNAL && coffSymbol->sectionIndex == 0){
// process non standard symbol
}
一旦检测到非标准符号,就必须区分外部函数和未初始化的变量。
外部函数符号的名称非常容易识别,因为它总是以__imp_开头,如果符号名称以这种模式开头,则可以假定它代表一个函数。
//pseudo code
char* symbolName = resolveSymbolName(coffSymbol);
if(strncmp(symbolName, "__imp_", 6) == 0){
// process the function
}
else{
// process the uninitialized variable
}
未初始化变量符号的处理相当简单,但对于函数来说,则需要更多工作。事实上,为了解析共享库中的函数,必须知道库名和函数名。
然而,在普通COFF文件中,函数符号只包含函数名(即__imp_printf)。这可以通过Dynamic Function Resolution约定来解决。
注:DFR为外部函数定义和名称设置特定的语法
以下代码显示了该Hello world程序的使用DFR:
DECLSPEC_IMPORT int __cdecl MSVCRT$printf(const char* test, ...);
int main(void){
MSVCRT$printf("Hello World !\n");
}
在这个约定中,库名称添加到函数名称中。编译后,printf符号将看起来像__imp_MSVCRT$printf。
此语法解决了所有问题,因为共享库的名称包含在符号名称中。然后可以像这样解析该函数:
// pseudo code
// char* symbolName : the symbol name
// Remove the __imp_
symbolName += 6;
char *splittedName = symbolName.split('$');
char *libraryName = splittedName[0];
char *functionName = splittedName[1];
void *functionAddress = GetProcAddress(GetModuleHandle(libraryName), functionName);
Cobalt Strike CoFFLoader的实现
测试执行文件:https://github.com/trustedsec/COFFLoader/blob/main/test.c
执行x86_64-w64-mingw32-gcc -c test.c -o test64.out命令,将.c文件编译对应的文件为.o文件
#include <windows.h>
#include <stdio.h>
#include <lm.h>
#include <dsgetdc.h>
#include "beacon.h"
#ifdef __cplusplus
#if defined(_MSC_VER) || defined(__GNUC__)
#define restrict __restrict
#else
#define restrict
#endif // defined(_MSC_VER) || defined(__GNUC__)
#endif // __cplusplus
DECLSPEC_IMPORT DWORD WINAPI NETAPI32$DsGetDcNameA(LPVOID, LPVOID, LPVOID, LPVOID, ULONG, LPVOID);
DECLSPEC_IMPORT DWORD WINAPI NETAPI32$NetApiBufferFree(LPVOID);
WINBASEAPI int __cdecl MSVCRT$printf(const char *restrict _Format,...);
char* TestGlobalString = "This is a global string";
/* Can't do stuff like "int testvalue;" in a coff file, because it assumes that
* the symbol is like any function, so you would need to allocate a section of bss
* (without knowing the size of it), and then resolve the symbol to that. So safer
* to just not support that */
int testvalue = 0;
int test(void){
MSVCRT$printf("Test String from test\n");
testvalue = 1;
return 0;
}
int test2(void){
MSVCRT$printf("Test String from test2\n");
return 0;
}
void go(char * args, unsigned long alen) {
DWORD dwRet;
PDOMAIN_CONTROLLER_INFO pdcInfo;
BeaconPrintf(1, "This GlobalString \"%s\"\n", TestGlobalString);
MSVCRT$printf("Test Value: %d\n", testvalue);
(void)test();
MSVCRT$printf("Test ValueBack: %d\n", testvalue);
(void)test2();
dwRet = NETAPI32$DsGetDcNameA(NULL, NULL, NULL, NULL, 0, &pdcInfo);
if (ERROR_SUCCESS == dwRet) {
MSVCRT$printf("%s", pdcInfo->DomainName);
}
NETAPI32$NetApiBufferFree(pdcInfo);
}

接着通过COFFLoader64.exe文件进行解析执行该object文件

解析符号表的核心逻辑
解析符号表核心逻辑:https://github.com/trustedsec/COFFLoader/blob/main/COFFLoader.c
通过objdump查看符号表的情况,其中对于这种外部符号表的函数正常情况下是无法解析到的,如下图所示
[ 22](sec 0)(fl 0x00)(ty 0)(scl 2) (nx 0) 0x0000000000000000 __imp_MSVCRT$printf
[ 23](sec 0)(fl 0x00)(ty 0)(scl 2) (nx 0) 0x0000000000000000 __imp_BeaconPrintf
[ 24](sec 0)(fl 0x00)(ty 0)(scl 2) (nx 0) 0x0000000000000000 __imp_NETAPI32$DsGetDcNameA
[ 25](sec 0)(fl 0x00)(ty 0)(scl 2) (nx 0) 0x0000000000000000 __imp_NETAPI32$NetApiBufferFree

在Cobalt Strike中就用了比较巧妙的方法
/* Helper function to process a symbol string, determine what function and
* library its from, and return the right function pointer. Will need to
* implement in the loading of the beacon internal functions, or any other
* internal functions you want to have available. */
void* process_symbol(char* symbolstring) {
void* functionaddress = NULL;
char localcopy[1024] = { 0 };
char* locallib = NULL;
char* localfunc = NULL;
#if defined(_WIN32)
int tempcounter = 0;
HMODULE llHandle = NULL;
#endif
strncpy(localcopy, symbolstring, sizeof(localcopy) - 1);
if (starts_with(symbolstring, PREPENDSYMBOLVALUE"Beacon") || starts_with(symbolstring, PREPENDSYMBOLVALUE"toWideChar") ||
starts_with(symbolstring, PREPENDSYMBOLVALUE"GetProcAddress") || starts_with(symbolstring, PREPENDSYMBOLVALUE"LoadLibraryA") ||
starts_with(symbolstring, PREPENDSYMBOLVALUE"GetModuleHandleA") || starts_with(symbolstring, PREPENDSYMBOLVALUE"FreeLibrary") ||
starts_with(symbolstring, "__C_specific_handler")) {
if(strcmp(symbolstring, "__C_specific_handler") == 0)
{
localfunc = symbolstring;
return InternalFunctions[29][1];
}
else
{
localfunc = symbolstring + strlen(PREPENDSYMBOLVALUE);
}
DEBUG_PRINT("\t\tInternalFunction: %s\n", localfunc);
/* TODO: Get internal symbol here and set to functionaddress, then
* return the pointer to the internal function*/
#if defined(_WIN32)
for (tempcounter = 0; tempcounter < 30; tempcounter++) {
if (InternalFunctions[tempcounter][0] != NULL) {
if (starts_with(localfunc, (char*)(InternalFunctions[tempcounter][0]))) {
functionaddress = (void*)InternalFunctions[tempcounter][1];
return functionaddress;
}
}
}
#endif
}
else if (strncmp(symbolstring, PREPENDSYMBOLVALUE, strlen(PREPENDSYMBOLVALUE)) == 0) {
DEBUG_PRINT("\t\tYep its an external symbol\n");
locallib = localcopy + strlen(PREPENDSYMBOLVALUE);
locallib = strtok(locallib, "$");
localfunc = strtok(NULL, "$");
DEBUG_PRINT("\t\tLibrary: %s\n", locallib);
localfunc = strtok(localfunc, "@");
DEBUG_PRINT("\t\tFunction: %s\n", localfunc);
/* Resolve the symbols here, and set the functionpointervalue */
#if defined(_WIN32)
llHandle = LoadLibraryA(locallib);
DEBUG_PRINT("\t\tHandle: 0x%lx\n", llHandle);
functionaddress = GetProcAddress(llHandle, localfunc);
DEBUG_PRINT("\t\tProcAddress: 0x%p\n", functionaddress);
#endif
}
return functionaddress;
}

sliver中的COFFLoader的实现
实现代码位于:x
可以看到x
havoc中的COFFLoader的实现
实现代码位于:x
可以看到x

浙公网安备 33010602011771号