系统调用与libc

N0va7
2025-09-21 / 0 评论 / 2 阅读 / 正在检测是否收录...
温馨提示:
本文最后更新于2025年09月21日,已超过255天没有更新,若内容或图片失效,请留言反馈。

系统调用

什么是系统调用-System call

用户态与内核态的概念

  • 用户态(User Mode):运行用户应用程序的非特权模式,无法直接访问硬件资源。
  • 内核态(Kernel Mode):操作系统核心运行环境,拥有对所有硬件资源的访问权限。

程序无法直接从用户态访问内核资源,必须通过系统调用切换进入内核态执行关键任务。

系统调用的触发机制

  • x86 架构使用 int 0x80 中断指令触发系统调用,借助中断机制切换到内核态。
  • x86_64 架构使用 syscall 指令,相比 int 0x80 更高效,减少切换延迟。

x86示例代码

const char msg[] = "Hello from int 0x80!\n";
int main() {
      asm volatile(
            "movl $4, %%eax;"
            "movl $1, %%ebx;"
            "movl %0, %%ecx;"
            "movl $21, %%edx;"
            "int $0x80;"
            :
            : "r"(msg)
            : "%eax", "%ebx", "%ecx", "%edx"
      );
      return 0;
}
gcc -m32 -fno-stack-protector -no-pie syscal_int80.c -o syscal_int80

gdb调试看汇编代码是否可以看到int $0x80

disas main

确认可以看到,接下来在此处打断点调试,发现执行完这个语句之后就成功输出内容

b main
run
b *main+38
run
si

可以看到我们这边的触发跟之前都不一样,之前的代码汇编都会出现call 某个地址,而我们直接进行系统调用的话,就不用去触发跳转,一步到位。

其实这种方式也在免杀中经常使用到,因为其避免了大部分调用链,不会出现多次调用,也就不容易被查杀

x86_64示例代码

看完x86的代码,我们再来看看x86_64的代码系统调用是怎么个事儿?

const char msg[] = "Hello from syscall!\n";
      int main() {
            asm volatile (
            "mov $1, %%rax;"
            "mov $1, %%rdi;"
            "mov %0, %%rsi;"
            "mov $20, %%rdx;"
            "syscall"
            :
            : "r"(msg)
            : "%rax", "%rdi", "%rsi", "%rdx"
      );
      return 0;
}
gcc -no-pie syscall_syscall.c -o syscall_syscall

这边也可以看到这个所谓的系统调用syscall

重复x86的调试步骤,看看单步执行之后是否会直接输出结果

也是一样的直接输出结果

那么我们现在肯定很多疑问,说了半天,这个什么调用我也看不懂啊?

没关系,下面就来解答问题,这些问题基本都跟系统调用表有关系

系统调用表与调用号-Syscall Number

  • 系统调用由唯一编号识别,操作系统维护一张系统调用表(system call table)。
  • 应用程序通过设置系统调用号在寄存器中,并传递参数,触发调用。
  • 例如:x86_64位Linux 下 write 对应 syscall number 为 1, read 为 0。

相关系统调用表:Linux X86架构 32 64系统调用表_32位 syscall-CSDN博客

常用系统调用函数

  • read :读取文件或标准输入
  • write :写入到标准输出或文件(这个函数就是我们用到的输出函数)
  • open / close :文件操作接口
  • execve :执行新程序
  • mmap :内存映射区域申请
  • fork :进程复制机制

我们在x86的程序下在系统调用之前打断点来观察寄存器

发现什么?eax为4,ecx为一个地址,edx为21,ebx为1

这边eax默认是系统调用表中的调用号,write对应4,也就是输出

ecx存储着字符串的地址,我们可以看下这个字符串

如果有细心的人其实就已经发现edx其实是字符串长度了,正好是21

至于ebx就是文件描述符,进程中每个打开的文件都用一个编号来标识,成为文件描述符,文件描述符1表示标准输出,对应的C标准I/O库的stdout

那么x86_64是什么样子的?

此时记得x86_64的write对应的调用号为1,rdi和ebx对应,rsi对应字符串地址

strace快速定位系统调用

一步一步这么分析是不是感觉很费劲,于是我们现在就要说一个小工具,它可以快速帮助我们展示程序运行过程中触发的所有系统调用

strace ./syscal_int80

直接定位出是wirte

什么是libc

libc 是用户程序调用系统服务的中间层。

包含标准函数(如 printf、malloc)并负责调用 syscall 实现底层功能。

常见实现

  • glibc:GNU 实现,功能最全面。
  • musl libc:轻量级,广泛用于容器环境。
  • uClibc:极简型,适合嵌入式系统。

常用函数族

基本上最底层的代码都会调用到这些类别

系统调用与 libc 的关系

libc 封装机制

  • write() → 底层 syscall(SYS_write, ...):本质还是syscall
  • libc 封装提供缓存、优化机制,提升程序运行效率。

直接 syscall 与间接调用 libc 区别

  • 直接 syscall 减少依赖,常用于 shellcode、逆向分析。
  • 间接调用依赖 libc,功能更丰富但易被 hook。

提权漏洞中的 libc 绕过

  • 利用 ROP(返回导向编程)链执行 system() 、 execve()函数
  • 可直接跳转到 libc 中函数,或构造 syscall 指令手动调用

最常见的比如说dirtycow、dirtypipe都是对libc的绕过来实现提权的

ELF GOT/PLT 与延迟绑定机制

  • PLT:程序首次调用某函数时才解析地址
  • GOT:记录已解析函数地址
  • 利用 GOT 劫持可实现控制流劫持与 ROP 利用

这个现在可以不用了解那么深,后续会继续讲到:
Pwn基础:PLT&GOT表以及延迟绑定机制-腾讯云开发者社区-腾讯云

不过这里我们还是进行一个简单的小demo了解一下:

源码:

#include <stdio.h>
int main() {
       puts("Hello puts1");
       puts("Hello puts2");
       return 0;
}

编译:

gcc -m32 -g -no-pie lazy.c -o lazy

然后进行gdb的调试:

先在main函数下断点,并查看puts函数的真正地址:

b main
run
print puts

此时puts函数的真正地址为:0xf7e4ed90

那么我们查看对应的main函数汇编代码,并在两个puts函数调用之前下断点:

disas main
b *main+36
b *main+54
n

跟进去查看对应的plt地址,发现其里面的第一条命令就是跳转到另一个地址(GOT)中,查看对应地址的内容发现并非我们想象中puts的函数地址,因为此时处于第一次调用还没加载函数地址到其中

x/i 0x80482e0
x/x 0x804a00c

让其继续执行到下一个puts函数的调用看看

n
x/x 0x804a00c
print puts

发现此时两者一致了,说明已经加载到GOT表中了

其实说了这么多,这个机制说白了就是对于没用到的函数第一次使用时才加载,后续使用直接到GOT表找到真正的函数地址调用,而对于没使用到的函数就直接不管不加载,这就是延迟绑定(懒加载)

Windows系统调用基础(了解)

Win API 与 Native API 的区别

  • Win API:高级接口,如:CreateFile(),面向开发者
  • Native API:底层接口,如:NtCreateFile(),通过 syscall 实现
通过 Win API 来实现调用的话确实很方便,但是调用非常明显,容易被杀软发现静态特征,如果通过 Native API 来实现的话就直接到达底层,对于杀软来说调用链非常隐蔽,静态特征不明显,容易绕过静态查杀
0

评论 (0)

取消