-
-
[原创]部分CWE漏洞代码模式(漏洞格式)列举
-
发表于: 2025-12-3 14:40 388
-
序
最近上课作业让列举一些常见漏洞的漏洞格式(符合一般开发者开发习惯、同时能引发漏洞的代码模式),发现列举出来对理解漏洞有一定帮助,所以报告写出来之后这里记录一下,本次报告涉及的漏洞都是二进制常见漏洞,其中cwe-366做了稍微详细的介绍,其他的主要在注释里面介绍了。
漏洞列表:
–CWE-125 Out-of-bounds Read
– CWE-190 Integer Overflow or Wraparound
– CWE-366 Race Condition within a Thread
– CWE-367 Time-of-check Time-of-use Race Condition
– CWE-416 Use After Free / CWE-415 Double Free
– CWE-441 Unintended Proxy or Intermediary
– CWE-476 NULL Pointer Dereference
– CWE-787 Out-of-bounds Write
1. CWE-125: Out-of-bounds Read(越界读)
1 2 3 4 5 6 7 8 9 10 | void process_data(const char* input) { char buffer[100]; strcpy(buffer, input); // 假设输入可控 // 常见的越界读:很多开发者习惯用strlen判断长度后直接访问 if (strlen(buffer) < 150) { // 这里认为150是安全的上限 char ch = buffer[120]; // 越界读!buffer只有100字节 printf("第120个字符是: %c\n", ch); // 读取了栈上其他变量或返回地址 }} |
2. CWE-190: Integer Overflow or Wraparound(整数溢出)
1 2 3 4 5 6 7 8 9 10 11 12 13 | // 常见的动态内存分配场景void* allocate_buffer(unsigned int width, unsigned int height) { unsigned int size = width * height; // 开发者常忽略溢出 // 例如 width=0x10000, height=0x10000 → size 会回绕成很小的数 return malloc(size); // 分配极小的内存,但后续按大图使用}void vulnerable_image_process(unsigned int w, unsigned int h) { void* buf = allocate_buffer(w, h); if (buf) { memset(buf, 0, w * h); // 再次溢出,导致堆溢出或只清了一小部分 }} |
3. CWE-366: Race Condition within a Thread(同一线程内的竞态条件)
1 2 3 4 5 6 7 8 9 10 11 12 | // 单线程中常见的“检查后使用”错误(很多新手会这样写)static int initialized = 0;static SomeComplexObject* g_obj = NULL;void lazy_init() { if (!initialized) { // 检查 usleep(100000); // 模拟耗时初始化(教学用) g_obj = create_complex_object(); initialized = 1; // 使用(但中间可能被中断/调度) } g_obj->do_something(); // 如果线程被抢占并重入,可能g_obj还是NULL} |
CWE-366(Race Condition within a Thread) 是这几个 CWE 里面最容易让人迷惑的一个,因为大家一听到“Race Condition”(竞态条件),第一反应都是“多线程”,但 CWE-366 特指 “单线程内部” 出现的竞态。
为什么单线程里也会有“竞态”?
现代操作系统和 CPU 随时可能在 你的函数还没执行完 的时候,把 CPU 让给其他线程/进程,或者触发信号处理函数、调试器、定时器、异步回调等。如果你写的函数在执行过程中 被自己再次进入(重入),就产生了“单线程竞态”。
最常见的两种触发场景:
信号处理函数(signal handler)中调用了同一个函数
某些库的回调(如 atexit、pthreads 的 cleanup handler、某些 GUI 框架的事件循环)导致函数重入
经典真实例子
1 2 3 4 5 6 7 8 9 10 11 12 13 | // 全局变量static int inited = 0;static SomeObj* global_obj = NULL;// 懒加载初始化(很多老项目都这么写)SomeObj* get_global_obj() { if (!inited) { // 第1次检查 global_obj = malloc(sizeof(SomeObj)); memset(global_obj, 0, sizeof(SomeObj)); expensive_init(global_obj); // 这步很慢,可能要几百毫秒 inited = 1; // 标记已初始化 } return global_obj; // 使用} |
看起来没问题对吧?
但如果这个函数被 信号处理函数 重入,就会出事:
1 2 3 4 5 6 7 8 9 10 11 12 | void signal_handler(int sig) { SomeObj* obj = get_global_obj(); // 信号来了,又调用了一次! obj->do_something();}int main() { signal(SIGUSR1, signal_handler); SomeObj* p = get_global_obj(); // 第一次调用,开始慢初始化 // 此时如果有人 kill -USR1 给这个进程 // → 信号处理函数里又调用 get_global_obj() // → 此时 inited 还是 0,global_obj 可能还是 NULL 或半初始化的 // → 要么返回 NULL,要么返回一个“正在构造中”的损坏对象} |
这就是典型的 CWE-366:
同一个线程(主线程)里,函数还没执行完,就被外部事件(信号)打断并再次进入,导致“检查”和“使用”之间状态不一致。
为什么 MITRE 要单独把这个列为 CWE-366?
因为:
它和传统多线程竞态(CWE-362)不一样,不需要锁也能触发
很多开发者完全想不到“单线程也会不安全”
修复方法也不一样(不能靠互斥锁,得用标志位或其他重入保护)
正确的写法(防重入)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | static volatile sig_atomic_t inited = 0; // 用 sig_atomic_t 防止信号中读到中间值SomeObj* get_global_obj_safe() { if (inited == 0) { if (__sync_bool_compare_and_swap(&inited, 0, 1)) { // 原子地从0改为1 // 只有抢到“初始化权”的那次才真正初始化 global_obj = create_and_init(); inited = 2; // 标记彻底完成 } else { // 其他重入的调用在这里忙等 while (inited != 2) sched_yield(); } } return global_obj; } |
总结一句话记住 CWE-366:
“单线程里,函数还没执行完就被自己再次进入,导致共享变量/对象处于‘半初始化’状态,这就是 CWE-366。”
常见触发点:信号处理函数、atexit、GUI 事件回调、某些 C 库的钩子函数等。
4. CWE-367: Time-of-check Time-of-use (TOCTOU)(检查-使用时间差竞态)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | // 经典的文件操作TOCTOUint read_config(const char* username) { char path[256]; snprintf(path, sizeof(path), "/home/%s/config", username); if (access(path, R_OK) == 0) { // 检查:文件是否存在且可读 usleep(200000); // 模拟处理延迟(实际可能更隐蔽) // 在这段时间内,攻击者可能把path替换成符号链接指向/etc/shadow FILE* f = fopen(path, "r"); // 使用:实际打开的是攻击者指定的文件 if (f) { // 读取敏感信息 } } return 0;} |
5. CWE-416 Use After Free & CWE-415 Double Free
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | struct Node { int data; struct Node* next;};void use_after_free_example() { struct Node* ptr = malloc(sizeof(struct Node)); ptr->data = 42; free(ptr); // 第一次释放 // 开发者常见错误:忘记置NULL,继续使用 ptr->data = 100; // CWE-416: Use After Free free(ptr); // CWE-415: Double Free(如果没置NULL) // 很多开发者会这样“保险”地再free一次} |
6. CWE-441: Unintended Proxy or Intermediary(非预期的代理/中间人)
1 2 3 4 5 6 7 8 9 10 11 | // 典型的未验证的HTTPS证书导致的中间人代理(常见于移动开发)void bad_https_request() { CURL* curl = curl_easy_init(); curl_easy_setopt(curl, CURLOPT_URL, "f23K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6S2M7r3W2Q4x3X3g2T1j5h3&6C8i4K6u0W2j5$3!0E0i4K6u0r3N6s2u0S2L8Y4y4X3k6i4t1`."); // 开发者为了“解决证书问题”直接关闭验证(非常常见!) curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L); curl_easy_perform(curl); // 此时完全可能被公司/校园代理MITM} |
7. CWE-476: NULL Pointer Dereference(空指针解引用)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | char* get_username() { char* name = malloc(32); if (!name) return NULL; // 忘记检查strcpy返回值,或者假设一定成功 strcpy(name, getenv("USER")); return name; // 可能返回NULL}void null_deref_example() { char* user = get_username(); // 开发者常见习惯:拿到指针就直接用,不再检查 printf("用户长度: %zu\n", strlen(user)); // 如果返回NULL → 崩溃 free(user);} |
8. CWE-787: Out-of-bounds Write(越界写)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | void vulnerable_copy(const char* src) { char dest[100]; // 绝大多数开发者都写过类似的代码 for (int i = 0; i < strlen(src); i++) { dest[i] = src[i]; // 没有检查边界 } dest[99] = '\0'; // 试图“安全”地结束,但如果src≥100字节,已经溢出}void another_common_pattern(const char* input) { char buffer[64]; // 常见错误:用sprintf而不是snprintf(老项目特别多) sprintf(buffer, "Welcome, %s!", input); // input过长 → 栈溢出 printf("%s\n", buffer);} |