Identificando problemas de acesso à memória com o AddressSanitizer

- por Sergio Prado

Categorias: Ferramentas, Linguagem C Tags: , ,

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
=&gt0x10000f840310: 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

  • Não conhecia esta ferramenta e nem a flag sanitize=address, ótimo artigo Sérgio parabéns!

  • Isto aqui acontece quando tento compilar o exemplo no meu gcc (7.3.0) :-)

    “`

    gcc -o hello hello.c
    test.c: In function ‘main’:
    test.c:8:5: warning: ‘__builtin_memcpy’ writing 7 bytes into a region of size 6 overflows the destination [-Wstringop-overflow ]
    strcpy(msg, “Hello!”);
    ^~~~~~~~~~~~~~~~~~~~~
    “`

    Anyway, impressionante a quantidade de bom ferramental que vem sendo lancado recentemente, incluindo várias coisas no llvm/clang, cppcheck, os fuzzers, etc. O mais difícil está sendo acompanhar e usar efetivamente tudo isto!

    • Aliás, antigamente eu usava o valgrind pra depurar este tipo de problema, bom que está virando mais “mainstream”.

      • É verdade.

        Mas uma coisa legal do Valgrind é que você não precisa recompilar a aplicação, nem mesmo ter acesso aos fontes (a não ser que precise gerar uma nova versão com simbolos de debugging).

        Então não acho que o ASAN substitui o Valgrind, mas sim que eles se complementam.

    • Olá Fabio!

      Se você compilar com a flag -fno-builtin o warning não deve aparecer.

      Possivelmente o compilador da sua máquina foi compilado com o ASAN habilitado, e como ele está usando uma função builtin (__builtin_memcpy) para fazer a cópia de buffer, acaba identificando o problema.

      Um abraço!

Navegue
Creative Commons Este trabalho de Sergio Prado é licenciado pelo
Creative Commons BY-NC-SA 3.0.