zahanzo/NoSTD-Framework

GitHub: zahanzo/NoSTD-Framework

NoSTD-Framework 是一个零依赖的 x86_64 裸机 C 执行框架,通过绕过 glibc 实现最小化二进制体积和直接内核系统调用。

Stars: 1 | Forks: 0

# NoSTD:裸机 x86_64 框架 ## 💀 理念:消除强制抽象 现代 C 语言开发已经完全脱离了硬件。当你编写 `int main()` 并使用标准 GNU C 库(`glibc`)进行编译时,你编写的并不是一个与计算机交互的程序;你编写的是一个与臃肿的庞然大物——中间人——进行交互的脚本。 **这个项目的存在就是为了消灭这个中间人。** `NoSTD` 框架是一个从零开始构建的、纯粹的、零依赖的 x86_64 生态系统。它彻底剥离了 C 标准库,绕过了 Userland 充满层层封装的环境,并通过原始的硬件系统调用将你的逻辑直接连接到 Linux Kernel。 ## 演示 https://github.com/user-attachments/assets/708bc42c-c523-4521-8a4d-c02ef300db01 *点击图片观看 NoSTD-Framework 的实际运行效果。* ## 🏗️ 问题所在:Userland vs. Ring 0 与 Glibc 的臃肿 要理解为什么需要 `NoSTD`,你必须了解现代处理器的特权环以及标准编译器向你隐藏了什么。 ### Userland (Ring 3) 与 Kernel Space (Ring 0) 你的 CPU 在特权环中运行。Linux Kernel 位于 **Ring 0** —— 它拥有对 RAM、CPU 缓存和硬件外围设备的绝对控制权。你的标准应用程序位于 **Ring 3 (Userland)** —— 一个受限制的、无特权的沙箱中。 Ring 3 进程无法分配内存、读取文件或生成进程。为了执行任何有用的操作,它必须触发硬件中断(`syscall` 操作码),并礼貌地请求 Kernel(Ring 0)代为完成工作。 ### Glibc 的臃肿灾难 当你编译一个标准的 C 程序时,`glibc` 劫持了二进制文件的入口点。在你的 `main()` 甚至还没有被执行之前,`glibc` 内部的 `_start` 例程会: 1. 初始化 Thread-Local Storage (TLS)。 2. 设置 `malloc` 内存 arena(即使你从不使用它们)。 3. 解析环境变量并设置全局状态。 4. 注册 `atexit` 析构函数。 5. 注入 Stack Canaries 以防止缓冲区溢出(触发 `__stack_chk_fail`)。 这会产生成千上万行无用的汇编指令,使你的二进制文件体积膨胀,增加了执行开销,并向程序员隐藏了机器的真实状态。 **NoSTD 通过完全劫持 ELF 入口点解决了这个问题。** 我们彻底移除了 `glibc`。你的代码成为了 Kernel 交出进程控制权后 CPU 执行的绝对第一条指令。没有隐藏的线程,没有后台初始化,没有强制的内存分配。 ## 对比:NoSTD-Framework vs. Glibc (Static) 以下是在标准情况下使用 `glibc`(静态链接以移除动态依赖)的 "Hello World" 与 `NoSTD-Framework` 之间的对比。 PoC | 特征 | 标准 (Static Glibc) | NoSTD-Framework | | :--- | :--- | :--- | | **二进制文件大小** | ~825 KB | **~9.1 KB** | | **启动 Syscalls** | 数十个 (`mmap`, `brk`, `arch_prctl` 等) | **3 (`execve`, `write`, `exit`)** | | **抽象程度** | 高(复杂的 Runtime) | **无(裸机)** | ### 1. 臃肿 vs. 高效 静态 `glibc` 体积庞大的原因是,标准库必须包含用于格式化 (`printf`)、内存分配 (`malloc`/`free`)、信号处理和本地化的复杂例程。`NoSTD` 是精准的手术刀:它只包含你的软件实际使用的系统调用所需的代码。 ### 2. 执行追踪分析 (strace) 通过 `strace` 分析执行过程时,行为上的差异变得至关重要。`glibc` 在执行 `main` 的第一行代码之前会执行一系列“不可见”的系统调用。这会产生可能被 EDR 或监控工具检测到的“噪声”。`NoSTD` 会立即执行你的代码,没有任何运行时初始化。 strace **从追踪中得出的关键观察:** * **Syscall 三部曲:** 请注意右侧干净的执行流程。我们通过 `execve` 启动,使用 `write` 执行我们的任务,最后以 `exit` 干净地终止。没有隐藏的线程本地存储设置调用或信号处理程序注册。 * **直接访问 Kernel:** 因为我们绕过了 C 库,所以我们在硬件层面上进行操作。Kernel 不知道(也不关心)是否有一个“程序”在运行;它只是处理直接传递给 CPU 寄存器的请求。 * **确定性:** 没有 `glibc` runtime,就没有意外。你对入口点的栈状态和寄存器拥有绝对的控制权,从而确保二进制文件的行为在任何兼容的 Linux 环境中都是完全一致的。 ## 🧬 架构深度剖析 ### 1. LP64 数据模型:为什么用 `long` 而不是 `int` 如果你查看 `NoSTD` 引擎的源代码,你会发现它彻底消灭了 `int` 数据类型,取而代之的是 `unsigned long`。这不是一种风格选择;**这是一项严格的硬件要求。** x86_64 架构上的 Linux 使用 **LP64 数据模型**: * `int` = 32 位(4 字节) * `long` = 64 位(8 字节) * 指针 (`void *`) = 64 位(8 字节) 用于与 Kernel 通信的 CPU 寄存器(`RAX`、`RDI`、`RSI` 等)宽度为 64 位。如果我们的 syscall 引擎接受 `int`(32 位),而你试图传递一个内存指针(例如字符串的地址 `0x00007FFE8B3A1234`),C 编译器将**截断**高 32 位。该指针将被破坏,变成 `0x8B3A1234`。当 Kernel 试图读取这个损坏的地址时,会立即触发 **Segmentation Fault**。 使用 `unsigned long` 保证了与硬件寄存器的 1:1 映射奇偶校验,确保指针和数据从你的 C 变量完美无瑕地传递到 CPU 的硅片中。 ### 2. 裸 Bootstrapper(绕过 `_start`) 因为我们消灭了 `glibc`,Kernel 不会礼貌地将 `argc` 和 `argv` 作为参数传递给函数。相反,在 `execve` syscall 返回时,Kernel 会将原始参数直接转储到 CPU 的栈(`RSP`)上。 为了将这种原始的内存布局转换为一个干净的 C 环境,我们使用了一个内联汇编 Bootstrapper: ``` __asm__( ".text\n" ".global _start\n" "_start:\n" " pop %rdi\n" // 1. Pop argc from the top of the stack straight into RDI (1st C argument) " mov %rsp, %rsi\n" // 2. RSP now points to argv[0]. Move it to RSI (2nd C argument) " and $-16, %rsp\n" // 3. CRITICAL: Align the stack to 16-bytes to respect the System V ABI " call nostd_main\n" // 4. Safely transition into our C logic " mov %rax, %rdi\n" // 5. Capture the C function's return value into RDI " mov $60, %rax\n" // 6. Load SYS_EXIT (60) into RAX " syscall\n" // 7. Command the Kernel to cleanly kill the process ); ``` **栈对齐:** `and $-16, %rsp` 指令是制胜法宝。如果在调用 C 函数之前栈没有对齐到 16 字节,任何执行 SIMD/Vector 指令(如 `movaps`)的现代 CPU 都会立即使程序崩溃。 ### 3. 通用 Syscall 路由器(元编程) Linux x86_64 Kernel 定义了超过 470 个系统调用。为每一个系统调用编写手动包装函数是臃肿且低效的。`NoSTD` 利用先进的 C 预处理器 (CPP) 元编程解决了这个问题。 ``` // Argument counter (Counts up to 6 arguments + 1 Syscall ID) #define __SYSCALL_NARGS(_1, _2, _3, _4, _5, _6, _7, N, ...) N #define __SYSCALL_COUNT(...) __SYSCALL_NARGS(__VA_ARGS__, 6, 5, 4, 3, 2, 1, 0) // Token concatenators #define __SYSCALL_CONCAT(a, b) a ## b #define _SYSCALL_CONCAT(a, b) __SYSCALL_CONCAT(a, b) // The Universal Gateway #define syscall(...) _SYSCALL_CONCAT(_sys_call, __SYSCALL_COUNT(__VA_ARGS__))(__VA_ARGS__) ``` **工作原理:** 当你编写 `syscall(SYS_WRITE, 1, buffer, length);` 时,宏会在编译时动态计算参数数量(总共 4 个)。然后,它会将你的代码无缝转换为 `_sys_call3(...)`。 这将把执行路由到一个内联汇编块,该块在执行硬件 `syscall` 指令之前,严格将变量对齐到 System V ABI 寄存器(`RAX` 用于系统调用 ID,随后是 `RDI`、`RSI`、`RDX`、`R10`、`R8`、`R9` 用于参数)。所有这些都瞬间在硅片中发生,零运行时开销,零内存分配。 ## 🚀 编译与使用 因为这个框架拒绝了标准生态系统,所以你必须严格命令编译器退居其次。 使用以下命令编译你的工具: ``` gcc -O2 -nostdlib -fno-stack-protector -static src/main.c -o my_tool ``` * `-O2`:强制编译器激进地内联 syscall 引擎,展平汇编并消除 `call`/`ret` 栈帧开销。 * `-nostdlib`:核心指令。指示 Linker 完全忽略 `glibc` 和标准启动例程 (`crt0`)。 * `-fno-stack-protector`:阻止编译器在栈上分配本地数组时注入 `__stack_chk_fail` 例程,赋予你对自己内存边界的绝对控制权。 ## ⚠️ Ring 0 的现实 这个框架提供了**零安全网**。你直接与 Kernel 进行对话。如果你未能对字符串进行 null 终止、传递了错误的指针,或者请求执行跳转到 `PROT_NONE` 内存,Kernel 会立即以 Segmentation Fault 惩罚你的进程。 欢迎来到裸机世界。
标签:Hpfeeds, Linux内核, x86_64, 安全渗透, 客户端加密, 无标准库, 系统底层, 系统调用, 裸金属编程, 高危端口监控