Argumente ausblenden, um ohne Quellcode zu programmieren

15

Ich muss einige sensible Argumente für ein von mir ausgeführtes Programm verbergen, habe aber keinen Zugriff auf den Quellcode. Ich führe dies auch auf einem gemeinsam genutzten Server aus, sodass ich so etwas nicht verwenden kann, hidepidweil ich keine sudo-Berechtigungen habe.

Hier sind einige Dinge, die ich ausprobiert habe:

  • export SECRET=[my arguments], gefolgt von einem Anruf bei ./program $SECRET, aber das scheint nicht zu helfen.

  • ./program `cat secret.txt`Wo secret.txtsind meine Argumente, aber der Allmächtige pskann meine Geheimnisse aufspüren.

Gibt es eine andere Möglichkeit, meine Argumente zu verbergen, ohne dass der Administrator eingreift?

FRAU
quelle
Was ist das für ein bestimmtes Programm? Wenn es ein gewöhnlicher Befehl ist, müssen Sie sagen (und es könnte einen anderen Ansatz geben), um welchen es sich handelt
Basile Starynkevitch
14
Damit Sie verstehen, was los ist, haben die Dinge, die Sie versucht haben, keine Chance zu funktionieren, da die Shell dafür verantwortlich ist, Umgebungsvariablen zu erweitern und eine Befehlssubstitution durchzuführen, bevor Sie das Programm aufrufen. pstut nichts Magisches, um "deine Geheimnisse herauszuspüren". Auf jeden Fall sollten vernünftig geschriebene Programme stattdessen eine Befehlszeilenoption bieten, mit der ein Geheimnis aus einer angegebenen Datei oder aus stdin gelesen werden kann, anstatt es direkt als Argument zu verwenden.
Jamesdlin
Ich führe ein Wettersimulationsprogramm aus, das von einer privaten Firma geschrieben wurde. Sie geben weder ihren Quellcode weiter, noch bieten ihre Dokumentationen eine Möglichkeit, ein Geheimnis aus einer Datei weiterzugeben. Möglicherweise sind hier keine Optionen verfügbar
MS

Antworten:

25

Wie hier erklärt , fügt Linux die Argumente eines Programms in den Datenraum des Programms ein und behält einen Zeiger auf den Anfang dieses Bereichs bei. Dies wird von psusw. verwendet, um die Programmargumente zu finden und anzuzeigen.

Da sich die Daten im Speicherbereich des Programms befinden, können sie bearbeitet werden. Um dies zu tun, ohne das Programm selbst zu ändern, muss ein Shim mit einer main()Funktion geladen werden, die vor der eigentlichen Hauptfunktion des Programms aufgerufen wird. Dieser Shim kann die echten Argumente in ein neues Feld kopieren und dann die ursprünglichen Argumente überschreiben, sodass psnur nuls angezeigt werden.

Der folgende C-Code führt das aus.

/* /unix//a/403918/119298
 * capture calls to a routine and replace with your code
 * gcc -Wall -O2 -fpic -shared -ldl -o shim_main.so shim_main.c
 * LD_PRELOAD=/.../shim_main.so theprogram theargs...
 */
#define _GNU_SOURCE /* needed to get RTLD_NEXT defined in dlfcn.h */
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <signal.h>
#include <unistd.h>
#include <dlfcn.h>

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

/* copy argv to new location */
char **copyargs(int argc, char** argv){
    char **newargv = malloc((argc+1)*sizeof(*argv));
    char *from,*to;
    int i,len;

    for(i = 0; i<argc; i++){
        from = argv[i];
        len = strlen(from)+1;
        to = malloc(len);
        memcpy(to,from,len);
        memset(from,'\0',len);    /* zap old argv space */
        newargv[i] = to;
        argv[i] = 0;
    }
    newargv[argc] = 0;
    return newargv;
}

static int mymain(int argc, char** argv, char** env) {
    fprintf(stderr, "main argc %d\n", argc);
    return real_main(argc, copyargs(argc,argv), env);
}

int __libc_start_main(pfi main, int argc,
                      char **ubp_av, void (*init) (void),
                      void (*fini)(void),
                      void (*rtld_fini)(void), void (*stack_end)){
    static int (*real___libc_start_main)() = NULL;

    if (!real___libc_start_main) {
        char *error;
        real___libc_start_main = dlsym(RTLD_NEXT, "__libc_start_main");
        if ((error = dlerror()) != NULL) {
            fprintf(stderr, "%s\n", error);
            exit(1);
        }
    }
    real_main = main;
    return real___libc_start_main(mymain, argc, ubp_av, init, fini,
            rtld_fini, stack_end);
}

Es ist nicht möglich, einzugreifen main(), aber Sie können in die Standard-C-Bibliotheksfunktion eingreifen __libc_start_main, die dann main aufruft. Kompilieren Sie diese Datei shim_main.cwie im Kommentar zu Beginn angegeben und führen Sie sie wie gezeigt aus. Ich habe einen printfim Code hinterlassen, damit Sie überprüfen, ob er tatsächlich aufgerufen wird. Führen Sie beispielsweise aus

LD_PRELOAD=/tmp/shim_main.so /bin/sleep 100

dann tut ein psund Sie erhalten ein leerer Befehl und args werden abgebildet.

Es gibt noch eine kleine Zeitspanne, in der die Befehlsargumente möglicherweise sichtbar sind. Um dies zu vermeiden, können Sie beispielsweise das Shim ändern, um Ihr Geheimnis aus einer Datei zu lesen und es zu den an das Programm übergebenen Argumenten hinzuzufügen.

meuh
quelle
12
Es wird jedoch weiterhin ein kurzes Fenster angezeigt, in dem /proc/pid/cmdlinedas Geheimnis angezeigt wird (genau wie beim curlVersuch, das in der Befehlszeile angegebene Kennwort zu verbergen). Während Sie LD_PRELOAD verwenden, können Sie main so umbrechen, dass das Geheimnis aus der Umgebung in das Argument kopiert wird, das main empfängt. Wie anrufen, LD_PRELOAD=x SECRET=y cmdwo Sie main()mit dem argv[]Sein anrufen[argv[0], getenv("SECRET")]
Stéphane Chazelas
Sie können die Umgebung nicht verwenden, um ein Geheimnis zu verbergen, wie es über sichtbar ist /proc/pid/environ. Dies kann auf die gleiche Weise wie die Argumente überschrieben werden, aber es bleibt das gleiche Fenster.
meuh
11
/proc/pid/cmdlineist öffentlich, /proc/pid/environist nicht. Es gab einige Systeme, auf denen ps(eine Setuid-Programmdatei) die Umgebung eines Prozesses offengelegt hat, aber ich glaube nicht, dass Sie heutzutage darauf stoßen werden. Die Umwelt wird im Allgemeinen als sicher genug angesehen . Es ist nicht sicher, Prozesse mit derselben euid auszuloten, aber diese können häufig die Erinnerung an Prozesse mit derselben euid lesen, sodass Sie nicht viel dagegen tun können.
Stéphane Chazelas
4
@ StéphaneChazelas: Wenn man die Umgebung verwendet, um Geheimnisse weiterzugeben, entfernt der Wrapper, der ihn an die mainMethode des umschlossenen Programms weiterleitet, im Idealfall auch die Umgebungsvariable, um ein versehentliches Verlieren für untergeordnete Prozesse zu vermeiden. Alternativ könnte der Wrapper alle Befehlszeilenargumente aus einer Datei lesen .
David Foerster
@ DavidFoerster, guter Punkt. Ich habe meine Antwort aktualisiert, um dies zu berücksichtigen.
Stéphane Chazelas
16
  1. Lesen Sie die Dokumentation der Befehlszeilenschnittstelle der betreffenden Anwendung. Es kann durchaus eine Option geben, das Geheimnis aus einer Datei anstatt direkt als Argument anzugeben.

  2. Wenn dies fehlschlägt, reichen Sie einen Fehlerbericht für die Anwendung mit der Begründung ein, dass es keine sichere Möglichkeit gibt, ein Geheimnis für die Anwendung bereitzustellen.

  3. Sie können die Lösung in meuhs Antwort jederzeit sorgfältig (!) An Ihre spezifischen Bedürfnisse anpassen . Achten Sie besonders auf Stéphanes Kommentar und seine Folgemaßnahmen.

David Foerster
quelle
12

Wenn Sie Argumente an das Programm übergeben müssen, um es zum Laufen zu bringen, haben Sie Pech, egal was Sie tun, wenn Sie es nicht hidepidfür procfs verwenden können.

Da Sie erwähnt haben, dass dies ein Bash-Skript ist, sollten Sie den Quellcode bereits verfügbar haben, da Bash keine kompilierte Sprache ist.

Andernfalls können Sie möglicherweise die Cmdline des Prozesses mit gdboder ähnlichem umschreiben und mit argc/ herumspielen, argvsobald er bereits gestartet wurde.

  1. Dies ist nicht sicher, da Sie Ihre Programmargumente zunächst noch offen legen, bevor Sie sie ändern
  2. Das ist ziemlich abgedreht, selbst wenn Sie es zum Laufen bringen könnten, würde ich nicht empfehlen, sich darauf zu verlassen

Ich würde wirklich nur empfehlen, den Quellcode zu beschaffen oder mit dem Hersteller zu sprechen, um den Code zu modifizieren. Die Angabe von Geheimnissen in der Befehlszeile in einem POSIX-Betriebssystem ist nicht mit dem sicheren Betrieb kompatibel.

Chris Down
quelle
11

Wenn ein Prozess einen Befehl ausführt (über den execve()Systemaufruf), wird sein Speicher gelöscht. Um einige Informationen über die Ausführung weiterzuleiten, benötigt der execve()Systemaufruf zwei Argumente: die Arrays argv[]und envp[].

Das sind zwei Arrays von Strings:

  • argv[] enthält die Argumente
  • envp[]Enthält die Umgebungsvariablendefinitionen als Zeichenfolgen im var=valueFormat (gemäß Konvention).

Wenn Sie das tun:

export SECRET=value; cmd "$SECRET"

(hier die fehlenden Anführungszeichen um die Parametererweiterung hinzugefügt).

Sie führen cmdmit secret ( value) aus, das sowohl in argv[]als auch übergeben wurdeenvp[] . argv[]wird ["cmd", "value"]und envp[]so ähnlich sein [..., "PATH=/bin:...", "HOME=...", ..., "SECRET=value", "TERM=xterm", ...]. Da cmdkeine getenv("SECRET")oder eine gleichwertige Aktion zum Abrufen des Werts des Geheimnisses von dieser SECRETUmgebungsvariablen ausgeführt wird, ist es nicht sinnvoll, es in die Umgebung zu stellen.

argv[]ist öffentliches Wissen. Es zeigt in der Ausgabe von ps. envp[]heutzutage nicht. Unter Linux wird dies in angezeigt /proc/pid/environ. Es wird in der Ausgabe von ps ewwwBSDs (und mit procps-ngs psunter Linux) angezeigt , jedoch nur für Prozesse, die mit derselben effektiven UID ausgeführt werden (und mit weiteren Einschränkungen für ausführbare setuid / setgid-Dateien). Es wird möglicherweise in einigen Überwachungsprotokollen angezeigt, aber auf diese Überwachungsprotokolle sollten nur Administratoren zugreifen können.

Kurz gesagt, die Umgebung, die an eine ausführbare Datei übergeben wird, soll privat sein oder zumindest ungefähr so ​​privat wie der interne Speicher eines Prozesses (auf den ein anderer Prozess mit den richtigen Berechtigungen unter Umständen auch mit einem Debugger zugreifen kann und kann) auch auf die Festplatte gespeichert werden).

Da dies argv[]allgemein bekannt ist, ist ein Befehl, der Daten erwartet, die in seiner Befehlszeile geheim sein sollen, vom Entwurf her fehlerhaft.

In der Regel stellen Befehle, die geheim gehalten werden müssen, eine andere Schnittstelle zur Verfügung, z. B. über eine Umgebungsvariable. Zum Beispiel:

IPMI_PASSWORD=secret ipmitool -I lan -U admin...

Oder über einen dedizierten Dateideskriptor wie stdin:

echo secret | openssl rsa -passin stdin ...

(echo Wird eingebaut, wird in der Ausgabe von nicht angezeigt. ps)

Oder eine Datei, wie die .netrcfürftp und ein paar andere Befehle oder

mysql --defaults-extra-file=/some/file/with/password ....

Einige Anwendungen wie curl(und das ist auch der Ansatz von @meuh hier ) versuchen, das Passwort, das sie argv[]von neugierigen Blicken erhalten haben , zu verbergen (auf einigen Systemen, indem sie den Teil des Speichers überschreiben, in dem sich das befindet)argv[] Zeichenfolgen gespeichert wurden). Aber das hilft nicht wirklich und gibt ein falsches Sicherheitsversprechen. Dadurch bleibt ein Fenster zwischen dem execve()und dem Überschreiben, in dem psdas Geheimnis noch angezeigt wird.

Wenn ein Angreifer zum Beispiel weiß, dass Sie ein Skript ausführen, das einen curl -u user:somesecret https://...(zum Beispiel in einem Cron-Job) ausführt, muss er nur die (vielen) Bibliotheken aus dem Cache entfernen, curldie (zum Beispiel durch Ausführen eines) verwendet werdensh -c 'a=a;while :; do a=$a$a;done' ) verwendet werden Um den Startvorgang zu verlangsamen, until grep 'curl.*[-]u' /proc/*/cmdline; do :; donereicht es aus, dieses Kennwort in meinen Tests abzufangen.

Wenn die Argumente die einzige Möglichkeit sind, das Geheimnis an die Befehle weiterzugeben, gibt es möglicherweise noch einige Dinge, die Sie versuchen könnten.

Auf einigen Systemen, einschließlich älterer Linux-Versionen, können nur die ersten Bytes (4096 unter Linux 4.1 und früher) der eingegebenen Zeichenfolgen argv[]abgefragt werden.

Dort könnten Sie tun:

(exec -a "$(printf %-4096s cmd)" cmd "$secret")

Und das Geheimnis wäre verborgen, weil es hinter den ersten 4096 Bytes ist. Jetzt müssen Leute, die diese Methode verwendet haben, es jetzt bereuen, da Linux seit 4.2 die Liste der Argumente in nicht mehr abschneidet /proc/pid/cmdline. Beachten Sie auch, dass es nicht so ist, weil psnicht mehr als so viele Bytes einer Befehlszeile angezeigt werden (wie bei FreeBSD, wo es auf 2048 beschränkt zu sein scheint), dass man nicht dieselbe API psverwenden kann, um mehr zu erhalten. Dieser Ansatz ist jedoch auf Systemen gültig, auf denen psnur ein normaler Benutzer diese Informationen abrufen kann (z. B. wenn die API privilegiert ist undps setgid oder setuid ist, um sie zu verwenden), die dort jedoch möglicherweise nicht zukunftssicher sind.

Ein anderer Ansatz wäre, das Geheimnis nicht weiterzugeben, argv[]sondern Code in das Programm einzufügen (mithilfe von gdboder einem $LD_PRELOADHack), bevor main()es gestartet wird, der das Geheimnis in das argv[]empfangene von einfügt execve().

Mit LD_PRELOADfür nicht-setuid / setgid dynamisch verknüpfte ausführbare Dateien auf einem GNU-System:

/* 
 * replace ***** with secret read from fd 9
 * gcc -Wall -fpic -shared -o inject_secret.so inject_secret.c -ldl 
 * LD_PRELOAD=/.../inject_secret.so cmd -p '*****' 9<<< secret
 */
#define _GNU_SOURCE
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <dlfcn.h>

#define PLACEHOLDER "*****"
static char secret[1024];

int __libc_start_main(int (*main) (int, char**, char**),
                      int argc,
                      char **argv,
                      void (*init) (void),
                      void (*fini)(void),
                      void (*rtld_fini)(void),
                      void (*stack_end)){
    static int (*real_libc_start_main)() = NULL;
    int n;

    if (!real_libc_start_main) {
        real_libc_start_main = dlsym(RTLD_NEXT, "__libc_start_main");
        if (!real_libc_start_main) abort();
    }

    n = read(9, secret, sizeof(secret));
    if (n > 0) {
      int i;

      if (secret[n - 1] == '\n') secret[--n] = '\0'; 
      for (i = 1; i < argc; i++)
        if (strcmp(argv[i], PLACEHOLDER) == 0)
          argv[i] = secret;
    }

    return real_libc_start_main(main, argc, argv, init, fini,
                                rtld_fini, stack_end);
}

Dann:

$ gcc -Wall -fpic -shared -o inject_secret.so inject_secret.c -ldl
$ LD_PRELOAD=$PWD/inject_secret.so  ps '*****' 9<<< "-opid,args"
  PID COMMAND
 7659 /bin/zsh
 8828 ps *****

Zu keinem Zeitpunkt hätte pssich das ps -opid,argsdort gezeigt ( -opid,argsin diesem Beispiel das Geheimnis). Beachten Sie, dass wir Elemente der sind zu ersetzen argv[]Array von Zeigern , die Saiten nicht zwingend durch diese Zeiger zeigten auf, weshalb unsere Änderungen werden nicht in dem Ausgang ps.

Mit gdb, immer noch für nicht-setuid / setgid dynamisch verknüpfte ausführbare Dateien und auf GNU-Systemen:

tmp=$(mktemp) && cat << EOF > "$tmp" &&
break __libc_start_main
commands 1
set argv[1]="-opid,args"
continue
end
run
EOF

gdb -n --batch-silent --return-child-result -x "$tmp" --args ps '*****'
rm -f -- "$tmp"

Noch mit gdb, ein nicht-GNU spezifischen Ansatz, der nicht auf ausführbare Dateien angewiesen ist dynamisch gelinkt oder Debug - Symbole werden und sollte für jede ELF ausführbare Datei auf Linux zumindest sein könnte funktionieren:

#! /bin/sh -
# gdb+sh polyglot script to replace "*****" arguments with the content
# of the SECRET environment variable *after* execve and before calling
# the executable's main() function.
#
# Usage: SECRET=somesecret cmd --password '*****'

if ':' - ':'
then
  # running in sh
  # retrieve the start address for the executable
  start=$(
    LC_ALL=C objdump -f -- "$(command -v -- "${1?}")" |
    sed -n 's/^start address //p'
  )
  [ -n "$start" ] || exit
  # re-exec ourself with gdb.
  exec gdb -n --batch-silent --return-child-result -iex "set \$start = $start" -x "$0" --args "$@"
  exit 1
fi
end
# running in gdb
break *$start
commands 1
  # The stack on startup contains:
  # argc argv[0]... argv[argc-1] 0 envp[0] envp[1]... 0 argv[] and envp[] strings
  set $argc = *((int*)$sp)
  set $argv = &((char**)$sp)[1]
  set $envp = &($argv[$argc+1])
  set $i = 0
  while $envp[$i]
    # look for an envp[] string starting with "SECRET=". We can't use strcmp()
    # here as there's no guarantee that the debugged executable has such
    # a function
    set $e = $envp[$i]
    if $e[0] == 'S' && \
       $e[1] == 'E' && \
       $e[2] == 'C' && \
       $e[3] == 'R' && \
       $e[4] == 'E' && \
       $e[5] == 'T' && \
       $e[6] == '='
      set $secret = &($e[7])
      # replace SECRET=xxx<NUL> with SECRE=<NUL>
      set $e[5] = '='
      set $e[6] = '\0'
      # not calling loop_break as that causes a SEGV with my version of gdb
    end
    set $i = $i + 1
  end
  if $secret
    # now looking for argv[] strings being "*****" and replace them with
    # the secret identified earlier
    set $i = 0
    while $i < $argc
      set $a = $argv[$i]
      if $a[0] == '*' && \
       $a[1] == '*' && \
       $a[2] == '*' && \
       $a[3] == '*' && \
       $a[4] == '*' && \
       $a[5] == '\0'
        set $argv[$i] = $secret
      end
      set $i = $i + 1
    end
  end
  # using "continue" as "detach" causes a SEGV with my version of gdb.
  continue
end
run

Testen mit einer statisch verknüpften ausführbaren Datei:

$ SECRET=/proc/self/cmdline ./replace_secret busybox cat '*****' | tr '\0' '\n'
/bin/busybox
cat
*****

Wenn die ausführbare Datei möglicherweise statisch ist, haben wir keine zuverlässige Möglichkeit, Speicher zum Speichern des Geheimnisses zuzuweisen. Daher müssen wir das Geheimnis von einer anderen Stelle abrufen, die sich bereits im Prozessspeicher befindet. Deshalb ist die Umwelt hier die naheliegende Wahl. Wir verbergen auch diese Umgebungsvariable für SECRETden Prozess (indem wir sie in ändern SECRE=), um zu verhindern, dass sie verloren geht, wenn der Prozess aus irgendeinem Grund beschließt, seine Umgebung zu sichern oder nicht vertrauenswürdige Anwendungen auszuführen.

Das funktioniert auch auf Solaris 11 (vorausgesetzt , GDB und GNU binutils installiert sind (Sie müssen umbenennen können objdumpzu gobjdump).

Unter FreeBSD (mindestens x86_64, ich bin mir nicht sicher, welche der ersten 24 Bytes (die 16 werden, wenn gdb (8.0.1) interaktiv ist und darauf hindeutet, dass es dort einen Fehler in gdb gibt) auf dem Stack sind), ersetzen Sie die argcund argv-Definitionen mit:

set $argc = *((int*)($sp + 24))
set $argv = &((char**)$sp)[4]

(Möglicherweise müssen Sie auch das gdbPaket / den Port installieren, da die Version, die sonst mit dem System geliefert wird, veraltet ist.)

Stéphane Chazelas
quelle
Zu (hier wurden die fehlenden Anführungszeichen um die Parametererweiterung hinzugefügt): Was ist falsch daran, die Anführungszeichen nicht zu verwenden? Gibt es wirklich einen Unterschied?
Yukashima Huksay
@yukashimahuksay, siehe zum Beispiel Sicherheitsaspekte des Vergessens, eine Variable in bash / POSIX-Shells und den dort verknüpften Fragen anzugeben .
Stéphane Chazelas
3

Was Sie tun könnten, ist

 export SECRET=somesecretstuff

Nehmen Sie dann an, Sie schreiben Ihr ./programin C (oder jemand anderes kann es für Sie ändern oder verbessern), und verwenden Sie getenv (3) in diesem Programm, vielleicht als

char* secret= getenv("SECRET");

und nach dem export laufen sie einfach ./programin der gleichen shell. Oder der Name der Umgebungsvariablen könnte übergeben werden (durch Ausführen von ./program --secret-var=SECRETetc ...)

psIch werde nichts über dein Geheimnis verraten, aber proc (5) kann immer noch eine Menge Informationen liefern (zumindest für andere Prozesse desselben Benutzers).

Lesen Sie auch diese Informationen, um die Übergabe von Programmargumenten zu vereinfachen.

In dieser Antwort finden Sie eine bessere Erklärung zum Globbing und zur Rolle einer Shell.

Vielleicht haben Sie programandere Möglichkeiten, um Daten abzurufen (oder die Kommunikation zwischen Prozessen klüger zu gestalten) als mit einfachen Programmargumenten (dies sollte auf jeden Fall der Fall sein, wenn sensible Informationen verarbeitet werden sollen). Lesen Sie die Dokumentation. Oder Sie missbrauchen dieses Programm (das nicht zur Verarbeitung geheimer Daten vorgesehen ist).

Geheime Daten zu verstecken ist wirklich schwierig. Es reicht nicht aus, es nicht durch Programmargumente zu geben.

Basile Starynkevitch
quelle
5
Aus der Frage geht ziemlich klar hervor, dass er nicht einmal den Quellcode dafür hat ./program, sodass die erste Hälfte dieser Antwort nicht relevant zu sein scheint.
Pipe