
Melhorando a segurança do Android - Parte 1
Vou começar uma série dividida em duas partes para melhorar a segurança dos aplicativos que produzimos. Ultimamente tenho encarado alguns desafios de segurança e vou compartilhar algumas informações sobre o assunto.
Com estes artigos pretendo cobrir a maior parte do problemas de seguranças diários. A maioria dos apps hoje se comunicam com APIs então é muito importante a segurança do lado do seu servidor, mas focarei na plataforma Android. Nesta primeira parte vamos discutir boas e fáceis práticas e posteriormente demonstrarei como encriptar dados com ferramentas que o framework nos auxilia.
Armazenamento de dados no dispositivo
Existem várias formas de você armazenar dados do seu app. Quando pretendemos armazenar dados no dispositivos podemos classificar de duas formas, armazenamento interno e armazenamento externo. O armazenamento interno salva os dados dentro da memória interna, em um espaço reservado para o seu app e o armazenamento externo salva os dados normalmente no cartão de memória do aparelho.
Independente do tipo de armazenamento no dispositivo escolhido, o desenvolvedor deve ter consciência da natureza de cada tipo e fazer algumas análises de risco pois estamos focando na segurança. Seguem as primeiras recomendações:
- Utilize o armazenamento interno caso os arquivos que pretende salvar sejam pequenos e precisam de alguma proteção. Utilize preferencialmente SharedPreferences e ContentProviders.
- Utilize armazenamento externo para arquivos grandes e que não são sensíveis a exposição. Aqui os arquivos são abertos para o usuário e outras aplicações.
Não existe o método errado, a classificação de sensibilidade do dado é responsabilidade daquela análises de risco citada anteriormente. Por exemplo, às vezes a equipe de desenvolvimento não quer expor as mídias usadas no app pois isso faz parte do núcleo do negócio. Então nem sempre essa exposição se refere a dados do usuário ou palavras chaves.
Os famosos dados do usuário e palavras chaves nem cogite expor. Aqui vai outra recomendação que vai além da segurança, evite requisitar e persistir dados do usuário desnecessariamente. Se existe a necessidade de salvar algo no armazenamento externo e você quer proteger o conteúdo, criptografe os dados. Vamos falar mais de criptografia na parte 2.
Comunicação entre processos
Use as ferramentas que o framework do Android te oferece para comunicação entre processos. Normalmente desenvolvedores que têm backgrounds diferentes usam conceitos e técnicas que não são os recomendados na plataforma. Por exemplo, usam sockets ou arquivos compartilhados para comunicação entre partes do seu código ou entre diferentes apps.
Use intents para correr entre telas ou chamar outra aplicação e se quer compartilhar dados existe o FileProvider. Para enviar dados para vários aplicativos utilize Broadcasts e BroadcastReceivers. É possível criar permissões customizadas para os seus Broadcasts. Acredito que a mensagem foi clara, use a plataforma a seu favor e não reinvente a roda.
Valide as entradas do usuário
Não é comum buffer overflow no Android mas vai a dica, sempre valide os inputs dos usuários. Você provavelmente vai usar banco de dados SQlite e se conectar em uma API, então evite Sql injection. Use as suas habilidades de expressões regulares, trate os dados e pelo menos especifique sempre o tamanho máximo dos seus EditTexts.
Strings
Normalmente temos uma quantidade imensa de strings no nosso código e elas podem representar perigo. Algumas delas devem ser protegidas como a key da sua API ou o pin do seu HTTP Public Key Pinning. Aqui vão dicas para evitar e dificultar a temida engenharia reversa:
- Colocar strings sensíveis no build.gradle é mais seguro, para acessa-las é só chamar BuildConfig.nomeString, exemplo:
def APP_ID = '"androidid_123"';android {
...
productFlavors {
production {
buildConfigField 'String', 'APP_ID', APP_ID
...
} develop {
buildConfigField 'String', 'APP_ID', APP_ID
...
}
}
...
}
- Não deixe strings de forma geral nas suas classes, coloque todas no strings.xml, além de mais seguro o seu código fica mais organizado;
- Use o ProGuard;
- Armazenar no C/C++ NDK.
ProGuard
O Proguard é uma ferramenta que já vem built-in quando começamos nosso projeto no Android Studio, mas que precisa ser ativada e configurada. Segundo a documentação do Android, resumidamente o Proguard oferece:
- Reduz código detectando e removendo classes, campos, métodos e atributos não utilizados do aplicativo empacotado;
- Otimiza o bytecode;
- Remove instruções de códigos não utilizados;
- Ofusca as classes. \o/
Tudo listado acima é ótimo mas o ultimo item é a cereja do bolo e nos ajuda contra engenharia reversa do seu APK. Lembra das strings problemáticas? Elas ficarão ofuscadas. Diferente de criptografia a ofuscação utiliza uma função matemática para embaralhar os textos das classes e deixar ilegível.
Snippet para configurar o Proguard no seu arquivo build.gradle:
android {
...
buildTypes {
release {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard- android.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.Release
}
debug {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.Developer
}
}
...
}Normalmente temos várias dependências no nosso código, isso pede gerar problemas na hora de executar nosso app depois de habilitar o Proguard. Isso ocorre por que é difícil analizar, otimizar, remover e ofuscar nosso código e todas as dependências embutidas. Para corrigir erros e forçar o ProGuard a manter algum código específico, adicione uma linha -keep ao arquivo de configuração do ProGuard(proguard-rules.pro). No arquivo proguard-rules.pro é onde configuramos o que o Proguard deve ignorar, por exemplo:
-keep public class YourClassPara ajudar a você se familiarizar existem repositórios no GitHub que contém alguns snippets para configurar as dependências como https://goo.gl/AtdddW e https://goo.gl/1aOfBt
Você pode adquirir também ofuscadores privados como o DexGuard que é um irmão do Proguard. Aí é bom estudar a sua necessidade e sempre utilize um dos dois.
C/C++ Nativo NDK
Talvez colocar suas Strings no strings.xml, no seu arquivo de configuração do Gradle ou no Proguard não sejam suficientes. Segundo a documentação o NDK é "um conjunto de ferramentas que permitem usar código C e C++ em aplicativos Android. É possível usá-lo também para compilar a partir do seu próprio código-fonte ou aproveitar as bibliotecas pré-compiladas."
A estratégia aqui é simples e eficaz pois estamos movendo nossas Strings do Java para bibliotecas nativas para adicionar outras camadas de complexidade. O código Java pode ser facilmente descompilado, por outro lado, o código em C não pode ser descompilado e sim desmontado, o que é um menos trivial.
Vamos ao um passo a passo de como fazer isso nos seus projetos:
- Vamos instalar o NDK, vá em Tools > Android > SDK Manager. Depois na aba SDK Tools e instale os itens LLDB, CMake e NDK.
- Crie uma pasta “jni” em src/main:

3. Dentro da pasta "jni" vamos criar dois arquivos. O primeiro é o Android.mk :
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := keys
LOCAL_SRC_FILES := keys.c
include $(BUILD_SHARED_LIBRARY)e o segundo Apllication.mk:
APP_ABI := all4. No arquivo AndroidManifest.xml:
android {
...
externalNativeBuild {
ndkBuild {
path 'src/main/jni/Android.mk'
}
}
...
}5. Finalmente vamos criar nosso arquivo C/C++ chamado “keys.c” dentro da pasta "jni". Segue o código:
#include <jni.h>
JNIEXPORT jstring JNICALL
Java_com_ndktest_MainActivity_getKey1(JNIEnv *env, jobject instance) {
return (*env)-> NewStringUTF(env, "TmF0aXZlNWVjcmV0UEBzc3cwcmQx");
}
JNIEXPORT jstring JNICALL
Java_com_ndktest_MainActivity_getKey2(JNIEnv *env, jobject instance){
return (*env)->NewStringUTF(env, "TmF0aXZlNWVjcmV0UEBzc3cwcmQy");
}Existe um padrão de nomeação das funções. Vamos pegar o exemplo da primeira função Java_com_ndktest_MainActivity_getKey1 do código acima. Sempre a função começa com "Java", depois o nome dos pacotes que no exemplo é "com.ndktest", o nome da classe que está acessando que no caso é MainActivity e finalmente o nome de acesso "getKey1" que é de sua escolha. Depois de concluída todas as configurações o Android Studio te auxilia na criação dessas funções. Então para acessar essas Strings na MainActivity:
static {
System.loadLibrary("keys");
}public native String getNativeKey1();
public native String getNativeKey2();String key1 = new String(getNativeKey1());
String key2 = new String(getNativeKey2());
Para finalizar sincronize e construa seu projeto novamente(sync and build) . Se a sua configuração de módulo estiver apontando para o caminho exato do NDK temos tudo funcionando como esperado.
Ferramentas
Segue algumas ferramentas que ajudam no desenvolvimento e segurança do seu app:
- Caso seu Android Studio estiver na versão 2.2 ou maior: Build -> Analyze APK -> Selecione seu APK
- IBM Application Secutiry on Cloud
- APKAnalyser
- Apktool
- Dex2jar
- Android apk decompiler
Pronto, acho que isso é o suficiente, espero que tenham gostado e concluímos com a parte 2, que vamos falar de criptografia.
