首页
社区
课程
招聘
[原创]部分CWE漏洞代码模式(漏洞格式)列举
发表于: 2025-12-3 14:40 388

[原创]部分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
// 经典的文件操作TOCTOU
int 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);
}

[培训]Windows内核深度攻防:从Hook技术到Rootkit实战!

收藏
免费 0
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回