MASTG-KNOW-0103: Memória do Processo
A análise da memória pode ajudar os desenvolvedores a identificar as causas raiz de problemas como travamentos de aplicativos. No entanto, ela também pode ser usada para acessar dados sensíveis. Esta seção descreve como verificar a memória do processo em busca de divulgação de dados.
Primeiro, identifique as informações sensíveis armazenadas na memória. É muito provável que ativos sensíveis sejam carregados na memória em algum momento. O objetivo é garantir que essas informações sejam expostas pelo menor tempo possível.
Para investigar a memória de um aplicativo, comece criando um despejo de memória. Alternativamente, você pode analisar a memória em tempo real com, por exemplo, um depurador. Independentemente do método usado, este é um processo muito propenso a erros, pois os despejos fornecem os dados deixados por funções executadas e você pode acabar não executando etapas críticas. Além disso, é fácil negligenciar dados durante a análise, a menos que você conheça a "pegada" dos dados que está procurando (seja seu valor exato ou seu formato). Por exemplo, se o aplicativo criptografa usando uma chave simétrica gerada aleatoriamente, é muito improvável que você localize a chave na memória, a menos que encontre seu valor por outros meios.
Antes de examinar o código-fonte, verificar a documentação e identificar os componentes do aplicativo fornece uma visão geral de onde os dados podem ser expostos. Por exemplo, enquanto os dados sensíveis recebidos de um backend existem no objeto de modelo final, múltiplas cópias também podem existir no cliente HTTP ou no analisador de XML. Todas essas cópias devem ser removidas da memória o mais rápido possível.
Compreender a arquitetura do aplicativo e sua interação com o sistema operacional ajudará você a identificar informações sensíveis que não precisam ser expostas na memória. Por exemplo, suponha que seu aplicativo receba dados de um servidor e os transfira para outro sem precisar de nenhum processamento adicional. Esses dados podem ser recebidos e manipulados de forma criptografada, o que evita a exposição via memória.
No entanto, se os dados sensíveis precisarem ser expostos via memória, certifique-se de que seu aplicativo exponha o menor número possível de cópias desses dados pelo menor tempo possível. Em outras palavras, você deseja um tratamento centralizado de dados sensíveis, baseado em estruturas de dados primitivas e mutáveis.
Tais estruturas de dados dão aos desenvolvedores acesso direto à memória. Certifique-se de que esse acesso seja usado para substituir os dados sensíveis e as chaves criptográficas por zeros. O Apple Secure Coding Guide sugere zerar dados sensíveis após o uso, mas não fornece formas recomendadas de fazer isso.
Exemplos de tipos de dados preferíveis incluem char [] e int [], mas não NSString ou String. Sempre que você tenta modificar um objeto imutável, como uma String, na verdade cria uma cópia e altera a cópia. Considere usar NSMutableData para armazenar segredos em Swift/Objective-C e usar o método resetBytes(in:) para zerar. Veja também Clean memory of secret data para referência.
Evite tipos de dados Swift que não sejam coleções, independentemente de serem considerados mutáveis. Muitos tipos de dados Swift mantêm seus dados por valor, não por referência. Embora isso permita a modificação da memória alocada para tipos simples como char e int, lidar com um tipo complexo como String por valor envolve uma camada oculta de objetos, estruturas ou arrays primitivos cuja memória não pode ser acessada ou modificada diretamente. Certos tipos de uso podem parecer criar um objeto de dados mutável (e até serem documentados como tal), mas na verdade criam um identificador mutável (variável) em vez de um identificador imutável (constante). Por exemplo, muitos pensam que o seguinte resulta em uma String mutável em Swift, mas este é na verdade um exemplo de uma variável cujo valor complexo pode ser alterado (substituído, não modificado no local):
var str1 = "Goodbye" // "Goodbye", endereço base: 0x0001039e8dd0
str1.append(" ") // "Goodbye ", endereço base: 0x608000064ae0
str1.append("cruel world!") // "Goodbye cruel world", endereço base: 0x6080000338a0
str1.removeAll() // "", endereço base 0x00010bd66180
Observe que o endereço base do valor subjacente muda a cada operação de string. Aqui está o problema: Para apagar com segurança as informações sensíveis da memória, não queremos simplesmente alterar o valor da variável; queremos alterar o conteúdo real da memória alocada para o valor atual. Swift não oferece tal função.
As coleções Swift (Array, Set e Dictionary), por outro lado, podem ser aceitáveis se coletarem tipos de dados primitivos como char ou int e forem definidas como mutáveis (ou seja, como variáveis em vez de constantes), caso em são mais ou menos equivalentes a um array primitivo (como char []). Essas coleções fornecem gerenciamento de memória, o que pode resultar em cópias não identificadas dos dados sensíveis na memória se a coleção precisar copiar o buffer subjacente para um local diferente para estendê-lo.
Usar tipos de dados Objective-C mutáveis, como NSMutableString, também pode ser aceitável, mas esses tipos têm o mesmo problema de memória que as coleções Swift. Preste atenção ao usar coleções Objective-C; elas mantêm dados por referência e apenas tipos de dados Objective-C são permitidos. Portanto, procuramos não por uma coleção mutável, mas por uma coleção que referencie objetos mutáveis.
Como vimos até agora, usar tipos de dados Swift ou Objective-C requer um entendimento profundo da implementação da linguagem. Além disso, houve uma refatoração central entre as principais versões do Swift, resultando em comportamentos de muitos tipos de dados incompatíveis com os de outros tipos. Para evitar esses problemas, recomendamos usar tipos de dados primitivos sempre que os dados precisarem ser apagados com segurança da memória.
Infelizmente, poucas bibliotecas e frameworks são projetados para permitir que dados sensíveis sejam sobrescritos. Nem mesmo a Apple considera essa questão na API oficial do iOS SDK. Por exemplo, a maioria das APIs para transformação de dados (analisadores, serializadores, etc.) opera em tipos de dados não primitivos. Da mesma forma, independentemente de você marcar algum UITextField como Secure Text Entry ou não, ele sempre retorna dados na forma de uma String ou NSString.