Como são registradas as poluições de canários de pilha?

11

O sinalizador GCC -fstack-protector permite o uso de canários de pilha para proteção contra estouro de pilha. O uso desse sinalizador por padrão tem sido mais proeminente nos últimos anos.

Se um pacote for compilado com -fstack-protector e sobrecarregarmos um buffer no programa, é provável que ocorram erros como:

*** buffer overflow detected ***: /xxx/xxx terminated

No entanto, "quem" é responsável por essas mensagens de erro? Onde essas mensagens são registradas? O daemon syslog seleciona essas mensagens?

aedcv
fonte

Respostas:

10

O esmagamento de pilha é detectado por libssp, do qual faz parte gcc. Ele tenta muito enviar a mensagem para um terminal e, somente se isso falhar, ele registra no log do sistema - na prática, você verá mensagens de buffer overflow nos logs para daemons e talvez aplicativos da GUI.

Depois de enviar sua mensagem, libssptenta várias maneiras de sair, incluindo travar o aplicativo; isso pode ser detectado por um dos registradores de saída anormais, mas isso não é garantido.

Stephen Kitt
fonte
1
Deixe-me dar um exemplo concreto como uma maneira de explorar mais essa explicação. Vamos escolher nginx para este exemplo. Eu compilei o nginx com canários de pilha. Quando executo o nginx, ele inicia um processo, mas não gera nada para o shell. Em vez disso, todas as mensagens são registradas em seus vários arquivos de log. Se o nginx detectar o esmagamento da pilha, ele libsspemitirá sua mensagem pela saída stderr usada pelo nginx. Em seguida, libssptente sair do processo (ou processo filho do nginx). Se "não precisar" travar o aplicativo, os registradores de saída anormais não serão atendidos. Essa é uma interpretação correta?
aedcv
Não é bem assim - ele vai tentar travar o aplicativo, usando __builtin_trap()em primeiro lugar, em seguida, se isso falhar, tentando provocar uma violação de segmento, e somente se isso falhar, sair com status de 127.
Stephen Kitt
A parte de impressão de mensagens não oferece melhores garantias de sucesso do que a saída por meio de um método de produção de núcleo (por exemplo abort()).
maxschlepzig
7

Distribuições modernas do Linux como o CentOS / Fedora configuram um daemon de tratamento de falhas (por exemplo, systemd-coredumpou abortd), por padrão.

Portanto, quando seu programa termina de forma anormal (segfault, exceção não capturada, abortamento, instrução ilegal etc.), esse evento é registrado e registrado por esse daemon. Assim, você encontra algumas mensagens no diário do sistema e, possivelmente, uma referência a um diretório com alguns detalhes adicionais (por exemplo, arquivo principal, logs, etc.).

Exemplo

$ cat test_stack_protector.c 
#include <string.h>

int f(const char *q)
{
  char s[10];
  strcpy(s, q);
  return s[0] + s[1];
}

int main(int argc, char **argv)
{
  return f(argv[1]);
}

Compilar:

$ gcc -Wall -fstack-protector test_stack_protector.c -o test_stack_protector

Executar:

$ ./test_stack_protector 'hello world'
*** stack smashing detected ***: ./test_stack_protector terminated
======= Backtrace: =========
/lib64/libc.so.6(+0x7c8dc)[0x7f885b4388dc]
/lib64/libc.so.6(__fortify_fail+0x37)[0x7f885b4dfaa7]
/lib64/libc.so.6(__fortify_fail+0x0)[0x7f885b4dfa70]
./test_stack_protector[0x400599]
./test_stack_protector[0x4005bd]
/lib64/libc.so.6(__libc_start_main+0xea)[0x7f885b3dc50a]
./test_stack_protector[0x40049a]
======= Memory map: ========
00400000-00401000 r-xp 00000000 00:28 1151979                            /home/juser/program/stackprotect/test_stack_protector
00600000-00601000 r--p 00000000 00:28 1151979                            /home/juser/program/stackprotect/test_stack_protector
00601000-00602000 rw-p 00001000 00:28 1151979                            /home/juser/program/stackprotect/test_stack_protector
0067c000-0069d000 rw-p 00000000 00:00 0                                  [heap]
7f885b1a5000-7f885b1bb000 r-xp 00000000 00:28 1052100                    /usr/lib64/libgcc_s-7-20170915.so.1
7f885b1bb000-7f885b3ba000 ---p 00016000 00:28 1052100                    /usr/lib64/libgcc_s-7-20170915.so.1
7f885b3ba000-7f885b3bb000 r--p 00015000 00:28 1052100                    /usr/lib64/libgcc_s-7-20170915.so.1
7f885b3bb000-7f885b3bc000 rw-p 00016000 00:28 1052100                    /usr/lib64/libgcc_s-7-20170915.so.1
7f885b3bc000-7f885b583000 r-xp 00000000 00:28 945348                     /usr/lib64/libc-2.25.so
7f885b583000-7f885b783000 ---p 001c7000 00:28 945348                     /usr/lib64/libc-2.25.so
7f885b783000-7f885b787000 r--p 001c7000 00:28 945348                     /usr/lib64/libc-2.25.so
7f885b787000-7f885b789000 rw-p 001cb000 00:28 945348                     /usr/lib64/libc-2.25.so
7f885b789000-7f885b78d000 rw-p 00000000 00:00 0 
7f885b78d000-7f885b7b4000 r-xp 00000000 00:28 945341                     /usr/lib64/ld-2.25.so
7f885b978000-7f885b97b000 rw-p 00000000 00:00 0 
7f885b9b0000-7f885b9b3000 rw-p 00000000 00:00 0 
7f885b9b3000-7f885b9b4000 r--p 00026000 00:28 945341                     /usr/lib64/ld-2.25.so
7f885b9b4000-7f885b9b6000 rw-p 00027000 00:28 945341                     /usr/lib64/ld-2.25.so
7ffc59966000-7ffc59987000 rw-p 00000000 00:00 0                          [stack]
7ffc5999c000-7ffc5999f000 r--p 00000000 00:00 0                          [vvar]
7ffc5999f000-7ffc599a1000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]
zsh: abort (core dumped)  ./test_stack_protector 'hello world'

O status de saída é 134, que é 128 + 6, ou seja, 128 mais o número do sinal de cancelamento.

O diário do sistema:

Oct 16 20:57:59 example.org audit[17645]: ANOM_ABEND auid=1000 uid=1000 gid=1000 ses=3 subj=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 pid=17645 comm="test_stack_prot" exe="/home/juser/program/stackprotect/test_stack_protector" sig=6 res=1
Oct 16 20:57:59 example.org systemd[1]: Started Process Core Dump (PID 17646/UID 0).
Oct 16 20:57:59 example.org audit[1]: SERVICE_START pid=1 uid=0 auid=4294967295 ses=4294967295 subj=system_u:system_r:init_t:s0 msg='unit=systemd-coredump@21-17646-0 comm="systemd" exe="/usr/lib/systemd/systemd" hostname=? addr=? terminal=? res=success'
Oct 16 20:57:59 example.org systemd-coredump[17647]: Process 17645 (test_stack_prot) of user 1000 dumped core.

                           Stack trace of thread 17645:
                           #0  0x00007f885b3f269b raise (libc.so.6)
                           #1  0x00007f885b3f44a0 abort (libc.so.6)
                           #2  0x00007f885b4388e1 __libc_message (libc.so.6)
                           #3  0x00007f885b4dfaa7 __fortify_fail (libc.so.6)
                           #4  0x00007f885b4dfa70 __stack_chk_fail (libc.so.6)
                           #5  0x0000000000400599 f (test_stack_protector)
                           #6  0x00000000004005bd main (test_stack_protector)
                           #7  0x00007f885b3dc50a __libc_start_main (libc.so.6)
                           #8  0x000000000040049a _start (test_stack_protector)
Oct 16 20:57:59 example.org audit[1]: SERVICE_STOP pid=1 uid=0 auid=4294967295 ses=4294967295 subj=system_u:system_r:init_t:s0 msg='unit=systemd-coredump@21-17646-0 comm="systemd" exe="/usr/lib/systemd/systemd" hostname=? addr=? terminal=? res=success'
Oct 16 20:58:00 example.org abrt-notification[17696]: Process 17645 (test_stack_protector) crashed in __fortify_fail()

Isso significa que você obtém o log do auditddaemon de auditoria e do systemd-coredumpmanipulador de falhas.

Para verificar se um daemon de manipulação de falhas está configurado, você pode verificar /proc, por exemplo:

$ cat /proc/sys/kernel/core_pattern
|/usr/lib/systemd/systemd-coredump %P %u %g %s %t %c %e

(tudo testado no Fedora 26, x86-64)

maxschlepzig
fonte
1
Estou muito feliz que você postou este exemplo. Os canários são colocados no lugar por gcc. (Corrija-me se estiver errado) Suponho que o que acontece seja algo como: gcc coloca "código extra" no programa para implementar a funcionalidade do canário; durante a execução e antes do retorno de uma função, o valor é verificado; se poluído, o programa emitirá a mensagem "pilha destruída detectada" e gera um erro. Esse erro é detectado pelo sistema operacional, reconhece uma falha de segmentação e imprime o backtrace e o mapa de memória que você postou. Por último, OS mata a aplicação, gera um dump de memória, e os registros para revista sys
aedcv
@ aedcv, essa é praticamente a história - para ser mais preciso: a pilha quebrando as chamadas de código de verificação abort()que produz um sinal de abortamento, ou seja, não há nenhuma falha de segmentação em andamento. É só que os manipuladores de sinal padrão para falha de aborto / segmentação etc. produzem a mesma ação: escreva o núcleo e saia do processo com um status de saída igual a zero que também codifica o número do sinal. A escrita principal é feita pelo kernel e seu comportamento é configurável via /proc/.../core_pattern. No exemplo acima, um auxiliar de espaço do usuário é configurado e, portanto, chamado. O kernel também aciona a auditoria.
maxschlepzig
@maxschlepzig não é bem abort(), o código SSP usa __builtin_trap()(mas o efeito é o mesmo).
Stephen Kitt
1
@StephenKitt, bem, dê uma olhada no rastreamento de pilha no exemplo acima. Lá você vê claramente como abort()é chamado.
maxschlepzig
1
@maxschlepzig sim, é claro, mas esse é um detalhe de implementação (o código GCC usa __builtin_trap()para evitar ter uma dependência explícita abort()). Outras distribuições têm diferentes rastreamentos de pilha.
Stephen Kitt