[LinuxFocus-icon]
Início  |  Mapa  |  Índice  |  Procura

Novidades | Arquivos | Links | Sobre LF
Este artigo está disponível em: English  Castellano  Deutsch  Francais  Nederlands  Portugues  Russian  Turkce  

convert to palmConvert to GutenPalm
or to PalmDoc

[image of the authors]
por Frédéric Raynal, Christophe Blaess, Christophe Grenier
<pappy(at)users.sourceforge.net, ccb(at)club-internet.fr, grenier(at)nef.esiea.fr>

Sobre o autor:

O Christophe Blaess é um engenheiro aeronáutico independente. Ele é um fã do Linux e faz muito do seu trabalho neste sistema. Coordena a tradução das páginas man publicadas no Projecto de Documentação do Linux.

O Christophe Grenier é um estudante no 5º ano na ESIEA, onde, também trabalha como administrador de sistema. Tem uma paixão por segurança de computadores.

O Frederic Raynal tem utilizado o Linux desde há alguns anos porque não polui, não usa hormonas, não usa MSG ou farinha animal ... reclama somente o suor e a astúcia.



Traduzido para Português por:
Bruno Sousa <bruno(at)linuxfocus.org>

Conteúdo:

 

Evitando falhas de segurança ao desenvolver uma aplicação - Parte 5: race conditions

[article illustration]

Abstrato:

Este quinto artigo da nossa série é dedicado a problemas de segurança relacionados com a multitarefa. As race condition ocorrem quando vários processos utilizam o mesmo recurso ao mesmo tempo (ficheiro, dispositivo, memória), de maneira a que cada um "acredita" que tem acesso exclusivo. Isto leva a bugs difíceis de detectar mas também a falhas de segurança reais capazes de comprometer a segurança global de um sistema.



 

Introdução

O princípio geral das condições race é o seguinte: um processo quer aceder a um recurso do sistema exclusivamente. Verifica se o recurso não está já a ser usado por outro processo, depois toma a sua posse e usa-o como quer. O problema aparece quando um outro processo tenta beneficiar do lapso de tempo entre a verificação e o verdadeiro acesso para tomar posse do mesmo recurso. Os resultados podem variar. O exemplo clássico na teoria dos S.O. é a verificação infinita de ambos os processos. Em casos mais práticos, isto conduz ao mau funcionamento das aplicações. ou a falhas de segurança quando um processo erradamente, beneficia dos privilégios de outro.

O que nós, previamente, chamamos de um recurso pode ter diferentes aspectos. A maioria dos problemas das race conditions por vezes descobertos e corrigidos no próprio kernel do Linux, assentam no acesso competitivo às áreas de memória. Aqui focaremos as aplicações de sistema e consideraremos que os recursos falados são nós do sistema de ficheiros. Isto não só diz respeito aos ficheiros comuns como também o acesso a dispositivos através de entradas especiais a partir do directório /dev/.

A maioria das vezes uma tentativa de ataque à segurança do sistema é feito contra as aplicações Set-UID, visto que o atacante pode correr o programa até que consiga beneficiar dos privilégios dados ao dono dos ficheiros executáveis. Contudo, diferentemente, das falhas de segurança já discutidas (o buffer overflow, formatação de strings...), as race conditions, normalmente, não nos permitem executar o código "personalizado". Dão somente a oportunidade de beneficiar dos recursos de um programa enquanto está a rodar. Este tipo de ataque também se aplica a utilitários "normais" ( e não só Set-UID), o pirata esperando escondido, à espera de outro utilizador, em especial o root, para correr a aplicação em questão de modo aceder aos seus recursos. Isto também é verdade para escrever para dentro de um ficheiro (por exemplo ~/.rhost no qual a string "+ +" fornece um acesso directo a partir de qualquer máquina sem pedir password) ou para ler um ficheiro confidencial (dados comerciais sensíveis, informação médica pessoal, ficheiro de passwords, chave privada...)

Diferentemente às falhas de segurança discutidas nos artigos anteriores este problema de segurança aplica-se a qualquer aplicação e não somente aos utilitários Set-UID e servidores de sistema ou demónios.

 

Primeiro Exemplo

Reparemos no comportamento de um programa Set-UID que tem de guardar os seus dados num ficheiro da pertença do utilizador. Podíamos, por exemplo, considerar o caso de um software transportador de mail como o sendmail. Suponhamos que o utilizador pode fornecer quer o nome do ficheiro de backup e uma mensagem para escrever dentro desse mesmo ficheiro, o que é plausível sobre determinadas circunstâncias. A aplicação deve depois verificar se o ficheiro pertence à pessoa a que iniciou o programa. Deve também verificar se não é um link para um ficheiro de sistema. Não esqueçamos que o programa ao ser Set-UID root, é - lhe permitido modificar qualquer ficheiro na máquina. Segundo isto o programa comparará o dono do ficheiro com o seu UID real. Escrevamos algo do género:

1     /* ex_01.c */
2     #include <stdio.h>
3     #include <stdlib.h>
4     #include <unistd.h>
5     #include <sys/stat.h>
6     #include <sys/types.h>
7
8     int
9     main (int argc, char * argv [])
10    {
11        struct stat st;
12        FILE * fp;
13
14        if (argc != 3) {
15            fprintf (stderr, "usage : %s file message\n", argv [0]);
16            exit(EXIT_FAILURE);
17        }
18        if (stat (argv [1], & st) < 0) {
19            fprintf (stderr, "can't find %s\n", argv [1]);
20            exit(EXIT_FAILURE);
21        }
22        if (st . st_uid != getuid ()) {
23            fprintf (stderr, "not the owner of %s \n", argv [1]);
24            exit(EXIT_FAILURE);
25        }
26        if (! S_ISREG (st . st_mode)) {
27            fprintf (stderr, "%s is not a normal file\n", argv[1]);
28            exit(EXIT_FAILURE);
29        }
30
31        if ((fp = fopen (argv [1], "w")) == NULL) {
32            fprintf (stderr, "Can't open\n");
33            exit(EXIT_FAILURE);
34        }
35        fprintf (fp, "%s\n", argv [2]);
36        fclose (fp);
37        fprintf (stderr, "Write Ok\n");
38        exit(EXIT_SUCCESS);
39    }

Como explicado no nosso primeiro artigo, seria melhor para uma aplicação Set-UID perder temporariamente os seus privilégios e abrir o ficheiro com o UID real do utilizador que o invocou. De facto a situação acima raramente corresponde à de um demónio que fornece serviços a qualquer utilizador. Sempre a correr com o ID do root, devia verificar o UID em vez do seu UID verdadeiro. Não obstante, manteremos este esquema, mesmo que não seja tão realístico, visto que permite entender o problema, enquanto "exploração" fácil da falha de segurança.

Como podemos ver, o programa começa por fazer todos os controles necessários, verificando se o ficheiro existe, se pertence ao utilizador e se é um ficheiro normal. De seguida abre, realmente, o ficheiro e escreve a mensagem. É aqui que assenta a falha de segurança. Ou mais exactamente no lapso de tempo entre a leitura dos atributos do ficheiro com o stat() e a sua abertura com o fopen(). Este lapso de tempo é geralmente muito curto mas não é nulo, então um atacante pode beneficiar disso para alterar as características do ficheiro. Para tornar o nosso ataque ainda mais fácil adicionemos uma linha que faça adormecer o processo entre as operações, tendo assim tempo necessário para o fazer à mão. Alteramos a linha 30 (previamente vazia) e inserimos:

30        sleep (20);

Implementêmo-lo, agora; mas façamos primeiro aplicação Set-UID root. Façamos de seguida, muito importante, um backup do nosso ficheiro de palavras passe /etc/shadow :

$ cc ex_01.c -Wall -o ex_01
$ su
Password:
# cp /etc/shadow /etc/shadow.bak
# chown root.root ex_01
# chmod +s ex_01
# exit
$ ls -l ex_01
-rwsrwsr-x 1 root  root    15454 Jan 30 14:14 ex_01
$

Está tudo pronto para o ataque. Estamos num directório da nossa pertença. Temos um utilitário Set-UID root (aqui ex_01) suportando uma falha de segurança e sentimo-nos como que a substituir a linha que diz respeito ao root no ficheiro /etc/shadow por uma linha contendo uma password vazia.

Primeiro, criamos um ficheiro fic que nos pertença:

$ rm -f fic
$ touch fic

De seguida, corremos a nossa aplicação em background "para manter a liderança". Pedimos-lhe para escrever uma string para dentro do ficheiro. Verificando o que contém, adormecendo por um pouco antes de realmente aceder ao ficheiro.

$ ./ex_01 fic "root::1:99999:::::" &
[1] 4426

O conteúdo da linha do root provém da página manual shadow(5), o mais importante é que o segundo campo esteja vazio (sem palavra passe). Enquanto o processo está adormecido temos, cerca de 20 segundos para remover o ficheiro fic e substituí-lo por um link (simbólico ou físico, ambos funcionam) ao ficheiro /etc/shadow. Lembremos que um utilizador pode criar um link para um ficheiro mesmo que não possa ler o seu conteúdo, num directório da sua pertença ( ou até mesmo em /tmp, como veremos mais tarde). contudo não é possível de criar uma cópia de tal ficheiro, visto que requeria um modo de leitura completa.

$ rm -f fic
$ ln -s /etc/shadow ./fic

Depois pedimos à shell para trazer o processo ex_01 para foreground com o comando fg, e esperamos que termine:

$ fg
./ex_01 fic "root::1:99999:::::"
Write Ok
$

Voilà ! Está terminado, o ficheiro /etc/shadow só contém uma linha indicando que o root não tem password. Não acredita ?

$ su
# whoami
root
# cat /etc/shadow
root::1:99999:::::
#

Terminemos a nossa experiência, repondo o velho ficheiro de passwords.

# cp /etc/shadow.bak /etc/shadow
cp: replace `/etc/shadow'? y
#
 

Sejamos mais realistas

Tivemos sucesso ao explorar uma condição num utilitário Set-UID root. Claro que este programa era muito "amigável" esperando 20 segundos, dando-nos tempo necessário de modificar os ficheiros nas suas costas. Mesmo numa aplicação real, a race condition só se aplica durante pequenos lapsos de tempo. Como beneficiar disto ?

Normalmente o princípio de ataque assenta num ataque brutal, renovando as tentativas de ataque, cem, mil ou dez mil vezes utilizando scripts para automatizar a sequência. É possível melhorar a chance de "descobrir" a falha de segurança com vários truques com vista a aumentar o lapso de tempo entre as duas operações que o programa, erradamente, considera como atomicamente ligadas. A ideia é abrandar o processo de destino para se administrar mais facilmente o atraso que precede a modificação do ficheiro. Diferentes pontos de vista podem ser arquitectados para alcançar o nosso objectivo:

O método que nos permite beneficiar de uma falha de segurança baseado em race conditions é por isso aborrecido e repetitivo, mas é realmente útil ! Tentemos descobrir soluções efectivas.

 

Possíveis Melhoramentos

O problema acima discutido assenta na habilidade de alterar o conteúdo de um objecto durante o lapso de tempo entre as duas operações respeitantes ao objecto, sendo uma coisa contínua tanto quanto possível. Na situação anterior, a modificação não dizia respeito ao ficheiro em si. Além disso, sendo um utilizador normal teria sido um pouco difícil, modificar ou até mesmo ler o ficheiro /etc/shadow. De facto, a alteração assenta na ligação entre o nó do ficheiro na árvore nomeada e o ficheiro em si mesmo como uma entidade física. Lembremos que a maioria dos comandos do sistema (rm, mv, ln, etc.) actuam sobre o nome do ficheiro e não no seu conteúdo. Mesmo quando se apaga um ficheiro (utilizando o rm e a chamada de sistema unlink()) o conteúdo é realmente apagado quando a última ligação física - última referência - é removida.

O erro feito no programa anterior é por isso, considerar a associação entre o nome do ficheiro e o seu conteúdo como inalterável, ou pelo menos constante durante o lapso de tempo entre a operação stat() e fopen(). Então, é suficiente aplicar um exemplo de uma ligação física para verificar esta associação não é de todo permanente. Vejamos um exemplo utilizando este tipo de ligação. Num directório da nossa pertença criamos um novo link para um ficheiro de sistema. Claro que o dono e o modo de acesso são mantidos. O comando ln com a opção -f força a criação mesmo que o dono já exista:

$ ln -f /etc/fstab ./myfile
$ ls -il /etc/fstab myfile
8570 -rw-r--r--   2 root  root  716 Jan 25 19:07 /etc/fstab
8570 -rw-r--r--   2 root  root  716 Jan 25 19:07 myfile
$ cat myfile
/dev/hda5   /                 ext2    defaults,mand   1 1
/dev/hda6   swap              swap    defaults        0 0
/dev/fd0    /mnt/floppy       vfat    noauto,user     0 0
/dev/hdc    /mnt/cdrom        iso9660 noauto,ro,user  0 0
/dev/hda1   /mnt/dos          vfat    noauto,user     0 0
/dev/hda7   /mnt/audio        vfat    noauto,user     0 0
/dev/hda8   /home/ccb/annexe  ext2    noauto,user     0 0
none        /dev/pts          devpts  gid=5,mode=620  0 0
none        /proc             proc    defaults        0 0
$ ln -f /etc/host.conf ./myfile
$ ls -il /etc/host.conf myfile 
8198 -rw-r--r--   2 root  root   26 Mar 11  2000 /etc/host.conf
8198 -rw-r--r--   2 root  root   26 Mar 11  2000 myfile
$ cat myfile
order hosts,bind
multi on
$

A opção /bin/ls -i apresenta o número do nó no inicio da linha. Então podemos ver que o mesmo nome aponta para dois nodos físicos diferentes.

De facto, gostaríamos que as funções usadas para verificar e aceder ao ficheiro, apontassem para o mesmo conteúdo e para o mesmo nodo. E é possível ! O kernel por si mesmo controla esta associação automaticamente, quando nos fornece um descritor de ficheiro. Quando abrimos um ficheiro para leitura, a chamada ao sistema open() retorna um valor inteiro que é o descritor, associando-o ao ficheiro físico com uma tabela interna. Toda a leitura que a seguir fizermos será relacionada com o conteúdo deste ficheiro, sem importar o nome utilizado durante a operação de abertura.

Destaquemos este ponto: Logo que um ficheiro for aberto, qualquer operação que diga respeito ao nome do ficheiro. incluindo a sua remoção, não terá efeito no seu conteúdo. Logo que exista um processo que tem um descritor para um ficheiro, o conteúdo do ficheiro não é removido do disco, mesmo que o seu nome desapareça do directório onde estava armazenada. O kernel assegura que associação ao conteúdo do ficheiro é mantida durante o lapso de tempo entre a chamada de sistema open() que fornece um descritor de ficheiro e a libertação desse descritor usando close() ou quando o processo termina.

Mas depois, obtemos a nossa solução ! O suficiente para começar a abrir o nosso ficheiro verificando depois as permissões, examinando as características do descritor em vez do nome do ficheiro. Isto é feito utilizando a chamada de sistema fstat() (esta última a trabalhar com o stat()), verificando geralmente o descritor do ficheiro em vez da sua path (caminho). Para obter um fluxo de E/S à volta do descritor utilizaremos a função fdopen() (trabalhando como o fopen()) ao assentar no descritor em vez do nome do ficheiro. Então o programa surge:

1    /* ex_02.c */
2    #include <fcntl.h>
3    #include <stdio.h>
4    #include <stdlib.h>
5    #include <unistd.h>
6    #include <sys/stat.h>
7    #include <sys/types.h>
8
9     int
10    main (int argc, char * argv [])
11    {
12        struct stat st;
13        int fd;
14        FILE * fp;
15
16        if (argc != 3) {
17            fprintf (stderr, "usage : %s file message\n", argv [0]);
18            exit(EXIT_FAILURE);
19        }
20        if ((fd = open (argv [1], O_WRONLY, 0)) < 0) {
21            fprintf (stderr, "Can't open %s\n", argv [1]);
22            exit(EXIT_FAILURE);
23        }
24        fstat (fd, & st);
25        if (st . st_uid != getuid ()) {
26            fprintf (stderr, "%s not owner !\n", argv [1]);
27            exit(EXIT_FAILURE);
28        }
29        if (! S_ISREG (st . st_mode)) {
30            fprintf (stderr, "%s not a normal file\n", argv[1]);
31            exit(EXIT_FAILURE);
32        }
33        if ((fp = fdopen (fd, "w")) == NULL) {
34            fprintf (stderr, "Can't open\n");
35            exit(EXIT_FAILURE);
36        }
37        fprintf (fp, "%s", argv [2]);
38        fclose (fp);
39        fprintf (stderr, "Write Ok\n");
40        exit(EXIT_SUCCESS);
41    }

Desta vez, após a linha 20, nenhuma modificação ao nome do ficheiro (remoção, re-nomeação, ligação) afectará o comportamento do nosso programa, o conteúdo do ficheiro original será mantido.

 

Linhas de Ajuda

É, então, importante ao manipular um ficheiro, garantir que a associação entre a representação interna e o conteúdo real permanece constante. Preferivelmente, utilizaremos as seguintes chamadas de sistema que manipulam um ficheiro físico como um descritor aberto em vez dos seus semelhantes que utilizam o caminho para o ficheiro:

Chamada de Sistema Uso
fchdir (int fd) Vai directamente para o directório representado por fd.
fchmod (int fd, mode_t mode) Altera as permissões de acesso ao ficheiro.
fchown (int fd, uid_t uid, gid_t gif) Altera o proprietário do ficheiro.
fstat (int fd, struct stat * st) Consulta a informação armazenada com o nodo do ficheiro físico.
ftruncate (int fd, off_t length) Trunca um ficheiro existente.
fdopen (int fd, char * mode) Obtém um fluxo de E/S à volta de um descritor já aberto. É uma rotina da biblioteca stdio e, não uma chamada de sistema.

Claro que depois, deve abrir o ficheiro no modo desejado, chamando o open() (não se esqueça do terceiro argumento ao criar um ficheiro novo). Falaremos mais acerca do open(), quando falarmos do problema dos ficheiros temporários.

Devemos insistir que é importante verificar os códigos de retorno das chamadas ao sistema. Por exemplo, mencionemos, mesmo não tendo nada haver com as race conditions, um problema numa implementação velha do /bin/login devido a uma negligência de verificar o código de retorno. Esta aplicação fornecia automaticamente acesso root quando não encontrava o ficheiro /etc/passwd. O comportamento até parece aceitável visto tratar-se de o dano de um ficheiro de sistema. Por outro lado, verificar se era impossível abrir o ficheiro em vez de verificar a sua existência é menos aceitável. Bastava chamar o /bin/login, após o número máximo de descritores permitidos para um utilizador, para conseguir ter acesso como root... Findemos esta digressão insistindo em como é importante verificar, não só as chamadas de sistema com sucesso ou falha, bem como os códigos de erro antes de levar em frente alguma acção respeitante à segurança do sistema.

 

Race conditions ao conteúdo do ficheiro

Um programa que diga respeito à segurança do sistema não deve assentar num acesso exclusivo ao conteúdo de um ficheiro. Mais precisamente, é preciso assegurar o risco das race conditions ao mesmo ficheiro. O principal perigo vem de um utilizador correndo múltiplas instâncias de uma aplicação Set-UID root ou estabelecendo várias ligações ao mesmo tempo com o mesmo demónio, esperando criar uma situação de race condition, durante a qual o conteúdo de um ficheiro de sistema podia ser modificado de um modo anormal.

Para evitar que um programa seja sensível a este tipo de situação, é necessário estabelecer um mecanismo de acesso exclusivo à informação do ficheiro. Este é o mesmo problema encontrado nas base de dados quando os utilizadores têm autorização para consultar ou alterar o conteúdo de um ficheiro. O principio de bloqueio de ficheiros permite resolver este problema.

Quando um processo deseja escrever para um ficheiro, pede ao kernel para o bloquear todo, ou parte dele. Logo que o processo mantém o bloqueio mais nenhum processo pode pedir o bloqueio para o mesmo ficheiro, ou pelo menos a mesma parte do ficheiro. Do mesmo modo que um processo pede para bloquear antes de ler o conteúdo de um ficheiro, o que assegura que não haverá modificações enquanto se mantiver o bloqueio.

De facto o sistema é mais esperto que isto: o kernel diferencia os bloqueios requeridos para escrita dos de leitura. Vários processos podem, simultaneamente, beneficiar de um bloqueio de leitura, visto que nenhum tentará alterar o conteúdo do ficheiro. Contudo só um processo é que pode beneficiar de um bloqueio de escrita num dado tempo e a mais nenhum dará dado bloqueio, nem sequer de leitura no mesmo intervalo de tempo.

Existem dois tipos de bloqueio (na maioria incompatíveis entre si). O primeiro vem do BSD e assenta na chamada de sistema flock(). O seu primeiro argumento é o descritor do ficheiro ao qual pretende aceder em modo exclusivo, o segundo é uma constante simbólica que representa a operação a ser feita. Pode ter valores diferentes: LOCK_SH (bloqueio para leitura), LOCK_EX (para escrita), LOCK_UN (libertar o bloqueio). A chamada de sistema permanece bloqueada até que a operação requerida seja impossível. Contudo, pode adicionar (utilizando um binário OR |) a constante LOCK_NB para a chamada falhar em vez de permanecer bloqueada.

O segundo tipo de bloqueio vem do Sistema V e, assenta na chamada ao sistema fcntl() cuja chamada é um pouco complicada. Existe uma função da biblioteca chamada lockf() muito semelhante à chamada de sistema mas sem tanta performance. O primeiro argumento do fcntl() é o descritor do ficheiro a bloquear. O segundo representa a operação a ser feita: F_SETLK e F_SETLKW administram um bloqueio, o segundo permanece bloqueado até a operação ser possível, enquanto que o primeiro retorna imediatamente no caso de erro. O F_GETLK permite consultar o estado do bloqueio de um ficheiro ( o que é inútil para as aplicações correntes). O terceiro argumento é um ponteiro para uma variável do tipo struct flock, a descrever o bloqueio. Os membros importantes da estrutura flock são os seguintes:

Nome Tipo Significado
l_type int Acção esperado : F_RDLCK (bloqueio para leitura), F_WRLCK (bloqueio para escrita) e F_UNLCK (libertar o bloqueio).
l_whence int l_start Origem do campo (normalmente SEEK_SET).
l_start off_t Posição do inicio do bloqueio (normalmente 0).
l_len off_t Tamanho do bloqueio, 0 para atingir o fim do ficheiro.

Podemos ver que o fcntl() pode bloquear partes limitadas do ficheiro, mas é capaz de mais em comparação ao flock(). Observemos um pequeno programa que pede um bloqueio para ler os ficheiros em causa cujos nomes são dados como argumento e, espera que o utilizador prima a tecla Enter antes de terminar (libertando assim os bloqueios).

1    /* ex_03.c */
2    #include <fcntl.h>
3    #include <stdio.h>
4    #include <stdlib.h>
5    #include <sys/stat.h>
6    #include <sys/types.h>
7    #include <unistd.h>
8
9    int
10   main (int argc, char * argv [])
11   {
12     int i;
13     int fd;
14     char buffer [2];
15     struct flock lock;
16
17     for (i = 1; i < argc; i ++) {
18       fd = open (argv [i], O_RDWR | O_CREAT, 0644);
19       if (fd < 0) {
20         fprintf (stderr, "Can't open %s\n", argv [i]);
21         exit(EXIT_FAILURE);
22       }
23       lock . l_type = F_WRLCK;
24       lock . l_whence = SEEK_SET;
25       lock . l_start = 0;
26       lock . l_len = 0;
27       if (fcntl (fd, F_SETLK, & lock) < 0) {
28         fprintf (stderr, "Can't lock %s\n", argv [i]);
29         exit(EXIT_FAILURE);
30       }
31     }
32     fprintf (stdout, "Press Enter to release the lock(s)\n");
33     fgets (buffer, 2, stdin);
34     exit(EXIT_SUCCESS);
35   }

Lançamos este programa a partir de uma primeira consola, onde fica à espera:

$ cc -Wall ex_03.c -o ex_03
$ ./ex_03 myfile
Prima Enter para libertar os bloqueio(s)
De outro terminal...
    $ ./ex_03 myfile
    Can't lock myfile
    $
Premindo Enter na primeira consola, libertamos os bloqueios.

Com este mecanismo, pode-se prevenir das race conditions dos directórios e filas de impressão, como é feito pelo demónio lpd, utilizando a flock() para bloquear o ficheiro /var/lock/subsys/lpd, permitindo assim que haja só uma ocorrência. Pode também administrar de um modo seguro o acesso a um ficheiro de sistema como /etc/passwd, bloqueando com o fcntl() a partir da biblioteca pam ao alterar os dados de utilizador.

Contudo isto só protege de interferências com aplicações bem comportadas, ou seja, pedir ao kernel para reservar o próprio acesso antes de ler ou escrever para um ficheiro importante do sistema. Falamos de bloqueios cooperativos, o que mostra os ricos dos acessos aos dados. Infelizmente um programa mal escrito é capaz de substituir o conteúdo de um ficheiro, mesmo apesar do bloqueio para escrita de um processo bem comportado. Aqui está um exemplo. Escrevemos algumas letras para um ficheiro e bloqueamo-lo utilizando o programa anterior.

$ echo "FIRST" > myfile
$ ./ex_03 myfile
Press Enter to release the lock(s)
De uma outra consola. podemos alterar o ficheiro :
    $ echo "SECOND" > myfile
    $
Voltando à primeira consola, verificamos os "estragos" :
(Enter)
$ cat myfile
SECOND
$

Para resolver este problema o kernel do Linux fornece ao administrador de sistema um mecanismo de bloqueio proveniente do sistema V. Assim só o pode utilizar com o bloqueio do fcntl() e não com o flock(). O administrador pode dizer ao kernel que os bloqueios do fcntl() são restritos, utilizando uma combinação particular de direitos de acesso. Assim de um processo bloqueia o ficheiro para escrita, um outro processo não será capaz de escrever nesse ficheiro (mesmo sendo root). A combinação especial é a utilização do bit Set-GID enquanto que o bit de execução é removido do grupo. Isto obtém-se com o comando :

$ chmod g+s-x myfile
$
Mas isto não é suficiente. Para um ficheiro beneficiar automaticamente de bloqueios cooperativos restritos, o atributo mandatory deve ser activado na partição onde pode ser encontrado. Normalmente, precisa de alterar o ficheiro /etc/fstab e adicionar a opção mand na 4º coluna, ou digitando o comando :
# mount
/dev/hda5 on / type ext2 (rw)
[...]
# mount / -o remount,mand
# mount
/dev/hda5 on / type ext2 (rw,mand)
[...]
#
Agora, a partir de outra consola podemos verificar que é impossível :
$ ./ex_03 myfile
Press Enter to release the lock(s)
De outro terminal :
    $ echo "THIRD" > myfile
    bash: myfile: Resource temporarily not available
    $
Regressando à primeira consola :
(Enter)
$ cat myfile
SECOND
$

O administrador decidiu e não o programador, fazer bloqueios de ficheiros restritos (por exemplo /etc/passwd, ou /etc/shadow). O Programador tem de controlar o modo como os dados são acedidos, o que assegura que a aplicação trabalha com dados coerentes quando lê e não é perigoso para outros processos quando escreve, desde que o ambiente esteja devidamente administrado.

 

Ficheiros Temporários

Muito Frequentemente um programa precisa de armazenar dados temporariamente num ficheiro externo. O caso mais usual é quando se insere um registo no meio de um ficheiro sequencial ordenado, o que implica que seja feita uma cópia do ficheiro original para um temporário, enquanto se adiciona a informação. A seguir a chamada ao sistema unlink() remove o ficheiro original e o rename() renomeia o ficheiro temporário para substituir o anterior.

Abrir um ficheiro temporário, se não for feito devidamente é, frequentemente, o ponto de partida para situações de race conditions para utilizadores mal intencionados. Falhas de segurança, baseadas em ficheiros temporários foram recentemente descobertas em aplicações como o Apache, Linuxconf, getty_ps, wu-ftpd, rdist, gpm, inn, etc. Lembremos alguns princípios para evitar este tipo de problemas.

Normalmente, a criação de ficheiros temporários é feita na directoria /tmp. Isto permite ao administrador de sistema saber onde a informação mais recente é guardada. E além disso é possível o programar uma limpeza periódica (utilizando o cron), a utilização de uma partição formatada independente no arranque, etc. Normalmente o administrador define a localização reservada para ficheiros temporários nos ficheiros <paths.h> e <stdio.h>, e nas constantes simbólicas _PATH_TMP e P_tmpdir. De facto, utilizar um outro directório por omissão diferente do /tmp não é assim tão bom, visto que obriga à compilação de toda a aplicação incluindo a biblioteca C. Contudo mencionemos que o comportamento da rotina da GlibC pode ser definida utilizando a variável de ambiente TMPDIR. Assim o utilizador pede que os ficheiros temporários sejam guardados num directório da sua pertença em vez do /tmp. Isto por vezes é obrigatório quando a partição dedicada /tmp é demasiado pequena para aplicações que requerem enorme espaço de armazenamento.

O directório de sistema /tmp é algo especial dados os seus direitos de acesso :

$ ls -ld /tmp
drwxrwxrwt 7 root  root    31744 Feb 14 09:47 /tmp
$

O Sticky-Bit representado pela letra t no fim ou o modo octal 01000, tem um significado particular quando aplicada a um directório : só o dono do directório (root ), e o dono de um ficheiro que se encontra nesse directório é que são capazes de apagar o ficheiro. O directório tem um acesso de escrita total, permite que cada utilizador coloque lá ficheiros, tendo a certeza que estão protegidos pelo menos até à próxima limpeza feita pelo administrador do sistema.

Contudo, a utilização de directórios temporários de armazenamento pode causar alguns problemas. Comecemos com um caso trivial, um aplicação Set-UID root que fala para um utilizador. Falemos de um programa transportador de mail, Se este processo recebe um sinal a pedir para terminar rapidamente, por exemplo o SIGTERM ou SIGQUIT durante um shutdown, ele pode tentar guardar no momento o mail escrito mas não enviado. Com velhas versões isto era feito em /tmp/dead.letter. Então um utilizador só tinha de criar (visto que é capaz de escrever no directório /tmp) uma ligação física para /etc/passwd com o nome dead.letter para o agente de mail ( a correr sobre um root efectivo) escrever para este ficheiro o conteúdo do mail ainda não acabado (acidentalmente contendo uma linha "root::1:99999:::::").

O primeiro problema com este comportamento é a natureza previsível do nome do ficheiro. Bastando observar uma só vez para deduzir que tal aplicação utilizará o nome de /tmp/dead.letter. Então primeiro passo é utilizar o nome definido para a instância do processo correcto. Existem várias funções de biblioteca capazes de fornecer um nome de ficheiro personalizado.

Suponhamos que temos tal função que nos fornece um nome único para o nosso ficheiro temporário. O software livre, estando disponível com o código fonte (o mesmo para a biblioteca C), o nome do ficheiro é também previsível apesar de difícil. Um atacante podia criar um link simbólico para o nome fornecido pela biblioteca C. A nossa primeira reacção é de verificar se o ficheiro existe antes de o abrir. Ingenuamente podíamos escrever algo do género:

  if ((fd = open (filename, O_RDWR)) != -1) {
    fprintf (stderr, "%s already exists\n", filename);
    exit(EXIT_FAILURE);
  }
  fd = open (filename, O_RDWR | O_CREAT, 0644);
  ...

Obviamente que estamos num caso típico de race condition, onde a falha de segurança reside no primeiro open() segundo possibilitando a um utilizador ter sucesso ao criar um link para /etc/passwd. Estas duas operações têm de ser feitas num modo atómico, que nenhuma manipulação seja possível entre elas. Isto é possível utilizando uma opção específica da chamada de sistema open(). chamada com O_EXCL, e usada em conjunto com O_CREAT, esta opção faz com que o open() falhe se o ficheiro já existir, mas a verificação da existência está ligada atomicamente à criação.

Além disso, a extensão Gnu 'x' para os modos de abertura da função fopen(), requer uma criação de ficheiro exclusiva, falhando no caso de existir:

  FILE * fp;

  if ((fp = fopen (filename, "r+x")) == NULL) {
    perror ("Can't create the file.");
    exit (EXIT_FAILURE);
  }

As permissões dos ficheiros temporários são bastante importantes também. Se tiver de escrever informação confidencial para um ficheiro no modo 644 (leitura/escrita para o dono e leitura para o resto do mundo) pode ser uma fonte de problemas. A

	#include <sys/types.h>
	#include <sys/stat.h>

        mode_t umask(mode_t mask);
função permite fixar as permissões para um ficheiro na altura ca criação. Então seguindo a chamada umask(077), o ficheiro será aberto no modo 600 (leitura/escrita para o dono, nenhum direitos para os restantes).

Normalmente, a criação de ficheiros temporários é feita em três passos:

  1. um nome de criação único (aleatório) ;
  2. abertura do ficheiro utilizando O_CREAT | O_EXCL, com permissões restritivas;
  3. verificar o resultado quando se abre o ficheiro e reagindo de acordo (quer tentando novamente ou abortando).

Como obter um ficheiro temporário ? As

      #include <stdio.h>

      char *tmpnam(char *s);
      char *tempnam(const char *dir, const char *prefix);

funções retornam ponteiros para os nomes de ficheiros criados aleatoriamente.

A primeira função aceita um argumento NULL, retornando de seguida o endereço do buffer estático. O seu conteúdo é alterado na próxima chamada tmpnam(NULL). Se o argumento é uma string alocada, o nome é copiado daqui, o que requer pelo menos L-tmpnam bytes. Tenha cuidado com buffer overflows ! A página man informa-o acerca de problemas quando a função é utilizada com um parâmetro a NULL, se forem definidas _POSIX_THREADS ou _POSIX_THREAD_SAFE_FUNCTIONS.

A função tempnam() retorna um ponteiro. O directório dir deve ser "apropriado" (a página man descreve o significado correcto de "suitable"). Esta função verifica se o ficheiro existe antes de retornar o seu nome. Contudo, mais uma vez, a página man não recomenda o seu uso, visto que "suitable" pode ter significados diferentes de acordo com as implementações da função. Mencionemos que o Gnome recomenda o seu uso deste modo :

  char *filename;
  int fd;

  do {
    filename = tempnam (NULL, "foo");
    fd = open (filename, O_CREAT | O_EXCL | O_TRUNC | O_RDWR, 0600);
    free (filename);
  } while (fd == -1);
O ciclo utilizado aqui, reduz os riscos criando novos. O que é que aconteceria se a partição onde quer criar os ficheiros temporários, estiver cheio ou sistema tivesse já aberto o número máximo de ficheiros disponíveis...

A

       #include <stdio.h>

       FILE *tmpfile (void);
função cria um nome de ficheiro único e abre-o. Este ficheiro é automaticamente apagado na altura em que é fechado.

Com a GlibC-2.1.3, este função utiliza um mecanismo semelhante ao tmpnam() para gerar o nome do ficheiro e abrir o descritor correspondente. O ficheiro é depois apagado, mas o Linux remove-o, realmente, quando mais nenhum recurso o está a utilizar, sendo então o descritor do ficheiro liberto, utilizando a chamada ao sistema close().

  FILE * fp_tmp;

  if ((fp_tmp = tmpfile()) == NULL) {
    fprintf (stderr, "Can't create a temporary file\n");
    exit (EXIT_FAILURE);
  }

  /* ... use of the temporary file ... */

  fclose (fp_tmp);  /* real deletion from the system */

Os casos mais simples não requerem que o nome do ficheiro seja alterado, ou que seja feita a transmissão do mesmo para outro processo, mas somente o armazenamento e a re-leitura da informação na área temporária. Assim não precisamos de saber o nome do ficheiro temporário pois só queremos aceder ao seu conteúdo. A função tmpfile() permite fazer isto.

A página man nada diz, mas o Secure-Programs-HOWTO não o recomenda. Segundo o autor, as especifícações não garantem a criação do ficheiro e ele não teve tempo de testar todas as suas implementações. Apesar disto esta função é muito eficaz.

Por último, as

       #include <stdlib.h>

       char *mktemp(char *template);
       int mkstemp(char *template);
funções criam um nome único a partir do modelo de strings terminadas em "XXXXXX". Estes 'X's são substituídos para obter o nome do ficheiro único.

Segundo as versões, o mktemp() substitui os primeiros cinco 'X' pelo ID do Processo (PID) ... o que torna o nome fácil de advinhar: só o último 'X' é que aleatório. Algumas versões permitem mais do que seis 'X's.

A função mkstemp() é recomendada no Secure-Programs-HOWTO. Eis aqui o método :

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>

 void failure(msg) {
  fprintf(stderr, "%s\n", msg);
  exit(1);
 }

/*
 * Creates a temporary file and returns it.
 * This routine removes the filename from the filesystem thus
 * it doesn't appear anymore when listing the directory.
 */
FILE *create_tempfile(char *temp_filename_pattern)
{
  int temp_fd;
  mode_t old_mode;
  FILE *temp_file;

  /* Create file with restrictive permissions */
  old_mode = umask(077);
  temp_fd = mkstemp(temp_filename_pattern);
  (void) umask(old_mode);
  if (temp_fd == -1) {
    failure("Couldn't open temporary file");
  }
  if (!(temp_file = fdopen(temp_fd, "w+b"))) {
    failure("Couldn't create temporary file's file descriptor");
  }
  if (unlink(temp_filename_pattern) == -1) {
    failure("Couldn't unlink temporary file");
  }
  return temp_file;
}

Estas funções mostram os problemas acerca da abstracção e portabilidade. Ou seja, espera-se que as funções das bibliotecas standard providenciem aspectos (abstracção) ... mas o modo da sua implementação varia de sistema para sistema (portabilidade). Por exemplo, a função tmpfile() abre um ficheiro temporário de diferentes modos (algumas versões não usam O_EXCL), ou o mkstemp() que suporta um número variável de 'X' de acordo com as implementações  

Conclusão

Voámos sobre muitos problemas de segurança respeitantes às race conditions de um mesmo recurso. Lembremos que nunca deve considerar duas operações sobre uma célula ligadas, a não ser que o kernel trate disso. Se as race conditions geram falhas de segurança, não deve neglicenciar as falhas assentes noutros recursos, como variáveis comuns a threads diferentes, ou a segmentos de memória partilhados a partir do shmget(). Os mecanismos de selecção de acesso (semáforos por exemplo) devem ser usados para evitar bugs difíceis de descobrir.

 

Ligações


 

Forma de respostas para este artigo

Todo artigo tem sua própria página de respostas. Nesta página você pode enviar um comentário ou ver os comentários de outros leitores:
 página de respostas 

Páginas Web mantidas pelo time de Editores LinuxFocus
© Frédéric Raynal, Christophe Blaess, Christophe Grenier, FDL
LinuxFocus.org

Clique aqui para reportar uma falha ou para enviar um comentário para LinuxFocus
Informação sobre tradução:
fr --> -- : Frédéric Raynal, Christophe Blaess, Christophe Grenier <pappy(at)users.sourceforge.net, ccb(at)club-internet.fr, grenier(at)nef.esiea.fr>
fr --> en: Georges Tarbouriech <georges.t(at)linuxfocus.org>
en --> en: Lorne Bailey <sherm_pbody(at)yahoo.com>
en --> pt: Bruno Sousa <bruno(at)linuxfocus.org>

2001-11-14, generated by lfparser version 2.21