Post

Intentional

Intentional (RE)

  • Categoria: médio
  • Pontos: 400
  • Data: 02/11/2023

Introdução

Intentional foi um desafio de engenharia reversa do CTF da Semana Aratu III, criado pelo purei.

Foi disponibilizado apenas um APK.

Análise Estática

Ao utilizar o jadx para descompilar o APK, é possível identificar alguns trechos de código interessantes.

1

No onCreate do ctf.boita.intentional.MainActivity vemos o atributo receiver recebendo uma instância da classe BroadcastReceiver. Esse receiver está esperando um broadcast com um parâmetro chamado decryptedResultJNI2, e após recebê-lo, mostra o seu valor na tela.

Também é possível observar uma chamada para registerReceiver que recebe uma instância da classe IntentReceiver, com a action goodjob.

Ao analisar a definição dessa classe, podemos ver que existe a declaração de um método nativo compareKeyWithPassword, e o carregamento de uma biblioteca chamada auth.

2

Um pouco abaixo, na mesma classe, identificamos outros dois métodos interessantes. O onReceive do broadcast, e o encryptPassword.

3

Basicamente, o método onReceive espera um broadcast com a action goodjob, e um parâmetro decrypt.

O valor de decrypt é “encriptado” com a chave 88tk (é feito um XOR), e é passado para o método nativo compareKeyWithPassword.

O valor retornado desse método é enviado em um broadcast que será recebido no método que vimos na MainActivity, e será mostrado na tela.

É possível enviar esse broadcast com o comando:

1
adb shell am broadcast -a goodjob --es decrypt 'teste'

Demonstração

O que nos interessa provavelmente está na definição do método nativo compareKeyWithPassword, por isso, irei abrir a biblioteca auth no Ghidra.

Para extrair a libauth.so, utilizei o apktool:

1
apktool d intentional.apk

Abri a biblioteca no Ghidra, e busquei pela função.

5

Abaixo o pseudocódigo gerado pelo Ghidra:

Spoiler
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
undefined4 Java_IntentReceiver_compareKeyWithPassword(int *param_1,undefined4 param_2,int param_3)

{
  basic_string bVar1;
  char *pcVar2;
  size_t sVar3;
  void *pvVar4;
  undefined4 *__s2;
  int iVar5;
  ulonglong *puVar6;
  uint uVar7;
  undefined4 uVar8;
  int in_GS_OFFSET;
  undefined8 *puVar9;
  __ndk1 local_54 [8];
  void *local_4c;
  undefined8 local_44;
  void *local_3c;
  basic_string local_34;
  char acStack51 [3];
  uint local_30;
  char *local_2c;
  undefined4 local_24;
  size_t local_20;
  void *local_1c;
  int local_14;
  
  local_14 = *(int *)(in_GS_OFFSET + 0x14);
  uVar8 = 0;
  if ((param_3 != 0) &&
     (pcVar2 = (char *)(**(code **)(*param_1 + 0x2a4))(param_1,param_3,0), pcVar2 != (char *)0x0)) {
    sVar3 = strlen(pcVar2);
    if (0xffffffef < sVar3) {
      if (*(int *)(in_GS_OFFSET + 0x14) == local_14) {
        uVar8 = FUN_0002df60(&local_24);
                    /* catch() { ... } // from try @ 0002de8b with catch @ 0002dece */
        if ((local_44 & 1) != 0) {
          operator.delete(local_3c);
        }
        operator.delete(pcVar2);
        if (((byte)local_34 & 1) != 0) {
          operator.delete(local_2c);
        }
        if ((local_24 & 1) != 0) {
          operator.delete(local_1c);
        }
        if (*(int *)(in_GS_OFFSET + 0x14) == local_14) {
                    /* WARNING: Subroutine does not return */
          FUN_00050690(uVar8);
        }
      }
      goto LAB_0002df53;
    }
    if (sVar3 < 0xb) {
      local_24 = local_24 & 0xffffff00 | (uint)(byte)((char)sVar3 * '\x02');
      pvVar4 = (void *)((int)&local_24 + 1);
    }
    else {
      pvVar4 = operator.new((sVar3 | 0xf) + 1);
      local_24 = (sVar3 | 0xf) + 2;
      local_20 = sVar3;
      local_1c = pvVar4;
    }
    memmove(pvVar4,pcVar2,sVar3);
    *(undefined *)((int)pvVar4 + sVar3) = 0;
                    /* try { // try from 0002dc5d to 0002dc6d has its CatchHandler @ 0002df28 */
    (**(code **)(*param_1 + 0x2a8))(param_1,param_3,pcVar2);
    while( true ) {
      sVar3 = local_20;
      if ((local_24 & 1) == 0) {
        sVar3 = local_24 >> 1 & 0x7f;
      }
      if (sVar3 == 0) break;
      pvVar4 = (void *)((int)&local_24 + 1);
      if ((local_24 & 1) != 0) {
        pvVar4 = local_1c;
      }
      if (*(char *)((int)pvVar4 + (sVar3 - 1)) != '\n') break;
                    /* try { // try from 0002dcbb to 0002dcce has its CatchHandler @ 0002df2a */
      std::__ndk1::basic_string<char,std::__ndk1::char_traits<char>,std::__ndk1::allocator<char>>::
      erase((basic_string<char,std::__ndk1::char_traits<char>,std::__ndk1::allocator<char>> *)
            &local_24,sVar3 - 1,0xffffffff);
    }
    local_44 = local_44 & 0xffff000000000000 | 0x7373617008;
                    /* try { // try from 0002dce3 to 0002dcfd has its CatchHandler @ 0002df11 */
    puVar9 = &local_44;
    xorStringWithpass(&local_34,(basic_string *)&local_24);
    if ((local_44 & 1) != 0) {
      operator.delete(local_3c);
    }
                    /* try { // try from 0002dd11 to 0002dd1c has its CatchHandler @ 0002df02 */
    __s2 = (undefined4 *)operator.new(0x10);
    pcVar2 = local_2c;
    bVar1 = local_34;
    __s2[2] = 0x17170636;
    __s2[1] = 0x29072437;
    *__s2 = 0x21435128;
    *(undefined *)(__s2 + 3) = 0;
    uVar7 = (uint)((byte)local_34 >> 1);
    if (((byte)local_34 & 1) != 0) {
      uVar7 = local_30;
    }
    if (uVar7 == 0xc) {
      if (((byte)local_34 & 1) == 0) {
        uVar7 = 0;
        do {
          if (acStack51[uVar7] != *(char *)((int)__s2 + uVar7)) goto LAB_0002dd9c;
          uVar7 = uVar7 + 1;
        } while ((byte)local_34 >> 1 != uVar7);
      }
      else {
        puVar9 = (undefined8 *)0xc;
        iVar5 = memcmp(local_2c,__s2,0xc);
        if (iVar5 != 0) goto LAB_0002dd9c;
      }
                    /* try { // try from 0002de07 to 0002de23 has its CatchHandler @ 0002def4 */
      std::__ndk1::operator+(local_54,"BOITA{",(basic_string *)&local_24);
                    /* try { // try from 0002de24 to 0002de35 has its CatchHandler @ 0002dedd */
      puVar6 = (ulonglong *)
               std::__ndk1::
               basic_string<char,std::__ndk1::char_traits<char>,std::__ndk1::allocator<char>>::
               append((basic_string<char,std::__ndk1::char_traits<char>,std::__ndk1::allocator<char> >
                       *)local_54,"}");
      local_3c = *(void **)(puVar6 + 1);
      local_44 = *puVar6;
      *(undefined4 *)((int)puVar6 + 4) = 0;
      *(undefined4 *)puVar6 = 0;
      *(undefined4 *)(puVar6 + 1) = 0;
      if (((byte)local_54[0] & 1) != 0) {
        operator.delete(local_4c);
      }
      pvVar4 = local_3c;
      if ((local_44 & 1) == 0) {
        pvVar4 = (void *)((int)&local_44 + 1);
      }
                    /* try { // try from 0002de8b to 0002de93 has its CatchHandler @ 0002dece */
      uVar8 = (**(code **)(*param_1 + 0x29c))(param_1,pvVar4);
      if ((local_44 & 1) != 0) {
        operator.delete(local_3c);
      }
    }
    else {
LAB_0002dd9c:
      if (((byte)bVar1 & 1) == 0) {
        pcVar2 = acStack51;
      }
                    /* try { // try from 0002ddb4 to 0002ddbc has its CatchHandler @ 0002def6 */
      uVar8 = (**(code **)(*param_1 + 0x29c))(param_1,pcVar2,puVar9);
    }
    operator.delete(__s2);
    if (((byte)local_34 & 1) != 0) {
      operator.delete(local_2c);
    }
    if ((local_24 & 1) != 0) {
      operator.delete(local_1c);
    }
  }
  if (*(int *)(in_GS_OFFSET + 0x14) == local_14) {
    return uVar8;
  }
LAB_0002df53:
                    /* WARNING: Subroutine does not return */
  __stack_chk_fail();
}

Nessa função existe uma chamada para a função xorStringWithPass. 19

No final da função xorStringWithPass encontrei uma linha que parece estar aplicando o XOR em uma string. 20

Renomeando algumas variáveis, fica um pouco mais fácil de entender a lógica. 21

Em uma das atribuições da variável key, vemos que o seu conteúdo é armazenado no registrador EDI. 22

Voltando para a função compareKeyWithPassword, um pouco abaixo é possível identificar o prefixo das flags utilizado no CTF. 6

Conseguimos identificar também que para essa concatenação acontecer, uma comparação de memória é feita. 7

Para entender melhor o processo, irei debugar a execução dessa função com o gdb.

Análise Dinâmica

Para debugar o app remotamente utilizando o gdb, com o app aberto, utilizo o seguinte comando:

1
adb forward tcp:1337 tcp:1337; adb shell 'gdbserver :1337 --attach `pidof ctf.boita.intentional`'

8

Agora, executo o gdb na minha máquina, e rodo o seguinte comando:

1
target remote :1337

9

Com o comando info sharedlibrary, conseguimos listar as bibliotecas carregadas pelo app.

Porém não é possível encontrar a região da memória que a libauth.so foi carregada. 10

Mais acima na saída do gdb, é possível visualizar um erro informando que a biblioteca não foi encontrada. 11

Ao analisar AndroidManifest.xml, vi que existe um atributo chamado “extractNativeLibs”.

Esse atributo indica se o instalador do pacote extrai as bibliotecas nativas do APK para o sistema de arquivos. Se definido como “false”, suas bibliotecas nativas vão ser armazenadas descompactadas no APK. Embora o APK possa ser maior, o aplicativo é carregado mais rapidamente porque as bibliotecas são carregadas diretamente pelo APK durante a execução.

12

A forma mais fácil que encontrei de resolver isso foi alterar o valor para true no AndroidManifest.xml que o apktool gerou no momento da descompilação, fazer o build do apk novamente, e reassiná-lo. 13 14

Utilizei essa ferramenta para reassinar o apk.

Após isso, desinstalei o app do dispositivo, e instalei o apk modificado.

Agora é possível visualizar a região de memória da biblioteca. 15 Com isso, conseguimos pegar o endereço base da biblioteca. 16

Função xorStringWithPass

Primeiramente, irei tentar identificar a chave utilizada na função xorStringWithPass.

Somando o endereço base com o offset da instrução que carrega a chave pra o registrador EDI (offset 0x1db42), temos um endereço válido.

Ao adicionar um breakpoint nesse endereço, e enviar um broadcast, paramos na instrução que atribui o valor ao EDI.

Damos um passo no gdb para que a instrução seja executada, utilizando o comando next ou apenas n.

Agora pdemos pegar o endereço que está no registrador, e dumpar o seu conteúdo. 23 Vemos que a chave é literalmente pass.

Função compareKeyWithPassword

Como vimos anteriormente na análise estática, existe uma comparação entre dois buffers de memória, onde supostamente um desses buffers guarda a nossa string, e o outro, a flag.

17

Os endereços são dinâmicos, então o endereço de alguns prints podem estar diferentes de outros, pois os testes foram feitos em momentos diferentes, e consequentemente, em outras instâncias.

Com um breakpoint nesse endereço, podemos enviar um broadcast.

24

Note os dois endereços passados como parâmetros.

Tendo em vista que a assinatura dessa função é:

1
int memcmp ( const void * ptr1, const void * ptr2, size_t num );

Os dois primeiros parâmetros são os endereços de memória a serem comparados, e o último parâmetro representa a quantidade de bytes a ser considerada na comparação.

Referência memcmp C++.

O primeiro buffer contém a string que enviei no broadcast (com o XOR).

O segundo buffer contém o conteúdo que é concatenado com o prefixo da flag.

25

No pseudocódigo gerado pelo Ghidra podemos identificar os bytes do segundo buffer na variável _s2.

Utilizei o Python para transformar essa sequência de bytes em um base64.

26

Depois disso, criei esse script que recebe o base64, e faz o XOR com a chave pass.

1
2
3
4
5
6
7
8
9
10
11
12
13
import base64
from sys import argv

data = argv[1]

key = "pass"

result = ""

for i, c in enumerate(base64.b64decode(data)):
    result += chr(c ^ ord(key[i % len(key)]))

print(result)

27

A flag é: BOITA{X00RGEtZFgdd}

Esta postagem está licenciada sob CC BY 4.0 pelo autor.

Trending Tags