环境变量安全:看不见的入侵通道

环境变量这东西,平时写代码时真不太会注意。但我要说,它其实是系统里最容易被忽视的攻击面之一。我早年做嵌入式Linux开发时,就吃过环境变量的亏——一个同事在测试脚本里随手设了个LD_PRELOAD,结果整个测试环境全乱了,查了两天才找到原因。

说白了,环境变量就是进程的「上下文」。你想想看,如果攻击者能控制这个上下文,那你的程序跑起来会是什么后果?嗯,今天我们就来聊聊这个话题。

核心观点:环境变量污染是提权攻击的经典入口。尤其是setuid程序,一旦环境变量没清理干净,就等于把系统后门拱手让人。

环境变量污染:攻击者的「隐形手」

环境变量污染,说白了就是攻击者通过修改环境变量,来影响程序的行为。为什么能成功?因为很多程序会「信任」环境变量,直接用它来决定加载哪个库、执行哪个命令。

我见过最典型的场景是这样的:一个普通用户运行了一个setuid程序,这个程序内部调用了system()popen()。如果PATH环境变量被篡改,那实际执行的命令可能根本不是开发者预期的那个。

// 危险的代码示例
#include <stdlib.h>
#include <stdio.h>

int main() {
    // 这里调用了外部命令
    system("ls -l /tmp");
    return 0;
}

这段代码有什么问题?问题大了。如果攻击者把PATH改成/tmp/malicious:$PATH,然后在/tmp/malicious/下放一个恶意的ls脚本,那你的程序就会执行这个恶意脚本。我当年在审计一个开源项目时,就发现类似的代码——还好只是内部工具,没造成实际损失。

警告:永远不要在setuid程序中直接调用system()popen()execvp()这类依赖PATH的函数。除非你明确清理了环境变量。

LD_PRELOAD攻击:最经典的库劫持

LD_PRELOAD,这玩意儿可以说是环境变量攻击里的「明星选手」。它的作用是什么?简单说,就是让动态链接器在加载共享库时,优先加载你指定的库。这意味着你可以用自己的函数「覆盖」掉系统库里的同名函数。

为什么会这样?因为动态链接器在处理LD_PRELOAD时,会把它指定的库放在所有库之前加载。如果这些库里有和标准库同名的函数,那标准库的函数就会被「遮蔽」掉。

// 攻击者编写的恶意库
#include <stdio.h>
#include <stdlib.h>

// 覆盖标准库的 rand() 函数
int rand() {
    fprintf(stderr, "rand() called - returning fixed value\n");
    return 42;  // 固定返回值
}

编译成共享库:

gcc -shared -fPIC -o malicious.so malicious.c

然后设置环境变量:

export LD_PRELOAD=./malicious.so

现在,任何调用rand()的程序都会返回42。如果这个程序是setuid的,而且依赖随机数做安全决策...嗯,后果你自己想。

我记得有一次做渗透测试,目标系统上有个setuid程序,它用rand()生成临时文件名。我通过LD_PRELOAD让rand()永远返回固定值,然后预测了临时文件路径,成功实现了符号链接攻击。整个过程不到10分钟。

防护建议:对于setuid程序,Linux内核会自动忽略LD_PRELOAD、LD_LIBRARY_PATH等环境变量。但如果你自己写setuid程序,最好还是手动清理一下——安全总比后悔好。

PATH劫持:命令执行的陷阱

PATH劫持,说白了就是让程序执行了「假」的命令。攻击者通过修改PATH环境变量,让程序优先加载攻击者指定的目录下的可执行文件。

我见过一个真实案例:某个运维工具内部调用了tcpdump来抓包,但用的是相对路径。攻击者在/tmp下放了一个同名的恶意脚本,然后修改了PATH。结果这个运维工具每次执行时,都会先执行攻击者的脚本,把抓到的数据包偷偷发到远程服务器。

// 有问题的代码
#include <unistd.h>

int main() {
    // 使用相对路径,依赖PATH
    execlp("tcpdump", "tcpdump", "-i", "eth0", NULL);
    return 0;
}

正确的做法是什么?用绝对路径:

// 安全的代码
#include <unistd.h>

int main() {
    // 使用绝对路径,不依赖PATH
    execl("/usr/sbin/tcpdump", "tcpdump", "-i", "eth0", NULL);
    return 0;
}

你可能会问:那如果攻击者替换了/usr/sbin/tcpdump本身呢?嗯,那是另一个层面的安全问题——文件权限和完整性保护。但至少,使用绝对路径能堵住PATH劫持这条路。

最佳实践:调用外部命令时,永远使用绝对路径。如果必须用相对路径,那就在程序启动时,把PATH设置成一个安全的、只包含系统目录的值。

setuid程序的环境变量清理

setuid程序,这是环境变量安全的重灾区。为什么?因为setuid程序会以文件所有者的权限运行(通常是root)。如果环境变量没清理干净,攻击者就能通过环境变量来操纵这个高权限进程。

Linux内核其实已经做了一些防护:对于setuid程序,内核会自动忽略LD_PRELOAD、LD_LIBRARY_PATH、LD_AUDIT等「危险」环境变量。但这不是万能的——有些环境变量内核不会管,比如PATH、IFS、HOME等。

我建议的做法是:在setuid程序的最开始,就手动清理环境变量。具体来说:

#include <stdlib.h>
#include <unistd.h>
#include <string.h>

// 清理环境变量,只保留必要的
void sanitize_environment() {
    // 清空所有环境变量
    clearenv();
    
    // 设置一个安全的PATH
    setenv("PATH", "/usr/bin:/bin", 1);
    
    // 设置其他必要的环境变量
    setenv("IFS", " \t\n", 1);  // 防止IFS攻击
    setenv("HOME", "/root", 1); // 明确HOME目录
    
    // 注意:不要设置任何用户可控的环境变量
}

int main(int argc, char *argv[], char *envp[]) {
    // 第一步:清理环境变量
    sanitize_environment();
    
    // 然后才是正常的程序逻辑
    // ...
    
    return 0;
}

这段代码里,我用了clearenv()清空所有环境变量,然后只设置几个绝对安全的。你可能会问:那用户需要的环境变量怎么办?嗯,对于setuid程序,用户的环境变量本来就不应该被信任。如果程序需要某些配置,应该通过配置文件或命令行参数来传递。

特别注意:IFS(Internal Field Separator)环境变量也是个攻击点。如果攻击者把IFS设置成/,那/bin/ls就会被解析成binls两个参数...你想想看,这会造成什么后果?

知识体系总览

下面这张图,是我整理的环境变量安全知识体系。你可以把它当作一个检查清单:

环境变量安全知识体系 攻击面 防御措施 LD_PRELOAD 攻击 PATH 劫持 IFS 环境变量攻击 环境变量信息泄露 使用绝对路径调用命令 setuid 程序清理环境变量 使用 secure_getenv() 白名单机制保留必要变量 环境变量 安全风险 核心原则:不信任任何外部输入的环境变量

实用检查清单

最后,我整理了一个检查清单。每次写setuid程序或涉及环境变量的代码时,可以对照着检查一遍:

检查项 说明 优先级
是否使用了绝对路径 所有外部命令调用都使用绝对路径
是否清理了环境变量 setuid程序启动时调用clearenv()
是否使用了secure_getenv() 对于setuid程序,用secure_getenv()替代getenv()
是否检查了IFS 确保IFS是安全的默认值
是否限制了PATH PATH只包含系统目录,不包含用户目录
是否使用了白名单 只保留必要的环境变量,其他全部丢弃

嗯,环境变量安全这块,说难不难,说简单也不简单。关键是要有「不信任」的意识——你永远不知道用户会设置什么环境变量。我曾经在一个项目里,就因为没清理HOME环境变量,导致setuid程序把日志写到了用户指定的目录下,结果被攻击者利用做了符号链接攻击。从那以后,我写setuid程序的第一行代码永远是clearenv()

记住一句话:环境变量是用户可控的输入,和命令行参数、文件内容一样,都不应该被信任。尤其是setuid程序,你多一分谨慎,系统就多一分安全。


公众号:蓝海资料掘金营,微信 deep3321