让我们谈谈测试、C 和 C++:CrowdStrike 错误是指针问题吗?

作者:微信公众号:【架构师老卢】
11-21 8:38
11

我们在系统中设置了大量网络安全钩子,而如今,由于克劳德斯特莱克(Crowdstrike)漏洞的出现,一些系统架构师可能会认为部署它们风险太大。这着实令人遗憾,因为这些钩子往往触及操作系统的核心,能够检测到网络传感器根本没机会察觉的情况。

C/C++指针问题 我之前教授过C和C++编程,甚至还就此主题写过几本书。授课时,在讲到指针之前一切都进展顺利,但一涉及指针,很多学生会对使用指针的关键概念感到吃力。因为使用指针时,我们可以随意操作内存,对于内存中能访问的内容没有什么限制。我们还能很随意地将字符串转换为整数,反之亦然。总体而言,对于那些不熟悉变量在内存中如何分配的人来说,C和C++可能会成为引发灾难的源头。

克劳德斯特莱克(Crowdstrike)漏洞 关于克劳德斯特莱克漏洞到底出了什么问题,目前尚未达成强烈共识,但有一点是确定的:这归根结底是缺乏恰当测试导致的。虽然测试可能既复杂又昂贵,但一家将其软件安装在系统核心位置的公司,绝不应该让这么多系统崩溃。对许多公司来说,测试过程可能比开发阶段耗时长得多。

就此而言,测试人员通常不应是编写代码的那个人。而且,测试人员很可能是有着丰富编码经验的人,知道如何逐行检查代码。通常这还包括分析生成的机器代码。除此之外,我们还会看到在各种各样的环境中进行浸泡测试,如果任何一项测试失败,就会暂停部署,直至问题得到解决。

或许2024年6月所报告的一个漏洞就凸显了缺乏恰当测试这一问题[此处]。

那么,让我们深入探讨一下指针、C和C++,看看能否从中找出可能导致问题的一些线索。

空指针(NULL pointer) 在C和C++中,指针包含一个内存位置的地址。因此,如果一个32位整数存储在内存位置0x29e574b6400处,那么这个整数的四个字节将存储在该地址及其后面三个字节位置上。这个存储区域被称为堆,是用于数据存储的通用区域。要在堆上分配一些空间,我们可以这样做:

int * ptr = new int;

它会在堆上分配内存,并返回一个指向该内存位置的指针。然后可以使用以下方式设置该值:

#include <iostream>
using namespace std;

int main() {

    int *ptr = new int;
    *ptr = 42;
    std::cout << ptr << std::endl;
    std::cout << *ptr << std::endl;
}

一次示例运行的输出如下:

0x29e574b6400
42

使用完之后,我们可以用以下方式释放内存:

0x1f2b7246400
22
0x1f2b7246400
-1223466864

我们可以看到指针仍然存在,但数据已经消失了。要移除这个指针,现在我们需要将其赋值为NULL

#include <iostream>
using namespace std;

int main() {

    int *ptr = new int;
    *ptr = 42;
    std::cout << ptr << std::endl;
    std::cout << *ptr << std::endl;
    delete ptr;
    std::cout << ptr << std::endl;
    std::cout << *ptr << std::endl;
    ptr = NULL;
    std::cout << ptr << std::endl;
    std::cout << *ptr << std::endl;
}

NULL的值为零,所以我们将指向一个未分配的内存区域。

未初始化指针 当我们创建了一个指针却未对其进行初始化时,就可能会出现严重问题。每次运行程序时,它都会创建一个新的内存位置,并使用该特定时刻该位置内存中的任何内容。例如,如果我们声明了一个ptr指针,却不给它分配任何值:

#include <iostream>
using namespace std;

int main() {

    int *ptr;

    std::cout << ptr << std::endl;
    std::cout << *ptr << std::endl;

}

如果我们运行三次,就会发现每次的内存位置都不一样,而且每个位置存储的都是随机数据:

C:\>a.exe
0x1b3028563c0
42295441

C:\>a.exe
0x2a0439063c0
1133535377

C:\>a.exe
0x1d589c263c0
-1983749999

因此,如果我们从未初始化的位置检索数据,就会得到不可预测的结果,进而可能导致程序崩溃。这样一来,我们的程序可能平时运行良好,但在某些时候,内存中的值可能会在代码的其他部分引发异常。

指针越界(Pointer overrun) 指针也可以指向数组的起始位置。在这种情况下,我们将为10个整数值预留空间,并以ptr作为内存分配的起始位置:

#include <iostream>
using namespace std;

int main() {

    int *ptr = new int[10];
    ptr[0] = 20;

    std::cout << ptr[0] << std::endl;

}

第一个元素的索引是0,最后一个元素的索引是9。如果我们错误地给第10个元素分配一个值,就会访问到未分配的空间:

#include <iostream>
using namespace std;

int main() {

    int *ptr = new int[10];
    ptr[10] = 20;

    std::cout << ptr[10] << std::endl;

}

这样可能会得到正确的结果,但我们已经将值写入了可能分配给其他变量的区域,从而可能导致程序崩溃。

缓冲区下溢(Buffer underrun) 当我们分配了一块内存区域,但尚未在该已分配空间中存储数据时,就会出现缓冲区下溢的情况。“心脏出血”(HeartBleed)漏洞就出现过这种类型的错误,程序向内存写入数据时并未覆盖原有内存内容,然后又从内存中读取这些数据。由于实际上并没有写入数据,程序返回的就是当时内存中的内容,而这可能是加密密钥或密码等信息。

在一个简单示例中,我们有一个可容纳10个字符的缓冲区,本应向其中写入一些数据,但它却处于数据未初始化(或仍保留着之前的数据)的状态:

#include <iostream>
using namespace std;
#include <cstring>

int main() {

    char *ptr = new char[10];

    // strcpy(ptr, "012345678"); 

    for (int i = 0; i < 10; i++) {
        std::cout << ptr[i];
    }

}

一次示例运行的输出如下:

C:\>a.exe
É`Çr   P

那么克劳德斯特莱克(Crowdstrike)的情况呢? 嗯,空指针异常很可能在测试过程中就会被捕获,但未初始化的数据区域在其运行时是不可预测的,这完全取决于特定时刻内存中的内容。在测试时,我们可能处于一个相当干净的环境中,程序可能不会检测到不良数据。我明白要将Rust与Windows内核集成可能很困难,但它针对一系列内存问题确实有更强的防护能力。

相关留言评论
昵称:
邮箱:
阅读排行