Engenharia Reversa, Ofuscação de Código e R8

Este artigo visa explicar essas palavras aleatórias e a importância delas em aplicações android.

Dois droids (mascotes do android), um tentando encaixar um quebra-cabeça e outro tentando entender um código ofuscado.

Já ouviu falar em Engenharia Reversa?

Se você não conhece, senta aí que eu vou te explicar brevemente o que significa.

Engenharia Reversa consiste em descobrir os caminhos e princípios de uma determinada criação, a fim de saber como se dá esse processo, como obter os meios necessários que o levaram a finalização e assim até mesmo a clonar tal criação.

Este conceito se dá em vários cenários e em várias gerações: desde tempos de guerra, quando armas e veículos eram desmontados, analisados e remontados em cada um dos seus componentes, até nos dias atuais, em criações do meio computacional, tais como componentes e softwares em geral.

E no mundo Android isso não é diferente. Sim, é possível clonar aplicativos. Inclusive é bem fácil, uma vez que o processo de empacotamento, vulgo o APK, é bem maleável para obtenção do código fonte.

Vários programas podem ser usados para decompilar um APK a fim de obter sua e código fonte. Tais como JADX, ClassyShark, entre outros.

Na Imagem abaixo, é possível ver o código fonte de uma APK descompilado usando o ClassyShark:

Imagem do programa ClassyShark mostrando o código fonte de um APK

Como proteger o código fonte do app

Felizmente, no android contamos com recursos de ofuscação de código. Ofuscar um código consiste em gerar ou alterar o código fonte de um programa de tal forma que as funcionalidades da aplicação não sejam afetadas, mas que a leitura do código seja incompreensível. Logo, uma pessoa mal intencionada, mesmo que tenha conseguido descompactar o seu APK, não conseguirá compreender a lógica e estrutura, o que dificultaria muito o processo de engenharia reversa.

No Android, a partir do Plug-in do Gradle 3.4.0, temos um compilador que converte o bytecode Java do projeto para o formato DEX.

Aliás, você sabe o que é o formato DEX?

DEX, é formato de arquivo resultante da conversão do bytecode Java no processo de compilação de um APK, inclusive ele tem uma limitação na quantidade de métodos existentes na aplicação, sendo possível um total de 65.536 métodos. Provavelmente se você já trabalhou em uma aplicação de médio ou grande porte, em algum momento foi necessário ativar a opção de multidex quando esse limite foi ultrapassado.

O compilador responsável por esses processos é o R8, e com ele podemos realizar a redução, ofuscação e otimização do código.

Tá, mas como fazer isso?

Primeiramente temos que ativar o R8 em nosso arquivo build.gradle no nível app

Obs.: Devido ao aumento no tempo de compilação pela ativação da redução, ofuscação e otimização de código, é recomendado ativá-la no build de versões releases, até mesmo para ajustar as exceções de compilação antes de fazer o lançamento do app.

android {
  buildTypes {
      
      debug {

      }

      release {
          // Permite redução, ofuscação e otimização de código apenas 
          // para o tipo de compilação de lançamento do seu projeto.
          minifyEnabled true

          // Ativa a redução de recursos, que é realizada 
          // pelo Plug-in do Android para Gradle.
          shrinkResources true

          // Inclui os arquivos de regras ProGuard padrão 
          // que são empacotados com o plugin Android Gradle. 
          proguardFiles getDefaultProguardFile(
                  'proguard-android-optimize.txt'),
                  'proguard-rules.pro'
      }
  }
  ...
}
Além do código fonte, podemos reduzir recursos (imagens, fontes, arquivos, dentre outros) que não estão sendo utilizados pela aplicação em tempo de execução. O atributo de ativação é o shrinkResources, como mostrado no trecho de código acima.

Após a ativação

Após a ativação do R8 no gradle, no momento de compilação da versão release, será realizado o processo de minificação e ofuscação do seu código. Ou seja: basta apenas fazer a ativação do mesmo que todo o processo é configurado para ser executado em tempo de compilação.

Mas nem tudo são flores. Durante o processo de desenvolvimento de um app, usamos diversas bibliotecas. Criamos algumas também, dependendo da necessidade do projeto. Nisso, temos algumas bibliotecas (principalmente as que geram reflexão de código) que não podem ser ofuscadas. Logo, temos que criar regras para que o R8 possa desconsiderar essas bibliotecas e/ou trechos de códigos que devemos manter sem nenhuma alteração.

Como criar essas regras?

Anteriormente na versão do Plug-in Gradle 3.4.0, usavamos o Proguard para realizar esse processo do R8. Devido ao suporte aos arquivos de configuração de regras do Proguard, ele se manteve no R8. Então, ao usar o R8, podemos usar os arquivos de configuração de regras do Proguard normalmente.

Quando criamos um app ou um módulo pelo Android Studio, são criados alguns arquivos, dentre eles temos o proguard-rules.pro. É nele que passamos as regras de exceção para o R8 as regras -keep. Além destes arquivos, podemos tratar essas regras via anotações @Keep*.

Um exemplo clássico de biblioteca, cujos métodos precisam ser ignorados pelo proguard, é o Retrofit (biblioteca utilizada para chamadas de rede no android).

Então iremos adicionar regras keep ao proguard-rules.pro, com os seguintes comandos.

-keepclasseswithmembers class * {
  @retrofit2.http.* <methods>;
}
-keepclassmembernames interface * {
  @retrofit2.http.* <methods>;
}

As regras adicionadas acima informam ao R8 que classes e interfaces que usam as anotações do Retrofit em seus métodos, essas anotações não devem ser ofuscadas, pois, embora não estejam sendo usadas durante o processo de compilação, elas são necessárias durante o processo de execução, por isso devemos mantê-las.

Agora um exemplo em que é utilizada a anotação @Keep diretamente na classe que desejamos manter sem alterações.

@Keep
data class PeopleDetailView(
  val biography: String,
  val gender: Int,
  val id: Int,
  val name: String,
  val placeOfBirth: String? = null,
  val profilePath: String
) : Serializable

Aqui a classe PeopleDetailView não é ofuscada pelo R8, uma vez que ela foi anotada pela anotação @Keep.

Um outro exemplo de regras seria o Coroutines (biblioteca Kotlin para trabalhar com multi threads).

-keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {}
-keepnames class kotlinx.coroutines.CoroutineExceptionHandler {}
-keepnames class kotlinx.coroutines.android.AndroidExceptionPreHandler {}
-keepnames class kotlinx.coroutines.android.AndroidDispatcherFactory {}

Neste caso, todas as classes citadas pelo keepnames precisam ser ignoradas pelo R8. Desta forma, a MainDispatcherFactory, CoroutineExceptionHandler, AndroidExceptionPreHandler e AndroidDispatcherFactory não serão ofuscadas.

Utilizando essas regras, conseguimos manter um alinhamento do que pode ser ofuscado e do que é necessário manter sem nenhuma alteração. Como dito acima, esse meio visa dificultar que o processo de Engenharia Reversa venha a acontecer em seu app, além de também otimizar o tamanho final do app.

Após todo esse processo de ativação e regras temos o seguinte resultado:

Um código no qual ficam incompreensíveis, as classes, métodos, variáveis e tudo o que leva a ter um entendimento lógico e estrutural por alguém mal intencionado que venha a descompactar o seu app

Imagem do programa ClassyShark mostrando o código fonte de um APK

Além disso, devido a redução e minificação do código, o tamanho final do apk é menor. O que é maravilhoso para o usuário final, que ainda tem um desempenho melhor e um consumo menor na memória do dispositivo que ele instala.

E o Analyze APK do Android Studio não me deixa mentir:

Imagem mostrando a diferença de tamanho entre dois APK's

Nele, podemos ver que ao ativar o R8 tivemos uma redução de aproximadamente 55% no tamanho do APK (de 7,3 MB para 3,3 MB).

Por hoje é isso

E com isso eu finalizo essa publicação, incentivando-os a se preocuparem com a distribuição de seus apps, com o tamanho final, com o limite de referências DEX, com a ofuscação do seu código fonte e em não deixar dados sensíveis junto ao código fonte do app. Já aviso também que no R8 às vezes dá um trabalhinho configurar essas regras e entender os erros quando as mesmas não são aplicadas. Mas nada que uma lida na documentação e algumas buscas não resolvam.

Deixo aqui abaixo as referências utilizadas para escrever essa publicação. No mais, agradeço pela leitura.

Reduzir, ofuscar e otimizar o App (Documentação Oficial Android)

Documentação Keep options

Sobre Engenharia Reversa

Quer ver mais publicações minhas? Acesse o perfil abaixo e veja todas as minhas publicações!