我们在系统中设置了大量网络安全钩子,而如今,由于克劳德斯特莱克(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内核集成可能很困难,但它针对一系列内存问题确实有更强的防护能力。