在 Linux 系统的安全攻防对抗中,二进制文件格式的底层特性往往成为攻击者利用的突破口。ELF(Executable and Linkable Format)作为 Linux 平台的标准二进制格式,其内部结构的复杂性为安全研究人员和攻击者都提供了丰富的操作空间。本文将聚焦于 ELF 文件中一个关键但常被忽视的组件 ——PT_INTERP 程序头,探讨其被滥用于动态链接器劫持的技术细节、工程实现方案以及相应的防御策略。
ELF PT_INTERP 字段的作用机制
ELF 文件中的程序头(Program Header)描述了系统加载器如何将文件映射到进程地址空间。其中,PT_INTERP 类型的程序头指定了程序解释器(Program Interpreter)的路径,这个解释器通常是动态链接器(如/lib64/ld-linux-x86-64.so.2)。
当内核执行一个动态链接的 ELF 可执行文件时,它会执行以下关键步骤:
- 读取 ELF 头部:验证文件格式并获取程序头表位置
- 加载程序段:根据 PT_LOAD 程序头将代码和数据段映射到内存
- 定位解释器:查找 PT_INTERP 程序头,获取解释器路径
- 移交控制权:将控制权转移给解释器,由解释器完成动态链接工作
动态链接器的主要职责包括:
- 加载所需的共享库(通过 DT_NEEDED 条目)
- 执行符号重定位
- 处理延迟绑定(PLT/GOT)
- 调用程序的入口点(_start)
根据 MITRE ATT&CK 框架的分类,动态链接器劫持被归类为 T1574.006 技术,攻击者通过操纵环境变量或修改二进制文件来注入恶意代码。
PT_INTERP 劫持的攻击向量
1. 直接修改二进制文件
攻击者可以通过直接修改 ELF 文件的 PT_INTERP 程序头,将解释器路径指向恶意动态链接器。这种攻击方式具有以下特点:
- 持久性强:一旦二进制文件被修改,每次执行都会加载恶意解释器
- 隐蔽性高:不依赖环境变量,难以通过常规监控发现
- 权限要求:需要文件写入权限,通常需要 root 权限或利用其他漏洞
技术实现上,攻击者需要:
- 解析 ELF 文件结构,定位程序头表
- 找到 PT_INTERP 类型的程序头
- 修改 p_offset 和 p_vaddr 指向新的解释器字符串
- 确保新的解释器字符串在文件中有足够的空间
2. 利用 LIEF 库进行自动化修改
LIEF(Library to Instrument Executable Formats)是一个强大的二进制操作库,支持多种文件格式的解析和修改。使用 LIEF 可以简化 PT_INTERP 修改过程:
import lief
# 加载目标ELF文件
binary = lief.parse("/usr/bin/target_program")
# 创建新的解释器段
interp_segment = lief.ELF.Segment()
interp_segment.type = lief.ELF.SEGMENT_TYPES.INTERP
interp_segment.content = b"/tmp/malicious_ld.so\x00"
# 添加新的PT_INTERP段
binary.add(interp_segment)
# 保存修改后的文件
binary.write("target_program_hijacked")
3. 恶意解释器的实现
恶意动态链接器需要模拟合法链接器的行为,同时注入恶意功能。关键实现点包括:
// 恶意动态链接器的简化示例
void _start(void) {
// 1. 执行恶意操作
perform_malicious_actions();
// 2. 加载原始动态链接器
void* handle = dlopen("/lib64/ld-linux-x86-64.so.2", RTLD_LAZY);
// 3. 获取原始入口点并跳转
void (*original_entry)(void) = dlsym(handle, "_dl_start");
original_entry();
}
工程实现方案与参数配置
1. 文件偏移计算与对齐
修改 ELF 文件时,必须正确处理文件偏移和对齐要求:
def calculate_interp_offset(binary):
"""计算新的解释器字符串的合适偏移"""
# 获取文件末尾位置
end_offset = max(segment.file_offset + segment.file_size
for segment in binary.segments)
# 对齐到页面边界(通常4KB)
aligned_offset = (end_offset + 0xFFF) & ~0xFFF
return aligned_offset
2. 段表与节表的协调更新
添加新的程序头会影响后续段和节的偏移:
def update_offsets(binary, added_size):
"""更新所有受影响的偏移"""
phdr_table_offset = binary.header.program_header_offset
# 更新程序头之后的段偏移
for segment in binary.segments:
if segment.file_offset > phdr_table_offset:
segment.file_offset += added_size
# 更新节表偏移
if binary.header.section_header_offset > phdr_table_offset:
binary.header.section_header_offset += added_size
3. 内存布局的保持
确保虚拟地址空间布局的一致性:
def ensure_memory_layout(binary, new_interp_segment):
"""确保新的解释器段不会破坏现有内存布局"""
# 检查地址冲突
for segment in binary.segments:
if (segment.virtual_address <= new_interp_segment.virtual_address <
segment.virtual_address + segment.virtual_size):
raise ValueError("地址空间冲突")
# 设置合适的权限标志
new_interp_segment.flags = lief.ELF.SEGMENT_FLAGS.R
检测与防御策略
1. 静态检测方法
文件完整性校验:
# 使用sha256sum验证文件完整性
sha256sum /usr/bin/critical_program
# 存储和比较基准哈希值
# 使用binwalk检查可疑的段
binwalk -e /usr/bin/critical_program
PT_INTERP 路径分析:
def check_interp_paths(binary_path):
"""检查PT_INTERP路径是否可疑"""
binary = lief.parse(binary_path)
for segment in binary.segments:
if segment.type == lief.ELF.SEGMENT_TYPES.INTERP:
interp_path = bytes(segment.content).decode('utf-8').strip('\x00')
# 检查是否在标准路径中
standard_paths = [
'/lib64/ld-linux-x86-64.so.2',
'/lib/ld-linux.so.2',
'/usr/lib/ld.so.1'
]
if interp_path not in standard_paths:
return False, f"可疑的解释器路径: {interp_path}"
return True, "解释器路径正常"
2. 运行时监控
动态链接器加载监控:
// 使用eBPF监控execve系统调用
SEC("tracepoint/syscalls/sys_enter_execve")
int trace_execve(struct trace_event_raw_sys_enter* ctx) {
char filename[256];
bpf_probe_read_user_str(filename, sizeof(filename),
(void*)ctx->args[0]);
// 检查是否加载了非标准解释器
if (strstr(filename, "ld.so") &&
!strstr(filename, "/lib") &&
!strstr(filename, "/usr/lib")) {
bpf_printk("可疑的动态链接器加载: %s", filename);
}
return 0;
}
环境变量审计:
# 监控LD_PRELOAD等环境变量的设置
auditctl -a always,exit -F arch=b64 -S execve -k exec_monitor
# 检查/etc/ld.so.preload文件
stat /etc/ld.so.preload
cat /etc/ld.so.preload 2>/dev/null
3. 系统加固措施
文件系统保护:
# 设置关键二进制文件的不可变属性
chattr +i /usr/bin/critical_program
# 使用SELinux或AppArmor限制二进制修改
aa-genprof /usr/bin/critical_program
# 启用内核模块签名验证
echo 1 > /sys/module/module/parameters/sig_enforce
编译时防护:
# 在编译时添加安全标志
CFLAGS += -fPIE -pie -Wl,-z,now,-z,relro
LDFLAGS += -Wl,-z,noexecstack -Wl,-z,separate-code
实际攻击案例分析
案例 1:Ebury 后门的动态链接器劫持
Ebury 是一个针对 OpenSSH 服务器的后门,它使用 LD_PRELOAD 技术注入恶意共享模块。当 Ebury 作为 OpenSSH 服务器运行时,它会 hook 多个 libc 函数(如system、popen、execve等),从而在 SSH 会话启动的程序中注入恶意代码。
攻击特征:
- 修改
/etc/ld.so.preload文件 - 注入的库文件通常位于隐蔽目录
- Hook 关键系统函数以隐藏自身
案例 2:COATHANGER 的持久化机制
COATHANGER 恶意软件通过修改 PT_INTERP 实现持久化。它将恶意文件/data2/.bd.key/preload.so复制到/lib/preload.so,然后执行带有恶意参数的/bin/authd,将恶意 preload.so 文件注入到 PID 1 的进程中。
技术特点:
- 直接修改系统二进制文件
- 针对 init 进程进行注入
- 替换关键系统函数
防御最佳实践清单
1. 预防措施
- 对所有生产系统二进制文件进行完整性校验
- 启用文件系统监控(如 auditd、inotify)
- 限制对
/usr/bin、/bin等目录的写权限 - 使用强制访问控制(SELinux/AppArmor)
2. 检测配置
- 部署 eBPF-based 运行时监控
- 配置集中式日志收集和分析
- 定期扫描非标准 PT_INTERP 路径
- 监控 LD_PRELOAD 环境变量使用
3. 响应流程
- 建立二进制文件篡改的应急响应流程
- 准备干净的系统二进制文件备份
- 制定动态链接器劫持的取证检查清单
- 定期进行红队演练
结论
ELF PT_INTERP 字段的滥用代表了二进制级别攻击的演进方向,攻击者通过操纵底层的文件格式结构实现持久化和隐蔽执行。与传统的 LD_PRELOAD 技术相比,PT_INTERP 劫持更加隐蔽且难以检测,因为它不依赖环境变量,而是直接修改二进制文件本身。
防御此类攻击需要多层次的安全策略:从静态文件完整性校验到运行时行为监控,从操作系统加固到应用程序安全编译。安全团队应当将二进制文件安全纳入整体安全框架,定期审计关键系统二进制文件,并建立针对二进制篡改的检测和响应能力。
随着攻击技术的不断演进,对底层系统组件的深入理解和持续监控将成为防御高级持久化威胁的关键。只有通过技术深度和防御广度的结合,才能在日益复杂的攻防对抗中保持优势。
资料来源
- MITRE ATT&CK T1574.006 - Dynamic Linker Hijacking (2025)
- LIEF Documentation - ELF Hooking Tutorial (2025)
- Stack Overflow - "Add interpreter to shared object without touching the code" (2024)
- Wiz.io - "Linux rootkits explained – Part 1: Dynamic linker hijacking" (2023)
- Elastic Security Labs - "Declawing PUMAKIT" (2024)