未初始化的变量值是否会存在安全风险?

14
在学习 C 语言时,我犯了一些错误,打印了未初始化的字符数组元素。当我将数组大小扩大到相当大,比如 100 万个元素大小,然后打印内容时,输出的结果不总是用户无法阅读的,但似乎包含了一些运行时信息。考虑以下代码:
#include <stdio.h>
main() {

        char s[1000000];
        int c, i;

        printf("Enter input string:\n");
        for (i = 0; ( c = getchar()) != '\n'; i++) {
                s[i] = c;
        }   

        printf("Contents of input string:\n");
        for (i = 0; i < 999999; i++) {
                putchar(s[i]);
        }   
        printf("\n");

        return 0;
}

浏览输出,我发现以下内容:

???l????????_dyldVersionNumber_dyldVersionString_dyld_all_image_infos_dyld_fatal_error_dyld_shared_cache_ranges_error_string__mh_dylinker_header_stub_binding_helper_dyld_func_lookup_offset_to_dyld_all_image_infos__dyld_start__ZN13dyldbootstrapL30randomizeExecutableLoadAddressEPK12macho_headerPPKcPm__ZN13dyldbootstrap5startEPK12macho_headeriPPKcl__ZN4dyldL17setNewProgramVarsERK11ProgramVars__ZN4dyld17getExecutablePathEv__ZN4dyld22mainExecutablePreboundEv__ZN4dyld14mainExecutableEv__ZN4dyld21findImageByMachHeaderEPK11mach_header__ZN4dyld26findImageContainingAddressEPKv

还有,

Apple Inc.1&0$U ?0?*?H??ot CA0?"0ple Certification Authority10U ?䑩 ??GP??^y?-?6?WLU????Kl??"0?>?P ?A?????f?$kУ????z ?G?[?73??M?i??r?]?_???d5#KY?????P??XPg? ?ˬ, op??0??C??=?+I(??ε??^??=?:??? ?b??q?GSU?/A????p??LE~LkP?A??tb
?!.t?< ?A?3???0X?Z2?h???es?g^e?I?v?3e?w??-??z0?v0U?0U?0?0U+?iG?v ??k?.@??GM^0U#0?+?iG?v ??k?.@??GM^0?U 0?0? ?H??cd0??0+https://www.apple.com/appleca/0?+0????Reliance on this certificate by any party assumes acceptance of the then applicable standard terms and conditions of use, certificate poli?\6?L-x?팛??w??v?w0O????=G7?@?,Ա?ؾ?s???d?yO4آ>?x?k??}9??S ?8ı??O 01?H??[d?c3w?:,V??!ںsO??6?U٧??2B???q?~?R??B$*??M?^c?K?P????????7?uu!0?0??0

我甚至看到过一次打印出了我的$PATH环境变量。

未初始化变量的内容是否会构成安全风险?

更新1

激励

更新2

从答案中可以清楚地看出,这确实是一种安全风险。这让我很惊讶。

难道没有一种方法可以使程序声明其内存内容受保护,以允许操作系统限制对其的访问,除了初始化该内存的程序之外的任何访问?


7
一个未初始化的变量的内容可以做任何事情。就有可能因为其中一个变量导致世界末日的发生。 - Raveline
使用未初始化的变量会导致未定义行为,因此可能发生任何事情,包括安全风险、程序崩溃或其他不可预见的情况。但是很少会发生这样的事情。 - Alok Save
是的,通常有特殊的加密版本的内存处理,确保在使用后清除值或永远不写入交换文件。使用calloc()将预先清零内存,在大多数Unix平台上,malloc()也会执行相同的操作。 - Martin Beckett
@MartinBeckett 大多数 Unix 平台,在我经常使用的三个平台上,malloc 不会将数据清零。但是它们通常会在交付内存页面之前清理它们。 - Tom Tanner
@TomTanner,我曾认为gcc/linux stdlib会这样做,但显然情况比那更复杂一些。https://dev59.com/mWsz5IYBdhLWcg3wQFUq - Martin Beckett
回答您在更新2中的问题:是的!分配给进程的任何内存都限制在该进程中,其他进程无法访问。如果此进程处理敏感数据,则应在处理完该数据后清除内存。在分配时初始化内存对于您进程的安全性没有任何帮助。 - Ioan
6个回答

12
大多数C程序使用malloc来分配内存。普遍的误解是malloc会将返回的内存清零,但事实并非如此。
因此,由于内存块被“循环利用”,很可能得到一个带有“值”信息的内存块。
这种漏洞的一个例子是Solaris上的tar程序,它输出了/etc/passwd的内容。原因是分配给tar从磁盘读取一个数据块的内存没有初始化,在获取该内存块之前,tar实用程序会进行一次操作系统系统调用来读取/etc/passwd。由于内存回收和tar未初始化内存块的事实,/etc/passwd的片段被打印到日志中。通过使用calloc替换malloc,问题得到了解决。
如果您不明确且正确地初始化内存,则这是安全隐患的实际示例。
因此,请确保正确初始化内存。
更新:

是否有办法让程序声明其内存内容受保护,以使操作系统限制对其的访问,除非是初始化该内存的程序?

答案是肯定的(见最后一段)和否定的。
我认为你的看法有误。更恰当的问题可能是,例如,为什么malloc不在请求时初始化内存或在释放时清除内存,而是循环利用它?
答案是API的设计者明确决定不初始化(或清除内存),因为对大块内存进行此操作1)会影响性能,2)并不总是必要(例如,您可能不处理实际上不关心是否暴露数据的应用程序或多个应用程序部分)。因此,设计者决定不这样做,因为这会意外地影响性能,并将球留给程序员来决定。
因此,将此应用到操作系统中,为什么清除页面的责任应该由操作系统承担呢?你期望操作系统及时地提供内存,但确保安全措施则是程序员的责任。

话虽如此,也有一些机制可供使用,例如在Linux中使用mlock,以确保敏感数据不存储在交换空间。

mlock() 和 mlockall() 可以锁定进程的部分或全部虚拟地址空间,防止这些内存被分页到交换区。munlock() 和 munlockall() 执行相反的操作,分别解锁调用进程的部分或全部虚拟地址空间,因此如果内核内存管理器需要,指定虚拟地址范围中的页面可以再次被交换出去。内存锁定和解锁以整个页面为单位执行。


安全性是操作系统的责任。现代操作系统在将内存页面交给应用程序之前会清除它们。然而,malloc不一定从操作系统请求新的内存页面,而是经常重复使用先前由进程释放的内存。来自未初始化内存的任何非零值都来自您自己的进程,而不是另一个进程(除非您的系统配置了类似CONFIG_MMAP_ALLOW_UNINITIALIZED的东西)。 - Lie Ryan

8

是的,至少在数据可能被传输给外部用户的系统上。

已经发生了一系列攻击,针对Web服务器(甚至iPod),通过让它转储来自其他进程的内存内容,从而获取操作系统的类型和版本、其他应用程序中的数据甚至像密码表这样的详细信息。


1
世界上充满了糟糕的操作系统! - Martin Beckett
1
分配给正在运行的进程的内存对另一个进程不可访问,但一旦释放,它可以很好地分配给另一个进程。这就是为什么原始进程在释放之前需要负责清除它(如果需要)。 - Ioan
1
@loan 特别是页面/交换文件是数据泄漏的臭名昭著来源。 - Martin Beckett
@MartinBeckett 这意味着任何在正常操作系统运行期间可以访问该文件的随机过程,我想不到合法的理由/情况,也不应该允许。 - Ioan
@loan,问题更多的是之前的进程可能已经将任何可能的秘密数据写入了交换文件,当进程退出或机器重新启动时没有被清除。您的新进程获取了一些交换内存,其中潜在地包含旧数据。 - Martin Beckett
显示剩余3条评论

4

在内存区域中执行一些敏感工作然后不清除该缓冲区是完全可能的。

以后的调用可以通过调用 malloc() 或通过检查堆(通过未初始化的缓冲区/数组声明)来检索该未清除的工作。 它可能会检查它(恶意地)或无意中复制它。 如果您正在进行任何敏感操作,因此在处理它之前清除该内存是有意义的(memset()或类似的方法),并且也许在使用/复制之前清除它。


这是问题的最佳解决方案和近似解。我看到的唯一其他事情是一个进程异常终止并且无法清除内容。在这种情况下,操作系统按原样释放内存,并可能带来风险。 - Ioan

1

来自C标准:

6.7.8 初始化

“如果一个具有自动存储持续期的对象没有被显式初始化,那么它的值是不确定的。”

不确定的值被定义为:

 either an unspecified value or a trap representation.

陷阱表示法的定义如下:

某些对象表示不需要表示对象类型的值。如果对象的存储值具有这种表示,并且被lvalue表达式读取,该表达式没有字符类型,则行为未定义。如果这样的表示是由副作用产生的,该副作用通过lvalue表达式修改对象的全部或任何部分,而该表达式没有字符类型,则行为未定义。41)这种表示称为陷阱表示。

访问这样的值会导致未定义的行为,并可能构成安全威胁。

本文攻击未初始化变量可以提供一些关于如何利用系统的见解。


0

如果您关心安全问题,最安全的方法是始终初始化您要使用的每个变量。这甚至可能帮助您找到一些错误。 虽然有一些不初始化内存的好理由,但在大多数情况下,初始化每个变量/内存都是一件好事。


0

读取未初始化的内存会导致未定义的行为。请记住,被初始化的含义取决于特定类型的不变量。例如,某些指针必须是非空的,某些枚举必须来自有效范围,或者某些参数必须是2的幂。复合结构的情况更加复杂。任意字节序列可能不表示有效对象。这就是为什么清零内存是不够的。如果期望的不变量被破坏,依赖它的某些代码路径将以未定义的方式运行,并可能构成安全问题。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接