A computação distribuída consiste em tempo e estado. Isto é, para que mais de um componente se comunique, é necessário compartilhar o estado, o que exige tempo.
A maioria dos programadores antropomorfiza seu trabalho. Eles enxergam um thread de controle executando todo o programa da mesma forma como enxergariam a si mesmos fazendo o trabalho inteiro por conta própria. Computadores modernos, entretanto, alternam entre tarefas com muita rapidez e, em sistemas multi-core, multi-CPU ou distribuídos, dois eventos podem ocorrer exatamente ao mesmo tempo. Defeitos rapidamente são postos nas lacunas entre o modelo do programador de como um programa é executado e o que ocorre na realidade. Esses defeitos estão relacionados com interações inesperadas entre threads, processos, tempo e informações. Essas interações ocorrem por meio de estados compartilhados: semáforos, variáveis, o sistema de arquivos e, basicamente, todas as coisas capazes de armazenar informações.
Race Condition: File System Access
1. O programa verifica a propriedade de um arquivo, fazendo referência a esse arquivo por seu nome.
2. O programa executa posteriormente uma operação do sistema de arquivos usando o mesmo nome de arquivo e considera que a propriedade verificada anteriormente não foi alterada.
Exemplo 1: O código a seguir é de um programa instalado
setuid root
. O programa executa certas operações de arquivo em nome de usuários sem privilégios e usa verificações de acesso para garantir que ele não use seus privilégios de root para executar operações que não deveriam estar disponíveis ao usuário atual. O programa usa a chamada de sistema access()
para verificar se a pessoa que está executando o programa tem permissão para acessar o arquivo especificado antes de abri-lo e executar as operações necessárias.
if (!access(file,W_OK)) {
f = fopen(file,"w+");
operate(f);
...
}
else {
fprintf(stderr,"Unable to open file %s.\n",file);
}
A chamada para
access()
se comporta conforme o esperado, retornando 0
quando o usuário que está executando o programa tem as permissões necessárias para gravar no arquivo e -1 em outros casos. No entanto, como access()
e fopen()
operam ambos em nomes de arquivo em vez de em identificadores de arquivo, não há nenhuma garantia de que a variável file
ao ser transmitida para fopen()
ainda faça referência ao mesmo arquivo no disco de quando ela foi transmitida para access()
. Se um invasor substituir file
após a chamada para access()
por um link simbólico para um arquivo diferente, o programa usará seus privilégios de root para operar nesse arquivo, mesmo que este seja um arquivo que o invasor seria incapaz de modificar em outras circunstâncias. Ao enganar o programa a ponto de fazer com que ele realizasse uma operação que de outra forma seria inadmissível, o invasor obteve privilégios elevados.Esse tipo de vulnerabilidade não está limitado a programas com privilégios de
root
. Se o aplicativo for capaz de realizar qualquer operação que o invasor de outra forma não teria permissão para realizar, então ele será um possível alvo.A janela de vulnerabilidade para tal ataque é o período entre o instante em que a propriedade é testada e o instante em que o arquivo é usado. Mesmo que o uso ocorra imediatamente após a verificação, os sistemas operacionais modernos não oferecem nenhuma garantia quanto ao volume de código que é executado antes de o processo liberar a CPU. Os invasores contam com uma variedade de técnicas para ampliar a janela de oportunidade e facilitar sua exploração. Entretanto, mesmo com uma janela pequena, uma tentativa de exploração pode ser repetidas várias e várias vezes, até ser bem-sucedida.
Exemplo 2: O código a seguir cria um arquivo e, em seguida, modifica seu proprietário.
fd = creat(FILE, 0644); /* Create file */
if (fd == -1)
return;
if (chown(FILE, UID, -1) < 0) { /* Change file owner */
...
}
O código pressupõe que o arquivo operado pela chamada para
chown()
seja igual ao arquivo criado pela chamada para creat()
, mas isso não é necessariamente o caso. Como chown()
opera em um nome de arquivo e não em um identificador de arquivo, um invasor pode ser capaz de substituir o arquivo por um link para um arquivo que ele não possui. Dessa maneira, a chamada para chown()
daria ao invasor a posse sobre o arquivo vinculado.1. O programa verifica uma propriedade de um arquivo, fazendo referência ao arquivo pelo nome.
2. O programa executa posteriormente uma operação do sistema de arquivos usando o mesmo nome de arquivo e considera que a propriedade verificada anteriormente não foi alterada.
Exemplo 1: O programa a seguir chama a rotina
CBL_CHECK_FILE_EXIST
para verificar se o arquivo existe antes de criar um e executa as operações necessárias.
CALL "CBL_CHECK_FILE_EXIST" USING
filename
file-details
RETURNING status-code
END-CALL
IF status-code NOT = 0
MOVE 3 to access-mode
MOVE 0 to deny-mode
MOVE 0 to device
CALL "CBL_CREATE_FILE" USING
filename
access-mode
deny-mode
device
file-handle
RETURNING status-code
END-CALL
END-IF
A chamada para
CBL_CHECK_FILE_EXIST
comporta-se como esperado e retorna um valor não zero, indicando que o arquivo não existe. Entretanto, como tanto CBL_CHECK_FILE_EXIST
quanto CBL_CREATE_FILE
operam de acordo com nomes de arquivo, em vez de identificadores de arquivo, não há garantia de que a variável filename
, ao ser passada para CBL_CREATE_FILE
, ainda faça referência ao mesmo arquivo a que se referia ao ser passada para CBL_CHECK_FILE_EXIST
. Se um invasor criar filename
após a chamada para CBL_CHECK_FILE_EXIST
, a chamada para CBL_CREATE_FILE
falhará, levando o programa a acreditar que o arquivo está vazio, quando, na realidade, ele contém dados controlados pelo invasor.A janela de vulnerabilidade para tal ataque é o período entre o instante em que a propriedade é testada e o instante em que o arquivo é usado. Mesmo que o uso ocorra imediatamente após a verificação, os sistemas operacionais modernos não oferecem nenhuma garantia quanto ao volume de código que é executado antes de o processo liberar a CPU. Os invasores contam com uma variedade de técnicas para ampliar a janela de oportunidade e facilitar sua exploração. Entretanto, mesmo com uma janela pequena, uma tentativa de exploração pode ser repetidas várias e várias vezes, até ser bem-sucedida.
Além disso, esse tipo de vulnerabilidade pode se aplicar a um programa com privilégios de
root
que executa certas operações de arquivo em nome de usuários sem privilégios e usa verificações de acesso para garantir que ele não use seus privilégios de root para executar operações que não deveriam estar disponíveis ao usuário atual. Ao enganar o programa e fazer com que ele execute uma operação que, de outro modo, não seria permitida, o invasor pode aumentar seus privilégios.