目录

C语言第七课-字符串与危险函数

【C语言】第七课 字符串与危险函数​​

C语言中的字符串处理既是基础,也是安全漏洞的重灾区。理解C风格字符串的底层原理及其危险函数的运作方式,对于编写安全代码和进行逆向工程分析至关重要。

🧩 C风格字符串的本质

C风格字符串本质上是以空字符'\0'(ASCII值为0)结尾的字符数组。这个终止符是字符串的“生命线”,它告诉字符串处理函数字符串在哪里结束。

  • 内存中的表示:字符串 "Hello" 在内存中实际存储为 {'H', 'e', 'l', 'l', 'o', '\0'}

  • 长度与容量:字符串的长度strlen() 返回的值(不包含'\0'),而容量是字符数组实际占用的总字节数。长度不能超过容量减一(必须为'\0'留出空间)。

  • 声明方式

    char str1[] = "Hello"; // 编译器自动计算大小,包含'\0'
    char str2[6] = {'H', 'e', 'l', 'l', 'o', '\0'}; // 手动指定大小并初始化
    char str3[10]; // 未初始化,后续需要手动添加'\0'
    

⚠️ 危险的字符串操作函数

C标准库提供了一系列字符串操作函数,但它们大多不检查目标缓冲区的边界,这是导致缓冲区溢出的根源。

以下是几个常见的高危函数及其安全注意事项:

函数用途危险原因安全替代建议
strcpy(dest, src)将源字符串复制到目标缓冲区src长度 > dest容量,导致溢出strncpy(dest, src, dest_size-1) 并手动添加 dest[dest_size-1] = '\0'
strcat(dest, src)将源字符串追加到目标字符串末尾若合并后总长度 > dest容量,导致溢出strncat(dest, src, dest_size - strlen(dest) - 1)
sprintf(dest, format, ...)格式化输出到字符串若生成的字符串长度 > dest容量,导致溢出snprintf(dest, dest_size, format, ...)
gets(dest)从标准输入读取一行到dest极度危险! 无法限制读取长度,必然溢出绝对不要使用!fgets(dest, size, stdin) 代替
scanf("%s", dest)读取字符串若输入过长,导致溢出始终指定宽度:scanf("%19s", dest) // 假设dest大小为20

安全函数的注意事项

  • strncpy不会自动添加终止符:如果源字符串长度超过或等于指定的最大复制长度,strncpy 不会 在目标末尾添加 '\0'你必须手动添加以确保字符串正确终止。

    char dest[10];
    strncpy(dest, "ThisIsAVeryLongString", sizeof(dest) - 1); // 只复制前9个字符
    dest[sizeof(dest) - 1] = '\0'; // 手动添加终止符,这是关键!
    
  • strncat 相对安全:它会自动在追加的字符串末尾添加 '\0',但你必须确保目标缓冲区有足够的剩余空间(包括终止符)。

💥 缓冲区溢出漏洞详解(以栈溢出为例)

缓冲区溢出是当数据写入缓冲区时,超出了缓冲区的边界,覆盖了相邻内存区域的行为。栈溢出是其中最常见且最危险的一种。

漏洞代码示例
#include <stdio.h>
#include <string.h>

void vulnerable_function(const char* input) {
    char buffer[16]; // 在栈上分配一个16字节的缓冲区
    strcpy(buffer, input); // 🚨 危险!无边界检查的复制
    printf("Buffer: %s\n", buffer);
}

int main() {
    char large_input[256] = "This string is definitely longer than sixteen bytes...";
    vulnerable_function(large_input);
    return 0;
}
溢出过程与逆向分析

在逆向工程中,理解函数调用时的栈帧布局至关重要。当调用 vulnerable_function 时,栈帧通常如下布局(简化示意,具体取决于编译器和架构):

内存地址(高)栈帧内容说明
ebp + 8参数 input传递给函数的参数
ebp + 4返回地址 (Return Address)这是攻击者的主要目标!
ebp保存的上一帧ebp (Saved EBP)
ebp - 4局部变量 buffer[12-15]
ebp - 8局部变量 buffer[8-11]
ebp - 12局部变量 buffer[4-7]
ebp - 16局部变量 buffer[0-3]
  1. 正常操作:如果输入的字符串长度小于16字节(包括结尾的'\0'),strcpy 会正常复制,不会破坏栈上的其他数据。
  2. 发生溢出:当输入远长于16字节时,strcpy 会持续复制,超出 buffer 的边界。
  3. 覆盖关键数据
    • 首先会覆盖保存的EBPebp指向的位置)。
    • 继续覆盖返回地址ebp + 4指向的位置)。攻击者可以精心构造输入数据,使这个返回地址指向他们注入的恶意代码(通常也在栈上)或现有的特殊函数
  4. 劫持程序流程:当 vulnerable_function 执行完毕,准备返回时,CPU会从栈上取出那个已被覆盖的返回地址,并跳转到该地址执行。程序的控制流就此被劫持
在调试器(GDB)中观察溢出
  1. 编译代码:使用调试信息编译(gcc -g -o program program.c)。

  2. 启动GDBgdb ./program

  3. 设置断点:在 vulnerable_functionstrcpy 之后设置断点。

    (gdb) break vulnerable_function
    (gdb) break *(vulnerable_function+某偏移量) # 在strcpy之后设置断点
  4. 运行并传递超长参数

    (gdb) run $(python -c "print 'A'*256)") # 使用一串'A'作为输入
  5. 观察栈内存

    • strcpy之前,使用 x/20xw $esp 查看栈内存(正常)。
    • strcpy之后,再次使用 x/20xw $esp,你会看到返回地址和被保存的EBP已被字符’A’(ASCII码0x41)覆盖
    (gdb) x/8xw $ebp # 查看ebp附近的内存
    0xffffd00c: 0x41414141 0x41414141 0x41414141 0x41414141 # 覆盖的EBP和返回地址
    0xffffd01c: 0x41414141 0x41414141 0x41414141 0x41414141
  6. 继续执行:当函数返回时(stepicontinue),程序会尝试跳转到地址 0x41414141(即"AAAA")去执行,这显然是一个非法地址,会导致段错误(Segmentation fault)。在真实的攻击中,这个地址会被替换为精心计算的、指向恶意代码的有效地址。

🛡️ 如何防范缓冲区溢出

  1. 使用安全函数:优先使用带 n 版本的函数(如 strncpy, strncat, snprintf)并正确使用它们(特别是为strncpy手动添加终止符)。
  2. 动态内存管理:如果可能,使用malloc根据字符串实际长度动态分配足够的内存,但记得最后要free
  3. 现代编译器和操作系统保护机制
    • 栈保护器(Stack Canaries):编译器(如GCC的-fstack-protector)会在栈上的返回地址前插入一个随机值(canary)。函数返回前检查该值是否被修改,若被修改则终止程序。
    • 数据执行保护(DEP/NX):将数据所在的内存页(如栈)标记为不可执行,即使攻击者注入了代码,也无法运行。
    • 地址空间布局随机化(ASLR):随机化进程内存布局(栈、堆、库的地址),使得攻击者难以预测恶意代码的准确地址。
  4. 静态代码分析工具:使用工具扫描代码,自动识别潜在的缓冲区溢出风险。
  5. 代码审计:养成良好的编程习惯,始终对外部输入保持怀疑,并手动检查所有缓冲区操作的边界。

💎 总结

理解C风格字符串和危险函数是C编程和逆向分析的基石。'\0'终止符是生命线,缓冲区边界是高压线。通过调试器亲眼目睹栈溢出如何覆盖返回地址,是理解整个漏洞机理最直观的方式。在开发中,务必摒弃危险的函数,采用安全替代方案,并利用现代系统的保护机制,从根本上减少漏洞的产生。