Linux ELF二进制文件数字签名工具:原理与设计思路(C/C++代码实现)

在软件安全领域,确保二进制文件的完整性与真实性是防御恶意篡改的关键环节。本文将介绍一款名为elf-sign的工具,它通过给Linux ELF(Executable and Linkable Format)二进制文件添加数字签名,实现对文件完整性的保护。我们将从工具的核心功能、实现原理、设计思路及相关领域知识展开分析,揭示其在软件安全体系中的作用。

一、工具的核心功能与意义

ELF是Linux系统中最常用的二进制文件格式(包括可执行文件、共享库等),其结构中包含代码段、数据段等关键信息。elf-sign的核心功能是为ELF文件的关键代码段添加数字签名,并将签名嵌入文件中,形成"自包含"的带签名二进制文件。

具体来说,它的作用体现在两个方面:

  1. 完整性保护 :通过数字签名确保ELF文件的关键代码段(如.text节,存放可执行代码)未被恶意篡改;
  2. 可验证性 :配合内核验证模块,在程序执行(如execve系统调用)时自动验证签名,若验证失败则阻止执行,从源头阻断篡改后的恶意程序运行。

这种机制在安全敏感场景(如服务器、嵌入式设备)中尤为重要,可有效防御病毒植入、代码注入等攻击。

二、核心实现原理

elf-sign的功能实现基于两大技术支柱:数字签名技术ELF文件格式解析。二者的结合确保了签名的生成、嵌入与后续验证的可行性。

1. 数字签名:确保"不可伪造"与"可验证"

数字签名是不对称加密技术的典型应用,其核心流程包括"签名生成"与"签名验证"两个阶段,elf-sign专注于前者:

  • 签名生成流程

    1. 哈希摘要 :对ELF文件中需要保护的关键部分(如.text节的代码)使用哈希算法(如SHA256)生成固定长度的摘要。哈希算法的特性确保:即使原始数据有微小改动,摘要也会完全不同;
    2. 私钥加密:使用私钥对摘要进行加密,生成"数字签名"。私钥由用户保管,确保只有合法持有者能生成有效签名;
    3. 签名存储:将生成的签名以特定格式(如PKCS#7或CMS,根据OpenSSL版本选择)保存,后续嵌入ELF文件。
  • 验证逻辑(内核端)

    验证时,内核使用对应的公钥(预先编译进内核)解密签名得到摘要,同时对当前文件的关键部分重新计算哈希。若两个摘要一致,则说明文件未被篡改。

2. ELF文件解析:签名的"无缝嵌入"

为了让签名与原始ELF文件共存且不影响其正常执行,elf-sign需要深入解析ELF格式,实现签名的"无缝嵌入"。ELF文件的核心结构包括:

  • ELF头:描述文件整体信息(如位数、字节序、节头表位置);
  • 节头表 :每个条目对应一个"节"(Section,如.text.data),记录节的名称、位置、大小等;
  • 字符串表 :存储节名等字符串(如.shstrtab存储节头名称)。

elf-sign的嵌入逻辑如下:

  1. 定位关键节 :解析ELF头与节头表,找到需要保护的.text节(代码段);
  2. 生成签名文件 :对.text节内容签名,生成临时签名文件(如.text_sig);
  3. 新增签名节 :在ELF文件中新增一个名为.text_sig的节,将签名数据存入其中;
  4. 更新ELF结构 :调整节头表(增加新节的条目)、字符串表(添加.text_sig节名),并保证所有节的对齐方式、偏移量等符合ELF规范,避免破坏文件的可执行性。

三、设计思路:平衡安全性与兼容性

c 复制代码
...
struct elf_signature {
	uint8_t		algo;		/* 公钥密码算法[0] */
	uint8_t		hash;		/* 摘要算法[0] */
	uint8_t		id_type;	/* 密钥标识符类型 [PKEY_ID_PKCS7] */
	uint8_t		signer_len;	/* 签名者姓名长度[0] */
	uint8_t		key_id_len;	/* 密钥标识符的长度[0] */
	uint8_t		__pad[3];
	uint32_t	sig_len;	/* 签名数据长度 */
};

#define PKEY_ID_PKCS7 2
int pem_pw_cb(char *buf, int len, int w, void *v);
EVP_PKEY *read_private_key(const char *private_key_name);
X509 *read_x509(const char *x509_name);
void sign_section(void *segment_buf, size_t segment_len,
		char *hash_algo, char *private_key_name, char *x509_name,
		char *section_name);
...
elf_back_up(char *elf_name, char *dest_name);

int main(int argc, char **argv) 
{

	int opt;
	do {
		opt = getopt(argc, argv, "h");
		switch (opt) {
			case 'h':
				format();
				break;
			case -1:
				break;
			default:
				format();
		}
	} while (opt != -1);

	argc -= optind;
	argv += optind;
	if (argc < 4 || argc > 5) {
		format();
	}

...
	fd = open(elf_name, O_RDONLY);
	ERR_ENO(fd < 0, errno, "Failed to open file: %s", elf_name);
	Elf64_Ehdr *ehdr = (Elf64_Ehdr *) malloc(sizeof(Elf64_Ehdr));
	ERR_ENO(!ehdr, ENOMEM, "Failed to malloc for ELF header");
	n = file_rw(fd, 0, ehdr, sizeof(Elf64_Ehdr), FILE_READ);
	ERR_ENO(n < 0, errno, "Failed to read ELF header");

	ERR_ENO(memcmp(ehdr->e_ident, ELFMAG, SELFMAG), EBADMSG, "Invalid ELF file: %s", elf_name);
	ERR_ENO(ehdr->e_ident[EI_VERSION] != EV_CURRENT, EBADMSG, "Not support ELF version");
	ERR_ENO(ehdr->e_ident[EI_CLASS] != ELFCLASS64, EBADMSG, "Not support byte long");
	printf(" --- 64-bit ELF file, version 1 (CURRENT), ");

	switch (ehdr->e_ident[EI_DATA]) {
		case ELFDATA2MSB:
			printf("big endian.\n");
			break;
		case ELFDATA2LSB:
			printf("little endian.\n");
			break;
		default:
			ERR_ENO(1, EBADMSG, "Not support data encoding.");
			break;
	}

	printf(" --- %d sections detected.\n", ehdr->e_shnum);

...
	Elf64_Shdr *shdr = (Elf64_Shdr *) malloc(ehdr->e_shentsize * (ehdr->e_shnum));
	ERR_ENO(!shdr, ENOMEM, "Failed to malloc ELF section header table");
	n = file_rw(fd, ehdr->e_shoff, shdr, ehdr->e_shentsize * ehdr->e_shnum, FILE_READ);
	ERR_ENO(n < 0, errno, "Failed to read section header table");

...
	Elf64_Shdr *shdr_strtab = shdr + ehdr->e_shstrndx;
	char *strtab = (char *) malloc(shdr_strtab->sh_size);
	ERR_ENO(!strtab, ENOMEM, "Failed to malloc for string table");
	n = file_rw(fd, shdr_strtab->sh_offset, strtab, shdr_strtab->sh_size, FILE_READ);
	ERR_ENO(n < 0, errno, "Failed to read string table");

	char *dynstrtab = NULL;
	Elf64_Shdr *shdr_p = NULL;

	int i = ehdr->e_shnum - 1;
	for (shdr_p = shdr + i; i >= 0; shdr_p--, i--) {
		char *scn_name = strtab + shdr_p->sh_name; 

		/**
		 * 1. 如果找到了".text_sig",则说明检测到的部分已经存在签名,退出!
		 * 2.如果找到".dynstr",则从文件中读入
		 */
		if (!memcmp(scn_name, SCN_TEXT_SIG, sizeof(SCN_TEXT_SIG))) {
			ERR_ENO(1, EEXIST, "File already been signed with section: [%s]", scn_name);
		} else if (!memcmp(scn_name, SCN_DYNSTR, sizeof(SCN_DYNSTR))) {
			dynstrtab = (char *) malloc(shdr_p->sh_size);
			ERR_ENO(!dynstrtab, ENOMEM, "Failed to malloc for data of section %s", scn_name);
			file_rw(fd, shdr_p->sh_offset, dynstrtab, shdr_p->sh_size, FILE_READ);
		}
	}

	/* 查找动态链接依赖项 */
	if (dynstrtab) {
		for (shdr_p = shdr, i = 0; i < ehdr->e_shnum; shdr_p++, i++) {
			char *scn_name = strtab + shdr_p->sh_name;
			if (!memcmp(scn_name, SCN_DYN, sizeof(SCN_DYN))) {
				Elf64_Dyn *scn_data = (Elf64_Dyn *) malloc(shdr_p->sh_size);
				ERR_ENO(!scn_data, ENOMEM, "Failed to malloc for data of section %s", scn_name);
				file_rw(fd, shdr_p->sh_offset, scn_data, shdr_p->sh_size, FILE_READ);

				for (Elf64_Dyn *dyn_ptr = scn_data;
						dyn_ptr < scn_data + shdr_p->sh_size; dyn_ptr++) {
					if (dyn_ptr->d_tag == DT_NEEDED) {
						printf(" --- [Library dependency]: %s\n",
								dynstrtab + dyn_ptr->d_un.d_val);
					} /* else if (dyn_ptr->d_tag == DT_RPATH) {
						printf(" --- [Library path]: %s\n",
								dynstrtab + dyn_ptr->d_un.d_val);
					} */
				}

				free(scn_data);
				scn_data = NULL;
			}
		}

		free(dynstrtab);
		dynstrtab = NULL;
	}

	/* 遍历各个部分,找到需要签名的部分 */
	for (shdr_p = shdr, i = 0; i < ehdr->e_shnum; shdr_p++, i++) {
		char *scn_name = strtab + shdr_p->sh_name;

		/* 已检测到待签名的部分*/
		if (!memcmp(scn_name, SCN_TEXT, sizeof(SCN_TEXT))) {
			/* .text detected. */
			printf(" --- Section %-4.4d [%s] detected.\n", i, scn_name);
			printf(" --- Length of section [%s]: %ld\n", scn_name, shdr_p->sh_size);

			char *scn_data = (char *) malloc(shdr_p->sh_size);
			ERR_ENO(!scn_data, ENOMEM, "Failed to malloc for data of section %s", scn_name);
			file_rw(fd, shdr_p->sh_offset, scn_data, shdr_p->sh_size, FILE_READ);

			/* 将节数据保存到临时文件中 */
			sign_section(scn_data, shdr_p->sh_size,
					hash_algo, private_key_name, x509_name, scn_name);

			free(scn_data);
			scn_data = NULL;
		}
	}

	close(fd);
	free(strtab);
	free(shdr);
	free(ehdr);
	ehdr = NULL;
	shdr = NULL;
	strtab = NULL;

	/* 复制未签名的文件 */
	elf_back_up(elf_name, dest_name);

	/* 如果未提供目标文件名,则修改原始文件 */
	if (!dest_name) {
		dest_name = elf_name;
	}

	/* 在目标ELF文件中添加签名部分 */
	insert_new_section(dest_name, SCN_TEXT_SIG); /* .text_sig */

	return 0;
}

If you need the complete source code, please add the WeChat number (c17865354792)

$ make

cc -o elf-sign elf_sign.c -lcrypto

./elf-sign.signed sha256 certs/kernel_key.pem certs/kernel_key.pem elf-sign

--- 64-bit ELF file, version 1 (CURRENT), little endian.

--- 29 sections detected.

--- [Library dependency]: libcrypto.so.1.1

--- [Library dependency]: libc.so.6

--- Section 0014 [.text] detected.

--- Length of section [.text]: 10223

--- Signature size of [.text]: 465

--- Writing signature to file: .text_sig

--- Removing temporary signature file: .text_sig

elf-sign.signed elf二进制文件已经由certs/kernel_key.pem中的私钥签名,因此它可以通过操作系统的验证来对新构建的elf签名进行签名。然后,使用签名的elf符号,您可以对计算机上的其他elf二进制文件进行签名。

显示帮助信息:

$ ./elf-sign

Usage: elf-sign [-h] []

-h, display the help and exit

Sign the to an optional with

private key in and public key certificate in

and the digest algorithm specified by . If no

is specified, the will be backup to

.old, and the original will be signed.

对存储库中的现有ELF文件进行签名:

$ ./elf-sign sha256 certs/kernel_key.pem certs/kernel_key.pem

test/func/hello-gcc hello-gcc

--- 64-bit ELF file, version 1 (CURRENT), little endian.

--- 29 sections detected.

--- [Library dependency]: libc.so.6

--- Section 0014 [.text] detected.

--- Length of section [.text]: 418

--- Signature size of [.text]: 465

--- Writing signature to file: .text_sig

--- Removing temporary signature file: .text_sig

要检查结果,请使用binutils中的readelf或objdump工具;

elf-sign的设计围绕"实用化"展开,既要保证签名的安全性,又要兼容不同场景下的ELF文件(如不同编译器生成的文件、动态链接文件等)。其关键设计思路包括:

1. 聚焦关键节:优先保护代码段

ELF文件包含多个节(如.text代码段、.data数据段、.dynamic动态链接信息等),elf-sign选择优先保护.text节。原因在于:.text节存放程序的可执行代码,是恶意篡改的主要目标(如植入后门指令),保护它可最大化防御效果。

2. 签名格式兼容:适配不同OpenSSL版本

数字签名需要依赖密码库实现,elf-sign选择OpenSSL作为底层库。考虑到不同系统中OpenSSL版本可能差异较大(如旧版本不支持CMS格式),工具通过条件编译实现兼容:

  • 若OpenSSL版本≥1.0.0或支持CMS,使用更灵活的CMS格式;
  • 否则降级为PKCS#7格式,确保在旧系统中仍能正常生成签名。

3. ELF结构自适应:兼容多样布局

不同编译器(如GCC、Golang)生成的ELF文件布局可能差异较大(如节的顺序、对齐方式不同)。elf-sign通过以下方式适配:

  • 动态解析ELF头与节头表,不依赖固定的节顺序;
  • 插入新节时自动计算偏移量与对齐(如8字节对齐),确保不破坏原有节的布局;
  • 处理动态链接文件时,会检测依赖的共享库(如通过.dynamic节),但不影响签名逻辑,确保对动态链接程序同样有效。

4. 操作安全性:备份与容错

为避免操作失误导致原始文件损坏,elf-sign在处理前会自动备份原始文件(如备份为<file>.old)。同时,签名过程中若出现错误(如ELF格式非法、签名生成失败),会及时终止并提示,避免生成无效文件。

四、相关领域知识拓展

理解elf-sign需要结合多个领域的基础知识,这些知识也是软件安全的重要组成部分:

1. ELF文件格式

ELF是跨平台的二进制格式,其灵活性使其成为Linux的标准。关键概念包括:

  • 节(Section):ELF的基本组成单位,按功能分类(代码、数据、链接信息等);
  • 节头表(Section Header Table):相当于ELF的"目录",记录所有节的元信息;
  • 对齐(Alignment):为提高内存访问效率,节的起始地址需按特定字节对齐(如8字节、16字节),工具必须遵守这一规则才能保证文件可执行。

2. 公钥密码学

数字签名依赖公钥密码体系(如RSA):

  • 私钥:用于生成签名,必须保密;
  • 公钥:用于验证签名,可公开(如编译进内核);
  • 哈希算法:签名前对数据哈希可减小加密数据量,同时利用哈希的抗碰撞性确保篡改可被检测(常用SHA256)。

3. 内核验证机制

elf-sign生成的签名需要内核配合验证。典型流程是:

  • 内核编译时嵌入公钥证书;
  • 当执行execve系统调用加载ELF文件时,内核自动查找.text_sig节,提取签名;
  • 使用内置公钥验证签名,若失败则拒绝执行文件。

这种机制将安全验证嵌入系统调用层面,从源头阻断恶意文件执行。

五、应用场景与扩展

elf-sign的设计使其可应用于多种安全场景:

  • 嵌入式系统:在固件中保护关键程序,防止物理接触后的篡改;
  • 服务器环境 :确保系统工具(如sshdsudo)未被替换,防御权限提升攻击;
  • 软件分发:开发者可为发布的二进制文件签名,用户通过验证签名确认文件未被篡改。

未来扩展方向包括:

  • 支持更多需要保护的节(如.data数据段);
  • 集成时间戳,防止"重放攻击"(即使用旧版本的合法文件替换新版本);
  • 适配更多架构(如ARM、RISC-V)的ELF文件。

总结

elf-sign通过结合数字签名技术与ELF文件解析,实现了对二进制文件的完整性保护。其设计思路既注重安全性(聚焦关键代码段、依赖成熟密码库),又兼顾兼容性(适配不同ELF布局、OpenSSL版本),为Linux系统提供了一种实用的软件防篡改方案。理解这一工具的原理,不仅能掌握二进制安全的关键技术,也能深入认识ELF格式与公钥密码学在实际场景中的应用。

Welcome to follow WeChat official account【程序猿编码