Pplware

Conheça melhor a segurança das aplicações Android

Ao contrário do que se possa pensar, a segurança é um mito. Não existem soluções totalmente seguras. Basta olhar para os últimos anos, com o ataque à infra-estrutura da Sony, o ataque às centrifugadoras de urânio iranianas ou ao mediático conflito entre a Apple e o FBI, que trouxe para a estrelato a empresa israelita Cellebrite no dito “impossível” desbloqueio do iPhone.

Desta forma vamos abordar mais a fundo o tema da segurança nas aplicações para Android.

Tudo se resume a uma questão de empenho, tanto em recursos como em tempo, para conseguir quebrar e entrar em qualquer sistema. Exemplos disso, por excelência, são os cofres-fortes. Explicando de forma resumida, a avaliação destes é feita com base no tempo de que um “ladrão” profissional necessita para o arrombar usando processos mecânicos e eléctricos, sem destruir o conteúdo. Um cofre da classe “TL-15” tem que, de acordo com a norma aguentar 15 minutos de tortura, com instrumentos mecânicos e/ou eléctricos, antes de ser aberto.

Escolha de um bom sistema de segurança

O segredo na escolha de um bom sistema de segurança, tanto no mundo real como no mundo digital, é encontrar uma solução que seja mais difícil de penetrar e eleve o risco de se ser apanhado, de forma a que o nosso ladrão não veja uma oportunidade, mas sim uma fonte de aborrecimentos. Precisamos de mostrar o seu atrevimento lhe vai sair caro.

Dito isto, falando agora no mundo digital e mais precisamente em aplicações Android, há medidas simples que podemos tomar para dificultar a vida aos piratas. E o que é mesmo uma aplicação Android? Não é mais do que um ficheiro comprimido, do formato ZIP, com a extensão apk (Android Package), que é lido pelo instalador do Android, responsável por definir espaço, criar pastas e copiar os ficheiros nos devidos destinos para depois ser executado pelo sistema operativo quando for solicitado pelo utilizador.

Dentro de um Android Package típico, temos a seguinte estrutura:

Ilustração 1 – Estrutura de um ficheiro APK

A pasta  assets contem todos os ficheiros anexos, como imagens, ícones ou ficheiros de ajuda. Na directoria META-INF, ficam as assinaturas dos ficheiros contidos no apk e, no directório res, temos os elementos gráficos, ícones e splash, usados pelo Launcher do Android.

As definições e os privilégios necessários para correr a aplicação ficam gravados no AndroidManifest.xml, e o mapeamento dos elementos gráficos usado no código ficam no resources.arsc. Por fim, e não menos importante, temos o classes.dex (Dalvik Executable) com o código compilado.

É aqui que o assunto se torna interessante. Uma aplicação nativa Android terá todo o código compilado no classes.dex e as imagens, entre outros recursos utilizados, localizadas na pasta assets.

Uma das características do Android é não usar a clássica máquina virtual JAVA, mas sim uma nova máquina virtual intitulada Dalvik, criada de raiz, pensada e optimizada para aumentar a velocidade e reduzir tamanhos. Não é usado o bytecode Java. Em vez disso, foi introduzido o DEX. Por exemplo, numa aplicação JAVA, cada classe é compilada para um ficheiro class individual, enquanto que, no DEX, todas as classes ficam juntas num único ficheiro. Outra vantagem do DEX é o tamanho, geralmente mais de 50% inferior ao JAR descomprimido.

Apesar destas diferenças, é possível converter o DEX em JAR. Temos várias aplicações freeware disponíveis para o efeito. A mais conhecida é o dex2jar, disponível no Sourceforge, entre outros repositórios. Fazemos o download, descomprimimos o pacote e executamos a aplicação, apontando para o nosso apk.

Ilustração 2 – Conversão de DEX para JAR

Descomprimimos o ficheiro JAR, também ele no formato ZIP, e usamos um de muitos descompiladores disponíveis para o JAVA para converter o ficheiro class. Pessoalmente gosto de usar o  JD-GUI Java Decompiler, de Emmanuel Dupuy .

Ilustração 3 – Resultado do JD-GUI Java Decompiler

Pegando numa aplicação híbrida, baseada no PhoneGap, o processo é muito mais simples. Esta  terá todo o seu código, html  e javascript, em texto livre, distribuído na pasta assets, deixando os plugins do PhoneGap no classes.dex.

Ilustração 4 – Estrutura de uma aplicação PhoneGap

Com isto tudo, podemos alterar o código e reverter o processo. Compilamos, criamos o JAR, convertemos para DEX, com ajuda do d2j-jar2dex. Precisamos de voltar a criar os hash de cada ficheiro, com base no jarsigner, e compactar tudo num novo ficheiro apk.

Como obter um apk? Para quem tiver o adb instalado, podemos ligar ao dispositivo e puxar diretamente o apk.

adb connect xxx.xxx.xxx.xxx:5555

adb shell pm list packages

adb pull /data/app/exemplo.app.pt.apk

Para quem tiver pressa, podemos usar aplicações disponíveis do googlePlay, como o APKoptic   ou o APK Extractor (Backup Apk) e gravar o ficheiro num cartão sd para depois transferir para o nosso pc.

Como saber o id do apk que pretendemos analisar? Ao navegar, no googlePlay, até à página de apresentação do produto, conseguimos ler, no url do browser, o seu id.

https://play.google.com/store/apps/details?id= exemplo.app.pt.apk

Nos outros ecossistemas, iOS, Blackberry  e Windows, o processo é semelhante, apesar do nível de dificuldade ser maior. De facto a Apple dedica muita energia à segurança. As aplicações são digitalmente assinadas na Apple Store para serem instaladas num dispositivo específico. O controlo é muito maior, e isso incluí os programadores.  Mas, há sempre um mas, ao fazer jailbreak de um iPhone, deitamos por terra esta barreira.

Será possível proteger as nossas aplicações ou temos que aceitar impavidamente esta situação? A resposta é nim.  Não é possível ter uma garantia total. No entanto,  é possível prevenir. É possível tornar a tarefa de tal modo hercúlea que faça desistir qualquer aprendiz a hacker.

Primeiro, a segurança deve ser pensada desde o início. Devemos conhecer os mecanismos de defesa que a nossa plataforma de eleição oferece e desenhar a nossa aplicação nesse sentido. Não se pode começar a codificar e esperar que a ofuscação do código faça milagres. Devemos assumir que o nosso código vai ser visto. Logo, ofuscar o código é essencial. Não resolve o problema, mas faz parte da solução. Para aplicações nativas, no Android Studio, basta configurar e activar o Proguard. Para aplicações híbridas, podemos usar o Google Closure Compiler com o nível de compilação ADVANCED_OPTIMIZATIONS.

Mesmo ofuscado, temos outro problema. Strings e valores de configuração, como por exemplo endereços de acesso e chaves de encriptação, estão disponíveis aos olhares mais indiscretos. O seu lugar, definitivamente, não é no código. Devem estar fora, idealmente criados na instalação, no “Internal Storage”.

O “Internal Storage” é um espaço criado pelo Android, de acesso exclusivo da aplicação, não sendo acessível aos outros processos em actividade, nem mesmo ao utilizador. Mesmo assim, num dispositivo “rooteado”, tal não é garantia de segurança. É possível corromper o sistema operativo e aceder ao “Internal Storage” . Aí precisamos de encriptar os dados. Hoje em dia, os sistemas em chip (SoC) possuem um Trusted Execution Environment (TEE). Trata-se, com respectivas diferenças de cada implementação, de cada fabricante, de um processador virtual com capacidade para criar e gerir chaves privadas, totalmente inacessível o resto dos processos em curso. Aliás é geralmente esse módulo que gere as chaves usadas pelas placas wifi desses mesmos SoC.

O uso não podia ser mais simples. Criamos uma chave, que fica armazenada internamente e segura dentro do chip.

try {

                //Valida se certificado existente. Se não… criar

                string lv_strAlias = ” Internal Storage Cert”;

                if (!keyStore.containsAlias(lv_strAlias)) {

                               Calendar lv_dtBegin = Calendar.getInstance();

                               Calendar lv_dtEnd = Calendar.getInstance();

                               lv_dtEnd.add(Calendar.YEAR, 1);

                               KeyPairGeneratorSpec lv_objSpec = new KeyPairGeneratorSpec.Builder(this)

                                                               .setAlias(lv_strAlias)

                                                               .setSubject(new X500Principal(“CN=Test “))

                                                               .setSerialNumber(BigInteger.ONE)

                                                               .setStartDate(lv_dtBegin.getTime())

                                                               .setEndDate(lv_dtEnd.getTime())

                                                               .build();

                               KeyPairGenerator lv_objGenerator = KeyPairGenerator.getInstance(“RSA”, “AndroidKeyStore”);

                               lv_objGenerator.initialize(lv_objSpec);

 

                               KeyPair lv_objKeyPair = lv_objGenerator.generateKeyPair();

                }

} catch (Exception e) {

                Log.e(TAG, Log.getStackTraceString(e));

}

Podemos criar as chaves de que precisamos como, por exemplo, uma para o Internal Storage, onde codificamos tudo e garantimos que os dados só serão usados nesse equipamento. Podemos ter outro certificado para as comunicações com o servidor XPTO, em que fornecemos a chave pública ao servidor.

String lv_strInitialText = “Exemplo de informação”;

String lv_strEncryptedText = “”;

                                              

try {

                KeyStore.PrivateKeyEntry lv_objPrivateKeyEntry = (KeyStore.PrivateKeyEntry)keyStore.getEntry(lv_strAlias, null);

                RSAPublicKey lv_objPublicKey = (RSAPublicKey) lv_objPrivateKeyEntry.getCertificate().getPublicKey();

 

                Cipher lv_objInput = Cipher.getInstance(“RSA/ECB/PKCS1Padding”, “AndroidOpenSSL”);

                lv_objInput.init(Cipher.ENCRYPT_MODE, lv_objPublicKey);

 

                ByteArrayOutputStream lv_objOutputStream = new ByteArrayOutputStream();

                CipherOutputStream lv_objCipherOutputStream = new CipherOutputStream(lv_objOutputStream, lv_objInput);

                lv_objCipherOutputStream.write(lv_strInitialText.getBytes(“UTF-8”));

                lv_objCipherOutputStream.close();

 

                byte [] lv_lstVals = lv_objOutputStream.toByteArray();

                lv_strEncryptedText.setText(Base64.encodeToString(lv_lstVals, Base64.DEFAULT));

} catch (Exception e) {

                Log.e(TAG, Log.getStackTraceString(e));

}

Comprometer uma KeyStore é possível, mas entramos numa outra dimensão no mundo da segurança. Cada fabricante terá a sua implementação, os seus mecanismos de defesa e as suas falhas. De tempos a tempos surgem notícias da falha A ou B. Temos que nos manter actualizados e ver as implicações das mesmas nas nossas aplicações.

O uso da keystore é uma boa solução, mas com restrições. Por exemplo, o keystore fica indisponível quando o equipamento se encontra bloqueado e requer autenticação do utilizador, por código ou por impressão digital – pequeno detalhe que precisamos de relembrar quando desenhamos um serviço. No entanto, combinando os vários pontos que vimos até agora, conseguimos ter uma segurança aceitável, que levará muito mais do que 15 minutos a piratear.

Exit mobile version