前置知识: C

安全函数与边界检查

4 minIntermediate2026/6/14

C11 Annex K安全函数

概述

C语言的传统标准库函数(如 strcpysprintfgets 等)缺乏边界检查,是缓冲区溢出漏洞的主要来源。C11 标准在 Annex K(边界检查接口)中引入了一系列带 _s 后缀的安全函数,要求调用者显式传入缓冲区大小,从而在编译期和运行期防止越界访问。此外,即使不使用 Annex K 函数,也可以通过 snprintfstrnlen 等替代方案实现安全的字符串操作。

基础概念

为什么需要安全函数

传统C函数的安全隐患:

// 危险:不检查目标缓冲区大小,可能导致溢出
char buf[10];
strcpy(buf, "这是一个很长的字符串"); // 缓冲区溢出!
sprintf(buf, "%d", 1234567890);       // 可能溢出
gets(buf);                            // 最危险的函数,无法限制输入长度

Annex K 安全函数的设计原则

  1. 所有涉及缓冲区写入的函数都增加一个表示缓冲区大小的参数
  2. 函数返回 errno_t 型(通常是 int),成功返回 0,失败返回非零错误码
  3. 如果检测到运行时约束违反(如空指针、缓冲区太小),会调用约束处理函数
  4. 约束处理函数可通过 set_constraint_handler_s 自定义

安全函数与原函数对照

原函数安全替代说明
strcpystrcpy_s字符串复制,需指定目标缓冲区大小
strcatstrcat_s字符串追加,需指定目标缓冲区大小
sprintfsprintf_s格式化写入,需指定缓冲区大小
fopenfopen_s打开文件,返回错误码而非指针
getsgets_s读取输入,需指定缓冲区大小
scanfscanf_s格式化输入,%s%c%[ 需指定长度
strncpystrncpy_s带长度限制的复制
memcpymemcpy_s内存复制,需指定目标和源大小

快速上手

使用安全函数的基本模式

#define __STDC_WANT_LIB_EXT1__ 1 // 启用 Annex K 扩展
#include <stdio.h>
#include <string.h>

int main(void) {
    char dest[20];
    const char *src = "Hello, World!";

    // 使用 strcpy_s 替代 strcpy
    errno_t err = strcpy_s(dest, sizeof(dest), src);
    if (err != 0) {
        printf("复制失败,错误码: %d\n", err);
        return 1;
    }
    printf("结果: %s\n", dest); // 输出: Hello, World!
    return 0;
}

使用 snprintf 替代 sprintf

在不支持 Annex K 的编译器上,snprintf 是最通用的安全替代方案:

#include <stdio.h>

int main(void) {
    char buf[32];
    int value = 42;

    // snprintf 会自动截断,保证不会溢出
    int len = snprintf(buf, sizeof(buf), "数值是: %d", value);
    if (len < 0 || len >= (int)sizeof(buf)) {
        printf("输出被截断\n");
    }
    printf("%s\n", buf);
    return 0;
}

详细用法

strcpy_s 详细用法

#define __STDC_WANT_LIB_EXT1__ 1
#include <stdio.h>
#include <string.h>

int main(void) {
    char dest[10];

    // 正常复制
    errno_t err = strcpy_s(dest, sizeof(dest), "Hi");
    if (err == 0) {
        printf("复制成功: %s\n", dest);
    }

    // 源字符串太长,目标缓冲区不够
    err = strcpy_s(dest, sizeof(dest), "这是一个很长的字符串");
    if (err != 0) {
        printf("复制失败: 缓冲区太小\n");
    }

    return 0;
}

strcat_s 详细用法

#define __STDC_WANT_LIB_EXT1__ 1
#include <stdio.h>
#include <string.h>

int main(void) {
    char buf[20] = "Hello";

    // 安全追加字符串
    errno_t err = strcat_s(buf, sizeof(buf), ", World!");
    if (err == 0) {
        printf("追加成功: %s\n", buf);
    }

    // 追加后超出缓冲区大小
    err = strcat_s(buf, sizeof(buf), " 这是一段额外的文字");
    if (err != 0) {
        printf("追加失败: 缓冲区不足\n");
    }

    return 0;
}

sprintf_s 详细用法

#define __STDC_WANT_LIB_EXT1__ 1
#include <stdio.h>

int main(void) {
    char buf[50];
    int id = 1001;
    double score = 95.5;

    // 安全格式化写入
    errno_t err = sprintf_s(buf, sizeof(buf), "学号: %d, 成绩: %.1f", id, score);
    if (err == 0) {
        printf("%s\n", buf);
    }
    return 0;
}

fopen_s 详细用法

#define __STDC_WANT_LIB_EXT1__ 1
#include <stdio.h>

int main(void) {
    FILE *fp = NULL;

    // 安全打开文件
    errno_t err = fopen_s(&fp, "data.txt", "r");
    if (err != 0) {
        printf("文件打开失败,错误码: %d\n", err);
        return 1;
    }

    // 正常读写操作
    char line[256];
    while (fgets(line, sizeof(line), fp)) {
        printf("%s", line);
    }

    fclose(fp);
    return 0;
}

scanf_s 详细用法

#define __STDC_WANT_LIB_EXT1__ 1
#include <stdio.h>

int main(void) {
    char name[32];
    int age;

    // scanf_s 中 %s、%c、%[ 需要额外传入缓冲区大小
    printf("请输入姓名和年龄: ");
    int result = scanf_s("%31s", name, (unsigned)sizeof(name), "%d", &age);

    if (result == 2) {
        printf("姓名: %s, 年龄: %d\n", name, age);
    } else {
        printf("输入格式错误\n");
    }

    return 0;
}

常见场景

场景一:安全读取用户输入

#include <stdio.h>
#include <string.h>

// 安全读取一行输入,防止缓冲区溢出
int safe_read_line(char *buf, size_t buf_size) {
    if (!buf || buf_size == 0) return -1;

    if (fgets(buf, (int)buf_size, stdin) == NULL) {
        return -1; // 读取失败或EOF
    }

    // 去除末尾的换行符
    size_t len = strnlen(buf, buf_size);
    if (len > 0 && buf[len - 1] == '\n') {
        buf[len - 1] = '\0';
    }

    // 检查是否输入被截断
    if (len == buf_size - 1) {
        // 可能还有未读取的字符,清空输入缓冲区
        int c;
        while ((c = getchar()) != '\n' && c != EOF);
        return 1; // 表示输入被截断
    }

    return 0;
}

int main(void) {
    char input[64];
    printf("请输入: ");

    int ret = safe_read_line(input, sizeof(input));
    if (ret < 0) {
        printf("读取失败\n");
    } else if (ret == 1) {
        printf("输入过长,已截断为: %s\n", input);
    } else {
        printf("你输入了: %s\n", input);
    }

    return 0;
}

场景二:安全拼接路径

#include <stdio.h>
#include <string.h>

// 安全拼接目录路径和文件名
int join_path(char *buf, size_t buf_size, const char *dir, const char *file) {
    if (!buf || !dir || !file || buf_size == 0) return -1;

    size_t dir_len = strnlen(dir, buf_size);
    size_t file_len = strnlen(file, buf_size);

    // 检查总长度是否超出缓冲区
    int need_sep = (dir_len > 0 && dir[dir_len - 1] != '/');
    size_t total = dir_len + (need_sep ? 1 : 0) + file_len + 1;

    if (total > buf_size) return -1;

    // 使用 snprintf 安全拼接
    if (need_sep) {
        snprintf(buf, buf_size, "%s/%s", dir, file);
    } else {
        snprintf(buf, buf_size, "%s%s", dir, file);
    }

    return 0;
}

int main(void) {
    char path[256];
    if (join_path(path, sizeof(path), "/home/user", "document.txt") == 0) {
        printf("路径: %s\n", path);
    }
    return 0;
}

场景三:安全复制结构体字符串字段

#include <stdio.h>
#include <string.h>

typedef struct {
    char name[32];
    char email[64];
} Person;

// 安全设置姓名字段
int set_name(Person *p, const char *name) {
    if (!p || !name) return -1;

    // 使用 strncpy + 手动添加终止符
    strncpy(p->name, name, sizeof(p->name) - 1);
    p->name[sizeof(p->name) - 1] = '\0'; // 确保以 null 结尾
    return 0;
}

// 安全设置邮箱字段
int set_email(Person *p, const char *email) {
    if (!p || !email) return -1;

    // 使用 snprintf 更安全
    snprintf(p->email, sizeof(p->email), "%s", email);
    return 0;
}

int main(void) {
    Person person = {0};
    set_name(&person, "张三");
    set_email(&person, "zhangsan@example.com");
    printf("姓名: %s, 邮箱: %s\n", person.name, person.email);
    return 0;
}

注意事项

Annex K 的可移植性问题

C11 的 Annex K 是可选的,编译器不一定要实现。目前的主要支持情况:

  • MSVC(Visual Studio):完整支持 _s 函数
  • GCC / Clang:默认不支持 Annex K,但部分函数可通过 __STDC_WANT_LIB_EXT1__ 宏启用
  • 嵌入式平台:通常不支持

在不支持 Annex K 的环境中,应使用 snprintfstrnlenfgets 等替代方案。

strncpy 的陷阱

strncpy 并不是 strcpy 的安全替代品,它有以下问题:

char buf[10];

// 问题一:如果源字符串长度 >= n,strncpy 不会添加终止符
strncpy(buf, "ABCDEFGHIJ", sizeof(buf)); // buf 没有 '\0'!

// 问题二:如果源字符串很短,strncpy 会用 '\0' 填充剩余空间,效率低
strncpy(buf, "Hi", sizeof(buf)); // 后面8个字节都被填为 '\0'

// 正确做法:手动添加终止符
strncpy(buf, src, sizeof(buf) - 1);
buf[sizeof(buf) - 1] = '\0';

约束处理函数

当安全函数检测到运行时约束违反时,会调用约束处理函数。默认行为通常是终止程序,可以通过 set_constraint_handler_s 自定义:

#define __STDC_WANT_LIB_EXT1__ 1
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// 自定义约束处理函数
void my_handler(const char *msg, void *ptr, errno_t err) {
    fprintf(stderr, "约束违反: %s (错误码: %d)\n", msg, err);
    // 可以选择不终止程序,而是返回错误
}

int main(void) {
    // 设置自定义处理函数
    set_constraint_handler_s(my_handler);

    char buf[5];
    // 这会触发约束处理函数,因为源字符串太长
    strcpy_s(buf, sizeof(buf), "Hello, World!");

    return 0;
}

缓冲区大小参数的

安全函数的缓冲区大小参数型为 rsize_t(通常是 size_t),且不能为零。传入零大小会导致运行时约束违反。

进阶用法

封装安全的字符串操作库

#include <stdio.h>
#include <string.h>
#include <stdarg.h>

// 安全的字符串复制
int safe_strcpy(char *dst, size_t dst_size, const char *src) {
    if (!dst || !src || dst_size == 0) return -1;

    size_t src_len = strnlen(src, dst_size);
    if (src_len >= dst_size) return -2; // 源字符串太长

    memcpy(dst, src, src_len + 1); // 包含终止符
    return 0;
}

// 安全的字符串追加
int safe_strcat(char *dst, size_t dst_size, const char *src) {
    if (!dst || !src || dst_size == 0) return -1;

    size_t dst_len = strnlen(dst, dst_size);
    size_t src_len = strnlen(src, dst_size);

    if (dst_len + src_len >= dst_size) return -2; // 空间不足

    memcpy(dst + dst_len, src, src_len + 1);
    return 0;
}

// 安全的格式化写入
int safe_sprintf(char *dst, size_t dst_size, const char *fmt, ...) {
    if (!dst || !fmt || dst_size == 0) return -1;

    va_list ap;
    va_start(ap, fmt);
    int len = vsnprintf(dst, dst_size, fmt, ap);
    va_end(ap);

    if (len < 0 || (size_t)len >= dst_size) return -2;
    return 0;
}

int main(void) {
    char buf[64] = "";

    safe_strcpy(buf, sizeof(buf), "Hello");
    safe_strcat(buf, sizeof(buf), ", ");
    safe_strcat(buf, sizeof(buf), "World!");

    char detail[64];
    safe_sprintf(detail, sizeof(detail), "内容: %s (长度: %zu)", buf, strnlen(buf, sizeof(buf)));
    printf("%s\n", detail);

    return 0;
}

使用静态分析工具辅助安全检查

// 编译时启用警告选项辅助发现不安全函数调用
// gcc -Wall -Wextra -Wformat=2 -Wformat-security -D_FORTIFY_SOURCE=2

#include <stdio.h>
#include <string.h>

int main(void) {
    char buf[16];

    // 编译器会在 -D_FORTIFY_SOURCE=2 时将 printf 检查为 __printf_chk
    // 并在运行时检测格式字符串攻击
    snprintf(buf, sizeof(buf), "%s", "安全输出");

    // 使用 strnlen 替代 strlen,防止未终止字符串导致越界
    const char *unknown_str = "测试字符串";
    size_t len = strnlen(unknown_str, 100); // 最多检查100个字符
    printf("长度: %zu\n", len);

    return 0;
}

运行时缓冲区溢出检测

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// 带溢出检测的缓冲区结构
typedef struct {
    char *data;
    size_t size;
    size_t capacity;
    const char *tag; // 用于调试标识
} SafeBuffer;

// 创建安全缓冲区
SafeBuffer *sbuf_create(size_t capacity, const char *tag) {
    SafeBuffer *sb = malloc(sizeof(SafeBuffer));
    if (!sb) return NULL;

    sb->data = calloc(capacity, 1);
    if (!sb->data) { free(sb); return NULL; }

    sb->size = 0;
    sb->capacity = capacity;
    sb->tag = tag;
    return sb;
}

// 安全写入数据
int sbuf_write(SafeBuffer *sb, const char *data, size_t len) {
    if (!sb || !data) return -1;

    if (sb->size + len >= sb->capacity) {
        fprintf(stderr, "[%s] 缓冲区溢出风险: 需要 %zu, 容量 %zu\n",
                sb->tag, sb->size + len, sb->capacity);
        return -2;
    }

    memcpy(sb->data + sb->size, data, len);
    sb->size += len;
    sb->data[sb->size] = '\0';
    return 0;
}

// 释放安全缓冲区
void sbuf_free(SafeBuffer *sb) {
    if (sb) {
        free(sb->data);
        free(sb);
    }
}

int main(void) {
    SafeBuffer *sb = sbuf_create(32, "用户输入");
    sbuf_write(sb, "Hello", 5);
    sbuf_write(sb, ", World!", 8);
    printf("内容: %s\n", sb->data);

    // 这会触发溢出警告
    sbuf_write(sb, " 这是一段很长的额外文字会导致溢出", 30);

    sbuf_free(sb);
    return 0;
}