Coff Loader 第一部分:Coff的解析

admin 2024年3月4日13:11:18评论18 views字数 4192阅读13分58秒阅读模式

前言

自 2020 年 6 月发布的 4.1 版本起,Cobalt Strike 引入了一项新功能,试图通过引入信标对象文件 (BOF) 来解决之前使用fork and run运行模块所产生OPSEC问题。BOF实际上就是windows编译过程中产生的中间文件(OBJ),这个中间文件使用COFF(Common of file format)文件格式,我们使用dumpbin查看obj文件格式时可以看到这一点:

Coff Loader 第一部分:Coff的解析

TrustedSec在2021年2月份实现的CoffLoader可以加载并运行COFF文件,为了更加深入了解COFF文件以及如何运行COFF,这个系列的文章意在实现一个类似CoffLoader的加载器,第一部分将先实现一个Coff解析工具。解析文件格式向来都是枯燥乏味的,且涉及各种结构字段,所以本文力求在不堆砌大量代码的前提下以实例和图解的方式来理解COFF格式。

友情提示https://github.com/jseclab/wechat_public/tree/main/bof/p1

代码使用纯C实现,支持多平台编译。

正文

PE文件格式衍生自Common Object File Format(COFF),所以他俩的文件格式具有诸多相似之处。这一点从微软对于PE和COFF文件格式的描述中可见一斑。

关于为何如此相似,这一原因大概在于Windows NT初始团队成员有很多来自Digital Equipment Corporation(DEC),而COFF文件格式正是用于DEC推出的VAX/VMS上。关于什么是VAX/VMS,ChatGPT如是说:

VAX/VMS是指Digital Equipment Corporation(DEC)推出的一系列计算机系统和操作系统。VAX代表“Virtual Address eXtension”,是DEC推出的一种计算机体系结构。VMS则代表“Virtual Memory System”,是VAX计算机上运行的操作系统。

相较于PE的文件格式COFF就简单了很多,很多东西都是

Coff Loader 第一部分:Coff的解析

文件头(file header)

相较于PE文件头,COFF格式文件头只有COFF FILE HEADER

Coff Loader 第一部分:Coff的解析所以对于文件头,我们只需要定义一个COFF FILE HEADER结构体用于解析,由于COFF没有可选头,所以SizeOfOptionalHeader应当为零。

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;

节表(section table)

紧随其后的是节表(section table),用于描述每个节的名称,位置,大小,属性等内容,属PE和COFF共有内容。不同的点在于节的名称,COFF独有的节组概念,如果大家看过我的这篇文章应该知道 $ 符号在节名中有特殊解释,比如.text$A,.text$B等,连接器连接时会丢弃 $ 后面的内容,统一将两个节划分为.text节,但是后面的字符决定了COFF中的这些节最终在PE中存放顺序,如上B最终会放在A之后。

依然按照老规矩定义节结构体用于解析,但对于COFF来说有些字段是可以设置为零的,比如其中的VirtualSizeVirtualAddress以及和不再使用的行号相关的

typedef struct coff_sec_header {
    char Name[8];
    uint32_t VirtualSize;   // zero for object files.
    uint32_t VirtualAddress;  // compilers should set this to zero
    uint32_t SizeOfRawData;   // When a section contains only uninitialized data, this field should be zero.
    uint32_t PointerToRawData;  // When a section contains only uninitialized data, this field should be zero.
    uint32_t PointerToRelocations;
    uint32_t PointerToLinenumbers;
    uint16_t NumberOfRelocations;
    uint16_t NumberOfLinenumbers; // COFF Line Numbers (Deprecated)
    uint32_t Characteristics;

}coff_sec_header_t;

补充:关于节名是一个8字节数组,PE是不支持长度超过8字节的节名的,但是COFF支持,如果节名长度超出了8字节,则此字段的表示位 / (十进制ASCII码) ,举个例子 / 32 36 34 32代表2 36代表6 34代表4 合起来就是264,表示在字符串表中的偏移,根据这个偏移就可以找到节名,但我们使用 cl 生成的obj文件一般不会有超过8字节的,如果你想看看的话可以尝试在函数前加上__declspec(code_seg(".abcdefghijklmn")),名字自己设置

比如如下代码

#include <stdio.h>

__declspec(code_seg(".abcdefghijklmn"))
void my_printf()
{
    printf("hello world");
}

int main()
{
    my_printf();
    return 0;
}

节数据(section data)

这点没什么好说的,可以直接根据节表中的PointerToRawDataSizeOfRawData来输出就好了

节重定位(section reloc)

重定位表紧跟节数据之后,有三个字段构成共计10个字节

typedef struct coff_reloc {
    uint32_t VirtualAddress;
    uint32_t SymbolTableIndex;
    uint16_t Type;
}coff_reloc_t;

VirtualAddres:节内偏移

SymbolTableIndex:需要重定位符号在符号表中的偏移

Type:重定位类型

如下图中字符串hello world,其符号为$SG9015以及函数printf需要被重定位

Coff Loader 第一部分:Coff的解析

自动化解析得到各个字段值如下:

Coff Loader 第一部分:Coff的解析

0x7:从节起始处偏移7个字节,指向25 01 00 00 位置,也就是需要重定位的hello world

49:符号在符号表中的偏移,如下图

0x4:重定位类型,决定了重定位的值如何计算,比如 25 01 00 00 其实是紧随重定位后的地址相对于符号所在地址的偏移hello world地址位0x130,紧随重定位地址之后的是0x0B,那么需重定位处值应为0x130-0xB = 0x125

IMAGE_REL_AMD64_REL32 0x0004 The 32-bit relative address from the byte following the relocation.

Coff Loader 第一部分:Coff的解析

符号表(symbol table)

符号表字段众多且字段之间含义有的相互关联,虽然微软文档中有明确的说明,但是在解析过程中依然碰到不符的字段值,尚不明白这是为什么,但是先行判断StorageClass是否为零则可以规避这些

typedef struct coff_sym {
    union {
        char Name[8];
        struct {
            uint32_t AllZero;
            uint32_t Offset;
        }ln;
    } sn;
    uint32_t Value;
    uint16_t SectionNumber;
    uint16_t Type;    // microsoft tools set this field to 0x20(function) or 0x0(not a function)
    uint8_t StorageClass;
    uint8_t NumberOfAuxSymbols;

} coff_sym_t;

符号名:使用联合体表示的原因是名称长度可能超过8字节,如果是短名称则直接按照字符串存储,否则AllZero字段全零表示名称超过8字节,应当使用字符串表地址加上Offset来查找

Value会根据StorageClass的不同而不同,比如下面.data $SG9015和.chks64,StorageClass的值都是3,如果Value的值不为零则代表符号在节中的偏移,但是这里是零,则代表这个符号就是节名,从这里可以看到 .data 和 $SG9015都是节13的名字

Coff Loader 第一部分:Coff的解析

SectionNumber:如果大于零则代表节的编号,否则有特殊含义,在文档中可见

Type:符号类型,一般是0x0(非函数类型)和0x20(函数类型)

Coff Loader 第一部分:Coff的解析

StorageClass:符号代表的含义,比如表示外部符号,代码的标签,静态数据等,不同的值导致Value的值也不同

所以综上所述,我们可以画出COFF文件的整体格式图了,如下:

Coff Loader 第一部分:Coff的解析

引用

TrustedSec CoffLoader:https://github.com/trustedsec/COFFLoader

sektor7 Coff Parser:https://blog.sektor7.net/res/2022/CaFeBiBa.c

原文始发于微信公众号(无名之):Coff Loader 第一部分:Coff的解析

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年3月4日13:11:18
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Coff Loader 第一部分:Coff的解析http://cn-sec.com/archives/2544478.html

发表评论

匿名网友 填写信息