change /proc/PID/environ after process start
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.
It has not been necessary to overwrite the strings above (not really on) the main thread's stack on Linux since 2010.
Both /proc/self/cmdline
and /proc/self/environ
are modifiable by the process itself at runtime, by dint of calling the prctl()
function with respectively PR_SET_MM_ARG_START
+PR_SET_MM_ARG_END
or PR_SET_MM_ENV_START
+PR_SET_MM_ENV_END
. These directly set the memory pointers into the process' application memory space, held by the kernel for every process, that are used to retrieve the contents of /proc/${PID}/cmdline
and /proc/${PID}/environ
, and hence the command line and environment reported by the ps
command.
So one simply needs to construct a new argument or environment string (not vector, notice — the memory pointed to must be the actual string data, concatenated and ␀
-delimited) and tell the kernel where it is.
This is documented in the Linux manual page for the prctl(2)
function as well as the environ(7)
manual page. What is not documented is that the kernel rejects any attempt to set the start address above the end address, or the end address below the start address; or to (re-)set either address to zero. Also, this is not the original mechanism proposed by Bryan Donlan in 2009, which permitted setting start and end in a single operation, atomically. Moreover, the kernel provides no way to get the current values of these pointers.
This makes it tricky to modify the environment and command line areas with prctl()
. One has to call the prctl()
function up to four times because the first attempts may result in attempts to set the start pointer higher than the end pointer, depending from where the old and new data are in memory. One has to call it a further four times if one wants to ensure that this does not result in a window of opportunity for other processes on the system to inspect an arbitrary range of the process' memory space in the period when the new start/end has been set but the new end/start has not been.
A single atomic system call that sets the entire range in one go would have been far easier for applications programs to use safely.
A further wrinkle is that, for no really good reason (given the checks in the kernel, the overwritability of the original data areas anyway, and the fact that the equivalents are not privileged operations on any of the BSDs), on Linux this requires superuser privileges.
I wrote fairly simple setprocargv()
and setprocenvv()
functions for my toolsets, that employ this. Chain-loading programs from the toolsets that are builtins, like setenv
and foreground
, thus reflect the chained-to command arguments and environment, where Linux permits.
# /package/admin/nosh/command/clearenv setenv WIBBLE wobble foreground pause \; true & [1] 1057 # hexdump -C /proc/1057/cmdline 00000000 66 6f 72 65 67 72 6f 75 6e 64 00 70 61 75 73 65 |foreground.pause| 00000010 00 3b 00 74 72 75 65 00 |.;.true.| 00000018 # hexdump -C /proc/1057/environ 00000000 57 49 42 42 4c 45 3d 77 6f 62 62 6c 65 00 |WIBBLE=wobble.| 0000000e # hexdump -C /proc/1058/cmdline 00000000 70 61 75 73 65 00 |pause.| 00000006 # hexdump -C /proc/1058/environ 00000000 57 49 42 42 4c 45 3d 77 6f 62 62 6c 65 00 |WIBBLE=wobble.| 0000000e #
Note that this does not militate against things that trace the process and access its memory directly by other means (rather than through these two pseudo-files), and of course leaves a window before the strings are modified where this information can be seen, just as overwriting the data above the main thread's stack does. And just as is the case with overwriting the data, this does not account for language runtime libraries that make copies of the environment (on the heap) in various circumstances. In general, do not consider this to be as good a mechanism for passing "secrets" to a program as (say) having it inherit an open file descriptor to the read end of an unnamed pipe, read into an input buffer wholly under your control that you then wipe.
Further reading
- Timo Sirainen (2009-10-02). Added PR_SET_PROCTITLE_AREA option for prctl(). Linux Kernel Mailing List.
- https://unix.stackexchange.com/a/432681/5132
- Daniel J. Bernstein. The checkpassword interface. cr.yp.to.
- https://github.com/jdebp/nosh/blob/master/source/setprocargv.cpp
- https://github.com/jdebp/nosh/blob/master/source/setprocenvv.cpp