Linux ELF二进制文件数字签名工具:原理与设计思路(C/C++代码实现)
在软件安全领域,确保二进制文件的完整性与真实性是防御恶意篡改的关键环节。本文将介绍一款名为elf-sign
的工具,它通过给Linux ELF(Executable and Linkable Format)二进制文件添加数字签名,实现对文件完整性的保护。我们将从工具的核心功能、实现原理、设计思路及相关领域知识展开分析,揭示其在软件安全体系中的作用。
一、工具的核心功能与意义
ELF是Linux系统中最常用的二进制文件格式(包括可执行文件、共享库等),其结构中包含代码段、数据段等关键信息。elf-sign
的核心功能是为ELF文件的关键代码段添加数字签名,并将签名嵌入文件中,形成"自包含"的带签名二进制文件。
具体来说,它的作用体现在两个方面:
- 完整性保护:通过数字签名确保ELF文件的关键代码段(如
.text
节,存放可执行代码)未被恶意篡改; - 可验证性:配合内核验证模块,在程序执行(如
execve
系统调用)时自动验证签名,若验证失败则阻止执行,从源头阻断篡改后的恶意程序运行。
这种机制在安全敏感场景(如服务器、嵌入式设备)中尤为重要,可有效防御病毒植入、代码注入等攻击。
二、核心实现原理
elf-sign
的功能实现基于两大技术支柱:数字签名技术与ELF文件格式解析。二者的结合确保了签名的生成、嵌入与后续验证的可行性。
1. 数字签名:确保"不可伪造"与"可验证"
数字签名是不对称加密技术的典型应用,其核心流程包括"签名生成"与"签名验证"两个阶段,elf-sign
专注于前者:
-
签名生成流程:
- 哈希摘要:对ELF文件中需要保护的关键部分(如
.text
节的代码)使用哈希算法(如SHA256)生成固定长度的摘要。哈希算法的特性确保:即使原始数据有微小改动,摘要也会完全不同; - 私钥加密:使用私钥对摘要进行加密,生成"数字签名"。私钥由用户保管,确保只有合法持有者能生成有效签名;
- 签名存储:将生成的签名以特定格式(如PKCS#7或CMS,根据OpenSSL版本选择)保存,后续嵌入ELF文件。
- 哈希摘要:对ELF文件中需要保护的关键部分(如
-
验证逻辑(内核端):
验证时,内核使用对应的公钥(预先编译进内核)解密签名得到摘要,同时对当前文件的关键部分重新计算哈希。若两个摘要一致,则说明文件未被篡改。
2. ELF文件解析:签名的"无缝嵌入"
为了让签名与原始ELF文件共存且不影响其正常执行,elf-sign
需要深入解析ELF格式,实现签名的"无缝嵌入"。ELF文件的核心结构包括:
- ELF头:描述文件整体信息(如位数、字节序、节头表位置);
- 节头表:每个条目对应一个"节"(Section,如
.text
、.data
),记录节的名称、位置、大小等; - 字符串表:存储节名等字符串(如
.shstrtab
存储节头名称)。
elf-sign
的嵌入逻辑如下:
- 定位关键节:解析ELF头与节头表,找到需要保护的
.text
节(代码段); - 生成签名文件:对
.text
节内容签名,生成临时签名文件(如.text_sig
); - 新增签名节:在ELF文件中新增一个名为
.text_sig
的节,将签名数据存入其中; - 更新ELF结构:调整节头表(增加新节的条目)、字符串表(添加
.text_sig
节名),并保证所有节的对齐方式、偏移量等符合ELF规范,避免破坏文件的可执行性。
三、设计思路:平衡安全性与兼容性
...
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
的设计使其可应用于多种安全场景:
- 嵌入式系统:在固件中保护关键程序,防止物理接触后的篡改;
- 服务器环境:确保系统工具(如
sshd
、sudo
)未被替换,防御权限提升攻击; - 软件分发:开发者可为发布的二进制文件签名,用户通过验证签名确认文件未被篡改。
未来扩展方向包括:
- 支持更多需要保护的节(如
.data
数据段); - 集成时间戳,防止"重放攻击"(即使用旧版本的合法文件替换新版本);
- 适配更多架构(如ARM、RISC-V)的ELF文件。
总结
elf-sign
通过结合数字签名技术与ELF文件解析,实现了对二进制文件的完整性保护。其设计思路既注重安全性(聚焦关键代码段、依赖成熟密码库),又兼顾兼容性(适配不同ELF布局、OpenSSL版本),为Linux系统提供了一种实用的软件防篡改方案。理解这一工具的原理,不仅能掌握二进制安全的关键技术,也能深入认识ELF格式与公钥密码学在实际场景中的应用。
Welcome to follow WeChat official account【程序猿编码】