http://www.perlmonks.org?node_id=1207173


in reply to Hide DBI password in scripts

Most of the posts I've seen say it is impossible to achieve unbreakable security in this respect.

Whatever you do, you will end with a program (C, Perl, whatever) that contains two parts:

  1. an encrypted or obfuscated secret (password or username and password)
  2. all code required to decrypt or de-obfuscate the secret

In pseudo-code:

my $secret="(binary garbage here)"; sub checkSecurity; sub decodeSecret; sub reencode; if (checkSecurity()) { # <--- bypass the function call or make the fu +nction return TRUE my $plaintext=decodeSecret($secret); # breakpoint here say $plaintext; # or, if you like: say reencode($plaintext); } else { die "Insecure condition found, won't tell you the secret!\n"; }

It should be obvious that this can be broken quite easily. Start the program under control of a debugger and let it run to the place where it has decrypted or de-obfuscated the secret, break and dump the secret. Yes, there are ways to make exactly this harder. Search the web for anti-debugging techniques and how to bypass them. You can and should include the security checks in the decoder. But again, you can bypass those checks.

But if you use an external program from a perl script, it is even easier: Just make the perl script print the secret. Everything needed to make the external program happy must be present in the perl script. For a DBI application, just replace DBI->connect() with die. You don't even have to modify the perl script, a tiny module like the following, combined with perl -MDBIspy victim.pl should be sufficient.

package DBIspy; use strict; use warnings; use feature 'say'; use DBI; no warnings 'redefine'; sub DBI::connect { say for @_; die 'BROKEN'; } 1;

Demo:

> perl -MDBIspy -E 'use DBI; DBI->connect("dbi:SQLite:","user","passwo +rd")' DBI dbi:SQLite: user password BROKEN at DBIspy.pm line 12. >

There are several password managers available, commonly bundled with the Linux desktop environment you use. Whatever interface it uses, guess what happens when you need to call DBI->connect() with a secret from the password manager. Right, you need code that finally extracts the secret as plain text. And guess what happens when you use that ridiculously trivial DBIspy module. Your secret is no longer secret.

Alexander

--
Today I will gladly share my knowledge and experience, for there are no sweeter words than "I told you so". ;-)

Replies are listed 'Best First'.
Re^2: Hide DBI password in scripts
by danielgr (Novice) on Jan 12, 2018 at 23:56 UTC

    Not sure if I misunderstood:

    1. In order to use the C debugger, you would need access to the source. The source can be stashed away after the executable is created. - sorry this is a misstatement. It has been a long time since I've used gdb, so the source may not be required. Ahh! Just realized you were talking about the Perl debugger.

    2. The C program would not relinquish the password if the inode of the registered script has changed, meaning the script has changed - so a hacker can't add a "die" or just print the password.

      Not sure if I misunderstood:

      1. In order to use the C debugger, you would need access to the source. The source can be stashed away after the executable is created. - sorry this is a misstatement. It has been a long time since I've used gdb, so the source may not be required. Ahh! Just realized you were talking about the Perl debugger.

      No, I was not talking specifically about the perl debugger, but of course, the perl debugger usually has source access (except for XS code).

      Source code is not needed to debug a compiled program. You won't have nice C source in the debugger output, but you can step through the disassembled code and set breakpoints, even if the debug information has been stripped from the binary.

      Semi-automatic disassemblers like IDA can be very helpful to find places to set breakpoints at runtime. Just reading the relevant parts of the disassembly can be sufficient, if the de-obfuscation / decryption code is a single function. And yes, IDA can disassemble executables for many different processors and operating systems.

      2. The C program would not relinquish the password if the inode of the registered script has changed, meaning the script has changed - so a hacker can't add a "die" or just print the password.

      I can make the perl program print out the secret and stop without ever touching the main script. Have a look at how I injected the DBIspy module.

      I can have a second perl script modify the script and restore all meta-data in the inode (see lstat and especially utime). File size won't change for an injected die, I can easily pad the file with some commented-out garbage to the old size.

      I can use environment variables to modify @INC and thus load modified modules. Your code surely uses strict somewhere. Guess how hard it would be to include DBIspy or similar in a modifed copy of strict.pm.

      I can use LD_PRELOAD to load an additional library into the perl interpreter. Code in the library would be executed before perl's main(), would call the magic password program and abort the program before perl's main() is reached.


      Update: strace-demo

      /tmp>cat secret-keeper.c #include <stdio.h> int main(int argc, char ** argv) { // note: security checks omitted fputs("find me, I'm the secret",stdout); return 0; } /tmp>make secret-keeper cc secret-keeper.c -o secret-keeper /tmp>strip secret-keeper /tmp>cat secret-reader.pl #!/usr/bin/perl use strict; use warnings; my $secret=qx(./secret-keeper); print "The secret is <$secret>\n"; /tmp>perl secret-reader.pl The secret is <find me, I'm the secret> /tmp>strace -o trace perl secret-reader.pl The secret is <find me, I'm the secret> /tmp>grep -C5 find trace /dev/null trace-read(5, "", 4) = 0 trace-close(5) = 0 trace-ioctl(3, TCGETS, 0x7ffeddbf80a0) = -1 ENOTTY (Inappropria +te ioctl for device) trace-lseek(3, 0, SEEK_CUR) = -1 ESPIPE (Illegal see +k) trace-fstat(3, {st_mode=S_IFIFO|0600, st_size=0, ...}) = 0 trace:read(3, "find me, I'm the secret", 8192) = 23 trace-read(3, "", 8192) = 0 trace---- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=12034, + si_uid=1001, si_status=0, si_utime=0, si_stime=0} --- trace-fstat(3, {st_mode=S_IFIFO|0600, st_size=0, ...}) = 0 trace-close(3) = 0 trace-wait4(12034, [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], 0, NULL) = + 12034 trace:write(1, "The secret is <find me, I'm the "..., 40) = 40 trace-rt_sigaction(SIGHUP, NULL, {SIG_DFL, [], 0}, 8) = 0 trace-rt_sigaction(SIGINT, NULL, {SIG_DFL, [], 0}, 8) = 0 trace-rt_sigaction(SIGQUIT, NULL, {SIG_DFL, [], 0}, 8) = 0 trace-rt_sigaction(SIGILL, NULL, {SIG_DFL, [], 0}, 8) = 0 trace-rt_sigaction(SIGTRAP, NULL, {SIG_DFL, [], 0}, 8) = 0 /tmp>

      Alexander

      --
      Today I will gladly share my knowledge and experience, for there are no sweeter words than "I told you so". ;-)
      The C program would not relinquish the password if the inode of the registered script has changed, meaning the script has changed - so a hacker can't add a "die" or just print the password.
      Oh, woe is me! I can't modify the Perl script to print out the parameters it passes to DBI::connect!

      Guess I'll edit DBI.pm instead and have the connect method print @_ before doing anything else.

      As afoken said, if you have both the hidden secret and the means of revealing the secret in the same place at the same time, then an attacker will be able to learn the secret, whether by subverting your decoding process or by learning how it works and applying it himself. You can make this more difficult, but you can never make it impossible, as has been repeatedly demonstrated by the ongoing wars between digital content providers and digital content pirates. The content providers have thrown boatloads of money at it and hired people far smarter than you or me, and the pirates invariably crack each new scheme in far less time than it took to devise and implement the scheme in the first place (often within a single day).

Re^2: Hide DBI password in scripts
by danielgr (Novice) on Jan 17, 2018 at 16:49 UTC

    We can eliminate the DBIspy threat by copying in the text of the DBI::connect sub to the caller program:

    sub DBI::connect { # code from original DBI::connect }


    This of course would have to be updated manually if a new version of DBI was installed.
    And, we can assume that the DBI.pm module is write-protected from all except root. (We can assume root is not compromised).
    Also, instead of the inode we can use checksums.

      We can eliminate the DBIspy threat by copying in the text of the DBI::connect sub to the caller program:

      sub DBI::connect { # code from original DBI::connect }

      Yes, sure. And I can read it from the script to be executed and look for functions called from the copied DBI::connect. Most trivially, the next function to attack would be the DBD's connect method. A few lines of code should be sufficient to load and patch the DBD before the script starts, very similar to my DBIspy hack.

      This of course would have to be updated manually if a new version of DBI was installed.

      So you are creating a maintainance nightmare here, for little or no gain.

      And, we can assume that the DBI.pm module is write-protected from all except root.

      Only if you use the system perl or a local perl installed by root. It won't be protected if perl is installed by the user running the script.

      (We can assume root is not compromised).

      Now, that's a very bold statement.

      With physical access to the machine, root is compromised within minutes on a standard installation of many Linux distributions. Simply because the boot loader does not prevent me from temporarily adding init=/bin/sh rw to the kernel's command line. After that, I have a root shell without a password prompt and can modify any file on the system. Or, I could reboot and run my own Linux from a CD or a USB stick to modify any file on the system. (So yes, you should prevent physical access, add BIOS and boot loader passwords, and encrypt all disks, if secret data is stored on a machine.)

      Without physical access, there are still many ways to get root access. Linux has security problems, like any other system. To make things worse, CPU bugs like Meltdown und Spectre hardware bugs can help gaining root access.

      But luckily, I don't need root access.

      Also, instead of the inode we can use checksums.

      Yes, but they won't help you against LD_PRELOAD. The perl script is unmodified, but I will still get the secret.

      Heck, I could even break checksum tests from within perl, with a perl loader script that has the following behaviour:

      1. Create a backup copy of victim.pl, including a-time and m-time (lstat).
      2. Completely replace the content of victim.pl with an attack script.
      3. exec("victim.pl",@ARGV), running the attack script.

      The attack script does the following:

      1. Restore victim.pl content from the backup copy. Note: at this point, perl has completely read the attack script from victim.pl and no longer cares about the content of victim.pl.
      2. Restore the original a-time and m-time of victim.pl (utime). Now, victim.pl looks sane and unmodified again, and it is. It will pass any checksum test. But perl is running my attack code.
      3. Remove the backup copy.
      4. Run any code from victim.pl required to execute secret-keeper (e.g. set up environment variables, file handles, ...)
      5. Run the unmodified, secret-revealing binary, and print out the secret revealed: print qx(./secret-keeper);

      secret-keeper has no chance to detect that attack. My loader script is no longer running. Its last action, exec("victim.pl"), removed all traces from memory. It will look like victim.pl was started directly by my shell. victim.pl's inode and content seem to be unmodified. This is also true for any module victim.pl might load. Whatever action the original victim.pl did before running secret-keeper, my attack script has done the same in the fourth step.

      Alexander

      --
      Today I will gladly share my knowledge and experience, for there are no sweeter words than "I told you so". ;-)

        update: this is an addendum to your node rather than a reply.

        Heck, I could even break checksum tests from within perl, with a perl loader script that has the following behaviour:
        ...

        These shenanigans can be countered, say, by using a monitored file system which reports writes and intercepts before you can exec the modified script. Next round.

        But then, having access, you could just take home a snapshot of all blocks pertaining to the suite, extract the password and come back later to do whatever you want to.

        Bottom line: if you have an automated process using credentials contained within, no matter how sophisticated the storage mechanisms are, all you can do is prevent interactive read/write/execute access to any parts of that automatic unit, which at the end of reasoning amounts to turning it into an inaccessible black box. But given that condition, there's no reason for not having plaintext credentials in it.

        So, I'd say that the OP is most likely about an XY Problem.

        See also.

        perl -le'print map{pack c,($-++?1:13)+ord}split//,ESEL'