Dissecando programas

há 4 dias 6

Saudações.

Hoje vou ensinar como usar o strace e o ltrace, comandos que me ajudam a resolver problemas no Linux.

Esses dois comandos sempre aparecem nas minhas conversas quando pergunto a um programador como o software dele funciona e ele responde “é secreto”, minha resposta é “nada é secreto no computador de outra pessoa”.

Ao apresentar esses comandos às pessoas elas perdem rapidinho a ilusão de segurança que antes tinham licenciando ou criptografando códigos PHP, Javascript ou Python.

Com maestria nesses programas você encerra pra sempre o famoso problema “na minha máquina funciona”.

Pré-requisitos:

  • Sistema Linux, Alpine ou Debian;
  • Internet no servidor (sua VPS ou host);

1 – Sobre o strace

Para explicar o que o strace faz, preciso explicar antes como funciona a execução de programas no Linux.

Sistemas operacionais são frameworks de execução de programas, ele fica entre o hardware e os programas que você executa (processos).

Isso é necessário para compartilhar recursos, como CPU, memória, rede, disco, etc. Em vez do software acessar diretamente o circuito eletrônico, o sistema operacional (kernel Linux) impede esse acesso direto colocando os processos para rodarem em modo não privilegiado na CPU, isso faz com que a CPU não obedeça às instruções de acesso direto ao hardware quando o programa do usuário está rodando (violar isso resulta em “ilegal instruçtion error”.

Conceito:

  • User-Space: Todos os programas que rodam em uma CPU com acesso a instruções simples (acesso limitado);
  • Kernel-Space: Todos os programas que rodam dentro do espaço do kernel e podem acessar todas as instruções de hardware diretamente.

Na prática, nenhum programa em user-space pode acessar recursos de hardware sem pedir ao kernel por meio de canais de software entre ele e o kernel – as APIs.

É aqui que o strace entra. Ele ordena ao kernel que copie todos os pedidos e respostas envolvendo um processo e seus filhos (threads, forks) para o espaço do strace, que por sua vez exibirá na tela (ou em arquivos de log) tudo que está acontecendo.

Eu utilizo o strace todo dia, sempre que um programa para de funcionar ou apresenta comportamento estranho eu assisto tudo que ele está fazendo e quais chamadas de API deram erros, principalmente seus argumentos (caminho de arquivos, nomes de DNS, IPs e portas, etc).

Ainda não é possível acessar o que o software faz quando ele usa instruções normais, todavia, se ele tentar qualquer acesso a APIs do kernel, acesso a arquivos e I/O, acesso a rede, sinais entre processos, o kernel fará a fofoca ao strace.

2 – Sobre o ltrace

O ltrace atua um pouco diferente, enquanto o strace foca na espionagem entre o processo e o kernel, o ltrace foca na espionagem entre o processo e suas bibliotecas.

Programas escritos em qualquer linguagem acabam por serem compilados com “linkagem dinâmica”, ou seja, no arquivo principal fica o programa e em outros arquivos ficam as funções que esse programa usa.

Exemplo: o NGINX é um software de servidor HTTP, para criptografar dados ele usa as bibliotecas do OpenSSL (libssl.so e libcrypt.so), para comprimir dados ele utiliza o ZSTD (libzstd.so).

Se substituirmos qualquer arquivo de biblioteca por outro manipulado, infectado ou capaz de extrair dados entre chamadas e retornos, estariamos fazendo um ataque de “Shared Library Hijacking“. Isso requer conhecimento avançado e dá muito trabalho embora seja assustadoramente eficiente.

Não queremos ter esse trabalho, mas podemos nos valer da possibilidade para nos intrometermos entre o programa principal e as bibliotecas, capturando a comunicação para analise. Essa é a função do ltrace.

3 – Instalando e usando

Vamos instalar os dois comandos:

Shell (root)

# Atualizar o sistema base apt -y update; apt -y upgrade; # Instalar strace e ltrace apt -y install strace; apt -y install ltrace; # Ferramentas auxiliares: apt -y install sysvinit-utils; # comando 'pidof'

3.1 – Usando o strace

Existem duas formas de usar o strace:

  • Programas em execução: Basta descobrir o PID do processos. Se o processo roda dentro de container é recomendável descobrir o PID dele no host, o strace não roda bem em containers pois requer acesso privilegiado ao kernel;
  • Antes de iniciar o processo: Precedendo o comando do programa com o comando strace, ele fará a ativação do monitoramento no kernel antes de chamar o execve() que cria o processo.

Monitorando programas em execução:

Shell (root)

# Monitorar software "unbound" em execucao: # 1. Descobrir o pid do bundound pifof unbound; # 1257 < retornou o numero do processo # 2. Acionar strace para espionar as chamadas de API do kernel strace -p 1257; # Retorno: # strace: Process 1257 attached # epoll_wait(49, [{events=EPOLLIN, data=0x3}], 32, 226455) = 1 # recvfrom(3, "oo\1\0\0\1\0\0\0\0\0\0\6google\3com\0\0\1\0\1", ... # sin_addr=inet_addr("127.0.0.1")}, [128 => 16]) = 28 # epoll_wait(49, [], 32, 0) = 0 # socket(AF_INET, SOCK_DGRAM, IPPROTO_IP) = 58 # setsockopt(58, SOL_IP, IP_MTU_DISCOVER, [5], 4) = 0 # bind(58, {sa_family=AF_INET, sin_port=htons(55762),... # ou: strace -p $(pidof unbound);

Monitorando programas ao executá-los (saída resumida para exemplo):

Shell (root)

# Preceder o comando com strace strace curl "https://api.ipify.org?format=json"; # Retorno: # execve("/usr/bin/curl", ["curl", "https://api.ipify.org?format=jso"...], # mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) # access("/etc/ld.so.preload", R_OK) # openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) # openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libcurl.so.4", ... # read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\0\0\0\0\0\0\0\0"... # close(3) # openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libz.so.1", ... # ... # openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libresolv.so.2", ... # newfstatat(AT_FDCWD, "/etc/gnutls/config", ENOENT (No such file or directory) # ... # openat(AT_FDCWD, "/root/.curlrc", O_RDONLY) = -1 ENOENT # openat(AT_FDCWD, "/root/.config/curlrc", O_RDONLY) = -1 ENOENT # ... # getsockname(4, {sa_family=AF_INET, sin_port=htons(61054), # sin_addr=inet_addr("191.37.79.91")}, [128 => 16]) = 0 # ... # write(1, "{\"ip\":\"191.37.79.91\"}", 21{"ip":"191.37.79.91"}) = 21 # # close(3) # close(4) # exit_group(0) # +++ exited with 0 +++

Com esses dois exemplos básicos você ja sentirá o poder do strace.

Alguns argumentos devem ser usados para melhor aproveitamento da analise:

  • -f segue forks e sub-processos;
  • -ff segue forks e sub-processos separadamente;
  • -t exibe a hora, ex: “[pid 1222] 18:33:18“;
  • -tt exibe a hora precisa, ex: “[pid 1216] 18:33:42.613544“, meu favorito;
  • -ttt exibe o timestamp, ex: “[pid 1777] 1681079647.005505“, ideal para plotar capturas em timeline e calcular a latência entre chamadas e respostas;
  • -T exibe o tempo de execução no final da chamada;
  • -s 128 exibe apenas os primeiros 128 bytes dos parâmetros (argumentos, caontúdo de arquivos lidos e escritos);
  • -i ativa exibição de ponteiro das instruções;
  • -c exibe sumário estatístico (tempo, chamadas, erros);
  • -o /tmp/strace-01.log salva as capturas em arquivos;
  • -q não mostra mensagens de anexação/desanexação;
  • -v verbosidade máxima sem abreviações;
  • -e …=… filtrar as chamadas a monitorar, coloque o nome das chamadas separando-as por virgula sem espaço, exemplo:
    • -e trace=open,openat,close: monitora abertura e fechamento de file-descriptors (arquivos, sockets);
    • -e trace=connect,accept,listen,bind: monitora chamadas de rede;
    • -e trace=file: monitora todas as operações de arquivos;
    • -e trace=network: monitora todas as operações de rede;
    • -e read=3,5: rastreia file descriptos de leitura 3 e 5;
    • -e write=4: rastreia file descripto de escrita 4;
    • -e chdir,getcwd: rastreia operações de mudança de diretório corrente;
    • -e open,close: rastreia abertura e fechamento de file descriptos;
    • -e malloc,calloc,realloc,free: rastreia alocações de memória;

Exemplos rodando o comando “sleep 3”:

Shell (root)

# Analise completa: strace -tt -ff -T -s 128 sleep 3; # Analise de abertura e fechamento de arquivos: strace -tt -ff -T -s 128 -e trace=open,openat,close sleep 3; # Analise de uso de rede: strace -tt -ff -T -s 128 -e trace=connect,accept,listen,bind,getsockopt,setsockopt \ curl "https://api.ipify.org?format=json";

3.2 – Usando o ltrace

O ltrace é sem dúvida o mais assustador dos analisadores de processos, ele revela todos os segredos, strings, chaves, senhas, variáveis, atributos e o que o processo fizer uso.

Ele pode ser usado antes de rodar o processo ou em um processo já em execução (-p pid).

Exemplo:

Shell (root)

# Analisar chamadas de funcoes em bibliotecas do comando curl: ltrace curl "https://api.ipify.org?format=json"; # fcntl(0, 1, 0x7fff68812440, 0) # fcntl(1, 1, 0, 0x7f07a9872aa0) # fcntl(2, 1, 0, 0x7f07a9872aa0) # signal(SIGPIPE, 0x1) # malloc(1264) # curl_global_init(3, 1264, 0, 0x561607cf39a0) # curl_version_info(11, 0x7f07a9a3f6c0, 0, 76) # curl_strequal(0x5615eb6f8ee3, 0x7f07a9a3acb9, 0, 0x7f07a9a71aeb) = 0 # ... # strncmp("libssh2", "libssh2/1.11.1", 7) # setlocale(LC_ALL, "") # setlocale(LC_NUMERIC, "C") # strcmp("https://api.ipify.org?format=jso"..., "--disable") # curl_getenv(0x5615eb6f80bf, 1, 1, 45) # curl_getenv(0x5615eb6f80c9, 1, 1, 45) # open("/root/.curlrc", 0, 00) # open("/root/.config/curlrc", 0, 00) # geteuid() # ... # strncmp("url", "expand-", 7) # strcmp("url", "ntlm-wb") # strcmp("url", "retry-max-time") # strcmp("url", "tftp-blksize") # strcmp("url", "trace-config") # strcmp("url", "user") # strcmp("url", "upload-flags") # strcmp("url", "url-query") # strcmp("url", "url") # ... # strdup("https://api.ipify.org?format=jso"...) # strdup("curl/8.14.1") # ... # memcpy(0x561607d0e220, "https://api.ipify.org?format=jso"..., 33) # curl_easy_init(0, 0x561607d0c430, 1, 1) # ... # strcmp("CURLOPT_BUFFERSIZE", "CURLOPT_SSL_VERIFYPEER") # strcmp("CURLOPT_BUFFERSIZE", "CURLOPT_SSL_VERIFYHOST") # strcmp("CURLOPT_BUFFERSIZE", "CURLOPT_SSL_ENABLE_NPN") # ... # free(nil) # free(nil) # free(nil) # free(0x561607cf39a0) # +++ exited (status 0) +++

Argumentos e opções:

  • -l /lib/libc.so.6 rastreia somente funções dessa biblioteca;
    • Liste as bibliotecas com o comando ldd caminho_binario;
  • -L para não mostrar chamadas para bibliotecas padrão;
  • -f para rastrear processos filhos (forks);
  • -o /tmp/ltrace-01.log salva as capturas em arquivos;
  • -S para monitorar chamadas de sistema (strace);
  • -t exibe o timestamp das chamadas;
  • -n 2 indentar saída (aninhamento de chamadas);
  • -s 128 exibe apenas os primeiros 128 bytes dos parâmetros (argumentos, caontúdo de arquivos lidos e escritos);
  • -e …=… filtrar as chamadas a monitorar:
    • -e malloc+free monitora alocação de memória virtual;
    • -e “printf*” monitorar funções que comecem com “printf”;
    • x
    • x
    • -e trace=file: monitora todas as operações de arquivos;
    • -e trace=network: monitora todas as operações de rede;
    • -e read=3,5: rastreia file descriptos de leitura 3 e 5;
    • -e write=4: rastreia file descripto de escrita 4;
    • -e chdir,getcwd: rastreia operações de mudança de diretório corrente;
    • -e open,close: rastreia abertura e fechamento de file descriptos;
    • -e malloc,calloc,realloc,free: rastreia alocações de memória;

Mais exemplos:

Shell (root)

# Listar bibliotecas do binario: ldd $(which ls); # linux-vdso.so.1 (0x00007f289807c000) # libselinux.so.1 => /lib/x86_64-linux-gnu/libselinux.so.1 # libcap.so.2 => /lib/x86_64-linux-gnu/libcap.so.2 # libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 # libpcre2-8.so.0 => /lib/x86_64-linux-gnu/libpcre2-8.so.0 # /lib64/ld-linux-x86-64.so.2 # Rastrear alocações de memória apenas ltrace -e malloc,calloc,realloc,free ls /tmp; # Rastrear funções de string ltrace -e "str*" ls /tmp; # Sumário de chamadas de biblioteca ltrace -c ls /tmp; # ltrace + strace combinados ltrace -S -e malloc+write ls /tmp; # Rastrear apenas libc ltrace -l /lib/x86_64-linux-gnu/libc.so.6 ls /tmp;

4 – Outros programas

Existem outros programas (comandos) que você pode aprender os 1% que faltam:

  • sysdig e csysdig
  • perf
  • bpftrace
  • lsof
  • fatrace
  • opensnoop
  • pcstat
  • ss
  • tcpdump
  • netstat
  • valgrind
  • heaptrack
  • pmap
  • execsnoop

Existem softwares feitos por pessoas paranóicas que aprenderam essas ferramentas e criaram artifícios para contorná-las, isso é perda de tempo.

O nível mais profundo e indefensável é o uso de QEMU-KVM.

Ao criar uma maquina virtual que faça a extração direto na CPU e memória da VM é possível enxergar absolutamente TUDO que um software faz, mesmo que esse software seja um módulo do kernel:

  • Snapshot de Memória: Muito simples de realizar, quando o software está rodando na VM o dump da RAM é realizado para um arquivo binário, tudo que o software fez, desde senhas até chaves privadas são revelados;
  • QEMU e o GDB Interativo: Permite controlar a execução da máquina virtual em tempo real e assistir linha a linha as instruções no processador e cada bloco lido ou escrito na memória;
  • Memflow: Captura de tráfego entre a VM e a RAM em tempo real.

Bom… agora você já sabe como abrir as tripas de qualquer software em execução.

Terminamos por hoje!

Patrick Brandão, patrickbrandao@gmail.com

Quem tem medo não mama em onça
Ditado brasileiro