Linux ELF Shellcode 生成与 Fileless 实战教学文档
本文档基于《Linux ELF Shellcode 生成与 Fileless 实战》一文,旨在详细讲解如何将 Linux ELF 可执行文件转换为 shellcode 并在无文件(Fileless)场景下应用。
1. 工具简介:zigdonut
zigdonut 是一个用 Zig 语言实现的开源工具,是经典工具 donut 的精简版实现。其核心功能是将符合条件的 Linux ELF 可执行文件转换为位置无关的 shellcode,以实现无文件攻击与执行。
- 项目地址:可从 GitHub 发布页下载:https://github.com/howmp/zigdonut/releases/tag/v2.0.0
- 核心特性:
- 输入要求:仅支持静态链接的PIE/ET_DYN 格式的 ELF 文件。
- 体积优化:制作 shellcode 时会进行压缩,加载时自动解压执行。
- 隐蔽执行:采用 Double fork 机制脱离控制终端,在后台执行,且不产生僵尸进程。
- 动态参数:可动态指定输出文件以及程序的命令行参数,并将 stdin/stderr 重定向到输出文件。
- 工作目录:自动切换工作目录到
/tmp。
2. 为什么需要“静态链接” + “PIE”?
这是使用 zigdonut 生成 ELF shellcode 的两个前提条件,缺一不可。
-
PIE(Position-Independent Executable,位置无关可执行文件):
- 原因:PIE 格式的可执行文件通过重定位表(Relocation Table)来修正代码中的绝对地址引用。这使得编译好的程序可以被加载到内存的任意位置执行。若非 PIE 格式,程序通常被绑定在固定的基址(如0x400000)加载,如果目标内存位置已被占用,将导致加载失败。
- 验证:执行
readelf -h 程序名 | grep "Type:",输出应为Type: DYN (Shared object file),表示是 PIE 格式。
-
静态链接(Statically Linked):
- 原因:静态链接意味着程序运行时不依赖任何外部的共享库(.so 文件)。Shellcode 在目标环境中执行时,通常无法保证所需动态库的存在或路径,因此必须将所有依赖编译进二进制文件内部。
- 验证:执行
ldd 程序名,若输出为statically linked或显示没有发现任何共享库,则表明是静态链接。
3. 编译符合要求的程序
3.1 编译 C 程序:以 BusyBox 为例
BusyBox 默认编译为静态 ELF,但通常不是 PIE 格式。以下步骤指导如何在 Alpine Linux 容器中使用 musl-gcc 编译出静态 PIE 版本的 BusyBox。
步骤 1:准备构建环境
创建 Dockerfile:
FROM alpine:3.20
RUN apk add --no-cache \
build-base \
wget \
tar \
bash \
linux-headers
步骤 2:编写构建脚本
创建 build.sh:
#!/bin/bash
set -e
# 清理并应用默认配置
make distclean
make defconfig
# 修改配置,启用静态编译和PIC
sed -i 's/.*CONFIG_STATIC.*/CONFIG_STATIC=y/' .config
sed -i '/CONFIG_EXTRA_CFLAGS/c\CONFIG_EXTRA_CFLAGS="-fPIC"' .config
sed -i '/CONFIG_EXTRA_LDFLAGS/c\CONFIG_EXTRA_LDFLAGS=""' .config
# 编译,应用PIC标志
make CFLAGS="-fPIC" -j$(nproc)
# 关键步骤:将编译生成的 .a 库文件重新链接为 static-pie
gcc -fPIC -static-pie -o busybox_musl_pie \
-Wl,--sort-common -Wl,--sort-section,alignment \
-Wl,--start-group \
applets/built-in.o archival/lib.a archival/libarchive/lib.a \
console-tools/lib.a coreutils/lib.a coreutils/libcoreutils/lib.a \
debianutils/lib.a klibc-utils/lib.a e2fsprogs/lib.a editors/lib.a \
findutils/lib.a init/lib.a libbb/lib.a libpwdgrp/lib.a \
loginutils/lib.a mailutils/lib.a miscutils/lib.a modutils/lib.a \
networking/lib.a networking/libiproute/lib.a networking/udhcp/lib.a \
printutils/lib.a procps/lib.a runit/lib.a selinux/lib.a \
shell/lib.a sysklogd/lib.a util-linux/lib.a util-linux/volume_id/lib.a \
-Wl,--end-group \
-lcrypt -lm -lpthread
# 可选:去除符号表和调试信息以减小体积
strip -s --remove-section=.note --remove-section=.comment busybox_musl_pie
# 验证
readelf -h busybox_musl_pie | grep "Type:" # 应输出 Type: DYN
ldd busybox_musl_pie # 应输出 statically linked
步骤 3:执行编译
# 构建Docker镜像
docker build --network=host -t build-busybox .
# 运行容器并编译
docker run -it --rm -v $(pwd):/code --network=host build-busybox sh
# 在容器内执行:
cd code && sh build.sh
关键点说明:
CONFIG_STATIC=y:在 BusyBox 配置中启用静态编译。CFLAGS="-fPIC":要求编译器生成位置无关代码(Position-Independent Code)。-static-pie:链接器选项,指示生成静态链接的 PIE 可执行文件。- 使用 musl-libc 而非 glibc 进行静态链接,因为 musl 库体积更小,更适合静态编译。
3.2 编译 Go 程序:以 fscan 为例
对于 Go 语言编写的程序,可以通过指定特定的编译参数和链接器标志,生成符合要求的静态 PIE 二进制文件。
编译命令示例:
CC="zig cc -target x86_64-linux-musl" go build -buildmode=pie -ldflags "-linkmode external -extldflags '-static -pie' -s -w" -trimpath .
-buildmode=pie:指定构建为 PIE 模式。-linkmode external与-extldflags '-static -pie':指示使用外部链接器,并传递-static -pie参数给链接器,以实现静态 PIE 链接。-s -w:省略符号表和调试信息以减小体积。-trimpath:从可执行文件中移除所有文件系统路径。- 通过
CC环境变量指定使用 Zig 的 musl 工具链进行编译,以确保生成纯静态二进制文件。
4. Fileless 使用场景与加载技术
生成 shellcode 后,核心在于如何将其加载到目标进程内存中并执行。
4.1 C2 插件场景
在命令与控制(C2)框架中,可以编写一个简单的加载器(Loader)作为插件。其核心逻辑是分配一块可读、可写、可执行(RWX)的内存,将 shellcode 复制进去,然后跳转到该内存地址执行。
核心C代码示例 (elfscloader.c):
// 分配 RWX 内存
void *sc_addr = mmap(NULL, map_size, PROT_READ | PROT_WRITE | PROT_EXEC,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
// 复制 shellcode
memcpy(sc_addr, data, size);
// 定义函数指针并跳转执行
typedef void (*shellcode_fn)(char *output, size_t argc, char **argv, char **envp);
shellcode_fn sc_fn = (shellcode_fn)sc_addr;
sc_fn(argv[2], (size_t)(argc - 3), argv + 3, envp);
这段代码不到 60 行,完成了内存分配、拷贝和执行跳转,其中 sc_fn 的函数签名与 zigdonut 生成的 shellcode 入口点约定一致。
4.2 免杀场景:Python 加载 Shellcode
在对抗终端检测与响应(EDR/AV)的免杀场景中,将 ELF 文件直接上传到目标机器风险较高。利用 Python 等解释型语言在内存中加载和执行 shellcode 是一种常见的无文件攻击手法。
Python 加载器核心逻辑:
- 导入模块:使用
ctypes和os模块调用系统函数。 - 分配可执行内存:通过
ctypes调用libc库的mmap函数,分配一块具有读、写、执行权限的匿名内存。 - 拷贝 Shellcode:将 zigdonut 生成的 shellcode 字节流复制到分配的内存中。
- 构建参数:为目标程序(如
fscan或busybox)准备argv(命令行参数数组)和envp(环境变量数组)。 - 执行:将内存地址强制转换为约定好的函数指针类型,并传入
output路径、参数个数、参数数组和环境变量数组进行调用。
优势:整个过程中,恶意负载(原始ELF)始终以 shellcode 的形式存在于内存中,没有文件实体落地,极大增加了检测难度。
5. 总结与要点回顾
- 工具目标:zigdonut 将静态链接的 PIE 格式 ELF 程序转换为位置无关的 shellcode,专为 Fileless 攻击设计。
- 核心要求:输入文件必须是 Static + PIE。可通过
readelf和ldd命令验证。 - 编译要点:
- C 程序:需使用
-fPIC编译选项和-static-pie链接选项,推荐搭配 musl-libc。 - Go 程序:需使用
-buildmode=pie和特定的-ldflags进行编译。
- C 程序:需使用
- 应用场景:
- C2 集成:可编写轻量级加载器插件,动态加载 shellcode。
- 内存免杀:通过 Python 等语言在内存中直接加载执行 shellcode,规避文件扫描。
- 实战效果:将
busybox或fscan等工具转换为 shellcode 后,体积显著缩小,并能在无文件落地的条件下,静默地在目标系统内存中执行完整功能。