Sembunyikan argumen ke program tanpa kode sumber

15

Saya perlu menyembunyikan beberapa argumen sensitif ke program yang saya jalankan, tetapi saya tidak memiliki akses ke kode sumber. Saya juga menjalankan ini pada server bersama sehingga saya tidak dapat menggunakan sesuatu seperti hidepidkarena saya tidak memiliki hak sudo.

Inilah beberapa hal yang telah saya coba:

  • export SECRET=[my arguments], diikuti dengan panggilan ke ./program $SECRET, tetapi ini sepertinya tidak membantu.

  • ./program `cat secret.txt`mana secret.txtberisi argumen saya, tetapi yang maha kuasa psmampu mengendus rahasiaku.

Apakah ada cara lain untuk menyembunyikan argumen saya yang tidak melibatkan intervensi admin?

NONA
sumber
Apa program khusus itu? Jika itu adalah perintah biasa, Anda perlu memberi tahu (dan mungkin ada beberapa pendekatan lain) yang mana itu
Basile Starynkevitch
14
Jadi Anda mengerti apa yang terjadi, hal-hal yang Anda coba tidak memiliki kesempatan untuk bekerja karena shell bertanggung jawab untuk memperluas variabel lingkungan dan untuk melakukan penggantian perintah sebelum menjalankan program. pstidak melakukan sesuatu yang ajaib untuk "mengendus rahasia Anda". Lagi pula, program yang ditulis secara wajar sebagai gantinya harus menawarkan opsi baris perintah untuk membaca rahasia dari file yang ditentukan atau dari stdin alih-alih menganggapnya langsung sebagai argumen.
jamesdlin
Saya menjalankan program simulasi cuaca yang ditulis oleh perusahaan swasta. Mereka tidak membagikan kode sumber mereka, dokumentasi mereka juga tidak memberikan cara apa pun untuk membagikan rahasia dari suatu file. Mungkin kehabisan pilihan di sini
MS

Jawaban:

25

Seperti dijelaskan di sini , Linux menempatkan argumen program di ruang data program, dan membuat pointer ke awal area ini. Inilah yang digunakan oleh psdan seterusnya untuk menemukan dan menunjukkan argumen program.

Karena data berada dalam ruang program, ia dapat memanipulasinya. Melakukan ini tanpa mengubah program itu sendiri melibatkan memuat shim dengan main()fungsi yang akan dipanggil sebelum utama sebenarnya dari program. Shim ini dapat menyalin argumen asli ke ruang baru, lalu menimpa argumen asli sehingga pshanya akan melihat nuls.

Kode C berikut melakukan ini.

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

Tidak dimungkinkan untuk melakukan intervensi main(), tetapi Anda dapat melakukan intervensi pada fungsi standar C library __libc_start_main, yang selanjutnya disebut main. Kompilasi file ini shim_main.cseperti yang tercantum dalam komentar di awal, dan jalankan seperti yang ditunjukkan. Saya telah meninggalkan printfkode sehingga Anda memeriksa apakah itu benar-benar dipanggil. Misalnya, jalankan

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

kemudian lakukan psdan Anda akan melihat perintah kosong dan args ditampilkan.

Masih ada sedikit waktu di mana perintah args dapat terlihat. Untuk menghindari ini, Anda bisa, misalnya, mengubah shim untuk membaca rahasia Anda dari file dan menambahkannya ke argumen yang diteruskan ke program.

meuh
sumber
12
Tetapi masih akan ada jendela pendek yang /proc/pid/cmdlineakan menunjukkan rahasianya (sama seperti ketika curlmencoba menyembunyikan kata sandi yang diberikan pada baris perintah). Saat Anda menggunakan LD_PRELOAD, Anda bisa membungkus utama sehingga rahasia disalin dari lingkungan ke argumen yang diterima utama. Seperti panggilan LD_PRELOAD=x SECRET=y cmdtempat Anda menelepon main()dengan argv[]menjadi[argv[0], getenv("SECRET")]
Stéphane Chazelas
Anda tidak dapat menggunakan lingkungan untuk menyembunyikan rahasia seperti yang terlihat melalui /proc/pid/environ. Ini mungkin ditimpa dengan cara yang sama seperti args, tetapi meninggalkan jendela yang sama.
meuh
11
/proc/pid/cmdlineadalah publik, /proc/pid/environbukan. Ada beberapa sistem di mana ps(setuid dieksekusi di sana) mengekspos lingkungan dari setiap proses, tapi saya tidak berpikir Anda akan menemukan apa pun saat ini. Lingkungan umumnya dianggap cukup aman . Tidak aman untuk mengorek dari proses dengan cairan yang sama, tetapi itu sering dapat membaca memori proses dengan cairan yang sama, jadi tidak banyak yang bisa Anda lakukan tentang itu.
Stéphane Chazelas
4
@ StéphaneChazelas: Jika seseorang menggunakan lingkungan untuk memberikan rahasia, idealnya pembungkus yang meneruskannya ke mainmetode program yang dibungkus juga menghilangkan variabel lingkungan untuk menghindari kebocoran yang tidak disengaja pada proses anak. Atau pembungkus bisa membaca semua argumen baris perintah dari file.
David Foerster
@ DavidFoerster, poin bagus. Saya telah memperbarui jawaban saya untuk memperhitungkannya.
Stéphane Chazelas
16
  1. Baca dokumentasi antarmuka baris perintah aplikasi yang dipermasalahkan. Mungkin ada opsi untuk memasok rahasia dari file alih-alih sebagai argumen secara langsung.

  2. Jika itu gagal, ajukan laporan bug terhadap aplikasi dengan alasan bahwa tidak ada cara aman untuk memberikan rahasia padanya.

  3. Anda selalu dapat dengan hati-hati (!) Menyesuaikan solusi dalam jawaban meuh dengan kebutuhan spesifik Anda. Berikan perhatian khusus pada komentar Stéphane dan tindak lanjutnya.

David Foerster
sumber
12

Jika Anda perlu menyampaikan argumen ke program untuk membuatnya bekerja, Anda akan kurang beruntung tidak peduli apa yang Anda lakukan jika Anda tidak dapat menggunakan hidepid pada procfs.

Karena Anda menyebutkan ini adalah skrip bash, Anda harus sudah memiliki kode sumber, karena bash bukan bahasa yang dikompilasi.

Jika gagal, Anda mungkin dapat menulis ulang cmdline proses menggunakan gdbatau serupa dan bermain-main dengan argc/ argvbegitu sudah dimulai, tetapi:

  1. Ini tidak aman, karena Anda masih mengekspos argumen program Anda pada awalnya sebelum mengubahnya
  2. Ini cukup berantakan, bahkan jika Anda bisa membuatnya bekerja, saya tidak akan merekomendasikan bergantung padanya

Saya benar-benar hanya merekomendasikan mendapatkan kode sumber, atau berbicara dengan vendor untuk mendapatkan kode yang dimodifikasi. Memberikan rahasia pada baris perintah dalam sistem operasi POSIX tidak kompatibel dengan operasi yang aman.

Chris Down
sumber
11

Ketika suatu proses mengeksekusi perintah (melalui execve()panggilan sistem), memorinya dihapus. Untuk meneruskan beberapa informasi di eksekusi, execve()panggilan sistem membutuhkan dua argumen untuk itu: argv[]danenvp[] array.

Itu adalah dua array string:

  • argv[] berisi argumen
  • envp[]berisi definisi variabel lingkungan sebagai string dalam var=valueformat (berdasarkan konvensi).

Saat kamu melakukan:

export SECRET=value; cmd "$SECRET"

(di sini menambahkan tanda kutip yang hilang di sekitar ekspansi parameter).

Anda mengeksekusi cmddengan rahasia ( value) yang dilewati di argv[]dan envp[]. argv[]akan ada ["cmd", "value"]dan envp[]sesuatu seperti [..., "PATH=/bin:...", "HOME=...", ..., "SECRET=value", "TERM=xterm", ...]. As cmdtidak melakukan apa pun getenv("SECRET")atau yang setara untuk mengambil nilai rahasia dari ituSECRET variabel lingkungan itu, meletakkannya di lingkungan tidak berguna.

argv[]adalah pengetahuan umum. Itu ditunjukkan dalam output dari ps. envp[]saat ini tidak. Di Linux, ini terlihat di /proc/pid/environ. Ini ditunjukkan dalam output ps ewwwpada BSD (dan dengan procps-ng psdi Linux), tetapi hanya untuk proses yang berjalan dengan uid efektif yang sama (dan dengan lebih banyak pembatasan untuk executable setuid / setgid). Mungkin terlihat di beberapa log audit, tetapi log audit tersebut hanya dapat diakses oleh administrator.

Singkatnya, lingkungan yang diteruskan ke yang dapat dieksekusi dimaksudkan untuk bersifat pribadi atau paling tidak sama privatnya dengan memori internal suatu proses (yang dalam beberapa keadaan proses lain dengan hak istimewa juga dapat mengakses dengan debugger misalnya dan dapat juga akan dibuang ke disk).

Karena argv[]pengetahuan publik, perintah yang mengharapkan data yang dirahasiakan pada baris perintahnya rusak oleh desain.

Biasanya, perintah yang perlu diberi rahasia, memberi Anda antarmuka lain untuk melakukannya, seperti melalui variabel lingkungan. Contohnya:

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

Atau melalui deskriptor file khusus seperti stdin:

echo secret | openssl rsa -passin stdin ...

( echosedang dibangun, itu tidak ditampilkan dalam output of ps)

Atau file, seperti .netrcuntuk ftpdan beberapa perintah lain atau

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

Beberapa aplikasi seperti curl(dan itu juga pendekatan yang diambil oleh @meuh di sini ) mencoba untuk menyembunyikan kata sandi yang mereka terima argv[]dari mencongkel mata (pada beberapa sistem dengan menimpa bagian memori di mana argv[]string disimpan). Tapi itu tidak benar-benar membantu dan memberikan janji keamanan palsu. Yang meninggalkan jendela di antara execve()dan menimpa di mana psmasih akan menunjukkan rahasianya.

Misalnya, jika penyerang tahu bahwa Anda menjalankan skrip yang melakukan curl -u user:somesecret https://...(misalnya dalam tugas cron), yang harus ia lakukan adalah mengusir dari cache (banyak) perpustakaan yang curlmenggunakan (misalnya dengan menjalankan a sh -c 'a=a;while :; do a=$a$a;done') jadi untuk memperlambat startup-nya, dan bahkan melakukan yang sangat tidak efisien until grep 'curl.*[-]u' /proc/*/cmdline; do :; donesudah cukup untuk menangkap kata sandi itu dalam pengujian saya.

Jika argumen adalah satu-satunya cara Anda bisa meneruskan rahasia ke perintah, mungkin masih ada beberapa hal yang bisa Anda coba.

Pada beberapa sistem, termasuk versi Linux yang lebih lama, hanya beberapa byte pertama (4096 pada Linux 4.1 dan sebelumnya) dari string yang argv[]dapat di -query.

Di sana, Anda bisa melakukan:

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

Dan rahasianya akan disembunyikan karena melewati 4096 byte pertama. Sekarang orang yang telah menggunakan metode itu harus menyesal sekarang karena Linux sejak 4.2 tidak lagi memotong daftar argumen di /proc/pid/cmdline. Perhatikan juga bahwa ini bukan karena tidak psakan menampilkan lebih dari byte byte perintah (seperti pada FreeBSD yang sepertinya terbatas pada 2048) yang tidak bisa digunakan oleh pengguna API yang sama psuntuk mendapatkan lebih banyak. Namun pendekatan itu valid pada sistem di mana pssatu-satunya cara bagi pengguna biasa untuk mengambil informasi itu (seperti ketika API diistimewakan danps ditetapkan atau digunakan untuk menggunakannya), tetapi masih berpotensi tidak menjadi bukti di masa depan di sana.

Pendekatan lain adalah dengan tidak meneruskan rahasia argv[]tetapi menyuntikkan kode ke dalam program (menggunakan gdbatau $LD_PRELOADhack) sebelum main()dimulai yang memasukkan rahasia ke dalam yang argv[]diterima dariexecve() .

Dengan LD_PRELOAD, untuk executable yang non-setuid / setgid yang terhubung secara dinamis pada sistem 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);
}

Kemudian:

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

Pada titik tidak akan psmenunjukkan di ps -opid,argssana ( -opid,argsmenjadi rahasia dalam contoh ini). Perhatikan bahwa kami mengganti elemen argv[]array pointer , bukan mengesampingkan string yang ditunjukkan oleh pointer tersebut, itulah sebabnya modifikasi kami tidak ditampilkan dalam outputps .

Dengan gdb, masih untuk executable yang non-setuid / setgid yang terhubung secara dinamis dan pada sistem 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"

Masih dengan gdb, pendekatan spesifik non-GNU yang tidak bergantung pada executable yang dihubungkan secara dinamis atau memiliki simbol debug dan harus bekerja untuk ELF yang dapat dieksekusi di Linux setidaknya bisa menjadi:

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

Pengujian dengan executable yang terhubung secara statis:

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

Ketika executable mungkin statis, kami tidak memiliki cara yang dapat diandalkan untuk mengalokasikan memori untuk menyimpan rahasia, jadi kami harus mendapatkan rahasia dari tempat lain yang sudah ada dalam memori proses. Itu sebabnya lingkungan adalah pilihan yang jelas di sini. Kami juga menyembunyikan SECRETenv var itu ke proses (dengan mengubahnya ke SECRE=) untuk menghindari kebocoran jika proses memutuskan untuk membuang lingkungannya untuk beberapa alasan atau menjalankan aplikasi yang tidak dipercaya.

Itu juga berfungsi pada Solaris 11 (asalkan gdb dan GNU binutils diinstal (Anda mungkin harus mengganti nama objdumpmenjadi gobjdump).

Pada FreeBSD (setidaknya x86_64, saya tidak yakin apa itu 24 byte pertama (yang menjadi 16 ketika gdb (8.0.1) interaktif menunjukkan ada bug di gdb di sana) pada stack), ganti argcdan argvdefinisi dengan:

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

(Anda mungkin juga perlu menginstal gdbpaket / port karena versi yang sebelumnya datang dengan sistem kuno).

Stéphane Chazelas
sumber
Re (di sini menambahkan tanda kutip yang hilang di sekitar ekspansi parameter): Apa yang salah dengan tidak menggunakan tanda kutip? Apakah benar-benar ada perbedaan?
yukashima huksay
@yukashimahuksay, lihat misalnya implikasi keamanan lupa mengutip variabel dalam bash / POSIX shells dan pertanyaan yang ditautkan di sana.
Stéphane Chazelas
3

Yang mungkin Anda lakukan adalah

 export SECRET=somesecretstuff

kemudian, dengan anggapan Anda sedang menulis ./programdi C (atau orang lain melakukannya, dan dapat mengubah atau memperbaikinya untuk Anda), gunakan getenv (3) dalam program itu, mungkin sebagai

char* secret= getenv("SECRET");

dan setelah itu export kamu jalankan saja ./programdi shell yang sama. Atau nama variabel lingkungan dapat diteruskan ke sana (dengan menjalankan ./program --secret-var=SECRETdll ...)

pstidak akan memberi tahu tentang rahasia Anda, tetapi proc (5) masih dapat memberikan banyak informasi (setidaknya untuk proses lain dari pengguna yang sama).

Lihat juga ini untuk membantu merancang cara yang lebih baik dalam menyampaikan argumen program.

Lihat jawaban ini untuk penjelasan yang lebih baik tentang globbing dan peran shell.

Mungkin Anda programmemiliki beberapa cara lain untuk mendapatkan data (atau menggunakan komunikasi antar-proses dengan lebih bijaksana) daripada argumen program biasa (tentu saja harus, jika itu dimaksudkan untuk memproses informasi sensitif). Baca dokumentasinya. Atau mungkin Anda menyalahgunakan program itu (yang tidak dimaksudkan untuk memproses data rahasia).

Menyembunyikan data rahasia sangat sulit. Tidak melewatinya melalui argumen program tidak cukup.

Basile Starynkevitch
sumber
5
Cukup jelas dari pertanyaan bahwa ia bahkan tidak memiliki kode sumber untuk ./program, sehingga paruh pertama jawaban ini tampaknya tidak relevan.
pipa