Hide arguments to program without source code

5,272

Solution 1

As explained here, Linux puts a program's arguments in the program's data space, and keeps a pointer to the start of this area. This is what is used by ps and so on to find and show the program arguments.

Since the data is in the program's space, it can manipulate it. Doing this without changing the program itself involves loading a shim with a main() function that will be called before the real main of the program. This shim can copy the real arguments to a new space, then overwrite the original arguments so that ps will just see nuls.

The following C code does this.

/* https://unix.stackexchange.com/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);
}

It is not possible to intervene on main(), but you can intervene on the standard C library function __libc_start_main, which goes on to call main. Compile this file shim_main.c as noted in the comment at the start, and run it as shown. I've left a printf in the code so you check that it is actually being called. For example, run

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

then do a ps and you will see a blank command and args being shown.

There is still a small amount of time that the command args may be visible. To avoid this, you could, for example, change the shim to read your secret from a file and add it to the args passed to the program.

Solution 2

  1. Read the documentation of the command line interface of the application in question. There may well be an option to supply the secret from a file instead of as an argument directly.

  2. If that fails, file a bug report against the application on the grounds that there is no secure way to supply a secret to it.

  3. You can always carefully(!) adapt the solution in meuh’s answer to your specific needs. Pay special consideration to Stéphane’s comment and its follow-ups.

Solution 3

If you need to pass arguments to the program to get it to work, you're going to be out of luck no matter what you do if you can't use hidepid on procfs.

Since you mentioned this is a bash script, you should already have the source code available, since bash is not a compiled language.

Failing that, you may be able to rewrite the cmdline of the process using gdb or similar and playing around with argc/argv once it's already started, but:

  1. This is not secure, since you still expose your program arguments initially prior to changing them
  2. This is pretty hacky, even if you could get it to work I wouldn't recommend relying on it

I'd really just recommend getting the source code, or talking to the vendor to get the code modified. Supplying secrets on the command line in a POSIX operating system is incompatible with secure operation.

Solution 4

When a process executes a command (via the execve() system call), its memory is wiped. To pass some information across the execution, the execve() system calls takes two arguments for that: the argv[] and envp[] arrays.

Those are two arrays of strings:

  • argv[] contains the arguments
  • envp[] contains the environment variable definitions as strings in the var=value format (by convention).

When you do:

export SECRET=value; cmd "$SECRET"

(here added the missing quotes around the parameter expansion).

You're executing cmd with the secret (value) passed both in argv[] and envp[]. argv[] will be ["cmd", "value"] and envp[] something like [..., "PATH=/bin:...", "HOME=...", ..., "SECRET=value", "TERM=xterm", ...]. As cmd is not doing any getenv("SECRET") or equivalent to retrieve the value of the secret from that SECRET environment variable, putting it in the environment is not useful.

argv[] is public knowledge. It shows in the output of ps. envp[] nowadays is not. On Linux, it shows in /proc/pid/environ. It shows in the output of ps ewww on BSDs (and with procps-ng's ps on Linux), but only to processes running with the same effective uid (and with more restrictions for setuid/setgid executables). It may show in some audit logs, but those audit logs should only be accessible by administrators.

In short the environ that is passed to an executable is meant to be private or at least about as private as the internal memory of a process (which under some circumstances an other process with the right privileges can also access with a debugger for instance and can also be dumped to disk).

Since argv[] is public knowledge, a command that expects data meant to be secret on its command line is broken by design.

Usually, commands that need to be given a secret, provides you with another interface for doing so, like via an environment variable. For instance:

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

Or via a dedicated file descriptor like stdin:

echo secret | openssl rsa -passin stdin ...

(echo being builtin, it doesn't show in the output of ps)

Or a file, like the .netrc for ftp and a few other commands or

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

Some applications like curl (and that's also the approach taken by @meuh here) try to hide the password that they received in argv[] from prying eyes (on some systems by overwriting the portion of memory where the argv[] strings were stored). But that's not really helping and gives a false promise of security. That leaves a window in between the execve() and the overwriting where ps will still show the secret.

For instance, if an attacker knows that you're running a script doing a curl -u user:somesecret https://... (for instance in a cron job), all he has to do is evict from the cache the (many) libraries that curl uses (for instance by running a sh -c 'a=a;while :; do a=$a$a;done') so as to slow down its startup, and even doing a very inefficient until grep 'curl.*[-]u' /proc/*/cmdline; do :; done is enough to catch that password in my tests.

If the arguments is the only way you can pass the secret to the commands, there may still be some things you could try.

On some systems, including older versions of Linux, only the first few bytes (4096 on Linux 4.1 and before) of the strings in argv[] can be queried.

There, you could do:

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

And the secret would be hidden because it's past the first 4096 bytes. Now people who have used that method must regret it now since Linux since 4.2 no longer truncates the list of args in /proc/pid/cmdline. Also note that it's not because ps won't show more than so-many-bytes of a command line (like on FreeBSD where it seems to be limited to 2048) that one can't use to same API ps uses to get more. That approach is valid however on systems where ps is the only way for a regular user to retrieve that information (like when the API is privileged and ps is setgid or setuid in order to use it), but is still potentially not future-proof there.

Another approach would be to not pass the secret in argv[] but inject code into the program (using gdb or a $LD_PRELOAD hack) before its main() is started that inserts the secret into the argv[] received from execve().

With LD_PRELOAD, for non-setuid/setgid dynamically linked executables on a GNU system:

/* 
 * 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);
}

Then:

$ 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 *****

At no point would ps have shown the ps -opid,args there (-opid,args being the secret in this example). Note that we're replacing elements of the argv[] array of pointers, not overriding the strings pointed to by those pointers which is why our modifications don't show in the output of ps.

With gdb, still for non-setuid/setgid dynamically linked executables and on GNU systems:

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"

Still with gdb, a non-GNU specific approach that doesn't rely on executables being dynamically linked or have debug symbols and should work for any ELF executable on Linux at least could be:

#! /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

Testing with a statically linked executable:

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

When the executable may be static, we don't have a reliable way to allocate memory to store the secret, so we have to get the secret from somewhere else that is already in the process memory. That's why the environ is the obvious choice here. We also hide that SECRET env var to the process (by changing it to SECRE=) to avoid it leaking if the process decides to dump its environment for some reason or execute untrusted applications.

That also works on Solaris 11 (provided gdb and GNU binutils are installed (you may have to rename objdump to gobjdump).

On FreeBSD (at least x86_64, I'm not sure what those first 24 bytes (which become 16 when gdb (8.0.1) is interactive suggesting there may be a bug in gdb there) on the stack are), replace the argc and argv definitions with:

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

(you may also need to install the gdb package/port as the version that otherwise comes with the system is ancient).

Solution 5

What you might do is

 export SECRET=somesecretstuff

then, assuming you are writing your ./program in C (or someone else does, and can change or improve it for you), use getenv(3) in that program, perhaps as

char* secret= getenv("SECRET");

and after the export you just run ./program in the same shell. Or the environment variable name could be passed to it (by running ./program --secret-var=SECRET etc...)

ps won't tell about your secret, but proc(5) can still give a lot of information (at least to other processes of the same user).

See also this to help design a better way of passing program arguments.

See this answer for a better explanation about globbing and the role of a shell.

Perhaps your program has some other ways to get data (or use inter-process communication more wisely) than plain program arguments (it certainly should, if it is intended to process sensitive information). Read its documentation. Or perhaps you are abusing that program (which is not intended to process secret data).

Hiding secret data is really difficult. Not passing it thru program arguments is not enough.

Share:
5,272

Related videos on Youtube

M.S.
Author by

M.S.

Updated on September 18, 2022

Comments

  • M.S.
    M.S. almost 2 years

    I need to hide some sensitive arguments to a program I am running, but I don't have access to the source code. I am also running this on a shared server so I can't use something like hidepid because I don't have sudo privileges.

    Here are some things I have tried:

    • export SECRET=[my arguments], followed by a call to ./program $SECRET, but this doesn't seem to help.

    • ./program `cat secret.txt` where secret.txt contains my arguments, but the almighty ps is able to sniff out my secrets.

    Is there any other way to hide my arguments that doesn't involve admin intervention?

    • Basile Starynkevitch
      Basile Starynkevitch over 6 years
      What is that particular program? If it is a usual command you need to tell (and there could be some other approach) which one it is
    • Snipe3000
      Snipe3000 over 6 years
      So you understand what's going on, the things you tried have no chance of working because the shell is responsible for expanding environment variables and for performing command substitution before invoking the program. ps is not doing anything magical to "sniff out your secrets". Anyway, reasonably-written programs instead should offer a command-line option to read a secret from a specified file or from stdin instead of taking it directly as an argument.
    • M.S.
      M.S. over 6 years
      I'm running a weather simulation program written by a private company. They don't share their source code, nor does their documentation provide any way to share a secret from a file. Might be out of options here
  • Stéphane Chazelas
    Stéphane Chazelas over 6 years
    But there will still be a short window during which /proc/pid/cmdline will show the secret (same as when curl tries to hide the password it is given on the command line). While you're at using LD_PRELOAD, you could wrap main so that the secret is copied from the environment to the argv that main receives. Like call LD_PRELOAD=x SECRET=y cmd where you call main() with argv[] being [argv[0], getenv("SECRET")]
  • meuh
    meuh over 6 years
    You cannot use the environment to hide a secret as it is visible via /proc/pid/environ. This may be overwritable in the same way as the args, but it leaves the same window.
  • Stéphane Chazelas
    Stéphane Chazelas over 6 years
    /proc/pid/cmdline is public, /proc/pid/environ is not. There were some systems where ps (a setuid executable there) exposed the environ of any process, but I don't think you'll come across any nowadays. The environment is generally considered safe enough. Not safe to prying from processes with the same euid, but those can often read the memory of processes by the same euid anyway, so there's not much you can do about it.
  • pipe
    pipe over 6 years
    It's pretty clear from the question that he does not even have the source code for ./program, so the first half of this answer does not seem to be relevant.
  • MoonCheese62
    MoonCheese62 over 6 years
    @StéphaneChazelas: If one uses the environment to pass secrets, ideally the wrapper that forwards it to the main method of the wrapped program also removes the environment variable to avoid accidental leakage to child processes. Alternatively the wrapper could read all command-line arguments from a file.
  • Stéphane Chazelas
    Stéphane Chazelas over 6 years
    @DavidFoerster, good point. I've updated my answer to take that into account.
  • yukashima huksay
    yukashima huksay over 6 years
    Re (here added the missing quotes around the parameter expansion): What is wrong with not using the quotes? Is there really a difference?
  • Stéphane Chazelas
    Stéphane Chazelas over 6 years
    @yukashimahuksay, see for instance Security implications of forgetting to quote a variable in bash/POSIX shells and the questions linked there.