隐藏不带源代码的程序参数


15

我需要对正在运行的程序隐藏一些敏感的参数,但是我无法访问源代码。我也在共享服务器上运行此命令,因此无法使用hidepid因为我没有sudo特权。

这是我尝试过的一些方法:

  • export SECRET=[my arguments],然后再呼叫./program $SECRET,但这似乎无济于事。

  • ./program `cat secret.txt`其中secret.txt包含我的论点,但全能ps者能够嗅出我的秘密。

有没有其他方法可以隐藏我的论证而无需管理员干预?


该特定程序是什么?如果这是通常的命令,则您需要告诉(并且可能还有其他方法)它是哪一个
Basile Starynkevitch

14
因此,您了解发生了什么事情,您尝试过的事情没有任何机会起作用,因为外壳程序负责调用程序之前扩展环境变量并执行命令替换。ps没有采取任何神奇的措施来“嗅出您的秘密”。无论如何,合理编写的程序应该提供命令行选项,以从指定的文件或stdin中读取机密,而不是直接将其作为参数。
jamesdlin

我正在运行由一家私人公司编写的天气模拟程序。他们不共享源代码,也不提供任何文档共享文件秘密的方法。可能是出于选择这里
MS

Answers:


25

正如解释在这里,Linux的投入在程序的数据空间中的程序的参数,并保持一个指向该区域的开始。这就是使用ps等方面查找和显示程序的参数。

由于数据在程序空间中,因此可以对其进行操作。在不更改程序本身的情况下执行此操作涉及将一个具有main()功能的填充加载到程序的真正主程序之前。该填充程序可以将实际参数复制到新的空间,然后覆盖原始参数,以便ps仅看到nul。

以下C代码执行此操作。

/* /unix//a/403918/119298
 * capture calls to a routine and replace with your code
 * gcc -Wall -O2 -fpic -shared -ldl -o shim_main.so shim_main.c
 * LD_PRELOAD=/.../shim_main.so theprogram theargs...
 */
#define _GNU_SOURCE /* needed to get RTLD_NEXT defined in dlfcn.h */
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <signal.h>
#include <unistd.h>
#include <dlfcn.h>

typedef int (*pfi)(int, char **, char **);
static pfi real_main;

/* copy argv to new location */
char **copyargs(int argc, char** argv){
    char **newargv = malloc((argc+1)*sizeof(*argv));
    char *from,*to;
    int i,len;

    for(i = 0; i<argc; i++){
        from = argv[i];
        len = strlen(from)+1;
        to = malloc(len);
        memcpy(to,from,len);
        memset(from,'\0',len);    /* zap old argv space */
        newargv[i] = to;
        argv[i] = 0;
    }
    newargv[argc] = 0;
    return newargv;
}

static int mymain(int argc, char** argv, char** env) {
    fprintf(stderr, "main argc %d\n", argc);
    return real_main(argc, copyargs(argc,argv), env);
}

int __libc_start_main(pfi main, int argc,
                      char **ubp_av, void (*init) (void),
                      void (*fini)(void),
                      void (*rtld_fini)(void), void (*stack_end)){
    static int (*real___libc_start_main)() = NULL;

    if (!real___libc_start_main) {
        char *error;
        real___libc_start_main = dlsym(RTLD_NEXT, "__libc_start_main");
        if ((error = dlerror()) != NULL) {
            fprintf(stderr, "%s\n", error);
            exit(1);
        }
    }
    real_main = main;
    return real___libc_start_main(mymain, argc, ubp_av, init, fini,
            rtld_fini, stack_end);
}

不能进行干预main(),但是您可以对标准C库函数进行干预,该函数__libc_start_main将继续调用main。shim_main.c如开始时的注释中所述编译该文件,然后如图所示运行它。我printf在代码中留了一个,以便您检查它实际上是否在被调用。例如,运行

LD_PRELOAD=/tmp/shim_main.so /bin/sleep 100

然后执行a ps,您将看到一个空命令和args显示。

仍然有少量时间可以看到命令args。为了避免这种情况,例如,您可以更改垫片以从文件中读取您的秘密,然后将其添加到传递给程序的args中。


12
但是仍然会有一个简短的窗口,在该窗口/proc/pid/cmdline中将显示机密(与curl尝试隐藏在命令行中提供的密码时相同)。在使用LD_PRELOAD时,可以包装main,以便将秘密从环境复制到main接收的argv。如呼叫LD_PRELOAD=x SECRET=y cmd,你打电话main()argv[][argv[0], getenv("SECRET")]
斯特凡Chazelas

您无法使用环境隐藏秘密,因为它可以通过看到/proc/pid/environ。可以用与args相同的方式将其重写,但是会保留相同的窗口。
meuh

11
/proc/pid/cmdline是公开的,/proc/pid/environ不是。在某些系统中ps(那里的setuid可执行文件)暴露了任何进程的环境,但是我认为您不会遇到当今的任何情况。通常认为环境足够安全。从具有相同euid的进程中撬出并不安全,但是无论如何它们通常可以通过相同的euid读取进程的内存,因此您无能为力。
斯特凡Chazelas

4
@StéphaneChazelas:如果使用环境来传递秘密,理想情况下,将其转发给main包装程序方法的包装程序还会删除环境变量,以避免意外泄漏给子进程。另外,包装程序可以从文件中读取所有命令行参数。
大卫·佛斯特

@DavidFoerster,好点。我已经更新了我的答案以考虑到这一点。
斯特凡Chazelas

16
  1. 阅读相关应用程序的命令行界面的文档。很可能有一个选项可以从文件提供机密,而不是直接作为参数。

  2. 如果失败,请以没有安全的方法为其提供机密的理由针对该应用程序提交错误报告。

  3. 您可以随时小心(!),适应于解决meuh的回答您的特定需求。特别注意Stéphane的评论及其后续行动。


12

如果您需要将参数传递给程序以使其正常工作,那么即使您不能hidepid在procfs上使用它,无论您做什么,都将不走运。

由于您提到的是bash脚本,因此您应该已经有可用的源代码,因为bash不是编译语言。

失败的话,您可能可以使用gdb或类似的方法重写进程的cm​​dline,并在argc/ argv开始后使用/ 进行操作,但是:

  1. 这是不安全的,因为您在更改参数之前仍会先暴露它们的程序参数
  2. 即使您可以使用它,这还是很棘手的,我也不建议您依赖它

我真的只是建议获取源代码,或者与供应商联系以修改代码。在POSIX操作系统中的命令行上提供机密与安全操作不兼容。


11

当进程执行命令(通过execve()系统调用)时,其内存将被擦除。为了在执行过程中传递一些信息,execve()系统调用为此使用两个参数:argv[]envp[]数组。

这是两个字符串数组:

  • argv[] 包含参数
  • envp[]包含环境变量定义,其var=value格式为字符串(按照约定)。

当您这样做时:

export SECRET=value; cmd "$SECRET"

(此处在参数扩展周围添加了缺少的引号)。

你执行cmd与秘密(value)传入两个argv[]envp[]argv[]将会["cmd", "value"]envp[]类似的东西[..., "PATH=/bin:...", "HOME=...", ..., "SECRET=value", "TERM=xterm", ...]。由于cmd没有执行任何getenv("SECRET")等效操作来从该SECRET环境变量中检索密钥的值,因此将其放入环境中没有用。

argv[]是公众知识。它显示在的输出中psenvp[]现在不是。在Linux上,显示为/proc/pid/environ。它ps ewww在BSD 的输出中显示(ps在Linux上是procps-ng的输出),但仅显示给使用相同有效uid(并且对setuid / setgid可执行文件有更多限制)的进程。它可能显示在某些审核日志中,但是这些审核日志只能由管理员访问。

简而言之,传递给可执行文件的环境是私有的,或者至少与进程的内部存储器一样私有(在某些情况下,具有正确特权的其他进程也可以使用调试器进行访问,例如也被转储到磁盘)。

既然argv[]是公共知识,那么设计中会破坏期望在其命令行上保留机密数据的命令。

通常,需要提供机密的命令会为您提供另一个接口,例如通过环境变量。例如:

IPMI_PASSWORD=secret ipmitool -I lan -U admin...

或通过专用文件描述符(如stdin):

echo secret | openssl rsa -passin stdin ...

echo是内置的,它不会显示在的输出中ps

或文件,例如.netrcfor ftp和其他一些命令,或

mysql --defaults-extra-file=/some/file/with/password ....

某些应用程序curl((这也是@meuh在此处采用的方法))试图隐藏其argv[]从窥探中收到的密码(在某些系统上,通过覆盖存储argv[]字符串的内存部分)。但这并没有真正的帮助,并且给安全带来了虚假的承诺。在execve()和覆盖之间留下一个窗口,该窗口ps仍会显示秘密。

例如,如果攻击者知道您正在运行一个脚本curl -u user:somesecret https://...(例如,在cron作业中),那么他所要做的就是从curl使用(例如,通过运行sh -c 'a=a;while :; do a=$a$a;done')的(许多)库中退出缓存。以减慢启动速度,即使效率很低until grep 'curl.*[-]u' /proc/*/cmdline; do :; done也足以在我的测试中捕获该密码。

如果参数是将秘密传递给命令的唯一方法,则可能仍然可以尝试某些操作。

在某些系统上,包括较旧的Linux版本,只能argv[]查询其中的字符串的前几个字节(在Linux 4.1及更高版本上为4096)。

在那里,您可以执行以下操作:

(exec -a "$(printf %-4096s cmd)" cmd "$secret")

秘密将被隐藏,因为它超过了前4096个字节。现在使用该方法的人现在必须后悔,因为从4.2版本开始,Linux不再截断/proc/pid/cmdline。中的args列表。还要注意,并不是因为ps命令行显示的字节数不多(例如在FreeBSD上似乎限制为2048),所以不能使用相同的API ps来获取更多信息。但是,ps该方法在常规用户检索该信息的唯一方法(例如,当对API进行特权设置并且ps为了使用它而使用setgid或setuid进行访问)的唯一方式下是有效的,但在那儿仍然可能无法满足未来需求。

另一种方法是通过的秘密argv[],但将代码注入程序(使用gdb$LD_PRELOAD黑客)它之前main()开始进行的插入秘密进入argv[]从接收execve()

使用LD_PRELOAD,对于GNU系统上非setuid / setgid动态链接的可执行文件:

/* 
 * replace ***** with secret read from fd 9
 * gcc -Wall -fpic -shared -o inject_secret.so inject_secret.c -ldl 
 * LD_PRELOAD=/.../inject_secret.so cmd -p '*****' 9<<< secret
 */
#define _GNU_SOURCE
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <dlfcn.h>

#define PLACEHOLDER "*****"
static char secret[1024];

int __libc_start_main(int (*main) (int, char**, char**),
                      int argc,
                      char **argv,
                      void (*init) (void),
                      void (*fini)(void),
                      void (*rtld_fini)(void),
                      void (*stack_end)){
    static int (*real_libc_start_main)() = NULL;
    int n;

    if (!real_libc_start_main) {
        real_libc_start_main = dlsym(RTLD_NEXT, "__libc_start_main");
        if (!real_libc_start_main) abort();
    }

    n = read(9, secret, sizeof(secret));
    if (n > 0) {
      int i;

      if (secret[n - 1] == '\n') secret[--n] = '\0'; 
      for (i = 1; i < argc; i++)
        if (strcmp(argv[i], PLACEHOLDER) == 0)
          argv[i] = secret;
    }

    return real_libc_start_main(main, argc, argv, init, fini,
                                rtld_fini, stack_end);
}

然后:

$ gcc -Wall -fpic -shared -o inject_secret.so inject_secret.c -ldl
$ LD_PRELOAD=$PWD/inject_secret.so  ps '*****' 9<<< "-opid,args"
  PID COMMAND
 7659 /bin/zsh
 8828 ps *****

任何地方都ps不会显示出该位置ps -opid,args-opid,args在此示例中是秘密)。请注意,我们要替换的argv[]指针数组的元素,而不是覆盖这些指针指向的字符串,这就是为什么我们的修改未显示在的输出中的原因ps

使用gdb,仍然适用于非setuid / setgid动态链接的可执行文件以及在GNU系统上:

tmp=$(mktemp) && cat << EOF > "$tmp" &&
break __libc_start_main
commands 1
set argv[1]="-opid,args"
continue
end
run
EOF

gdb -n --batch-silent --return-child-result -x "$tmp" --args ps '*****'
rm -f -- "$tmp"

仍然可以使用gdb一种不依赖于GNU的非特定方法,该方法不依赖于动态链接的可执行文件或具有调试符号,并且至少应适用于Linux上的任何ELF可执行文件:

#! /bin/sh -
# gdb+sh polyglot script to replace "*****" arguments with the content
# of the SECRET environment variable *after* execve and before calling
# the executable's main() function.
#
# Usage: SECRET=somesecret cmd --password '*****'

if ':' - ':'
then
  # running in sh
  # retrieve the start address for the executable
  start=$(
    LC_ALL=C objdump -f -- "$(command -v -- "${1?}")" |
    sed -n 's/^start address //p'
  )
  [ -n "$start" ] || exit
  # re-exec ourself with gdb.
  exec gdb -n --batch-silent --return-child-result -iex "set \$start = $start" -x "$0" --args "$@"
  exit 1
fi
end
# running in gdb
break *$start
commands 1
  # The stack on startup contains:
  # argc argv[0]... argv[argc-1] 0 envp[0] envp[1]... 0 argv[] and envp[] strings
  set $argc = *((int*)$sp)
  set $argv = &((char**)$sp)[1]
  set $envp = &($argv[$argc+1])
  set $i = 0
  while $envp[$i]
    # look for an envp[] string starting with "SECRET=". We can't use strcmp()
    # here as there's no guarantee that the debugged executable has such
    # a function
    set $e = $envp[$i]
    if $e[0] == 'S' && \
       $e[1] == 'E' && \
       $e[2] == 'C' && \
       $e[3] == 'R' && \
       $e[4] == 'E' && \
       $e[5] == 'T' && \
       $e[6] == '='
      set $secret = &($e[7])
      # replace SECRET=xxx<NUL> with SECRE=<NUL>
      set $e[5] = '='
      set $e[6] = '\0'
      # not calling loop_break as that causes a SEGV with my version of gdb
    end
    set $i = $i + 1
  end
  if $secret
    # now looking for argv[] strings being "*****" and replace them with
    # the secret identified earlier
    set $i = 0
    while $i < $argc
      set $a = $argv[$i]
      if $a[0] == '*' && \
       $a[1] == '*' && \
       $a[2] == '*' && \
       $a[3] == '*' && \
       $a[4] == '*' && \
       $a[5] == '\0'
        set $argv[$i] = $secret
      end
      set $i = $i + 1
    end
  end
  # using "continue" as "detach" causes a SEGV with my version of gdb.
  continue
end
run

使用静态链接的可执行文件进行测试:

$ SECRET=/proc/self/cmdline ./replace_secret busybox cat '*****' | tr '\0' '\n'
/bin/busybox
cat
*****

当可执行文件可能是静态的时,我们没有可靠的方式分配内存来存储机密,因此我们必须从进程内存中已经存在的其他位置获取机密。这就是为什么环境在这里是显而易见的选择。我们还将该SECRETenv var 隐藏到该进程中(通过将其更改为SECRE=),以避免在该进程出于某种原因决定转储其环境或执行不受信任的应用程序时泄漏它。

在Solaris 11上也可以使用(安装了gdb和GNU binutils(您可能必须重命名objdumpgobjdump))。

在FreeBSD上(至少x86_64,我不确定堆栈上的前24个字节是什么(当gdb(8.0.1)处于交互状态,这表明gdb中可能存在bug时变为16)),替换argcargv定义与:

set $argc = *((int*)($sp + 24))
set $argv = &((char**)$sp)[4]

(您可能还需要安装gdb软件包/端口,因为系统附带的版本是古老的)。


重新(这里在参数扩展周围添加了缺少的引号):不使用引号怎么了?真的有区别吗?
yukashima huksay,

@yukashimahuksay,例如,请参阅忘记在bash / POSIX shell中引用一个变量及其链接的问题所带来的安全隐患
斯特凡Chazelas

3

您可能会做的是

 export SECRET=somesecretstuff

然后,假设您./program使用C语言编写代码(或其他人编写的代码,并且可以为您更改或改进),请在该程序中使用getenv(3)

char* secret= getenv("SECRET");

之后,export 您只需./program在同一shell中运行即可。或者可以将环境变量名称传递给它(通过运行./program --secret-var=SECRET等)。

ps不会透露您的秘密,但是proc(5)仍然可以提供很多信息(至少提供给同一用户的其他进程)。

又见帮助设计传递程序参数的更好的方法。

请参阅此答案,以获取有关globbing和shell角色的更好解释。

也许您program有一些其他方法来获取数据(或更明智地使用进程间通信),而不是使用普通程序参数(如果要处理敏感信息,则肯定可以)。阅读其文档。也许您正在滥用该程序(该程序无意于处理机密数据)。

隐藏秘密数据真的很困难。不通过程序参数传递它是不够的。


5
这是从这个问题很清楚,他甚至没有在源代码./program,所以这个答案上半年似乎并不相关。
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.