How does curl protect a password from appearing in ps output?
Solution 1
When the kernel executes a process, it copies the command line arguments to read-write memory belonging to the process (on the stack, at least on Linux). The process can write to that memory like any other memory. When ps
displays the argument, it reads back whatever is stored at that particular address in the process's memory. Most programs keep the original arguments, but it's possible to change them. The POSIX description of ps
states that
It is unspecified whether the string represented is a version of the argument list as it was passed to the command when it started, or is a version of the arguments as they may have been modified by the application. Applications cannot depend on being able to modify their argument list and having that modification be reflected in the output of ps.
The reason this is mentioned is that most unix variants do reflect the change, but POSIX implementations on other types of operating systems may not.
This feature is of limited use because the process can't make arbitrary changes. At the very least, the total length of the arguments cannot be increased, because the program can't change the location where ps
will fetch the arguments and can't extend the area beyond its original size. The length can effectively be decreased by putting null bytes at the end, because arguments are C-style null-terminated strings (this is indistinguishable from having a bunch of empty arguments at the end).
If you really want to dig, you can look at the source of an open-source implementation. On Linux, the source of ps
isn't interesting, all you'll see there is that it reads the command line arguments from the proc filesystem, in /proc/PID/cmdline
. The code that generates the content of this file is in the kernel, in proc_pid_cmdline_read
in fs/proc/base.c
. The part of the process's memory (accessed with access_remote_vm
) goes from the address mm->arg_start
to mm->arg_end
; these addresses are recorded in the kernel when the process starts and can't be changed afterwards.
Some daemons use this ability to reflect their status, e.g. they change their argv[1]
to a string like starting
or available
or exiting
. Many unix variants have a setproctitle
function to do this. Some programs use this ability to hide confidential data. Note that this is of limited use since the command line arguments are visible while the process starts.
Most high-level languages copy the arguments to string objects and don't give a way to modify the original storage. Here's a C program that demonstrates this ability by changing argv
elements directly.
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
int main(int argc, char *argv[])
{
int i;
system("ps -p $PPID -o args=");
for (i = 0; i < argc; i++)
{
memset(argv[i], '0' + (i % 10), strlen(argv[i]));
}
system("ps -p $PPID -o args=");
return 0;
}
Sample output:
./a.out hello world
0000000 11111 22222
You can see argv
modification in the curl source code. Curl defines a function cleanarg
in src/tool_paramhlp.c
which is used to change an argument to all spaces using memset
. In src/tool_getparam.c
this function is used a few times, e.g. by redacting the user password. Since the function is called from the parameter parsing, it happens early in a curl invocation, but dumping the command line before this happens will still show any passwords.
Since the arguments are stored in the process's own memory, they cannot be changed from the outside except by using a debugger.
Solution 2
The other answers answer the question well in a general manner. To specifically answer "How is this effect achieved? Is it somewhere in the source code of curl?":
In the argument parsing section of the curl source code, the -u
option is handled as follows:
case 'u':
/* user:password */
GetStr(&config->userpwd, nextarg);
cleanarg(nextarg);
break;
And the cleanarg()
function is defined as follows:
void cleanarg(char *str)
{
#ifdef HAVE_WRITABLE_ARGV
/* now that GetStr has copied the contents of nextarg, wipe the next
* argument out so that the username:password isn't displayed in the
* system process list */
if(str) {
size_t len = strlen(str);
memset(str, ' ', len);
}
#else
(void)str;
#endif
}
So we can explicitly see that the username:password argument in argv
is overwritten with spaces, as described by the other answers.
Solution 3
A process can not only read its parameters but write them, too.
I am not a developer so I am not familiar with this stuff but it may be possible from the outside with an approach similar to the changing of environment parameters:
Related videos on Youtube
![Wildcard](https://i.stack.imgur.com/SbCyV.png?s=256&g=1)
Wildcard
Updated on September 18, 2022Comments
-
Wildcard almost 2 years
I noticed some time ago that usernames and passwords given to
curl
as command line arguments don't appear inps
output (although of course they may appear in your bash history).They likewise don't appear in
/proc/PID/cmdline
.(The length of the combined username/password argument can be derived, though.)
Demonstration below:
[root@localhost ~]# nc -l 80 & [1] 3342 [root@localhost ~]# curl -u iamsam:samiam localhost & [2] 3343 [root@localhost ~]# GET / HTTP/1.1 Authorization: Basic aWFtc2FtOnNhbWlhbQ== User-Agent: curl/7.19.7 (x86_64-redhat-linux-gnu) libcurl/7.19.7 NSS/3.15.3 zlib/1.2.3 libidn/1.18 libssh2/1.4.2 Host: localhost Accept: */* [1]+ Stopped nc -l 80 [root@localhost ~]# jobs [1]+ Stopped nc -l 80 [2]- Running curl -u iamsam:samiam localhost & [root@localhost ~]# ps -ef | grep curl root 3343 3258 0 22:37 pts/1 00:00:00 curl -u localhost root 3347 3258 0 22:38 pts/1 00:00:00 grep curl [root@localhost ~]# od -xa /proc/3343/cmdline 0000000 7563 6c72 2d00 0075 2020 2020 2020 2020 c u r l nul - u nul sp sp sp sp sp sp sp sp 0000020 2020 2020 0020 6f6c 6163 686c 736f 0074 sp sp sp sp sp nul l o c a l h o s t nul 0000040 [root@localhost ~]#
How is this effect achieved? Is it somewhere in the source code of
curl
? (I assume it is acurl
feature, not aps
feature? Or is it a kernel feature of some sort?)
Also: can this be achieved from outside the source code of a binary executable? E.g. by using shell commands, probably combined with root permissions?
In other words could I somehow mask an argument from appearing in
/proc
or inps
output (same thing, I think) that I passed to some arbitrary shell command? (I would guess the answer to this is "no" but it seems worth including this extra half-a-question.)-
Admin almost 7 yearsNot an answer, but note that this approach is not safe. There is a race window between start of the program and clearing the argument strings during which any user can read the password. Do not accept sensitive passwords on the command line.
-
Admin almost 7 yearsNow we just need a port of grep that does the same thing... Thanks for asking about this - very interesting.
-
Admin almost 7 years@JPhi1618, what? Why should
grep
be modified to do this? -
Admin almost 7 yearsBecause I'm lazy and don't want grep to show up in my
ps | grep
results... I know there's ways around it. -
Admin almost 7 yearsLoosely related: To whom do environment variables belong? and Does anybody in practice use
environ
directly to access environment variables? — the bottom line: the argument list, like the list of environment variables, is in read/write user process memory, and can be modified by the user process. -
Admin almost 7 years@JPhi1618, just make the first character of your
grep
pattern a character class. E.g.ps -ef | grep '[c]url'
-
Admin almost 7 years@Wildcard: This is astonishing, can you please comment why this omits the line with
grep curl...
in it? -
Admin almost 7 years@mpy, t's not very complicated. Some regexes match themselves and some don't.
curl
matchescurl
but[c]url
doesn't match[c]url
. If you need more detail ask a new question and I'd be happy to answer.
-
-
Wildcard almost 7 yearsOkay, but running e.g.
bash -c 'awk 1 /proc/$$/cmdline; set -- something; awk 1 /proc/$$/cmdline'
shows that at least in the shell, setting the parameters is distinct from modifying what the kernel sees as the process parameters. -
Gilles 'SO- stop being evil' almost 7 years@Wildcard Positional arguments in a shell script are initially copies of some of the shell process's command line arguments. Most shells don't let the script change the original arguments.
-
Wildcard almost 7 yearsGreat! So, regarding that specs snippet, I understand it to mean that it would be POSIX-compliant to make your kernel store a process's original command line arguments outside the process's read-write memory (in addition to the copy in the read-write memory)? And then have
ps
report arguments from that piece of kernel's memory, ignoring any changes made in processes' read-write memory? But (if I got it right?) most UNIX variations don't even do the former, so you can't make aps
implementation do the latter without kernel modifications, since the original data isn't kept anywhere? -
Gilles 'SO- stop being evil' almost 7 years@Wildcard Correct. There may be Unix implementations that do keep the original but I don't think any of the common ones do. The C language allows the contents of
argv
entries to be changed (you can't setargv[i]
, but you can write toargv[i][0]
throughargv[i][strlen(argv[i])]
), so there has to be a copy in the process's memory. -
sebasth almost 7 yearsRelevant function in curl source code: github.com/curl/curl/blob/master/src/tool_paramhlp.c#L139
-
Wildcard almost 7 years@Gilles, yes, that was the point of my comment. :) That the general statement that a process can do that (first sentence of this answer) doesn't answer whether this can be achieved by existing shell features. The answer to this appears to be "no," which is what I guessed at the very bottom of my question.
-
BowlOfRed almost 7 years@Wildcard, Solaris does this. Command line seen by /usr/ucb/ps is the process owned (mutable) copy. Command line seen by /usr/bin/ps is the kernel owned (immutable) copy. Kernel only keeps first 80 characters though. Anything else is truncated.
-
Wildcard almost 7 yearsGilles, I'm having trouble grasping "The length can be decreased by putting null bytes at the end (arguments are C-style null-terminated strings)." A command such as
bash -c 'od -xa /proc/$$/cmdline;echo' '' '' ''
shows four null bytes at the end, denoting three empty arguments. If you fill that piece of your read-write memory with null bytes, won't it just look like a lot of empty arguments? -
Gilles 'SO- stop being evil' almost 7 years@Wildcard Indeed the trailing nulls are empty arguments. In the
ps
output, a lot of empty arguments looks like there's nothing there, but yes, it does make a difference if you check how many spaces there are, and you can observe more directly from/proc/PID/cmdline
. -
Floris almost 7 yearsI like that the comment in
cleanarg
states explicitly that it is doing what the question is asking!