头图

大家好,今天和大家分享一个项目中遇到的问题~
在实际项目中,因业务功能的原因在编写程序中经常会调用第三方命令或系统命令来完成相应的功能,不由在想这种方式真的安全吗?
由于这种疑问让我产生了浓烈的兴趣,本次将分析的经过记录了下来。接下来小伙伴们跟我一起研究学习吧~

首先,下面代码块中使用了Go语言的 exec.Command 的函数进行执行 zip 命令压缩本地文件。其中 -rP 参数代表递归压缩并进行加密,密码abcd1234

很大部分场景中,在做业务功能时有很多类似的做法。例如进行执行加密、解密过程、获取系统某些信息等,习惯性调用系统命令。为了演示,在执行 zip 命令之前,加了命令sleep 60 等待,只是为了让大家更好的看到效果。
下面通过go build进行编译,接着让编译后的命令运行起来。

$ go build -o zipcommand main.go
$ ./zipcommand

用户态层面

接触过Linux的小伙伴肯定首先想到使用 ps 命令来看进程的上下文信息。

图中,先将zipcommand进程PID找到,然后使用pstree将该进程树打印,发现go中 exec.Command 函数在调用shell命令时会fork()一个子进程执行。 将第一个子进程 3648095 展开COMMAND时,发现shell与我们在go代码中传入shell命令是一致的,这样就将命令裸露到系统层面上了。 图中第二个子进程 3648096 是因为使用了 && 拼接符,又利用fork()子进程方式先执行 sleep 后在执行 zip 命令。
还可以通过 /proc/pid/cmdline 方式读取 COMMAND 进程启动命令。

注:这里大家可以详细查看linux进程fork()机制的材料,有助于加深理解。golang封装的 exec.Command 这种方式最终也是操作系统的系统调用真正在做处理。

系统调用层面

下面我在想,现在已经知道程序中调用shell命令会暴露在系统层面,那程序在 exec.Command() 传入shell命令后会被篡改吗?
大概思路是使用ptrace()系统调用方式,先拿到程序中传入的shell命令变量内存地址,进行尝试修改变量值。

上面go程序中的内存地址不能与c语言直接处理,下面举例先全部使用c语言代码演示。
代码中将 cmd 内存地址直接输出,让ptrace()尝试修改内存地址,达到篡改进程调用的目的。通过 gcc cmd.c -o zipcommand 进行编译。

下面将zipcommand命令运行,然后拿到cmd变量的内存地址。
ptrace程序关键代码

主要是通过系统调用ptrace()方法,先attach到指定进程上,在基于拿到的内存地址,进行修改后让进程继续运行。

本次所用到ptrace()的类型如下:
PTRACE_ATTACHptrace()attach到一个指定的进程,使其成为当前进程跟踪的子进程。
PTRACE_PEEKDATA向内存区域中写入一个WORD,内存地址为addr。
PTRACE_DETACH进程在每次系统调用之后暂停。

通过 gcc ptrace.c -o ptrace 进行编译。
下面尝试使用ptracezipcommand变量内存地址尝试篡改。

我们可以看到 zipcommand 中的cmd变量值已经被篡改成功。
ptrace.c详细代码

#include <stdio.h>
#include <stdlib.h>
#include <sys/ptrace.h>
#include <sys/wait.h>
#include <unistd.h>
#include <sys/reg.h>
#include <sys/syscall.h>

int main(int argc, char* argv[])
{
    pid_t attack_pid;
    char cmd[] = "echo helloword";
    if(argc != 2) {
        printf("Usage: %s <pid to be traced>\n",
               argv[0]);
        exit(1);
    }
    attack_pid = atoi(argv[1]);
    if (ptrace(PTRACE_ATTACH, attack_pid, NULL, NULL) < 0)
    {
        printf("attach failed\n");
        return 0;
    }
    //读数据
    printf("stack_arg %d\n", ptrace(PTRACE_PEEKDATA , attack_pid, (void*)0x7ffc41334450, NULL));
    //修改数据
    putdata(attack_pid, (void*)0x7ffc41334450, cmd, 44);
    ptrace(PTRACE_DETACH, attack_pid, NULL, NULL);
    waitpid(attack_pid, NULL, WUNTRACED);
    return 0;
}

void putdata(pid_t child, long addr, char *str, int len) {
    char *laddr;
    int long_size = sizeof(long);
    int i, j;
    union u {
        long val;
        char chars[long_size];
    } data;

    i = 0;
    j = len / long_size;
    laddr = str;

    while (i < j) {
        memcpy(data.chars, laddr, long_size);
        //每次写入一个字
        ptrace(PTRACE_POKEDATA, child, addr + i * 8, data.val);
        ++i;
        laddr += long_size;
    }
    j = len % long_size;
    if (j != 0) {
        memcpy(data.chars, laddr, j);
        ptrace(PTRACE_POKEDATA, child, addr + i * 8, data.val);
    }
}

上面通过两个层面,说明了在程序中调用shell命令的面临的风险。 为了让程序更加安全,应减少采用调用shell命令这种方式。

技术文章持续更新,请大家多多关注呀~~

搜索微信公众号,关注我【 帽儿山的枪手 】


帽儿山的枪手
71 声望18 粉丝