Ẩn đối số để lập trình mà không cần mã nguồn


15

Tôi cần ẩn một số đối số nhạy cảm với chương trình tôi đang chạy, nhưng tôi không có quyền truy cập vào mã nguồn. Tôi cũng đang chạy cái này trên một máy chủ dùng chung vì vậy tôi không thể sử dụng cái gì đó như hidepidvì tôi không có đặc quyền sudo.

Đây là một số điều tôi đã thử:

  • export SECRET=[my arguments], theo sau là một cuộc gọi đến ./program $SECRET, nhưng điều này dường như không có ích.

  • ./program `cat secret.txt`nơi secret.txtchứa đựng những lý lẽ của tôi, nhưng toàn năng pscó thể đánh hơi được bí mật của tôi.

Có cách nào khác để che giấu những lập luận của tôi không liên quan đến sự can thiệp của quản trị viên không?


Chương trình cụ thể đó là gì? Nếu đó là một lệnh thông thường bạn cần nói (và có thể có một cách tiếp cận khác) đó là cách nào
Basile Starynkevitch

14
Vì vậy, bạn hiểu những gì đang xảy ra, những điều bạn đã cố gắng không có cơ hội làm việc vì shell chịu trách nhiệm mở rộng các biến môi trường và thực hiện thay thế lệnh trước khi gọi chương trình. pskhông làm bất cứ điều gì kỳ diệu để "đánh hơi bí mật của bạn". Dù sao, các chương trình được viết hợp lý thay vào đó nên cung cấp tùy chọn dòng lệnh để đọc một bí mật từ một tệp được chỉ định hoặc từ stdin thay vì lấy nó trực tiếp làm đối số.
jamesdlin

Tôi đang chạy một chương trình mô phỏng thời tiết được viết bởi một công ty tư nhân. Họ không chia sẻ mã nguồn của họ, tài liệu của họ cũng không cung cấp bất kỳ cách nào để chia sẻ bí mật từ một tệp. Có thể không có lựa chọn nào ở đây
MS

Câu trả lời:


25

Như đã giải thích ở đây , Linux đặt các đối số của chương trình vào không gian dữ liệu của chương trình và giữ một con trỏ đến đầu khu vực này. Đây là những gì được sử dụng psvà vv để tìm và hiển thị các đối số chương trình.

Vì dữ liệu nằm trong không gian của chương trình, nên nó có thể thao tác với nó. Làm điều này mà không thay đổi chính chương trình liên quan đến việc tải một shim với một main()chức năng sẽ được gọi trước chính thực sự của chương trình. Shim này có thể sao chép các đối số thực sang một không gian mới, sau đó ghi đè lên các đối số ban đầu để pschỉ nhìn thấy nuls.

Mã C sau đây thực hiện điều này.

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

Không thể can thiệp vào main(), nhưng bạn có thể can thiệp vào chức năng thư viện C tiêu chuẩn __libc_start_main, tiếp tục gọi chính. Biên dịch tệp này shim_main.cnhư đã ghi chú trong phần bình luận khi bắt đầu và chạy nó như hiển thị. Tôi đã để lại một printfmã để bạn kiểm tra xem nó có thực sự được gọi không. Ví dụ: chạy

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

sau đó làm một psvà bạn sẽ thấy một lệnh trống và đối số được hiển thị.

Vẫn còn một lượng nhỏ thời gian mà lệnh args có thể hiển thị. Để tránh điều này, ví dụ, bạn có thể thay đổi shim để đọc bí mật của bạn từ một tệp và thêm nó vào các đối số được truyền cho chương trình.


12
Nhưng vẫn sẽ có một cửa sổ ngắn trong đó /proc/pid/cmdlinesẽ hiển thị bí mật (giống như khi curlcố gắng ẩn mật khẩu, nó được đưa ra trên dòng lệnh). Trong khi bạn đang sử dụng LD_PRELOAD, bạn có thể gói chính để bí mật được sao chép từ môi trường sang đối số mà chính nhận được. Giống như cuộc gọi LD_PRELOAD=x SECRET=y cmdmà bạn gọi main()với argv[]bị[argv[0], getenv("SECRET")]
Stéphane Chazelas

Bạn không thể sử dụng môi trường để ẩn một bí mật vì nó hiển thị thông qua /proc/pid/environ. Điều này có thể được ghi đè theo cùng một cách như các đối số, nhưng nó để lại cùng một cửa sổ.
meuh

11
/proc/pid/cmdlinelà công khai, /proc/pid/environlà không. Có một số hệ thống trong đó ps(một bộ thực thi setuid ở đó) đã phơi bày môi trường của bất kỳ quy trình nào, nhưng tôi không nghĩ rằng bạn sẽ bắt gặp bất kỳ ngày nay. Môi trường thường được coi là đủ an toàn . Không an toàn khi tò mò từ các quy trình có cùng euid, nhưng dù sao thì những người đó thường có thể đọc bộ nhớ của các quy trình bởi cùng một euid, vì vậy bạn không thể làm gì nhiều về nó.
Stéphane Chazelas

4
@ StéphaneChazelas: Nếu một người sử dụng môi trường để truyền bí mật, lý tưởng nhất là trình bao bọc chuyển tiếp nó tới mainphương thức của chương trình được bao bọc cũng loại bỏ biến môi trường để tránh rò rỉ ngẫu nhiên cho các tiến trình con. Ngoài ra, trình bao bọc có thể đọc tất cả các đối số dòng lệnh từ một tệp.
David Foerster

@DavidFoerster, điểm tốt. Tôi đã cập nhật câu trả lời của mình để tính đến điều đó.
Stéphane Chazelas

16
  1. Đọc tài liệu về giao diện dòng lệnh của ứng dụng được đề cập. Cũng có thể có một tùy chọn để cung cấp bí mật từ một tệp thay vì làm đối số trực tiếp.

  2. Nếu thất bại, hãy gửi báo cáo lỗi đối với ứng dụng với lý do không có cách nào an toàn để cung cấp bí mật cho nó.

  3. Bạn luôn có thể cẩn thận (!) Điều chỉnh giải pháp trong câu trả lời của meuh cho nhu cầu cụ thể của bạn. Đặc biệt chú ý đến nhận xét của Stéphane và các phần tiếp theo của nó.


12

Nếu bạn cần chuyển các đối số cho chương trình để làm cho nó hoạt động, bạn sẽ không gặp may cho dù bạn có làm gì nếu bạn không thể sử dụng hidepidtrên Procfs.

Vì bạn đã đề cập đây là tập lệnh bash, nên bạn đã có sẵn mã nguồn, vì bash không phải là ngôn ngữ được biên dịch.

Không, bạn thể viết lại cmdline của quá trình bằng cách sử dụng gdbhoặc tương tự và chơi xung quanh với argc/ argvkhi nó đã bắt đầu, nhưng:

  1. Điều này không an toàn, vì ban đầu bạn vẫn phơi bày các đối số chương trình của mình trước khi thay đổi chúng
  2. Điều này khá khó khăn, ngay cả khi bạn có thể làm cho nó hoạt động, tôi không khuyên bạn nên dựa vào nó

Tôi thực sự chỉ khuyên bạn nên lấy mã nguồn hoặc nói chuyện với nhà cung cấp để sửa đổi mã. Cung cấp bí mật trên dòng lệnh trong hệ điều hành POSIX không tương thích với hoạt động an toàn.


11

Khi một quá trình thực thi một lệnh (thông qua execve()cuộc gọi hệ thống), bộ nhớ của nó sẽ bị xóa. Để truyền một số thông tin qua thực thi, các execve()cuộc gọi hệ thống cần hai đối số cho điều đó: argv[]envp[]mảng.

Đó là hai mảng của chuỗi:

  • argv[] chứa các đối số
  • envp[]chứa các định nghĩa biến môi trường dưới dạng các chuỗi trong var=valueđịnh dạng (theo quy ước).

Khi bạn làm:

export SECRET=value; cmd "$SECRET"

(ở đây đã thêm các trích dẫn còn thiếu xung quanh việc mở rộng tham số).

Bạn đang thực hiện cmdvới bí mật ( value) được truyền cả trong argv[]envp[]. argv[]sẽ được ["cmd", "value"]envp[]một cái gì đó như [..., "PATH=/bin:...", "HOME=...", ..., "SECRET=value", "TERM=xterm", ...]. Vì cmdkhông thực hiện bất kỳ getenv("SECRET")hoặc tương đương để lấy giá trị của bí mật từ SECRETbiến môi trường đó, nên đặt nó vào môi trường là không hữu ích.

argv[]là kiến ​​thức công cộng. Nó cho thấy trong đầu ra của ps. envp[]ngày nay thì không. Trên Linux, nó hiển thị trong /proc/pid/environ. Nó cho thấy trong đầu ra của ps ewwwBSD (và với Procps-ng pstrên Linux), nhưng chỉ với các quy trình chạy với cùng một uid hiệu quả (và có nhiều hạn chế hơn đối với các tệp thực thi setuid / setgid). Nó có thể hiển thị trong một số nhật ký kiểm toán, nhưng những nhật ký kiểm toán đó chỉ có thể được truy cập bởi các quản trị viên.

Nói tóm lại, môi trường được truyền cho một thực thi có nghĩa là riêng tư hoặc ít nhất là riêng tư như bộ nhớ trong của một tiến trình (trong một số trường hợp, một quy trình khác có đặc quyền phù hợp cũng có thể truy cập bằng trình gỡ lỗi và có thể cũng được đổ vào đĩa).

argv[]là kiến ​​thức công cộng, một lệnh dự kiến ​​dữ liệu có nghĩa là bí mật trên dòng lệnh của nó bị phá vỡ theo thiết kế.

Thông thường, các lệnh cần được cung cấp một bí mật, cung cấp cho bạn một giao diện khác để thực hiện điều đó, như thông qua một biến môi trường. Ví dụ:

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

Hoặc thông qua một mô tả tập tin chuyên dụng như stdin:

echo secret | openssl rsa -passin stdin ...

( echođang được dựng sẵn, nó không hiển thị trong đầu ra của ps)

Hoặc một tệp, như .netrcfor ftpvà một vài lệnh khác hoặc

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

Một số ứng dụng như curl(và đó cũng là cách tiếp cận được thực hiện bởi @meuh tại đây ) cố gắng ẩn mật khẩu mà họ nhận được argv[]khỏi con mắt tò mò (trên một số hệ thống bằng cách ghi đè lên phần bộ nhớ nơi argv[]lưu trữ chuỗi). Nhưng điều đó không thực sự giúp ích và đưa ra một lời hứa sai về bảo mật. Điều đó để lại một cửa sổ ở giữa execve()và ghi đè nơi psvẫn sẽ hiển thị bí mật.

Chẳng hạn, nếu kẻ tấn công biết rằng bạn đang chạy tập lệnh đang thực hiện curl -u user:somesecret https://...(ví dụ như trong công việc định kỳ), tất cả những gì anh ta phải làm là xóa khỏi bộ đệm (các) thư viện curlsử dụng (ví dụ bằng cách chạy a sh -c 'a=a;while :; do a=$a$a;done') như làm chậm quá trình khởi động của nó và thậm chí làm rất kém hiệu quả until grep 'curl.*[-]u' /proc/*/cmdline; do :; donelà đủ để bắt được mật khẩu đó trong các thử nghiệm của tôi.

Nếu các đối số là cách duy nhất bạn có thể truyền bí mật cho các lệnh, thì vẫn có thể có một số điều bạn có thể thử.

Trên một số hệ thống, bao gồm các phiên bản cũ hơn của Linux, chỉ argv[]có thể truy vấn một vài byte đầu tiên (4096 trên Linux 4.1 trở lên) của chuỗi .

Ở đó, bạn có thể làm:

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

Và bí mật sẽ bị ẩn bởi vì nó đã vượt qua 4096 byte đầu tiên. Bây giờ những người đã sử dụng phương pháp đó phải hối hận vì Linux kể từ phiên bản 4.2 không còn cắt xén danh sách các đối số /proc/pid/cmdline. Cũng lưu ý rằng không phải vì pssẽ không hiển thị nhiều hơn một byte dòng lệnh (như trên FreeBSD, nơi dường như bị giới hạn ở năm 2048) mà người ta không thể sử dụng cùng một API psđể sử dụng nhiều hơn. Tuy nhiên, cách tiếp cận đó là hợp lệ trên các hệ thống pslà cách duy nhất để người dùng thông thường truy xuất thông tin đó (như khi API được đặc quyền và psđược setgid hoặc setuid để sử dụng), nhưng vẫn không có khả năng trong tương lai.

Một cách tiếp cận khác là không truyền bí mật vào argv[]mà tiêm mã vào chương trình (sử dụng gdbhoặc $LD_PRELOADhack) trước khi main()bắt đầu chèn bí mật vào argv[]nhận được từ đó execve().

Với LD_PRELOAD, đối với các tệp thực thi được liên kết động không phải setuid / setgid trên hệ thống 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);
}

Sau đó:

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

Không có điểm nào pscho thấy điều ps -opid,argsđó ( -opid,argslà bí mật trong ví dụ này). Lưu ý rằng chúng tôi thay thế các phần tử của argv[]mảng con trỏ , không ghi đè các chuỗi được trỏ bởi các con trỏ đó, đó là lý do tại sao các sửa đổi của chúng tôi không hiển thị trong đầu ra của ps.

Với gdb, vẫn dành cho các tệp thực thi được liên kết động không setuid / setgid và trên các hệ thống 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"

Tuy nhiên gdb, một cách tiếp cận cụ thể không phải là GNU không dựa vào các tệp thực thi được liên kết động hoặc có các ký hiệu gỡ lỗi và nên hoạt động cho mọi ELF thực thi trên Linux ít nhất có thể là:

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

Kiểm tra với một thực thi được liên kết tĩnh:

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

Khi thực thi có thể là tĩnh, chúng ta không có cách đáng tin cậy để phân bổ bộ nhớ để lưu trữ bí mật, vì vậy chúng ta phải lấy bí mật từ một nơi khác đã có trong bộ nhớ quy trình. Đó là lý do tại sao môi trường là sự lựa chọn rõ ràng ở đây. Chúng tôi cũng ẩn SECRETenv var đó vào quy trình (bằng cách thay đổi nó SECRE=) để tránh nó bị rò rỉ nếu quá trình quyết định kết xuất môi trường của nó vì một số lý do hoặc thực thi các ứng dụng không tin cậy.

Điều đó cũng hoạt động trên Solaris 11 (được cung cấp gdb và GNU binutils (bạn có thể phải đổi tên objdumpthành gobjdump).

Trên FreeBSD (ít nhất là x86_64, tôi không chắc 24 byte đầu tiên (trở thành 16 khi gdb (8.0.1) tương tác cho thấy có thể có lỗi trong gdb ở đó) trên stack), thay thế argcargvđịnh nghĩa với:

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

(bạn cũng có thể cần cài đặt gdbgói / cổng vì phiên bản đi kèm với hệ thống là cổ xưa).


Re (ở đây đã thêm các trích dẫn bị thiếu xung quanh việc mở rộng tham số): Điều gì sai khi không sử dụng dấu ngoặc kép? Có gì khác sao?
yukashima huksay

@yukashimahuksay, xem ví dụ: Ý nghĩa bảo mật của việc quên trích dẫn một biến trong shell bash / POSIX và các câu hỏi được liên kết ở đó.
Stéphane Chazelas

3

Những gì bạn có thể làm là

 export SECRET=somesecretstuff

sau đó, giả sử bạn đang viết bằng ./programC (hoặc người khác làm và có thể thay đổi hoặc cải thiện nó cho bạn), hãy sử dụng getenv (3) trong chương trình đó, có lẽ như

char* secret= getenv("SECRET");

và sau khi export bạn chỉ chạy ./programtrong cùng một vỏ. Hoặc tên biến môi trường có thể được truyền cho nó (bằng cách chạy ./program --secret-var=SECRETvv ...)

pssẽ không nói về bí mật của bạn, nhưng Proc (5) vẫn có thể cung cấp nhiều thông tin (ít nhất là cho các quy trình khác của cùng một người dùng).

Xem thêm điều này để giúp thiết kế một cách tốt hơn để vượt qua các đối số chương trình.

Xem câu trả lời này để được giải thích rõ hơn về Globing và vai trò của vỏ.

Có lẽ bạn programcó một số cách khác để nhận dữ liệu (hoặc sử dụng giao tiếp giữa các quá trình một cách khôn ngoan hơn) so với các đối số chương trình đơn giản (chắc chắn nên, nếu có ý định xử lý thông tin nhạy cảm). Đọc tài liệu của nó. Hoặc có lẽ bạn đang lạm dụng chương trình đó (không nhằm xử lý dữ liệu bí mật).

Che giấu dữ liệu bí mật thực sự khó khăn. Không vượt qua nó thông qua các đối số chương trình là không đủ.


5
Nó khá rõ ràng từ câu hỏi rằng ông thậm chí không có mã nguồn cho ./program, vì vậy nửa đầu của câu trả lời này dường như không có liên quan.
đường ống
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.