44

I'm running a software daemon that requires for certain actions to enter a passphrase to unlock some features which looks for example like that:

$ darkcoind masternode start <mypassphrase>

Now I got some security concerns on my headless debian server.

Whenever I search my bash history for example with Ctrl+R I can see this super strong password. Now I imagine my server is compromized and some intruder has shell access and can simply Ctrl+R to find my passphrase in the history.

Is there a way to enter the passphrase without it to be shown in bash history, ps, /proc or anywhere else?


Update 1: Passing no password to the daemon throws an error. This is no option.


Update 2: Don't tell me to delete the software or other helpful hints like hanging the developers. I know this is not a best-practice example but this software is based on bitcoin and all bitcoin based clients are some kind of json rpc server which listens to these commands and its a known security issue still being discussed (a, b, c).


Update 3: The daemon is already started and running with the command

$ darkcoind -daemon

Doing ps shows only the startup command.

$ ps aux | grep darkcoin
user     12337  0.0  0.0  10916  1084 pts/4    S+   09:19   0:00 grep darkcoin
user     21626  0.6  0.3 1849716 130292 ?      SLl  May02   6:48 darkcoind -daemon

So passing the commands with the passphrase does not show up in ps or /proc at all.

$ darkcoind masternode start <mypassphrase>
$ ps aux | grep darkcoin
user     12929  0.0  0.0  10916  1088 pts/4    S+   09:23   0:00 grep darkcoin
user     21626  0.6  0.3 1849716 130292 ?      SLl  May02   6:49 darkcoind -daemon

This leaves the question where does the history show up? Only in .bash_history?

12
  • 1
    The first question has to be: what happens if you start the daemon without the passphrase argument. Does it just prompt for it?
    – MadHatter
    May 2, 2014 at 15:32
  • 31
    I don't think there's an answer that will work. The inability to prompt for a passphrase is is a major shortcoming in the daemon. If it's free software, get a programmer in and fix it; don't forget to publish your changes. If it's proprietary software, ring up the vendor and shout at them (that won't fix anything, but it'll make you feel better).
    – MadHatter
    May 2, 2014 at 15:55
  • 4
    Check your documentation, it may support reading that password from a system environment variable. May 2, 2014 at 18:11
  • 3
    Even if the password is not given on the command line to the daemon, it is still problematic to give it on the command line of any other command. It is only visible in the ps output for a very short time, but a process running in the background could still pick it up. But it is of course still worthwhile making it harder to pick up the password.
    – kasperd
    May 3, 2014 at 8:05
  • 2
    Look at the answers to this question, they deal with exactly this issue.
    – dotancohen
    May 4, 2014 at 16:05

9 Answers 9

76

Really, this should be fixed in the application itself. And such applications should be open source, so that fixing the issue in the app itself should be an option. A security related application which makes this kind of mistake might make other mistakes as well, so I wouldn't trust it.

Simple interposer

But you were asking for a different way, so here is one:

#define _GNU_SOURCE
#include <dlfcn.h>

int __libc_start_main(
    int (*main) (int, char * *, char * *),
    int argc, char * * ubp_av,
    void (*init) (void),
    void (*fini) (void),
    void (*rtld_fini) (void),
    void (* stack_end)
  )
{
  int (*next)(
    int (*main) (int, char * *, char * *),
    int argc, char * * ubp_av,
    void (*init) (void),
    void (*fini) (void),
    void (*rtld_fini) (void),
    void (* stack_end)
  ) = dlsym(RTLD_NEXT, "__libc_start_main");
  ubp_av[argc - 1] = "secret password";
  return next(main, argc, ubp_av, init, fini, rtld_fini, stack_end);
}

Compile this with

gcc -O2 -fPIC -shared -o injectpassword.so injectpassword.c -ldl

then run your process with

LD_PRELOAD=$PWD/injectpassword.so darkcoind masternode start fakepasshrase

The interposer library will run this code before the main function from your application gets executed. It will replace the last command line argument by the actual password in the call to main. The command line as printed in /proc/*/cmdline (and therefore seen by tools such as ps) will still contain the fake argument, though. Obviously you'd have to make the source code and the library you compile from it readable only to yourself, so best operate in a chmod 0700 directory. And since the password isn't part of the command invocation, your bash history is safe as well.

More advanced interposer

If you want to do anything more elaborate, you should keep in mind that __libc_start_main gets executed before the runtime library has been properly initialized. So I'd suggest avoiding any function calls unless they are absolutely essential. If you want to be able to call functions to your heart's content, make sure you do so just before main itself gets invoked, after all initialization is done. For the following example I have to thank Grubermensch who pointed out how to hide a password passed as command line argument which brought getpass to my attention.

#define _GNU_SOURCE
#include <dlfcn.h>
#include <unistd.h>

static int (*real_main) (int, char * *, char * *);

static int my_main(int argc, char * * argv, char * * env) {
  char *pass = getpass(argv[argc - 1]);
  if (pass == NULL) return 1;
  argv[argc - 1] = pass;
  return real_main(argc, argv, env);
}

int __libc_start_main(
    int (*main) (int, char * *, char * *),
    int argc, char * * ubp_av,
    void (*init) (void),
    void (*fini) (void),
    void (*rtld_fini) (void),
    void (* stack_end)
  )
{
  int (*next)(
    int (*main) (int, char * *, char * *),
    int argc, char * * ubp_av,
    void (*init) (void),
    void (*fini) (void),
    void (*rtld_fini) (void),
    void (* stack_end)
  ) = dlsym(RTLD_NEXT, "__libc_start_main");
  real_main = main;
  return next(my_main, argc, ubp_av, init, fini, rtld_fini, stack_end);
}

This prompts for the password, so you no longer have to keep the interposer library a secret. The placeholder argument is reused as password prompt, so invoke this like

LD_PRELOAD=$PWD/injectpassword.so darkcoind masternode start "Password: "

Another alternative would read the password from a file descriptor (like e.g. gpg --passphrase-fd does), or from x11-ssh-askpass, or whatever.

16
  • 4
    Although I don't understand and can't test the code, I get the gist of it, and this looks like an actual answer and should be the top answer. May 3, 2014 at 22:40
  • This is indeed awesome.
    – q9f
    May 4, 2014 at 11:19
  • 1
    It should be possible to take the password on STDIN and still have this work, which removes the strings vulnerability. See SO: Hide password input on terminal. May 4, 2014 at 17:32
  • 1
    @mulg0r: Standard extern "C" should do the trick of suppressing the name mangling for the relevant function, namely __libc_start_main.
    – MvG
    Apr 16, 2019 at 13:06
  • 1
    @polpetti: If you read github.com/nodejs/node/blob/… you can see that this assertion checks that memory is adjacent. So instead of replacing a single argument, you would have to allocate a block of memory for all the arguments you want to pass to main, then place the arguments into that one after the other. On the other hand I'm at a loss why you would want to use this hack for node. As I wrote, if you have the source code, fixing there is better by far. So edit the JavaScript or patch node.
    – MvG
    Feb 27, 2020 at 1:50
28

It's not just the history. It is going to show up in ps output as well.

Whoever wrote that piece of software should be hung, drawn and quartered. It is an absolute NO to have to supply a password on the command-line regardless whatever software it is.
For a daemon process it is even MORE unforgivable...

Besides rm -f on the software itself I don't know any solution for this. Honestly: Find other software to get the job done. Don't use such junk.

15
  • 9
    Thanks for not being helpful at all. This is a long discussed security issue, still unsolved and I need a better workaround than rm -f now.
    – q9f
    May 2, 2014 at 15:47
  • 17
    Actually, he's being very helpful. If you're passing the passphrase as an argument, it WILL show up in ps. So until the dev can fix that, he's suggesting using something else.
    – Safado
    May 2, 2014 at 15:49
  • 3
    Then you better start writing another Operating System. There is NO other solution currently available that I am aware of. By God I wish there was one. You are not the only one with this problem.
    – Tonny
    May 2, 2014 at 15:51
  • 8
    vertoe, don't get snippy. You can ask for a way to pass it on little slips of paper, but that doesn't mean any such way automatically exists. read_x is fine, but still exposes the passphrase via eg ps, so it's no better than the rm solution.
    – MadHatter
    May 2, 2014 at 15:57
  • 7
    Before y'all go and throw another +1 on this not-really-an-answer and complain that this is impossible, I suggest you review MvG's answer below May 3, 2014 at 22:45
20

This will clear the ps output.

BE VERY AWARE: This could break the application. You are duly warned that here be dragons.

  • Foreign processes shouldn't be fiddling around in a processes memory.
  • If the process relies on this region for the password, you may break your application.
  • Doing this could corrupt any working data you have in that process.
  • This is an insane hack.

Now you are duly notified of these dire warnings. This will clear the output displayed in ps. It will not clear your history, nor will it clear the bash job history (such as running the process like myprocess myargs &). But ps will no longer show the arguments.

#!/usr/bin/python
import os, sys
import re

PAGESIZE=4096

if __name__ == "__main__":
  if len(sys.argv) < 2:
    sys.stderr.write("Must provide a pid\n")
    sys.exit(1)

  pid = sys.argv[1]

  try:
    cmdline = open("/proc/{0}/cmdline".format(pid)).read(8192)

    ## On linux, at least, argv is located in the stack. This is likely o/s
    ## independent.
    ## Open the maps file and obtain the stack address.
    maps = open("/proc/{0}/maps".format(pid)).read(65536)
    m = re.search('([0-9a-f]+)-([0-9a-f]+)\s+rw.+\[stack\]\n', maps)
    if not m:
      sys.stderr.write("Could not find stack in process\n");
      sys.exit(1)

    start = int("0x"+m.group(1), 0)
    end = int("0x"+m.group(2), 0)

    ## Open the mem file
    mem = open('/proc/{0}/mem'.format(pid), 'r+')
    ## As the stack grows downwards, start at the end. It is expected
    ## that the value we are looking for will be at the top of the stack
    ## somewhere
    ## Seek to the end of the stack minus a couple of pages.
    mem.seek(end-(2*PAGESIZE))

    ## Read this buffer to the end of the stack
    stackportion = mem.read(8192)
    ## look for a string matching cmdline. This is pretty dangerous.
    ## HERE BE DRAGONS
    m = re.search(cmdline, stackportion)
    if not m:
      ## cause this is an example dont try to search exhaustively, just give up
      sys.stderr.write("Could not find command line in the stack. Giving up.")
      sys.exit(1)

    ## Else, we got a hit. Rewind our file descriptor, plus where we found the first argument.
    mem.seek(end-(2*PAGESIZE)+m.start())
    ## Additionally, we'll keep arg0, as thats the program name.
    arg0len = len(cmdline.split("\x00")[0]) + 1
    mem.seek(arg0len, 1)

    ## lastly overwrite the remaining region with nulls.
    writeover = "\x00" * (len(cmdline)-arg0len)
    mem.write(writeover)

    ## cleanup
    mem.close()

  except OSError, IOError:
    sys.stderr.write("Cannot find pid\n")
    sys.exit(1)

Invoke the program by saving it, chmod +x it. Then doing ./whatever <pidoftarget> If this works, it will produce no output. If it fails, it will complain about something and quit.

4
  • 18
    . . . this is both creative and frightening.
    – voretaq7
    May 2, 2014 at 22:06
  • EEK! Now I'm scared. May 3, 2014 at 8:59
  • Yikkes, that could work... I am not sure it something like AppArmor would catch this ? Also the virusscanner could potentially catch this and cause havoc by blocking the offending account which would be 'root'. There be dragons indeed....
    – Tonny
    May 3, 2014 at 10:49
  • @Tonny For protected domains, SELinux would prevent this. Your basic Unix permissions (DAC) lacks enough subject granularity to offer any protections from this behaviour (permits modification of processes memory within the same UID). Anyways, its not a bug -- its a feature. I believe this is how gdb can modify the memory of running processes (with much more surgical precision than this I might add). May 3, 2014 at 13:17
11

Can you pass the argument from a file, accessible only by root or the required user?

It's a HUGE no-no to type passwords in the console, but last recourse...begin your line with a space so it doesn't appear in the history.

2
  • There was a shell option which enables it, but I think it wasn't enabled by default. May 2, 2014 at 19:24
  • 1
    export HISTCONTROL=ignoreboth ignores both duplicates and lines with a leading space for entry into the history. Add it to your .bashrc or .bash_profile.
    – Andreas
    Feb 7, 2017 at 20:40
7

Maybe this works (?):

darkcoind masternode start `cat password.txt`
8
  • 4
    Or even darkcoind masternode start `head -1`, if you want to enter the password manually.
    – kasperd
    May 2, 2014 at 19:58
  • 16
    The passphrase is still available via ps and similar utilities.
    – voretaq7
    May 2, 2014 at 21:17
  • 1
    Moving from a plaintext password in .bash_history to a plaintext password in password.txt gains you what, exactly?
    – MikeyB
    May 3, 2014 at 14:24
  • 2
    @MikeyB: There is a small win: you won't accidentially expose it while searching through your history while someone is looking over your shoulder.
    – MvG
    May 3, 2014 at 20:49
  • 1
    @MikeyB, you may create and remove that file each time.
    – RiaD
    May 4, 2014 at 8:11
5

Unfortunately, if your darkcoind command expects the password as a command-line argument, then it will be exposed through utilities such as ps. The only real solution is to educate the developers.

While the ps exposure might be unavoidable, you could at least keep the password from being written out in the shell history file.

$ xargs darkcoind masternode start

password

CtrlD

The history file should only record xargs darkcoind masternode start, not the password.

1
  • 2
    Or if you're using bash, put ignorespace in $HISTCONTROL, and then you can prevent any command from going into the shell history by prefixing the command with a space.
    – derobert
    May 5, 2014 at 17:48
3

As others have stated, look into your shell history control for hiding the information from history.

But one thing nobody seems to have suggested yet is to mount /proc with the hidepid parameter. Try modifying your /proc line in /etc/fstab to include hidepid, like this:

# <file system> <mount point>   <type>  <options>       <dump>  <pass>
proc            /proc           proc    defaults,hidepid=2        0       0
3

For Bitcoin, the official developer answer is to use the provided python wrapper in contrib/bitrpc/bitrpc.py (github):

It asks for a password in a secure way if you use the command walletpassphrase, for example. There are no plans to add interactive functionality to bitcoin-cli.

and:

bitcoin-cli will remain as-is and not gain interactive functionality.

Source: #2318

Unlock wallet:

$ python bitrpc.py walletpassphrase

Change passphrase:

$ python bitrpc.py walletpassphrasechange

https://github.com/bitcoin/bitcoin/tree/master/contrib/bitrpc

For darkcoin it works anlogue:

https://github.com/darkcoin/darkcoin/tree/master/contrib/bitrpc

2

You can keep the password out of your shell's history by executing the command from a new shell process, which you then immediately terminate. For example:

bash$ sh
sh$ darkcoind masternode start 'correct horse battery staple'
sh$ exit
bash$

Make sure sh is configured not to save its history in a file.

Of course this doesn't address the other problems, such as the password being visible in ps. There are, I believe, ways for the darkcoind program itself to hide the information from ps, but that only shortens the window of vulnerability.

3
  • 1
    the passphrase is still available via ps and similar utilities.
    – voretaq7
    May 2, 2014 at 21:17
  • 3
    @voretaq7: Yes, as I explicitly acknowleged in the last paragraph of my answer. May 2, 2014 at 21:36
  • 3
    Indeed - you were the victim of wanton copypasta on my part:)
    – voretaq7
    May 2, 2014 at 22:12

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .