Qualidade do Código de Aplicativos Móveis¶
Desenvolvedores de aplicativos móveis utilizam uma ampla variedade de linguagens de programação e frameworks. Dessa forma, vulnerabilidades comuns como injeção de SQL, estouro de buffer e cross-site scripting (XSS) podem se manifestar em aplicativos quando práticas de programação segura são negligenciadas.
As mesmas falhas de programação podem afetar tanto aplicativos Android quanto iOS em algum grau, portanto, forneceremos uma visão geral das classes de vulnerabilidade mais comuns frequentemente na seção geral do guia. Em seções posteriores, abordaremos instâncias específicas do sistema operacional e recursos de mitigação de exploração.
Falhas de Injeção¶
Uma falha de injeção descreve uma classe de vulnerabilidade de segurança que ocorre quando a entrada do usuário é inserida em consultas ou comandos de backend. Ao injetar metacaracteres, um atacante pode executar código malicioso que é inadvertidamente interpretado como parte do comando ou consulta. Por exemplo, ao manipular uma consulta SQL, um atacante poderia recuperar registros arbitrários do banco de dados ou manipular o conteúdo do banco de dados backend.
Vulnerabilidades desta classe são mais prevalentes em serviços web do lado do servidor. Instâncias exploráveis também existem dentro de aplicativos móveis, mas as ocorrências são menos comuns e a superfície de ataque é menor.
Por exemplo, enquanto um aplicativo pode consultar um banco de dados SQLite local, esses bancos de dados geralmente não armazenam dados sensíveis (assumindo que o desenvolvedor seguiu práticas básicas de segurança). Isso torna a injeção de SQL um vetor de ataque não viável. No entanto, vulnerabilidades de injeção exploráveis às vezes ocorrem, o que significa que a validação adequada de entrada é uma prática necessária para programadores.
Injeção de SQL¶
Um ataque de injeção de SQL envolve integrar comandos SQL em dados de entrada, imitando a sintaxe de um comando SQL predefinido. Um ataque de injeção de SQL bem-sucedido permite que o atacante leia ou escreva no banco de dados e possivelmente execute comandos administrativos, dependendo das permissões concedidas pelo servidor.
Aplicativos tanto no Android quanto no iOS usam bancos de dados SQLite como meio de controlar e organizar o armazenamento local de dados. Suponha que um aplicativo Android gerencie a autenticação local do usuário armazenando as credenciais do usuário em um banco de dados local (uma prática de programação ruim que ignoraremos para fins deste exemplo). No login, o aplicativo consulta o banco de dados para procurar um registro com o nome de usuário e senha inseridos pelo usuário:
SQLiteDatabase db;
String sql = "SELECT * FROM users WHERE username = '" + username + "' AND password = '" + password +"'";
Cursor c = db.rawQuery( sql, null );
return c.getCount() != 0;
Vamos supor ainda que um atacante insira os seguintes valores nos campos "username" e "password":
username = 1' or '1' = '1
password = 1' or '1' = '1
Isso resulta na seguinte consulta:
SELECT * FROM users WHERE username='1' OR '1' = '1' AND Password='1' OR '1' = '1'
Como a condição '1' = '1' sempre avalia como verdadeira, esta consulta retorna todos os registros no banco de dados, fazendo com que a função de login retorne true mesmo que nenhuma conta de usuário válida tenha sido inserida.
A Ostorlab explorou o parâmetro de ordenação do aplicativo de clima móvel do Yahoo com adb usando esta carga útil de injeção de SQL.
Outra instância real de injeção de SQL do lado do cliente foi descoberta por Mark Woods nos aplicativos Android "Qnotes" e "Qget" executando em dispositivos de armazenamento QNAP NAS. Esses aplicativos exportavam provedores de conteúdo vulneráveis à injeção de SQL, permitindo que um atacante recuperasse as credenciais para o dispositivo NAS. Uma descrição detalhada deste problema pode ser encontrada no Blog da Nettitude.
Injeção de XML¶
Em um ataque de injeção de XML, o atacante injeta metacaracteres XML para alterar estruturalmente o conteúdo XML. Isso pode ser usado para comprometer a lógica de um aplicativo ou serviço baseado em XML, bem como possivelmente permitir que um atacante explore a operação do analisador XML que processa o conteúdo.
Uma variante popular deste ataque é XML eXternal Entity (XXE). Aqui, um atacante injeta uma definição de entidade externa contendo um URI na entrada XML. Durante a análise, o analisador XML expande a entidade definida pelo atacante acessando o recurso especificado pelo URI. A integridade do aplicativo de análise determina em última instância as capacidades concedidas ao atacante, onde o usuário malicioso poderia fazer qualquer (ou todos) dos seguintes: acessar arquivos locais, acionar solicitações HTTP para hosts e portas arbitrários, lançar um ataque de cross-site request forgery (CSRF) e causar uma condição de negação de serviço. O guia de teste web da OWASP contém o seguinte exemplo para XXE:
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE foo [
<!ELEMENT foo ANY >
<!ENTITY xxe SYSTEM "file:///dev/random" >]><foo>&xxe;</foo>
Neste exemplo, o arquivo local /dev/random é aberto onde um fluxo interminável de bytes é retornado, potencialmente causando uma negação de serviço.
A tendência atual no desenvolvimento de aplicativos concentra-se principalmente em serviços baseados em REST/JSON, já que o XML está se tornando menos comum. No entanto, nos raros casos em que conteúdo fornecido pelo usuário ou de outra forma não confiável é usado para construir consultas XML, ele pode ser interpretado por analisadores XML locais, como NSXMLParser no iOS. Como tal, referida entrada deve sempre ser validada e metacaracteres devem ser escapados.
Vetores de Ataque de Injeção¶
A superfície de ataque de aplicativos móveis é bastante diferente de aplicações web e de rede típicas. Aplicativos móveis não expõem frequentemente serviços na rede, e vetores de ataque viáveis na interface do usuário de um aplicativo são raros. Ataques de injeção contra um aplicativo são mais prováveis de ocorrer através de interfaces de comunicação interprocessos (IPC), onde um aplicativo malicioso ataca outro aplicativo em execução no dispositivo.
Localizar uma vulnerabilidade potencial começa por:
- Identificar possíveis pontos de entrada para entrada não confiável e, em seguida, rastrear a partir desses locais para ver se o destino contém funções potencialmente vulneráveis.
- Identificar chamadas de biblioteca/API perigosas conhecidas (por exemplo, consultas SQL) e, em seguida, verificar se a entrada não verificada interface com as respectivas consultas.
Durante uma revisão de segurança manual, você deve empregar uma combinação de ambas as técnicas. Em geral, entradas não confiáveis entram em aplicativos móveis através dos seguintes canais:
- Chamadas IPC
- Esquemas de URL personalizados
- Códigos QR
- Arquivos de entrada recebidos via Bluetooth, NFC ou outros meios
- Pasteboards
- Interface do usuário
Verifique se as seguintes práticas recomendadas foram seguidas:
- Entradas não confiáveis são verificadas por tipo e/ou validadas usando uma lista de valores aceitáveis.
- Instruções preparadas com vinculação de variáveis (ou seja, consultas parametrizadas) são usadas ao executar consultas de banco de dados. Se instruções preparadas são definidas, dados fornecidos pelo usuário e código SQL são automaticamente separados.
- Ao analisar dados XML, garanta que o aplicativo analisador esteja configurado para rejeitar a resolução de entidades externas para prevenir ataques XXE.
- Ao trabalhar com dados de certificado no formato x509, garanta que analisadores seguros sejam usados. Por exemplo, Bouncy Castle abaixo da versão 1.6 permite Execução Remota de Código por meio de reflexão insegura.
Cobriremos detalhes relacionados a fontes de entrada e APIs potencialmente vulneráveis para cada sistema operacional móvel nos guias de teste específicos do sistema operacional.
Falhas de Cross-Site Scripting¶
Problemas de cross-site scripting (XSS) permitem que atacantes injetem scripts do lado do cliente em páginas web visualizadas por usuários. Este tipo de vulnerabilidade é prevalente em aplicações web. Quando um usuário visualiza o script injetado em um navegador, o atacante ganha a capacidade de contornar a política de mesma origem, permitindo uma ampla variedade de explorações (por exemplo, roubar cookies de sessão, registrar pressionamentos de tecla, executar ações arbitrárias, etc.).
No contexto de aplicativos nativos, os riscos de XSS são muito menos prevalentes pela simples razão de que esses tipos de aplicações não dependem de um navegador web. No entanto, aplicativos usando componentes WebView, como WKWebView ou o obsoleto UIWebView no iOS e WebView no Android, são potencialmente vulneráveis a tais ataques.
Um exemplo antigo, mas bem conhecido, é o problema de XSS local no aplicativo Skype para iOS, identificado pela primeira vez por Phil Purviance. O aplicativo Skype não codificou adequadamente o nome do remetente da mensagem, permitindo que um atacante injetasse JavaScript malicioso para ser executado quando um usuário visualizasse a mensagem. Nesta prova de conceito, Phil mostrou como explorar o problema e roubar a lista de endereços de um usuário.
Análise Estática - Considerações de Teste de Segurança¶
Observe atentamente quaisquer WebViews presentes e investigue entradas não confiáveis renderizadas pelo aplicativo.
Problemas de XSS podem existir se a URL aberta pelo WebView for parcialmente determinada pela entrada do usuário. O seguinte exemplo é de um problema de XSS no Zoho Web Service, relatado por Linus Särud.
Java
webView.loadUrl("javascript:initialize(" + myNumber + ");");
Kotlin
webView.loadUrl("javascript:initialize($myNumber);")
Outro exemplo de problemas de XSS determinados pela entrada do usuário são métodos públicos sobrescritos.
Java
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
if (url.substring(0,6).equalsIgnoreCase("yourscheme:")) {
// parse the URL object and execute functions
}
}
Kotlin
fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
if (url.substring(0, 6).equals("yourscheme:", ignoreCase = true)) {
// parse the URL object and execute functions
}
}
Sergey Bobrov foi capaz de tirar vantagem disso no seguinte relatório do HackerOne. Qualquer entrada para o parâmetro HTML seria confiável no ActionBarContentActivity do Quora. Cargas úteis foram bem-sucedidas usando adb, dados da área de transferência via ModalContentActivity e Intents de aplicativos de terceiros.
- ADB
$ adb shell
$ am start -n com.quora.android/com.quora.android.ActionBarContentActivity \
-e url 'http://test/test' -e html 'XSS<script>alert(123)</script>'
- Dados da Área de Transferência
$ am start -n com.quora.android/com.quora.android.ModalContentActivity \
-e url 'http://test/test' -e html \
'<script>alert(QuoraAndroid.getClipboardData());</script>'
- Intent de terceiro em Java ou Kotlin:
Intent i = new Intent();
i.setComponent(new ComponentName("com.quora.android",
"com.quora.android.ActionBarContentActivity"));
i.putExtra("url","http://test/test");
i.putExtra("html","XSS PoC <script>alert(123)</script>");
view.getContext().startActivity(i);
val i = Intent()
i.component = ComponentName("com.quora.android",
"com.quora.android.ActionBarContentActivity")
i.putExtra("url", "http://test/test")
i.putExtra("html", "XSS PoC <script>alert(123)</script>")
view.context.startActivity(i)
Se um WebView for usado para exibir um site remoto, o ônus de escapar o HTML passa para o lado do servidor. Se existir uma falha de XSS no servidor web, isso pode ser usado para executar script no contexto do WebView. Como tal, é importante realizar análise estática do código-fonte da aplicação web.
Verifique se as seguintes práticas recomendadas foram seguidas:
- Nenhum dado não confiável é renderizado em HTML, JavaScript ou outros contextos interpretados, a menos que seja absolutamente necessário.
- Codificação apropriada é aplicada para escapar caracteres, como codificação de entidade HTML. Nota: regras de escape tornam-se complicadas quando o HTML está aninhado dentro de outro código, por exemplo, renderizando uma URL localizada dentro de um bloco JavaScript.
Considere como os dados serão renderizados em uma resposta. Por exemplo, se os dados são renderizados em um contexto HTML, seis caracteres de controle que devem ser escapados:
| Caractere | Escapado |
|---|---|
| & | & |
| < | < |
| > | > |
| " | " |
| ' | ' |
| / | / |
Para uma lista abrangente de regras de escape e outras medidas de prevenção, consulte a OWASP XSS Prevention Cheat Sheet.
Análise Dinâmica - Considerações de Teste de Segurança¶
Problemas de XSS podem ser melhor detectados usando fuzzing de entrada manual e/ou automatizado, ou seja, injetando tags HTML e caracteres especiais em todos os campos de entrada disponíveis para verificar se a aplicação web nega entradas inválidas ou escapa os metacaracteres HTML em sua saída.
Um ataque de XSS refletido refere-se a uma exploração onde código malicioso é injetado via um link malicioso. Para testar esses ataques, o fuzzing de entrada automatizado é considerado um método eficaz. Por exemplo, o BURP Scanner é altamente eficaz na identificação de vulnerabilidades de XSS refletido. Como sempre com análise automatizada, garanta que todos os vetores de entrada sejam cobertos com uma revisão manual dos parâmetros de teste.
Bugs de Corrupção de Memória¶
Bugs de corrupção de memória são um favorito popular entre hackers. Esta classe de bug resulta de um erro de programação que faz com que o programa acesse um local de memória não intencional. Sob as condições certas, os atacantes podem capitalizar esse comportamento para sequestrar o fluxo de execução do programa vulnerável e executar código arbitrário. Este tipo de vulnerabilidade ocorre de várias maneiras:
-
Estouros de buffer: Isso descreve um erro de programação onde um aplicativo escreve além de um intervalo de memória alocado para uma operação particular. Um atacante pode usar essa falha para sobrescrever dados de controle importantes localizados na memória adjacente, como ponteiros de função. Estouros de buffer eram anteriormente o tipo mais comum de falha de corrupção de memória, mas tornaram-se menos prevalentes ao longo dos anos devido a vários fatores. Notavelmente, a conscientização entre os desenvolvedores sobre os riscos de usar funções não seguras da biblioteca C é agora uma prática comum e, além disso, pegar bugs de estouro de buffer é relativamente simples. No entanto, ainda vale a pena testar tais defeitos.
-
Acesso fora dos limites: A aritmética de ponteiro com bugs pode fazer com que um ponteiro ou índice referencie uma posição além dos limites da estrutura de memória pretendida (por exemplo, buffer ou lista). Quando um aplicativo tenta escrever em um endereço fuera dos limites, uma falha ou comportamento não intencional ocorre. Se o atacante puder controlar o deslocamento alvo e manipular o conteúdo escrito até certo ponto, a exploração de execução de código é provavelmente possível.
-
Ponteiros pendurados: Estes ocorrem quando um objeto com uma referência de entrada para um local de memória é excluído ou desalocado, mas o ponteiro do objeto não é redefinido. Se o programa posteriormente usar o ponteiro pendurado para chamar uma função virtual do objeto já desalocado, é possível sequestrar a execução sobrescrevendo o ponteiro vtable original. Alternativamente, é possível ler ou escrever variáveis de objeto ou outras estruturas de memória referenciadas por um ponteiro pendurado.
-
Use-after-free: Isso se refere a um caso especial de ponteiros pendurados referenciando memória liberada (desalocada). Depois que um endereço de memória é limpo, todos os ponteiros referenciando o local se tornam inválidos, fazendo com que o gerenciador de memória retorne o endereço para um pool de memória disponível. Quando este local de memória é eventualmente realocado, acessar o ponteiro original lerá ou escreverá os dados contidos na memória recém-alocada. Isso geralmente leva à corrupção de dados e comportamento indefinido, mas atacantes astutos podem configurar os locais de memória apropriados para alavancar o controle do ponteiro de instrução.
-
Estouros de inteiro: Quando o resultado de uma operação aritmética excede o valor máximo para o tipo inteiro definido pelo programador, isso resulta no valor "envolvendo" o valor máximo do inteiro, inevitavelmente resultando em um valor pequeno sendo armazenado. Por outro lado, quando o resultado de uma operação aritmética é menor que o valor mínimo do tipo inteiro, ocorre um estouro negativo de inteiro onde o resultado é maior que o esperado. Se um bug particular de estouro/estouro negativo de inteiro é explorável depende de como o inteiro é usado. Por exemplo, se o tipo inteiro representasse o comprimento de um buffer, isso poderia criar uma vulnerabilidade de estouro de buffer.
-
Vulnerabilidades de string de formato: Quando a entrada do usuário não verificada é passada para o parâmetro de string de formato das funções da família
printfda C, os atacantes podem injetar tokens de formato como '%c' e '%n' para acessar a memória. Bugs de string de formato são convenientes para explorar devido à sua flexibilidade. Se um programa outputar o resultado da operação de formatação de string, o atacante pode ler e escrever na memória arbitrariamente, contornando assim recursos de proteção como ASLR.
O objetivo principal na exploração de corrupção de memória é geralmente redirecionar o fluxo do programa para um local onde o atacante colocou instruções de máquina montadas referidas como shellcode. No iOS, o recurso de prevenção de execução de dados (como o nome implica) impede a execução a partir de memória definida como segmentos de dados. Para contornar esta proteção, os atacantes alavancam a programação orientada a retorno (ROP). Este processo envolve encadear pequenos pedaços de código pré-existentes ("gadgets") no segmento de texto onde esses gadgets podem executar uma função útil para o atacante ou, chamar mprotect para alterar as configurações de proteção de memória para o local onde o atacante armazenou o shellcode.
Aplicativos Android são, em sua maioria, implementados em Java, que é inerentemente seguro contra problemas de corrupção de memória por design. No entanto, aplicativos nativos utilizando bibliotecas JNI são suscetíveis a este tipo de bug. Em casos raros, aplicativos Android que usam analisadores XML/JSON para desembrulhar objetos Java também estão sujeitos a bugs de corrupção de memória. Um exemplo de tal vulnerabilidade foi encontrado no aplicativo PayPal.
Da mesma forma, aplicativos iOS podem envolver chamadas C/C++ em Obj-C ou Swift, tornando-os suscetíveis a este tipo de ataques.
Exemplo:
O seguinte snippet de código mostra um exemplo simples para uma condição resultando em uma vulnerabilidade de estouro de buffer.
void copyData(char *userId) {
char smallBuffer[10]; // tamanho de 10
strcpy(smallBuffer, userId);
}
Para identificar potenciais estouros de buffer, procure por usos de funções de string não seguras (strcpy, strcat, outras funções começando com o prefixo "str", etc.) e construções de programação potencialmente vulneráveis, como copiar entrada do usuário em um buffer de tamanho limitado. O seguinte deve ser considerado bandeiras vermelhas para funções de string não seguras:
strcatstrcpystrncatstrlcatstrncpystrlcpysprintfsnprintfgets
Além disso, procure por instâncias de operações de cópia implementadas como loops "for" ou "while" e verifique se as verificações de comprimento são realizadas corretamente.
Verifique se as seguintes práticas recomendadas foram seguidas:
- Ao usar variáveis inteiras para indexação de array, cálculos de comprimento de buffer ou qualquer outra operação crítica de segurança, verifique que tipos inteiros não assinados são usados e testes de pré-condição são realizados para prevenir a possibilidade de envolvimento de inteiro.
- O aplicativo não usa funções de string não seguras como
strcpy, a maioria das outras funções começando com o prefixo "str",sprint,vsprintf,gets, etc.; - Se o aplicativo contiver código C++, classes de string ANSI C++ são usadas;
- No caso de
memcpy, certifique-se de verificar que o buffer de destino é pelo menos do mesmo tamanho que a fonte e que ambos os buffers não se sobrepõem. - Aplicativos iOS escritos em Objective-C usam a classe NSString. Aplicativos C no iOS devem usar CFString, a representação Core Foundation de uma string.
- Nenhum dado não confiável é concatenado em strings de formato.
Considerações de Teste de Segurança de Análise Estática¶
Análise de código estático de código de baixo nível é um tópico complexo que poderia facilmente preencher seu próprio livro. Ferramentas automatizadas como RATS combinadas com esforços limitados de inspeção manual são geralmente suficientes para identificar frutas ao alcance. No entanto, condições de corrupção de memória muitas vezes decorrem de causas complexas. Por exemplo, um bug use-after-free pode realmente ser o resultado de uma condição de corrida intricada e contra-intuitiva não imediatamente aparente. Bugs que se manifestam a partir de instâncias profundas de deficiências de código negligenciadas são geralmente descobertos através de análise dinâmica ou por testadores que investem tempo para obter um entendimento profundo do programa.
Considerações de Teste de Segurança de Análise Dinâmica¶
Bugs de corrupção de memória são melhor descobertos via fuzzing de entrada: uma técnica de teste de software de caixa preta automatizada na qual dados malformados são continuamente enviados para um aplicativo para pesquisar condições de vulnerabilidade potencial. Durante este processo, o aplicativo é monitorado por mau funcionamento e falhas. Se ocorrer uma falha, a esperança (pelo menos para testadores de segurança) é que as condições que criaram a falha revelem uma falha de segurança explorável.
Técnicas ou scripts de teste de fuzzing (muitas vezes chamados de "fuzzers") normalmente gerarão múltiplas instâncias de entrada estruturada de uma forma semi-correta. Essencialmente, os valores ou argumentos gerados são pelo menos parcialmente aceitos pelo aplicativo alvo, mas também contêm elementos inválidos, potencialmente acionando falhas de processing de entrada e comportamentos inesperados do programa. Um bom fuzzer expõe uma quantidade substancial de possíveis caminhos de execução do programa (ou seja, saída de alta cobertura). As entradas são geradas do zero ("baseado em geração") ou derivadas da mutação de dados de entrada válidos conhecidos ("baseado em mutação").
Para mais informações sobre fuzzing, consulte o OWASP Fuzzing Guide.
Mecanismos de Proteção Binária¶
Código Independente de Posição¶
PIC (Código Independente de Posição) é código que, sendo colocado em algum lugar na memória principal, executa corretamente independentemente de seu endereço absoluto. PIC é comumente usado para bibliotecas compartilhadas, para que o mesmo código de biblioteca possa ser carregado em um local em cada espaço de endereço do programa onde não sobreponha outra memória em uso (por exemplo, outras bibliotecas compartilhadas).
PIE (Executável Independente de Posição) são binários executáveis feitos inteiramente de PIC. Binários PIE são usados para permitir ASLR (Randomização de Layout do Espaço de Endereço) que organiza aleatoriamente as posições do espaço de endereço de áreas de dados chave de um processo, incluindo a base do executável e as posições da pilha, heap e bibliotecas.
Gerenciamento de Memória¶
Contagem Automática de Referência¶
ARC (Contagem Automática de Referência) é um recurso de gerenciamento de memória do compilador Clang exclusivo para Objective-C e Swift. ARC libera automaticamente a memória usada por instâncias de classe quando essas instâncias não são mais necessárias. ARC difere da coleta de lixo por rastreamento por não haver um processo em segundo plano que desaloque os objetos de forma assíncrona em tempo de execução.
Ao contrário da coleta de lixo por rastreamento, o ARC não lida com ciclos de referência automaticamente. Isso significa que, desde que haja referências "fortes" a um objeto, ele não será desalocado. Referências cruzadas fortes podem, consequentemente, criar deadlocks e vazamentos de memória. Cabe ao desenvolvedor quebrar ciclos usando referências fracas. Você pode aprender mais sobre como isso difere da Coleta de Lixo aqui.
Coleta de Lixo¶
Coleta de Lixo (GC) é um recurso de gerenciamento de memória automático de algumas linguagens como Java/Kotlin/Dart. O coletor de lixo tenta recuperar memória que foi alocada pelo programa, mas não é mais referenciada—também chamada de lixo. O Android runtime (ART) faz uso de uma versão melhorada de GC. Você pode aprender mais sobre como isso difere do ARC aqui.
Gerenciamento Manual de Memória¶
Gerenciamento manual de memória é tipicamente necessário em bibliotecas nativas escritas em C/C++ onde ARC e GC não se aplicam. O desenvolvedor é responsável por fazer o gerenciamento adequado de memória. O gerenciamento manual de memória é conhecido por permitir que várias classes principais de bugs entrem em um programa quando usado incorretamente, notadamente violações de segurança de memória ou vazamentos de memória.
Mais informações podem ser encontradas em "Bugs de Corrupção de Memória").
Proteção contra Stack Smashing¶
Canários de stack ajudam a prevenir ataques de estouro de buffer de pilha armazenando