Linux ELF Shellcode 生成与 Fileless 实战
字数 3009
更新时间 2026-04-28 13:27:41

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
  • 核心特性
    1. 输入要求:仅支持静态链接PIE/ET_DYN 格式的 ELF 文件。
    2. 体积优化:制作 shellcode 时会进行压缩,加载时自动解压执行。
    3. 隐蔽执行:采用 Double fork 机制脱离控制终端,在后台执行,且不产生僵尸进程。
    4. 动态参数:可动态指定输出文件以及程序的命令行参数,并将 stdin/stderr 重定向到输出文件。
    5. 工作目录:自动切换工作目录到 /tmp

2. 为什么需要“静态链接” + “PIE”?

这是使用 zigdonut 生成 ELF shellcode 的两个前提条件,缺一不可。

  1. PIE(Position-Independent Executable,位置无关可执行文件)

    • 原因:PIE 格式的可执行文件通过重定位表(Relocation Table)来修正代码中的绝对地址引用。这使得编译好的程序可以被加载到内存的任意位置执行。若非 PIE 格式,程序通常被绑定在固定的基址(如0x400000)加载,如果目标内存位置已被占用,将导致加载失败。
    • 验证:执行 readelf -h 程序名 | grep "Type:",输出应为 Type: DYN (Shared object file),表示是 PIE 格式。
  2. 静态链接(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 加载器核心逻辑

  1. 导入模块:使用 ctypesos 模块调用系统函数。
  2. 分配可执行内存:通过 ctypes 调用 libc 库的 mmap 函数,分配一块具有读、写、执行权限的匿名内存。
  3. 拷贝 Shellcode:将 zigdonut 生成的 shellcode 字节流复制到分配的内存中。
  4. 构建参数:为目标程序(如 fscanbusybox)准备 argv(命令行参数数组)和 envp(环境变量数组)。
  5. 执行:将内存地址强制转换为约定好的函数指针类型,并传入 output 路径、参数个数、参数数组和环境变量数组进行调用。

优势:整个过程中,恶意负载(原始ELF)始终以 shellcode 的形式存在于内存中,没有文件实体落地,极大增加了检测难度。

5. 总结与要点回顾

  1. 工具目标:zigdonut 将静态链接的 PIE 格式 ELF 程序转换为位置无关的 shellcode,专为 Fileless 攻击设计。
  2. 核心要求:输入文件必须是 Static + PIE。可通过 readelfldd 命令验证。
  3. 编译要点
    • C 程序:需使用 -fPIC 编译选项和 -static-pie 链接选项,推荐搭配 musl-libc。
    • Go 程序:需使用 -buildmode=pie 和特定的 -ldflags 进行编译。
  4. 应用场景
    • C2 集成:可编写轻量级加载器插件,动态加载 shellcode。
    • 内存免杀:通过 Python 等语言在内存中直接加载执行 shellcode,规避文件扫描。
  5. 实战效果:将 busyboxfscan 等工具转换为 shellcode 后,体积显著缩小,并能在无文件落地的条件下,静默地在目标系统内存中执行完整功能。
相似文章
相似文章
 全屏