MASTG-TECH-0077: Análise de Código Native Desmontado
A análise de código nativo desmontado requer uma boa compreensão das convenções de chamada e instruções utilizadas pela plataforma subjacente. Nesta seção, examinaremos a desmontagem ARM64 do código nativo. Um bom ponto de partida para aprender sobre a arquitetura ARM está disponível em Introduction to ARM Assembly Basics pelos Tutoriais da Azeria Labs. Este é um resumo rápido dos aspectos que utilizaremos nesta seção:
- No ARM64, um registro possui 64 bits e é referido como Xn, onde n é um número de 0 a 31. Se os 32 bits inferiores (LSB) do registro forem utilizados, ele é referido como Wn.
- Os parâmetros de entrada de uma função são passados nos registros X0 a X7.
- O valor de retorno da função é passado através do registro X0.
- As instruções de carga (LDR) e armazenamento (STR) são usadas para ler ou escrever na memória a partir/para um registro.
- B, BL, BLX são instruções de desvio (branch) usadas para chamar uma função.
Como também mencionado anteriormente, o código Objective-C é compilado para código binário nativo, mas analisar código nativo C/C++ pode ser mais desafiador. No caso do Objective-C, existem vários símbolos (especialmente nomes de funções) presentes, o que facilita a compreensão do código. Na seção anterior, aprendemos que a presença de nomes de funções como setText, isEqualStrings pode nos ajudar a entender rapidamente a semântica do código. No caso de código nativo C/C++, se todos os binários estiverem stripped, pode haver muito poucos ou nenhum símbolo presente para nos auxiliar na análise.
Descompiladores podem nos ajudar a analisar código nativo, mas devem ser usados com cautela. Descompiladores modernos são muito sofisticados e, entre as muitas técnicas que utilizam para descompilar código, algumas são baseadas em heurísticas. Técnicas baseadas em heurísticas nem sempre fornecem resultados corretos, um exemplo sendo a determinação do número de parâmetros de entrada para uma determinada função nativa. Ter conhecimento para analisar código desmontado, auxiliado por descompiladores, pode tornar a análise de código nativo menos propensa a erros.
Analisaremos a função nativa identificada na função viewDidLoad na seção anterior. A função está localizada no offset 0x1000080d4. O valor de retorno desta função é utilizado na chamada da função setText para o label. Este texto é usado para comparar com a entrada do usuário. Assim, podemos ter certeza de que esta função retornará uma string ou equivalente.

A primeira coisa que podemos observar na desmontagem da função é que não há entrada para a função. Os registros X0 a X7 não são lidos em toda a função. Além disso, há múltiplas chamadas para outras funções, como as em 0x100008158, 0x10000dbf0, etc.
As instruções correspondentes a uma dessas chamadas de função podem ser vistas abaixo. A instrução de desvio bl é usada para chamar a função em 0x100008158.
1000080f0 1a 00 00 94 bl FUN_100008158
1000080f4 60 02 00 39 strb w0,[x19]=>DAT_10000dbf0
O valor de retorno da função (encontrado em W0) é armazenado no endereço no registro X19 (strb armazena um byte no endereço no registro). Podemos observar o mesmo padrão para outras chamadas de função: o valor retornado é armazenado no registro X19 e cada vez o offset é um a mais que a chamada de função anterior. Este comportamento pode estar associado ao preenchimento de cada índice de um array de strings por vez. Cada valor de retorno é escrito em um índice deste array de strings. Existem 11 chamadas desse tipo e, com a evidência atual, podemos fazer uma suposição inteligente de que o comprimento do flag oculto é 11. Ao final da desmontagem, a função retorna com o endereço para este array de strings.
100008148 e0 03 13 aa mov x0=>DAT_10000dbf0,x19
Para determinar o valor do flag oculto, precisamos saber o valor de retorno de cada uma das chamadas de função subsequentes identificadas acima. Ao analisar a função 0x100006fb4, podemos observar que esta função é muito maior e mais complexa que a anterior que analisamos. Grafos de função podem ser muito úteis ao analisar funções complexas, pois ajudam a entender melhor o fluxo de controle da função. Grafos de função podem ser obtidos no Ghidra clicando no ícone Display function graph no submenu.

Analisar manualmente todas as funções nativas completamente será demorado e pode não ser a abordagem mais sábia. Nesse cenário, é altamente recomendável usar uma abordagem de análise dinâmica (consulte Análise Dinâmica no iOS). Por exemplo, usando técnicas como hooking ou simplesmente depurando a aplicação, podemos determinar facilmente os valores retornados. Normalmente, é uma boa ideia usar uma abordagem de análise dinâmica e depois recorrer à análise manual das funções em um ciclo de feedback. Dessa forma, você pode se beneficiar de ambas as abordagens ao mesmo tempo, economizando tempo e reduzindo esforço.