ソースコードなしでプログラムの引数を非表示にする


15

実行中のプログラムに対する機密性の高い引数を非表示にする必要がありますが、ソースコードにアクセスできません。また、共有サーバーでこれを実行しているので、hidepidsudo特権がないため、次のようなものは使用できません。

ここに私が試したいくつかのものがあります:

  • export SECRET=[my arguments]に続いてを呼び出しますが./program $SECRET、これは役に立たないようです。

  • ./program `cat secret.txt`どこsecret.txtに私の議論が含まれていますが、全能者psは私の秘密を嗅ぎ分けることができます。

管理者の介入を必要としない私の引数を隠す他の方法はありますか?


その特定のプログラムは何ですか?それが通常のコマンドである場合、どのコマンドであるかを伝える必要があります(他の方法もあります)
Basile Starynkevitch

14
シェルは環境変数を展開し、プログラムを呼び出す前にコマンド置換を実行するため、実行しようとしていることは機能しません。ps「秘密を嗅ぎ分ける」ために魔法のようなことをしていません。とにかく、合理的に作成されたプログラムは、コマンドラインオプションを提供して、引数として直接受け取るのではなく、指定されたファイルまたはstdinからシークレットを読み取る必要があります。
ジェームズドリン

私は民間企業が作成した気象シミュレーションプログラムを実行しています。彼らはソースコードを共有せず、ドキュメントはファイルから秘密を共有する方法を提供しません。ここでのオプションのうちかもしれない
MS

回答:


25

ここで説明したように、Linuxはプログラムの引数をプログラムのデータスペースに配置し、この領域の開始点へのポインターを保持します。これはps、プログラムの引数を見つけて表示するためなどに使用されます。

データはプログラムのスペースにあるため、操作できます。プログラム自体を変更せずにこれを行うにmain()は、プログラムの実際のメインの前に呼び出される関数をシムにロードする必要があります。このシムは、実際の引数を新しいスペースにコピーし、元の引数を上書きして、psnulが見えるようにします。

次の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を呼び出すmain()標準Cライブラリ関数__libc_start_mainに介入することができます。shim_main.c冒頭のコメントに記載されているようにこのファイルをコンパイルし、次のように実行します。printf実際に呼び出されているかどうかを確認するために、コードを残しました。たとえば、実行

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

するとps、空のコマンドと引数が表示されます。

コマンドargsが表示される時間はまだわずかです。これを回避するには、たとえば、ファイルからシークレットを読み取るようにシムを変更し、プログラムに渡される引数に追加することができます。


12
ただし/proc/pid/cmdline、秘密を表示するための短いウィンドウがまだあります(curlコマンドラインで指定されたパスワードを非表示にしようとする場合と同じです)。LD_PRELOADを使用している間、mainをラップして、環境からmainが受信するargvにシークレットをコピーできます。LD_PRELOAD=x SECRET=y cmdあなたが存在main()argv[]ていると呼ぶコールのように[argv[0], getenv("SECRET")]
ステファンシャゼル

環境を使用して、で表示される秘密を非表示にすることはできません/proc/pid/environ。これは、引数と同じ方法で上書き可能ですが、同じウィンドウを残します。
meuh

11
/proc/pid/cmdline公開されて/proc/pid/environいますが、公開されていません。ps(setuid実行可能ファイル)がプロセスの環境を公開するシステムがいくつかありましたが、最近は出会うとは思いません。一般に、環境は十分に安全であると考えられています。同じeuidのプロセスからこじ開けるのは安全ではありませんが、それらはとにかく同じeuidでプロセスのメモリを読み取ることができるので、できることはあまりありません。
ステファンシャゼラス

4
@StéphaneChazelas:環境を使用してシークレットを渡す場合、理想的にmainは、ラップされたプログラムのメソッドにそれを転送するラッパーも環境変数を削除して、子プロセスへの偶発的な漏洩を防ぎます。あるいは、ラッパーはファイルからすべてのコマンドライン引数を読み取ることができます。
デビッドフォースター

@DavidFoerster、良い点。それを考慮して回答を更新しました。
ステファンシャゼラス

16
  1. 問題のアプリケーションのコマンドラインインターフェイスのドキュメントをお読みください。引数として直接ではなく、ファイルから秘密を提供するオプションがあるかもしれません。

  2. それが失敗した場合、アプリケーションに秘密を提供する安全な方法がないという理由で、アプリケーションに対してバグレポートを提出してください。

  3. あなたは、常に慎重に(!)にソリューションを適応させることができmeuhの答え、あなたの特定のニーズに。Stéphaneのコメントとそのフォローアップに特に注意してください。


12

プログラムを機能させるためにプログラムに引数を渡す必要がある場合hidepid、procfsで使用できない場合は、何をしようとも運が悪くなります。

これはbashスクリプトであると述べたので、bashはコンパイルされた言語ではないため、ソースコードは既に利用可能になっているはずです。

あなたは、それに失敗使用して、プロセスのコマンドラインパラメータを書き換えることができgdbまたは類似とで遊んでargc/ argvそれはすでに始まっています一度、しかし:

  1. 変更する前にプログラムの引数を最初に公開するため、これは安全ではありません。
  2. これはかなりハッキーです、たとえあなたがそれを動作させることができたとしても、それに頼ることはお勧めしません

ソースコードを入手するか、ベンダーに連絡してコードを修正することをお勧めします。POSIXオペレーティングシステムのコマンドラインでシークレットを提供することは、安全な操作と互換性がありません。


11

プロセスが(execve()システムコールを介して)コマンドを実行すると、そのメモリは消去されます。実行中に情報を渡すために、execve()システム呼び出しはそのための2つの引数を取ります:argv[]およびenvp[]配列。

これらは文字列の2つの配列です。

  • 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[]公共の知識です。の出力に表示されますpsenvp[]最近ではありません。Linuxでは、に表示され/proc/pid/environます。ps ewwwBSD(およびpsLinux のprocps-ngの)の出力に表示されますが、同じ有効な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のアプローチここにあります)は、受け取ったパスワードをpr索好きなargv[]目から隠そうとします(一部のシステムでは、argv[]文字列が保存されているメモリの部分を上書きします)。しかし、それは実際には役に立たず、セキュリティの誤った約束を与えます。これにより、秘密が表示されるexecve()上書きと上書きの間にウィンドウpsが残ります。

たとえば、攻撃者がcurl -u user:somesecret https://...(たとえばcronジョブで)スクリプトを実行していることを知っている場合、彼がしなければならないことcurlは(たとえばを実行することでsh -c 'a=a;while :; do a=$a$a;done')使用する(多くの)ライブラリをキャッシュから削除することです起動を遅くすることに関しては、非常に非効率的でさえuntil grep 'curl.*[-]u' /proc/*/cmdline; do :; done、私のテストでそのパスワードをキャッチするのに十分です。

引数がコマンドに秘密を渡すことができる唯一の方法である場合、まだ試せることがいくつかあります。

古いバージョンのLinuxを含む一部のシステムでは、文字列の最初の数バイト(Linux 4.1以前では4096)のみをargv[]照会できます。

そこで、次のことができます。

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

そして、最初の4096バイトを超えているため、秘密が隠されます。Linuxは4.2以降ではargsのリストを切り捨てないため、このメソッドを使用した人は今では後悔する必要があり/proc/pid/cmdlineます。またps、同じAPIをps使用してより多くを取得することができないのは、(2048に制限されているように見えるFreeBSDのように)コマンドラインを多バイト以上表示しないからではないことに注意してください。ただし、このアプローチはps、通常のユーザーがその情報を取得する唯一の方法であるシステムでは有効ですが(APIが特権を持ちps、使用するためにsetgidまたはsetuidである場合など)、潜在的に将来性がない可能性があります。

別のアプローチはするだろうない秘密を渡すargv[]が、プログラム(使用に注入コードgdbまたは$LD_PRELOADその前にはハック)main()への挿入秘密ことを開始しているargv[]から受け取りましたexecve()

を使用LD_PRELOADして、GNUシステム上で動的にリンクされたsetuid / setgid以外の場合:

/* 
 * 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,argsargv[]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実行可能ファイルが動的にリンクされていることやデバッグシンボルを持つことに依存せず、少なくともLinux上の任意のELF実行可能ファイルで動作する、GNU固有でないアプローチは次のようになります。

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

実行可能ファイルが静的である可能性がある場合、シークレットを格納するためのメモリを割り当てる信頼できる方法がないため、すでにプロセスメモリにある別の場所からシークレットを取得する必要があります。だからこそ、環境はここで明らかな選択肢です。また、プロセスが何らかの理由で環境をダンプするか、信頼できないアプリケーションを実行することを決定した場合のリークを回避するためSECRETに、そのenv varをプロセスに非表示にしSECRE=ます。

これはSolaris 11でも機能します(gdbとGNU binutilsがインストールされている場合(名前objdumpをに変更する必要がある場合がありますgobjdump)。

FreeBSD(少なくともx86_64、スタック上のそれらの最初の24バイト(gdb(8.0.1)がgdbにバグがあるかもしれないことを示唆する対話型の場合16バイトになる)が何であるかわからない)、argcおよびargv定義を置き換えるで:

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

gdbパッケージ/ポートをインストールする必要があるかもしれません。そうでなければ、システムに付属しているバージョンは古いためです)


再(ここでパラメーター展開の周りに欠落している引用符を追加):引用符を使用しないと何が問題になりますか?本当に違いはありますか?
ユカシマフクサイ18年

@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この回答の最初の半分が関連していないようですので、。
パイプ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.