Приховати аргументи для програми без вихідного коду


15

Мені потрібно приховати делікатні аргументи для запущеної програми, але я не маю доступу до вихідного коду. Я також запускаю це на спільному сервері, тому я не можу використовувати щось на кшталт hidepidтому, що у мене немає привілеїв sudo.

Ось деякі речі, які я спробував:

  • export SECRET=[my arguments], після чого дзвінок на адресу ./program $SECRET, але це, здається, не допомагає.

  • ./program `cat secret.txt`де secret.txtмістяться мої аргументи, але всемогучий psздатний винюхати мої секрети.

Чи є інший спосіб приховати мої аргументи, які не передбачають втручання адміністратора?


Що це за конкретна програма? Якщо це звичайна команда, вам потрібно сказати (а може бути якийсь інший підхід), яка вона
Базиль Старинкевич

14
Таким чином, ви розумієте, що відбувається, те, що ви намагалися, не має шансів працювати, оскільки оболонка відповідає за розширення змінних середовища та за підміну команд перед тим, як викликати програму. psне робить нічого магічного, щоб «обнюхати свої секрети». У будь-якому разі, розумно написані програми замість цього повинні пропонувати командному рядку можливість зчитувати секрет із вказаного файлу або з stdin, а не приймати його безпосередньо як аргумент.
jamesdlin

Я запускаю програму симуляції погоди, написану приватною компанією. Вони не діляться своїм вихідним кодом, а також їх документація не дає жодного способу поділитися секретом з файлу. Тут можуть бути недоступні варіанти
MS

Відповіді:


25

Як пояснено тут , Linux розміщує аргументи програми в просторі даних програми і зберігає вказівку на початок цієї області. Це те, чим користуєтьсяps і так далі для пошуку та показу програмних аргументів.

Оскільки дані знаходяться в просторі програми, вони можуть ними маніпулювати. Здійснення цього без зміни самої програми передбачає завантаження прокладки з main()функцією, яка буде викликана перед справжнім основним програмою. Ця shim може скопіювати реальні аргументи в новий простір, а потім перезаписати оригінальні аргументи так, що psпросто побачать нулі.

Наступний код 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);
}

Не можна втручатися main(), але ви можете втрутитися у стандартну функцію C бібліотеки __libc_start_main, яка продовжує виклик main. Складіть цей файл, shim_main.cяк зазначено в коментарі на початку, та запустіть його, як показано. Я залишив printfкод у коді, щоб ви перевірили, що він насправді викликається. Наприклад, бігайте

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

тоді зробіть a, psі ви побачите порожню команду та аргументи.

Є ще невеликий проміжок часу, щоб аргументи команд були видні. Щоб цього уникнути, ви могли, наприклад, змінити лайм, щоб прочитати ваш секрет з файлу, і додати його до аргументів, переданих програмі.


12
Але все одно знайдеться коротке вікно, під час якого /proc/pid/cmdlineбуде показаний секрет (те саме, коли curlнамагається приховати пароль, який він задається в командному рядку). Поки ви використовуєте LD_PRELOAD, ви можете обернути основне, щоб секрет був скопійований із середовища в аргумент, який отримує головний. Як виклик , LD_PRELOAD=x SECRET=y cmdде ви телефонуєте main()з argv[]того[argv[0], getenv("SECRET")]
Stéphane Chazelas

Ви не можете використовувати оточення, щоб приховати секрет, як це видно через /proc/pid/environ. Це може бути замінено таким же чином, як і арг, але воно залишає те саме вікно.
meuh

11
/proc/pid/cmdlineє публічним, /proc/pid/environні. Були деякі системи, де ps(виконаний із встановленим файлом там) викривали середовище будь-якого процесу, але я не думаю, що ви зараз натрапите на будь-який. Навколишнє середовище, як правило, вважається досить безпечним . Не безпечно для довірених процесів з тим самим еуїдом, але вони часто можуть зчитувати пам'ять процесів тим самим еуїдом, так що ви не можете з цим зробити багато.
Стефан Шазелас

4
@ StéphaneChazelas: Якщо хтось використовує середовище для передачі секретів, в ідеалі обгортка, яка передає його mainметоду обгорнутої програми, також видаляє змінну середовища, щоб уникнути випадкового витоку дочірніх процесів. Крім того, обгортка може читати всі аргументи командного рядка з файлу.
Девід Фоерстер

@DavidFoerster, хороший момент. Я оновив свою відповідь, щоб врахувати це.
Стефан Шазелас

16
  1. Прочитайте документацію інтерфейсу командного рядка відповідної програми. Цілком може бути можливість передавати секрет із файлу, а не як аргумент безпосередньо.

  2. Якщо це не вдалося, подайте звіт про помилку проти програми на тій підставі, що немає безпечного способу передавати секрет.

  3. Ви завжди можете ретельно (!) Адаптувати рішення у відповіді meuh до ваших конкретних потреб. Зверніть особливу увагу на коментар Стефана та його подальші дії.


12

Якщо вам потрібно передавати аргументи програмі, щоб змусити її працювати, вам не пощастить, незалежно від того, що ви робите, якщо не можете використовувати hidepid profs.

Оскільки ви згадали, що це сценарій bash, у вас уже повинен бути доступний вихідний код, оскільки bash не є компільованою мовою.

Якщо цього не зробити, можливо , ви зможете переписати cmdline процесу за допомогою gdbабо подібного та пограти з argc/ argvяк тільки він вже розпочався, але:

  1. Це не є безпечним, оскільки ви все-таки виставляєте свої програмні аргументи спочатку перед їх зміною
  2. Це досить хакі, навіть якщо ви зможете змусити його працювати, я б не рекомендував покладатися на нього

Я дійсно рекомендую отримати вихідний код або поговорити з постачальником, щоб змінити код. Подання секретів у командному рядку в операційній системі POSIX несумісне з безпечною роботою.


11

Коли процес виконує команду (через execve()системний виклик), її пам'ять стирається. Для передачі деякої інформації під час виконання execve()системний виклик бере два аргументи для цього: argv[]іenvp[] масиви.

Це два масиви рядків:

  • argv[] містить аргументи
  • envp[]містить визначення змінної середовища у вигляді рядків у var=valueформаті (за умовами ).

Коли ви робите:

export SECRET=value; cmd "$SECRET"

(сюди додані відсутні лапки навколо розширення параметра).

Ви виконуєте cmdсекрет ( value), переданий і в, argv[]і в envp[]. argv[]буде ["cmd", "value"]і envp[]щось подібне [..., "PATH=/bin:...", "HOME=...", ..., "SECRET=value", "TERM=xterm", ...]. Оскільки cmdце не робить жодного getenv("SECRET")або рівнозначного, щоб отримати значення секрету з цьогоSECRET змінної середовища, розміщення його в середовищі не корисне.

argv[]це суспільне знання. Це показано на виході ps. envp[]нині це не так. У Linux він показує в /proc/pid/environ. Він відображається у виводі ps ewwwна BSD (і з procps-ng's psв Linux), але тільки для процесів, що працюють з тим же ефективним uid (і з більшими обмеженнями для setuid / setgid виконуваних файлів). Це може відображатися в деяких журналах аудиту, але до цих журналів аудиту повинні бути доступні лише адміністратори.

Коротше кажучи, середовище, яке передається виконуваному файлу, повинно бути приватним або принаймні таким же приватним, як внутрішня пам'ять процесу (який, за певних обставин, інший процес з правами привілеїв може також отримати доступ до відладчика, наприклад, і може також скидаються на диск).

Оскільки argv[]це загальнодоступне знання, команда, яка очікує, що дані, які повинні бути секретними у своєму командному рядку, розбита конструкцією.

Зазвичай команди, яким потрібно надати секрет, надають вам інший інтерфейс для цього, наприклад, через змінну середовища. Наприклад:

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

Або через спеціальний дескриптор файлу, наприклад stdin:

echo secret | openssl rsa -passin stdin ...

( echoбудується вбудовано, воно не відображається на виходіps )

Або файл, як, наприклад, .netrcдля ftpдекількох команд або

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

Деякі програми на зразок curl(і це також підхід @meuh тут ) намагаються приховати пароль, який вони отримали argv[]від сторонніх очей (в деяких системах, перезаписавши частину пам'яті, де argv[]зберігалися рядки). Але це насправді не допомагає і дає помилкову обіцянку безпеки. Це залишає вікно між тим execve()і перезаписом, де psвсе ще з’явиться секрет.

Наприклад, якщо зловмисник знає, що ви запускаєте скрипт, який виконує curl -u user:somesecret https://...(наприклад, у роботі cron), все, що він повинен зробити, - це вилучити з кеша (безліч) бібліотек, які curlвикористовують (наприклад, за допомогою a sh -c 'a=a;while :; do a=$a$a;done') як уповільнити його запуск, і навіть робити дуже неефективним until grep 'curl.*[-]u' /proc/*/cmdline; do :; done, достатньо, щоб зловити цей пароль у моїх тестах.

Якщо аргументи - це єдиний спосіб передавати секрет командам, можливо, ви можете спробувати деякі речі.

У деяких системах, включаючи старіші версії Linux, лише перші кілька байтів (4096 в Linux 4.1 і раніше) рядків у argv[] можна запитувати .

Там ви можете зробити:

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

І секрет був би прихований, оскільки він минув перші 4096 байт. Тепер люди, які використовували цей метод, повинні пошкодувати про це вже зараз, оскільки Linux з 4.2 більше не скорочує список аргументів /proc/pid/cmdline. Також зауважте, що це не тому ps, що не відображатиметься більше, ніж стільки байтів командного рядка (як, наприклад, у FreeBSD, де, здається, обмежено 2048), що не можна використовувати для тих самих API, psщоб отримати більше. Цей підхід є дійсним, але в системах, де psзвичайний користувач - єдиний спосіб отримати цю інформацію (наприклад, коли привілейований API іps встановлений жорсткий або налаштований для його використання), але він все ще не може бути майбутньому.

Іншим підходом було б не передавати секрет, argv[]а вводити код у програму (за допомогою gdbабо $LD_PRELOADхак) перед її main()запуском, який вставляє секрет у argv[]отримане відexecve() .

З LD_PRELOAD, для не встановлених / setgid динамічно пов'язаних виконуваних файлів у системі 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);
}

Потім:

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

Ні в якому разі там би psне показали ps -opid,args( -opid,argsщо є секретом у цьому прикладі). Зауважте, що ми замінюємо елементи argv[]масиву покажчиків , не переосмислюючи рядки, на які вказують ці вказівники, тому наші модифікації не відображаються у висновкуps .

З gdb, як і раніше для не встановлених / встановлених жорстких динамічно пов'язаних виконуваних файлів та для 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"

Тим не менш gdb, не-GNU-підхід, який не покладається на те, що виконувані файли будуть динамічно пов'язані або мають налагоджувальні символи, і повинні працювати принаймні для будь-якого ELF, що виконується в 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

Тестування зі статично пов'язаним виконуваним файлом:

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

Коли виконуваний файл може бути статичним, у нас немає надійного способу виділити пам'ять для зберігання секрету, тому нам доведеться дістати секрет з іншого місця, яке вже є в пам'яті процесу. Ось чому середовище - очевидний вибір тут. Ми також приховуємо цей SECRETenv var до процесу (змінивши його наSECRE= ), щоб уникнути його протікання, якщо процес вирішить знеструмлювати його середовище з якихось причин або виконати ненадійні програми.

Це також працює на Solaris 11 (за умови встановлення бінарних файлів gdb та GNU (можливо, доведеться перейменувати objdumpнаgobjdump ).

У FreeBSD (принаймні x86_64, я не впевнений, що ті перші 24 байти (які стають 16, коли gdb (8.0.1) є інтерактивним, що говорить про те, що там може бути помилка в gdb)), замініть argcі argvвизначення з:

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

(можливо, вам також знадобиться встановити gdbпакет / порт, оскільки версія, яка в іншому випадку поставляється із системою, є давньою).


Знову (сюди додано відсутні процитировані параметри розширення параметра): Що не так з використанням лапок? Чи справді є різниця?
юкасіма хуксай

@yukashimahuksay, див., наприклад, наслідки для безпеки забування процитувати змінну в оболонках bash / POSIX та пов'язані з цим питання.
Стефан Шазелас

3

Що ви можете зробити, так і є

 export SECRET=somesecretstuff

тоді, припускаючи, що ви пишете свій ./programC (або хтось інший робить, і може змінити або покращити його для вас), використовуйте getenv (3) у цій програмі, можливо як

char* secret= getenv("SECRET");

а після export ви просто біжите ./programв одній оболонці. Або ім'я змінної середовища може бути передано до неї (запустивши ./program --secret-var=SECRETтощо ...)

psне розповість про вашу таємницю, але proc (5) все одно може дати багато інформації (принаймні, для інших процесів того ж користувача).

Дивіться також це, щоб допомогти розробити кращий спосіб передачі аргументів програми.

Дивіться цю відповідь для кращого пояснення щодо глобалізації та ролі оболонки.

Можливо, у вас programє інші способи отримання даних (або використання міжпроцесорного зв’язку розумніше), ніж прості програмні аргументи (це, безумовно, повинно, якщо він призначений для обробки конфіденційної інформації). Прочитайте його документацію. Або, можливо, ви зловживаєте цією програмою (яка не призначена для обробки секретних даних).

Приховати секретні дані дійсно важко. Не здати її через програмні аргументи недостатньо.


5
Це досить ясно з питання , що він навіть не має вихідний код для ./program, тому першу половину цієї відповіді , здається, не буде актуальною.
труба
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.