Identificando problemas de acesso à memória com o AddressSanitizer
- por Sergio Prado
AddressSanitizer (ASan) é uma ferramenta de instrumentação criada pelos pesquisadores de segurança do Google para identificar problemas de acesso à memória em programas C/C++.
De estouros de buffer a vazamentos de memória, identificar problemas de acesso à memória em programas C/C++ é extremamente complicado. É por este motivo que estou sempre escrevendo artigos sobre o tema no blog.
Uma outra opção para combater este problema é a funcionalidade de AddressSanitizer, disponível nos dois principais compiladores de software livre da atualidade, o GCC e o Clang.
Implementada através de flags de compilação, esta ferramenta instrumenta o código-fonte para identificar diversos problemas de acesso à memória, incluindo o uso de memória desalocada, estouros de buffer no stack e no heap, acesso à memória não inicializada e vazamentos de memória.
Por exemplo, o programa em C abaixo tem um problema de acesso à memória. Você consegue identificar?
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#include <stdio.h> #include <string.h> int main(int argc, const char *argv[]) { char msg[6]; strcpy(msg, "Hello!"); printf("%s\n", msg); return 0; } |
O fato é que este programa compila e roda normalmente:
$ gcc main.c -o main $ ./main Hello! |
Mas ele tem um bug de acesso à memória e o AddressSanitizer é capaz de identificá-lo. Para compilar a aplicação com o AddressSanitizer, basta passar o parâmetro -fsanitize=address para o compilador:
$ gcc main.c -fsanitize=address -o main |
Ao executar a aplicação, o erro de acesso à memória será identificado e um dump deste erro será exibido pela ferramenta:
$ ./main ================================================================= ==25697== ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7ffc7c241896 at pc 0x4009e1 bp 0x7ffc7c241850 sp 0x7ffc7c241848 WRITE of size 1 at 0x7ffc7c241896 thread T0 #0 0x4009e0 (/tmp/main+0x4009e0) #1 0x7feecc8bef44 (/lib/x86_64-linux-gnu/libc-2.19.so+0x21f44) #2 0x4007f8 (/tmp/main+0x4007f8) Address 0x7ffc7c241896 is located at offset 38 in frame of T0's stack: This frame has 1 object(s): [32, 38) 'msg' HINT: this may be a false positive if your program uses some custom stack unwind mechanism or swapcontext (longjmp and C++ exceptions *are* supported) Shadow bytes around the buggy address: 0x10000f8402c0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x10000f8402d0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x10000f8402e0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x10000f8402f0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x10000f840300: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 f1 f1 =>0x10000f840310: f1 f1[06]f4 f4 f4 00 00 00 00 00 00 00 00 00 00 0x10000f840320: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x10000f840330: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x10000f840340: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x10000f840350: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x10000f840360: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 Shadow byte legend (one shadow byte represents 8 application bytes): Addressable: 00 Partially addressable: 01 02 03 04 05 06 07 Heap left redzone: fa Heap righ redzone: fb Freed Heap region: fd Stack left redzone: f1 Stack mid redzone: f2 Stack right redzone: f3 Stack partial redzone: f4 Stack after return: f5 Stack use after scope: f8 Global redzone: f9 Global init order: f6 Poisoned by user: f7 ASan internal: fe ==25697== ABORTING |
Conseguiu achar o erro? A função strcpy() está escrevendo 7 caracteres (a mensagem “hello!” mais o caractere de fim de string) em um buffer de 6 posições!
Neste outro exemplo temos um problema de vazamento de memória:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#include <stdio.h> #include <stdlib.h> void alloc() { char *ptr = malloc(10); } int main(int argc, const char *argv[]) { int i; for (i = 0; i < 3; i++) alloc(); return 0; } |
O problema é identificado rapidamente quando compilamos com o AddressSanitizer e executamos a aplicação. Para fazer a checagem de vazamento de memória, é necessário definir a variável de ambiente ASAN_OPTIONS com o valor detect_leaks=1:
$ clang main.c -fsanitize=address -g -o main $ ASAN_OPTIONS=detect_leaks=1 ./main ==20677==WARNING: Trying to symbolize code, but external symbolizer is not initialized! ================================================================= ==20677==ERROR: LeakSanitizer: detected memory leaks Direct leak of 30 byte(s) in 3 object(s) allocated from: #0 0x465319 (/tmp/main+0x465319) #1 0x47b588 (/tmp/main+0x47b588) #2 0x47b8c7 (/tmp/main+0x47b8c7) #3 0x7f5e28457f44 (/lib/x86_64-linux-gnu/libc.so.6+0x21f44) SUMMARY: AddressSanitizer: 30 byte(s) leaked in 3 allocation(s). |
E como esta ferramenta funciona? A implementação é simples. Basicamente, cada acesso à memória é verificado antes de ser realizado. E o segredo está na forma como ela foi implementada, para garantir uma performance aceitável. Para os mais curiosos, no site do projeto tem uma explicação bem didática sobre o algoritmo desta ferramenta.
Um outro ponto legal do AddressSanitizer é o fato de sua implementação ser realizada através de algumas bibliotecas, que são mantidas no repositório do LLVM e compartilhadas com o GCC. Este é um exemplo claro de que, nos últimos anos, existe uma colaboração crescente entre as comunidades do GCC e do Clang, os dois principais projetos de ferramentas de compilação de software livre da atualidade.
É importante ressaltar também que estas verificações de memória adicionam uma considerável sobrecarga de processamento à aplicação, e devem ser utilizadas apenas durante o desenvolvimento e testes. Segundo a documentação do projeto, o uso do AddressSanitizer pode diminuir o tempo de execução da aplicação em até 2x. O que não é ruim. Na verdade, isso é positivamente impressionante, levando em consideração que a ferramenta intercepta e verifica cada acesso à memória feito pela aplicação.
Além do AddressSanitizer, existem ainda outros sanitizers como o ThreadSanitizer, capaz de identificar problemas de concorrência (race conditions e deadlocks) e o MemorySanitizer, capaz de identificar uso de memória não inicializada. Todos eles estão disponíveis na página do projeto no GitHub.
O suporte ao AddressSanitizer existe no Clang desde a versão 3.1 e no GCC desde a versão 4.8. Se algum dos seus projetos utiliza o GCC ou o Clang, pare agora o que você está fazendo, habilite a flag -fsanitize=address e teste seu código. Você pode se surpreender com o resultado! :-)
Um abraço,
Sergio Prado