Original in fr Frédéric Raynal, Christophe Blaess, Christophe Grenier
fr to en Georges Tarbouriech
en to en Lorne Bailey
en to pt Bruno Sousa
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.
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.
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 #
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:
nice -n 20
;
while (1);
);
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.
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.
É, 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.
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.
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:
O_CREAT | O_EXCL
, com
permissões restritivas;
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
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.