Nascondi argomenti da programmare senza codice sorgente


15

Devo nascondere alcuni argomenti sensibili a un programma in esecuzione, ma non ho accesso al codice sorgente. Lo sto anche eseguendo su un server condiviso, quindi non posso usare qualcosa del genere hidepidperché non ho i privilegi di sudo.

Ecco alcune cose che ho provato:

  • export SECRET=[my arguments], seguito da una chiamata a ./program $SECRET, ma questo non sembra aiutare.

  • ./program `cat secret.txt`dove secret.txtcontiene i miei argomenti, ma l'Onnipotente psè in grado di annusare i miei segreti.

C'è un altro modo per nascondere i miei argomenti che non comportano l'intervento dell'amministratore?


Cos'è quel particolare programma? Se è un solito comando devi dire (e potrebbe esserci qualche altro approccio) quale sia
Basile Starynkevitch

14
Quindi capisci cosa sta succedendo, le cose che hai provato non hanno possibilità di funzionare perché la shell è responsabile dell'espansione delle variabili di ambiente e dell'esecuzione della sostituzione dei comandi prima di invocare il programma. psnon sta facendo nulla di magico per "fiutare i tuoi segreti". Comunque, i programmi scritti in modo ragionevole dovrebbero invece offrire un'opzione da riga di comando per leggere un segreto da un file specificato o da stdin invece di prenderlo direttamente come argomento.
jamesdlin,

Sto gestendo un programma di simulazione meteorologica scritto da una società privata. Non condividono il loro codice sorgente, né la loro documentazione fornisce alcun modo per condividere un segreto da un file. Potrebbe essere fuori dalle opzioni qui
MS

Risposte:


25

Come spiegato qui , Linux inserisce gli argomenti di un programma nello spazio dati del programma e mantiene un puntatore all'inizio di quest'area. Questo è ciò che viene utilizzato da pse così via per trovare e mostrare gli argomenti del programma.

Poiché i dati si trovano nello spazio del programma, possono manipolarli. Fare questo senza cambiare il programma stesso comporta il caricamento di uno shim con una main()funzione che verrà chiamata prima del vero main del programma. Questo shim può copiare gli argomenti reali in un nuovo spazio, quindi sovrascrivere gli argomenti originali in modo che psvedano solo gli null.

Il seguente codice C fa questo.

/* /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);
}

Non è possibile intervenire main(), ma è possibile intervenire sulla funzione di libreria C standard __libc_start_main, che continua a chiamare main. Compilare questo file shim_main.ccome indicato nel commento all'inizio ed eseguirlo come mostrato. Ho lasciato un printfcodice nel codice in modo da verificare che venga effettivamente chiamato. Ad esempio, esegui

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

quindi fai un pse vedrai un comando vuoto e verrà mostrato args.

C'è ancora una piccola quantità di tempo in cui il comando args potrebbe essere visibile. Per evitare ciò, è possibile, ad esempio, modificare lo spessore per leggere il proprio segreto da un file e aggiungerlo agli argomenti passati al programma.


12
Ma ci sarà ancora una breve finestra durante la quale /proc/pid/cmdlineverrà mostrato il segreto (come quando si curltenta di nascondere la password che viene data sulla riga di comando). Mentre stai usando LD_PRELOAD, potresti avvolgere main in modo che il segreto venga copiato dall'ambiente nell'argv che main riceve. Come chiamare LD_PRELOAD=x SECRET=y cmddove chiami main()con argv[]essere[argv[0], getenv("SECRET")]
Stéphane Chazelas,

Non è possibile utilizzare l'ambiente per nascondere un segreto in quanto è visibile tramite /proc/pid/environ. Questo può essere sovrascrivibile allo stesso modo degli arg, ma lascia la stessa finestra.
Meuh

11
/proc/pid/cmdlineè pubblico, /proc/pid/environnon lo è. C'erano alcuni sistemi in cui ps(un eseguibile setuid lì) esponeva l'ambiente di qualsiasi processo, ma non credo che ti imbatterai in nessun giorno d'oggi. L'ambiente è generalmente considerato abbastanza sicuro . Non è sicuro fare leva sui processi con lo stesso euid, ma questi possono spesso leggere la memoria dei processi dallo stesso euid, quindi non c'è molto che puoi fare al riguardo.
Stéphane Chazelas,

4
@ StéphaneChazelas: se si utilizza l'ambiente per passare segreti, idealmente il wrapper che lo inoltra al mainmetodo del programma di wrapping rimuove anche la variabile di ambiente per evitare perdite accidentali ai processi figlio. In alternativa, il wrapper potrebbe leggere tutti gli argomenti della riga di comando da un file.
David Foerster,

@DavidFoerster, buon punto. Ho aggiornato la mia risposta per tenerne conto.
Stéphane Chazelas,

16
  1. Leggi la documentazione dell'interfaccia della riga di comando dell'applicazione in questione. Potrebbe esserci un'opzione per fornire il segreto da un file anziché direttamente come argomento.

  2. In caso contrario, presentare una segnalazione di bug sull'applicazione in quanto non esiste un modo sicuro per fornirgli un segreto.

  3. Puoi sempre attentamente (!) Adattare la soluzione nella risposta di meuh alle tue esigenze specifiche. Presta particolare attenzione al commento di Stéphane e ai suoi follow-up.


12

Se hai bisogno di passare argomenti al programma per farlo funzionare, sarai sfortunato, non importa cosa fai se non puoi usare hidepidsu procfs.

Dato che hai menzionato questo è uno script bash, dovresti già avere il codice sorgente disponibile, poiché bash non è un linguaggio compilato.

In mancanza di questo, si può essere in grado di riscrivere il cmdline del processo utilizzando gdbo simili e giocare con argc/ argvuna volta che è già stato avviato, ma:

  1. Questo non è sicuro, poiché esponi ancora gli argomenti del tuo programma inizialmente prima di modificarli
  2. Questo è piuttosto confuso, anche se riuscissi a farlo funzionare, non consiglierei di fare affidamento su di esso

Consiglierei semplicemente di ottenere il codice sorgente o di parlare con il fornitore per modificare il codice. Fornire segreti sulla riga di comando in un sistema operativo POSIX è incompatibile con il funzionamento sicuro.


11

Quando un processo esegue un comando (tramite la execve()chiamata di sistema), la sua memoria viene cancellata. Per passare alcune informazioni attraverso l'esecuzione, le execve()chiamate di sistema prendono due argomenti per questo: the argv[]e envp[]array.

Quelle sono due matrici di stringhe:

  • argv[] contiene gli argomenti
  • envp[]contiene le definizioni delle variabili di ambiente come stringhe nel var=valueformato (per convenzione).

Quando lo fai:

export SECRET=value; cmd "$SECRET"

(qui sono state aggiunte le virgolette mancanti attorno all'espansione del parametro).

Stai eseguendo cmdcon il segreto ( value) passato sia in argv[]che envp[]. argv[]sarà ["cmd", "value"]e envp[]qualcosa del genere [..., "PATH=/bin:...", "HOME=...", ..., "SECRET=value", "TERM=xterm", ...]. Come cmdnon sta facendo alcuno getenv("SECRET")o equivalente per recuperare il valore del segreto da quella SECRETvariabile d'ambiente, metterlo nell'ambiente non è utile.

argv[]è conoscenza pubblica. Mostra nell'output di ps. envp[]al giorno d'oggi non lo è. Su Linux, mostra in /proc/pid/environ. Mostra nell'output di ps ewwwsu BSD (e con procps-ng'sps su Linux), ma solo ai processi in esecuzione con lo stesso uid efficace (e con più restrizioni per gli eseguibili setuid / setgid). Può essere visualizzato in alcuni registri di controllo, ma tali registri di controllo dovrebbero essere accessibili solo agli amministratori.

In breve, l'ambiente che viene passato a un eseguibile deve essere privato o almeno privato quanto la memoria interna di un processo (a cui in alcune circostanze un altro processo con i giusti privilegi può anche accedere con un debugger e può anche essere scaricato su disco).

Dato che argv[]è di dominio pubblico, un comando che prevede che i dati intesi come segreti siano segreti sulla sua riga di comando viene interrotto in base alla progettazione.

Di solito, i comandi che devono avere un segreto, forniscono un'altra interfaccia per farlo, come tramite una variabile d'ambiente. Per esempio:

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

O tramite un descrittore di file dedicato come stdin:

echo secret | openssl rsa -passin stdin ...

( echoessendo incorporato, non viene mostrato nell'output di ps)

O un file, come il .netrcfor ftpe alcuni altri comandi o

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

Alcune applicazioni come curl(e questo è anche l'approccio adottato da @meuh qui ) cercano di nascondere la password che hanno ricevuto argv[]da occhi indiscreti (su alcuni sistemi sovrascrivendo la porzione di memoria in cui argv[]erano archiviate le stringhe). Ma questo non aiuta davvero e dà una falsa promessa di sicurezza. Ciò lascia una finestra tra la execve()e la sovrascrittura dove psmostrerà ancora il segreto.

Ad esempio, se un utente malintenzionato sa che stai eseguendo uno script eseguendo un curl -u user:somesecret https://...(ad esempio in un processo cron), tutto ciò che deve fare è eliminare dalla cache le (molte) librerie che curlutilizza (ad esempio eseguendo a sh -c 'a=a;while :; do a=$a$a;done') quindi come rallentare il suo avvio, e anche fare un metodo molto inefficiente until grep 'curl.*[-]u' /proc/*/cmdline; do :; doneè sufficiente per catturare quella password nei miei test.

Se gli argomenti sono l'unico modo per passare il segreto ai comandi, potrebbero esserci ancora alcune cose che potresti provare.

Su alcuni sistemi, comprese le versioni precedenti di Linux, solo i primi byte (4096 su Linux 4.1 e precedenti) delle stringhe in argv[] possibile interrogare .

Lì, potresti fare:

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

E il segreto sarebbe nascosto perché è passato i primi 4096 byte. Ora le persone che hanno usato quel metodo ora devono rimpiangerlo dal momento che Linux dal 4.2 non tronca più l'elenco degli arg in /proc/pid/cmdline. Nota anche che non è perché psnon mostrerà più di così tanti byte di una riga di comando (come su FreeBSD dove sembra essere limitato al 2048) che non si può usare con la stessa API psutilizzata per ottenere di più. Tale approccio è valido tuttavia su sistemi in cui psè l'unico modo per un utente normale di recuperare tali informazioni (come quando l'API è privilegiata ed psè setgid o setuid per usarla), ma potenzialmente non è ancora a prova di futuro.

Un altro approccio sarebbe quello di non passare il segreto argv[]ma di iniettare codice nel programma (usando gdbo un $LD_PRELOADhack) prima che main()venga avviato che inserisca il segreto nel argv[]ricevuto da execve().

Con LD_PRELOAD, per eseguibili non setuid / setgid collegati dinamicamente su un sistema GNU:

/* 
 * 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);
}

Poi:

$ 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 *****

In nessun momento avrebbe psmostrato il ps -opid,argslì ( -opid,argsessendo il segreto in questo esempio). Si noti che stiamo sostituendo elementi argv[]dell'array di puntatori , non sovrascrivendo le stringhe indicate da quei puntatori, motivo per cui le nostre modifiche non vengono visualizzate nell'output di ps.

Con gdb, ancora per eseguibili non setuid / setgid collegati dinamicamente e su sistemi GNU:

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"

Tuttavia gdb, un approccio non GNU specifico che non si basa sul fatto che gli eseguibili siano collegati dinamicamente o che abbiano simboli di debug e che dovrebbe funzionare almeno per qualsiasi eseguibile ELF su Linux potrebbe essere:

#! /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

Test con un eseguibile collegato staticamente:

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

Quando l'eseguibile può essere statico, non abbiamo un modo affidabile di allocare memoria per archiviare il segreto, quindi dobbiamo ottenere il segreto da qualche altra parte che è già nella memoria di processo. Ecco perché l'ambiente è la scelta ovvia qui. Inoltre, nascondiamo SECRETquell'ambiente var al processo (modificandolo inSECRE= ) per evitare perdite se il processo decide di scaricare il suo ambiente per qualche motivo o eseguire applicazioni non attendibili.

Che funziona anche su Solaris 11 (fornito gdb e GNU binutils sono installate (potrebbe essere necessario cambiare titolo objdumpalgobjdump ).

Su FreeBSD (almeno x86_64, non sono sicuro di quali siano quei primi 24 byte (che diventano 16 quando gdb (8.0.1) è interattivo e suggerisce che potrebbero esserci degli errori in gdb) nello stack), sostituisci le definizioni argce argvcon:

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

(potrebbe anche essere necessario installare il gdbpacchetto / porta poiché la versione che altrimenti viene fornita con il sistema è antica).


Ri (qui aggiunte le virgolette mancanti attorno all'espansione del parametro): Cosa c'è di sbagliato nel non usare le virgolette? C'è davvero una differenza?
Yukashima Huksay,


3

Quello che potresti fare è

 export SECRET=somesecretstuff

quindi, supponendo che tu stia scrivendo il tuo ./programin C (o qualcun altro lo fa, e puoi cambiarlo o migliorarlo per te), usa getenv (3) in quel programma, forse come

char* secret= getenv("SECRET");

e dopo export aver appena eseguito ./programnella stessa shell. Oppure il nome della variabile di ambiente potrebbe essere passato ad esso (eseguendo ./program --secret-var=SECRETecc ...)

psnon parlerà del tuo segreto, ma proc (5) può comunque fornire molte informazioni (almeno ad altri processi dello stesso utente).

Vedi anche questo per aiutare a progettare un modo migliore di passare argomenti del programma.

Vedi questa risposta per una migliore spiegazione sul globbing e sul ruolo di un guscio.

Forse hai programaltri modi per ottenere dati (o utilizzare la comunicazione tra processi in modo più saggio) rispetto ai semplici argomenti del programma (certamente dovrebbe, se è destinato a elaborare informazioni riservate). Leggi la sua documentazione. O forse stai abusando di quel programma (che non ha lo scopo di elaborare dati segreti).

Nascondere i dati segreti è davvero difficile. Non passarlo attraverso gli argomenti del programma non è sufficiente.


5
E 'abbastanza chiaro dalla questione che non ha nemmeno il codice sorgente per ./program, quindi la prima metà di questo risposta non sembra essere rilevante.
pipe l'
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.