Ocultar argumentos para programar sin código fuente


15

Necesito ocultar algunos argumentos sensibles a un programa que estoy ejecutando, pero no tengo acceso al código fuente. También estoy ejecutando esto en un servidor compartido, así que no puedo usar algo como hidepidporque no tengo privilegios de sudo.

Aquí hay algunas cosas que he intentado:

  • export SECRET=[my arguments], seguido de una llamada a ./program $SECRET, pero esto no parece ayudar.

  • ./program `cat secret.txt`donde secret.txtcontiene mis argumentos, pero el Todopoderoso pses capaz de descubrir mis secretos.

¿Hay alguna otra forma de ocultar mis argumentos que no implique la intervención del administrador?


¿Cuál es ese programa en particular? Si es un comando habitual, debe indicar (y podría haber algún otro enfoque) cuál es
Basile Starynkevitch,

14
Entonces entiendes lo que está sucediendo, las cosas que intentaste no tienen posibilidad de funcionar porque el shell es responsable de expandir las variables de entorno y de realizar la sustitución de comandos antes de invocar el programa. psno está haciendo nada mágico para "descubrir tus secretos". De todos modos, los programas razonablemente escritos deberían ofrecer una opción de línea de comandos para leer un secreto de un archivo especificado o de stdin en lugar de tomarlo directamente como argumento.
jamesdlin

Estoy ejecutando un programa de simulación del clima escrito por una empresa privada. No comparten su código fuente, ni su documentación proporciona ninguna forma de compartir un secreto de un archivo. Podría estar fuera de opciones aquí
MS

Respuestas:


25

Como se explica aquí , Linux coloca los argumentos de un programa en el espacio de datos del programa y mantiene un puntero al inicio de esta área. Esto es lo que es usado por psy así sucesivamente para encontrar y mostrar los argumentos del programa.

Como los datos están en el espacio del programa, pueden manipularlos. Hacer esto sin cambiar el programa en sí mismo implica cargar una cuña con una main()función que se llamará antes que la parte principal real del programa. Esta cuña puede copiar los argumentos reales en un nuevo espacio, luego sobrescribir los argumentos originales para que pssolo se vean nuls.

El siguiente código C hace esto.

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

No es posible intervenir main(), pero puede intervenir en la función de biblioteca C estándar __libc_start_main, que pasa a llamar a main. Compile este archivo shim_main.ccomo se indica en el comentario al inicio y ejecútelo como se muestra. He dejado un printfcódigo en el código para que compruebes que realmente se está llamando. Por ejemplo, ejecutar

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

luego haga un psy verá un comando en blanco y argumentos que se muestran.

Todavía hay una pequeña cantidad de tiempo que los argumentos del comando pueden ser visibles. Para evitar esto, podría, por ejemplo, cambiar la cuña para leer su secreto de un archivo y agregarlo a los argumentos pasados ​​al programa.


12
Pero todavía habrá una ventana corta durante la cual /proc/pid/cmdlinese mostrará el secreto (igual que cuando curlintenta ocultar la contraseña que se le da en la línea de comando). Mientras utiliza LD_PRELOAD, puede ajustar main para que el secreto se copie del entorno al argumento que recibe main. Como llamar LD_PRELOAD=x SECRET=y cmddonde llamas main()con argv[]ser[argv[0], getenv("SECRET")]
Stéphane Chazelas

No puede usar el entorno para ocultar un secreto, ya que es visible a través de /proc/pid/environ. Esto puede sobrescribirse de la misma manera que los argumentos, pero deja la misma ventana.
Meuh

11
/proc/pid/cmdlinees público, /proc/pid/environno lo es. Hubo algunos sistemas donde ps(un ejecutable setuid allí) expuso el entorno de cualquier proceso, pero no creo que se encuentre con ninguno hoy en día. El medio ambiente generalmente se considera lo suficientemente seguro . No es seguro extraer de los procesos con el mismo euid, pero de todos modos a menudo pueden leer la memoria de los procesos del mismo euid, por lo que no hay mucho que pueda hacer al respecto.
Stéphane Chazelas

44
@ StéphaneChazelas: si se usa el entorno para transmitir secretos, lo ideal es que el contenedor que lo reenvía al mainmétodo del programa envuelto también elimine la variable de entorno para evitar fugas accidentales a los procesos secundarios. Alternativamente, el contenedor podría leer todos los argumentos de la línea de comandos de un archivo.
David Foerster

@DavidFoerster, buen punto. He actualizado mi respuesta para tener eso en cuenta.
Stéphane Chazelas

16
  1. Lea la documentación de la interfaz de línea de comandos de la aplicación en cuestión. Puede haber una opción para proporcionar el secreto de un archivo en lugar de como un argumento directamente.

  2. Si eso falla, presente un informe de error contra la aplicación alegando que no hay una forma segura de proporcionarle un secreto.

  3. Siempre puede (cuidadosamente) adaptar la solución en la respuesta de meuh a sus necesidades específicas. Preste especial atención al comentario de Stéphane y sus seguimientos.


12

Si necesita pasar argumentos al programa para que funcione, no tendrá suerte, no importa lo que haga si no puede usarlo hidepiden procfs.

Como mencionó que este es un script bash, ya debería tener el código fuente disponible, ya que bash no es un lenguaje compilado.

De lo contrario, puede volver a escribir el cmdline del proceso usando gdbo similar y jugando con argc/ argvuna vez que ya ha comenzado, pero:

  1. Esto no es seguro, ya que aún expone los argumentos de su programa inicialmente antes de cambiarlos.
  2. Esto es bastante hacky, incluso si pudieras hacerlo funcionar, no recomendaría confiar en él

Realmente solo recomiendo obtener el código fuente, o hablar con el proveedor para modificar el código. El suministro de secretos en la línea de comandos en un sistema operativo POSIX es incompatible con una operación segura.


11

Cuando un proceso ejecuta un comando (a través de la execve()llamada al sistema), su memoria se borra. Para pasar información a través de la ejecución, las execve()llamadas al sistema toman dos argumentos para eso: las matrices argv[]y envp[].

Esas son dos matrices de cadenas:

  • argv[] contiene los argumentos
  • envp[]contiene las definiciones de variables de entorno como cadenas en el var=valueformato (por convención).

Cuando tu lo hagas:

export SECRET=value; cmd "$SECRET"

(Aquí se agregan las comillas faltantes alrededor de la expansión del parámetro).

Estás ejecutando cmdcon el secreto ( value) pasado tanto en argv[]y envp[]. argv[]será ["cmd", "value"]y envp[]algo así [..., "PATH=/bin:...", "HOME=...", ..., "SECRET=value", "TERM=xterm", ...]. Como cmdno está haciendo nada getenv("SECRET")o equivalente para recuperar el valor del secreto de esa SECRETvariable de entorno, ponerlo en el entorno no es útil.

argv[]Es de conocimiento público. Se muestra en la salida de ps. envp[]hoy en día no lo es. En Linux, se muestra en /proc/pid/environ. Se muestra en la salida de los ps ewwwBSD (y con procps-ng psen Linux), pero solo en los procesos que se ejecutan con el mismo uid efectivo (y con más restricciones para los ejecutables setuid / setgid). Puede aparecer en algunos registros de auditoría, pero solo los administradores deben tener acceso a esos registros de auditoría.

En resumen, el entorno que se pasa a un ejecutable debe ser privado o al menos tan privado como la memoria interna de un proceso (que, en algunas circunstancias, otro proceso con los privilegios correctos también puede acceder con un depurador, por ejemplo, y puede también ser volcado al disco).

Dado que argv[]es de conocimiento público, el diseño rompe un comando que espera que los datos destinados a ser secretos en su línea de comando se rompan.

Por lo general, los comandos que necesitan un secreto, le proporcionan otra interfaz para hacerlo, como a través de una variable de entorno. Por ejemplo:

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

O a través de un descriptor de archivo dedicado como stdin:

echo secret | openssl rsa -passin stdin ...

( echoestando integrado, no se muestra en la salida de ps)

O un archivo, como .netrcfor ftpy algunos otros comandos o

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

Algunas aplicaciones como curl(y ese también es el enfoque adoptado por @meuh aquí ) intentan ocultar la contraseña que recibieron argv[]de miradas indiscretas (en algunos sistemas sobrescribiendo la porción de memoria donde argv[]se almacenaron las cadenas). Pero eso no ayuda mucho y da una falsa promesa de seguridad. Eso deja una ventana entre execve()y la sobrescritura donde psaún se mostrará el secreto.

Por ejemplo, si un atacante sabe que está ejecutando un script haciendo curl -u user:somesecret https://...(por ejemplo, en un trabajo cron), todo lo que tiene que hacer es expulsar del caché las (muchas) bibliotecas que curlusa (por ejemplo ejecutando a sh -c 'a=a;while :; do a=$a$a;done') para ralentizar su inicio, e incluso hacer un método muy ineficiente until grep 'curl.*[-]u' /proc/*/cmdline; do :; donees suficiente para atrapar esa contraseña en mis pruebas.

Si los argumentos son la única forma en que puede pasar el secreto a los comandos, aún puede intentar hacer algunas cosas.

En algunos sistemas, incluidas las versiones anteriores de Linux, solo se pueden consultar los primeros bytes (4096 en Linux 4.1 y anteriores) de las cadenas argv[].

Ahí puedes hacer:

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

Y el secreto estaría oculto porque ya pasaron los primeros 4096 bytes. Ahora las personas que han usado ese método deben arrepentirse ahora ya que Linux desde 4.2 ya no trunca la lista de argumentos /proc/pid/cmdline. También tenga en cuenta que no es porque psno muestre más que tantos bytes de una línea de comando (como en FreeBSD donde parece estar limitado a 2048) que no se puede usar los mismos psusos de API para obtener más. Sin embargo, ese enfoque es válido en sistemas en los que pses la única forma en que un usuario normal puede recuperar esa información (como cuando la API tiene privilegios y psestá configurada o configurada para usarla), pero aún no está allí a prueba de futuro.

Otro enfoque sería no pasar el secreto argv[]sino inyectar código en el programa (usando gdbo $LD_PRELOADpiratear) antes de que main()se inicie, que inserta el secreto en el argv[]recibido execve().

Con LD_PRELOAD, para ejecutables no setuid / setgid vinculados dinámicamente en un sistema 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);
}

Luego:

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

En ningún momento habría psmostrado el ps -opid,argsallí ( -opid,argssiendo el secreto en este ejemplo). Tenga en cuenta que estamos reemplazando elementos de la argv[]matriz de punteros , no anulando las cadenas señaladas por esos punteros, por lo que nuestras modificaciones no se muestran en la salida de ps.

Con gdb, aún para ejecutables vinculados dinámicamente no setuid / setgid y en sistemas 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"

Aún así gdb, un enfoque específico que no sea GNU que no se base en que los ejecutables estén vinculados dinámicamente o que tengan símbolos de depuración y que debería funcionar para cualquier ejecutable ELF en Linux al menos podría ser:

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

Prueba con un ejecutable vinculado estáticamente:

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

Cuando el ejecutable puede ser estático, no tenemos una forma confiable de asignar memoria para almacenar el secreto, por lo que debemos obtener el secreto de otro lugar que ya esté en la memoria del proceso. Es por eso que el medio ambiente es la opción obvia aquí. También ocultamos ese SECRETentorno al proceso (cambiándolo a SECRE=) para evitar que se filtre si el proceso decide volcar su entorno por algún motivo o ejecutar aplicaciones no confiables.

Que también funciona en Solaris 11 (siempre que el BGF y se instalan GNU binutils (puede que tenga que cambiar el nombre objdumpa gobjdump).

En FreeBSD (al menos x86_64, no estoy seguro de lo que los primeros 24 bytes (que se convierten en 16 cuando GDB (8.0.1) es interactivo sugiere que puede haber un error en el BGF allí) en la pila son), reemplazan el argcy argvdefiniciones con:

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

(es posible que también necesite instalar el gdbpaquete / puerto ya que la versión que viene con el sistema es antigua).


Re (aquí se agregaron las comillas faltantes alrededor de la expansión del parámetro): ¿Qué hay de malo en no usar las comillas? ¿Existe realmente una diferencia?
yukashima huksay

@yukashimahuksay, vea, por ejemplo , las implicaciones de seguridad de olvidar citar una variable en shells bash / POSIX y las preguntas vinculadas allí.
Stéphane Chazelas

3

Lo que puedes hacer es

 export SECRET=somesecretstuff

luego, suponiendo que está escribiendo su ./programen C (o alguien más lo hace, y puede cambiarlo o mejorarlo por usted), use getenv (3) en ese programa, tal vez como

char* secret= getenv("SECRET");

y después de export que acaba de correr ./programen el mismo shell. O se le puede pasar el nombre de la variable de entorno (ejecutando ./program --secret-var=SECRETetc ...)

psno contará sobre su secreto, pero proc (5) aún puede proporcionar mucha información (al menos a otros procesos del mismo usuario).

Vea también esto para ayudar a diseñar una mejor manera de pasar los argumentos del programa.

Consulte esta respuesta para obtener una mejor explicación sobre el globbing y el papel de un shell.

Quizás programtenga otras formas de obtener datos (o utilizar la comunicación entre procesos de manera más inteligente) que los argumentos simples del programa (ciertamente debería hacerlo, si está destinado a procesar información confidencial). Lee su documentación. O tal vez está abusando de ese programa (que no está destinado a procesar datos secretos).

Ocultar datos secretos es realmente difícil. No pasarlo por los argumentos del programa no es suficiente.


55
De la pregunta queda bastante claro que ni siquiera tiene el código fuente./program , por lo que la primera mitad de esta respuesta no parece ser relevante.
tubería
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.