Linux C中system函数安全使用注意事项?

99ANYc3cd6
预计阅读时长 24 分钟
位置: 首页 C语言 正文

system() 函数是什么?

system() 是 C 标准库中的一个函数,定义在 <stdlib.h> 头文件中,它的作用是调用系统的命令解释器(shell)来执行一个指定的命令字符串

linux c语言system
(图片来源网络,侵删)

你可以在你的 C 程序中执行任何你在 Linux 终端(命令行)中可以输入的命令。

函数原型

#include <stdlib.h>
int system(const char *command);

参数:

  • command: 一个指向以 null 结尾的字符串的指针,这个字符串就是你想要在 shell 中执行的命令,如果参数是 NULLsystem() 函数会返回一个非零值,表示当前系统的 shell 是否可用(/bin/sh 是否存在且可执行)。

返回值: system() 的返回值比较复杂,它取决于调用的命令是否执行成功,返回值是 waitpid() 函数返回的状态值的“包装”,具体解释如下:

  1. commandNULL:

    linux c语言system
    (图片来源网络,侵删)
    • shell 可用(/bin/sh 存在且可执行),返回一个非零值。
    • shell 不可用,返回 0
  2. command 不是 NULL:

    • 如果无法启动 shell/bin/sh 不存在),返回 -1
    • 如果成功启动了 shellsystem() 会等待 shell 命令执行完毕,然后返回一个由 waitpid() 获取的状态码,这个状态码需要使用 WIFEXITEDWEXITSTATUS 宏来解析:
      • WIFEXITED(status): 如果子进程(即 shell)正常终止,这个宏返回真。
      • WEXITSTATUS(status): WIFEXITED 为真,这个宏返回子进程的退出状态。这就是我们通常关心的命令是否执行成功的状态码,对于 shell 而言,它返回的是你所执行命令的退出码。
        • 退出码为 0 通常表示命令成功执行。
        • 0 的退出码通常表示命令执行失败或出现了错误。

工作原理

system() 函数的实现原理大致如下(它比你想象的要复杂):

  1. 检查 command 是否为 NULL:如果是,则检查 shell 是否存在。
  2. 调用 fork():创建一个子进程来执行命令,父进程会等待子进程结束。
  3. 在子进程中
    • 调用 signal(SIGINT, SIG_IGN)signal(SIGQUIT, SIG_IGN),忽略 Ctrl+CCtrl+\ 信号,这是为了防止用户中断子进程时也意外中断了父进程。
    • 调用 execve() 函数,加载并执行 /bin/sh,并将 command 字符串作为 sh 的参数传递给它。system("ls -l") 会让子进程执行 /bin/sh -c "ls -l"
  4. 在父进程中
    • 调用 waitpid() 或类似的函数来等待子进程的终止。
  5. 恢复信号处理:子进程结束后,父进程会恢复之前被忽略的信号处理方式。
  6. 返回状态system() 最终返回 waitpid() 获取的状态。

代码示例

示例 1:执行简单命令

这个例子执行 ls -l 命令,并打印其返回值。

#include <stdio.h>
#include <stdlib.h>
int main() {
    int ret;
    printf("Executing command: ls -l\n");
    // 执行 "ls -l" 命令
    ret = system("ls -l");
    // 解析返回值
    if (ret == -1) {
        perror("system"); // 如果启动 shell 失败
    } else {
        // WIFEXITED 检查子进程是否正常退出
        if (WIFEXITED(ret)) {
            // WEXITSTATUS 获取子进程的退出码
            int exit_status = WEXITSTATUS(ret);
            printf("Command exited with status: %d\n", exit_status);
            if (exit_status == 0) {
                printf("Command executed successfully.\n");
            } else {
                printf("Command failed with error code %d.\n", exit_status);
            }
        }
    }
    return 0;
}

编译和运行:

linux c语言system
(图片来源网络,侵删)
gcc -o system_example system_example.c
./system_example

你会看到终端输出了当前目录的详细列表,并且程序会打印出命令的退出状态 0

示例 2:执行一个会失败的命令

这个例子执行一个不存在的命令 foo_bar

#include <stdio.h>
#include <stdlib.h>
int main() {
    int ret;
    printf("Executing command: foo_bar\n");
    ret = system("foo_bar"); // 这个命令肯定会失败
    if (ret == -1) {
        perror("system");
    } else {
        if (WIFEXITED(ret)) {
            int exit_status = WEXITSTATUS(ret);
            printf("Command 'foo_bar' exited with status: %d\n", exit_status);
            // shell 会返回 127,表示 "command not found"
            if (exit_status == 127) {
                printf("The command was not found.\n");
            }
        }
    }
    return 0;
}

运行结果会显示 Command 'foo_bar' exited with status: 127

安全性:为什么 system() 很危险?

system() 的最大问题是命令注入(Command Injection),如果你的程序中,命令的任何部分来自用户输入,那么你就必须非常小心。

危险示例:

假设你写一个程序,让用户输入用户名,然后显示该用户的信息。

#include <stdio.h>
#include <stdlib.h>
int main() {
    char username[100];
    char command[200];
    printf("Enter a username to check: ");
    scanf("%99s", username); // 危险!直接将用户输入拼接到命令中
    sprintf(command, "grep %s /etc/passwd", username); // 拼接命令
    printf("Executing: %s\n", command);
    system(command);
    return 0;
}

这个程序看起来很正常,但如果一个恶意用户输入以下内容: myuser; rm -rf /

command 变量的值就会变成: grep myuser; rm -rf / /etc/passwd

system() 会把这个字符串交给 shell 执行,shell 会把它解释为两条命令:

  1. grep myuser /etc/passwd
  2. rm -rf /

第二条命令会递归地删除根目录下的所有文件,后果不堪设想!

如何避免?

  • 永远不要将用户输入直接拼接到 system() 的命令字符串中。
  • 如果必须使用,对输入进行严格的过滤和验证,只允许包含安全的字符(只允许字母和数字)。
  • 更好的方法是完全避免使用 system(),转而使用更安全的替代方案。

替代方案

有更安全、更灵活、更高效的方式来替代 system()

使用 fork()exec() 系列函数

这是 system() 内部的工作方式,但你可以直接控制它,从而避免启动一个额外的 shell 进程,并能更精细地处理输入和输出。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <string.h>
int main() {
    pid_t pid;
    int status;
    // 定义要执行的命令和参数
    char *args[] = {"ls", "-l", NULL}; // 必须以 NULL 
    pid = fork();
    if (pid == -1) {
        perror("fork");
        exit(EXIT_FAILURE);
    }
    if (pid == 0) { // 子进程
        // 替换子进程的映像为 "ls" 程序
        execvp("ls", args);
        // execvp 成功,这里永远不会被执行
        // execvp 失败,打印错误并退出子进程
        perror("execvp");
        exit(EXIT_FAILURE);
    } else { // 父进程
        // 等待子进程结束
        waitpid(pid, &status, 0);
        if (WIFEXITED(status)) {
            printf("Child process exited with status: %d\n", WEXITSTATUS(status));
        }
    }
    return 0;
}

优点:

  • 安全:不会启动 shell,因此无法执行 shell 特有的语法(如 , , &),从根本上避免了命令注入。
  • 高效:少创建了一个进程。
  • 灵活:可以完全控制子进程的输入/输出(通过管道 pipe)。

使用 popen()

popen() 是一个折中方案,它也启动一个 shell,但允许你通过管道与子进程进行单向通信(要么从子进程读,要么向子进程写)。

#include <stdio.h>
#include <stdlib.h>
int main() {
    FILE *fp;
    char path[1024];
    // 使用 "r" 模式打开管道,读取命令的输出
    fp = popen("ls -l", "r");
    if (fp == NULL) {
        perror("popen");
        exit(EXIT_FAILURE);
    }
    // 逐行读取命令的输出
    while (fgets(path, sizeof(path), fp) != NULL) {
        printf("%s", path);
    }
    // 关闭管道,并获取命令的退出状态
    int status = pclose(fp);
    if (status == -1) {
        perror("pclose");
    } else {
        printf("\nCommand exited with status: %d\n", WEXITSTATUS(status));
    }
    return 0;
}

优点:

  • 比直接 system() 更方便地获取命令的输出。
  • 比自己写 fork + pipe + exec 简单。

缺点:

  • 仍然启动了 shell,所以仍然存在命令注入的风险(如果输入不安全)。
  • 只支持单向通信。
特性 system() fork() + exec() popen()
安全性 (易受命令注入) (不启动 shell) (启动 shell)
易用性 非常高 (代码复杂) 中等
效率 (多一个 shell 进程) (进程最少) 中等 (多一个 shell 进程)
I/O 控制 完全控制 (双向) 单向控制 (读或写)
主要用途 执行简单、无用户输入的命令,需要最大程度简化代码时。 需要高性能、高安全性、或复杂 I/O 重定向的场景。 需要读取命令输出或向命令写入输入的简单场景。
  • 优先使用 fork() + exec(),尤其是在安全性要求高或性能是关键因素时。
  • 当你需要快速执行一个简单的、内部的、绝对安全的命令时,可以考虑 system()
  • 当你需要读取一个命令的输出时,popen() 是一个不错的选择,但同样要警惕其安全性。
  • 永远不要将 system() 用于处理任何形式的用户输入。
-- 展开阅读全文 --
头像
C语言在Linux下如何正确使用sleep函数?
« 上一篇 01-10
c语言syntax error
下一篇 » 01-10

相关文章

取消
微信二维码
支付宝二维码

目录[+]