O GDB, acrônimo de GNU Debugger, é uma ferramenta essencial para debugging que permite inspecionar e controlar a execução de programas em tempo real. Esta ferramenta desempenhou um papel fundamental no ecossistema de desenvolvimento de software livre, viabilizando a análise detalhada de programas escritos em linguagens de baixo nível, como Assembly, bem como em linguagens amplamente utilizadas, como C e Rust.

Essa ferramenta desempenha um papel crucial no campo da cibersegurança. Um de seus principais usos inclui a engenharia reversa de binários, a identificação de vulnerabilidades no código e o desenvolvimento ou mitigação de exploits.

A capacidade do GDB de modificar o fluxo de execução de um programa e manipular seu estado em tempo real faz dele uma ferramenta indispensável para a pesquisa de malware e a avaliação da segurança de software. Além disso, sua integração com outras ferramentas de análise de segurança e seu suporte a scripts permitem que muitas tarefas repetitivas sejam automatizadas, aumentando a eficiência da análise de segurança.

Outro aspecto fundamental do GDB é sua portabilidade: embora seja uma ferramenta nativa e pré-instalada para sistemas operacionais Linux, também pode ser usada em sistemas Windows e MacOS.

Embora o GDB não tenha uma interface gráfica, existem vários plug-ins e complementos que facilitam a visualização das informações, como o GDBgui ou Data Display Debugger. Ao longo deste post, usaremos o GDB Dashboard, uma variação do arquivo de configuração inicial do GDB que colore e categoriza as informações fornecidas pela ferramenta.

Execução de um programa

Primeiramente, para iniciar a ferramenta, executamos o comando gdb no terminal, localizado no diretório onde se encontra o arquivo executável a ser analisado. Em seguida, utilizamos o comando run, acompanhado dos argumentos que o programa deve receber, se for o caso.

Suponha que você tenha um executável no Linux chamado "suma" e deseje observar sua operação. Para isso, inicializamos o GDB e executamos o programa passando dois números como argumentos:

DBD-linux-debugging-herramientas-1

Como esse programa não tem interrupções ou esperas, podemos ver a conclusão de sua execução na própria ferramenta.

DBD-linux-debugging-herramientas-2

Até agora, executamos com sucesso o programa "suma". No entanto, o ponto mais interessante da ferramenta é a capacidade de observar o programa com sua execução interrompida em alguma instrução ou função.

E se o nosso programa não tiver expressões que causem uma interrupção temporária, como a espera pela entrada do usuário, ou se quisermos interromper a execução em funções específicas, como podemos fazer isso no GDB?

Breakpoints e watchpoints

Em qualquer debugger , os breakpoints e watchpointssão peças essenciais para que você observe o conteúdo de um programa, entenda a lógica, encontre funções vulneráveis conhecidas e muito mais.

Especificamente, os breakpoints são pontos de interrupção definidos no código-fonte. Quando a execução do programa atinge um ponto de breakpoint, o programa é interrompido, permitindo que a ferramenta inspecione o estado atual do programa, como o valor das variáveis, o fluxo de controle e o conteúdo da memória.

Os watchpoints, por outro lado, concentram-se na observação de alterações nos valores de variáveis específicas. Um watchpoint interrompe a execução do programa sempre que o valor de uma variável observada é alterado, independentemente de onde essa alteração ocorre no código. Eles permitem que você monitore o estado de variáveis específicas, o que é crucial para detectar e entender como e quando os valores dessas variáveis mudam durante a execução do programa. Isso é particularmente útil para encontrar erros que envolvam corrupção de memória ou valores inesperados.

Para complementar o uso de breakpoints e watchpoints, o GDB fornece comandos de navegação e controle, como step, next ou continue. Vamos falar mais sobre eles no próximo ponto deste artigo.

Continuando com nosso exemplo do executável suma, colocaremos um breakpoint na função principal com o comando break main. Se iniciarmos a execução do GDB, poderemos ver o estado do programa de forma abrangente. Nesse caso, podemos ver tanto a desmontagem do executável quanto os valores correspondentes à memória, como os registros:

DBD-linux-debugging-herramientas-3

Assim como seu código-fonte, a linha que será executada em seguida, variáveis e muito mais:

DBD-linux-debugging-herramientas-4

Alterando o fluxo do programa

Para aproveitar ao máximo as interrupções colocadas em execução, o GDB fornece vários comandos para controlar a execução do programa em detalhes. A seguir, definiremos as principais funções: step, next, continue, finish.

O comando step permite avançar na execução do programa uma linha de código por vez, entrando em qualquer função chamada nessa linha. Esse comando é extremamente útil para examinar detalhadamente o comportamento das funções e acompanhar o fluxo de execução passo a passo.

O comando next também avança a execução do programa uma linha de cada vez, mas, ao contrário do step, não entra nas funções chamadas nessa linha. Em vez disso, ele executa a função inteira e para na próxima linha do código principal, o que acelera o processo de debugging se estivermos analisando seções menos críticas do código.

Em outra categoria, temos os comandos continue e finish. O comando continue retoma a execução do programa até que o próximo breakpoint ou watchpoint seja alcançado, ou até que o programa termine. Já o comando finish permite continuar a execução até que a função atual seja concluída, retornando ao contexto e à linha de código onde a função foi originalmente chamada.

Em nosso exemplo anterior, e executando em seguida para deixar passar algumas linhas, chegamos à chamada de uma função que pode ser de interesse.

DBD-linux-debugging-herramientas-5

E, ao acessar a função com o comando step, podemos observar como é feito o tratamento da entrada dos números a serem somados. Neste caso, é utilizada a função atoi em C, o que é interessante, pois não é uma escolha segura por parte do desenvolvedor.

DBD-linux-debugging-herramientas-6

Interagindo com a memória

Manipular e visualizar a memória observada por um programa também é um dos pontos fortes do GDB. Ele fornece vários comandos que permitem que você modifique variáveis e valores de retorno, visualize a pilha e o conteúdo de endereços de memória específicos. Vamos dar uma olhada em alguns dos comandos mais comumente usados.

Para alterar o valor de uma variável, utiliza-se o comando set. Esse comando permite atribuir um novo valor a uma variável específica durante a execução do programa, facilitando a testagem de diferentes cenários sem a necessidade de modificar o código-fonte.

O GDB também permite modificar o valor de retorno de uma função antes que ela termine sua execução. Isso é útil para testar como o programa lida com diferentes resultados de função sem precisar modificar o código. Por exemplo, o comando return 42 forçará a função atual a retornar 42, independentemente do cálculo que estava sendo realizado. Esse recurso é particularmente útil ao analisar funções críticas cujos resultados afetam significativamente o fluxo do programa.

Também dispomos de comandos para observar a pilha (ou stack), como o backtrace. Esse comando exibe a pilha de chamadas atual, detalhando todas as funções que foram chamadas até o ponto de interrupção.

Voltando ao nosso executável e lembrando que encontramos uma função que pode causar falhas no programa que estávamos analisando, podemos modificar a variável de entrada para verificá-la:

DBD-linux-debugging-herramientas-7

E, assim, forçar um erro de overflow aritmético no programa, descrito como uma Segmentation Fault.

DBD-linux-debugging-herramientas-8

O GDB também oferece alguns comandos mais avançados, úteis principalmente para a análise de binários. Um deles é o comando x, que permite examinar o conteúdo da memória em um endereço específico. Esse comando é altamente configurável e permite visualizar a memória em diferentes formatos. Por exemplo, o comando x/4xw 0x7fffffffe000 exibe quatro palavras (cada palavra com 4 bytes) em formato hexadecimal, a partir do endereço de memória 0x7fffffffe000.

Conclusões

O GDB é uma ferramenta extremamente poderosa e versátil, amplamente utilizada tanto na cibersegurança quanto na programação: desde a análise de malware, passando pela exploração de vulnerabilidades, até a correção de um programa com erros.

As funcionalidades vão muito além das básicas mencionadas nesta postagem. Entre elas, estão a debugging remota, a inspeção de estruturas de dados complexas, a análise de código de montagem e muito mais.

Apesar de seu poder, essa ferramenta pode ser intimidadora para iniciantes devido à grande quantidade de comandos e opções disponíveis, além do manuseio pelo terminal. Embora seja necessário tempo e prática para aprender a usá-la de forma eficaz, recomendamos utilizar as chamadas "cheat sheets" (folhas de referência) para uma rápida consulta à sintaxe do programa.