Uma introdução ao Smali
Smali é um tipo de linguagem assembly para a máquina virtual Dalvik, que é usada por dispositivos Android. Ele é usado para modificar e fazer engenharia reversa de aplicativos Android e permite que os desenvolvedores façam alterações no bytecode de um aplicativo.
Uma das principais vantagens do Smali é que ele permite um controle mais refinado sobre o comportamento de um aplicativo do que o Java, pois opera no nível do bytecode. Isso o torna útil para tarefas como conectar-se a métodos específicos ou modificar o comportamento de um aplicativo sem modificar seu código-fonte Java.
Neste blog, veremos três casos de teste diferentes que incluem a descompilação dos APKs e a conversão deles em código Smali, que pode ser editado, modificado e recompilado de volta nos APKs. Então, vamos começar.
Pré-requisito
Este blog tem alguns pré-requisitos de que se deve estar familiarizado com os fundamentos de java e conhecimento básico de aplicativos Android. Se você não estiver familiarizado com eles, primeiro leia alguns blogs para obter alguns insights.
Introdução
smali/baksmali é um montador/desmontador para o formato dex usado pelo dalvik, a implementação Java VM do Android. Quando criamos um aplicativo Android, a estrutura da pasta apk se parece com a seguinte:
Como você pode ver, um dos arquivos é classes.dex, que é basicamente um bytecode dalvik binário que a plataforma Android entende. Suponha que, se quisermos ler/modificar esse arquivo binário, precisamos convertê-lo em um formato mais legível por humanos. É quando o smali entra em jogo. As classes são então convertidas em código Smali, que pode ser editado e modificado. O código modificado pode ser recompilado em um arquivo APK e instalado em um dispositivo Android.
Por exemplo, digamos que você tenha um código Java que faz algo como
int x = 42;
Supondo que esta seja a primeira variável, o código dex para o método provavelmente conterá a sequência hexadecimal
13 00 2A 00
Se você executar baksmali nele, obterá um arquivo de texto contendo a linha
const/16 v0, 42
O que é obviamente muito mais legível do que o código binário.
Então, vamos começar aprendendo o básico do smali primeiro. Em seguida, criaremos um aplicativo Android e modificaremos algumas variáveis. Depois disso, reconstruiremos o mod apk.
Diretiva de Registros e Locais
- Em Smali, os registradores são usados para armazenar qualquer tipo de valor e são sempre de 32 bits. Para manter valores de 64 bits (longo/duplo), dois registradores são usados.
- Cada método/função tem uma declaração do número de registros usados nesse método/função.
- A diretiva .registers especifica o número total de registros usados no método, enquanto a diretiva .locals alternativa especifica o número de registros sem parâmetros usados no método.
- Por exemplo, se houver um método não estático com dois argumentos, haverá três registros de parâmetros. Se o número total de registros usados nesse método for 5, a diretiva .locals será 2, pois 3 são registros de parâmetros (this, arg1, arg2).
Por exemplo, observe o seguinte código java:
Convertendo este código java em Smali Bytecode usando a diretiva locals:
Não se preocupe se você não entender o código, nós o explicaremos em uma seção posterior. Por enquanto, você pode descobrir que o número de diretivas locais usadas é 1. Portanto, podemos concluir que o método addIntger e addInteger2 têm um registro de não parâmetro.
Agora, Smali Bytecode usando a diretiva registers:
No código Smali acima, no qual usamos a diretiva registers, você pode descobrir que o método addInteger tem quatro registradores, enquanto o método addInteger2 tem três registradores. Isso ocorre porque o método addInteger não é estático, enquanto o método addInteger2 é estático. Como na classe não estática, precisamos criar uma instância da classe primeiro e, em seguida, usando essa palavra-chave, chamamos o método.
Portanto, o número total de registradores usados no método addInteger é 4, então a diretiva .locals será 1, pois 3 são registradores de parâmetros (this, arg1, arg2). Da mesma forma, o número total de registros usados no método addInteger2 é 3, que inclui 1 diretiva locals e 2 registros de parâmetros (arg1, arg2).
Então, agora abordamos como as variáveis são definidas no código smali. Agora vamos dar uma olhada nos tipos de dados Smali. A tabela abaixo mostra como os tipos de dados de Java são mapeados em Smali:
Data Type | Smali Notation |
byte | B |
short | S |
int | I |
long | J |
float | F |
double | D |
boolean | Z |
char | C |
class or interface | Lclassname; |
Uma classe HelloWorld
Agora seguiremos a mesma tradição de aprender smali que qualquer desenvolvedor faz para aprender qualquer outro idioma. Veremos um famoso exemplo HelloWorld em Smali. Também anexei o código java para o método HelloWorld, pois ajudará a entender melhor o código. Mas durante a demonstração prática, não tivemos acesso ao código java.
Código Java:
Código Smali:
A parte do código Smali acima tem um método chamado “HelloWorld” que não recebe argumentos e retorna nulo.
A primeira linha “.method public HelloWorld()V” declara o método “HelloWorld” como público e não usa argumentos e retorna nulo.
A próxima linha “.locals 2” declara que esse método usa 2 variáveis locais. As variáveis locais são usadas para manter temporariamente os valores dentro de um método.
A próxima linha “.line 6” é usada para indicar o número da linha do código-fonte, neste caso, a linha 6.
A próxima linha “sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;” é usada para recuperar o valor do campo “out” da classe “java.lang.System”, que é uma instância da classe “java.io/PrintStream”, e armazená-lo no registro v0.
A próxima linha “const-string v1, “Hello World!”” carrega a string “Hello World!” no registro v1.
A próxima linha “invoke-virtual {v0, v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V” invoca o método “println” da classe “java.io.PrintStream”, passando o valor de v1 como argumento. O método “println” grava a string passada na saída padrão.
A próxima linha “.line 7” é usada para indicar o número da linha do código-fonte, neste caso, a linha 7.
A última linha “return-void” é usada para retornar do método e indicar que o método retorna void.
Dalvik Opcodes
Você pode estar se perguntando de onde vem o sget-object ou const-string no smali. Estas são basicamente instruções que são usadas para manipular o valor nos registradores. A instrução “sget-object” é usada para recuperar o valor de um campo de objeto estático e armazená-lo em um registrador, enquanto a instrução “const-string” é usada para carregar um valor de string constante em um registrador. O “s” em “sget” significa “estático”, indicando que o campo é um campo estático.
Trabalhar com a Smali requer um bom entendimento da máquina virtual Dalvik e seu conjunto de instruções, bem como experiência com programação em linguagem assembly.
Você pode encontrar dalvik-opcodes aqui que o ajudarão a começar com o que uma instrução específica faz.
Demonstração prática
Agora veremos três casos de teste em que tentaremos modificar o código smali do aplicativo e, em seguida, corrigi-lo novamente. Comecemos, pois, sem mais delongas.
Caso de teste 1: Modificar o valor de uma variável
Neste caso de teste, tentaremos modificar alguma variável para obter alguma confiança de que smali não é nada difícil de ler. Para isso, criamos um aplicativo Android chamado Smali Application 1 que está fazendo uma operação de adição em dois números:
Então, como o aplicativo está nos desafiando a provar que está errado.
Primeiro, vamos puxar o arquivo apk do aplicativo Smali 1 instalado do dispositivo, descompilá-lo usando apktool e depois analisar o código smali.
Eu escrevi um pequeno script bash que puxa o arquivo apk base e descompila o aplicativo:
-
puxar adb \
-
/data/app/com.app.smali1-c8JScvSVY2hu8a-65GbOIg==/base.apk ./smali1.apk
-
apktool d -r -f smali1.apk
A execução do script bash acima criará um diretório chamado smali1 do Smali Application 1. Em primeiro lugar, procuraremos por MainActivity neste diretório, pois essa atividade geralmente é o ponto de entrada para qualquer aplicativo. Portanto, é melhor começar a examinar esse arquivo primeiro.
Carregue o arquivo smali1/smali_classes3/com/app/smali1/MainActivity.smali em qualquer editor de sua escolha e tente ler o código smali.
Você encontrará uma função chamada addInteger. Nessa função, há uma variável local usada, ou seja, v0. A partir da linha 18, podemos concluir que são necessários dois argumentos do tipo de dados Integer e retornar um integer. O .param, p1 e p2 são os parâmetros que recebem entrada como argumentos na função. E na linha 24, ele tem um add-int de opcode que está adicionando p1 e p2 e armazenando o resultado em v0. E, finalmente, v0 retorna à função de chamada de addInteger.
Rolando abaixo até MainActivity.smali, você encontrará um método chamado onCreate que está chamando o método addInteger e fornecendo argumentos de v0 e v1. O valor de v0 e v1 é definido como 0x14 que é 20 em decimal.
Agora vamos modificar essa variável simplesmente para outro valor hexadecimal, digamos 0x1f, corrigir o aplicativo e instalá-lo no dispositivo. Estou usando a extensão APKlab no VSCode para corrigir e assinar o aplicativo, mas você pode usar da maneira que preferir.
Agora vá para a pasta dist e instale o arquivo apk no dispositivo.
Caso de teste 2: Alterar o valor retornado de uma função
No caso de teste anterior, modificamos com sucesso o valor de uma variável. Neste caso de teste, vamos alterar o valor de retorno de uma função. Para a demonstração deste caso de teste, criamos outro aplicativo chamado Smali Application2.
Ele também está executando a operação de adição em dois números. Vamos descompilar o apk e analisar o código smali para entender o aplicativo. O MainActivity.smali tem uma função chamada add_nums, conforme mostrado abaixo
Como o objetivo deste caso de teste é modificar o valor de retorno da função add_nums, modificaremos a própria função. Em vez de adição, realizaremos a subtração adicionando uma nova variável local v1 na função. Assim, a contagem de diretivas locais aumentará para 2. Aqui está a aparência do código modificado:
Vamos corrigir a reconstrução do aplicativo e instalá-lo no dispositivo.
E modificamos com sucesso este aplicativo. Você também pode modificar outras variáveis, como pode tentar modificar esta mensagem de alerta.
Caso de teste 3: Escreva uma nova função maligna e chame essa função maligna em vez de uma função legítima em smali
Até agora, modificamos com sucesso a variável, inserimos um novo código smali dentro de uma função. Neste caso de teste, vamos inserir uma função maligna em smali e chamá-la de função maligna em vez de uma função legítima. Para a demonstração deste caso de teste, criamos outro aplicativo chamado Smali Application3,
Como você pode ver, está apenas exibindo uma equipe Hello … string na página inicial com um alerta de “Estou dizendo olá para a equipe”.
Vamos analisar o aplicativo descompilando-o usando apktool. O MainActivity.smali tem um método chamado say_hello conforme mostrado abaixo
E esse método say_hello é chamado dentro do método onCreate do arquivo MainActivity.smali.
Primeiro, inseriremos um novo evil_method dentro do arquivo MainActivity.smali e chamaremos esse evil_method em vez de say_hello método. Veja como será a evil_method:
Está simplesmente retornando uma sequência de “Cheers Team!!”. Vamos substituir o método say_hello() no método onCreate por evil_method e reconstruir o aplicativo.
Portanto, inserimos com sucesso um método dentro do aplicativo, modificando o código smali.
Conclusão
É isso para este blog e para o próximo blog, veremos alguns cenários reais de como um invasor pode modificar o aplicativo Smali para obter acesso a alguns dados confidenciais ou ignorar algumas verificações.