Hide arguments to program without source code
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
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.
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.
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:
- This is not secure, since you still expose your program arguments initially prior to changing them
- 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 thevar=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.
Related videos on Youtube
M.S.
Updated on September 18, 2022Comments
-
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`
wheresecret.txt
contains my arguments, but the almightyps
is able to sniff out my secrets.
Is there any other way to hide my arguments that doesn't involve admin intervention?
-
Basile Starynkevitch over 6 yearsWhat 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 over 6 yearsSo 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. over 6 yearsI'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 over 6 yearsBut there will still be a short window during which
/proc/pid/cmdline
will show the secret (same as whencurl
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 callLD_PRELOAD=x SECRET=y cmd
where you callmain()
withargv[]
being[argv[0], getenv("SECRET")]
-
meuh over 6 yearsYou 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 over 6 years
/proc/pid/cmdline
is public,/proc/pid/environ
is not. There were some systems whereps
(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 over 6 yearsIt'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 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 over 6 years@DavidFoerster, good point. I've updated my answer to take that into account.
-
yukashima huksay over 6 yearsRe (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 over 6 years@yukashimahuksay, see for instance Security implications of forgetting to quote a variable in bash/POSIX shells and the questions linked there.