Dissecando programas

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