thb*_*thb 17 linux security memory
在我的 Debian GNU/Linux 9 系统上,当执行二进制文件时,
为什么?
我假设零初始化提高了安全性,但是,如果对于堆,那么为什么不也为堆栈呢?堆栈也不需要安全性吗?
据我所知,我的问题并不是针对 Debian 的。
示例 C 代码:
#include <stddef.h>
#include <stdlib.h>
#include <stdio.h>
const size_t n = 8;
// --------------------------------------------------------------------
// UNINTERESTING CODE
// --------------------------------------------------------------------
static void print_array(
const int *const p, const size_t size, const char *const name
)
{
printf("%s at %p: ", name, p);
for (size_t i = 0; i < size; ++i) printf("%d ", p[i]);
printf("\n");
}
// --------------------------------------------------------------------
// INTERESTING CODE
// --------------------------------------------------------------------
int main()
{
int a[n];
int *const b = malloc(n*sizeof(int));
print_array(a, n, "a");
print_array(b, n, "b");
free(b);
return 0;
}
Run Code Online (Sandbox Code Playgroud)
输出:
a at 0x7ffe118997e0: 194 0 294230047 32766 294230046 32766 -550453275 32713
b at 0x561d4bbfe010: 0 0 0 0 0 0 0 0
Run Code Online (Sandbox Code Playgroud)
当然,C 标准malloc()
在分配内存之前并不要求清除内存,但我的 C 程序仅用于说明。问题不是关于 C 或关于 C 的标准库的问题。相反,问题是关于为什么内核和/或运行时加载器将堆清零而不是堆栈清零的问题。
另一个实验
我的问题是关于可观察的 GNU/Linux 行为,而不是标准文档的要求。如果不确定我的意思,请尝试以下代码,它会调用进一步的未定义行为(未定义,即就 C 标准而言)来说明这一点:
a at 0x7ffe118997e0: 194 0 294230047 32766 294230046 32766 -550453275 32713
b at 0x561d4bbfe010: 0 0 0 0 0 0 0 0
Run Code Online (Sandbox Code Playgroud)
我机器的输出:
0x555e86696010 0 1
0x555e86696010 0 1
0x555e86696010 0 1
0x555e86696010 0 1
Run Code Online (Sandbox Code Playgroud)
就 C 标准而言,行为是未定义的,所以我的问题与 C 标准无关。调用malloc()
不需要每次都返回相同的地址,但是由于这个调用malloc()
确实每次都返回相同的地址,有趣的是注意到堆上的内存每次都被清零。
相比之下,堆栈似乎没有归零。
我不知道后面的代码会在你的机器上做什么,因为我不知道 GNU/Linux 系统的哪一层导致了观察到的行为。你可以试试。
更新
@Kusalananda 在评论中观察到:
值得一提的是,您的最新代码在 OpenBSD 上运行时会返回不同的地址和(偶尔)未初始化(非零)数据。这显然没有说明您在 Linux 上看到的行为。
我的结果与 OpenBSD 上的结果不同确实很有趣。显然,我的实验没有发现内核(或链接器)安全协议,正如我所想的那样,而只是一个实现工件。
有鉴于此,我相信@mosvy、@StephenKitt 和@AndreasGrapentin 的以下答案共同解决了我的问题。
另请参阅堆栈溢出:为什么 malloc 将 gcc 中的值初始化为 0?(信用:@bta)。
mos*_*svy 30
malloc() 返回的存储不是零初始化的。永远不要假设它是。
在您的测试程序中,这只是侥幸:我猜malloc()
刚刚得到了一个新的块mmap()
,但也不要依赖它。
例如,如果我以这种方式在我的机器上运行您的程序:
$ echo 'void __attribute__((constructor)) p(void){
void *b = malloc(4444); memset(b, 4, 4444); free(b);
}' | cc -include stdlib.h -include string.h -xc - -shared -o pollute.so
$ LD_PRELOAD=./pollute.so ./your_program
a at 0x7ffd40d3aa60: 1256994848 21891 1256994464 21891 1087613792 32765 0 0
b at 0x55834c75d010: 67372036 67372036 67372036 67372036 67372036 67372036 67372036 67372036
Run Code Online (Sandbox Code Playgroud)
你的第二个例子只是malloc
在 glibc 中暴露了一个实现的工件;如果您使用大于 8 个字节的缓冲区重复malloc
/ free
,您将清楚地看到只有前 8 个字节被清零,如下面的示例代码所示。
#include <stddef.h>
#include <stdlib.h>
#include <stdio.h>
const size_t n = 4;
const size_t m = 0x10;
int main()
{
for (size_t i = n; i; --i) {
int *const p = malloc(m*sizeof(int));
printf("%p ", p);
for (size_t j = 0; j < m; ++j) {
printf("%d:", p[j]);
++p[j];
printf("%d ", p[j]);
}
free(p);
printf("\n");
}
return 0;
}
Run Code Online (Sandbox Code Playgroud)
输出:
0x55be12864010 0:1 0:1 0:1 0:1 0:1 0:1 0:1 0:1 0:1 0:1 0:1 0:1 0:1 0:1 0:1 0:1
0x55be12864010 0:1 0:1 1:2 1:2 1:2 1:2 1:2 1:2 1:2 1:2 1:2 1:2 1:2 1:2 1:2 1:2
0x55be12864010 0:1 0:1 2:3 2:3 2:3 2:3 2:3 2:3 2:3 2:3 2:3 2:3 2:3 2:3 2:3 2:3
0x55be12864010 0:1 0:1 3:4 3:4 3:4 3:4 3:4 3:4 3:4 3:4 3:4 3:4 3:4 3:4 3:4 3:4
Run Code Online (Sandbox Code Playgroud)
Ste*_*itt 25
无论堆栈如何初始化,您都看不到原始堆栈,因为 C 库在调用 之前做了很多事情main
,并且它们会接触堆栈。
对于 GNU C 库,在 x86-64 上,执行从_start入口点开始,该入口点调用__libc_start_main
进行设置,而后者最终调用main
. 但是在调用 之前main
,它会调用许多其他函数,这会导致将各种数据写入堆栈。堆栈的内容不会在函数调用之间清除,因此当您进入 时main
,您的堆栈包含先前函数调用的剩余部分。
这仅解释了您从堆栈中获得的结果,请参阅有关您的一般方法和假设的其他答案。
Tob*_*ght 20
在这两种情况下,您都会获得未初始化的内存,并且无法对其内容做出任何假设。
当操作系统必须将新页面分配给您的进程时(无论是用于其堆栈还是用于 所使用的领域malloc()
),它保证不会暴露来自其他进程的数据;确保这一点的常用方法是用零填充它(但用其他任何东西覆盖它同样有效,甚至包括一页/dev/urandom
——事实上,一些调试malloc()
实现编写了非零模式,以捕捉错误的假设,比如你的假设)。
如果malloc()
可以满足这个进程已经使用和释放的内存的请求,它的内容就不会被清除(实际上,清除是无关紧要的,malloc()
也不能——它必须在内存映射到之前发生您的地址空间)。您可能会获得先前由您的进程/程序写入的内存(例如 before main()
)。
在您的示例程序中,您会看到malloc()
该进程尚未写入的区域(即它直接来自新页面)和已写入的堆栈(通过main()
程序中的预代码)。如果你检查更多的堆栈,你会发现它在更远的地方(在它的增长方向上)是零填充的。
如果你真的想了解情况,在操作系统层面上,我建议你绕过C库层和交互使用系统调用,如brk()
和mmap()
代替。
小智 9
你的前提是错误的。
您所描述的“安全性”实际上是机密性,这意味着任何进程都不能读取另一个进程的内存,除非这些内存在这些进程之间明确共享。在操作系统中,这是隔离并发活动或进程的一个方面。
操作系统为确保这种隔离所做的工作是,每当进程请求内存用于堆或堆栈分配时,该内存要么来自物理内存中被零填充的区域,要么来自充满垃圾的区域来自同一个过程。
这将确保你只看到过零,或者你自己的垃圾,所以保密性得到保证,并且都堆和栈是“安全”的,虽然不一定(零)初始化。
你对你的测量读得太多了。
归档时间: |
|
查看次数: |
5699 次 |
最近记录: |