On Linux, you can overwrite the value of the environment strings on the stack.
So you can hide the entry by overwriting it with zeros or anything else:
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char* argv[], char* envp[]) {
char cmd[100];
while (*envp) {
if (strncmp(*envp, "k=", 2) == 0)
memset(*envp, 0, strlen(*envp));
envp++;
}
sprintf(cmd, "cat /proc/%u/environ", getpid());
system(cmd);
return 0;
}
Run as:
$ env -i a=foo k=v b=bar ./wipe-env | hd
00000000 61 3d 66 6f 6f 00 00 00 00 00 62 3d 62 61 72 00 |a=foo.....b=bar.|
00000010
the k=v
has been overwritten with \0\0\0
.
Note that setenv("k", "", 1)
to overwrite the value won't work as in that case, a new "k="
string is allocated.
If you've not otherwise modified the k
environment variable with setenv()
/putenv()
, then you should also be able to do something like this to get the address of the k=v
string on the stack (well, of one of them):
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char* argv[]) {
char cmd[100];
char *e = getenv("k");
if (e) {
e -= strlen("k=");
memset(e, 0, strlen(e));
}
sprintf(cmd, "cat /proc/%u/environ", getpid());
system(cmd);
return 0;
}
Note however that it removes only one of the k=v
entries received in the environment. Usually, there is only one, but nothing is stopping anyone from passing both k=v1
and k=v2
(or k=v
twice) in the env list passed to execve()
. That has been the cause of security vulnerabilities in the past such as CVE-2016-2381. It could genuinely happen with bash
prior to shellshock when exporting both a variable and function by the same name.
In any case, there will always be a small window during which the env var string has not been overridden yet, so you may want to find another way to pass the secret information to the command (like a pipe for instance) if exposing it via /proc/pid/environ
is a concern.
Also note that contrary to /proc/pid/cmdline
, /proc/pid/environment
is only accessible by processes with the same euid or root (or root only if the euid and ruid of the process are not the same it would seem).
You can hide that value from them in /proc/pid/environ
, but they may still be able to get any other copy you've made of the string in memory, for instance by attaching a debugger to it.
See https://www.kernel.org/doc/Documentation/security/Yama.txt for ways to prevent at least non-root users from doing that.
Best Answer
When a program starts, it receives its environment as an array of pointers to some strings in the format
var=value
. On Linux, those are located at the bottom of the stack. At the very bottom, you have all the strings tucked one after the other (that's what's shown in/proc/pid/environ
). And above you have an array of pointers (NULL terminated) to those strings (that's what goes intochar *envp[]
in yourint main(int argc, char* argv[], char* envp[])
, and the libc would generally initialiseenviron
to).putenv()
/setenv()
/unsetenv()
, do not modify those strings, they don't generally even modify the pointers. On some systems, those (strings and pointers) are read-only.While the libc will generally initialise
char **environ
to the address of the first pointer above, any modification of the environment (and those are for future execs), will generally cause a new array of pointers to be created and assigned toenviron
.If
environ
is initially[a,b,c,d,NULL]
, wherea
is a pointer tox=1
,b
toy=2
,c
toz=3
,d
toq=5
, if you do aunsetenv("y")
,environ
would have to become[a,c,d,NULL]
. On systems where the initial array list is read-only, a new list would have to be allocated and assigned toenviron
and[a,c,d,NULL]
stored in there. Upon the nextunsetenv()
, the list could be modified in place. Only if you didunsetenv("x")
above could a list not be reallocated (environ
could just be incremented to point to&envp[1]
. I don't know if some libc implementations actually perform that optimisation).In anycase, there's no reason for the strings themselves stored at the bottom of the stack to be modified in any way. Even if an
unsetenv()
implementation was actually modifying the data initially received on the stack in-place, it would only modify the pointers, it wouldn't go all the trouble of also erasing the strings they point to. (that seems to be what the GNU libc does on Linux systems (with ELF executables at least), it does modify the list of pointers atenvp
in place as long as the number of environment variables doesn't increase.You can observe the behaviour using a program like:
On Linux with the GNU libc (same with klibc, musl libc or dietlibc except for the fact that they use mmapped anonymous memory instead of the heap for allocated memory), when run as
env -i a=1 x=3 ./e
, that gives (comments inline):On FreeBSD (11-rc1 here), a new list is allocated already upon
unsetenv()
. Not only that, but the strings themselves are being copied onto the heap as well soenviron
is completely disconnected from theenvp[]
that the program received on start-up after the first modification of the environment:On Solaris (11 here), we see the optimisation mentioned above (where
unsetenv("a")
ends up being done with aenviron++
), the slot freed byunsetenv()
being reused forb
, but of course a new list of pointers has to be allocated upon the insertion of a new environment variable (c
):