boxmoe_header_banner_img

Binary Security Researcher at ETH Zurich

文章导读

使用libbfd构建二进制加载器


avatar
ra1ny 2026-04-14 9

二进制文件描述符(Binary File Descriptor, BFD)库(libbfd)为读取和解析所有二进制格式提供了一个公共接口。

BFD库是GUN项目的一部分,并被binutils套件中的许多应用程序使用,包括objdump、readelf及GDB。该套件为二进制格式中使用的所有常见组件提供通用抽象,如描述二进制文件的目标和属性、节列表、重定位集及符号表等。在Ubuntu上,libbfd是binutils-dev软件包的一部分。

所以先安装

sudo apt update
sduo apt install binutils-dev

4.2 一个简单的二进制加载接口

#ifndef LOADER_H
#define LOADER_H

#include <stdint.h>   // 提供 uint64_t、uint8_t 等定宽整数类型
#include <string>     // 提供 std::string
#include <vector>     // 提供 std::vector

// 前向声明,用于类之间的相互引用
class Binary;
class Section;
class Symbol;

/**
 * @class Symbol
 * @brief 表示二进制文件中的一个符号(例如函数名、全局变量等)
 */
class Symbol {
public:
    /**
     * @enum SymbolType
     * @brief 符号的类型
     */
    enum SymbolType {
        SYM_TYPE_UKN = 0,   ///< 未知类型
        SYM_TYPE_FUNC = 1   ///< 函数符号
    };

    /**
     * @brief 默认构造函数,将符号初始化为未知类型,地址为0
     */
    Symbol() : type(SYM_TYPE_UKN), name(), addr(0) {}

    SymbolType  type;   ///< 符号类型
    std::string name;   ///< 符号名称
    uint64_t    addr;   ///< 符号在内存中的虚拟地址
};

/**
 * @class Section
 * @brief 表示二进制文件中的一个节(Section),例如 .text、.data 等
 */
class Section {
public:
    /**
     * @enum SectionType
     * @brief 节的内容类型
     */
    enum SectionType {
        SEC_TYPE_NONE = 0,  ///< 未定义类型
        SEC_TYPE_CODE = 1,  ///< 可执行代码节(如 .text)
        SEC_TYPE_DATA = 2   ///< 数据节(如 .data、.rodata)
    };

    /**
     * @brief 默认构造函数,将成员初始化为安全默认值
     */
    Section() : binary(NULL), type(SEC_TYPE_NONE), vma(0), size(0), bytes(NULL) {}

    /**
     * @brief 判断给定的虚拟地址是否位于当前节内部
     * @param addr 要检查的虚拟地址
     * @return 若地址在 [vma, vma+size) 范围内则返回 true,否则返回 false
     * @note 原清单中条件表达式缺漏,此处修正为 (addr - vma < size)
     */
    bool contains(uint64_t addr) const {
        return (addr >= vma) && (addr - vma < size);
    }

    Binary*     binary; ///< 指向所属 Binary 对象的指针
    std::string name;   ///< 节名称(如 ".text"、".data")
    SectionType type;   ///< 节类型
    uint64_t    vma;    ///< 节的起始虚拟内存地址(Virtual Memory Address)
    uint64_t    size;   ///< 节的大小(字节数)
    uint8_t*    bytes;  ///< 指向节原始字节内容的指针(可能由加载器分配)
};

/**
 * @class Binary
 * @brief 表示一个完整的二进制可执行文件(如 ELF、PE)
 */
class Binary {
public:
    /**
     * @enum BinaryType
     * @brief 二进制文件的格式类型
     */
    enum BinaryType {
        BIN_TYPE_AUTO = 0,  ///< 自动检测格式
        BIN_TYPE_ELF  = 1,  ///< ELF 格式(Linux、Unix)
        BIN_TYPE_PE   = 2   ///< PE 格式(Windows)
    };

    /**
     * @enum BinaryArch
     * @brief 二进制文件的目标架构
     */
    enum BinaryArch {
        ARCH_NONE = 0,  ///< 未知架构
        ARCH_X86  = 1   ///< x86 / x86-64 架构
    };

    /**
     * @brief 默认构造函数
     */
    Binary() : type(BIN_TYPE_AUTO), arch(ARCH_NONE), bits(0), entry(0) {}

    /**
     * @brief 获取代码节(.text)的指针
     * @return 若存在 .text 节则返回其指针,否则返回 NULL
     */
    Section* get_text_section() {
        for (auto &s : sections) {
            if (s.name == ".text")
                return &s;
        }
        return NULL;
    }

    std::string          filename;   ///< 二进制文件的路径
    BinaryType           type;       ///< 文件格式类型
    std::string          type_str;   ///< 文件格式的字符串描述(如 "ELF"、"PE")
    BinaryArch           arch;       ///< CPU 架构
    std::string          arch_str;   ///< 架构的字符串描述(如 "x86")
    unsigned             bits;       ///< 字长(32 或 64)
    uint64_t             entry;      ///< 程序入口点的虚拟地址
    std::vector<Section> sections;   ///< 所有节的列表
    std::vector<Symbol>  symbols;    ///< 所有符号的列表
};

/**
 * @brief 加载二进制文件并解析其结构
 * @param fname 文件路径(输入)
 * @param bin   指向 Binary 对象的指针,解析结果将填充至此对象
 * @param type  期望的二进制格式类型(可用 BIN_TYPE_AUTO 自动检测)
 * @return 成功返回 0,失败返回非 0 错误码
 */
int load_binary(std::string &fname, Binary *bin, Binary::BinaryType type);

/**
 * @brief 释放与已加载二进制文件相关的资源
 * @param bin 指向要卸载的 Binary 对象
 */
void unload_binary(Binary *bin);

#endif /* LOADER_H */

主要的类:
Binary类是根类,表示整个二进制文件的抽象类。
Section类和 Symbol类分别表示二进制文件中包含的节和符号。

主要的两个函数:
laod_binary,其使用要加载的二进制文件和名称 fname 指向一个二进制对象,该对象包含加载的二进制文件以及二进制类型描述符 type。函数将请求的二进制加载到bin参数。
unload_binary,其得到指向先前加载Binary对象的指针,然后将其卸载。

4.2.1 Binary类

其中的一些属性包括

    std::string          filename;   ///< 二进制文件的路径
    
    // 双重表示形式,包含数字类型标识符,同时type_str包含二进制类型的字符串表示形式。
    BinaryType           type;       ///< 文件格式类型
    std::string          type_str;   ///< 文件格式的字符串描述(如 "ELF"、"PE")
    
    // 体系结构使用相同的双重表示形式
    BinaryArch           arch;       ///< CPU 架构
    std::string          arch_str;   ///< 架构的字符串描述(如 "x86")
    
    unsigned             bits;       ///< 字长(32 或 64)
    uint64_t             entry;      ///< 程序入口点的虚拟地址
    std::vector<Section> sections;   ///< 所有节的列表
    std::vector<Symbol>  symbols;    ///< 所有符号的列表

二进制的有效类型在BinaryType枚举体中定义

    /**
     * @enum BinaryType
     * @brief 二进制有效类型
     */
    enum BinaryType {
        BIN_TYPE_AUTO = 0,  ///< 自动检测格式
        BIN_TYPE_ELF  = 1,  ///< ELF 格式(Linux、Unix)
        BIN_TYPE_PE   = 2   ///< PE 格式(Windows)
    };

    /**
     * @enum BinaryArch
     * @brief 二进制文件的目标架构
     */
    enum BinaryArch {
        ARCH_NONE = 0,  ///< 未知架构
        ARCH_X86  = 1   ///< x86 / x86-64 架构
    };

在这个文件的例子中,唯一有效的体系结构是ARCH_X86,包括x86和x86-64,两者之间的区别是Binary类的bit成员决定的。对于x86,bit被设置为32为;对于x86-64,bit被设置为64位。

4.2.2 Section类

节由section类型的对象表示。Section类是味道节的主要属性的简单封装

    Binary*     binary; ///< 指向所属 Binary 对象的指针
    std::string name;   ///< 节名称(如 ".text"、".data")
    SectionType type;   ///< 节类型
    uint64_t    vma;    ///< 节的起始虚拟内存地址(Virtual Memory Address)
    uint64_t    size;   ///< 节的大小(字节数)
    uint8_t*    bytes;  ///< 指向节原始字节内容的指针(可能由加载器分配)

其中,节的类型由枚举体SectionType的值表示,该值告诉你该段包含的是代码段还是数据段。

    enum SectionType {
        SEC_TYPE_NONE = 0,  ///< 未定义类型
        SEC_TYPE_CODE = 1,  ///< 可执行代码节(如 .text)
        SEC_TYPE_DATA = 2   ///< 数据节(如 .data、.rodata)
    };

4.2.3 Symbol类

二进制文件包含许多组件类型的符号,包括局部和全局变量、函数、重定位表达式及对象等。
为了简单,这里加载器接口只公开一种符号:函数符号,加载器使用Symbol类表示符号,该类包含一个符号类型,表示为SymbolType枚举体,其唯一的有效值为

    enum SymbolType {
        SYM_TYPE_UKN = 0,   ///< 未知类型
        SYM_TYPE_FUNC = 1   ///< 函数符号
    };

另外,该类还包含符号名,以及符号描述的函数起始地址。

    SymbolType  type;   ///< 符号类型
    std::string name;   ///< 符号名称
    uint64_t    addr;   ///< 函数起始地址

4.3 实现二进制加载器

所有使用libbfd的程序都需要包含bfd.h头文件,如清单4-2所示,并通过指定链接器标志-lbfd来链接libbfd。除了链接libbfd,加载器还需要包含4.2节中创建接口的头文件。

#include <bfd.h>
#include "loader.h"

下面主要两个函数

int load_bianry(std::string &fname, Binary *bin, Binary::BinaryType type) {
	return load_binary_bfd(fname, bin, type);
}

void unload_bianry(Binary *bin) {
	size_t i;
	Section *sec;
	
	for(i = 0; i < bin->sections.size(); i++) {
		sec = &bin->sections[i];
		if(sec->bytes) {
			free(sec->bytes);
		}
	}
}

4.3.1 初始化libbfd打开二进制文件。

/**
 * @brief 使用 libbfd 打开指定的二进制文件,并验证其格式
 * @param fname 文件路径
 * @return 成功返回 bfd 句柄,失败返回 NULL
 */
static bfd* open_bfd(std::string &fname) {
    static int bfd_inited = 0;
    bfd *bfd_h;

    // libbfd 只需初始化一次
    if (!bfd_inited) {
        bfd_init();
        bfd_inited = 1;
    }

    // 打开二进制文件,第二个参数为 NULL 表示让 libbfd 自动检测格式
    bfd_h = bfd_openr(fname.c_str(), NULL);
    if (!bfd_h) {
        fprintf(stderr, "failed to open binary '%s' (%s)\n",
                fname.c_str(), bfd_errmsg(bfd_get_error()));
        return NULL;
    }

    // 检查文件是否为一个可识别的对象文件(可执行文件、目标文件或共享库)
    if (!bfd_check_format(bfd_h, bfd_object)) {
        fprintf(stderr, "file '%s' does not look like an executable (%s)\n",
                fname.c_str(), bfd_errmsg(bfd_get_error()));
        bfd_close(bfd_h);
        return NULL;
    }

    // 某些版本的 bfd_check_format 会残留错误状态,手动清除以避免后续问题
    bfd_set_error(bfd_error_no_error);

    // 进一步确认文件格式是否为已知类型(ELF、COFF/PE 等)
    if (bfd_get_flavour(bfd_h) == bfd_target_unknown_flavour) {
        fprintf(stderr, "unrecognized format for binary '%s' (%s)\n",
                fname.c_str(), bfd_errmsg(bfd_get_error()));
        bfd_close(bfd_h);
        return NULL;
    }

    return bfd_h;
}

4.3.2 解析基础二进制属性

/**
 * @brief 核心加载函数:解析二进制文件并填充 Binary 对象
 */
static int load_binary_bfd(std::string &fname, Binary *bin, Binary::BinaryType type) {
    int ret = -1;
    bfd *bfd_h = NULL;
    const bfd_arch_info_type *bfd_info;

    // 打开二进制文件
    bfd_h = open_bfd(fname);
    if (!bfd_h) {
        goto fail;
    }

    // 复制文件名
    bin->filename = std::string(fname);

    // 获取程序入口点地址
    bin->entry = bfd_get_start_address(bfd_h);

    // 获取二进制文件格式的字符串描述(例如 "elf64-x86-64")
    bin->type_str = std::string(bfd_h->xvec->name);

    // 根据 libbfd 探测到的格式类型设置 Binary::BinaryType
    switch (bfd_h->xvec->flavour) {
    case bfd_target_elf_flavour:
        bin->type = Binary::BIN_TYPE_ELF;
        break;
    case bfd_target_coff_flavour:
        bin->type = Binary::BIN_TYPE_PE;
        break;
    case bfd_target_unknown_flavour:
    default:
        fprintf(stderr, "unsupported binary type (%s)\n", bfd_h->xvec->name);
        goto fail;
    }

    // 获取体系结构信息
    bfd_info = bfd_get_arch_info(bfd_h);
    bin->arch_str = std::string(bfd_info->printable_name);

    // 根据机器类型设置架构和位宽
    switch (bfd_info->mach) {
    case bfd_mach_i386_i386:
        bin->arch = Binary::ARCH_X86;
        bin->bits = 32;
        break;
    case bfd_mach_x86_64:
        bin->arch = Binary::ARCH_X86;
        bin->bits = 64;
        break;
    default:
        fprintf(stderr, "unsupported architecture (%s)\n", bfd_info->printable_name);
        goto fail;
    }

    // 加载符号表(静态和动态),即使失败也尽力而为,不影响主流程
    load_symbols_bfd(bfd_h, bin);
    load_dynsym_bfd(bfd_h, bin);

    // 加载节信息(关键步骤)
    if (load_sections_bfd(bfd_h, bin) < 0) {
        goto fail;
    }

    ret = 0; // 成功

fail:
    if (bfd_h) {
        bfd_close(bfd_h);
    }
    return ret;
}

4.3.3 加载符号

// ---------- 加载静态符号表 ----------

/**
 * @brief 从静态符号表中提取函数符号
 */
static int load_symbols_bfd(bfd *bfd_h, Binary *bin) {
    long storage_needed;
    asymbol **bfd_symtab = NULL;
    long nsyms, i;

    // 获取存储所有符号指针所需的字节数
    storage_needed = bfd_get_symtab_upper_bound(bfd_h);
    if (storage_needed < 0) {
        fprintf(stderr, "failed to read symtab (%s)\n", bfd_errmsg(bfd_get_error()));
        goto fail;
    }
    if (storage_needed == 0) {
        // 没有符号表,直接返回成功
        return 0;
    }

    // 分配内存
    bfd_symtab = (asymbol**)malloc(storage_needed);
    if (!bfd_symtab) {
        fprintf(stderr, "out of memory\n");
        goto fail;
    }

    // 实际读取符号表
    nsyms = bfd_canonicalize_symtab(bfd_h, bfd_symtab);
    if (nsyms < 0) {
        fprintf(stderr, "failed to read symtab (%s)\n", bfd_errmsg(bfd_get_error()));
        goto fail;
    }

    // 遍历符号表,仅保留函数类型的符号
    for (i = 0; i < nsyms; i++) {
        if (bfd_symtab[i]->flags & BSF_FUNCTION) {
            bin->symbols.push_back(Symbol());
            Symbol *sym = &bin->symbols.back();
            sym->type = Symbol::SYM_TYPE_FUNC;
            sym->name = std::string(bfd_symtab[i]->name);
            sym->addr = bfd_asymbol_value(bfd_symtab[i]);
        }
    }

    free(bfd_symtab);
    return 0;

fail:
    if (bfd_symtab) free(bfd_symtab);
    return -1;
}

// ---------- 加载动态符号表 ----------

/**
 * @brief 从动态符号表中提取函数符号,流程与静态符号表类似
 */
static int load_dynsym_bfd(bfd *bfd_h, Binary *bin) {
    long storage_needed;
    asymbol **bfd_dynsym = NULL;
    long nsyms, i;

    storage_needed = bfd_get_dynamic_symtab_upper_bound(bfd_h);
    if (storage_needed < 0) {
        fprintf(stderr, "failed to read dynamic symtab (%s)\n", bfd_errmsg(bfd_get_error()));
        goto fail;
    }
    if (storage_needed == 0) {
        return 0; // 没有动态符号表
    }

    bfd_dynsym = (asymbol**)malloc(storage_needed);
    if (!bfd_dynsym) {
        fprintf(stderr, "out of memory\n");
        goto fail;
    }

    nsyms = bfd_canonicalize_dynamic_symtab(bfd_h, bfd_dynsym);
    if (nsyms < 0) {
        fprintf(stderr, "failed to read dynamic symtab (%s)\n", bfd_errmsg(bfd_get_error()));
        goto fail;
    }

    for (i = 0; i < nsyms; i++) {
        if (bfd_dynsym[i]->flags & BSF_FUNCTION) {
            bin->symbols.push_back(Symbol());
            Symbol *sym = &bin->symbols.back();
            sym->type = Symbol::SYM_TYPE_FUNC;
            sym->name = std::string(bfd_dynsym[i]->name);
            sym->addr = bfd_asymbol_value(bfd_dynsym[i]);
        }
    }

    free(bfd_dynsym);
    return 0;

fail:
    if (bfd_dynsym) free(bfd_dynsym);
    return -1;
}

4.3.4 加载节信息

// ---------- 加载节(Section)信息 ----------

/**
 * @brief 遍历二进制文件的所有节,复制代码节和数据节的内容到 Binary 对象
 */
static int load_sections_bfd(bfd *bfd_h, Binary *bin) {
    asection *bfd_sec;
    Section::SectionType sectype;

    // libbfd 通过链表管理节,从 bfd_h->sections 开始遍历
    for (bfd_sec = bfd_h->sections; bfd_sec != NULL; bfd_sec = bfd_sec->next) {
        // 获取节的标志位
        int bfd_flags = bfd_get_section_flags(bfd_h, bfd_sec);

        // 只关心代码段和数据段
        if (bfd_flags & SEC_CODE) {
            sectype = Section::SEC_TYPE_CODE;
        } else if (bfd_flags & SEC_DATA) {
            sectype = Section::SEC_TYPE_DATA;
        } else {
            continue; // 忽略其他类型的节(如调试信息、符号表等)
        }

        // 获取节的虚拟地址、大小和名称
        uint64_t vma   = bfd_section_vma(bfd_h, bfd_sec);
        uint64_t size  = bfd_section_size(bfd_h, bfd_sec);
        const char *secname = bfd_section_name(bfd_h, bfd_sec);
        if (!secname) secname = "<unnamed>";

        // 在 Binary 对象中创建对应的 Section 条目
        bin->sections.push_back(Section());
        Section *sec = &bin->sections.back();

        sec->binary = bin;
        sec->name   = std::string(secname);
        sec->type   = sectype;
        sec->vma    = vma;
        sec->size   = size;
        sec->bytes  = (uint8_t*)malloc(size);
        if (!sec->bytes) {
            fprintf(stderr, "out of memory\n");
            return -1;
        }

        // 将节的原始内容复制到分配的内存中
        if (!bfd_get_section_contents(bfd_h, bfd_sec, sec->bytes, 0, size)) {
            fprintf(stderr, "failed to read section '%s' (%s)\n",
                    secname, bfd_errmsg(bfd_get_error()));
            return -1;
        }
    }
    return 0;
}


评论(0)

查看评论列表

暂无评论


发表评论