Скрыть аргументы для программы без исходного кода


15

Мне нужно скрыть некоторые секретные аргументы для программы, которую я запускаю, но у меня нет доступа к исходному коду. Я также запускаю это на общем сервере, поэтому я не могу использовать что-то подобное, hidepidпотому что у меня нет привилегий sudo.

Вот несколько вещей, которые я пробовал:

  • export SECRET=[my arguments]с последующим вызовом ./program $SECRET, но это, похоже, не помогает.

  • ./program `cat secret.txt`где secret.txtсодержатся мои аргументы, но всемогущий psспособен раскрыть мои секреты.

Есть ли другой способ скрыть мои аргументы, которые не связаны с вмешательством администратора?


Что это за конкретная программа? Если это обычная команда, вам нужно сказать (и может быть какой-то другой подход), какая это
Василий Старынкевич

14
Таким образом, вы понимаете, что происходит, у того, что вы пробовали, нет шансов работать, потому что оболочка отвечает за расширение переменных среды и выполнение подстановки команд перед вызовом программы. psне делает ничего волшебного, чтобы «раскрыть свои секреты». В любом случае, разумно написанные программы вместо этого должны предлагать параметр командной строки для чтения секрета из указанного файла или из стандартного ввода, вместо того, чтобы принимать его непосредственно в качестве аргумента.
Джеймсдлин

Я управляю программой моделирования погоды, написанной частной компанией. Они не делятся своим исходным кодом, и их документация не предоставляет никакого способа поделиться секретом из файла. Может быть здесь нет вариантов
MS

Ответы:


25

Как объясняется здесь , Linux помещает аргументы программы в пространство данных программы и сохраняет указатель на начало этой области. Это то, что используется psи так далее, чтобы найти и показать аргументы программы.

Поскольку данные находятся в пространстве программы, они могут манипулировать ими. Выполнение этого без изменения самой программы включает в себя загрузку прокладки с main()функцией, которая будет вызываться перед реальной основной программой. Эта прокладка может копировать реальные аргументы в новое пространство, а затем перезаписывать исходные аргументы, чтобы psпросто видеть nuls.

Следующий код 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, вы можете обернуть main так, чтобы секрет копировался из окружения в argv, который получает main. Как звонить, LD_PRELOAD=x SECRET=y cmdгде вы звоните main()с argv[]существом[argv[0], getenv("SECRET")]
Стефан

Вы не можете использовать окружение, чтобы скрыть секрет, поскольку он виден через /proc/pid/environ. Это может быть перезаписано так же, как аргументы, но оно оставляет то же самое окно.
Мех

11
/proc/pid/cmdlineпублично, /proc/pid/environнет. Были некоторые системы, в которых ps(исполняемый файл setuid) представлял окружение любого процесса, но я не думаю, что вы встретите это в наши дни. Окружающая среда обычно считается достаточно безопасной . Не безопасно извлекать информацию из процессов с одним и тем же euid, но они в любом случае часто могут читать память процессов одним и тем же euid, так что вы ничего не можете с этим поделать.
Стефан

4
@ StéphaneChazelas: Если кто-то использует среду для передачи секретов, в идеале оболочка, которая передает его в mainметод упакованной программы, также удаляет переменную среды, чтобы избежать случайной утечки в дочерние процессы. Кроме того, оболочка может прочитать все аргументы командной строки из файла.
Дэвид Фёрстер

@DavidFoerster, хорошая мысль. Я обновил свой ответ, чтобы учесть это.
Стефан Шазелас

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

  2. Если это не удается, подайте отчет об ошибке в приложение на том основании, что не существует безопасного способа предоставить ему секрет.

  3. Вы всегда можете тщательно (!) Адаптировать решение в ответе meuh на ваши конкретные потребности. Обратите особое внимание на комментарий Стефана и его последующие действия.


12

Если вам нужно передать аргументы программе, чтобы она заработала, вам не повезет, что бы вы ни делали, если не можете использовать hidepidв procfs.

Поскольку вы упомянули, что это скрипт bash, у вас уже должен быть доступный исходный код, поскольку bash не является скомпилированным языком.

В противном случае вы можете переписать командную строку процесса, используя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 в psLinux), но только для процессов, работающих с одинаковым эффективным uid (и с большими ограничениями для исполняемых файлов setuid / setgid). Это может отображаться в некоторых журналах аудита, но эти журналы аудита должны быть доступны только администраторам.

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

Поскольку argv[]это общедоступное знание, команда, которая ожидает, что данные, предназначенные для секретности, в своей командной строке не работает.

Обычно команды, которым необходимо дать секрет, предоставляют вам другой интерфейс для этого, например, через переменную окружения. Например:

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

Или с помощью специального файлового дескриптора, такого как stdin:

echo secret | openssl rsa -passin stdin ...

( echoбудучи встроенным, он не отображается на выходеps )

Или файл, например, .netrcfor 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, argv[]могут быть запрошены только первые несколько байтов (4096 в Linux 4.1 и ранее) строк .

Там вы можете сделать:

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

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

Другой подход заключается в том, чтобы не передавать секрет, argv[]а вводить код в программу (используя gdbили $LD_PRELOADвзломать) до ее main()запуска, которая вставляет секрет в argv[]полученный отexecve() .

С LD_PRELOAD, для динамически связанных исполняемых файлов не-setuid / 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, все еще для динамически связанных исполняемых файлов не-setuid / setgid и в системах 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 Binutils установлены (вы , возможно , придется переименовать 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пакет / порт, поскольку версия, поставляемая с системой, устарела).


Re (здесь добавлены пропущенные кавычки вокруг расширения параметра): что плохого в том, чтобы не использовать кавычки? Есть ли разница?
Юкашима Хуксай

@yukashimahuksay, посмотрите, например, последствия для безопасности, связанные с забыванием заключать в кавычки переменную в оболочках bash / POSIX и связанные с ней вопросы.
Стефан Шазелас

3

Что вы можете сделать, это

 export SECRET=somesecretstuff

затем, если вы пишете свой язык ./programC (или кто-то другой делает это и может изменить или улучшить его для вас), используйте getenv (3) в этой программе, возможно, как

char* secret= getenv("SECRET");

и после export вы просто запускаете ./programв одной оболочке. Или имя переменной среды может быть передано ему (запустив./program --secret-var=SECRET etc ...)

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

Смотрите также это чтобы помочь разработать лучший способ передачи аргументов программы.

Смотрите этот ответ для лучшего объяснения глобализации и роли оболочки.

Возможно, у вас programесть и другие способы получить данные (или использовать межпроцессное взаимодействие более разумно), чем простые программные аргументы (это, безусловно, следует, если он предназначен для обработки конфиденциальной информации). Прочитайте его документацию. Или, возможно, вы злоупотребляете этой программой (которая не предназначена для обработки секретных данных).

Скрывать секретные данные действительно сложно. Недостаточно передать его через аргументы программы.


5
Это довольно ясно из вопроса , что он даже не имеет исходный код для ./program, поэтому первую половину этого ответа , кажется, не будет актуальной.
труба
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.