Ocultar argumentos para programar sem código fonte


15

Preciso ocultar alguns argumentos confidenciais para um programa que estou executando, mas não tenho acesso ao código-fonte. Também estou executando isso em um servidor compartilhado, portanto não posso usar algo como, hidepidporque não tenho privilégios de sudo.

Aqui estão algumas coisas que eu tentei:

  • export SECRET=[my arguments], seguido de uma chamada para ./program $SECRET, mas isso não parece ajudar.

  • ./program `cat secret.txt`onde secret.txtcontém meus argumentos, mas o todo ps- poderoso é capaz de farejar meus segredos.

Existe alguma outra maneira de ocultar meus argumentos que não envolva intervenção do administrador?


O que é esse programa em particular? Se for um comando de costume, você precisa dizer (e poderia haver alguma outra abordagem) qual é
Basile Starynkevitch

14
Para entender o que está acontecendo, as coisas que você tentou não têm chance de funcionar porque o shell é responsável por expandir as variáveis ​​de ambiente e por executar a substituição de comandos antes de chamar o programa. psnão está fazendo nada de mágico para "descobrir seus segredos". De qualquer forma, os programas razoavelmente escritos devem oferecer uma opção de linha de comando para ler um segredo de um arquivo especificado ou do stdin, em vez de levá-lo diretamente como argumento.
Jamesdlin 11/11

Estou executando um programa de simulação climática elaborado por uma empresa privada. Eles não compartilham seu código-fonte nem sua documentação fornece nenhuma maneira de compartilhar um segredo de um arquivo. Pode estar fora de opções aqui
MS

Respostas:


25

Conforme explicado aqui , o Linux coloca os argumentos de um programa no espaço de dados do programa e mantém um ponteiro para o início dessa área. É isso que é usado por pse assim por diante para encontrar e mostrar os argumentos do programa.

Como os dados estão no espaço do programa, eles podem ser manipulados. Fazer isso sem alterar o próprio programa envolve carregar um calço com uma main()função que será chamada antes do principal principal do programa. Esse calço pode copiar os argumentos reais para um novo espaço e, em seguida, substituir os argumentos originais, de forma que pseles apareçam nulos.

O código C a seguir faz isso.

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

Não é possível intervir main(), mas você pode intervir na função padrão da biblioteca C __libc_start_main, que passa a chamar main. Compile esse arquivo shim_main.cconforme observado no comentário no início e execute-o como mostrado. Deixei um printfno código para verificar se ele está sendo chamado. Por exemplo, execute

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

então faça ae psvocê verá um comando em branco e argumentos sendo mostrados.

Ainda há uma pequena quantidade de tempo que o comando args pode estar visível. Para evitar isso, você pode, por exemplo, alterar o calço para ler seu segredo de um arquivo e adicioná-lo aos argumentos passados ​​para o programa.


12
Mas ainda haverá uma pequena janela durante a qual /proc/pid/cmdlineo segredo será exibido (o mesmo que quando curltenta ocultar a senha fornecida na linha de comando). Enquanto estiver usando LD_PRELOAD, você pode agrupar main para que o segredo seja copiado do ambiente para o argumento que o main recebe. Como chamada LD_PRELOAD=x SECRET=y cmdonde você chamar main()com argv[]sendo[argv[0], getenv("SECRET")]
Stéphane Chazelas

Você não pode usar o ambiente para ocultar um segredo, pois ele é visível via /proc/pid/environ. Isso pode ser sobrescrito da mesma maneira que os argumentos, mas deixa a mesma janela.
meuh

11
/proc/pid/cmdlineé público, /proc/pid/environnão é. Havia alguns sistemas em que ps(um executável setuid) expunha o ambiente de qualquer processo, mas acho que você não se deparará hoje em dia. O ambiente é geralmente considerado suficientemente seguro . Não é seguro desviar de processos com o mesmo euid, mas eles geralmente podem ler a memória dos processos pelo mesmo euid de qualquer maneira, portanto, não há muito o que fazer sobre isso.
Stéphane Chazelas

4
@ StéphaneChazelas: Se alguém usa o ambiente para passar segredos, o ideal é que o wrapper que o encaminha para o mainmétodo do programa empacotado também remova a variável de ambiente para evitar vazamentos acidentais nos processos filhos. Como alternativa, o wrapper pode ler todos os argumentos da linha de comando de um arquivo.
David Foerster

@DavidFoerster, bom ponto. Atualizei minha resposta para levar isso em conta.
Stéphane Chazelas

16
  1. Leia a documentação da interface da linha de comandos do aplicativo em questão. Pode muito bem haver uma opção para fornecer o segredo a partir de um arquivo, e não como um argumento diretamente.

  2. Se isso falhar, envie um relatório de bug contra o aplicativo, alegando que não há uma maneira segura de fornecer um segredo a ele.

  3. Você sempre pode adaptar (!) Cuidadosamente a solução na resposta do meuh às suas necessidades específicas. Preste atenção especial ao comentário de Stéphane e seus acompanhamentos.


12

Se você precisar passar argumentos para o programa para fazê-lo funcionar, você ficará sem sorte, não importa o que faça, se não puder usar hidepidno procfs.

Como você mencionou que este é um script bash, você já deve ter o código fonte disponível, pois o bash não é um idioma compilado.

Caso contrário, você poderá reescrever o cmdline do processo usando gdbou similar e brincar com argc/ argvuma vez que ele já foi iniciado, mas:

  1. Isso não é seguro, pois você ainda expõe os argumentos do programa inicialmente antes de alterá-los
  2. Isso é muito hacky, mesmo se você conseguisse fazê-lo funcionar, eu não recomendaria confiar nele

Eu realmente recomendo obter o código fonte ou conversar com o fornecedor para modificar o código. O fornecimento de segredos na linha de comandos em um sistema operacional POSIX é incompatível com a operação segura.


11

Quando um processo executa um comando (por meio da execve()chamada do sistema), sua memória é limpa. Para passar algumas informações pela execução, as execve()chamadas do sistema usam dois argumentos para isso: the argv[]e envp[]matrizes.

Essas são duas matrizes de strings:

  • argv[] contém os argumentos
  • envp[]contém as definições de variáveis ​​de ambiente como cadeias de caracteres no var=valueformato (por convenção).

Quando você faz:

export SECRET=value; cmd "$SECRET"

(aqui foram adicionadas as aspas ausentes ao redor da expansão do parâmetro).

Você está executando cmdcom o secret ( value) passado em argv[]e envp[]. argv[]será ["cmd", "value"]e envp[]algo assim [..., "PATH=/bin:...", "HOME=...", ..., "SECRET=value", "TERM=xterm", ...]. Como cmdnão está fazendo nada getenv("SECRET")ou equivalente para recuperar o valor do segredo dessa SECRETvariável de ambiente, colocá-lo no ambiente não é útil.

argv[]é conhecimento público. Mostra na saída de ps. envp[]hoje em dia não é. No Linux, mostra em /proc/pid/environ. Ele é mostrado na saída de ps ewwwBSDs (e com procps-ng's psno Linux), mas apenas para processos em execução com o mesmo uid efetivo (e com mais restrições para executáveis ​​setuid / setgid). Pode aparecer em alguns logs de auditoria, mas esses logs de auditoria devem ser acessíveis apenas pelos administradores.

Em resumo, o ambiente que é passado para um executável deve ser privado ou pelo menos tão privado quanto a memória interna de um processo (que, em algumas circunstâncias, outro processo com os privilégios corretos também pode acessar com um depurador, por exemplo, e pode também ser despejado no disco).

Como argv[]é de conhecimento público, um comando que espera que os dados sigam em sua linha de comando é quebrado por design.

Geralmente, os comandos que precisam receber um segredo fornecem uma outra interface para isso, como por meio de uma variável de ambiente. Por exemplo:

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

Ou através de um descritor de arquivo dedicado como stdin:

echo secret | openssl rsa -passin stdin ...

( echosendo incorporado, ele não aparece na saída de ps)

Ou um arquivo, como o .netrcfor ftpe alguns outros comandos ou

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

Alguns aplicativos como curl(e essa também é a abordagem adotada por @meuh aqui ) tentam ocultar a senha que eles receberam argv[]de olhares indiscretos (em alguns sistemas, substituindo a parte da memória em que as argv[]strings estavam armazenadas). Mas isso realmente não está ajudando e oferece uma falsa promessa de segurança. Isso deixa uma janela entre a execve()e a substituição, onde psainda mostrará o segredo.

Por exemplo, se um invasor sabe que você está executando um script curl -u user:somesecret https://...(por exemplo em um trabalho cron), tudo o que ele precisa fazer é remover do cache as (muitas) bibliotecas que curlusam (por exemplo, executando a sh -c 'a=a;while :; do a=$a$a;done'), para para desacelerar sua inicialização e até mesmo fazer uma ineficiência until grep 'curl.*[-]u' /proc/*/cmdline; do :; doneé suficiente para capturar essa senha nos meus testes.

Se os argumentos forem a única maneira de transmitir o segredo aos comandos, ainda pode haver algumas coisas que você pode tentar.

Em alguns sistemas, incluindo versões mais antigas do Linux, apenas os primeiros bytes (4096 no Linux 4.1 e anterior) das cadeias argv[]podem ser consultados.

Lá, você pode fazer:

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

E o segredo seria oculto porque passou dos primeiros 4096 bytes. Agora, as pessoas que usaram esse método devem se arrepender agora, já que o Linux, desde o 4.2, não trunca mais a lista de argumentos /proc/pid/cmdline. Observe também que não é porque psnão mostrará mais do que muitos bytes de uma linha de comando (como no FreeBSD, onde parece estar limitado a 2048) que não se pode usar o mesmo uso da API pspara obter mais. Entretanto, essa abordagem é válida em sistemas onde psé a única maneira de um usuário comum recuperar essas informações (como quando a API é privilegiada e psé setgid ou setuid para usá-la), mas ainda não é potencialmente disponível para o futuro.

Outra abordagem seria não passar o segredo, argv[]mas injetar código no programa (usando gdbou $LD_PRELOADhackear) antes de main()iniciar, que insere o segredo no argv[]recebido execve().

Com LD_PRELOAD, para executáveis ​​dinamicamente não-setuid / setgid vinculados em um 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);
}

Então:

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

Em nenhum momento teria psmostrado o ps -opid,argslá ( -opid,argssendo o segredo neste exemplo). Observe que estamos substituindo elementos da argv[]matriz de ponteiros , não substituindo as seqüências apontadas por esses ponteiros, razão pela qual nossas modificações não aparecem na saída de ps.

With gdb, ainda para executáveis ​​dinamicamente não-setuid / setgid vinculados e em 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"

Ainda assim gdb, uma abordagem não-GNU específica, que não depende de executáveis ​​vinculados dinamicamente ou que possuam símbolos de depuração, deve funcionar para qualquer ELF executável no Linux, pelo menos, poderia 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

Testando com um executável vinculado estaticamente:

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

Quando o executável pode ser estático, não temos uma maneira confiável de alocar memória para armazenar o segredo; portanto, precisamos obtê-lo de outro lugar que já esteja na memória do processo. É por isso que o ambiente é a escolha óbvia aqui. Também ocultamos esse SECRETenv var ao processo (alterando-o para SECRE=) para evitar vazamentos se o processo decidir despejar seu ambiente por algum motivo ou executar aplicativos não confiáveis.

Isso também funciona no Solaris 11 (desde que os binutils gdb e GNU estejam instalados (pode ser necessário renomear objdumppara gobjdump).

No FreeBSD (pelo menos x86_64, eu não sei o que aqueles primeiros 24 bytes (que se tornam 16 quando gdb (8.0.1) é interativo sugerindo que pode haver um erro no gdb lá) na pilha são), substitua o argce argvdefinições com:

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

(você também pode precisar instalar o gdbpacote / porta, pois a versão que vem com o sistema é antiga).


Re (aqui foram adicionadas as aspas ausentes na expansão do parâmetro): O que há de errado em não usar as aspas? Existe realmente alguma diferença?
yukashima huksay

@yukashimahuksay, veja, por exemplo, implicações de segurança de se esquecer de citar uma variável nos shells bash / POSIX e as perguntas aí relacionadas.
Stéphane Chazelas

3

O que você pode fazer é

 export SECRET=somesecretstuff

então, supondo que você esteja escrevendo seu ./programem C (ou alguém o faça e possa alterá-lo ou melhorá-lo), use getenv (3) nesse programa, talvez como

char* secret= getenv("SECRET");

e depois que export você acabou de executar ./programno mesmo shell. Ou o nome da variável de ambiente pode ser passado para ele (executando ./program --secret-var=SECRETetc ...)

psnão revelará seu segredo, mas o proc (5) ainda pode fornecer muitas informações (pelo menos para outros processos do mesmo usuário).

Consulte também isso para ajudar a projetar uma maneira melhor de transmitir argumentos do programa.

Veja esta resposta para uma melhor explicação sobre globbing e o papel de uma concha.

Talvez você programtenha outras maneiras de obter dados (ou usar a comunicação entre processos com mais sabedoria) do que argumentos simples do programa (certamente deveria, se se pretende processar informações confidenciais). Leia sua documentação. Ou talvez você esteja abusando desse programa (que não se destina a processar dados secretos).

Ocultar dados secretos é realmente difícil. Não passar por argumentos do programa não é suficiente.


5
É muito claro a partir da pergunta que ele nem sequer tem o código-fonte para ./program, de modo que o primeiro semestre de esta resposta não parece ser relevante.
pipe
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.