Ukryj argumenty do programowania bez kodu źródłowego

15

Muszę ukryć niektóre wrażliwe argumenty w programie, który uruchamiam, ale nie mam dostępu do kodu źródłowego. Używam tego również na serwerze współdzielonym, więc nie mogę używać czegoś takiego, hidepidponieważ nie mam uprawnień sudo.

Oto kilka rzeczy, które próbowałem:

  • export SECRET=[my arguments], a następnie wezwanie do ./program $SECRET, ale to nie pomaga.

  • ./program `cat secret.txt`gdzie secret.txtzawiera moje argumenty, ale Wszechmocny psjest w stanie odkryć moje sekrety.

Czy jest jakiś inny sposób na ukrycie moich argumentów, który nie wymaga interwencji administratora?

Stwardnienie rozsiane
źródło
Co to za konkretny program? Jeśli jest to zwykłe polecenie, musisz powiedzieć (i może istnieć inne podejście), które to polecenie
Basile Starynkevitch
14
Więc rozumiesz, co się dzieje, rzeczy, które próbowałeś, nie mają szans na działanie, ponieważ powłoka jest odpowiedzialna za rozszerzanie zmiennych środowiskowych i wykonywanie zastępowania poleceń przed wywołaniem programu. psnie robi nic magicznego, aby „wykopać swoje sekrety”. W każdym razie rozsądnie napisane programy powinny zamiast tego oferować opcję wiersza polecenia do odczytywania sekretu z określonego pliku lub ze standardowego wejścia zamiast bezpośredniego przyjmowania go jako argumentu.
jamesdlin,
Prowadzę program do symulacji pogody napisany przez prywatną firmę. Nie dzielą się swoim kodem źródłowym, ani ich dokumentacja nie udostępnia żadnego sposobu na udostępnienie sekretu z pliku. Możliwe, że nie ma tu opcji
MS

Odpowiedzi:

25

Jak wyjaśniono tutaj , Linux umieszcza argumenty programu w przestrzeni danych programu i utrzymuje wskaźnik na początku tego obszaru. To jest używane przez psi tak dalej, aby znaleźć i pokazać argumenty programu.

Ponieważ dane znajdują się w przestrzeni programu, można nimi manipulować. Wykonanie tego bez zmiany samego programu wymaga załadowania podkładki z main()funkcją, która zostanie wywołana przed prawdziwą funkcją główną programu. Ta podkładka może skopiować prawdziwe argumenty na nowe miejsce, a następnie zastąpić oryginalne argumenty, aby pspo prostu zobaczyć null.

Robi to następujący kod C.

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

Nie można interweniować main(), ale można interweniować w standardową funkcję biblioteki C __libc_start_main, która następnie wywołuje main. Skompiluj ten plik shim_main.czgodnie z uwagą na początku i uruchom go, jak pokazano. Zostawiłam printfw kodzie, więc sprawdzasz, czy faktycznie jest wywoływany. Na przykład uruchom

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

następnie wykonaj a, psa zobaczysz puste polecenie i argumenty.

Nadal jest mało czasu, aby argumenty poleceń mogły być widoczne. Aby tego uniknąć, możesz na przykład zmienić podkładkę, aby odczytać swój sekret z pliku i dodać go do argumentów przekazanych do programu.

meuh
źródło
12
Ale nadal będzie krótkie okno, w którym /proc/pid/cmdlinepokaże sekret (tak samo, jak gdy curlpróbuje ukryć hasło podane w wierszu poleceń). Podczas korzystania z LD_PRELOAD możesz owinąć main, aby sekret został skopiowany ze środowiska do argv, który otrzymuje main. Jak wezwanie LD_PRELOAD=x SECRET=y cmdgdzie dzwonisz main()z argv[]bycia[argv[0], getenv("SECRET")]
Stéphane Chazelas
Nie możesz używać środowiska do ukrywania sekretu, ponieważ jest on widoczny przez /proc/pid/environ. Może to być nadpisywalne w taki sam sposób jak argumenty, ale pozostawia to samo okno.
Meuh
11
/proc/pid/cmdlinejest publiczny, /proc/pid/environnie jest. Było kilka systemów, w których ps(plik wykonywalny setuid) eksponował środowisko dowolnego procesu, ale nie sądzę, żebyś mógł je teraz spotkać. Środowisko jest ogólnie uważane za wystarczająco bezpieczne . Nie można bezpiecznie wścibić się w procesy z tym samym euidem, ale i tak często potrafią odczytać pamięć procesów przez tego samego euida, więc niewiele można na to poradzić.
Stéphane Chazelas,
4
@ StéphaneChazelas: Jeśli ktoś używa środowiska do przekazywania tajemnic, najlepiej opakowanie, które przekazuje je do mainmetody opakowanego programu, usuwa również zmienną środowiskową, aby uniknąć przypadkowego wycieku do procesów potomnych. Alternatywnie opakowanie może odczytać wszystkie argumenty wiersza polecenia z pliku.
David Foerster,
@DavidFoerster, dobry punkt. Zaktualizowałem swoją odpowiedź, aby to uwzględnić.
Stéphane Chazelas
16
  1. Przeczytaj dokumentację interfejsu wiersza poleceń danej aplikacji. Może istnieć opcja podania tajnego klucza z pliku zamiast bezpośrednio jako argumentu.

  2. Jeśli to się nie powiedzie, zgłoś zgłoszenie błędu do aplikacji, uzasadniając to tym, że nie ma bezpiecznego sposobu na podanie jej tajemnicy.

  3. Zawsze możesz ostrożnie (!) Dostosować rozwiązanie w odpowiedzi Meuh do swoich konkretnych potrzeb. Zwróć szczególną uwagę na komentarz Stéphane'a i dalsze działania.

David Foerster
źródło
12

Jeśli potrzebujesz przekazać argumenty do programu, aby działał, nie będziesz miał szczęścia, bez względu na to, co zrobisz, jeśli nie będziesz mógł używać go hidepidw procfs.

Ponieważ wspomniałeś, że jest to skrypt bash, powinieneś już mieć dostępny kod źródłowy, ponieważ bash nie jest językiem kompilowanym.

Jeżeli to niemożliwe, to może być w stanie przepisać CMDLINE procesu korzystania gdblub podobne i zabawy z argc/ argvgdy to już się rozpoczęła, ale:

  1. Nie jest to bezpieczne, ponieważ nadal ujawniasz argumenty programu przed ich zmianą
  2. Jest to dość zuchwałe, nawet jeśli potrafisz go uruchomić, nie polecam polegać na nim

Naprawdę po prostu zaleciłbym uzyskanie kodu źródłowego lub rozmowę z dostawcą w celu zmodyfikowania kodu. Podawanie sekretów w wierszu poleceń w systemie operacyjnym POSIX jest niezgodne z bezpieczną operacją.

Chris Down
źródło
11

Gdy proces wykonuje polecenie (poprzez execve()wywołanie systemowe), jego pamięć jest czyszczona. Aby przekazać pewne informacje w trakcie wykonywania, execve()wywołania systemowe wymagają dwóch argumentów: argv[]i envp[]tablic.

Są to dwie tablice ciągów:

  • argv[] zawiera argumenty
  • envp[]zawiera definicje zmiennych środowiskowych jako ciągi znaków w var=valueformacie (zgodnie z konwencją).

Kiedy to zrobisz:

export SECRET=value; cmd "$SECRET"

(tutaj dodano brakujące cudzysłowy wokół rozszerzenia parametru).

Wykonujesz cmdz sekretem ( value) przekazanym zarówno do, jak argv[]i do envp[]. argv[]będzie ["cmd", "value"]i envp[]coś w tym rodzaju [..., "PATH=/bin:...", "HOME=...", ..., "SECRET=value", "TERM=xterm", ...]. Ponieważ cmdnie robi się nic, getenv("SECRET")ani nie można pobrać wartości tajnej z tej SECRETzmiennej środowiskowej, umieszczenie jej w środowisku nie jest przydatne.

argv[]to wiedza publiczna. Pokazuje na wyjściu ps. envp[]obecnie nie jest. W systemie Linux pokazuje się w /proc/pid/environ. Pokazuje się w ps ewwwwynikach BSD (i procps-ng w psLinuksie), ale tylko dla procesów działających z tym samym efektywnym UID (i z większymi ograniczeniami dla plików wykonywalnych setuid / setgid). Może być wyświetlany w niektórych dziennikach kontroli, ale te dzienniki kontroli powinny być dostępne tylko dla administratorów.

Krótko mówiąc, środowisko przekazywane do pliku wykonywalnego ma być prywatne lub przynajmniej tak prywatne jak wewnętrzna pamięć procesu (do którego w niektórych okolicznościach inny proces z odpowiednimi uprawnieniami może również uzyskać dostęp za pomocą debuggera i może również zrzucony na dysk).

Ponieważ argv[]jest to wiedza publiczna, polecenie, które oczekuje, że dane, które mają być tajne w wierszu poleceń, jest zepsute przez projekt.

Zazwyczaj polecenia, które wymagają tajnego klucza, zapewniają inny interfejs, np. Poprzez zmienną środowiskową. Na przykład:

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

Lub za pomocą dedykowanego deskryptora pliku, takiego jak stdin:

echo secret | openssl rsa -passin stdin ...

( echojest wbudowany, nie wyświetla się w wynikach ps)

Lub plik, taki jak .netrcfor ftpi kilka innych poleceń lub

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

Niektóre aplikacje, takie jak curl(i takie podejście przyjęła tutaj @meuh ), próbują ukryć hasło, które otrzymali argv[]przed wścibskimi oczami (w niektórych systemach, nadpisując część pamięci, w której argv[]przechowywano ciągi znaków). Ale to naprawdę nie pomaga i daje fałszywą obietnicę bezpieczeństwa. To pozostawia okno pomiędzy execve()i nadpisywaniem, gdzie psnadal będzie pokazywany sekret.

Na przykład, jeśli osoba atakująca wie, że uruchamiasz skrypt wykonujący curl -u user:somesecret https://...(na przykład zadanie crona), wszystko, co musi zrobić, to eksmitować z pamięci podręcznej (wiele) bibliotek, które curlużywają (na przykład uruchamiając a sh -c 'a=a;while :; do a=$a$a;done'), więc aby spowolnić jego uruchomienie, a nawet zrobienie bardzo nieefektywnego until grep 'curl.*[-]u' /proc/*/cmdline; do :; donewystarczy, aby złapać to hasło w moich testach.

Jeśli argumenty to jedyny sposób na przekazanie sekretu komendom, nadal możesz spróbować kilku rzeczy.

W niektórych systemach, w tym starszych wersjach Linuksa, argv[]można przeszukiwać tylko kilka pierwszych bajtów (4096 w Linuksie 4.1 i wcześniejszych) ciągów .

Tam możesz zrobić:

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

I sekret zostałby ukryty, ponieważ przekroczyłby pierwsze 4096 bajtów. Teraz ludzie, którzy używali tej metody, muszą teraz tego żałować, ponieważ Linux od wersji 4.2 nie obcina już listy argumentów /proc/pid/cmdline. Zauważ też, że nie dlatego, psże nie będzie wyświetlał więcej niż tak wielu bajtów linii poleceń (jak na FreeBSD, gdzie wydaje się być ograniczony do 2048), których nie można użyć do tego samego interfejsu API, psaby uzyskać więcej. Podejście to jest jednak ważne w systemach, w których pszwykły użytkownik jest jedynym sposobem na uzyskanie tych informacji (na przykład, gdy interfejs API jest uprzywilejowany i psjest ustawiony jako setgid lub setuid, aby z niego skorzystać), ale wciąż nie jest tam potencjalnie zabezpieczony na przyszłość.

Innym podejściem byłoby nie przekazywanie sekretu, argv[]ale wstrzykiwanie kodu do programu (za pomocą gdblub $LD_PRELOADwłamania) przed jego main()uruchomieniem, które wstawia sekret do argv[]otrzymanego z execve().

Z LD_PRELOAD, dla dynamicznie połączonych plików wykonywalnych innych niż setuid / setgid w systemie 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);
}

Następnie:

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

W żadnym momencie nie pspokazałbym tego ps -opid,args(co -opid,argsjest tajemnicą w tym przykładzie). Zauważ, że zastępujemy elementy argv[]tablicy wskaźników , nie zastępując ciągów wskazanych przez te wskaźniki, dlatego nasze modyfikacje nie pokazują się na wyjściu ps.

Z gdb, wciąż dla dynamicznie powiązanych plików wykonywalnych innych niż setuid / setgid oraz w systemach 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"

Mimo gdbto podejście nie specyficzne dla GNU, które nie polega na dynamicznym łączeniu plików wykonywalnych lub symboli debugowania, i powinno działać przynajmniej dla każdego pliku wykonywalnego ELF w systemie Linux:

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

Testowanie ze statycznie połączonym plikiem wykonywalnym:

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

Gdy plik wykonywalny może być statyczny, nie mamy niezawodnego sposobu na przydzielenie pamięci do przechowywania sekretu, więc musimy uzyskać sekret z innego miejsca, które jest już w pamięci procesu. Właśnie dlatego środowisko jest tutaj oczywistym wyborem. Ukrywamy również SECRETzmienną env var procesu (zmieniając ją na SECRE=), aby uniknąć wycieku, jeśli proces z jakiegoś powodu zdecyduje się zrzucić swoje środowisko lub uruchomić niezaufane aplikacje.

Który działa również w systemie Solaris 11 (pod warunkiem, gdb i binutils GNU są zainstalowane (być może trzeba będzie zmienić nazwę objdumpna gobjdump).

W FreeBSD (przynajmniej x86_64 nie jestem pewien, jakie są te pierwsze 24 bajty (które stają się 16, gdy gdb (8.0.1) jest interaktywny, co sugeruje, że może tam być błąd w gdb) na stosie), zamień argci argvdefinicje z:

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

(może być również konieczne zainstalowanie gdbpakietu / portu, ponieważ wersja dostarczana z systemem jest starodawna).

Stéphane Chazelas
źródło
Re (tutaj dodano brakujące cudzysłowy wokół rozszerzenia parametru): Co jest złego w nieużywaniu cudzysłowów? Czy naprawdę jest jakaś różnica?
yukashima huksay
@yukashimahuksay, zobacz na przykład konsekwencje bezpieczeństwa dla zapomnienia o cytowaniu zmiennej w powłokach bash / POSIX i pytania z nią powiązane.
Stéphane Chazelas
3

Co możesz zrobić, to

 export SECRET=somesecretstuff

następnie, zakładając, że piszesz ./programw C (lub robi to ktoś inny i możesz go zmienić lub ulepszyć), użyj getenv (3) w tym programie, być może jako

char* secret= getenv("SECRET");

a po tym export biegniesz ./programw tej samej powłoce. Lub nazwa zmiennej środowiskowej może być do niej przekazana (uruchamiając ./program --secret-var=SECRETetc ...)

psnie zdradzi twojego sekretu, ale proc (5) wciąż może przekazać wiele informacji (przynajmniej innym procesom tego samego użytkownika).

Zobacz także to, aby pomóc zaprojektować lepszy sposób przekazywania argumentów programu.

Zobacz tę odpowiedź, aby uzyskać lepsze wyjaśnienie na temat globowania i roli powłoki.

Być może twój program inne sposoby pozyskiwania danych (lub mądrzejszego korzystania z komunikacji międzyprocesowej ) niż zwykłe argumenty programu (z pewnością powinno, jeśli jest przeznaczone do przetwarzania poufnych informacji). Przeczytaj dokumentację. A może nadużywasz tego programu (który nie jest przeznaczony do przetwarzania tajnych danych).

Ukrywanie tajnych danych jest naprawdę trudne. Nie przekazanie go przez argumenty programu nie wystarczy.

Basile Starynkevitch
źródło
5
Jest to dość oczywiste, od kwestii, że nawet nie ma kodu źródłowego dla ./program, więc pierwsza połowa tej odpowiedzi nie wydają się być istotne.
rura